Skip to content

Commit 4ef11d9

Browse files
committed
Add declarative identity and channel resources
Change-Id: I2c8c162fbe08f4a53b79d3d66df7fdf9cd44df23
1 parent ddef91c commit 4ef11d9

32 files changed

Lines changed: 1045 additions & 171 deletions

docs/examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The [`examples/`](../examples) directory has runnable configs for every provider
1212
| 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/) |
1313
| Use an official MCP server | [`examples/bailian/with-mcp/`](../examples/bailian/with-mcp/) |
1414
| Use a vault | [`examples/bailian/with-vault/`](../examples/bailian/with-vault/) · [`examples/qoder/with-vault/`](../examples/qoder/with-vault/) |
15+
| Connect a credential-based IM Channel | [`examples/qoder/with-channel/`](../examples/qoder/with-channel/) |
1516
| Use memory stores | [`examples/qoder/with-memory/`](../examples/qoder/with-memory/) · [`examples/ark/full/`](../examples/ark/full/) |
1617
| Upload local files (Files API) | [`examples/bailian/with-files/`](../examples/bailian/with-files/) · [`examples/ark/with-files/`](../examples/ark/with-files/) |
1718
| Coordinate multiple agents | [`examples/claude/multiagent/`](../examples/claude/multiagent/) · [`examples/ark/multiagent/`](../examples/ark/multiagent/) |

docs/guides/deploy-to-qoder.md

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ providers:
2222

2323
| Feature | Tier |
2424
|---------|:----:|
25-
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session | native |
25+
| Environment, Vault, Skill, Agent, MCP Server, Memory Store, Deployment, Session, Identity, Channel | native |
2626
| Multi-Agent | unsupported |
2727

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

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

101100
```yaml
102101
defaults:
103102
provider: qoder
104-
session:
105-
qoder:
106-
identity_id: idn_xxx
103+
identity: chen
104+
105+
identities:
106+
chen:
107+
external_id: user_456
108+
name: Chen
107109
```
108110

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

115-
The CLI resolves the applied Template and optional configured Identity, creates a Forward Session, and routes create, send,
116-
stream, get, and archive operations through the Forward gateway. It never creates or updates an Identity as a side effect
117-
of starting a Session. Use `--identity-id idn_xxx` to override the YAML default for one invocation. Managed sessions keep
118-
using the Cloud gateway.
117+
`agents apply` manages the declared Identity. The CLI resolves `defaults.identity` through state, creates a Forward
118+
Session, and routes create, send, stream, get, and archive operations through the Forward gateway. Use
119+
`--identity-id idn_xxx` only as a runtime escape hatch. Managed sessions keep using the Cloud gateway.
120+
121+
Credential-based messaging Channels are declarative too:
122+
123+
```yaml
124+
channels:
125+
support-dingtalk:
126+
agent: forward-assistant
127+
type: dingtalk
128+
credentials:
129+
client_id: ${DINGTALK_CLIENT_ID}
130+
client_secret: ${DINGTALK_CLIENT_SECRET}
131+
```
119132

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

124137
Forward-delivered agents still cannot be referenced by OpenAgentPack deployments; scheduled Managed Deployment runs
125138
require an Agent resource.

docs/guides/run-sessions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A **session** is a runtime conversation started from a managed agent. Sessions a
1212
agents session run "Summarize the repo structure" --agent assistant
1313
```
1414

15-
`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.
15+
`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.
1616

1717
Options:
1818

docs/reference/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ Manage runtime agent sessions.
111111
| `session events <session-id>` | List event history for a session. |
112112
| `session delete <session-id>` | Delete a session. |
113113

114-
`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`.
114+
`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`.
115115

116116
## `agents deployment`
117117

docs/reference/configuration.md

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@ version: "1"
99
providers: { <name>: <provider-config> }
1010
defaults:
1111
provider: <name> | "all"
12-
session:
13-
qoder: { identity_id: <existing-idn-id> }
12+
identity: <identity-name>
1413
environments: { <name>: EnvironmentDecl }
1514
tunnels: { <name>: TunnelDecl }
1615
vaults: { <name>: VaultDecl }
1716
memory_stores:{ <name>: MemoryStoreDecl }
1817
skills: { <name>: SkillDecl }
1918
files: { <name>: FileDecl }
19+
identities: { <name>: IdentityDecl }
2020
agents: { <name>: AgentDecl }
21+
channels: { <name>: ChannelDecl }
2122
deployments: { <name>: DeploymentDecl }
2223
```
2324
@@ -26,18 +27,67 @@ deployments: { <name>: DeploymentDecl }
2627
| `version` | string | yes | Schema version. Currently `"1"`. |
2728
| `providers` | map | yes | One block per provider; each holds its credentials. |
2829
| `defaults.provider` | string | no | Default target for `plan`/`apply`. `all` targets every declared provider. |
29-
| `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`. |
30+
| `defaults.identity` | string | no | Logical name of the default declared Identity used by identity-aware resources and Forward Sessions. |
3031
| `environments` | map | no | Cloud runtimes. |
3132
| `tunnels` | map | no | Existing Qoder BYOC tunnels referenced by sessions; OpenCMA does not manage their lifecycle. |
3233
| `vaults` | map | no | Credential stores. |
3334
| `memory_stores` | map | no | Persistent agent context (Qoder, Volcengine Ark). |
3435
| `skills` | map | no | Reusable capability modules. |
3536
| `files` | map | no | Local files uploaded to the Files API (Bailian, Volcengine Ark). |
37+
| `identities` | map | no | Stable end-user identities. Provider support is capability-gated. |
3638
| `agents` | map | no | The core managed-agent resources. |
39+
| `channels` | map | no | External messaging channels bound to an Identity and Agent. Provider support is capability-gated. |
3740
| `deployments` | map | no | Repeatable run units. |
3841

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

44+
## Identity
45+
46+
Managed identities use the integrating product's stable end-user id:
47+
48+
```yaml
49+
identities:
50+
chen:
51+
provider: qoder
52+
external_id: user_456
53+
name: Chen
54+
enabled: true
55+
metadata:
56+
department: engineering
57+
```
58+
59+
`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:
60+
61+
```yaml
62+
identities:
63+
chen:
64+
provider: qoder
65+
identity_id: idn_019eabc123
66+
```
67+
68+
External references are verified and recorded but never updated or deleted.
69+
70+
## Channel
71+
72+
```yaml
73+
channels:
74+
support-dingtalk:
75+
provider: qoder # optional; inherits defaults.provider
76+
agent: support-agent
77+
identity: chen # optional; inherits defaults.identity
78+
type: dingtalk
79+
name: Support DingTalk # optional; defaults to the YAML key
80+
enabled: true # optional; defaults to true
81+
credentials:
82+
client_id: ${DINGTALK_CLIENT_ID}
83+
client_secret: ${DINGTALK_CLIENT_SECRET}
84+
options:
85+
include_tool_calls: false
86+
include_thinking: false
87+
```
88+
89+
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.
90+
4191
## Provider configuration
4292

4393
Each provider under `providers` is validated by the provider's own config schema.

examples/qoder/byoc/agents.yaml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Qoder BYOC 端到端测试配置示例
2+
#
3+
# 用途:验证 Qoder BYOC 接入(工具调用)是否打通。
4+
# 使用前请确保已设置环境变量:
5+
# - QODER_PAT Qoder API 个人访问令牌(https://qoder.com/account/integrations)
6+
# - BYOC_ENVIRONMENT_ID 已创建的 BYOC environment id
7+
# - BYOC_TUNNEL_ID 已创建的 BYOC tunnel id
8+
# - DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET 钉钉渠道凭证(可选)
9+
#
10+
# 关键约束:
11+
# - environment_id / tunnel_id 指向的 BYOC 资源由平台侧开通,OpenCMA 只引用不修改。
12+
# - tunnel 只是 agents.yaml 内的命名别名,直接引用 Qoder 下发的 tunnel_id。
13+
14+
version: "1"
15+
16+
providers:
17+
qoder:
18+
api_key: ${QODER_PAT}
19+
20+
defaults:
21+
provider: qoder
22+
identity: byoc
23+
24+
# Forward Identity 表示集成侧的终端用户。OpenCMA 按 external_id 创建/复用并管理远端 Identity。
25+
identities:
26+
byoc:
27+
external_id: byoc
28+
name: "byoc"
29+
enabled: true
30+
metadata:
31+
purpose: byoc-assistant
32+
33+
channels:
34+
byoc-dingtalk:
35+
name: "byoc Agent DingTalk"
36+
agent: byoc-agent
37+
type: dingtalk
38+
enabled: true
39+
credentials:
40+
client_id: ${DINGTALK_CLIENT_ID}
41+
client_secret: ${DINGTALK_CLIENT_SECRET}
42+
options:
43+
include_tool_calls: false
44+
include_thinking: false
45+
46+
# 已存在的 BYOC 环境,通过 environment_id 直接引用,不需要 state import。
47+
environments:
48+
byoc:
49+
description: "BYOC self-hosted environment"
50+
environment_id: ${BYOC_ENVIRONMENT_ID}
51+
config:
52+
type: self_hosted
53+
metadata:
54+
purpose: byoc-test
55+
56+
# 已存在的 BYOC tunnel。直接引用 Qoder 下发的 tunnel_id,OpenCMA 不会尝试创建或修改它。
57+
tunnels:
58+
internal:
59+
description: "BYOC tunnel"
60+
tunnel_id: ${BYOC_TUNNEL_ID}
61+
62+
agents:
63+
byoc-agent:
64+
description: "Assistant running inside BYOC"
65+
model:
66+
qoder: auto
67+
instructions: |
68+
You are a BYOC end-to-end test assistant running in a self-hosted environment.
69+
Your goal is to verify that Qoder BYOC tool calling is working.
70+
Answer user questions concisely. Use the available builtin tools when helpful.
71+
If a tool call fails, report the error clearly so connectivity issues can be diagnosed.
72+
environment: byoc
73+
tunnel: internal
74+
tools:
75+
builtin: [Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, DeliverArtifacts]
76+
permissions:
77+
Bash: allow
78+
Read: allow
79+
Write: allow
80+
Edit: allow
81+
Glob: allow
82+
Grep: allow
83+
WebFetch: allow
84+
WebSearch: allow
85+
DeliverArtifacts: allow
86+
delivery:
87+
qoder:
88+
type: forward
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Qoder credential-based Channel
2+
3+
This example declares a Forward Identity, Forward Template, and DingTalk Channel without copying remote Qoder ids.
4+
5+
Set `QODER_PAT`, `SUPPORT_USER_ID`, `DINGTALK_CLIENT_ID`, and `DINGTALK_CLIENT_SECRET`, then run:
6+
7+
```bash
8+
agents plan
9+
agents apply
10+
```
11+
12+
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.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
version: "1"
2+
3+
providers:
4+
qoder:
5+
api_key: ${QODER_PAT}
6+
7+
defaults:
8+
provider: qoder
9+
identity: support-user
10+
11+
identities:
12+
support-user:
13+
external_id: ${SUPPORT_USER_ID}
14+
name: "Support User"
15+
16+
environments:
17+
runtime:
18+
config:
19+
type: cloud
20+
networking:
21+
type: unrestricted
22+
23+
agents:
24+
support-agent:
25+
model:
26+
qoder: auto
27+
instructions: |
28+
You are a concise support assistant.
29+
environment: runtime
30+
delivery:
31+
qoder:
32+
type: forward
33+
34+
channels:
35+
support-dingtalk:
36+
agent: support-agent
37+
type: dingtalk
38+
credentials:
39+
client_id: ${DINGTALK_CLIENT_ID}
40+
client_secret: ${DINGTALK_CLIENT_SECRET}
41+
options:
42+
include_tool_calls: false
43+
include_thinking: false

packages/sdk/src/internal/core/destroy-runtime.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ export interface DestroyProjectResult extends DestroyPlanResult {
4747

4848
const destroyOrder: Record<ResourceType, number> = {
4949
deployment: 0,
50+
channel: 0,
5051
agent: 1,
5152
template: 1,
52-
skill: 2,
53-
memory_store: 3,
54-
vault: 4,
55-
file: 5,
56-
environment: 6,
53+
identity: 2,
54+
skill: 3,
55+
memory_store: 4,
56+
vault: 5,
57+
file: 6,
58+
environment: 7,
5759
};
5860

5961
export function planDestroyProjectContext(ctx: ProjectRuntimeContext): DestroyPlanResult {
@@ -100,7 +102,7 @@ async function destroyOneResource(
100102
// BYOC environments are provisioned and owned by QCA. `environment_id` means
101103
// this project only references that environment, so destroy must never make a
102104
// remote lifecycle call for it.
103-
if (isExternalEnvironment(ctx, resource)) {
105+
if (isExternalReference(ctx, resource)) {
104106
ctx.state.removeResource(resource.address);
105107
return successResult(resource, "reference_removed");
106108
}
@@ -170,11 +172,15 @@ async function destroyOneResource(
170172
}
171173
}
172174

173-
function isExternalEnvironment(ctx: ProjectRuntimeContext, resource: ResourceState): boolean {
174-
return (
175-
resource.address.type === "environment" &&
176-
(resource.externally_managed || Boolean(ctx.config.environments?.[resource.address.name]?.environment_id))
177-
);
175+
function isExternalReference(ctx: ProjectRuntimeContext, resource: ResourceState): boolean {
176+
if (resource.externally_managed) return true;
177+
if (resource.address.type === "environment") {
178+
return Boolean(ctx.config.environments?.[resource.address.name]?.environment_id);
179+
}
180+
if (resource.address.type === "identity") {
181+
return Boolean(ctx.config.identities?.[resource.address.name]?.identity_id);
182+
}
183+
return false;
178184
}
179185

180186
function successResult(
@@ -225,6 +231,17 @@ async function deleteRemoteResource(
225231
case "deployment":
226232
await provider.deleteDeployment(id);
227233
return;
234+
case "identity":
235+
if (!provider.deleteIdentity) throw new UserError(`Provider does not support identities`);
236+
await provider.deleteIdentity(id);
237+
return;
238+
case "channel":
239+
if (!provider.deleteChannel) throw new UserError(`Provider does not support channels`);
240+
await provider.deleteChannel(id);
241+
return;
242+
case "file":
243+
await provider.deleteFile(id);
244+
return;
228245
}
229246
}
230247

0 commit comments

Comments
 (0)