@sema-agent/sdk 6.6.1 → 6.7.0

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
@@ -515,6 +515,15 @@ paths:
515
515
  errorCode "steering.not_running" — the run is terminal, is running on ANOTHER replica
516
516
  (cross-replica live-steer is not wired), or the durable park lost its CAS (the checkpoint
517
517
  resolved/expired in between). NOT the ordinary suspended case — that one parks and 202s.
518
+ errorCode "steering.queue_full" (server >=7.3.0 / core 5.14.0) — the run IS parked, but the
519
+ steering queue on its checkpoint is full. The queue is bounded and FAIL-LOUD: the engine never
520
+ evicts an already-accepted instruction to make room. Opposite handling from not_running: the
521
+ text is still valid — let the run resume (which drains the queue) or reduce concurrent steers,
522
+ then resend.
523
+ errorCode "steering.duplicate_input_id" (server >=7.3.0 / core 5.14.0) — the Idempotency-Key you
524
+ sent is already parked with DIFFERENT steering content (same key + same content is an idempotent
525
+ no-op and 202s). The engine refuses rather than silently picking a side. Reissue with a fresh
526
+ key — this is a new instruction, not a retry.
518
527
  content:
519
528
  application/json:
520
529
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -2800,6 +2809,97 @@ paths:
2800
2809
  '404': { description: Unknown/settled/expired/foreign approval (no existence oracle). }
2801
2810
  '501': { description: Live tool-approval HITL not enabled (TOOL_APPROVAL_ENABLED). }
2802
2811
 
2812
+ /v1/tasks/{taskId}/asks/{askId}/decision:
2813
+ parameters:
2814
+ - $ref: '#/components/parameters/PrincipalHeader'
2815
+ - $ref: '#/components/parameters/TaskIdPath'
2816
+ - { name: askId, in: path, required: true, schema: { type: string }, description: 'From ApprovalRequestFrame.askId (the durable identity, NOT approvalId).' }
2817
+ post:
2818
+ tags: [questions]
2819
+ operationId: askDecision
2820
+ x-status: planned # design/172 §3.2 (#151 车4) — UNRELEASED; ships with server 7.3.0, gated OFF by STREAM_APPROVAL_ENABLED. SDK toolApprovals.decideAsk() 消费。
2821
+ summary: Settle an IN-STREAM approval card (design/172 streaming approval protocol).
2822
+ description: >
2823
+ The in-stream sibling of POST /v1/tool-approvals/{id}/respond: the approver is live on the SSE stream, so
2824
+ the ask does a stream round-trip and the run never parks. Keyed on `askId` (the DURABLE identity derived
2825
+ from a five-tuple), so the decision is accepted by ANY replica (the CAS lands in the persistent store).
2826
+
2827
+ Decision is a TWO-word set (`approve`/`deny`) — `allow_session` is a live-bridge-only semantic and is NOT
2828
+ on this endpoint. `actor` accepts ONLY `label`: `id`/`verified` are always server-minted and an actor
2829
+ NEVER participates in the permission decision (design/171 §5.1).
2830
+
2831
+ Response doctrine (design/172 §3.2, four rules): ① same `idempotencyKey` retried → the ORIGINAL 200
2832
+ replayed (no second CAS); ② already DECIDED with a DIFFERENT decision → 409 `conflict.ask_decided`
2833
+ carrying the first decision echo (`decision`/`decidedAtMs`/`actor`; `note` is NEVER echoed across
2834
+ permission boundaries); ③ PARKED/DENIED/VOID → 410 `gone.ask_parked` (with the gate reconciliation
2835
+ coordinates `{sessionId, gateBoundCallId, gateBoundInputHash}` — NEVER the checkpoint token) or
2836
+ `gone.ask`; ④ PARKING → 425 `parking.ask_retry` — retrying fetches the FINAL ROUTING INFO, it does NOT
2837
+ retry acceptance of the decision (the window is already closed).
2838
+
2839
+ Attribution split: DENIED is produced ONLY by routing failure (`routing_failure_fail_closed`); an
2840
+ operator refusal is DECIDED(decision=deny). A timeout NEVER produces a deny.
2841
+ requestBody:
2842
+ required: true
2843
+ content:
2844
+ application/json:
2845
+ schema: { $ref: '#/components/schemas/AskDecisionBody' }
2846
+ responses:
2847
+ '200':
2848
+ description: The decision was accepted (or replayed for the same idempotencyKey).
2849
+ content:
2850
+ application/json:
2851
+ schema: { $ref: '#/components/schemas/AskDecisionAck' }
2852
+ '400':
2853
+ description: 'Malformed body — `request.invalid_json` (unparseable) or `request.body_shape` (schema reject, e.g. an `actor` carrying anything but `label`).'
2854
+ content:
2855
+ application/json:
2856
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2857
+ '401': { $ref: '#/components/responses/Unauthorized' }
2858
+ '404':
2859
+ description: >
2860
+ Unknown askId / taskId mismatch / non-owner — ONE shape, NO existence oracle (`not_found.ask`).
2861
+ NOT the cross-leg case: a park+resume mints a NEW askId, and the OLD row survives in its terminal
2862
+ state, so deciding against the old askId gets that row's terminal answer (200 idempotent replay /
2863
+ 409 first-decision echo / 410) — the "no consent is ever reused across legs" guarantee comes from
2864
+ the new leg having its own independent row, not from the old one 404-ing.
2865
+ content:
2866
+ application/json:
2867
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2868
+ '409':
2869
+ description: >
2870
+ Already settled with a DIFFERENT decision (`conflict.ask_decided`, first-decision echo in the body),
2871
+ or the idempotencyKey belongs to another ask (`conflict.ask_idempotency`).
2872
+ content:
2873
+ application/json:
2874
+ schema: { $ref: '#/components/schemas/AskDecidedConflictBody' }
2875
+ '410':
2876
+ description: >
2877
+ The window is closed and the outcome is already cast: `gone.ask_parked` (gate coordinates in the
2878
+ body — route to the durable gate flow) or `gone.ask` (`{state, deniedReason?}`). The two legs carry
2879
+ DISJOINT bodies.
2880
+ content:
2881
+ application/json:
2882
+ schema: { $ref: '#/components/schemas/AskGoneBody' }
2883
+ '425':
2884
+ description: >
2885
+ PARKING — transferring to the delivery plane. `parking.ask_retry`; the retry fetches FINAL ROUTING
2886
+ INFO, never acceptance. The body carries `retryAfterSec` and NO gate coordinates (they are not
2887
+ written until bind).
2888
+ headers:
2889
+ Retry-After:
2890
+ schema: { type: integer }
2891
+ description: 'Seconds to wait before polling again for the final routing info. Paired with the body''s `retryAfterSec`.'
2892
+ content:
2893
+ application/json:
2894
+ schema: { $ref: '#/components/schemas/AskParkingBody' }
2895
+ '501':
2896
+ description: >
2897
+ In-stream approval protocol not on this worker (`feature.approval_ask_disabled`) — no ask store, or
2898
+ STREAM_APPROVAL_ENABLED is off. Probe the capability, do not trial-by-501.
2899
+ content:
2900
+ application/json:
2901
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
2902
+
2803
2903
  /v1/side-query:
2804
2904
  parameters:
2805
2905
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -4173,14 +4273,30 @@ components:
4173
4273
  type: string
4174
4274
  enum: [default, plan, acceptEdits, bypassPermissions, auto]
4175
4275
  description: >
4176
- CC permission-mode INTENT (the five CC modes verbatim, post-[816]/[820]/[822]). The frontend (TUI shell /
4177
- web portal) carries the RAW mode; the SERVICE interprets it TIGHTEN-ONLY vs the deployment baseline:
4276
+ CC-parity permission-mode INTENT — the five values THIS wire's closed submit-gate accepts (post-[816]/
4277
+ [820]/[822]; not a claim these are CC's/sema-cli's complete mode vocabulary, which is wider, e.g.
4278
+ `dontAsk`). The frontend (TUI shell / web portal) carries the RAW mode; the SERVICE interprets it TIGHTEN-ONLY vs the deployment baseline:
4178
4279
  `plan` ⇒ mount present_plan (core enablePlanMode) + read-only hands (CC EnterPlanMode research);
4179
- `default` adds the manual ask gate; `auto` = same ask gate with core's auto-mode classifier screening
4180
- asks upstream (entitlement-gated in core); `acceptEdits`/`bypassPermissions` are honored as
4181
- "add less/no mode-derived gating" — they can never subtract from the deployment/operator policy
4182
- (deny-wins). An unknown string coerces to `default` (the most-asking mode). Carrying the intent
4183
- (not pre-interpreted core fields) = one interpretation across frontends + service-owned governance.
4280
+ `default` adds the manual ask gate on non-exempt writes (scratchpad dirs / remembered session
4281
+ exemptions still auto-allow — not literally every Write/Edit); the server emits the SAME gate-policy
4282
+ for `auto` as for `default` — sending `auto` is NOT itself what arms core's auto-mode classifier; that
4283
+ classifier is armed purely by the principal's entitlement (`runtimeCaps.autoMode===true` plus a
4284
+ deployment `deps.autoMode` wiring), independent of which mode word the request sends: an entitled
4285
+ principal's `default` request is screened too, and a non-entitled principal's `auto` request behaves
4286
+ as plain `default`; `acceptEdits` adds the SAME gate narrowed to auto-allow inside the working
4287
+ directory — but only when the service has a trusted working directory to scope it to; on a sandbox
4288
+ execution lane with none wired, `acceptEdits` degrades to asking on every non-exempt write, same
4289
+ as `default`; `bypassPermissions` adds no mode-derived gate at all. None of the five can ever subtract
4290
+ from the deployment/operator policy (deny-wins). Omitting this field is NOT itself "no mode-derived
4291
+ gate" — this field wins over a `settings.permissions.defaultMode` bundle when both are sent, but when
4292
+ THIS field is omitted the bundled mode (if any) still supplies one; only when neither carrier supplies
4293
+ a mode is there no mode-derived gate (the same posture as `bypassPermissions`). Sending this field is
4294
+ also NOT the same as leaving it unset — an explicit `default` actively installs the ask gate. A fresh
4295
+ submit with an unknown string is rejected fail-loud with 400 (sibling of `promptProfile`); the old
4296
+ silent most-asking-mode fallback no longer applies to fresh submits — a lenient version of that
4297
+ fallback survives only internally, for RESUME replay of a pre-gate persisted
4298
+ body. Carrying the intent (not pre-interpreted core fields) = one interpretation across frontends +
4299
+ service-owned governance.
4184
4300
  attachmentIds:
4185
4301
  type: array
4186
4302
  maxItems: 16
@@ -4259,6 +4375,17 @@ components:
4259
4375
  type: string
4260
4376
  enum: [simple, classic]
4261
4377
  description: Which prompt-assembly profile to use.
4378
+ toolMaterializeStrategy:
4379
+ type: string
4380
+ enum: [static, swap]
4381
+ description: >
4382
+ server >=7.3.0 (core 5.14.0, board [2856]2) — how a DEFERRED tool (see `deferTools`) supplies its
4383
+ schema AFTER ToolSearch activates it. `static` keeps the placeholder (activation only flips
4384
+ callability; the advertised schema stays the empty object); `swap` advertises the REAL schema on the
4385
+ next request. A provider whose decoding is CONSTRAINED by the advertised schema loops UNBOUNDED under
4386
+ `static` (every round emits `{}` and the next round still advertises an empty schema), which is why
4387
+ this is a per-task knob. OMIT to inherit the engine chain (`spec ?? env ?? static`) — sending an
4388
+ explicit value takes that decision away from the deployment env lane. Unknown value => 400 fail-loud.
4262
4389
  clientContext:
4263
4390
  type: object
4264
4391
  description: Non-authoritative client hints (never trusted for authz).
@@ -4276,8 +4403,11 @@ components:
4276
4403
  description: >
4277
4404
  TOC `settings.json` contract carried to the worker (CC-parity v1: permissions/hooks/env/model/
4278
4405
  outputStyle). The SDK owns the schema in `src/settings.ts` (`SemaSettings`) — it is deliberately NOT
4279
- duplicated here as a component: the service passes it through to core rather than interpreting it, so
4280
- the TS type is the single authority. OPEN object.
4406
+ duplicated here as a component to keep ONE schema authority instead of two drifting copies. The
4407
+ service ACTIVELY interprets this bundle (task-settings.ts: permissions + permissions.defaultMode +
4408
+ model + outputStyle + env shipped in the 1.26.0 v1 scope; hooks was deferred at that point and wired
4409
+ in a later batch — both are live and interpreted today, not a raw uninterpreted passthrough);
4410
+ `permissions.defaultMode`'s specific fold is documented on `TaskRequest.permissionMode`. OPEN object.
4281
4411
  additionalProperties: true
4282
4412
  cwd: { type: string, description: Working directory for the execution env. }
4283
4413
  selfOrchestration: { type: boolean, description: Permit S8 self-orchestration (run_workflow) inside this task. }
@@ -7010,6 +7140,9 @@ components:
7010
7140
  - $ref: '#/components/schemas/Event_elicitation'
7011
7141
  - $ref: '#/components/schemas/Event_elicitation_complete'
7012
7142
  - $ref: '#/components/schemas/Event_workflow_complete'
7143
+ # [2854] core 5.14.0 TaskEvent 16->18 (server 7.3.0 pickup): both new arms, projected.
7144
+ - $ref: '#/components/schemas/Event_human_input'
7145
+ - $ref: '#/components/schemas/Event_wiring_manifest'
7013
7146
  discriminator:
7014
7147
  propertyName: type
7015
7148
  mapping:
@@ -7046,6 +7179,8 @@ components:
7046
7179
  elicitation: '#/components/schemas/Event_elicitation'
7047
7180
  elicitation_complete: '#/components/schemas/Event_elicitation_complete'
7048
7181
  workflow_complete: '#/components/schemas/Event_workflow_complete'
7182
+ human_input: '#/components/schemas/Event_human_input'
7183
+ wiring_manifest: '#/components/schemas/Event_wiring_manifest'
7049
7184
 
7050
7185
  # design/99 §E2 (SHIPPED, core TaskEventIdentity) — additive per-event identity on every CONTENT event (NOT on
7051
7186
  # the lifecycle arms turn_end/compacted/suspended/done/failed). eventId = uuidv7 dedup/resume handle (the durable
@@ -7321,6 +7456,13 @@ components:
7321
7456
  properties:
7322
7457
  type: { const: task_progress }
7323
7458
  taskId: { type: string, description: 'The subagent task id (the delegated sub-run).' }
7459
+ taskType:
7460
+ type: string
7461
+ description: >
7462
+ server >=7.3.0 (core 5.14.0, board [2854] six-car batch item 2) — the DELEGATION KIND discriminator
7463
+ (`background_agent` | `workflow`), so a consumer routes a progress frame without inferring the kind
7464
+ from the id shape. ABSENT on a synchronous child's frames (absence means "no fleet row" — tolerate
7465
+ it). Open on read.
7324
7466
  usage:
7325
7467
  type: object
7326
7468
  description: 'Cumulative usage as of this turn boundary (closed 3-field set, byte-aligned to core).'
@@ -7339,6 +7481,121 @@ components:
7339
7481
  parentToolCallId: { type: string }
7340
7482
  sourceTaskId: { type: string }
7341
7483
  bgAgentId: { type: string }
7484
+ Event_human_input:
7485
+ type: object
7486
+ description: >
7487
+ server >=7.3.0 (core 5.14.0 design/171) — HUMAN INPUT LIFECYCLE: who fed this run what, and whether it
7488
+ landed. One frame per carrier (objective / live steer / nextTurn / parked-steer resume / wake). It carries
7489
+ NO body text: this is the attribution + delivery ledger, not the message (join content through
7490
+ `message_committed` + stream order). Live emissions PRECEDE the commit and omit `entryId`.
7491
+ SERVICE PROJECTION: core's `principal` is deliberately NOT forwarded (the stream is already owner-scoped —
7492
+ the frame does not restate the tenant). `issuer` / `actor.id` / `actor.issuer` are host-derived identity
7493
+ strings and arrive SECRET-REDACTED.
7494
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
7495
+ additionalProperties: false # 封闭 + identity 本地镜像:见 Event_reasoning 处的长注
7496
+ required: [type, inputId]
7497
+ properties:
7498
+ type: { const: human_input }
7499
+ inputId: { type: string, description: 'Stable per-input id (also the durable steering queue idempotency key).' }
7500
+ sessionSeq: { type: integer, description: 'Leg-scoped monotone sequence of human inputs.' }
7501
+ carrier: { type: string, description: 'Which carrier delivered it (steer / objective / a source word). Open on read.' }
7502
+ source: { type: string, description: 'objective | steer | next_turn | wake | external | system. Open on read.' }
7503
+ delivery: { type: string, description: 'applied | queued | parked_for_wake. Open on read.' }
7504
+ issuer: { type: string, description: 'Who asserted the identity (host-derived; REDACTED).' }
7505
+ actor:
7506
+ type: object
7507
+ description: >
7508
+ Speaker attribution — NEVER authority. `hostAsserted:false` means the host did NOT derive the identity
7509
+ from ingress credentials; render it as unverified (core's own `[from ...]` renderer appends
7510
+ " (unverified)" on exactly this bit). `id`/`issuer` arrive REDACTED.
7511
+ additionalProperties: false
7512
+ properties:
7513
+ id: { type: string }
7514
+ hostAsserted: { type: boolean }
7515
+ issuer: { type: string }
7516
+ entryId: { type: string, description: 'The committed session entry this input became (absent on the live pre-commit emission).' }
7517
+ eventId: { type: string }
7518
+ parentToolCallId: { type: string }
7519
+ sourceTaskId: { type: string }
7520
+ bgAgentId: { type: string }
7521
+ Event_wiring_manifest:
7522
+ type: object
7523
+ description: >
7524
+ server >=7.3.0 (core 5.14.0 design/173) — ASSEMBLY SELF-EVIDENCE: what this leg actually wired, emitted at
7525
+ prepare. Root / child / resume legs each emit their OWN manifest (a delegated child's rides the CHILD's
7526
+ stream by design). SERVICE PROJECTION: the `governance` section core stamps `audience:"operator"` is
7527
+ STRIPPED by the server before this frame reaches a tenant stream (core contract: "no projection => do not
7528
+ disclose") — it is deliberately absent from this schema. `configFingerprint` is ALSO stripped for the same
7529
+ reason: core hashes the whole manifest (governance included) unsalted, and governance is four booleans —
7530
+ sixteen combinations a tenant could brute-force against everything else it already receives, recovering the
7531
+ section that was removed. Both come back on the operator-scoped projection, not here.
7532
+ allOf: [{ $ref: '#/components/schemas/EventIdentity' }]
7533
+ additionalProperties: false # 封闭 + identity 本地镜像:见 Event_reasoning 处的长注
7534
+ required: [type, manifest]
7535
+ properties:
7536
+ type: { const: wiring_manifest }
7537
+ manifest:
7538
+ type: object
7539
+ additionalProperties: false
7540
+ properties:
7541
+ schemaVersion: { type: integer }
7542
+ leg:
7543
+ type: object
7544
+ additionalProperties: false
7545
+ properties: { kind: { type: string, description: 'root | child | resume. Open on read.' } }
7546
+ ask:
7547
+ type: object
7548
+ additionalProperties: false
7549
+ description: 'Approval seam: how it is wired, where it came from, and what it effectively resolves to.'
7550
+ properties:
7551
+ form: { type: string }
7552
+ provenance: { type: string, description: 'spec | deps. Open on read.' }
7553
+ effective: { type: string }
7554
+ question:
7555
+ type: object
7556
+ additionalProperties: false
7557
+ description: >
7558
+ Question channel — three-valued (wired / absent / stripped_bg_lane, the last meaning the engine
7559
+ stripped a bg child per-request face). `interactiveToolsWithoutDeliveryFace` is the composition-lie flag.
7560
+ properties:
7561
+ wired: { type: string }
7562
+ provenance: { type: string }
7563
+ interactiveToolsWithoutDeliveryFace: { type: boolean }
7564
+ interaction:
7565
+ type: object
7566
+ additionalProperties: false
7567
+ properties: { posture: { type: string, description: 'interactive | headless | absent. Open on read.' } }
7568
+ elicit:
7569
+ type: object
7570
+ additionalProperties: false
7571
+ properties:
7572
+ seamWired: { type: boolean }
7573
+ serversOptedIn: { type: integer }
7574
+ parkLane:
7575
+ type: object
7576
+ additionalProperties: false
7577
+ description: >
7578
+ Durable park lane: capability vs EFFECTIVE policy. `effective` is THREE-VALUED — `"unresolved"`
7579
+ means the engine has not decided yet; never fold it to false.
7580
+ properties:
7581
+ capable: { type: boolean }
7582
+ effective: { oneOf: [{ type: boolean }, { type: string }] }
7583
+ reasons: { type: array, items: { type: string } }
7584
+ checkpointDurability: { type: string }
7585
+ session:
7586
+ type: object
7587
+ additionalProperties: false
7588
+ properties: { store: { type: string } }
7589
+ fleet:
7590
+ type: object
7591
+ additionalProperties: false
7592
+ properties:
7593
+ backgroundAgentStore: { type: boolean }
7594
+ hostChildEventSink: { type: boolean }
7595
+ eventId: { type: string }
7596
+ parentToolCallId: { type: string }
7597
+ sourceTaskId: { type: string }
7598
+ bgAgentId: { type: string }
7342
7599
  Event_workspace_changed:
7343
7600
  type: object
7344
7601
  description: >
@@ -7705,6 +7962,313 @@ components:
7705
7962
  enum: [allowed, denied, expired]
7706
7963
  description: '"tool_approval_complete" only. `expired` = TTL/abort/disconnect — a fail-closed DENY the shell should render as such, not as "still pending".'
7707
7964
 
7965
+ # ── design/172 流内审批协议(#151;UNRELEASED,候 server 7.3.0 + STREAM_APPROVAL_ENABLED,默认 OFF)──
7966
+ # 与上面的 ToolApprovalFrame 是**并行加帧,不替换**:开关打开时同一只 ask 出两帧(先 tool_approval,
7967
+ # 后 approval_request),两帧同带 `approvalId` ⇒ 消费端按它去重、优先渲染新帧。
7968
+ ApprovalRequestFrame:
7969
+ type: object
7970
+ description: >
7971
+ IN-STREAM approval card (SSE, `type` IS the event name; design/172 §3.1). NOT an AgentEvent arm — it
7972
+ interleaves on the live token stream like tool_approval/question/elicitation. Settle it via
7973
+ POST /v1/tasks/{taskId}/asks/{askId}/decision.
7974
+
7975
+ WINDOW: `expiresAtMs` is cast ONCE and never recomputed; `expiresInMs` = max(0, expiresAtMs - serverNowMs)
7976
+ and is therefore MONOTONICALLY DECREASING across replays — a reconnect NEVER renews the window.
7977
+
7978
+ REPLAY: on a new connection the server replays every STREAM_PENDING card as a PREAMBLE, and that preamble
7979
+ is the shell's FULL RECONCILIATION BASELINE — any local card not in it must be dropped (revoke frames can
7980
+ be lost across a process boundary; the preamble is the structural compensation). Historical approval
7981
+ frames replayed from the durable events tail are for TIMELINE RENDERING ONLY, not a card-set baseline
7982
+ (their `expiresInMs` is the value frozen when the frame was minted). Preamble frames carry no SSE `id:`
7983
+ and take no part in the Last-Event-ID cursor.
7984
+
7985
+ UNKNOWN SHAPES ARE NEVER AUTO-DENIED: an unknown schemaVersion/kind or an unrenderable card gets a
7986
+ GENERIC card (safety metadata + manual approve/deny); if it cannot be shown, keep the frame and tell the
7987
+ operator, letting the window fall through to park. Folding "I don't understand it" into a refusal is the
7988
+ deadlock this protocol exists to remove.
7989
+
7990
+ NOT EVERY ASK HAS A CARD: the non-streaming POST /v1/tasks (no delivery plane) and council/team subtask
7991
+ legs produce none; an adhoc (non-durable) sync stream's taskId is one-shot and has no replay handle; a
7992
+ background child's ask arriving after the host run ended is NOT thrown at the dead stream — it parks onto
7993
+ the durable gate (the existing inbox surface).
7994
+ required: [type, schemaVersion, askId, taskId, kind, card, expiresInMs, expiresAtMs, serverNowMs, approvalId]
7995
+ additionalProperties: true
7996
+ properties:
7997
+ type: { type: string, enum: [approval_request] }
7998
+ schemaVersion:
7999
+ type: integer
8000
+ enum: [1]
8001
+ description: >
8002
+ Frame shape version. THIS SCHEMA DESCRIBES v1 ONLY — a frame carrying any other version is not this
8003
+ shape; read it as ApprovalFrameEnvelope and render the generic card. NEVER auto-deny.
8004
+ askId:
8005
+ type: string
8006
+ description: >
8007
+ The DURABLE identity (derived from the five-tuple sourceTaskId/runId/toolCallId/legKey/
8008
+ parentToolCallId). Dismissal, decision and reconciliation all key on THIS. Deduplicate by it (the
8009
+ same frame may be delivered repeatedly). VOID AFTER A PARK and never reused across legs — a resume
8010
+ re-asks with a NEW askId, so "the thing you just approved may be asked again" is normal and the
8011
+ shell must explain it.
8012
+ taskId: { type: string, description: 'wire run id — the {taskId} segment of the decision path.' }
8013
+ kind:
8014
+ type: string
8015
+ enum: [permission]
8016
+ description: >
8017
+ v1 closed set. `content` is a reserved slot, not implemented (design/172 §8③). A frame with any
8018
+ other kind is not this shape — read it as ApprovalFrameEnvelope and render the generic card.
8019
+ card: { $ref: '#/components/schemas/ApprovalCard' }
8020
+ expiresInMs: { type: integer, description: 'Remaining budget = max(0, expiresAtMs - serverNowMs). Monotonically decreasing across replays; the window is never renewed.' }
8021
+ expiresAtMs: { type: integer, description: 'Absolute server-clock deadline. Cast ONCE, never recomputed.' }
8022
+ serverNowMs: { type: integer, description: 'Server clock at mint time — the shell corrects its local clock skew with it.' }
8023
+ approvalId:
8024
+ type: string
8025
+ description: >
8026
+ Bridge to the legacy frame / dedupe key — same value as the parallel tool_approval frame. During the
8027
+ transition window POST /v1/tool-approvals/{approvalId}/respond still works.
8028
+
8029
+ ApprovalCard:
8030
+ type: object
8031
+ description: >
8032
+ The card payload — a NEUTRAL PROJECTION (design/172 §6.7: the server neither restates nor rewrites; the
8033
+ shell owns rendering). UNTRUSTED FOR DISPLAY: `message`/`args`/`sourceAgentName`/`delegation.agentName`
8034
+ are server-redacted and length-bounded but remain model/tool-authored bytes — render only, never re-feed
8035
+ to a model. `args` has a 16 KiB BYTE cap (UTF-8 bytes, not characters); over the cap it is omitted with
8036
+ `argsOmitted: true` and the shell must render an elision stub, NOT conclude "no arguments".
8037
+ required: [toolName, message, risk]
8038
+ additionalProperties: true
8039
+ properties:
8040
+ toolName: { type: string, description: 'Canonical core tool name.' }
8041
+ message: { type: string, description: 'The engine ask text, secret-redacted. UNTRUSTED for display.' }
8042
+ args: { description: 'The pending call''s args, redacted + byte-bounded. Absent (with argsOmitted) when over the cap or unserializable. UNTRUSTED for display.' }
8043
+ argsOmitted: { type: boolean, enum: [true], description: 'Present (true) only when args exceeded the byte cap. Never false.' }
8044
+ toolCallId: { type: string, description: 'The engine''s tool-call id (the `call_…` on the assistant message''s tool_use block) — same domain as ToolApprovalFrame.toolCallId.' }
8045
+ risk: { $ref: '#/components/schemas/ApprovalRiskAxes' }
8046
+ fromSubagent: { type: boolean, enum: [true], description: 'Present ⇔ the ask comes from a DELEGATED child — the explicit discriminator (a worker cannot forge it).' }
8047
+ sourceTaskId: { type: string, description: 'The child''s core session id — row-join value + fallback discriminator.' }
8048
+ sourceAgentName: { type: string, description: 'The child agent''s display name, redacted. UNTRUSTED.' }
8049
+ delegation: { $ref: '#/components/schemas/ApprovalCardDelegation' }
8050
+
8051
+ ApprovalRiskAxes:
8052
+ type: object
8053
+ description: >
8054
+ The two risk axes plus the coarse safety marker (design/172 §3.1; SDK ApprovalRiskAxes).
8055
+
8056
+ 🔴 THREE-STATE, NOT TWO: true / false / ABSENT = the engine did not annotate it. The shell must render
8057
+ absence as "not annotated" and MUST NOT fold it to false — rendering "unknown" as "safe" is exactly the
8058
+ failure this contract forbids. The axes are optional because core exposes them additively on AskRequest,
8059
+ so an older engine/server omits them entirely.
8060
+
8061
+ `requiresRealApproval` is the ONE marker core always carries ("this is a safety-class ask" — the gate
8062
+ that refuses blanket-allow). It is NOT a substitute for, nor the conjunction of, the two axes: folding
8063
+ "deletes data" and "sends data out" into a single boolean erases the basis for the human decision.
8064
+ required: [requiresRealApproval]
8065
+ additionalProperties: true
8066
+ properties:
8067
+ irreversible: { type: boolean, description: 'The call is irreversible. ABSENT = not annotated — do not read as false.' }
8068
+ egress: { type: boolean, description: 'The call sends data across a boundary. ABSENT = not annotated — do not read as false.' }
8069
+ requiresRealApproval: { type: boolean, description: 'Coarse safety-class marker (core AskRequest.requiresRealApproval). Always present.' }
8070
+
8071
+ ApprovalCardDelegation:
8072
+ type: object
8073
+ description: 'Delegation provenance for a child ask (present only alongside fromSubagent).'
8074
+ required: [parentToolCallId, depth]
8075
+ additionalProperties: true
8076
+ properties:
8077
+ parentToolCallId: { type: string, description: 'The host-side delegating tool-call id — the card''s attribution anchor.' }
8078
+ depth: { type: integer, description: 'Delegation depth (a direct child of the host = 1).' }
8079
+ agentName: { type: string, description: 'Child agent name. UNTRUSTED — spawning-model free text (server-redacted and length-bounded).' }
8080
+
8081
+ ApprovalRevokeFrame:
8082
+ type: object
8083
+ description: >
8084
+ BATCH-LEVEL card revocation (SSE; design/172 §3.3). A batch = the blast radius of ONE run-level abort
8085
+ (every in-flight tool call of that run plus its synchronous delegation subtree). On receipt the shell
8086
+ clears that batch's local cards (whitelist-pick by `askIds`) and waits for the new askIds a resume mints.
8087
+
8088
+ Siblings already DECIDED are NOT in `askIds` (an accepted decision is not affected by revocation).
8089
+ Revoke frames CAN BE LOST (frames produced by the reconciler / orphan legs have no seq to allocate and
8090
+ ride the live plane only) — the structural compensation is the open-stream preamble baseline (see
8091
+ ApprovalRequestFrame), not a redelivery of this frame.
8092
+ required: [type, schemaVersion, batchId, askIds, reason, serverNowMs]
8093
+ additionalProperties: true
8094
+ properties:
8095
+ type: { type: string, enum: [approval_revoke] }
8096
+ schemaVersion: { type: integer, enum: [1], description: 'This schema describes v1 only (see ApprovalFrameEnvelope for the unknown-version read).' }
8097
+ batchId: { type: string }
8098
+ askIds: { type: array, items: { type: string }, description: 'The asks revoked (moved to VOID) by this frame.' }
8099
+ reason:
8100
+ type: string
8101
+ description: >
8102
+ `superseded_by_park` = a sibling''s window expired and the whole batch degraded to the durable park;
8103
+ `aborted` = the run was cancelled. Read as OPEN: an unknown reason is handled as a generic revoke
8104
+ (clear the cards), never specially.
8105
+ serverNowMs: { type: integer }
8106
+
8107
+ AskDecidedConflictBody:
8108
+ allOf:
8109
+ - $ref: '#/components/schemas/ErrorResponse'
8110
+ - type: object
8111
+ properties:
8112
+ decision:
8113
+ allOf: [{ $ref: '#/components/schemas/ApprovalAskDecision' }]
8114
+ description: 'FIRST-DECISION ECHO (`conflict.ask_decided` only): the decision that was accepted.'
8115
+ decidedAtMs: { type: integer, description: 'When the first decision was durably accepted.' }
8116
+ actor:
8117
+ allOf: [{ $ref: '#/components/schemas/ActorAssertion' }]
8118
+ description: 'Who decided. Omitted when the persisted actor fails its schema check (car4 §12-D).'
8119
+ description: >
8120
+ 409 body. `conflict.ask_decided` carries the FIRST-DECISION ECHO; `conflict.ask_idempotency` carries the
8121
+ bare `errorCode` (the key belongs to a different ask — change the key, not the ask).
8122
+ 🔴 `note` is NEVER echoed: the echo is bounded by the requester's visible permissions (design/172 §3.2).
8123
+ Retrying the same decision can never succeed — single-winner CAS means the outcome is already cast.
8124
+
8125
+ AskGoneBody:
8126
+ allOf:
8127
+ - $ref: '#/components/schemas/ErrorResponse'
8128
+ - type: object
8129
+ properties:
8130
+ sessionId: { type: string, description: 'gone.ask_parked ONLY — gate reconciliation coordinate 1; drive the existing durable-gate flow with it.' }
8131
+ gateBoundCallId: { type: string, description: 'gone.ask_parked ONLY — gate reconciliation coordinate 2.' }
8132
+ gateBoundInputHash: { type: string, description: 'gone.ask_parked ONLY — gate reconciliation coordinate 3.' }
8133
+ state: { type: string, enum: [DENIED, VOID], description: 'gone.ask ONLY — the terminal state.' }
8134
+ deniedReason:
8135
+ type: string
8136
+ description: >
8137
+ gone.ask ONLY — attribution, GROUPED BY `state`. `DENIED` carries the single-member deny
8138
+ vocabulary `routing_failure_fail_closed`; `VOID` carries the separate (and growing) void
8139
+ vocabulary, e.g. `adhoc_leg_no_durable_domain`. The two vocabularies never mix.
8140
+ description: >
8141
+ 410 body. THE TWO LEGS ARE DISJOINT: `gone.ask_parked` carries exactly the three gate coordinates,
8142
+ `gone.ask` carries exactly `{state, deniedReason?}`. Routing off field presence across the legs is how a
8143
+ consumer ends up driving the wrong durable gate.
8144
+ 🔴 The checkpoint token is NEVER here: the resume credential is not externally disclosed. Use
8145
+ `sessionId`.
8146
+ 🔴 ATTRIBUTION SPLIT: `DENIED` is produced ONLY by routing failure. An operator refusal is
8147
+ `DECIDED(decision="deny")` and reaches the caller as a 200 or as the 409 first-decision echo — never as
8148
+ `DENIED`. A timeout never produces a deny.
8149
+
8150
+ AskParkingBody:
8151
+ allOf:
8152
+ - $ref: '#/components/schemas/ErrorResponse'
8153
+ - type: object
8154
+ properties:
8155
+ retryAfterSec: { type: integer, description: 'Suggested lower bound before polling again; paired with the Retry-After header.' }
8156
+ description: >
8157
+ 425 body (`parking.ask_retry`). 🔴 NOT "retry may succeed": the window is already closed and the decision
8158
+ can never be accepted. The retry fetches the FINAL ROUTING INFO (poll until the 410 hands over the gate
8159
+ coordinates). Carries NO gate coordinates — they are not written until the batch binds.
8160
+
8161
+ ApprovalFrameEnvelope:
8162
+ type: object
8163
+ description: >
8164
+ The UNKNOWN-SHAPE read of either approval frame — the minimal common shape on the wire, and the typed
8165
+ counterpart of design/172's "unknown shapes are never auto-denied" clause.
8166
+
8167
+ ApprovalRequestFrame and ApprovalRevokeFrame describe **v1 only** (`schemaVersion: 1`, plus
8168
+ `kind: permission` on the request frame). Leaving those discriminants open would let a schema
8169
+ simultaneously say "any version may arrive" and keep promising the complete v1 `card` — which is exactly
8170
+ how a consumer ends up dereferencing `card.risk.requiresRealApproval` on a frame that never promised it.
8171
+ Read the frame as this envelope, narrow on the discriminants, and fall back to the generic card when it
8172
+ does not narrow.
8173
+ required: [type, schemaVersion]
8174
+ additionalProperties: true
8175
+ properties:
8176
+ type: { type: string, description: 'Echo of the SSE event name. Open: a third frame family may appear.' }
8177
+ schemaVersion: { type: integer, description: 'Frame shape version. Anything but 1 means this package does not describe the payload.' }
8178
+
8179
+ ApprovalAskDecision:
8180
+ type: string
8181
+ enum: [approve, deny]
8182
+ description: >
8183
+ The decision on an IN-STREAM approval card (design/172 §3.2). A TWO-word set — deliberately NOT the
8184
+ three-word ToolApprovalDecision: `allow_session` ("remember for this session") is a live-bridge-only
8185
+ semantic and does not exist on POST /v1/tasks/{taskId}/asks/{askId}/decision.
8186
+
8187
+ ActorAssertion:
8188
+ type: object
8189
+ description: >
8190
+ Decision-maker identity assertion (design/171 §5.1; the cross-repo source of truth is
8191
+ `@sema-agent/registry-core` ActorAssertionWire — the SDK holds a value copy).
8192
+
8193
+ NORMATIVE, not a style note: an ACTOR NEVER PARTICIPATES IN THE PERMISSION DECISION — authority always
8194
+ belongs to the principal; this shape exists only for audit/echo/presentation. `id` is ALWAYS server-minted
8195
+ (= the verified principal; an auth-off deployment mints the `"_"` sentinel), and a wire requester may
8196
+ submit ONLY `label`.
8197
+ required: [id]
8198
+ additionalProperties: false # registry-core's ActorAssertionWire is .strict()
8199
+ properties:
8200
+ id: { type: string, minLength: 1, maxLength: 256, description: 'Server-minted principal ("_" sentinel when auth is off). Never submittable by the requester.' }
8201
+ label: { type: string, maxLength: 256, description: 'Unverified display text — the ONLY field a requester may submit (server redacts + bounds it). UNTRUSTED.' }
8202
+ verified: { type: boolean, description: 'Whether the server minted this id under an authenticated context (auth-off ⇒ false, honestly). Never submittable.' }
8203
+ via:
8204
+ type: string
8205
+ enum: [owner, operator]
8206
+ description: >
8207
+ Audit axis for which authorization path accepted the decision: `owner` (row owner) or `operator`
8208
+ (explicit operator seat). CLOSED — verbatim-equal to the owning schema
8209
+ (registry-core ActorAssertionWire). Widening it here without widening the owner is copy drift.
8210
+
8211
+ AskDecisionBody:
8212
+ type: object
8213
+ description: 'Request body of POST /v1/tasks/{taskId}/asks/{askId}/decision (design/172 §3.2).'
8214
+ required: [decision]
8215
+ additionalProperties: false
8216
+ properties:
8217
+ decision: { $ref: '#/components/schemas/ApprovalAskDecision' }
8218
+ updatedInput:
8219
+ description: >
8220
+ Operator-edited args (WHOLE REPLACEMENT, not a patch). Valid on the approve arm only (leniently
8221
+ ignored on deny). Passed through verbatim: core re-runs PreToolUse + the full policy chain on the
8222
+ edit; the server interprets nothing. Whether it was forwarded shows up as AskDecisionAck
8223
+ .updatedInputForwarded.
8224
+ note:
8225
+ type: string
8226
+ maxLength: 2048
8227
+ description: >
8228
+ Audit note. NEVER echoed back on the 409 first-decision echo (the echo is bounded by the requester's
8229
+ visible permissions).
8230
+ idempotencyKey:
8231
+ type: string
8232
+ minLength: 1
8233
+ maxLength: 255
8234
+ description: >
8235
+ Decision idempotency key (<= 255). The same key retried replays the ORIGINAL 200 (no second CAS);
8236
+ the same key against a DIFFERENT askId is a 409 `conflict.ask_idempotency` — deliberately fail-loud,
8237
+ because replaying across asks would both mask a caller key-management bug and violate design/172''s
8238
+ "never reuse an old consent" clause.
8239
+ actor:
8240
+ type: object
8241
+ description: 'Display name of the decision-maker. ONLY `label` is accepted — id/verified are always server-minted; any extra key is a 400 request.body_shape.'
8242
+ required: [label]
8243
+ additionalProperties: false
8244
+ properties:
8245
+ label: { type: string, maxLength: 256 }
8246
+
8247
+ AskDecisionAck:
8248
+ type: object
8249
+ description: 'The 200 ack of POST /v1/tasks/{taskId}/asks/{askId}/decision (design/172 §3.2 rule ①; also the replay body for an idempotent retry).'
8250
+ required: [askId, taskId, decision, decidedAtMs]
8251
+ additionalProperties: false # server-composed envelope (car4 §6), not an engine passthrough — closed on purpose
8252
+ properties:
8253
+ askId: { type: string }
8254
+ taskId: { type: string }
8255
+ decision: { $ref: '#/components/schemas/ApprovalAskDecision' }
8256
+ decidedAtMs: { type: integer, description: 'Server instant at which the decision was durably accepted.' }
8257
+ actor:
8258
+ allOf: [{ $ref: '#/components/schemas/ActorAssertion' }]
8259
+ description: >
8260
+ OPTIONAL: when the persisted decision_actor fails its schema check the server OMITS this field
8261
+ rather than answering 500 (car4 §12-D). Never dereference `actor.id` without a presence check.
8262
+ updatedInputForwarded:
8263
+ type: boolean
8264
+ enum: [true]
8265
+ description: >
8266
+ Present (true) when the approve carried `updatedInput` AND a live in-process entry was actually
8267
+ settled with it (the server forwarded the edited args to the engine). 🔴 TRUE OR ABSENT, NEVER FALSE
8268
+ — so "no live entry (pure durable / another replica)" and "no edit sent" are indistinguishable on the
8269
+ wire. An idempotent replay does NOT promise byte-identity on this field: it is a function of THIS
8270
+ replica's live state and is not persisted.
8271
+
7708
8272
  QuestionFrame:
7709
8273
  type: object
7710
8274
  description: >