@sema-agent/sdk 6.13.0 → 6.15.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
@@ -4084,6 +4084,140 @@ paths:
4084
4084
  schema: { $ref: '#/components/schemas/ErrorResponse' }
4085
4085
  '501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required
4086
4086
 
4087
+ # ── design/203 §2(server 7.12.0)—— 持久权限规则的**撤销面**两口。与上面的导入两口共用部署谓词
4088
+ # (PERMISSION_RULES_ENABLED + 规则店装配,缺一 ⇒ 501),但多一条 operator 越权域:`principal` 让一个
4089
+ # 列了名的 operator 读或收回**任一租户**的规则。方向决定授权 —— 收回是 TIGHTENING(core removePersistedRule
4090
+ # 头注逐字:a host may narrow on a user's behalf, it may not widen),而回收权限正是撤销面的本义。
4091
+ # 🔴 路径**没有 :id 段**:一条规则的身份就是它的 (rule, scope) 内容对,服务端不铸行 id,所以删是按内容删。
4092
+ /v1/rules:
4093
+ parameters:
4094
+ - $ref: '#/components/parameters/PrincipalHeader'
4095
+ get:
4096
+ tags: [rules]
4097
+ operationId: rulesList
4098
+ x-status: gated # server routes/rules.ts handleRuleList;同 cc-import 的部署谓词。SDK rules.list() 消费。
4099
+ summary: List the rules that are LIVE under a principal (keyset-paged, cursor bound to the revision).
4100
+ description: >
4101
+ List the persisted allow rules currently live under one principal (tombstones already folded).
4102
+ Ordering is deterministic — `(scope, rule)` lexicographic — because keyset paging's whole premise is
4103
+ that the same data comes back in the same order every time. The order is decided by the server, NOT
4104
+ by the store (core's `list()` promises none).
4105
+
4106
+ 🔴 THE CURSOR IS BOUND TO `(rev, principal, scope)` AND A MISMATCH IS REFUSED, NOT RESET. A rule set
4107
+ is read WHOLE, so there is no tearing WITHIN a page — tearing happens BETWEEN pages, when somebody
4108
+ adds or removes a rule after page 1 and the keyset start now lands in a different set (silently
4109
+ dropped or duplicated rows). Sending a cursor minted at another revision / principal / scope filter
4110
+ gets a 400 (`request.query_invalid`, body carries the current `rev`); restart the listing without a
4111
+ cursor. Silently starting over would let a client read page 2 as "continuing page 1" and hand a
4112
+ governance surface a list that is both short and duplicated, with nobody the wiser.
4113
+
4114
+ `scope` FILTER: given but unreadable ⇒ 400, never "treated as unfiltered" — that would silently turn
4115
+ "show me only this project" into a full listing the caller would accept as the project's list.
4116
+ `limit` is CLAMPED (not refused) into 1..200, default 50 — a mistyped paging knob must not make a
4117
+ governance page unopenable.
4118
+
4119
+ OPERATOR OVERRIDE: `principal` reads ANOTHER tenant's rules and is `OPERATOR_PRINCIPALS`-gated
4120
+ (an EMPTY list means NOBODY — never inferred from the service token, which is a shared deployment
4121
+ credential, not a person). Omitted, or equal to your own principal, is the ordinary self-read and
4122
+ needs no operator standing.
4123
+ parameters:
4124
+ - name: principal
4125
+ in: query
4126
+ required: false
4127
+ schema: { type: string, minLength: 1, maxLength: 190 }
4128
+ description: >
4129
+ OPERATOR-ONLY cross-tenant read. Absent (or equal to the caller's own principal) = read your own
4130
+ rules. Anything else requires explicit OPERATOR_PRINCIPALS membership ⇒ otherwise 403
4131
+ `auth.operator_only`. Rule text contains command shapes, so third-party reads are an information
4132
+ disclosure and are deliberately not opened to non-operators.
4133
+ - name: scope
4134
+ in: query
4135
+ required: false
4136
+ schema: { type: string, minLength: 1 }
4137
+ description: >
4138
+ Filter to one scope, spelled `global` or `project:<root>` with a NON-EMPTY root. Unreadable ⇒ 400
4139
+ `request.query_invalid` (never silently unfiltered). Matching is done on the CANONICAL form, so an
4140
+ equivalent-but-differently-spelled root filters to an empty page.
4141
+ - name: limit
4142
+ in: query
4143
+ required: false
4144
+ schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
4145
+ description: 'Page size. CLAMPED into 1..200 (non-numeric / out-of-range is clamped, not refused); default 50.'
4146
+ - name: cursor
4147
+ in: query
4148
+ required: false
4149
+ schema: { type: string }
4150
+ description: >
4151
+ The `nextCursor` from the previous page, VERBATIM (base64url, opaque — do not parse or mint one).
4152
+ It is bound to the revision, the principal and the scope filter it was minted on; any mismatch is
4153
+ a 400 `request.query_invalid` carrying the current `rev`. Restart the listing without a cursor.
4154
+ responses:
4155
+ '200':
4156
+ description: One page of live rules plus the store revision the page was read at.
4157
+ content:
4158
+ application/json:
4159
+ schema: { $ref: '#/components/schemas/RuleListResult' }
4160
+ '400': { $ref: '#/components/responses/BadRequest' } # request.query_invalid — unreadable `scope`, unreadable `cursor`, or a cursor minted at a different (rev, principal, scope). Body carries `rev` on the revision-mismatch arm.
4161
+ '401': { $ref: '#/components/responses/Unauthorized' }
4162
+ '403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only — reading another principal's rules without explicit OPERATOR_PRINCIPALS membership (an EMPTY list means nobody)
4163
+ '429': { $ref: '#/components/responses/RateLimited' }
4164
+ '501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required — this deployment has no permission-rule store wired
4165
+ delete:
4166
+ tags: [rules]
4167
+ operationId: rulesRevoke
4168
+ x-status: gated # server routes/rules.ts handleRuleRevoke;同 cc-import 的部署谓词。SDK rules.revoke() 消费。
4169
+ summary: Revoke a persisted rule BY CONTENT — `(rule, scope)`, with no id in the path.
4170
+ description: >
4171
+ Revoke one persisted allow rule identified by its CONTENT pair `(rule, scope)`. There is no `:id`
4172
+ path segment on purpose: a rule's identity IS that pair, the server mints no row id, and the engine's
4173
+ `removePersistedRule` primitive (which this endpoint always goes through) works in observed-remove
4174
+ form over the add dots.
4175
+
4176
+ 🔴 IDEMPOTENT, AND DELIBERATELY NOT A 404. Revoking twice, or revoking something that was never
4177
+ there, is `200 {status:"no-op"}` — a "did this rule ever exist?" 404 would be an existence oracle
4178
+ (a CROSS-TENANT one under the operator override), and deletion has no business answering that
4179
+ question. Read `status` to learn whether you removed anything.
4180
+
4181
+ 🔴 `stillLive` IS LOAD-BEARING ON BOTH ARMS. `true` means the tombstone landed but the rule is
4182
+ STILL live under add-wins — a fresh approval was recorded during the call. Folding that into a plain
4183
+ 200 would let a client read an INCOMPLETE revocation as complete, which is precisely what a
4184
+ governance surface must never get wrong. The `no-op` arm carries it for the same reason: the engine
4185
+ answers `no-op` off its initial snapshot without a read-back, so an approval landing in that window
4186
+ leaves the rule alive while the verb says "nothing to remove". Both arms therefore ship the same key
4187
+ set and one consumer branch reads both.
4188
+
4189
+ OPERATOR OVERRIDE: `principal` revokes on ANOTHER tenant's behalf, gated exactly like the read half
4190
+ (explicit `OPERATOR_PRINCIPALS`; empty list = nobody). The direction is what makes the override
4191
+ legitimate — revoking is TIGHTENING — but the identity test does not relax because of it.
4192
+ requestBody:
4193
+ required: true
4194
+ content:
4195
+ application/json:
4196
+ schema: { $ref: '#/components/schemas/RuleRevokeRequest' }
4197
+ responses:
4198
+ '200':
4199
+ description: >
4200
+ The revocation outcome. `removed` = a tombstone was written; `no-op` = nothing matched (idempotent
4201
+ repeat, or it was never there). BOTH arms carry `stillLive` — read it before telling a human the
4202
+ rule is gone.
4203
+ content:
4204
+ application/json:
4205
+ schema: { $ref: '#/components/schemas/RuleRevokeResult' }
4206
+ '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — body is not {rule, scope, principal?}, or `scope` is not `global`/`project:<non-empty root>`
4207
+ '401': { $ref: '#/components/responses/Unauthorized' }
4208
+ '403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only — revoking another principal's rule without explicit OPERATOR_PRINCIPALS membership
4209
+ '429': { $ref: '#/components/responses/RateLimited' }
4210
+ '503':
4211
+ description: >
4212
+ errorCode `state.rule_remove_failed` — the engine could not complete the removal right now. Core's
4213
+ contract for this arm is "a read-back confirmed nothing was written, OR the read-back itself failed
4214
+ and the outcome is undetermined" — both are RETRY-CAN-CHANGE-IT, which is why it is a 503 and not a
4215
+ 500. Retry, then reconcile with `GET /v1/rules`.
4216
+ content:
4217
+ application/json:
4218
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
4219
+ '501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required
4220
+
4087
4221
  # ── design/183 —— 身份收编面(form b:纯身份重绑)。operator-only 的一次性部署级动作,零模型工作。 ──
4088
4222
  /v1/adoption:
4089
4223
  parameters:
@@ -4820,6 +4954,35 @@ components:
4820
4954
  items: { $ref: '#/components/schemas/TaskAgentDefinition' }
4821
4955
  description: Per-task sub-agent definitions (roster additions scoped to this task only).
4822
4956
  interactiveTools: { type: boolean, description: Mount interactive (prompting) tools. }
4957
+ oneShot:
4958
+ type: boolean
4959
+ description: >
4960
+ core >=5.23.0 `TaskSpec.oneShot`, server >=7.12.0 — declare that THIS SUBMISSION is one-shot: no
4961
+ later turn exists in which an asynchronous background notification could land. The archetype is a
4962
+ headless `sema -p` whose process exits when the turn ends.
4963
+
4964
+ 🔴 IT GRANTS NOTHING AND GUARANTEES NOTHING — core consumes it as GUIDANCE ONLY. What changes is
4965
+ the wording the engine gives the MODEL for `RunWorkflow` / delegation receipts: "end your turn, you
4966
+ will be notified" (correct only when a next turn exists) becomes "block-wait via
4967
+ `TaskOutput({block:true})`". Omitting it on a headless call is a known way to LOSE background
4968
+ results — that is why the key exists — but sending it is **best-effort**, not a completion
4969
+ contract: the model may ignore the instruction or run out of budget, and some legs have no poll
4970
+ path at all. If your caller REQUIRES the results, keep your own wait/reconcile step; do not treat
4971
+ `oneShot: true` as one. There is no tenancy gate because there is no capability to gate.
4972
+
4973
+ 🔴 PROBE `capabilities.oneShot` BEFORE SENDING IT: the task body is an OPEN set (the server does
4974
+ not refuse unknown keys), so a worker that does not yet consume this field accepts `oneShot: true`,
4975
+ answers 200, and still tells the model to end its turn and await a notification the process will
4976
+ never receive — no 400, no warning. The capability bit (server >=7.12.0) is what distinguishes the
4977
+ two; ABSENT means the key is being ignored. Note the bit being PRESENT does not retire the
4978
+ caller-side wait step: it proves the key is consumed, not that the results arrive.
4979
+
4980
+ PER-REQUEST on purpose: "does this submission expect to be continued" is a property of the
4981
+ SUBMISSION, not of the connection it arrived on. Sibling of `interactiveTools` (the same `-p`
4982
+ posture) and passed through identically — a boolean rides, ABSENT ⇒ the key is omitted and core's
4983
+ default (interactive) applies. Anything that is neither absent nor a boolean is a fail-loud 400
4984
+ `request.field_invalid` ("oneShot must be a boolean"), NOT a silent coercion. Rides the persisted
4985
+ body onto resume legs.
4823
4986
  retainBackgroundProcesses: { type: boolean, description: Keep background processes alive past the turn. }
4824
4987
  forwardSubagentEvents:
4825
4988
  type: boolean
@@ -4958,15 +5121,10 @@ components:
4958
5121
  Status of the occupying run (`running` | `suspended` | `needs_review`) on a session-active-run
4959
5122
  rejection. Splits the exit families: running => steer/cancel; parked => decide (see `pendingGate`) / cancel.
4960
5123
  pendingGate:
4961
- type: object
4962
- required: [kind, decidePath]
4963
- additionalProperties: false
5124
+ allOf: [{ $ref: '#/components/schemas/PendingGateMaterial' }]
4964
5125
  description: >
4965
5126
  Present when the occupying run is parked on a pending durable gate: `decidePath` is THE ONE correct
4966
5127
  resume entry for its `kind` (gate-kind routed; sessionId-addressed — checkpoint tokens never appear on the wire).
4967
- properties:
4968
- kind: { type: string }
4969
- decidePath: { type: string }
4970
5128
  model:
4971
5129
  type: string
4972
5130
  description: >
@@ -5294,6 +5452,20 @@ components:
5294
5452
  input: { description: tool-call — redacted args. }
5295
5453
  callId: { type: string }
5296
5454
  isError: { type: boolean }
5455
+ # 🔴 core >=5.18.1 (#187) —— 与 `Event_tool_end.settledBy` **同一个值**:server 的共享挑键器
5456
+ # `toolResultFieldsOf`(src/trace/project.ts)同时喂 turns 面与 trace SSE 面,而写侧
5457
+ # `toolEndEventData` 喂 durable 账本 —— 三面一个真源,所以这里的词表与那边逐字相同。
5458
+ settledBy:
5459
+ type: string
5460
+ enum: [human, timeout, aborted]
5461
+ description: >
5462
+ tool-result — HOW this call was settled when it went through an approval: `human` = somebody
5463
+ actually answered; `timeout` = the approval window elapsed; `aborted` = abort / unclonable
5464
+ arguments / out-of-contract. Present ONLY on the settled call.
5465
+ ABSENCE CARRIES NO SEMANTICS (an older worker, or a posture arm that deliberately leaves it
5466
+ unset — the two are indistinguishable), so absent is neither "human" nor "no approval
5467
+ happened". The read leg re-validates the closed set independently of the write leg, because a
5468
+ ledger row can come from any engine generation.
5297
5469
  tokens:
5298
5470
  type: object
5299
5471
  properties:
@@ -5558,9 +5730,73 @@ components:
5558
5730
  permissionRules:
5559
5731
  type: boolean
5560
5732
  description: >
5561
- The permission-rule lane (/v1/rules/cc-import/* plus the respond persistRule redemption arm)
5562
- is available. Server predicate = PERMISSION_RULES_ENABLED=true AND the rule store is wired
5563
- (default OFF => false; the revocation face is a prerequisite for enabling).
5733
+ The permission-rule lane is available: /v1/rules/cc-import/* , the respond persistRule redemption
5734
+ arm, and (from server 7.12.0) the GET/DELETE /v1/rules revocation face. Server predicate =
5735
+ PERMISSION_RULES_ENABLED AND a wired rule store — the SAME predicate behind every one of those
5736
+ endpoints' 501s, so "says yes" means all of the ones this server has actually work.
5737
+ 🔴 THE KNOB'S DEFAULT READS IN TWO SEGMENTS: default OFF (opt-in) on server <= 7.11.0, default ON
5738
+ (opt-out) from 7.12.0, where the revocation face — the prerequisite that kept it opt-in — landed.
5739
+ So "the operator said nothing" means OPPOSITE things across that boundary; only an explicit
5740
+ `false` disables it. Read THIS BIT; never infer the lane's state from a knob default or a
5741
+ version number.
5742
+ permissionRulesRevoke:
5743
+ type: boolean
5744
+ description: >
5745
+ server >=7.12.0 — the rule REVOCATION face (GET + DELETE /v1/rules) is ROUTED on this worker.
5746
+ Same deps predicate as `permissionRules`; what carries the extra information is this key's mere
5747
+ PRESENCE. 🔴 That is the whole point of it being a second bit rather than a tightening of the
5748
+ first: on a <=7.11.0 worker `permissionRules` already answers true while these two routes 404, so
5749
+ that bit cannot distinguish "the lane is off" from "this build predates the face", and its
5750
+ published wording ("all four endpoints work") is already untrue there — you can add a bit, you
5751
+ cannot retroactively change one. ABSENT => an older worker: expect 404 `not_found.route` from
5752
+ `rules.list` / `rules.revoke` and hide the governance affordance. PRESENT => the routes exist
5753
+ (whether they then answer depends on `permissionRules`, as always).
5754
+ modeShellGateTranslation:
5755
+ type: boolean
5756
+ description: >
5757
+ design/201 §3 (server >=7.12.0) — this binary CONTAINS the `permissionMode` -> `spec.shellGate`
5758
+ translation table (bypassPermissions => off; auto/default/acceptEdits/plan => classify; no stated
5759
+ mode => the key is not written). HARD-CODED true and landed in the SAME COMMIT as the table, so
5760
+ "says yes <=> the translation is really there" is structural, not deps-derived — no missing
5761
+ dependency can make it false.
5762
+ 🔴 THE PROBE MUST BE THIS BIT ON THE SERVER YOU ARE TALKING TO, NEVER YOUR OWN SHELL VERSION. A
5763
+ shell drops its unconditional `MANUAL_MODE_SHELL_GATE` env injection on the strength of this bit;
5764
+ keyed off the shell's own version instead, the three real deployments — new shell against an old
5765
+ server, a replica rolled back to an older binary, a mixed-version fleet — each open a window where
5766
+ the shell has stopped injecting and the server cannot yet translate. Absent/false ⇒ keep injecting.
5767
+ oneShot:
5768
+ type: boolean
5769
+ description: >
5770
+ server >=7.12.0 — this worker actually CONSUMES `TaskRequest.oneShot` (projects it into `TaskSpec`
5771
+ when it is a boolean, omits the key when absent, and 400s a non-boolean). Hard-coded true: the
5772
+ consumption depends on no optional facility. 🔴 PROBE IT BEFORE RELYING ON THE FIELD: the task
5773
+ request body is an OPEN set, so a worker that does not consume the key still accepts it and
5774
+ answers 200 — a headless caller would declare itself one-shot, never receive core's block-wait
5775
+ guidance, and lose its background results with nothing on the wire to reveal it. ABSENT => the key
5776
+ is being ignored; keep your own wait/reconcile step. (That step remains good practice even when
5777
+ the bit is present: the field is guidance to the model, not a completion contract.)
5778
+ outcomeLedger:
5779
+ type: boolean
5780
+ description: >
5781
+ server >=7.12.0 — the `GET /v1/outcomes` mechanical-signal read aggregate (design/73 §7.2) is
5782
+ available. Predicate mirrors the route's refusal arms exactly: a QUERYABLE ledger is wired (a
5783
+ file-only sink writes JSONL and has no query face ⇒ 501), AND on a multi-tenant deployment the
5784
+ operator list is non-empty (single-user has no such gate — the only user IS the operator).
5785
+ Added because this face previously had a 501 but NO capability bit, leaving consumers to
5786
+ trial-by-501 — the exact pattern this capability surface exists to remove.
5787
+ images:
5788
+ type: boolean
5789
+ description: >
5790
+ server >=7.12 — the sandbox-image-pool P1 face (`GET /v1/images`, `/v1/images/{profile}`,
5791
+ `/v1/images/digests/{digest}`, `POST /v1/images/select`) is available. The predicate is verbatim
5792
+ the domain's mount condition (a wired image index), so true ⟺ the catalog reads work. When it is
5793
+ absent the whole domain is unmounted and those paths answer 501
5794
+ `capability.image_index_required`; BEFORE this bit existed they fell through to the global
5795
+ 404 `not_found.route` — the same code as a misspelled path, which left consumers structurally
5796
+ unable to tell "this deployment has no image pool" from "I typed the URL wrong". The `local`
5797
+ store backend deliberately wires no image index (the pool is cloud/fleet-only) ⇒ false there.
5798
+ SCOPE: P1 only — `/v1/images/bakes*` (the P2 bake control plane) is a separate dependency and an
5799
+ operator/runner-internal face, NOT promised by this bit.
5564
5800
  sharedMemory:
5565
5801
  type: boolean
5566
5802
  description: >
@@ -6838,6 +7074,37 @@ components:
6838
7074
  description: Principal that granted it; null = ownerless/anonymous grant.
6839
7075
  createdAt: { type: number, description: Epoch MS of the FIRST grant (re-grant refreshes grantedBy only). }
6840
7076
 
7077
+ PendingGateMaterial:
7078
+ type: object
7079
+ required: [kind, decidePath]
7080
+ # 🔴 **刻意开放**(#220):`governanceForced` 这次就是从一个 `additionalProperties: false` 的形上
7081
+ # additive 长出来的 —— 封闭的形让「server 加一个只在为真时在场的键」变成对严格校验消费端的破坏性
7082
+ # 变更。出路格(kind/decidePath)靠 `required` 守住,新出身格靠登记守住。
7083
+ additionalProperties: true
7084
+ description: >
7085
+ Pending-gate material on a `conflict.session_active_run` rejection (shared by the 409 body and the SSE
7086
+ `done` frame's reject shape). `kind`/`decidePath` = the EXIT (where to decide: THE ONE correct resume
7087
+ entry for that gate kind — sessionId-addressed; checkpoint tokens never appear on the wire).
7088
+ `governanceForced` = the ORIGIN.
7089
+ properties:
7090
+ kind: { type: string }
7091
+ decidePath: { type: string }
7092
+ governanceForced:
7093
+ const: true
7094
+ description: >
7095
+ server >=7.12 (#220) — ADDITIVE, present ONLY when true (the server never writes `false`). The
7096
+ gate was mandated by the deployment's runtime GOVERNANCE layer (`AUTONOMY` / `MANUAL_MODE_SHELL_GATE`).
7097
+ Derived as a CONJUNCTION: the persisted row's `gate.riskDescriptor.shellGateDoctrine === "always"`
7098
+ AND that deployment's governance layer itself mandating `always` (the doctrine alone is not
7099
+ sufficient — a supervisor-routing posture produces it too). Same family + same discipline as the
7100
+ live card frame's `governanceForced`.
7101
+ 🔴 ABSENCE IS NOT A DENIAL: it means "no evidence of a governance origin" and also covers the
7102
+ shapes where the row cannot tell (e.g. the `classify` doctrine, where an ask may equally come
7103
+ from another gate). Render a "governance-mandated" badge on PRESENCE only.
7104
+ 🔴 TRIAGE HINT, NOT A VERDICT: the deployment half of the conjunction is read at RESPONSE time
7105
+ while the row was minted earlier, so a hot governance change can make an old row's attribution
7106
+ drift in either direction. It feeds no gate/decision path — display only.
7107
+
6841
7108
  PendingCheckpoint:
6842
7109
  type: object
6843
7110
  description: >
@@ -8134,8 +8401,35 @@ components:
8134
8401
  redeem it. The engine ALWAYS emits the section, so `false` is a REAL reading ("no rule lane on this
8135
8402
  worker"), NOT "this engine is too old". The section is optional HERE only because this spec's support
8136
8403
  floor is server 3.0.0, which predates it; absent = the worker never sent it.
8404
+
8405
+ 🔴 `syncWired` / `orgGoverned` (core >=5.23.0 design/182 §9/§7, server >=7.12.0) sit in THIS
8406
+ section — the engine deliberately did not put them in the operator-only `governance` section, and
8407
+ the server projects all three bits on both faces. THEY WERE THE REASON THIS SECTION HAD TO BE
8408
+ REDECLARED: this arm is `additionalProperties: false`, so a strict consumer validating a real
8409
+ frame against the 6.13.0 spec did not "miss two fields" — the WHOLE SECTION failed validation and
8410
+ the diagnostics surface went unreadable. Same failure family as `errorCode` on `Event_tool_end`.
8137
8411
  properties:
8138
8412
  storeWired: { type: boolean }
8413
+ syncWired:
8414
+ type: boolean
8415
+ description: >
8416
+ core >=5.23.0 (design/182 §9) — this worker merges persisted rules into the SAME consent trust
8417
+ domain as its cross-device / transport / server-side peers. Read with `storeWired` to answer
8418
+ "how far does this 'don't ask again' actually travel?": the same click means something wider
8419
+ here than on a worker that keeps its rules local.
8420
+ 🔴 PRESENT-AND-FALSE IS A REAL READING — "this engine has the concept, this deployment has not
8421
+ wired it" — NOT "too old to know". Absent (the whole key missing) is the only "did not send it"
8422
+ signal. On server 7.12.0 this is honestly `false` everywhere: the seam exists in core, the
8423
+ server has not wired a supplier yet.
8424
+ orgGoverned:
8425
+ type: boolean
8426
+ description: >
8427
+ core >=5.23.0 (design/182 §7) — an ORGANIZATION governance layer is armed over this rule lane.
8428
+ Load-bearing consequence: that layer is FAIL-CLOSED, so when the org snapshot cannot be read,
8429
+ EVERY allow is escalated into a real human approval rather than quietly honored. A shell that
8430
+ renders "don't ask again" without reading this bit is advertising an outcome it cannot promise.
8431
+ 🔴 Same three-way reading as `syncWired`: present-and-false = concept known, not armed here;
8432
+ absent = the worker never sent the key. Honestly `false` on server 7.12.0.
8139
8433
  governance:
8140
8434
  type: object
8141
8435
  additionalProperties: false
@@ -8753,6 +9047,16 @@ components:
8753
9047
  `rule_not_offered` — the echoed text is not among THIS ask's candidates (a stale card, or someone
8754
9048
  trying to mint a rule of their own).
8755
9049
  `rule_store_error` — the rule store wobbled. The adjudication is unaffected.
9050
+ `rule_governance_forced` (server >=7.12.0, #204) — the gate on THIS ask came from the deployment's
9051
+ operator governance layer, so the ask never entered the rule lane at all.
9052
+ 🔴 SPLIT FROM `rule_lane_unavailable` ON PURPOSE: that one is "one of four conjuncts failed" (no rule
9053
+ store, no candidate, unreadable command bytes, no verified owner — two of which are per-CARD limits),
9054
+ this one is "the lane is perfectly healthy, only THIS ask is operator-governed". Folding them would
9055
+ erase a real distinction on the card.
9056
+ ⚠️ NEITHER IS A DEPLOYMENT-LEVEL VERDICT. Every member of this enum describes ONE ask's outcome; the
9057
+ note on `rule_lane_unavailable` above already says never to disable the feature globally on it, and
9058
+ the same holds here. The one correct source for "does this worker serve rules at all" is
9059
+ `capabilities.permissionRules`. Keep offering "don't ask again" on the next ask; just not on this one.
8756
9060
  enum:
8757
9061
  - no-candidates
8758
9062
  - unknown-candidate
@@ -8762,6 +9066,7 @@ components:
8762
9066
  - rule_input_edited
8763
9067
  - rule_not_offered
8764
9068
  - rule_store_error
9069
+ - rule_governance_forced
8765
9070
 
8766
9071
  ApprovalRiskAxes:
8767
9072
  type: object
@@ -9134,15 +9439,10 @@ components:
9134
9439
  type: string
9135
9440
  description: '`running` | `suspended` | `needs_review` — best-effort (absent when the store lookup degraded).'
9136
9441
  pendingGate:
9137
- type: object
9138
- required: [kind, decidePath]
9139
- additionalProperties: false
9442
+ allOf: [{ $ref: '#/components/schemas/PendingGateMaterial' }]
9140
9443
  description: >
9141
9444
  Present when the occupying run is parked on a pending durable gate: `decidePath` is THE ONE correct
9142
9445
  resume entry for its `kind` (gate-kind routed; sessionId-addressed).
9143
- properties:
9144
- kind: { type: string }
9145
- decidePath: { type: string }
9146
9446
  Event_failed:
9147
9447
  type: object
9148
9448
  # 封闭 + 回填 `activeTaskId`(census 轴一,2026-07-30):[1833] G10 —— session 已有活跃 run 的冲突
@@ -10520,6 +10820,138 @@ components:
10520
10820
  properties:
10521
10821
  result: { $ref: '#/components/schemas/CcImportResult' }
10522
10822
 
10823
+ # ── design/203 §2 撤销面(server 7.12.0)—— 列举行 + 两口的 200 体 ────────────────────────────────
10824
+ # 行形逐字段照 server `src/rules-consent.ts` 的 `PersistedRuleWireRow`(它自己逐字段照 core 的
10825
+ # `PersistedAllowRule`,只把 `scope` 换成判别式串)。
10826
+
10827
+ RuleDot:
10828
+ type: object
10829
+ description: >
10830
+ The immutable causal identity of ONE add — a replica identity plus a monotonic counter (core
10831
+ `RuleDot`). Compared by IDENTITY, never by causal order: single dots minted on different replicas
10832
+ have no order to compare. Treat it as an opaque pair; it is not a timestamp and not a sort key.
10833
+ additionalProperties: false
10834
+ required: [actor, counter]
10835
+ properties:
10836
+ actor: { type: string, description: 'The replica identity that minted this add.' }
10837
+ counter: { type: integer, description: 'That replica''s monotonic counter at mint time. Monotonic PER ACTOR only — never comparable across actors.' }
10838
+
10839
+ RuleAddOrigin:
10840
+ type: string
10841
+ description: >
10842
+ Where ONE add came from (core `RuleAddOrigin`). Stored PER ADD and never folded across dots — a
10843
+ folded provenance could not follow a dot that is later deleted on its own.
10844
+ `user` = minted from an approval card ("don't ask again"); `imported-cc` = redeemed through the
10845
+ CC-settings import lane; `starter` = a deployment-seeded rule.
10846
+ Closed vocabulary on the wire; read it with a default arm anyway (the owner is the engine).
10847
+ enum: [user, imported-cc, starter]
10848
+
10849
+ RuleAdd:
10850
+ type: object
10851
+ description: >
10852
+ ONE add of one logical rule, carrying its own dot and its own provenance (core `RuleAdd`). A rule is
10853
+ LIVE iff at least one of its adds survives the tombstones, which is why this is a SET and not a
10854
+ scalar: the same `(rule, scope)` can be approved several times, from different origins, at different
10855
+ moments. Folding it to a single "addedAt" would make "did I import this or click it?" unanswerable on
10856
+ a governance surface — which is the one place it matters.
10857
+ additionalProperties: false
10858
+ required: [dot, origin, createdAt]
10859
+ properties:
10860
+ dot: { $ref: '#/components/schemas/RuleDot' }
10861
+ origin: { $ref: '#/components/schemas/RuleAddOrigin' }
10862
+ createdAt: { type: string, description: 'When this ADD was recorded (engine-stamped ISO-8601). It dates the add, NOT the rule — a rule with several adds has several of these.' }
10863
+
10864
+ PersistedRule:
10865
+ type: object
10866
+ description: >
10867
+ One LIVE persisted allow rule as `GET /v1/rules` puts it on the wire (server `PersistedRuleWireRow`).
10868
+ Tombstones are already folded — every row here is live at `RuleListResult.rev`.
10869
+ 🔴 `scope` is the DISCRIMINANT STRING (`global` / `project:<root>`), NOT the `RuleScope` object the
10870
+ import lane speaks: this is the exact byte sequence the revoke body wants back, so echo it VERBATIM
10871
+ rather than re-serializing a parsed form (an equivalent-but-differently-spelled root matches nothing).
10872
+ The rule lane is ALLOW-only; deny/ask rules are the tightening direction and never appear here.
10873
+ additionalProperties: false
10874
+ required: [rule, scope, tool, match, command, adds]
10875
+ properties:
10876
+ rule: { type: string, description: 'Canonical rule text — `Bash(git status)` (exact) or `Bash(git status:*)` (prefix). Pass back VERBATIM to revoke.' }
10877
+ scope: { type: string, description: 'Scope discriminant string: `global`, or `project:<root>` with a NON-EMPTY root. Pass back VERBATIM to revoke.' }
10878
+ tool: { type: string, enum: [Bash], description: 'The one tool the v1 rule lane speaks for. The field exists so a later version can widen without a shape change — read it, do not assume it.' }
10879
+ match:
10880
+ type: string
10881
+ enum: [exact, prefix]
10882
+ description: '`exact` = this one command only; `prefix` = word-boundary prefix. (core reserves a `wildcard` form for v2 that no current version produces.)'
10883
+ command: { type: string, description: 'What the matcher actually compares against, parsed out of `rule` at mint time and stored so no consumer re-parses. Trust this + `match`, not your own parse of `rule`.' }
10884
+ adds:
10885
+ type: array
10886
+ description: 'Every LIVE add of this rule (see RuleAdd — a real set, deliberately not folded).'
10887
+ items: { $ref: '#/components/schemas/RuleAdd' }
10888
+
10889
+ RuleListResult:
10890
+ type: object
10891
+ description: >
10892
+ The 200 body of `rulesList` — one keyset page plus the revision it was read at.
10893
+ `nextCursor` ABSENT = this was the last page (there is no empty-string sentinel). `rev` is the rule
10894
+ bucket's OCC revision; the cursor is bound to it, so a page fetched with a stale cursor is REFUSED
10895
+ rather than silently restarted.
10896
+ additionalProperties: false
10897
+ required: [rules, rev]
10898
+ properties:
10899
+ rules:
10900
+ type: array
10901
+ description: 'Live rules, ordered `(scope, rule)` lexicographic — a server-decided deterministic order, since keyset paging depends on it.'
10902
+ items: { $ref: '#/components/schemas/PersistedRule' }
10903
+ rev: { type: integer, description: 'The rule bucket''s OCC revision at read time. Bucket-LOCAL: two tenants each holding one rule both read `rev: 1`, so it is not a global clock and not comparable across principals.' }
10904
+ nextCursor: { type: string, description: 'Opaque base64url cursor for the next page. ABSENT ⇒ no further pages. Pass back VERBATIM; do not parse or mint one.' }
10905
+
10906
+ RuleRevokeRequest:
10907
+ type: object
10908
+ description: >
10909
+ The `DELETE /v1/rules` body. A rule is addressed BY CONTENT — there is no id anywhere in this face.
10910
+ Echo `rule` and `scope` VERBATIM from the listing: both are compared as opaque bytes, so an
10911
+ equivalent-but-differently-spelled scope root simply matches nothing (and answers a cheerful `no-op`).
10912
+ # 封闭:server 侧 zod 是 `.strict()` —— 把 `scope` 拼成 `scopes` 的客户端应当场知道,而不是
10913
+ # 拿到一个「删掉了 global 那条」的意外结果。
10914
+ additionalProperties: false
10915
+ required: [rule, scope]
10916
+ properties:
10917
+ rule: { type: string, minLength: 1, maxLength: 1024, description: 'The canonical rule text, VERBATIM as `RuleListResult.rules[].rule` gave it.' }
10918
+ scope: { type: string, minLength: 1, maxLength: 1088, description: 'The scope discriminant string — `global` or `project:<root>` with a NON-EMPTY root (an empty root would match every cwd). VERBATIM as the listing gave it.' }
10919
+ principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant revoke. Absent (or your own principal) = revoke your own rule.' }
10920
+
10921
+ RuleRevokeResult:
10922
+ type: object
10923
+ description: >
10924
+ The 200 body of `rulesRevoke`. `removed` = a tombstone was written; `no-op` = nothing matched (an
10925
+ idempotent repeat, or it was never there — deliberately NOT a 404, which would be an existence
10926
+ oracle). BOTH arms carry `stillLive`, and the key set is identical on purpose so one consumer branch
10927
+ reads both.
10928
+ additionalProperties: false
10929
+ required: [status, rev, stillLive]
10930
+ properties:
10931
+ status:
10932
+ type: string
10933
+ enum: [removed, "no-op"]
10934
+ description: 'Whether this call wrote a tombstone. `no-op` says nothing about whether the rule EVER existed — do not render it as "not found".'
10935
+ rev: { type: integer, description: 'The rule bucket''s OCC revision after the operation.' }
10936
+ stillLive:
10937
+ type: boolean
10938
+ description: >
10939
+ 🔴 `true` = the rule is STILL LIVE despite this call — under add-wins, an approval recorded during
10940
+ the operation keeps it alive. NEVER tell a human "the rule is gone" without reading this. It rides
10941
+ the `no-op` arm too, because the engine answers `no-op` off its initial snapshot with no read-back,
10942
+ so a concurrent approval leaves the rule alive while the verb reports nothing to remove.
10943
+
10944
+ 🔴 THE TWO TRUTH VALUES ARE NOT EQUALLY STRONG, AND THE ASYMMETRY IS IN THE ENGINE, NOT HERE.
10945
+ `true` is positive evidence: something was observed alive. `false` is only "no evidence it
10946
+ survived" — on the `removed` arm the engine performs the confirming read-back inside a `catch`
10947
+ that swallows a failure and reports `false` (core `removePersistedRule`), so a degraded store
10948
+ yields a byte-identical `{status:"removed", stillLive:false}` to a genuinely verified removal.
10949
+ The `no-op` arm does NOT share that hole: the server does that read-back itself and lets a failure
10950
+ surface as a 500 rather than invent an answer. So: act on `true`; when it MATTERS that a rule is
10951
+ really gone (an operator revoking a standing allow), confirm with `GET /v1/rules` instead of
10952
+ treating `false` as proof. Registered upstream — an indeterminate outcome should be its own state
10953
+ rather than folding into `false`.
10954
+
10523
10955
  # ═══════════════════════════════════════════════════════════════════════════════════════════════
10524
10956
  # design/183 —— 身份收编(form b:纯身份重绑)。形逐字段照 server `src/adoption/wire.ts` 的 zod
10525
10957
  # (那份 schema 自陈是「跨仓 wire 契约,下游按字段名逐字钉围栏」)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/sdk",
3
- "version": "6.13.0",
3
+ "version": "6.15.0",
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",