Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/guides/configure-an-agent.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@ agents:
| `allow` | 自动允许,Agent 使用时无需人工确认 |
| `ask` | 每次使用前需要人工确认 |

`tools.default_permission` 控制所有已启用内置工具的默认权限,省略时为 `allow`。
`permissions` 只覆盖其中明确列出的工具;工具名匹配不区分大小写和分隔符,
例如 `WebSearch` 与 `web_search` 等价。引用未在 `tools.builtin` 中启用的工具会在校验时报错。

```yaml
tools:
builtin: [Read, Bash, WebSearch]
default_permission: allow
permissions:
bash: ask
```

## Drift Detection 与 Refresh

`plan` 和 `apply` 默认会先刷新远端状态。对于支持完整 drift detection 的资源,OpenAgentPack 会读取远端可比较内容;如果有人绕过 OpenAgentPack 在控制台或 API 中修改了资源,`plan` 会显示 `Remote drift detected`。
Expand Down Expand Up @@ -517,7 +529,9 @@ Deployment 运行时可挂载的资源:
|---------|------|------|
| `file` | `file_id` / `source` / `mount_path` | 文件资源;`file_id` 引用已上传文件;`source` 为本地路径,Qoder 和 Claude 在 `apply` 时上传 |
| `memory_store` | `memory_store` / `access` / `instructions` | 引用 Memory Store;`access` 为 `read_write`(默认)或 `read_only` |
| `github_repository` | `url` / `checkout` / `mount_path` / `authorization_token` | 检出 Git 仓库(Qoder 和 Claude) |
| `github_repository` | `url` / `checkout` / `mount_path` / `authorization_token` | 检出 Git 仓库(Qoder 和 Claude)。Qoder 的挂载路径必须以 `/data/` 开头;省略时自动传递 `/data/workspace/<仓库名>` |

各 Provider 的挂载根目录是固定约束:Qoder 为 `/data`,Claude 为 `/workspace`,百炼和 Ark 为 `/mnt`。上传文件使用相对路径时,OpenAgentPack 会在目标 Provider 的根目录下解析;显式绝对路径必须已经位于对应根目录,并保持原样传递,前缀不匹配时直接报错,不做静默改写。GitHub Session 资源省略 `mount_path` 时,Qoder 自动使用 `/data/workspace/<仓库名>`,Claude 自动使用 `/workspace/<仓库名>`。

### 运行与查看

Expand Down
28 changes: 27 additions & 1 deletion docs/guides/run-sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,33 @@ agents session delete <session-id>

## What a session binds

A Managed Session binds an Agent + environment + vaults + memory stores + files. A Qoder Forward Session binds a Template + Identity; the Template already owns its environment, tunnel, vault, and MCP configuration. `session create` lets callers override the relevant runtime bindings.
A Managed Session binds an Agent + environment + vaults + memory stores + files + declared resources. A Qoder Forward Session binds a Template + Identity; the Template already owns its environment, tunnel, vault, and MCP configuration. `session create` lets callers override the relevant runtime bindings.

To mount a private GitHub repository in every managed Session, declare a provider-neutral resource on the Agent. Qoder and Claude receive their respective API wire shapes from the same configuration:

```yaml
agents:
assistant:
# model, instructions, environment, ...
resources:
- type: github_repository
url: ${GITHUB_REPOSITORY_URL}
authorization_token: ${GITHUB_TOKEN}
checkout: { branch: main }
```

Store both variables in `.env` (which is gitignored). The token must be able to read the repository. `checkout` and `mount_path` are optional in the portable declaration. For Qoder, OpenAgentPack always sends a path under `/data`: when omitted it derives `/data/workspace/<repo-name>` from `url`; an explicit Qoder `mount_path` must start with `/data/`. This is required for Qoder to materialize the repository in the Session environment. Other providers retain their own path semantics.

Provider mount roots are fixed and OpenAgentPack applies the same policy to wire requests and prompt file hints:

| Provider | Required mount root | Default GitHub repository path |
| --- | --- | --- |
| Qoder | `/data` | `/data/workspace/<repo-name>` |
| Claude | `/workspace` | `/workspace/<repo-name>` |
| Bailian | `/mnt` | GitHub Session resources unsupported |
| Ark | `/mnt` | GitHub Session resources unsupported |

An explicit absolute path must already use the target provider's root and is passed through unchanged; an absolute path under another root is a validation error. A relative uploaded-file path is resolved under the provider root. Portable configurations should normally omit repository `mount_path` and let the provider adapter derive it.

## Programmatic usage

Expand Down
27 changes: 25 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,12 @@ agents:
environment: <string>
tunnel: <string> # optional; Qoder BYOC tunnel name
provider: <string>
tools: { builtin: [...], mcp: [...], permissions: {...} }
tools: { builtin: [...], default_permission: allow, mcp: [...], permissions: {...} }
mcp_servers: [ { name, type?, url? } ]
skills: [ <string> | { type, skill_id, version? } ]
vault: <string>
memory_stores: [ <string> ]
resources: [ SessionResource ]
multiagent: { type: "coordinator", agents: [...] }
metadata: { <key>: <string> }
```
Expand All @@ -258,16 +259,38 @@ agents:
| `tunnel` | string | no | Qoder BYOC tunnel name from `tunnels`; unsupported for other providers. |
| `provider` | string | no | Pin the agent to one provider. |
| `tools.builtin` | string[] | yes (in `tools`) | Lowercase tool names. |
| `tools.permissions` | map<string,`"allow"`\|`"ask"`> | no | Per-tool permission policy. |
| `tools.default_permission` | `"allow"` \| `"ask"` | no | Permission inherited by enabled builtins; defaults to `allow`. |
| `tools.permissions` | map<string,`"allow"`\|`"ask"`> | no | Case- and separator-insensitive overrides for enabled builtins. Unknown and duplicate normalized names are rejected. |
| `tools.mcp[]` | McpToolkitDecl[] | no | Select tools from an official MCP server. |
| `mcp_servers[]` | `{ name, type?, url? }` | no | URL (`url`/`http`) or `official` MCP server. |
| `skills[]` | string \| AgentSkillRef | no | Skill name or `{ type: "official"\|"custom", skill_id, version? }`. |
| `vault` | string | no | Vault name. |
| `memory_stores` | string[] | no | Bound memory stores. |
| `resources` | SessionResource[] | no | Resources attached to every managed Session created for the Agent. |
| `multiagent.type` | `"coordinator"` | no | Declare a coordinator agent. |
| `multiagent.agents` | string[] | yes (with multiagent) | Agents it orchestrates. |
| `metadata` | map<string,string> | no | Free-form metadata. |

### Session resources

Qoder and Claude managed Sessions support a provider-neutral GitHub repository resource:

```yaml
agents:
reviewer:
# ...
resources:
- type: github_repository
url: https://github.com/acme/private-repo.git
authorization_token: ${GITHUB_TOKEN}
checkout: { branch: main } # or: { commit: <full-sha> }
mount_path: /data/workspace/private-repo # optional
```

Keep `authorization_token` in `.env`; never put its value directly in `agents.yaml`. Qoder mount paths must start with `/data/`. If the field is omitted for Qoder, OpenAgentPack sends `/data/workspace/<repo-name>` automatically; for the URL above that is `/data/workspace/private-repo`. Qoder requires this `/data` path for the repository mount to take effect. Other providers retain their own path semantics.

Mount roots are provider invariants: Qoder uses `/data`, Claude uses `/workspace`, and Bailian and Ark use `/mnt`. A relative uploaded-file path is resolved under the target root. An explicit absolute path must already use the matching root and is passed through unchanged; OpenAgentPack rejects mismatched absolute paths instead of silently rewriting them. For GitHub Session resources, when the path is omitted Qoder derives `/data/workspace/<repo-name>` and Claude derives `/workspace/<repo-name>`.

### MCP toolkit (`tools.mcp[]`)

```yaml
Expand Down
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ examples/
│ ├── with-skills/ skill + file-referenced instructions
│ ├── with-mcp/ MCP server + vault + restricted network
│ ├── with-memory/ persistent memory store (beta)
│ ├── github-session/ private GitHub repository mounted into each Session
│ ├── multiagent/ coordinator multi-agent (Claude only)
│ ├── multi-provider/ same agent on both Claude + Qoder
│ ├── deployment/ schedule + outcome rubric (native)
Expand All @@ -31,6 +32,7 @@ examples/
│ ├── with-mcp/ MCP server + vault + restricted network
│ ├── with-memory/ persistent memory store
│ ├── with-vault/ vault only
│ ├── github-session/ private GitHub repository mounted into each Session
│ ├── vault-only/ vault-only project
│ ├── multi-provider/ same agent on both Claude + Qoder
│ ├── deployment/ schedule + memory_store (native)
Expand Down Expand Up @@ -65,6 +67,7 @@ extensions, and live-test commands.
| Multi-Agent | unsupported | unsupported | native | native | Claude and Volcengine Ark support coordinator. |
| Deployment | emulated | native | native | emulated | Qoder and Claude schedule server-side; Bailian and Ark expand into a session at `run` time. |
| Session | native | native | native | native | All four support runtime sessions. |
| GitHub Session resource | unsupported | native | native | unsupported | Qoder and Claude clone and mount repositories at Session creation. |

## Tool naming differences

Expand Down Expand Up @@ -103,3 +106,10 @@ agents session list --agent assistant # filter by agent
agents session get sess_abc123 # session detail
agents session delete sess_abc123 # delete a session
```

GitHub Session examples require `GITHUB_REPOSITORY_URL` and a read-capable
`GITHUB_TOKEN` in `.env`. See `qoder/github-session` and `claude/github-session`.

Qoder repository mounts must live under `/data`. Its example uses an explicit
`/data/workspace/repository`; when `mount_path` is omitted, OpenAgentPack sends
`/data/workspace/<repo-name>` automatically.
32 changes: 32 additions & 0 deletions examples/claude/github-session/agents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
version: "1"

providers:
claude:
api_key: ${ANTHROPIC_API_KEY}

defaults:
provider: claude

environments:
dev:
config:
type: cloud
networking:
type: unrestricted

agents:
repository-reader:
description: "Read a private GitHub repository in a Claude managed Session"
model: claude-sonnet-4-6
instructions: |
Inspect the mounted repository without modifying it. Report the current
branch, latest commit, and top-level files.
environment: dev
tools:
builtin: [read, glob, grep, bash]
default_permission: allow
resources:
- type: github_repository
url: ${GITHUB_REPOSITORY_URL}
authorization_token: ${GITHUB_TOKEN}
mount_path: /workspace/repository
32 changes: 32 additions & 0 deletions examples/qoder/github-session/agents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder

environments:
dev:
config:
type: cloud
networking:
type: unrestricted

agents:
repository-reader:
description: "Read a private GitHub repository in a Qoder managed Session"
model: ultimate
instructions: |
Inspect the mounted repository without modifying it. Report the current
branch, latest commit, and top-level files.
environment: dev
tools:
builtin: [read, glob, grep, bash]
default_permission: allow
resources:
- type: github_repository
url: ${GITHUB_REPOSITORY_URL}
authorization_token: ${GITHUB_TOKEN}
mount_path: /data/workspace/repository
2 changes: 2 additions & 0 deletions packages/sdk/src/internal/core/session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export async function createSessionForAgent(
vaultIds: options.vaultIds,
memoryStores: options.memoryStores,
files: options.files,
resources: options.resources,
title: options.title,
metadata: options.metadata,
});
Expand All @@ -173,6 +174,7 @@ export async function startSessionRun(
vaultIds: options.vaultIds,
memoryStores: options.memoryStores,
files: options.files,
resources: options.resources,
title: options.title,
metadata: options.metadata,
});
Expand Down
45 changes: 45 additions & 0 deletions packages/sdk/src/internal/core/validate-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { getProvider } from "../providers/registry.ts";
import type { ProjectConfig } from "../types/config.ts";
import type { Diagnostic } from "../types/plan.ts";
import type { ResourceAddress } from "../types/state.ts";
import { providerMountPrefix } from "../utils/sandbox-mount.ts";
import { findMissingBailianMcpToolConfigs } from "../validation/bailian.ts";

export interface ValidateProjectConfigOptions {
Expand Down Expand Up @@ -226,6 +227,50 @@ export function collectProviderCapabilities(
for (const [name, agent] of Object.entries(config.agents ?? {})) {
if (agent.provider && agent.provider !== providerName) continue;
const delivery = agent.delivery?.[providerName]?.type ?? "managed";
const address: ResourceAddress = {
type: delivery === "forward" ? "template" : "agent",
name,
provider: providerName,
};
const asksForApproval =
agent.tools?.default_permission === "ask" ||
Object.values(agent.tools?.permissions ?? {}).some((permission) => permission === "ask");
if (asksForApproval && !def.features.tool_permissions) {
diagnostics.error(
`${providerName}.agent.tool_permissions.unsupported`,
`agent.${name}: provider '${providerName}' cannot enforce interactive tool permission 'ask'.`,
address,
);
}
for (const resource of agent.resources ?? []) {
if (!def.features.session_resources.includes(resource.type)) {
diagnostics.error(
`${providerName}.agent.session_resource.${resource.type}.unsupported`,
`agent.${name}: provider '${providerName}' does not support Session resource type '${resource.type}'.`,
address,
);
}
const mountPrefix = providerMountPrefix(providerName);
if (
mountPrefix &&
resource.mount_path &&
resource.mount_path !== mountPrefix &&
!resource.mount_path.startsWith(`${mountPrefix}/`)
) {
diagnostics.error(
`${providerName}.agent.session_resource.mount_path.invalid`,
`agent.${name}: ${providerName} Session resource mount_path must start with '${mountPrefix}/'.`,
address,
);
}
}
if (delivery === "forward" && agent.resources?.length) {
diagnostics.error(
`${providerName}.template.session_resources.unsupported`,
`agent.${name}: Forward delivery cannot attach Agent Session resources; use managed delivery.`,
address,
);
}
if (delivery === "forward" && !isSupported(caps, "template")) {
diagnostics.error(
`${providerName}.agent.delivery.forward.unsupported`,
Expand Down
51 changes: 46 additions & 5 deletions packages/sdk/src/internal/parser/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { canonicalToolName } from "../utils/tool-permissions.ts";

const networkingSchema = z.object({
type: z.enum(["unrestricted", "limited"]),
Expand Down Expand Up @@ -168,11 +169,37 @@ const mcpToolkitSchema = z
configs: t.configs,
}));

const toolsSchema = z.object({
builtin: z.array(z.string()),
mcp: z.array(mcpToolkitSchema).optional(),
permissions: z.record(z.string(), z.enum(["allow", "ask"])).optional(),
});
const toolsSchema = z
.object({
builtin: z.array(z.string()),
default_permission: z.enum(["allow", "ask"]).optional(),
mcp: z.array(mcpToolkitSchema).optional(),
permissions: z.record(z.string(), z.enum(["allow", "ask"])).optional(),
})
.superRefine((tools, ctx) => {
const enabled = new Set(tools.builtin.map(canonicalToolName));
const seen = new Map<string, string>();
for (const key of Object.keys(tools.permissions ?? {})) {
const canonical = canonicalToolName(key);
const previous = seen.get(canonical);
if (previous) {
ctx.addIssue({
code: "custom",
path: ["permissions", key],
message: `duplicates permission key '${previous}' after tool-name normalization`,
});
} else {
seen.set(canonical, key);
}
if (!enabled.has(canonical)) {
ctx.addIssue({
code: "custom",
path: ["permissions", key],
message: `references tool '${key}' which is not enabled in tools.builtin`,
});
}
}
});

const multiagentSchema = z.object({
type: z.literal("coordinator"),
Expand All @@ -199,6 +226,19 @@ const agentDeliverySchema = z.object({
type: z.enum(["managed", "forward"]),
});

const sessionGithubRepoResourceSchema = z.object({
type: z.literal("github_repository"),
url: z.string().url(),
checkout: z
.object({ branch: z.string().min(1).optional(), commit: z.string().min(1).optional() })
.refine((value) => !(value.branch && value.commit), {
message: "checkout accepts either branch or commit, not both",
})
.optional(),
mount_path: z.string().optional(),
authorization_token: z.string().min(1),
});

const agentSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
Expand All @@ -212,6 +252,7 @@ const agentSchema = z.object({
skills: z.array(z.union([z.string(), agentSkillRefSchema])).optional(),
vault: z.string().optional(),
memory_stores: z.array(z.string()).optional(),
resources: z.array(sessionGithubRepoResourceSchema).optional(),
multiagent: multiagentSchema.optional(),
metadata: z.record(z.string(), z.string()).optional(),
delivery: z.record(z.string(), agentDeliverySchema).optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/internal/providers/ark/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ registerProvider({
name: "ark",
configSchema: arkConfigSchema,
capabilities: ARK_CAPABILITIES,
features: { tool_permissions: true, session_resources: [] },
createAdapter: (config, projectName) => {
const c = config as ArkConfig;
return new ArkAdapter(c.api_key, projectName);
Expand Down
Loading