Skip to content

Commit 92a4eca

Browse files
authored
fix(managed-agent): Declare the tenant filter's 403 on the task read routes (#12966)
* fix(managed-agent): Declare the tenant filter's 403 on the task read routes The tenant filter answers 403 actor_scope_mismatch on every /v1/agents and WebShell route when an authenticated actor does not belong to the tenant. The Session, lifecycle, operation and Turn routes declare it, but the task list, detail and event routes did not, and the task contract note said that the read routes declare no 403. Declare it on the six task read routes of both surfaces, say in the shared Forbidden response that it covers this code, and move the contract to v1.21.0. The contract test probes the 403 on the four served task routes. The note also records the A9 decision: the Legacy paused and pausing states map to waiting, and TaskState gains no state. Part of #12847. * fix(managed-agent): Pin the planned task routes' 403 and the filter prefix The contract test probes the 403 only on mapped routes, so nothing checked the declarations on the planned task event routes. A new check in PlannedTaskContractTest requires the 403 on the planned task event and cancel routes of both surfaces. The tenant filter matches the /v1/agents/ prefix with its trailing slash, so the collection path POST /v1/agents skips it. Say /v1/agents/ in the Forbidden description and the v1.21 note, as the design note does. * docs(managed-agent): Name both 403 triggers and the filter's 400 The tenant filter answers 403 actor_scope_mismatch for an actor from another tenant and for an actor with an invalid ID; the Forbidden description named only the first. The task contract note now names both, lists the filter's 400 invalid_tenant beside it, and records the sixth PlannedTaskContractTest test in sections 5 and 6.
1 parent ab097b9 commit 92a4eca

6 files changed

Lines changed: 86 additions & 16 deletions

File tree

‎docs/design/2026-09-27-managed-agent-task-contract.md‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,25 +269,33 @@ declare.
269269
Errors keep `ErrorEnvelope` and the shared `BadRequest`, `Forbidden`,
270270
`NotFound`, `Conflict` and `CursorExpired` responses. The codes are those the
271271
API contract already froze, `invalid_idempotency_key`, which the idempotent
272-
routes already return, and three new task codes:
272+
routes already return, the tenant filter's `invalid_tenant` and
273+
`actor_scope_mismatch`, and three new task codes:
273274

274275
| Status | Code | When |
275276
| ------ | ------------------------- | ---------------------------------------------------------------------------------------- |
277+
| `400` | `invalid_tenant` | `X-Qwen-Tenant-Id` is missing or malformed (tenant filter). |
276278
| `400` | `invalid_cursor` | The task list cursor is malformed. |
277279
| `400` | `invalid_event_cursor` | `after` is malformed or belongs to another task. |
278280
| `400` | `invalid_limit` | `limit` is outside 1 to 100. |
279281
| `400` | `invalid_request` | `Idempotency-Key` is missing. |
280282
| `400` | `invalid_idempotency_key` | `Idempotency-Key` is malformed, as on the other idempotent routes. |
281283
| `400` | `unsupported_feature` | The Session does not serve tasks (`capabilities.tasks` is `false`). |
282284
| `403` | `task_forbidden` | The caller can read the task but may not cancel it. New. |
285+
| `403` | `actor_scope_mismatch` | The authenticated actor belongs to another tenant or has an invalid ID (tenant filter). |
283286
| `404` | `session_not_found` | The Session is absent or outside the caller's scope. |
284287
| `404` | `task_not_found` | The task is absent or outside the caller's scope. New. |
285288
| `409` | `cursor_expired` | `after` is older than the retained events. |
286289
| `409` | `task_action_unavailable` | A new key while `action_capabilities` lacks `cancel`, which includes settled tasks. New. |
287290
| `409` | `idempotency_conflict` | The key was used with a different request. |
288291

289292
A caller that cannot read a task gets `404`, not `403`, as API contract
290-
section 10 requires, so the read routes declare no `403`; only cancel does. `cursor_expired` leaves the envelope's
293+
section 10 requires. The only `403` a read route answers is the tenant
294+
filter's `actor_scope_mismatch`, for an authenticated actor from another
295+
tenant or with an invalid ID. The filter covers every `/v1/agents/` and
296+
WebShell route; the task read routes declare it from `1.21.0`, as the
297+
Session and Turn reads do, and cancel adds `task_forbidden`. `cursor_expired`
298+
leaves the envelope's
291299
`replay_floor_sequence` and `snapshot_through_sequence` absent, because task
292300
cursors are opaque. An output event expires only after its text is in an
293301
Artifact, so a caller that first reads the retained events from the start and
@@ -318,7 +326,9 @@ one shape per event type, the event versions, list and page cursors, and the
318326
and, except the public-only `object` check, checked again, renamed to
319327
camelCase, against the WebShell mirror, so a
320328
conditional copied wrongly into one surface fails the test. The WebShell
321-
cancel and event query requests are checked as well.
329+
cancel and event query requests are checked as well. From `1.21.0` it also
330+
requires the tenant filter's `403` on the four `planned` task routes, which
331+
`ManagedAgentApiContractTest` cannot probe.
322332

323333
## 6. Validation
324334

@@ -327,7 +337,8 @@ cancel and event query requests are checked as well.
327337
`managed-agent-api.test.ts` passes.
328338
- `ManagedAgentApiContractTest` (5 tests), `PlannedTaskContractTest` (5
329339
tests, 103 validations: 50 public, 49 WebShell mirrors and 4 WebShell
330-
requests) and `ManagedSessionStoreContractFixtureTest` (3 tests) pass
340+
requests; `1.21.0` adds a sixth test, see section 5) and
341+
`ManagedSessionStoreContractFixtureTest` (3 tests) pass
331342
without new gap lines.
332343
- Mutations fail the matching gate:
333344
- Removing, on one surface, the conditionals of the task, the task event,
@@ -368,8 +379,11 @@ cancel and event query requests are checked as well.
368379
`artifact_refs` cannot be tied back to its task. The Artifact slices (O2,
369380
O4) should add one of the two before a task can rotate that many.
370381
- **Legacy states.** The daemon's task status includes `paused`, and
371-
workflow runs add `pausing`; `TaskState` has neither. The adapter slice (H3
372-
or H4) maps them, most likely to `waiting`, or the design adds a state.
382+
workflow runs add `pausing`; `TaskState` has neither. Decided in #12847
383+
(A9): the adapter slice (H3 or H4) maps both to `waiting`, and `TaskState`
384+
gains no state. H0c made `TaskState` `partial` with its eight values, and
385+
under section 5 of the API contract a new value would now be a breaking
386+
change.
373387
- **Later additions.** Query filters (`kind`, `state`), a `send_input` route
374388
and any display label are additive `planned` changes. `SessionTaskView`
375389
has no title; the first slice that renders tasks in WebShell should decide

‎docs/design/2026-09-27-managed-agent-task-contract.zh-CN.md‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,25 +202,29 @@ operation 模型;本变更选择接受它可见。
202202
### 4.7 错误
203203

204204
错误沿用 `ErrorEnvelope` 以及共用的 `BadRequest`、`Forbidden`、`NotFound`、`Conflict` 和
205-
`CursorExpired` 响应。错误码包括 API 契约已冻结的那些、幂等路由已在返回的 `invalid_idempotency_key`,
206-
以及三个新增的任务错误码:
205+
`CursorExpired` 响应。错误码包括 API 契约已冻结的那些、幂等路由已在返回的 `invalid_idempotency_key`、
206+
租户过滤器的 `invalid_tenant` 与 `actor_scope_mismatch`,以及三个新增的任务错误码:
207207

208208
| 状态 | 错误码 | 何时返回 |
209209
| ----- | ------------------------- | ------------------------------------------------------------------------ |
210+
| `400` | `invalid_tenant` | 缺少 `X-Qwen-Tenant-Id` 或格式错误(租户过滤器)。 |
210211
| `400` | `invalid_cursor` | 任务列表游标格式错误。 |
211212
| `400` | `invalid_event_cursor` | `after` 格式错误或属于另一个任务。 |
212213
| `400` | `invalid_limit` | `limit` 不在 1~100 之间。 |
213214
| `400` | `invalid_request` | 缺少 `Idempotency-Key`。 |
214215
| `400` | `invalid_idempotency_key` | `Idempotency-Key` 格式错误,与其他幂等路由相同。 |
215216
| `400` | `unsupported_feature` | 该 Session 不提供任务(`capabilities.tasks` 为 `false`)。 |
216217
| `403` | `task_forbidden` | 调用方可以读取该任务,但无权取消它。新增。 |
218+
| `403` | `actor_scope_mismatch` | 已认证的 actor 属于其他租户或其 ID 非法(租户过滤器)。 |
217219
| `404` | `session_not_found` | Session 不存在或不在调用方范围内。 |
218220
| `404` | `task_not_found` | 任务不存在或不在调用方范围内。新增。 |
219221
| `409` | `cursor_expired` | `after` 早于保留的事件。 |
220222
| `409` | `task_action_unavailable` | 使用新键时 `action_capabilities` 不含 `cancel`,包括已结算的任务。新增。 |
221223
| `409` | `idempotency_conflict` | 同一个键用于不同的请求。 |
222224

223-
按 API 契约第 10 节,无权读取任务的调用方收到 `404`,而不是 `403`,所以只读路由不声明 `403`,只有取消声明。`cursor_expired`
225+
按 API 契约第 10 节,无权读取任务的调用方收到 `404`,而不是 `403`。只读路由唯一会返回的 `403` 是租户过滤器的
226+
`actor_scope_mismatch`,针对来自其他租户或 ID 非法的已认证 actor。过滤器覆盖每条 `/v1/agents/` 与 WebShell 路由;任务只读路由从
227+
`1.21.0` 起声明它,与 Session 和 Turn 的读取路由一致,取消路由另有 `task_forbidden`。`cursor_expired`
224228
的错误封装中 `replay_floor_sequence` 和 `snapshot_through_sequence` 保持缺省,因为任务游标是不透明的。
225229
输出事件只有在其文本已进入 Artifact 后才会过期,因此调用方先从头读完保留的事件、再读取任务的 Artifact,
226230
不会漏掉任何输出:在读取事件开始之前过期的每个事件,都已在那之前进入 Artifact。反过来的顺序可能漏掉在两次读取之间
@@ -240,14 +244,15 @@ operation 模型;本变更选择接受它可见。
240244
任务不变式、禁止字段、每种事件类型只有一种结构、事件版本、列表与分页游标,以及 `task_cancel` operation
241245
(包括经由 `PublicOperation` 和 `WebShellOperation` union 的校验)。每个实例按公共形状只写一次,
242246
除仅限公共形状的 `object` 检查外,再改名为 camelCase 后针对 WebShell 镜像校验一遍,因此条件约束在某一个接口面抄错时测试会失败。
243-
WebShell 的取消请求和事件查询请求也在校验之列。
247+
WebShell 的取消请求和事件查询请求也在校验之列。从 `1.21.0` 起,它还要求四条 `planned` 任务路由声明租户过滤器的
248+
`403`,因为 `ManagedAgentApiContractTest` 无法探测它们。
244249

245250
## 6. 验证
246251

247252
- 在 `packages/web-shell` 中运行 `npm run generate:managed-agent-api`,
248253
`client/components/managed/generated/managed-agent-api.ts` 没有变化,`managed-agent-api.test.ts` 通过。
249254
- `ManagedAgentApiContractTest`(5 个测试)、`PlannedTaskContractTest`(5 个测试,103 次校验:
250-
50 次公共形状、49 次 WebShell 镜像、4 次 WebShell 请求)和
255+
50 次公共形状、49 次 WebShell 镜像、4 次 WebShell 请求;`1.21.0` 增加了第六个测试,见第 5 节)和
251256
`ManagedSessionStoreContractFixtureTest`(3 个测试)通过,没有新增 gap 行。
252257
- 变异都会使对应门禁失败:
253258
- 在同一个接口面上删除任务、任务事件、任务列表的条件约束、`task_cancel` 规则以及输出最小长度后,
@@ -275,7 +280,9 @@ WebShell 的取消请求和事件查询请求也在校验之列。
275280
所以 `artifact_refs` 中最新 100 个之外的 Artifact 无法追溯到其任务。Artifact 切片(O2、O4)
276281
应在任务能轮转出这么多 Artifact 之前加上两者之一。
277282
- **Legacy 状态。** daemon 的任务状态包括 `paused`,workflow 运行还有 `pausing`;`TaskState`
278-
两者都没有。适配切片(H3 或 H4)负责映射它们(最可能映射为 `waiting`),或由设计增加一个状态。
283+
两者都没有。已在 #12847(A9)决定:适配切片(H3 或 H4)把两者都映射为 `waiting`,`TaskState`
284+
不增加状态。H0c 已把 `TaskState` 连同其八个值标为 `partial`,按 API 契约第 5 节,此后再增加
285+
一个值就是破坏性变更。
279286
- **后续新增。** 查询过滤(`kind`、`state`)、`send_input` 路由以及显示标签都是增量的 `planned`
280287
变更。`SessionTaskView` 没有标题;第一个在 WebShell 中渲染任务的切片应决定是否需要它。
281288

0 commit comments

Comments
 (0)