@sema-agent/sdk 6.16.0 → 7.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.yaml CHANGED
@@ -480,17 +480,38 @@ paths:
480
480
  parameters:
481
481
  - $ref: '#/components/parameters/PrincipalHeader'
482
482
  - $ref: '#/components/parameters/TaskIdPath'
483
+ # 🔴 #193 车8 P1-11(codex 交叉复审 R1 high,验真后采纳):此前**没声明**这个头,而 server 的 steer 段
484
+ # 真读它并 scoped+sha256 收成 core 的 `inputId`。它是 core 5.14.0 把 pendingSteer 队列化之后唯一能防
485
+ # 「202 丢在路上 → 无键重试 → 同一条操作员指令被再追加一条」的东西(队列只有 3 个位子,三次重试就满,
486
+ # 之后合法转向一律 409 `steering.queue_full`)。最刺眼的自证:本 op 的 409 臂里承诺的
487
+ # `steering.duplicate_input_id` 语义**完全依赖这个头** —— 不声明它,就是声明了一条谁也走不到的臂。
488
+ # ⚠️ 与 submit-class(`/v1/tasks`、`/v1/runs`)不同,这里的键**不是重试许可证**:steer 是
489
+ # AT-MOST-ONCE,SDK 不自动铸键也不因为有键就重试;它纯粹是服务端的去重令牌。
490
+ # ⚠️⚠️ **作用域只到 parked 两腿**(codex R2 复审报出,亲读 server 证实):只有
491
+ # `setPendingSteer(token, scope, steerInput())` 那两条腿把派生的 `inputId` 交给 core;live 腿走的是
492
+ # `live.steer(text, { trusted })`,**没传 inputId**,core 的 `TaskStream.steer()` 今天也不收 ——
493
+ # 所以 200 `applied` 腿**不去重**。这条限定写进下面 409 的描述里,别在别处把它说成整个 verb 的幂等。
494
+ - $ref: '#/components/parameters/IdempotencyKey'
483
495
  post:
484
496
  tags: [runs]
485
497
  operationId: runsSteer
486
- x-status: draft # design/80 D-A; core landing the slice (pendingSteer freeze/close-tag). Skeleton per the pinned wire contract.
498
+ x-status: live # #193 车8 P1-11(2026-08-15):draft 标签过期作废。它当初的理由是「core 正在落
499
+ # pendingSteer freeze/close-tag 这一刀」——那把守卫**早已在 server 侧落地**
500
+ # (`src/http/routes/runs.ts` 的 steer 段:200 applied / 202 queued+parked_for_wake /
501
+ # 409 三码 / 422 steering.invalid_content / Idempotency-Key→core inputId),SDK 自己的
502
+ # `resources/runs.ts` JSDoc 从 [2400] 起就写着「🔴 ONE narrow caveat (NOT "steer is a
503
+ # draft")」。唯一仍是 forward-draft 的是**per-call `mode` 参数**(core 的 steer 今天是
504
+ # harness 级),那条窄注记挂在下面 `mode` 字段自己的 description 上,而不是整个 verb ——
505
+ # 一个字段没接线不构成「整条面未定形」,而 draft 的语义是把它整条排除在契约断言集外。
487
506
  summary: Steer a RUNNING durable run — inject mid-flight direction (supervision).
488
507
  description: >
489
508
  design/80 D-A. The core of *supervision* (vs mere approve/deny): inject direction into a running run.
490
509
  AT-MOST-ONCE — steer is NOT idempotent (no retry). `trusted` is NOT a body field — the server derives it
491
510
  from the authenticated operator (the BFF's operator role); a network steer defaults to untrusted. A
492
- durable-`suspended` run → the steer is PARKED on the pending checkpoint and answered **202**
493
- `{status:"suspended", delivery:"queued"}`; it is injected when the run resumes. (Corrected 2026-07-31:
511
+ durably PARKED run (row status `suspended` OR `needs_review`) → the steer is PARKED on the pending
512
+ checkpoint and answered **202** `{status:<the row status the server read just before parking>,
513
+ delivery:"queued"}`; it is injected when the run resumes. (server 7.16.0: `status` echoes the REAL
514
+ row word — a plan-review park answers `"needs_review"`, not a hardcoded `"suspended"`.) (Corrected 2026-07-31:
494
515
  this paragraph used to say "409 — steer is not queued; resolve its approval instead", which the server
495
516
  stopped doing — `routes/runs.ts` parks it. 409 now only means the park LOST: the checkpoint resolved or
496
517
  expired between the read and the CAS.) A worker with no checkpoint store answers 501 instead.
@@ -529,12 +550,30 @@ paths:
529
550
  '202':
530
551
  description: >-
531
552
  Steer accepted and PARKED. Two shapes share this code — branch on `delivery`, not the status:
532
- a durable-`suspended` run parks on its pending checkpoint (`delivery:"queued"`) and drains on resume;
553
+ a durably PARKED run (`suspended`/`needs_review`) parks on its pending checkpoint (`delivery:"queued"`,
554
+ `status` = the real row word) and drains on resume;
533
555
  an ALREADY-ENDED run gets a freshly-minted `task_done` checkpoint (`delivery:"parked_for_wake"`) and
534
556
  the message is delivered only by POST /v1/sessions/{sessionId}/wake.
535
557
  content:
536
558
  application/json:
537
559
  schema: { $ref: '#/components/schemas/SteerReceipt' }
560
+ '400':
561
+ # 🔴 #193 车8 P1-11(2026-08-15):**整条 400 臂此前缺席** —— draft 把这条 op 排除在契约断言集外,
562
+ # 于是它攒下了三个从未进 spec 的真码。server 的 steer 段在 run 查找**之前**就跑完这三道校验
563
+ # (`src/http/routes/runs.ts`),所以 400 与运行态无关,live/suspended 上一模一样。
564
+ description: >-
565
+ THREE distinct machine codes share this status (`routes/runs.ts`, all validated BEFORE the run
566
+ lookup — so a 400 is your request's fault regardless of run state):
567
+ errorCode "request.invalid_json" — the body did not parse as JSON;
568
+ errorCode "request.body_shape" — `text` is missing, not a string, or empty (the message is the
569
+ literal accepted shape: `{ text: string (non-empty), mode?: 'all' | 'one-at-a-time',
570
+ priority?: 'now' | 'next' | 'later' }`);
571
+ errorCode "request.field_invalid" — `mode` or `priority` carried an off-enum value. FAIL-LOUD by
572
+ design: the server never coerces an unknown word to a default (a silently-defaulted `priority`
573
+ would look accepted and behave differently).
574
+ content:
575
+ application/json:
576
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
538
577
  '401': { $ref: '#/components/responses/Unauthorized' }
539
578
  '404': { $ref: '#/components/responses/NotFound' }
540
579
  '409':
@@ -551,14 +590,31 @@ paths:
551
590
  sent is already parked with DIFFERENT steering content (same key + same content is an idempotent
552
591
  no-op and 202s). The engine refuses rather than silently picking a side. Reissue with a fresh
553
592
  key — this is a new instruction, not a retry.
593
+ 🔴 SCOPE (#193 车8, codex R2 — read this before treating the key as verb-wide idempotency): this
594
+ code, and idempotency in general, exist ONLY on the PARKED legs (202 `queued` / `parked_for_wake`),
595
+ because only those thread the derived `inputId` into core via `setPendingSteer`. The 200 `applied`
596
+ leg calls core's `TaskStream.steer(text, {trusted})`, which takes NO inputId today — so it does
597
+ NOT dedupe: a lost 200 retried with the SAME key injects the instruction a SECOND time. Worse, the
598
+ receipt's `messageId` is derived from that key, so both receipts are byte-identical and the
599
+ duplicate looks like an idempotent hit. Treat a lost 200 as "outcome unknown", not "safe to retry".
554
600
  content:
555
601
  application/json:
556
602
  schema: { $ref: '#/components/schemas/ErrorResponse' }
557
603
  '413':
604
+ # 🔴 #193 车8 P1-11(2026-08-15)**改判**,依据=亲读 server 两处铸造点。原文写的是
605
+ # `steer.content_too_large` @ 256 KiB —— 那是**另一条腿**的事实:该码只在 subagent steer
606
+ # (`routes/runs.ts` 的子代 verb 区,判 `body.content.length > STEER_IN_MAX_REQUEST_CHARS` = 256 KiB)
607
+ # 与 workflow-agent steer(`routes/workflows.ts`)两处发出。**本条主 run steer 从不发它**:它的
608
+ # 413 来自 `readJson` 抛的 `HttpError(413, "request body too large")`(**不带 code**),经
609
+ # `httpErrorCode(413, undefined)`(`src/http/send.ts`)落成 `request.payload_too_large`,阈值是
610
+ # `MAX_BODY` = 8 MiB。按旧文案写 `errorCode === "steer.content_too_large"` 的消费方在这条腿上永不命中。
558
611
  description: >-
559
- errorCode "steer.content_too_large" — the steer body exceeded the server's request cap (256 KiB,
560
- server 1.279.2). REJECTED, never truncated: server-side truncation would make the engine's
561
- "[+N chars]" disclosure under-report. Shorten and resend; retrying the same bytes is pointless.
612
+ errorCode "request.payload_too_large" — the request body exceeded the worker's global body cap
613
+ (`MAX_BODY`, 8 MiB), raised by the shared JSON reader before this handler sees a `text` at all.
614
+ REJECTED, never truncated. Shorten and resend; retrying the same bytes is pointless.
615
+ ⚠️ NOT `steer.content_too_large` — that code (and its 256 KiB threshold) belongs to the SUBAGENT
616
+ steer `POST /v1/runs/{taskId}/subagents/{target}/steer` and the workflow-agent steer, which cap
617
+ the steer TEXT itself. This verb has no separate text cap.
562
618
  content:
563
619
  application/json:
564
620
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -568,6 +624,21 @@ paths:
568
624
  application/json:
569
625
  schema: { $ref: '#/components/schemas/ErrorResponse' }
570
626
  '429': { $ref: '#/components/responses/RateLimited' } # (declared 2026-07-31) rateLimited||quotaExceeded||leaseDenied all gate this verb (routes/runs.ts)
627
+ '501':
628
+ # 🔴 #193 车8 P1-11(2026-08-15):**臂缺席,而这条 op 自己的 description 早就在说 501**
629
+ #(「A worker with no checkpoint store answers 501 instead」)—— 散文说有、机器面没有,正是
630
+ # 生成式消费方结构性看不见的那一格。两个码来自两处不同的谓词,处置也不同(见描述)。
631
+ description: >-
632
+ TWO distinct capability codes share this status (`routes/runs.ts`):
633
+ errorCode "capability.run_store_required" — the worker wires no durable run store
634
+ (DB_BACKEND=mysql|pg), so the whole async-run family is off. Nothing about THIS request can fix it;
635
+ read the `capabilities` bit instead of probing.
636
+ errorCode "capability.checkpoint_store_required" — the run store IS wired and the run is durably
637
+ PARKED, but there is no checkpoint store to park the steer on. The live-run path (200 `applied`)
638
+ still works on such a worker — only the parked leg 501s.
639
+ content:
640
+ application/json:
641
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
571
642
 
572
643
  /v1/runs/{taskId}/subagents/{target}/steer:
573
644
  parameters:
@@ -2616,10 +2687,29 @@ paths:
2616
2687
  get:
2617
2688
  tags: [trace]
2618
2689
  operationId: traceStream
2619
- x-status: draft # trace relay (same source/semantics as runs.events); kept for the workspace live view.
2690
+ x-status: live # #193 车8 P2-15②(2026-08-15):trace relay,2026-06 起成熟在产(cli `-p` 腿消费,
2691
+ # live SSE 的 `id:` 游标行早已在发)。draft 标签过期作废——它的语义是「响应形还没定
2692
+ # 形、别过度断言」,而这条流的形自 `TraceStreamEvent` 那次修正(2026-07-25)起就已按
2693
+ # server 真码钉死(`routes/trace-usage.ts` 的 `streamTaskTrace` + `trace/project.ts`
2694
+ # 的 `mapTraceEvent`)。属主裁定翻 live,一并补齐 draft 免检期里漏掉的 416 臂与
2695
+ # `from` 续传参数(两者都真在 server 上,只是从没进机器可读面)。
2620
2696
  summary: Trace relay SSE (same source/semantics as runs.events).
2621
2697
  parameters:
2622
2698
  - $ref: '#/components/parameters/LastEventId'
2699
+ # 🔴 #193 车8 P2-15②(2026-08-15):server 的 resume 点是
2700
+ # `req.headers["last-event-id"] ?? searchParams.get("from")`(`routes/trace-usage.ts` 的
2701
+ # `streamTaskTrace`)—— **两个入口同源**,spec 此前只声明了 header 那一个。后果不是文档不全:一个
2702
+ # 不能自定义 SSE 请求头的消费方(浏览器原生 `EventSource` 就是这一类)照 spec 生成客户端时,
2703
+ # 结构上拿不到任何可用的续传入口,只能每次从头重放整条流。
2704
+ - name: from
2705
+ in: query
2706
+ required: false
2707
+ schema: { type: integer }
2708
+ description: >
2709
+ Resume cursor — the durable `task_event.seq` to replay AFTER (the same value an `id:` line
2710
+ carries). Equivalent to `Last-Event-ID`, which WINS when both are sent. For callers that cannot
2711
+ set request headers (native `EventSource`) this is the only resume entry. A non-numeric or
2712
+ negative value replays from the beginning (no error).
2623
2713
  responses:
2624
2714
  '200':
2625
2715
  # 🔴 修正(2026-07-25):此处原先 $ref 到 `AgentEvent`,是真错 —— 而且本文件自己就矛盾:专门为这条路写的
@@ -2638,6 +2728,20 @@ paths:
2638
2728
  schema: { $ref: '#/components/schemas/TraceStreamEvent' }
2639
2729
  '401': { $ref: '#/components/responses/Unauthorized' }
2640
2730
  '404': { $ref: '#/components/responses/NotFound' }
2731
+ '416':
2732
+ # 🔴 #193 车8 P2-15②(2026-08-15):**整条 416 臂此前缺席**,而这是 resumable 流的**正常**降级路径:
2733
+ # `streamTaskTrace` 在写 SSE 头**之前**判 `retainedFrom > from + 1` ⇒
2734
+ # `sendError(res, 416, "limit.retention_evicted", …, { retainedFrom })`。三处早就知道它
2735
+ #(server 真码 / `TraceStreamEvent` 的 description / SDK 的 `trace.stream()` 实现),唯独机器可读的
2736
+ # responses 里没有。姊妹面 `GET /v1/runs/{taskId}/events` 是正确形对照组,复用它的同一具名信封。
2737
+ description: >-
2738
+ errorCode "limit.retention_evicted" — the resume point (`Last-Event-ID` / `from`) was evicted past
2739
+ retention, so the gap cannot be replayed. Sent BEFORE any SSE header, so this is an ordinary JSON
2740
+ error response, not a stream frame. Read `retainedFrom`, full-sync the gap via
2741
+ `GET /v1/tasks/{taskId}/turns`, then resume the stream from `retainedFrom`.
2742
+ content:
2743
+ application/json:
2744
+ schema: { $ref: '#/components/schemas/ResumeEvicted' }
2641
2745
  '501': { $ref: '#/components/responses/NotImplemented' } # (declared 2026-07-31) `routes/trace-usage.ts`'s `!deps.runStore` guard fires before the /turns|/stream|/artifacts sub-dispatch — capability.run_store_required
2642
2746
 
2643
2747
  /v1/usage:
@@ -3389,7 +3493,9 @@ paths:
3389
3493
  operationId: imagesRegister
3390
3494
  x-status: live # operator face; entry shape = the image-index row (open, server-validated).
3391
3495
  summary: 'OPERATOR: register one image-index entry → { id }.'
3392
- requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3496
+ # 🔴 #193 车8 P1-12(2026-08-15):此前是裸开集 object(= 生成式消费方拿到 `any`),现挂具名
3497
+ # `ImageRegisterRequest`(字段集对 `routes/images.ts` 亲读,含每个键的 server 侧默认值)。
3498
+ requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/ImageRegisterRequest' } } } }
3393
3499
  responses:
3394
3500
  '200': { description: '{ id }.', content: { application/json: { schema: { type: object, required: [id], properties: { id: { type: string } }, additionalProperties: false } } } }
3395
3501
  '400': { $ref: '#/components/responses/BadRequest' }
@@ -3443,7 +3549,10 @@ paths:
3443
3549
  description: >
3444
3550
  Whitelisted-args validation (400/501); an identical-argv in-flight bake is idempotently reused (202);
3445
3551
  busy ⇒ 409 {activeBakeId?, eventsUrl?}; submission rate-limit ⇒ 429 + retryAfterSec.
3446
- requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3552
+ # 🔴 #193 车8 P1-12(2026-08-15):挂具名 `BakeCreateRequest`(字段集对 `images/bake-validate.ts` 的
3553
+ # `validateBake` 亲读)。一条 build-host-RCE-capable 的门用裸开集 object 描述请求体,与它自己的
3554
+ # 「每个字段都过冻结闭集」纪律直接矛盾。
3555
+ requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/BakeCreateRequest' } } } }
3447
3556
  responses:
3448
3557
  '202': { description: '{ bakeId, eventsUrl, status, state }.', content: { application/json: { schema: { $ref: '#/components/schemas/BakeSubmitAck' } } } }
3449
3558
  '400': { $ref: '#/components/responses/BadRequest' }
@@ -3555,7 +3664,9 @@ paths:
3555
3664
  x-status: live
3556
3665
  summary: 'RUNNER: push one build.sh event line + lease heartbeat (x-bake-ingest-secret header from the claim).'
3557
3666
  description: 'line = {event, …}; event=heartbeat renews the lease only. 409 = lease lost/stolen; cancelRequested=true ⇒ the runner should kill the build.'
3558
- requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: true } } } }
3667
+ # 🔴 #193 车8 P1-12(2026-08-15):挂具名 `BakeIngestLine`(字段集对 `routes/images.ts` 的
3668
+ # `ingestBakeLine` 亲读)。刻意保持**开集** —— 非终态帧的其余键是 build.sh 帧的逐字透传。
3669
+ requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/BakeIngestLine' } } } }
3559
3670
  responses:
3560
3671
  '200': { description: 'One of four shapes (heartbeat / non-terminal append / terminal done-success / terminal done-needs-flip) — see BakeIngestAck.', content: { application/json: { schema: { $ref: '#/components/schemas/BakeIngestAck' } } } }
3561
3672
  '403':
@@ -5322,11 +5433,15 @@ components:
5322
5433
  ResumeEvicted:
5323
5434
  type: object
5324
5435
  description: >
5325
- 416 body for an evicted SSE resume point (`GET /v1/runs/:id/events`). Carries the machine code
5326
- `errorCode: "limit.retention_evicted"`; matching the 416 STATUS alone is equally valid. Read
5327
- `retainedFrom`, then full-sync via `/v1/tasks/:id/turns` (pinned SSE contract).
5436
+ 416 body for an evicted SSE resume point. Shared by BOTH resumable streams — `GET /v1/runs/:id/events`
5437
+ (AgentEvent vocabulary) and `GET /v1/tasks/{taskId}/stream` (the block-grained trace vocabulary): the
5438
+ two handlers mint it through the SAME `sendError(…, 416, "limit.retention_evicted", …, {retainedFrom})`
5439
+ site shape, so one envelope serves both. Carries the machine code `errorCode:
5440
+ "limit.retention_evicted"`; matching the 416 STATUS alone is equally valid. Read `retainedFrom`, then
5441
+ full-sync via `/v1/tasks/:id/turns` (pinned SSE contract).
5328
5442
  (Corrected 2026-07-31 — see the 416 response note on that path: the "no machine code" claim predates
5329
- the site's move onto the shared `sendError`.)
5443
+ the site's move onto the shared `sendError`. Widened to both paths by #193 车8 P2-15②, 2026-08-15 —
5444
+ the trace stream had the same 416 arm in code all along, just never declared.)
5330
5445
  required: [error, errorCode, retainedFrom]
5331
5446
  properties:
5332
5447
  error: { type: string }
@@ -5429,6 +5544,52 @@ components:
5429
5544
  capabilities: { $ref: '#/components/schemas/ImageCapabilities' }
5430
5545
  manifestSha: { type: ['string', 'null'], description: 'the source entry''s manifestSha — nullable (ImageIndexEntry.manifestSha is string|null; was wrongly non-nullable here).' }
5431
5546
 
5547
+ ImageRegisterRequest:
5548
+ # 🔴 新(#193 车8 P1-12,2026-08-15)—— `POST /v1/images/register` 的请求体。此前这条 op 的 requestBody
5549
+ # 是裸 `{type: object, additionalProperties: true}`(SDK 侧对应 `Record<string, unknown>`):operator
5550
+ # 面上一个**打错的键名**既不会被 server 拒(白名单是「读已知键」,不是「拒未知键」),也不会被类型
5551
+ # 或校验器拦下 —— 它安静地落成该字段的默认值。字段集逐条对 `src/http/routes/images.ts` 的 register
5552
+ # 分支亲读:三个必填由 handler 显式 400(`request.field_invalid`),其余每一个都有 handler 里写死的默认值。
5553
+ #
5554
+ # ⚠️ `additionalProperties: false` 在这里是**客户端护栏**,不是 server 行为的镜像:server 并不拒收
5555
+ # 未知键,它只是**读不到就用默认值**。封闭声明的价值恰在于把那个静默 no-op 变成一个当场可见的错误。
5556
+ # ⚠️ `id` / `createdAt` / `updatedAt` 刻意不在场:handler 一个都不读(id 由 `idFor(repo,digest)` 确定性
5557
+ # 派生,两个时间戳由 store 写)。送它们是纯粹的误解,所以封闭形要让它们编译不过。
5558
+ type: object
5559
+ description: >
5560
+ `POST /v1/images/register` body — the operator-only image-index upsert (P1 seeding; the P3
5561
+ atomic-publish pipeline drives it for real). STATUS-MONOTONIC: `registerBuilding` never downgrades an
5562
+ already-`published` row, so this door cannot de-publish a live image (take-down is the explicit
5563
+ deprecate path). Everything except profile/repo/digest is optional with a server-side default.
5564
+ required: [profile, repo, digest]
5565
+ additionalProperties: false
5566
+ properties:
5567
+ profile: { type: string, description: 'REQUIRED — missing/non-string ⇒ 400 request.field_invalid.' }
5568
+ repo: { type: string, description: 'REQUIRED — same 400.' }
5569
+ digest: { type: string, description: 'REQUIRED — same 400. The immutable "sha256:<64hex>" this entry is keyed on.' }
5570
+ # ⚠️ 下面每一条 enum 都是**契约**,不是 server 的运行期校验:除 profile/repo/digest 三个必填外,
5571
+ # handler 对取值**不做集合校验** —— 一个打错的 `status: "publish"` 会原样落库,而
5572
+ # `latestPublished` 只认 `"published"` ⇒ 这张镜像从此对选择面隐形,且没有任何错误。
5573
+ # 这正是把这条体从裸开集 object 收成具名封闭形的理由。
5574
+ bands: { type: array, items: { type: string }, description: 'Absent / not-an-array ⇒ `[]`.' }
5575
+ tag: { type: string, description: 'Absent / non-string ⇒ `""`.' }
5576
+ toolchainVersions: { type: object, additionalProperties: { type: string }, description: 'Absent ⇒ `{}`.' }
5577
+ capabilities: { $ref: '#/components/schemas/ImageCapabilities' }
5578
+ podContract: { $ref: '#/components/schemas/ImagePodContract' }
5579
+ sizeBytes: { type: ['integer', 'null'], description: 'Anything that is not a number ⇒ `null` (no coercion).' }
5580
+ status: { type: string, enum: [building, published, deprecated, failed], description: 'Absent / non-string ⇒ `"building"`.' }
5581
+ visibility:
5582
+ type: string
5583
+ enum: [public, tenant]
5584
+ description: 'STRICT equality to `"tenant"` selects tenant; EVERY other value (including a typo) ⇒ `"public"`.'
5585
+ tenantId: { type: ['string', 'null'], description: 'Non-string ⇒ `null`.' }
5586
+ manifestSha: { type: ['string', 'null'], description: 'Non-string ⇒ `null`.' }
5587
+ recipeGitSha: { type: ['string', 'null'], description: 'Non-string ⇒ `null`.' }
5588
+ generatorVersion: { type: ['string', 'null'], description: 'Non-string ⇒ `null`.' }
5589
+ buildDate: { type: ['string', 'null'], description: 'ISO date-time string; non-string ⇒ `null`.' }
5590
+ supersedes: { type: ['string', 'null'], description: 'Version-lineage anchor; non-string ⇒ `null`.' }
5591
+ signed: { type: boolean, description: 'STRICT `=== true`; anything else (including the string "true") ⇒ `false`.' }
5592
+
5432
5593
  QuotaError:
5433
5594
  type: object
5434
5595
  description: >
@@ -6208,6 +6369,7 @@ components:
6208
6369
  type: array
6209
6370
  items: { type: string }
6210
6371
  description: 'E7 (SHIPPED) — the /effort picker default set (minimal/low/medium/high); present ONLY for a reasoning model.'
6372
+ atMentionable: { type: boolean, description: '#233/A-002.8 (SHIPPED) — @model mention allowlist verdict; present ONLY when the deployment configured an at-mention allowlist. Absent = no allowlist verdict, NOT "not mentionable" — do not narrow absence to false.' }
6211
6373
 
6212
6374
  ElicitResponse:
6213
6375
  type: object
@@ -7200,6 +7362,30 @@ components:
7200
7362
  toolName: { type: string }
7201
7363
  summary: { type: string }
7202
7364
  touchedPaths: {}
7365
+ # server 7.16.0 [3684]② — durable 队列行归因两键(与活卡 tool_approval 帧的同名键语义同族、判据不同源;
7366
+ # 两条腿对同一只 ask 的在场性可以不一致,两侧都只认在场、都禁把缺席读成否定)。
7367
+ ruleSuggestions:
7368
+ type: array
7369
+ minItems: 1
7370
+ items: { $ref: '#/components/schemas/RuleSuggestion' }
7371
+ description: >
7372
+ server 7.16.0 ([3684]②), ADDITIVE: the rule candidates minted for this ask, replayed off the durable
7373
+ row (checkpoint.rule_suggestions). DISPLAY-ONLY on this leg — the durable queue has no persistRule
7374
+ redemption; the only redemption path is the LIVE card leg (respond + persistRule). Array order =
7375
+ display order; an EXACT entry, when present, is always first. core contract caps candidates at 2;
7376
+ the server enforces a tolerance cap of 4 on read-back — lay out for 4. Absent = pre-column row
7377
+ (parked before the column existed) OR no candidates; render both as "no candidates", never an error.
7378
+ governanceForced:
7379
+ type: boolean
7380
+ enum: [true]
7381
+ description: >
7382
+ server 7.16.0 ([3684]②), ADDITIVE, present ONLY when true: this gate comes from the OPERATOR
7383
+ GOVERNANCE layer. Authority = `governanceOriginOf` (row evidence ∧ deployment posture conjunction),
7384
+ the single owner of BOTH the /v1/approvals row and the 409 body; the live card frame's
7385
+ `governanceForced` is the same family but a DIFFERENT source (in-process governance mark table), so
7386
+ presence may differ between the two legs for one ask. Derived from hot-reloadable governance knobs —
7387
+ long subscribers follow the stream's re-emitted frames. Absence ≠ "not governance-forced"; it means
7388
+ "no governance-origin evidence". Never written as false.
7203
7389
 
7204
7390
  # ── assistant-scheduler wire (ASSISTANT-WIRE-CONTRACT.md, service main 6cd0164 / core 1.110.0, LIVE) ──
7205
7391
  CheckpointSummary:
@@ -9981,6 +10167,153 @@ components:
9981
10167
  type: [string, 'null']
9982
10168
  enum: [band-conflict, duplicate, disk, timeout, cancelled, null]
9983
10169
 
10170
+ BakeCreateRequest:
10171
+ # 🔴 新(#193 车8 P1-12,2026-08-15)—— `POST /v1/images/bakes` 的请求体。此前是裸
10172
+ # `{type: object, additionalProperties: true}`。这条门是 §P2.4 的**反 RCE 核心**:bands/profile/
10173
+ # baseRef/bump 组合出一个 Dockerfile + 一个 FROM + 一次 docker build = build host 上的任意代码,所以
10174
+ # 每个字段在**任何 argv 被拼出来之前**就按冻结闭集校验(`src/images/bake-validate.ts` 的 `validateBake`)。
10175
+ # 一个连字段名都不具名的请求体和这条纪律是矛盾的:调用方连「哪些键会被看」都无从知道。
10176
+ # 字段集逐条对 `validateBake` 的 `BakeSubmit` 亲读,外加 `routes/images.ts` 自己读的 `idempotencyKey`。
10177
+ #
10178
+ # ⚠️ profile 与 bands 是 **XOR**(恰好给一个):两个都给 ⇒ 400「provide profile OR bands, not both」,
10179
+ # 一个都不给 ⇒ 400「one of profile | bands is required」。JSON Schema 表达不出「恰好一个」而不牺牲
10180
+ # 可读性,所以两者都标 optional 并在这里说清 —— 判据的属主是 server 的校验器,不是本文件。
10181
+ type: object
10182
+ description: >
10183
+ `POST /v1/images/bakes` body — the operator-only bake submit. Give EXACTLY ONE of `profile` | `bands`.
10184
+ Every value is checked against a FROZEN closed set server-side before any argv exists; an off-list
10185
+ value is a 400 (or a 501 for the reserved `custom` band = the P4 tenant affordance), NEVER coerced.
10186
+ The output repo/tag and the `FROM` base are server-fixed — a caller cannot set them.
10187
+ ⚠️ `additionalProperties: false` here is a CLIENT-SIDE guardrail, not a mirror of server behaviour:
10188
+ `validateBake` reads NAMED fields, so an unknown key is silently ignored rather than rejected — a
10189
+ typo'd knob is a wire no-op. Declaring the body closed is what turns that no-op into a visible error.
10190
+ additionalProperties: false
10191
+ properties:
10192
+ profile:
10193
+ type: string
10194
+ description: >
10195
+ One of the FROZEN profile catalog (`code-python` … `code-full-db`, mirrored from the recipe
10196
+ `gen-dockerfile.sh`). Unknown profile ⇒ 400. XOR with `bands`.
10197
+ bands:
10198
+ type: array
10199
+ items: { type: string }
10200
+ description: >
10201
+ Non-empty, duplicate-free list from the FROZEN band set (one dir per band under the recipe's
10202
+ `bands/`). XOR with `profile`; a `bands` bake records profile="custom". An off-list id ⇒ 400
10203
+ (with `conflictBands` for the composer UI); the reserved id `custom` ⇒ 501 "custom/tenant bake is P4".
10204
+ baseRef:
10205
+ type: string
10206
+ description: >
10207
+ An immutable `sha256:<64hex>` — and it must additionally be a KNOWN PUBLISHED `sandbox-base`
10208
+ digest the index has seen (400 `request.unknown_reference` otherwise). Absent ⇒ the server picks
10209
+ the base itself (`SANDBOX_BASE_REF` pin, else the index's latest published sandbox-base), which is
10210
+ why a caller can never inject an arbitrary FROM.
10211
+ bump:
10212
+ type: object
10213
+ additionalProperties: { type: string }
10214
+ description: >
10215
+ `{ tool: version }` — tool ∈ the FROZEN bump-tool set, version ∈ `^[A-Za-z0-9._-]{1,40}$`.
10216
+ Off-list tool or off-range version ⇒ 400.
10217
+ push: { type: boolean, description: 'STRICT `=== true`; anything else ⇒ false (node-local build, no registry push).' }
10218
+ dryRun: { type: boolean, description: 'STRICT `=== true`. Resolve-only: bypasses the single-flight busy guard and never takes the lease.' }
10219
+ logs: { type: boolean, description: 'STRICT `=== true`. Opt-in raw build-log tail rows on the event stream.' }
10220
+ pkgSource:
10221
+ type: string
10222
+ enum: [cn, global]
10223
+ description: 'Bake-time package-source profile (RFC B4). Closed set, never coerced; absent ⇒ build.sh default (cn).'
10224
+ idempotencyKey:
10225
+ type: string
10226
+ description: >
10227
+ Durable submit-idempotency, read ONLY when the `Idempotency-Key` HEADER is absent (the header
10228
+ wins). Scoped to the submitting principal server-side, so a replay dedupes for the SAME caller only.
10229
+
10230
+ BakeIngestLine:
10231
+ # 🔴 新(#193 车8 P1-12,2026-08-15)—— `POST /v1/images/bakes/{bakeId}/ingest` 的请求体(一行 build.sh
10232
+ # 事件)。此前是裸 `{type: object, additionalProperties: true}`;SDK 侧同样是 `Record<string, unknown>`,
10233
+ # 于是 runner 侧写错一个键名(`manifestSha` 打成 `manifest_sha`)在两侧都是静默的。
10234
+ # 字段集对 `routes/images.ts` 的 `ingestBakeLine` + ingest handler 亲读:handler 只在 `event` 上分派,
10235
+ # 终态 `done` 那一支另读 8 个键,非终态支把**剩余全部键**原样(经 redactDeep)存进事件 data。
10236
+ #
10237
+ # ⚠️ 这条**刻意保持开集**(`additionalProperties: true`,与上面两条请求体相反):非终态帧的 `...data`
10238
+ # 是 build.sh 帧字段的逐字透传,封闭它等于在这里发明一份 build.sh 没有承诺过的帧契约。
10239
+ type: object
10240
+ description: >
10241
+ One runner-forwarded build.sh event line. The handler dispatches on `event` alone; a
10242
+ `heartbeat` renews the lease and appends NO event, every other value appends one durable event whose
10243
+ data is the line MINUS `event`/`lineOrd`/`line_ord` (redacted). OPEN by design — build.sh frame fields
10244
+ ride through verbatim.
10245
+ required: [event]
10246
+ additionalProperties: true
10247
+ properties:
10248
+ event:
10249
+ type: string
10250
+ description: >
10251
+ build.sh's frame name — `resolved | state | step | image | manifest | log | done`, plus the
10252
+ runner-synthesized `heartbeat`. Open set: an unknown name is stored under that name as the SSE
10253
+ kind. A non-string ⇒ treated as `""` (still a non-terminal append), never an error.
10254
+ lineOrd: { type: integer, description: 'Per-line ordinal — the at-least-once dedupe key (uq_line). `line_ord` is accepted as a snake_case alias.' }
10255
+ line_ord: { type: integer, description: 'snake_case alias of `lineOrd` (the handler reads either).' }
10256
+ state:
10257
+ # 🔴 codex R2 复审后统一口径:**开集**,与 `BakeRecordView.state` 的闭集刻意不对称。请求侧镜像
10258
+ # **产者**的 wire —— server 自己的 `bake-runner/protocol.ts` 把它声明为 `state: string`,且
10259
+ # `setState` 落库只做 `as BakeState` 强转、不做集合校验;响应侧才镜像闭集域类型。
10260
+ type: string
10261
+ description: >-
10262
+ `event:"state"` only — denormalizes the UI progress bar. NEVER the coarse terminality (§P2.3).
10263
+ Today's values: PENDING | BUILDING | PUSHING | VERIFYING | REGISTERING | COMPLETE | FAILED |
10264
+ CANCELLED. OPEN on purpose: build.sh owns this vocabulary and the server stores it unvalidated,
10265
+ so a future ninth state must not make a legal frame fail validation.
10266
+ level: { type: string, description: '`event:"log"` only — the literal `"raw"` marks an opt-in build-log tail row (evicted first).' }
10267
+ status:
10268
+ # 🔴 自审修(#193 车8,提交后本车自查):这个键**跨帧种复用**,所以它绝不能是闭集 enum。
10269
+ # `done` 帧上是 COMPLETE|FAILED|CANCELLED(handler 唯一读它的地方);而 **`step` 帧上同名键**
10270
+ # 带的是 build.sh 的步骤状态(runner 的 `dockerBuildTickFrame` 就恒发 `status:"running"`,
10271
+ # server 自铸的 register step 发 running|ok|fail|skipped)。写死三值 enum ⇒ 每一条合法的
10272
+ # step 帧都被严格校验器判违约 —— 与本文件里 `Event_tool_end.errorCode` 那条「开集写 enum 比
10273
+ # 漏声明更糟」的裁定同族,方向一致。
10274
+ type: string
10275
+ description: >
10276
+ OVERLOADED across frame kinds — do NOT treat as a closed set. On `event:"done"` (the only place the
10277
+ handler reads it) it is the build outcome `COMPLETE` | `FAILED` | `CANCELLED`, and a non-string is
10278
+ treated as FAILED. On `event:"step"` frames the SAME key carries build.sh's step status
10279
+ (`running` / `ok` / `fail`), which the handler does not read — it rides through into the event data.
10280
+ exitCode:
10281
+ type: [integer, 'null']
10282
+ description: >-
10283
+ `event:"done"` only. Absent ⇒ 0 on COMPLETE, else null. 🔴 EXPLICITLY nullable: the runner's
10284
+ cancel arm (`bake-runner/protocol.ts`) sends `{status:"CANCELLED", exitCode: null}` verbatim.
10285
+ error:
10286
+ type: [string, 'null']
10287
+ description: >-
10288
+ `event:"done"` only — free text; secret-redacted server-side before storage. 🔴 EXPLICITLY
10289
+ nullable: the runner's push-recovery leg (`bake-runner/runner.ts`, exitCode===13) sends
10290
+ `{status:"COMPLETE", exitCode: 0, error: null}` verbatim.
10291
+ tag: { type: string, description: '`event:"done"` only; absent ⇒ the bake row''s existing tag.' }
10292
+ digest: { type: string, description: '`event:"done"` only — the built image digest (a no-push build sends build.sh''s node-local LOCAL_ID).' }
10293
+ manifestBody: { type: object, additionalProperties: true, description: '`event:"done"` only — the build manifest BODY the runner read off the build host (`manifest`/`done` carry only a host FS path). Required for auto-register.' }
10294
+ manifest:
10295
+ # 🔴 codex R2 复审报出、验真属实:build.sh 的 done 行带的是 manifest **路径字符串**
10296
+ # (`bake-runner/protocol.ts` 的 `manifestPathFromLine` 首条分支就是 typeof === "string"),
10297
+ # runner 逐字转发。声明成纯 object ⇒ 严格校验器拒收每一条真实 terminal ingest。
10298
+ oneOf:
10299
+ - { type: string }
10300
+ - { type: object, additionalProperties: true }
10301
+ description: >-
10302
+ EITHER build.sh's manifest PATH (a build-host filesystem string, what the real `done` line
10303
+ carries) OR the manifest body object. When it is an object it is the fallback source for the
10304
+ manifest body and its `sha256` is the fallback for `manifestSha`; when it is a string those two
10305
+ fallbacks simply do not fire (the server gates on `typeof manifestBody === "object"`).
10306
+ manifestSha: { type: string, description: '`event:"done"` only; falls back to `manifest.sha256`, then the bake row''s stored value.' }
10307
+ errorCode:
10308
+ type: string
10309
+ enum: [band-conflict, duplicate, disk, timeout, cancelled]
10310
+ description: >
10311
+ `event:"done"` only — a runner-supplied structured terminal code, PREFERRED over the exit-code
10312
+ recompute (the runner knows codes the exit code cannot reconstruct, e.g. the disk guard). Validated
10313
+ by set membership: an off-list value is DROPPED (never trusted as free text), not an error.
10314
+ conflictBands: { type: array, items: { type: string }, description: '`event:"done"` only — honored ONLY when the FINAL code is `band-conflict`; the composer UI red-highlights these chips.' }
10315
+ sizeBytes: { type: integer, description: '`event:"done"` only — rides the terminal done event; non-number ⇒ null.' }
10316
+
9984
10317
  BakeClaim:
9985
10318
  type: object
9986
10319
  description: >
@@ -10273,7 +10606,7 @@ components:
10273
10606
  `applied` (200, `routes/runs.ts`) — injected into the run LIVE on this replica, drains at the next
10274
10607
  turn boundary (`status:"running"`);
10275
10608
  `queued` (202) — the run is durably SUSPENDED, so the steer was PARKED on its pending checkpoint
10276
- and is injected on resume (`status:"suspended"`);
10609
+ and is injected on resume (`status` = the real row word: `suspended` or `needs_review`, server 7.16.0);
10277
10610
  `parked_for_wake` (202) — the run already ENDED; the server minted a `task_done` checkpoint and
10278
10611
  parked the message on it, delivered ONLY by POST /v1/sessions/{sessionId}/wake — nothing happens until
10279
10612
  the session is woken.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/sdk",
3
- "version": "6.16.0",
3
+ "version": "7.0.0-rc.1",
4
4
  "description": "Typed, zero-runtime-dependency SDK for the Sema agent fleet usage plane. The shared substrate for all doors (CC/Codex MCP façade + web). Server-side only — tokens never enter the browser.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",