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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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: 12 additions & 4 deletions apps/server/openapi.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -721,7 +721,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -752,7 +754,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -787,7 +791,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand DownExpand Up@@ -843,7 +849,9 @@
"agent",
"template",
"deployment",
"file"
"file",
"identity",
"channel"
]
},
"name": {
Expand Down
16 changes: 12 additions & 4 deletions apps/webui/src/lib/api/generated/schema.d.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -283,7 +283,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -298,7 +300,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand All@@ -315,7 +319,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
};
Expand All@@ -341,7 +347,9 @@ export interface paths {
| "agent"
| "template"
| "deployment"
| "file";
| "file"
| "identity"
| "channel";
name: string;
provider: string;
}[];
Expand Down
1 change: 1 addition & 0 deletions docs/examples.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
| Connect an MCP server + vault | [`examples/claude/with-mcp/`](../examples/claude/with-mcp/) · [`examples/qoder/with-mcp/`](../examples/qoder/with-mcp/) · [`examples/ark/with-mcp/`](../examples/ark/with-mcp/) |
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |
Expand Down
41 changes: 27 additions & 14 deletions docs/guides/deploy-to-qoder.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,7 +22,7 @@ providers:

| Feature | Tier |
|---------|:----:|
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
| Multi-Agent | unsupported |

A `deployment run` on Qoder creates a native Deployment Run and associated Session. Cron schedules run server-side.
Expand DownExpand Up@@ -94,16 +94,18 @@ agents:
`agents plan/apply/destroy` then manage a Qoder Forward Template (`tmpl_...`) instead of a Managed Agent
(`agent_...`). The default remains Managed Agent delivery when `delivery` is omitted.

Identity is optional for local Forward Session testing. If omitted, OpenCMA finds the enabled Qoder Identity whose
`external_id` is `__qca_admin_identity__`, then sends its real `idn_...` id to the Session API. To run as a specific
user—or when the reserved Identity is unavailable—configure an existing Identity explicitly:
Forward Identity represents an end user in the integrating product. Declare it once by its stable business id and select
it as the project default:

```yaml
defaults:
provider: qoder
session:
qoder:
identity_id: idn_xxx
identity: chen

identities:
chen:
external_id: user_456
name: Chen
```

Then use the same session command to test a Forward-delivered agent:
Expand All@@ -112,14 +114,25 @@ Then use the same session command to test a Forward-delivered agent:
agents session run "Hello" --agent forward-assistant
```

The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
using the Cloud gateway.
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.

Credential-based messaging Channels are declarative too:

```yaml
channels:
support-dingtalk:
agent: forward-assistant
type: dingtalk
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
```

Production messaging delivery is a separate Forward Channel concern. An IM Channel explicitly binds a business Identity
and Template; do not treat the CLI's local testing Identity as an end-user Identity. Qoder
recommends QR-code authorization for Channel binding so channel credentials do not need to be stored locally.
The Channel inherits `defaults.identity`; the Qoder adapter resolves both remote ids and maps the generic declaration to
the Forward Channel request. Feishu uses `app_id`/`app_secret`, and WeCom uses `bot_id`/`secret`. Personal WeChat is QR-only
and is not supported by credential-based apply.

Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
require an Agent resource.
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/run-sessions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
agents session run "Summarize the repo structure" --agent assistant
```

`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. For a Qoder agent with `delivery.qoder.type: forward`, Identity is optional: without one OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Configure `defaults.session.qoder.identity_id` or pass `--identity-id` to select an existing business Identity. OpenCMA never creates or updates Identity resources implicitly.
`session run` creates a session, sends the prompt, and polls until the response completes. Pass `--stream` to receive live events over SSE. When only one agent is configured, `--agent` is auto-detected. A Qoder agent with `delivery.qoder.type: forward` requires a declared Identity selected through `defaults.identity`; `agents apply` creates or resolves it before sessions run. Pass `--identity-id` only to override the resolved default with an existing provider Identity for one invocation.

Options:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/cli.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,7 +111,7 @@ Manage runtime agent sessions.
| `session events <session-id>` | List event history for a session. |
| `session delete <session-id>` | Delete a session. |

`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. `--identity-id` selects an existing Qoder Forward Identity and overrides `defaults.session.qoder.identity_id`; when neither is provided, OpenCMA resolves the Identity whose `external_id` is `__qca_admin_identity__`. OpenCMA never creates or updates an Identity. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.
`session create` / `session run` accept `--agent`, `--identity-id`, `--environment`, `--vault`, `--memory-stores`, `--title`, and `--provider`. Forward Sessions resolve the declared logical `defaults.identity`; `--identity-id` overrides it with an existing provider id for one invocation. `session run` and `session send` use polling by default and accept `--stream` to opt into SSE streaming, plus `--json` for JSON output. `session list` accepts `--agent` and `--all`; `session events` accepts `--limit`, `--all`, `--json`.

## `agents deployment`

Expand Down
56 changes: 53 additions & 3 deletions docs/reference/configuration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,15 +9,16 @@ version: "1"
providers: { <name>: <provider-config> }
defaults:
provider: <name> | "all"
session:
qoder: { identity_id: <existing-idn-id> }
identity: <identity-name>
environments: { <name>: EnvironmentDecl }
tunnels: { <name>: TunnelDecl }
vaults: { <name>: VaultDecl }
memory_stores:{ <name>: MemoryStoreDecl }
skills: { <name>: SkillDecl }
files: { <name>: FileDecl }
identities: { <name>: IdentityDecl }
agents: { <name>: AgentDecl }
channels: { <name>: ChannelDecl }
deployments: { <name>: DeploymentDecl }
```

Expand All@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
| `version` | string | yes | Schema version. Currently `"1"`. |
| `providers` | map | yes | One block per provider; each holds its credentials. |
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
| `defaults.session.qoder.identity_id` | string | no | Existing Qoder Forward Identity used as the local Session default. If omitted, OpenCMA looks up the enabled Identity whose `external_id` is `__qca_admin_identity__` and sends its real `idn_...` id. Never created or managed by `apply`. |
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
| `environments` | map | no | Cloud runtimes. |
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
| `vaults` | map | no | Credential stores. |
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
| `skills` | map | no | Reusable capability modules. |
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
| `agents` | map | no | The core managed-agent resources. |
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
| `deployments` | map | no | Repeatable run units. |

Secrets use `${VAR_NAME}` and resolve from `.env` (walking up to the project root). `agents init` appends `agents.state.json` and `.env` to `.gitignore`.

## Identity

Managed identities use the integrating product's stable end-user id:

```yaml
identities:
chen:
provider: qoder
external_id: user_456
name: Chen
enabled: true
metadata:
department: engineering
```

`agents apply` creates or updates the remote Identity and stores its provider id in state. To reference an Identity managed outside this project, use the mutually exclusive external-reference form:

```yaml
identities:
chen:
provider: qoder
identity_id: idn_019eabc123
```

External references are verified and recorded but never updated or deleted.

## Channel

```yaml
channels:
support-dingtalk:
provider: qoder # optional; inherits defaults.provider
agent: support-agent
identity: chen # optional; inherits defaults.identity
type: dingtalk
name: Support DingTalk # optional; defaults to the YAML key
enabled: true # optional; defaults to true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false
```

The declaration intentionally uses logical `agent` and `identity` references. Provider adapters resolve remote ids and map `type`, `credentials`, and `options` to provider wire fields. Qoder Channels require the referenced Agent to use Forward delivery. Credential-based Qoder support currently covers DingTalk, Feishu, and WeCom; personal WeChat remains QR-only.

## Provider configuration

Each provider under `providers` is validated by the provider's own config schema.
Expand Down
88 changes: 88 additions & 0 deletions examples/qoder/byoc/agents.yaml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
# Qoder BYOC 端到端测试配置示例
#
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
# 使用前请确保已设置环境变量:
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
#
# 关键约束:
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。

version: "1"

providers:
qoder:
api_key: ${QODER_PAT}

defaults:
provider: qoder
identity: byoc

# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
identities:
byoc:
external_id: byoc
name: "byoc"
enabled: true
metadata:
purpose: byoc-assistant

channels:
byoc-dingtalk:
name: "byoc Agent DingTalk"
agent: byoc-agent
type: dingtalk
enabled: true
credentials:
client_id: ${DINGTALK_CLIENT_ID}
client_secret: ${DINGTALK_CLIENT_SECRET}
options:
include_tool_calls: false
include_thinking: false

# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
environments:
byoc:
description: "BYOC self-hosted environment"
environment_id: ${BYOC_ENVIRONMENT_ID}
config:
type: self_hosted
metadata:
purpose: byoc-test

# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
tunnels:
internal:
description: "BYOC tunnel"
tunnel_id: ${BYOC_TUNNEL_ID}

agents:
byoc-agent:
description: "Assistant running inside BYOC"
model:
qoder: auto
instructions: |
You are a BYOC end-to-end test assistant running in a self-hosted environment.
Your goal is to verify that Qoder BYOC tool calling is working.
Answer user questions concisely. Use the available builtin tools when helpful.
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
environment: byoc
tunnel: internal
tools:
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
permissions:
Bash: allow
Read: allow
Write: allow
Edit: allow
Glob: allow
Grep: allow
WebFetch: allow
WebSearch: allow
DeliverArtifacts: allow
delivery:
qoder:
type: forward
12 changes: 12 additions & 0 deletions examples/qoder/with-channel/README.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
# Qoder credential-based Channel

This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.

Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:

```bash
agents plan
agents apply
```

For Feishu, set `type: feishu` and use `app_id` / `app_secret`. For WeCom, set `type: wecom` and use `bot_id` / `secret`. Personal WeChat requires QR binding and is outside this credential-based workflow.
Loading