@sema-agent/sdk 6.13.0 → 6.14.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
@@ -5294,6 +5457,20 @@ components:
5294
5457
  input: { description: tool-call — redacted args. }
5295
5458
  callId: { type: string }
5296
5459
  isError: { type: boolean }
5460
+ # 🔴 core >=5.18.1 (#187) —— 与 `Event_tool_end.settledBy` **同一个值**:server 的共享挑键器
5461
+ # `toolResultFieldsOf`(src/trace/project.ts)同时喂 turns 面与 trace SSE 面,而写侧
5462
+ # `toolEndEventData` 喂 durable 账本 —— 三面一个真源,所以这里的词表与那边逐字相同。
5463
+ settledBy:
5464
+ type: string
5465
+ enum: [human, timeout, aborted]
5466
+ description: >
5467
+ tool-result — HOW this call was settled when it went through an approval: `human` = somebody
5468
+ actually answered; `timeout` = the approval window elapsed; `aborted` = abort / unclonable
5469
+ arguments / out-of-contract. Present ONLY on the settled call.
5470
+ ABSENCE CARRIES NO SEMANTICS (an older worker, or a posture arm that deliberately leaves it
5471
+ unset — the two are indistinguishable), so absent is neither "human" nor "no approval
5472
+ happened". The read leg re-validates the closed set independently of the write leg, because a
5473
+ ledger row can come from any engine generation.
5297
5474
  tokens:
5298
5475
  type: object
5299
5476
  properties:
@@ -5558,9 +5735,60 @@ components:
5558
5735
  permissionRules:
5559
5736
  type: boolean
5560
5737
  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).
5738
+ The permission-rule lane is available: /v1/rules/cc-import/* , the respond persistRule redemption
5739
+ arm, and (from server 7.12.0) the GET/DELETE /v1/rules revocation face. Server predicate =
5740
+ PERMISSION_RULES_ENABLED AND a wired rule store — the SAME predicate behind every one of those
5741
+ endpoints' 501s, so "says yes" means all of the ones this server has actually work.
5742
+ 🔴 THE KNOB'S DEFAULT READS IN TWO SEGMENTS: default OFF (opt-in) on server <= 7.11.0, default ON
5743
+ (opt-out) from 7.12.0, where the revocation face — the prerequisite that kept it opt-in — landed.
5744
+ So "the operator said nothing" means OPPOSITE things across that boundary; only an explicit
5745
+ `false` disables it. Read THIS BIT; never infer the lane's state from a knob default or a
5746
+ version number.
5747
+ permissionRulesRevoke:
5748
+ type: boolean
5749
+ description: >
5750
+ server >=7.12.0 — the rule REVOCATION face (GET + DELETE /v1/rules) is ROUTED on this worker.
5751
+ Same deps predicate as `permissionRules`; what carries the extra information is this key's mere
5752
+ PRESENCE. 🔴 That is the whole point of it being a second bit rather than a tightening of the
5753
+ first: on a <=7.11.0 worker `permissionRules` already answers true while these two routes 404, so
5754
+ that bit cannot distinguish "the lane is off" from "this build predates the face", and its
5755
+ published wording ("all four endpoints work") is already untrue there — you can add a bit, you
5756
+ cannot retroactively change one. ABSENT => an older worker: expect 404 `not_found.route` from
5757
+ `rules.list` / `rules.revoke` and hide the governance affordance. PRESENT => the routes exist
5758
+ (whether they then answer depends on `permissionRules`, as always).
5759
+ modeShellGateTranslation:
5760
+ type: boolean
5761
+ description: >
5762
+ design/201 §3 (server >=7.12.0) — this binary CONTAINS the `permissionMode` -> `spec.shellGate`
5763
+ translation table (bypassPermissions => off; auto/default/acceptEdits/plan => classify; no stated
5764
+ mode => the key is not written). HARD-CODED true and landed in the SAME COMMIT as the table, so
5765
+ "says yes <=> the translation is really there" is structural, not deps-derived — no missing
5766
+ dependency can make it false.
5767
+ 🔴 THE PROBE MUST BE THIS BIT ON THE SERVER YOU ARE TALKING TO, NEVER YOUR OWN SHELL VERSION. A
5768
+ shell drops its unconditional `MANUAL_MODE_SHELL_GATE` env injection on the strength of this bit;
5769
+ keyed off the shell's own version instead, the three real deployments — new shell against an old
5770
+ server, a replica rolled back to an older binary, a mixed-version fleet — each open a window where
5771
+ the shell has stopped injecting and the server cannot yet translate. Absent/false ⇒ keep injecting.
5772
+ oneShot:
5773
+ type: boolean
5774
+ description: >
5775
+ server >=7.12.0 — this worker actually CONSUMES `TaskRequest.oneShot` (projects it into `TaskSpec`
5776
+ when it is a boolean, omits the key when absent, and 400s a non-boolean). Hard-coded true: the
5777
+ consumption depends on no optional facility. 🔴 PROBE IT BEFORE RELYING ON THE FIELD: the task
5778
+ request body is an OPEN set, so a worker that does not consume the key still accepts it and
5779
+ answers 200 — a headless caller would declare itself one-shot, never receive core's block-wait
5780
+ guidance, and lose its background results with nothing on the wire to reveal it. ABSENT => the key
5781
+ is being ignored; keep your own wait/reconcile step. (That step remains good practice even when
5782
+ the bit is present: the field is guidance to the model, not a completion contract.)
5783
+ outcomeLedger:
5784
+ type: boolean
5785
+ description: >
5786
+ server >=7.12.0 — the `GET /v1/outcomes` mechanical-signal read aggregate (design/73 §7.2) is
5787
+ available. Predicate mirrors the route's refusal arms exactly: a QUERYABLE ledger is wired (a
5788
+ file-only sink writes JSONL and has no query face ⇒ 501), AND on a multi-tenant deployment the
5789
+ operator list is non-empty (single-user has no such gate — the only user IS the operator).
5790
+ Added because this face previously had a 501 but NO capability bit, leaving consumers to
5791
+ trial-by-501 — the exact pattern this capability surface exists to remove.
5564
5792
  sharedMemory:
5565
5793
  type: boolean
5566
5794
  description: >
@@ -8134,8 +8362,35 @@ components:
8134
8362
  redeem it. The engine ALWAYS emits the section, so `false` is a REAL reading ("no rule lane on this
8135
8363
  worker"), NOT "this engine is too old". The section is optional HERE only because this spec's support
8136
8364
  floor is server 3.0.0, which predates it; absent = the worker never sent it.
8365
+
8366
+ 🔴 `syncWired` / `orgGoverned` (core >=5.23.0 design/182 §9/§7, server >=7.12.0) sit in THIS
8367
+ section — the engine deliberately did not put them in the operator-only `governance` section, and
8368
+ the server projects all three bits on both faces. THEY WERE THE REASON THIS SECTION HAD TO BE
8369
+ REDECLARED: this arm is `additionalProperties: false`, so a strict consumer validating a real
8370
+ frame against the 6.13.0 spec did not "miss two fields" — the WHOLE SECTION failed validation and
8371
+ the diagnostics surface went unreadable. Same failure family as `errorCode` on `Event_tool_end`.
8137
8372
  properties:
8138
8373
  storeWired: { type: boolean }
8374
+ syncWired:
8375
+ type: boolean
8376
+ description: >
8377
+ core >=5.23.0 (design/182 §9) — this worker merges persisted rules into the SAME consent trust
8378
+ domain as its cross-device / transport / server-side peers. Read with `storeWired` to answer
8379
+ "how far does this 'don't ask again' actually travel?": the same click means something wider
8380
+ here than on a worker that keeps its rules local.
8381
+ 🔴 PRESENT-AND-FALSE IS A REAL READING — "this engine has the concept, this deployment has not
8382
+ wired it" — NOT "too old to know". Absent (the whole key missing) is the only "did not send it"
8383
+ signal. On server 7.12.0 this is honestly `false` everywhere: the seam exists in core, the
8384
+ server has not wired a supplier yet.
8385
+ orgGoverned:
8386
+ type: boolean
8387
+ description: >
8388
+ core >=5.23.0 (design/182 §7) — an ORGANIZATION governance layer is armed over this rule lane.
8389
+ Load-bearing consequence: that layer is FAIL-CLOSED, so when the org snapshot cannot be read,
8390
+ EVERY allow is escalated into a real human approval rather than quietly honored. A shell that
8391
+ renders "don't ask again" without reading this bit is advertising an outcome it cannot promise.
8392
+ 🔴 Same three-way reading as `syncWired`: present-and-false = concept known, not armed here;
8393
+ absent = the worker never sent the key. Honestly `false` on server 7.12.0.
8139
8394
  governance:
8140
8395
  type: object
8141
8396
  additionalProperties: false
@@ -8753,6 +9008,16 @@ components:
8753
9008
  `rule_not_offered` — the echoed text is not among THIS ask's candidates (a stale card, or someone
8754
9009
  trying to mint a rule of their own).
8755
9010
  `rule_store_error` — the rule store wobbled. The adjudication is unaffected.
9011
+ `rule_governance_forced` (server >=7.12.0, #204) — the gate on THIS ask came from the deployment's
9012
+ operator governance layer, so the ask never entered the rule lane at all.
9013
+ 🔴 SPLIT FROM `rule_lane_unavailable` ON PURPOSE: that one is "one of four conjuncts failed" (no rule
9014
+ store, no candidate, unreadable command bytes, no verified owner — two of which are per-CARD limits),
9015
+ this one is "the lane is perfectly healthy, only THIS ask is operator-governed". Folding them would
9016
+ erase a real distinction on the card.
9017
+ ⚠️ NEITHER IS A DEPLOYMENT-LEVEL VERDICT. Every member of this enum describes ONE ask's outcome; the
9018
+ note on `rule_lane_unavailable` above already says never to disable the feature globally on it, and
9019
+ the same holds here. The one correct source for "does this worker serve rules at all" is
9020
+ `capabilities.permissionRules`. Keep offering "don't ask again" on the next ask; just not on this one.
8756
9021
  enum:
8757
9022
  - no-candidates
8758
9023
  - unknown-candidate
@@ -8762,6 +9027,7 @@ components:
8762
9027
  - rule_input_edited
8763
9028
  - rule_not_offered
8764
9029
  - rule_store_error
9030
+ - rule_governance_forced
8765
9031
 
8766
9032
  ApprovalRiskAxes:
8767
9033
  type: object
@@ -10520,6 +10786,138 @@ components:
10520
10786
  properties:
10521
10787
  result: { $ref: '#/components/schemas/CcImportResult' }
10522
10788
 
10789
+ # ── design/203 §2 撤销面(server 7.12.0)—— 列举行 + 两口的 200 体 ────────────────────────────────
10790
+ # 行形逐字段照 server `src/rules-consent.ts` 的 `PersistedRuleWireRow`(它自己逐字段照 core 的
10791
+ # `PersistedAllowRule`,只把 `scope` 换成判别式串)。
10792
+
10793
+ RuleDot:
10794
+ type: object
10795
+ description: >
10796
+ The immutable causal identity of ONE add — a replica identity plus a monotonic counter (core
10797
+ `RuleDot`). Compared by IDENTITY, never by causal order: single dots minted on different replicas
10798
+ have no order to compare. Treat it as an opaque pair; it is not a timestamp and not a sort key.
10799
+ additionalProperties: false
10800
+ required: [actor, counter]
10801
+ properties:
10802
+ actor: { type: string, description: 'The replica identity that minted this add.' }
10803
+ counter: { type: integer, description: 'That replica''s monotonic counter at mint time. Monotonic PER ACTOR only — never comparable across actors.' }
10804
+
10805
+ RuleAddOrigin:
10806
+ type: string
10807
+ description: >
10808
+ Where ONE add came from (core `RuleAddOrigin`). Stored PER ADD and never folded across dots — a
10809
+ folded provenance could not follow a dot that is later deleted on its own.
10810
+ `user` = minted from an approval card ("don't ask again"); `imported-cc` = redeemed through the
10811
+ CC-settings import lane; `starter` = a deployment-seeded rule.
10812
+ Closed vocabulary on the wire; read it with a default arm anyway (the owner is the engine).
10813
+ enum: [user, imported-cc, starter]
10814
+
10815
+ RuleAdd:
10816
+ type: object
10817
+ description: >
10818
+ ONE add of one logical rule, carrying its own dot and its own provenance (core `RuleAdd`). A rule is
10819
+ LIVE iff at least one of its adds survives the tombstones, which is why this is a SET and not a
10820
+ scalar: the same `(rule, scope)` can be approved several times, from different origins, at different
10821
+ moments. Folding it to a single "addedAt" would make "did I import this or click it?" unanswerable on
10822
+ a governance surface — which is the one place it matters.
10823
+ additionalProperties: false
10824
+ required: [dot, origin, createdAt]
10825
+ properties:
10826
+ dot: { $ref: '#/components/schemas/RuleDot' }
10827
+ origin: { $ref: '#/components/schemas/RuleAddOrigin' }
10828
+ 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.' }
10829
+
10830
+ PersistedRule:
10831
+ type: object
10832
+ description: >
10833
+ One LIVE persisted allow rule as `GET /v1/rules` puts it on the wire (server `PersistedRuleWireRow`).
10834
+ Tombstones are already folded — every row here is live at `RuleListResult.rev`.
10835
+ 🔴 `scope` is the DISCRIMINANT STRING (`global` / `project:<root>`), NOT the `RuleScope` object the
10836
+ import lane speaks: this is the exact byte sequence the revoke body wants back, so echo it VERBATIM
10837
+ rather than re-serializing a parsed form (an equivalent-but-differently-spelled root matches nothing).
10838
+ The rule lane is ALLOW-only; deny/ask rules are the tightening direction and never appear here.
10839
+ additionalProperties: false
10840
+ required: [rule, scope, tool, match, command, adds]
10841
+ properties:
10842
+ rule: { type: string, description: 'Canonical rule text — `Bash(git status)` (exact) or `Bash(git status:*)` (prefix). Pass back VERBATIM to revoke.' }
10843
+ scope: { type: string, description: 'Scope discriminant string: `global`, or `project:<root>` with a NON-EMPTY root. Pass back VERBATIM to revoke.' }
10844
+ 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.' }
10845
+ match:
10846
+ type: string
10847
+ enum: [exact, prefix]
10848
+ description: '`exact` = this one command only; `prefix` = word-boundary prefix. (core reserves a `wildcard` form for v2 that no current version produces.)'
10849
+ 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`.' }
10850
+ adds:
10851
+ type: array
10852
+ description: 'Every LIVE add of this rule (see RuleAdd — a real set, deliberately not folded).'
10853
+ items: { $ref: '#/components/schemas/RuleAdd' }
10854
+
10855
+ RuleListResult:
10856
+ type: object
10857
+ description: >
10858
+ The 200 body of `rulesList` — one keyset page plus the revision it was read at.
10859
+ `nextCursor` ABSENT = this was the last page (there is no empty-string sentinel). `rev` is the rule
10860
+ bucket's OCC revision; the cursor is bound to it, so a page fetched with a stale cursor is REFUSED
10861
+ rather than silently restarted.
10862
+ additionalProperties: false
10863
+ required: [rules, rev]
10864
+ properties:
10865
+ rules:
10866
+ type: array
10867
+ description: 'Live rules, ordered `(scope, rule)` lexicographic — a server-decided deterministic order, since keyset paging depends on it.'
10868
+ items: { $ref: '#/components/schemas/PersistedRule' }
10869
+ 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.' }
10870
+ nextCursor: { type: string, description: 'Opaque base64url cursor for the next page. ABSENT ⇒ no further pages. Pass back VERBATIM; do not parse or mint one.' }
10871
+
10872
+ RuleRevokeRequest:
10873
+ type: object
10874
+ description: >
10875
+ The `DELETE /v1/rules` body. A rule is addressed BY CONTENT — there is no id anywhere in this face.
10876
+ Echo `rule` and `scope` VERBATIM from the listing: both are compared as opaque bytes, so an
10877
+ equivalent-but-differently-spelled scope root simply matches nothing (and answers a cheerful `no-op`).
10878
+ # 封闭:server 侧 zod 是 `.strict()` —— 把 `scope` 拼成 `scopes` 的客户端应当场知道,而不是
10879
+ # 拿到一个「删掉了 global 那条」的意外结果。
10880
+ additionalProperties: false
10881
+ required: [rule, scope]
10882
+ properties:
10883
+ rule: { type: string, minLength: 1, maxLength: 1024, description: 'The canonical rule text, VERBATIM as `RuleListResult.rules[].rule` gave it.' }
10884
+ 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.' }
10885
+ principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant revoke. Absent (or your own principal) = revoke your own rule.' }
10886
+
10887
+ RuleRevokeResult:
10888
+ type: object
10889
+ description: >
10890
+ The 200 body of `rulesRevoke`. `removed` = a tombstone was written; `no-op` = nothing matched (an
10891
+ idempotent repeat, or it was never there — deliberately NOT a 404, which would be an existence
10892
+ oracle). BOTH arms carry `stillLive`, and the key set is identical on purpose so one consumer branch
10893
+ reads both.
10894
+ additionalProperties: false
10895
+ required: [status, rev, stillLive]
10896
+ properties:
10897
+ status:
10898
+ type: string
10899
+ enum: [removed, "no-op"]
10900
+ description: 'Whether this call wrote a tombstone. `no-op` says nothing about whether the rule EVER existed — do not render it as "not found".'
10901
+ rev: { type: integer, description: 'The rule bucket''s OCC revision after the operation.' }
10902
+ stillLive:
10903
+ type: boolean
10904
+ description: >
10905
+ 🔴 `true` = the rule is STILL LIVE despite this call — under add-wins, an approval recorded during
10906
+ the operation keeps it alive. NEVER tell a human "the rule is gone" without reading this. It rides
10907
+ the `no-op` arm too, because the engine answers `no-op` off its initial snapshot with no read-back,
10908
+ so a concurrent approval leaves the rule alive while the verb reports nothing to remove.
10909
+
10910
+ 🔴 THE TWO TRUTH VALUES ARE NOT EQUALLY STRONG, AND THE ASYMMETRY IS IN THE ENGINE, NOT HERE.
10911
+ `true` is positive evidence: something was observed alive. `false` is only "no evidence it
10912
+ survived" — on the `removed` arm the engine performs the confirming read-back inside a `catch`
10913
+ that swallows a failure and reports `false` (core `removePersistedRule`), so a degraded store
10914
+ yields a byte-identical `{status:"removed", stillLive:false}` to a genuinely verified removal.
10915
+ The `no-op` arm does NOT share that hole: the server does that read-back itself and lets a failure
10916
+ surface as a 500 rather than invent an answer. So: act on `true`; when it MATTERS that a rule is
10917
+ really gone (an operator revoking a standing allow), confirm with `GET /v1/rules` instead of
10918
+ treating `false` as proof. Registered upstream — an indeterminate outcome should be its own state
10919
+ rather than folding into `false`.
10920
+
10523
10921
  # ═══════════════════════════════════════════════════════════════════════════════════════════════
10524
10922
  # design/183 —— 身份收编(form b:纯身份重绑)。形逐字段照 server `src/adoption/wire.ts` 的 zod
10525
10923
  # (那份 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.14.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",