Skip to content

Commit 8a058da

Browse files
yiliang114qwencoder
andcommitted
Merge origin/main into feat/hosted-bound-later-turns
Main's permission Actions work (#13101) and this branch's Workspace-bound later Turns both extended WebShellSessionCapabilities with a second field, so the record, its single construction site, the generated WebShell types and the cross-tenant contract assertion each need the union rather than one side. - ApiModels: the record is now (tasks, actions, workspaceTurns). - ManagedAgentService: pass both hasActions(session) and maySubmitWorkspaceTurn(session, actorId). - managed-agent-api.ts: emit actions then workspaceTurns, matching the contract's property order now that neither field is planned; actions stays required and workspaceTurns optional per the schema's required list. - ManagedAgentApiContractTest: the foreign-tenant session asserts both capabilities as false. Co-authored-by: Qwen-Coder <[email protected]> Patrol-Run: qwen-pr-conflict/jmuob3c85i1
2 parents b8c5830 + 78143fe commit 8a058da

34 files changed

Lines changed: 2992 additions & 233 deletions

‎docs/design/2026-09-30-managed-agent-actions.md‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
[English](2026-09-30-managed-agent-actions.md) | [简体中文](2026-09-30-managed-agent-actions.zh-CN.md)
44

5-
Status: D6a (Hosted Harness) implemented; D6b (Java server) designed and lands
6-
separately.
5+
Status: D6a (Hosted Harness) and D6b (Java server) implemented.
76
Date: 2026-09-30
87
Issue: [#12867](https://github.com/QwenLM/qwen-code/issues/12867), part of [#12380](https://github.com/QwenLM/qwen-code/issues/12380)
98
Decisions: [#12867 comment](https://github.com/QwenLM/qwen-code/issues/12867#issuecomment-5895205811)
@@ -19,7 +18,7 @@ Harness requested can be answered through either surface and the Turn
1918
continues, a replayed response returns the original result, and a responder
2019
without the right gets `403`.
2120

22-
`main` has the durable pieces but nothing that uses them:
21+
Before D6, `main` had the durable pieces but nothing that used them:
2322

2423
- The Session authority has `requestToolAction`, a permission ticket only the
2524
current Harness activation may open, and `resolveAction`, the trusted
@@ -96,8 +95,8 @@ of which the Hosted path has. A timeout outside its range answers the same, and
9695
`yolo` ignores the timeout because it never waits. A Session without a tool
9796
profile keeps ignoring the mode, because it runs no tools.
9897

99-
Java keeps refusing Workspace files with any mode but `yolo` until D6b can
100-
answer an Action, so no Session waits for an approval that nobody can answer.
98+
D6b enables `default` and `auto-edit` for Workspace files and serves their
99+
Actions. `yolo` remains the deployment default.
101100

102101
### 5.2 Where the Turn waits
103102

@@ -228,8 +227,12 @@ H records. It also projects `action.changed` into a new Actions table in the
228227
same transaction, reading the options from the `optionsRef` resource that the
229228
Harness publishes before it commits the request, and appends an
230229
`action.updated` Session event. The table
231-
needs a Flyway migration; open pull requests hold V19 to V21, so its number is
232-
settled at merge.
230+
uses Flyway migration V24; tool publication uses V20–V22 and the Session MCP
231+
catalog uses V23.
232+
Projection validates the original options resource and immutable revision chain.
233+
Decision receipt IDs are opaque product handles derived from the recorded
234+
decision, never raw storage resource IDs. The public Turn ID is resolved from
235+
the Hosted prompt ID when a matching Java Turn exists.
233236

234237
### 6.2 Routes
235238

@@ -254,7 +257,7 @@ settled at merge.
254257
decided with this response's decision (Java compares the digest using the
255258
exact decision encoding in section 5.4), and with its end state
256259
(`action_expired`, `action_cancelled` or `action_already_resolved`)
257-
otherwise. A `400` from the Harness completes it with that error. While the
260+
otherwise, exposed as `failure_code` (`failureCode` on WebShell). A `400` from the Harness completes it with that error. While the
258261
Action stays `requested`, for example on a recovery-blocked Session, the
259262
operation stays `running`. The WebShell request gains `requestId`.
260263

@@ -267,16 +270,22 @@ settled at merge.
267270
`409 action_already_resolved`, and an unknown Action gets
268271
`404 action_not_found`. The contract gains these codes.
269272
- The Session capability `actions` reads `true` for a Session whose approval
270-
mode can ask.
273+
mode can ask. Java pins that mode on Workspace Session admission; migrated
274+
Sessions default to `yolo`. Owner checks use the existing creator record
275+
without adding grants or owners. Unknown creators fail closed.
276+
- `allow` and `deny` are stable option IDs. Actions expose both revisions, the
277+
function call ID, tool name and expiry. Arguments come from Items. Only one
278+
Hosted approval is pending per Turn; list returns requested Actions, newest
279+
first, without locally expiring them.
271280

272281
The Turn keeps reading `running` while it waits; its pending Actions are what
273282
tells a client that it is waiting.
274283

275284
### 6.4 Java configuration
276285

277286
A deployment with Workspace files enabled may set `default` or `auto-edit`.
278-
Java sends a deployment-wide approval timeout with the mode, and `yolo` stays
279-
the default. For a Session with a tool profile, Java records the mode it sent
287+
Java sends `QWEN_MANAGED_AGENT_APPROVAL_TIMEOUT` (default `10m`, between `1s`
288+
and `24h`) with the mode, and `yolo` stays the default. For a Session with a tool profile, Java records the mode it sent
280289
at creation and uses the Session only when the Harness reports that mode as
281290
`approvalMode` on create and on every load: a Harness from before D6a omits
282291
it, and would ignore the mode and run every call unasked.
@@ -295,10 +304,10 @@ it, and would ignore the mode and run every call unasked.
295304
recovery-blocked Session answers what it already recorded and admits no
296305
decision or expiry write that was queued before it blocked, a cancel request
297306
releases the Workspace, and a load keeps and reports the saved mode.
298-
- **D6b:** projection and route tests on H2 and MySQL, owner and non-owner
307+
- **D6b:** projection and route tests on H2 and MariaDB (MySQL driver), owner and non-owner
299308
responses, replay, expiry and the contract test's traffic on both surfaces,
300309
and a Hosted process test in which the owner answers through the public API
301-
and the Turn completes.
310+
and WebShell, and the Turn completes.
302311

303312
## 8. Risks and follow-up
304313

‎docs/design/2026-09-30-managed-agent-actions.zh-CN.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[English](2026-09-30-managed-agent-actions.md) | [简体中文](2026-09-30-managed-agent-actions.zh-CN.md)
44

5-
状态:D6a(Hosted Harness)已实现;D6b(Java 服务端)已设计,单独合入。
5+
状态:D6a(Hosted Harness)与 D6b(Java 服务端)均已实现。
66
日期:2026-09-30
77
Issue:[#12867](https://github.com/QwenLM/qwen-code/issues/12867),属于 [#12380](https://github.com/QwenLM/qwen-code/issues/12380)
88
决定:[#12867 评论](https://github.com/QwenLM/qwen-code/issues/12867#issuecomment-5895205811)
@@ -12,7 +12,7 @@ Issue:[#12867](https://github.com/QwenLM/qwen-code/issues/12867),属于 [#12
1212

1313
[公共 API 契约][contract]第 10 节与[契约收敛][closure]第 3 节定义了 Action:需要人授权的工具调用会暂停,回答者通过任一入口回答,然后工具执行或被拒绝。#12867 为 D6 规定的验收条件是:Harness 发起的审批可以通过任一入口回答,Turn 随之继续;重复的回答返回原结果;无权的回答者得到 `403`。
1414

15-
`main` 已经有持久化所需的部件,但没有任何代码使用它们:
15+
D6 之前,`main` 已经有持久化所需的部件,但没有任何代码使用它们:
1616

1717
- Session authority 有 `requestToolAction`(只有当前 Harness activation 能开启的权限票据)和 `resolveAction`(可信仲裁者的最终决定),但没有代码调用它们。
1818
- Harness 句柄有 `commitDurableWait` 与 `resolveDurableWait`,审批未决期间保持一个 `await_action` 检查点,此时 Runtime 派发拒绝启动。
@@ -55,7 +55,7 @@ Harness 遵循 Java 创建 Hosted Session 时已经以 `approvalMode` 发送的
5555

5656
对于工具配置,`plan` 与 `auto` 返回 `400 invalid_hosted_approval`:plan 模式需要自己的规划语义,auto 模式需要分类器,而 Hosted 路径两者都没有。超出范围的超时也返回同样的错误;`yolo` 从不等待,因此忽略超时。没有工具配置的 Session 继续忽略该模式,因为它不运行工具。
5757

58-
在 D6b 能够回答 Action 之前,Java 仍然拒绝 Workspace 文件与 `yolo` 以外的任何模式同时开启,因此不会有 Session 等待一个无人能回答的审批。
58+
D6b 为 Workspace 文件开启 `default` 与 `auto-edit`,并提供它们的审批入口。默认仍为 `yolo`。
5959

6060
### 5.2 Turn 在哪里等待
6161

@@ -118,31 +118,32 @@ Workspace 每个 Turn 获取一次,在 Turn 等待期间保持占用,与模
118118

119119
### 6.1 投影
120120

121-
Session Store 已经会读取每一行已提交的 journal 来投影 Stage H 记录。它还会在同一个事务中把 `action.changed` 投影到一张新的 Action 表,从 Harness 在提交请求之前发布的 `optionsRef` 资源中读取选项,并追加一个 `action.updated` Session 事件。这张表需要一个 Flyway 迁移;在飞的 PR 占用了 V19 至 V21,因此它的版本号在合入时确定。
121+
Session Store 已经会读取每一行已提交的 journal 来投影 Stage H 记录。它还会在同一个事务中把 `action.changed` 投影到一张新的 Action 表,从 Harness 在提交请求之前发布的 `optionsRef` 资源中读取选项,并追加一个 `action.updated` Session 事件。该表使用 Flyway V24;V20–V22 已由工具发布功能使用,V23 已由 Session MCP 目录使用。投影验证原始选项资源及不可变的版本链。决定回执 ID 是从已记录决定派生的不透明产品句柄,不暴露存储资源 ID。存在对应的 Java Turn 时,从 Hosted prompt ID 解析公共 Turn ID。
122122

123123
### 6.2 路由
124124

125125
- **列表:** `GET …/actions` 与 WebShell `actions/query` 按从新到旧分页列出 Session 中 `requested` 的 Action,使用 Turn 列表的游标与 limit 规则。
126126
- **读取:** `GET …/actions/{actionId}` 与 WebShell `actions/get` 返回任何状态的 Action;已决定的 Action 带有 `decision_receipt_id`。
127-
- **回答:** `POST …/actions/{actionId}/responses` 与 WebShell `actions/respond` 以 `202` 返回 `action_response` command operation。受理前按 Action 的类型、版本、选项、状态与过期时间检查请求。该 operation 在 D4 的幂等域(租户、Session、类型、actor 与键)内幂等。worker 把它转发到 Harness 路由,失败的尝试按 dispatch 退避回到 pending;与 D4 一样,没有最后一次尝试。worker 以 Java 从 journal 投影出的 Action 为准判断结果,从不根据时钟或仅凭失败的调用推断:Harness 已记录的决定可能已经让调用运行,只是它的回答丢失了,或者 Harness 已经重启。Harness 在返回 `200` 之前已经提交了决定,因此投影中已经能看到它。投影出的 Action 进入终态后,该 operation 才完成:若为 `decided` 且决定与本次回答相同(Java 按第 5.4 节的确切决定编码比较摘要),以 `action_resolution`(`decided`,带决定回执)完成;否则以其结束状态(`action_expired`、`action_cancelled` 或 `action_already_resolved`)完成。Harness 返回 `400` 时以该错误完成。Action 仍为 `requested` 时(例如在恢复阻塞的 Session 上),operation 保持 `running`。WebShell 请求增加 `requestId`。
127+
- **回答:** `POST …/actions/{actionId}/responses` 与 WebShell `actions/respond` 以 `202` 返回 `action_response` command operation。受理前按 Action 的类型、版本、选项、状态与过期时间检查请求。该 operation 在 D4 的幂等域(租户、Session、类型、actor 与键)内幂等。worker 把它转发到 Harness 路由,失败的尝试按 dispatch 退避回到 pending;与 D4 一样,没有最后一次尝试。worker 以 Java 从 journal 投影出的 Action 为准判断结果,从不根据时钟或仅凭失败的调用推断:Harness 已记录的决定可能已经让调用运行,只是它的回答丢失了,或者 Harness 已经重启。Harness 在返回 `200` 之前已经提交了决定,因此投影中已经能看到它。投影出的 Action 进入终态后,该 operation 才完成:若为 `decided` 且决定与本次回答相同(Java 按第 5.4 节的确切决定编码比较摘要),以 `action_resolution`(`decided`,带决定回执)完成;否则以其结束状态(`action_expired`、`action_cancelled` 或 `action_already_resolved`)完成,并通过 `failure_code`(WebShell 为 `failureCode`)公开。Harness 返回 `400` 时以该错误完成。Action 仍为 `requested` 时(例如在恢复阻塞的 Session 上),operation 保持 `running`。WebShell 请求增加 `requestId`。
128128

129129
### 6.3 检查
130130

131131
- 读取保持其他 Session 读取的检查。
132132
- 只有 Session 的 owner(记录为其创建者的可信 actor)可以回答。其他 actor 得到 `403 action_forbidden`。
133133
- 迟到的回答得到 `409 action_expired`、`409 action_cancelled` 或 `409 action_already_resolved`,未知的 Action 得到 `404 action_not_found`。契约会新增这些错误码。
134-
- 对于审批模式可能询问的 Session,Session 能力 `actions` 为 `true`。
134+
- 对于审批模式可能询问的 Session,Session 能力 `actions` 为 `true`。Java 在 Workspace Session 受理时固定模式;迁移前的 Session 默认为 `yolo`。owner 检查使用已有创建者记录,不新增授权或 owner;无法确定创建者时拒绝回答。
135+
- `allow` 与 `deny` 是稳定的选项 ID。Action 提供两个版本、function call ID、工具名称及过期时间;参数从 Items 读取。每个 Turn 同时最多有一个 Hosted 审批待处理。列表只返回 `requested` Action,按从新到旧排序,不在本地将它们标记为过期。
135136

136137
Turn 在等待期间仍读作 `running`;让客户端知道它在等待的,是它未决的 Action。
137138

138139
### 6.4 Java 配置
139140

140-
开启 Workspace 文件的部署可以设置 `default` 或 `auto-edit`。Java 随模式一起发送部署级的审批超时,默认仍为 `yolo`。对于带工具配置的 Session,Java 记录创建时发送的模式,只有当 Harness 在创建和每次加载的回答中都以 `approvalMode` 报告这个模式时才使用该 Session:D6a 之前的 Harness 不带这个字段,它会忽略模式,不经询问就运行每个调用。
141+
开启 Workspace 文件的部署可以设置 `default` 或 `auto-edit`。Java 随模式一起发送 `QWEN_MANAGED_AGENT_APPROVAL_TIMEOUT`(默认 `10m`,范围 `1s` 至 `24h`),默认模式仍为 `yolo`。对于带工具配置的 Session,Java 记录创建时发送的模式,只有当 Harness 在创建和每次加载的回答中都以 `approvalMode` 报告这个模式时才使用该 Session:D6a 之前的 Harness 不带这个字段,它会忽略模式,不经询问就运行每个调用。
141142

142143
## 7. 测试
143144

144145
- **D6a:** 针对第一轮审批 Turn 绑定的核心测试。使用假模型的 Hosted 测试:被允许的写入会执行;被拒绝的写入返回拒绝结果,模型继续;混合批次只执行被允许的调用;过期的审批拒绝该调用;被中止的 Turn 取消其 Action 且不再询问;重复同一决定返回同样的结果;其他决定与迟到的决定返回 `409`;`auto-edit` 模式下拒绝 Shell 调用后编辑仍会执行;过期的审批让该 Turn 不再询问;journal 写入失败会立即阻塞 Session;其他地方的写入失败会在一秒内被察觉;恢复阻塞的 Session 回答已记录的内容,且不受理阻塞前已排队的决定或过期写入;取消请求会释放 Workspace;加载时保持并报告已保存的模式。
145-
- **D6b:** 在 H2 与 MySQL 上的投影与路由测试,owner 与非 owner 的回答、重放、过期,以及契约测试在两个入口上的请求;还有一个 Hosted 进程测试,由 owner 通过公共 API 回答,Turn 完成。
146+
- **D6b:** 在 H2 与 MariaDB(MySQL 驱动)上的投影与路由测试,owner 与非 owner 的回答、重放、过期,以及契约测试在两个入口上的请求;还有一个 Hosted 进程测试,由 owner 通过公共 API 与 WebShell 回答,Turn 完成。
146147

147148
## 8. 风险与后续工作
148149

‎packages/cli/src/serve/hosted-harness-session.test.ts‎

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,19 +2347,24 @@ describe('Hosted Harness no-tool session', () => {
23472347
});
23482348
const admitted = await send();
23492349
expect(admitted.status).toBe(202);
2350-
await vi.waitFor(async () => {
2351-
const transcript = await headers(
2352-
supertest(server).get(`/session/${SESSION_ID}/transcript`),
2353-
).set('X-Qwen-Client-Id', clientId);
2354-
expect(transcript.body.events).toEqual(
2355-
expect.arrayContaining([
2356-
expect.objectContaining({
2357-
type: terminalType,
2358-
promptId: PROMPT_ID,
2359-
}),
2360-
]),
2361-
);
2362-
});
2350+
// The default 1s waitFor timeout races the settlement retry's durable
2351+
// writes on contended CI runners; the assertions are unchanged.
2352+
await vi.waitFor(
2353+
async () => {
2354+
const transcript = await headers(
2355+
supertest(server).get(`/session/${SESSION_ID}/transcript`),
2356+
).set('X-Qwen-Client-Id', clientId);
2357+
expect(transcript.body.events).toEqual(
2358+
expect.arrayContaining([
2359+
expect.objectContaining({
2360+
type: terminalType,
2361+
promptId: PROMPT_ID,
2362+
}),
2363+
]),
2364+
);
2365+
},
2366+
{ timeout: 10_000 },
2367+
);
23632368
expect(failed).toBe(true);
23642369
expect(state.model).toHaveBeenCalledTimes(modelCalls);
23652370
const status = await headers(
@@ -2388,19 +2393,22 @@ describe('Hosted Harness no-tool session', () => {
23882393
payloadDigest: `sha256:${createHash('sha256').update(JSON.stringify(nextPrompt)).digest('hex')}`,
23892394
});
23902395
expect(next.status).toBe(202);
2391-
await vi.waitFor(async () => {
2392-
const transcript = await headers(
2393-
supertest(server).get(`/session/${SESSION_ID}/transcript`),
2394-
).set('X-Qwen-Client-Id', loaded.body.clientId as string);
2395-
expect(transcript.body.events).toEqual(
2396-
expect.arrayContaining([
2397-
expect.objectContaining({
2398-
type: 'turn_complete',
2399-
promptId: nextPromptId,
2400-
}),
2401-
]),
2402-
);
2403-
});
2396+
await vi.waitFor(
2397+
async () => {
2398+
const transcript = await headers(
2399+
supertest(server).get(`/session/${SESSION_ID}/transcript`),
2400+
).set('X-Qwen-Client-Id', loaded.body.clientId as string);
2401+
expect(transcript.body.events).toEqual(
2402+
expect.arrayContaining([
2403+
expect.objectContaining({
2404+
type: 'turn_complete',
2405+
promptId: nextPromptId,
2406+
}),
2407+
]),
2408+
);
2409+
},
2410+
{ timeout: 10_000 },
2411+
);
24042412
expect(state.model).toHaveBeenCalledTimes(modelCalls + 1);
24052413
await headers(supertest(server).delete(`/session/${SESSION_ID}`));
24062414
},

0 commit comments

Comments
 (0)