@sema-agent/sdk 8.1.0 → 8.2.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
@@ -3358,31 +3358,21 @@ paths:
3358
3358
  decision: { type: string, enum: [allow, allow_session, deny] }
3359
3359
  updatedInput: { description: 'ctrl+g edited tool args (replaces the asked args on allow).' }
3360
3360
  persistRule:
3361
- type: object
3362
- # 刻意**不**封闭:server 的 `parseToolApprovalResponse` 只读 `rule`,对象里多给的键它**忽略**
3363
- # (与 `updatedInput` 同一条宽收姿势)。写 `additionalProperties:false` 会让照 spec 生成的
3364
- # 客户端拒发一个 server 其实会受理的请求 —— 那是我们自己造的漂移,不是上游的约束。
3365
- required: [rule]
3366
- properties:
3367
- rule: { type: string, minLength: 1, maxLength: 512, description: 'Non-empty (the server parser rejects an empty string); at most MAX_RULE_TEXT_CHARS = 512.' }
3361
+ allOf: [{ $ref: '#/components/schemas/PersistRuleSelection' }]
3368
3362
  description: >
3369
- server #154 车二, ADDITIVE — the person ticked "don't ask again": echo back, VERBATIM, the
3370
- `rule` of the chosen entry of the frame's `ruleSuggestions`. Allow-family only (on `deny`
3371
- the server tolerantly ignores it, same posture as `updatedInput`).
3372
-
3373
- 🔴 A CANDIDATE, NOT FREE TEXT: the server locates this string in the candidate table the
3374
- engine minted and refuses if it does not match (`ruleRefusal: "rule_not_offered"`). Do not
3375
- hand-build it, re-case it or append `:*` — take it from the frame.
3376
- 🔴 Together with `updatedInput` the persistence is refused (`rule_input_edited`): the call
3377
- that takes effect is the edited one while the candidates came from the ORIGINAL command.
3378
- The decision itself still stands.
3379
- ⚠️ If the frame carried no `ruleSuggestions`, do not send this key — there is nothing
3380
- redeemable for THIS card and the answer is necessarily `rulePersisted:false` +
3381
- `rule_lane_unavailable` (the missing material short-circuits BEFORE any candidate
3382
- comparison, so it is never `rule_not_offered`). Absence has THREE possible causes — no rule
3383
- store wired, the engine minted no candidate for this command (compound / redirect /
3384
- substitution), or the command bytes were unreadable — so do NOT diagnose the deployment's
3385
- rule lane from one frame or from that refusal.
3363
+ The person ticked "don't ask again". TWO MUTUALLY EXCLUSIVE ARMS — the candidate/edited arm
3364
+ `{ rule, edited? }` and the batch arm `{ batchOfferIndex }`; the shapes, the server's four
3365
+ verbatim 400 texts, the two capability bits to probe first and the `deny` posture are on
3366
+ the PersistRuleSelection schema. Allow-family only (on `deny` the whole field is ignored).
3367
+
3368
+ ⚠️ If the frame carried neither `ruleOffers` nor `ruleSuggestions`, do not send this key —
3369
+ there is nothing redeemable for THIS card and the answer is necessarily
3370
+ `rulePersisted:false` + `rule_lane_unavailable` (the missing material short-circuits BEFORE
3371
+ any candidate comparison, so it is never `rule_not_offered`). Absence has several causes —
3372
+ no rule store wired, the engine minted no offer for this command (compound / redirect /
3373
+ substitution), the command bytes were unreadable, or this ask belongs to one of the seven
3374
+ families that never get offers — so do NOT diagnose the deployment's rule lane from one
3375
+ frame or from that one refusal.
3386
3376
  note:
3387
3377
  type: string
3388
3378
  maxLength: 2048
@@ -6603,6 +6593,39 @@ components:
6603
6593
  `noteRecorded` (a live-card-only deployment has no row to write, and a flapping store cannot write
6604
6594
  one; both answer `false` while the decision itself still stands). This bit only says "this build
6605
6595
  knows the key" — same family as `permissionRulesRevoke`, deliberately gated on no facility.
6596
+ respondFreeFormRules:
6597
+ type: boolean
6598
+ description: >
6599
+ server >= 7.44.0 (#340), hard-coded true in the capability object — the VERSION PROBE for the
6600
+ free-text arm of POST /v1/tool-approvals/{id}/respond (`persistRule.edited: true`): the rule to
6601
+ persist is text the person HAND-EDITED on the card, not one of the engine's candidates.
6602
+
6603
+ PROBE IT BEFORE RENDERING THE EDITABLE INPUT. The live respond body validator is a NON-STRICT
6604
+ hand-written parser, so an older worker DROPS the unknown `edited` key and judges the text AS A
6605
+ CANDIDATE, answering `rule_not_offered` — honest, but a person reads it as "I typed it wrong".
6606
+ Absent means an older worker: do not render the affordance.
6607
+
6608
+ IT DOES NOT PROMISE THIS ONE WILL LAND (deliberately no facility predicate, same family as
6609
+ `approvalDecisionNote`): that is per-call and answered by `rulePersisted` / `ruleRefusal` —
6610
+ `edit-unsupported` (this card has no binding anchor, STOP rendering the box) versus
6611
+ `edit-rejected` (fix it and try again). Whether the LANE exists at all is `permissionRules`,
6612
+ computed per caller; read the two bits together.
6613
+ respondBatchRuleOffers:
6614
+ type: boolean
6615
+ description: >
6616
+ server >= 7.46.0 (design/377), hard-coded true — the VERSION PROBE for the batch arm of the same
6617
+ endpoint (`persistRule.batchOfferIndex`): the person ticked a `kind: "batch"` offer, and the
6618
+ selection key is that offer's index in the frame's `ruleOffers`.
6619
+
6620
+ PROBE IT BEFORE RENDERING A REDEEMABLE BATCH AFFORDANCE: an older worker drops the unknown key and
6621
+ then fails the `rule` shape check with a 400 — honest, but it cannot tell a batch was ticked. With
6622
+ the bit absent, render batch offers read-only.
6623
+
6624
+ Same posture as above: it only says "this binary knows this key". Whether THIS redemption lands is
6625
+ answered per call by `rulePersisted` / `ruleRefusal`, with `persistedRules` on success and (server
6626
+ >= 7.48.0) the per-entry anchors `persistedRuleAnchors` — that additive key deliberately gets NO
6627
+ second capability bit, since it rides the same batch redemption channel and a consumer just reads
6628
+ whether it is there.
6606
6629
  promptSuggestions: { type: boolean, description: "Follow-up prompt suggestions are generated." }
6607
6630
  modelUsage:
6608
6631
  type: boolean
@@ -8023,6 +8046,27 @@ components:
8023
8046
  display order; an EXACT entry, when present, is always first. core contract caps candidates at 2;
8024
8047
  the server enforces a tolerance cap of 4 on read-back — lay out for 4. Absent = pre-column row
8025
8048
  (parked before the column existed) OR no candidates; render both as "no candidates", never an error.
8049
+ ruleOffers:
8050
+ type: array
8051
+ minItems: 1
8052
+ items: { $ref: '#/components/schemas/RuleOffer' }
8053
+ description: >
8054
+ server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union successor of the retired
8055
+ `ruleSuggestions` above on the DURABLE queue row (core `PendingAction.ruleOffers`, the same
8056
+ contract as the sync leg's `AskRequest.ruleOffers`). A >= 7.46.0 row carries only the new key;
8057
+ pre-5.58 rows fail the new narrow read entry by entry and land as "no supply" (no compatibility
8058
+ read — a stale schema means re-ask).
8059
+
8060
+ 🔴 THE INDEX ON THIS LEG IS NOT A SELECTION KEY. This projection drops malformed offers one by one
8061
+ and COMPACTS, so an excision shifts every later index forward. That is only sound because this leg
8062
+ has NO redemption mouth (POST /v1/approvals/{sessionId}/decide takes no rule field). Using it as
8063
+ `persistRule.batchOfferIndex` would redeem a different rule than the one the person clicked. The
8064
+ live card leg is the opposite (pure prefix truncation, no per-entry drops) and is where redemption
8065
+ actually happens.
8066
+
8067
+ 🔴 DISPLAY / TRIAGE ONLY, and ABSENT MEANS THERE REALLY IS NO SUPPLY (rule lane unarmed, the
8068
+ command yields no rule, no rule could silence this ask, or an old row) — never "the projection
8069
+ dropped it", which is precisely the bug this key exists to close. Never `null`, never `[]`.
8026
8070
  governanceForced:
8027
8071
  type: boolean
8028
8072
  enum: [true]
@@ -9889,6 +9933,38 @@ components:
9889
9933
  replica ⇒ redeemable; after a restart / on another replica that `approvalId` 404s and the person
9890
9934
  must use the durable decide leg, which carries NO rule field in v1 (its body is strict — an extra
9891
9935
  `persistRule` is a loud 400, not a silent drop).
9936
+ ruleOffers:
9937
+ type: array
9938
+ minItems: 1
9939
+ items: { $ref: '#/components/schemas/RuleOffer' }
9940
+ description: >
9941
+ server >= 7.46.0 (core 5.58.0, design/375), "tool_approval" only — the ENGINE-minted "don't ask
9942
+ again" OPTIONS for this ask, as a DISCRIMINATED UNION (`single` | `batch`). It is the BREAKING
9943
+ REPLACEMENT of `ruleSuggestions` above: a >= 7.46.0 engine mints the old key nowhere, there is no
9944
+ alias, and the two keys are declared side by side only so one type surface covers both generations.
9945
+
9946
+ 🔴 READ `kind` TO TELL THE ARMS APART, never a positional assumption. ORDER IS CONTRACT: a
9947
+ whole-string exact `single` is index 0 when present and a `batch` is always last; core's own
9948
+ cardinality is <= 2 while the server enforces a tolerance cap of 4.
9949
+
9950
+ 🔴 INDEX SEMANTICS DIFFER PER LEG, and only one leg can redeem. On THIS leg (and on the
9951
+ `approval_request` card and the durable replay of both) the server prefix-truncates and never drops
9952
+ an entry, so the index equals core's offer index and IS a legal selection key for
9953
+ `persistRule.batchOfferIndex`. On the durable queue row (PendingCheckpoint.ruleOffers) the server
9954
+ drops malformed entries and COMPACTS, so indexes shift there and that leg has no redemption mouth
9955
+ at all. A consumer that drops entries itself must keep every survivor's ORIGINAL wire index, or
9956
+ suppress its persistence actions entirely (core's normative clause) — a compacted index means the
9957
+ k-th option the person clicked is not the k-th rule the server redeems.
9958
+
9959
+ 🔴 PRESENCE IS THE PROMISE (same as `ruleSuggestions`): present only where a permission-rule store
9960
+ is really wired. The converse does not hold — a wired deployment still omits it for mandated /
9961
+ requiresRealApproval / shadowed / hook-produced / inherited-unresolved / ancestor-resolved /
9962
+ anonymous asks. Absence reads as "THIS CARD has no such option", never as "this deployment has no
9963
+ rule lane" (that is `capabilities.permissionRules`) and never as "your rule broke" (that is
9964
+ `persistedRuleShadowed`).
9965
+
9966
+ ⚠️ REDEMPTION SCOPE is the live respond leg keyed by this frame's `approvalId` only; a replayed
9967
+ offer on another replica is not a promise that it is still redeemable.
9892
9968
  persistedRuleShadowed:
9893
9969
  type: string
9894
9970
  description: >
@@ -10123,6 +10199,15 @@ components:
10123
10199
  the PRESENCE-IS-THE-PROMISE clause (present only where a rule store is wired) and the redemption
10124
10200
  scope are stated verbatim on ToolApprovalFrame.ruleSuggestions — the live frame, the stored
10125
10201
  `card_json` and the replayed frame carry the SAME material.
10202
+ ruleOffers:
10203
+ type: array
10204
+ minItems: 1
10205
+ items: { $ref: '#/components/schemas/RuleOffer' }
10206
+ description: >
10207
+ server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union replacement of
10208
+ `ruleSuggestions` above. Same material as the live frame and the stored `card_json` (the server
10209
+ projects all three through one `copyRuleOffer`), so semantics, order-is-contract, presence-is-the-
10210
+ promise and the per-leg index rules are stated verbatim on ToolApprovalFrame.ruleOffers.
10126
10211
  persistedRuleShadowed:
10127
10212
  type: string
10128
10213
  description: >
@@ -10163,13 +10248,269 @@ components:
10163
10248
  description: '`exact` = this one command only (`Bash(git status)`); `prefix` = word-boundary prefix (`Bash(git status:*)`). Closed vocabulary — an unknown value is not a candidate this contract describes; do not render it as redeemable.'
10164
10249
  command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
10165
10250
 
10251
+ RuleOfferMatch:
10252
+ type: string
10253
+ description: >
10254
+ The MATCH FORM of a persisted rule — the server's `PERSISTED_RULE_MATCHES`, verbatim (the vocabulary
10255
+ is OWNED BY CORE; the server only transcribes it and pins set equality, so core adding a member turns
10256
+ the server red at compile time).
10257
+
10258
+ `exact` = this one command only (`Bash(git status)`). `prefix` = the historical colon-star
10259
+ word-boundary prefix (`Bash(git status:*)`) and the ONLY spelling for a compound prefix form; it stays
10260
+ a permanent compatibility read. `wildcard` = the trailing space-star form (`Bash(npm run *)`), the
10261
+ same predicate as `prefix` on a single command body, minted by core's suggestion face since
10262
+ design/382 / #510. `subpath` = the `Read(//abs-dir/**)` directory form, which never holds a command.
10263
+
10264
+ 🔴 FOUR MEMBERS, not "the three today's miner emits": this type describes what a consumer may RECEIVE,
10265
+ and the server's wire validator accepts all four. A consumer type narrower than the mint point is a
10266
+ lie — it lets a `switch` claim exhaustiveness the wire does not honour. (The retired `RuleSuggestion`
10267
+ declared only two, which is exactly why a legal `wildcard` candidate used to be dropped as malformed.)
10268
+ enum: [exact, prefix, wildcard, subpath]
10269
+
10270
+ RuleOfferUncoveredReason:
10271
+ type: string
10272
+ description: >
10273
+ Why one segment of a compound command is STILL not covered after redeeming the batch (design/382
10274
+ §3.5; the server's `UNCOVERED_SEGMENT_REASONS`, core-owned). `redirection` = a redirection is asked
10275
+ every time, permanently. `no_rule_form` = there is no rule form that could cover this segment.
10276
+ `cap_overflow` = deduplication plus the batch cap pushed it out.
10277
+ enum: [redirection, no_rule_form, cap_overflow]
10278
+
10279
+ RuleOfferUncoveredDetail:
10280
+ type: object
10281
+ description: >
10282
+ design/382 §3.5, ADDITIVE — one "why is this segment still uncovered" row. `segment` is the folded
10283
+ segment's ORIGINAL BYTES (command family: UNTRUSTED for display, and subject to the same redaction /
10284
+ truncation discipline as a member's `segment`).
10285
+ additionalProperties: false
10286
+ required: [segment, reason]
10287
+ properties:
10288
+ segment: { type: string, maxLength: 512 }
10289
+ reason: { $ref: '#/components/schemas/RuleOfferUncoveredReason' }
10290
+
10291
+ RuleOfferBatchMember:
10292
+ description: >
10293
+ ONE MEMBER of a `batch` offer — the rule folded out of one segment of a compound command
10294
+ (design/382 §2.3 B3; discriminated on `kind`, core-owned closed vocabulary).
10295
+
10296
+ `segment` records WHICH folded segment this member came from. Core states it explicitly as a RENDERING
10297
+ SEAT that never participates in adjudication, and it is UNTRUSTED for display exactly like
10298
+ `rule` / `command` / `directory`. ⚠️ Only `segment` may be TRUNCATED by the server (an honest
10299
+ elision); `rule` / `command` / `directory` sit on the left-hand side of the redemption equality and
10300
+ are NEVER truncated.
10301
+
10302
+ 🔴 AN UNRECOGNISED MEMBER `kind` DROPS THE WHOLE BATCH, never the single member (core's normative
10303
+ degradation clause): a batch missing one member renders "yes to N" as "yes to N-1", which is worse
10304
+ than not rendering it. Any `single` offers beside it are complete and honest, so they stay.
10305
+ oneOf:
10306
+ - type: object
10307
+ additionalProperties: false
10308
+ required: [kind, rule, match, command, segment]
10309
+ properties:
10310
+ kind: { type: string, enum: [command] }
10311
+ rule: { type: string, maxLength: 512 }
10312
+ match: { $ref: '#/components/schemas/RuleOfferMatch' }
10313
+ command: { type: string, maxLength: 512 }
10314
+ segment: { type: string, maxLength: 512 }
10315
+ - type: object
10316
+ additionalProperties: false
10317
+ required: [kind, rule, directory, segment]
10318
+ properties:
10319
+ kind: { type: string, enum: [directoryRead] }
10320
+ rule: { type: string, maxLength: 512, description: 'The `Read(//abs-dir/**)` directory-read grant minted from a `cd` segment.' }
10321
+ directory: { type: string, maxLength: 512, description: 'Lexically normalised absolute directory — a rendering seat, so the shell need not re-parse `rule`. This arm carries NO `match` / `command`.' }
10322
+ segment: { type: string, maxLength: 512 }
10323
+ discriminator:
10324
+ propertyName: kind
10325
+
10326
+ RuleOffer:
10327
+ description: >
10328
+ ONE "don't ask again" OPTION on an approval card (core 5.58.0 design/375 §3.1; server >= 7.46.0 emits
10329
+ the same shape on THREE legs — the live `tool_approval` frame, the stored `card_json`, and the durable
10330
+ park row). It is the BREAKING replacement of `RuleSuggestion`, with no alias.
10331
+
10332
+ 🔴 READ `kind`. ORDER IS CONTRACT: a whole-string exact `single` is index 0 when present, a `batch` is
10333
+ always last, core's cardinality is <= 2 and the server's enforcement cap is 4.
10334
+
10335
+ 🔴 TWO ARMS, TWO SELECTION KEYS (see PersistRuleSelection). The `single` arm is redeemed by echoing its
10336
+ `rule` text VERBATIM — the server locates it BY TEXT, so a hand-built string is refused
10337
+ (`rule_not_offered`) by construction. The `batch` arm has no single text to copy (it is ONE yes for
10338
+ every member, with no per-member ticking), so it is redeemed BY INDEX, and the server additionally
10339
+ checks an anti-drift equality before honouring it: the batch re-minted from the adjudicated command
10340
+ must have the SAME member text set as the one the person saw, or the redemption is refused and the way
10341
+ forward is to re-trigger the command for a fresh card.
10342
+
10343
+ 🔴 INDEX SEMANTICS DIFFER PER LEG — the live card leg's index is a legal selection key, the durable
10344
+ queue row's is not (that projection compacts). Both statements are on the two `ruleOffers` properties.
10345
+ oneOf:
10346
+ - type: object
10347
+ additionalProperties: false
10348
+ required: [kind, rule, match, command]
10349
+ properties:
10350
+ kind: { type: string, enum: [single] }
10351
+ rule: { type: string, maxLength: 512, description: 'The canonical rule text to echo back verbatim on redemption (server MAX_RULE_TEXT_CHARS = 512).' }
10352
+ match: { $ref: '#/components/schemas/RuleOfferMatch' }
10353
+ command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
10354
+ - type: object
10355
+ additionalProperties: false
10356
+ required: [kind, rules, uncoveredSegments]
10357
+ properties:
10358
+ kind: { type: string, enum: [batch] }
10359
+ rules:
10360
+ type: array
10361
+ minItems: 1
10362
+ items: { $ref: '#/components/schemas/RuleOfferBatchMember' }
10363
+ description: 'The per-segment members (segment order, deduplicated by rule text). Core contract 1..5; the server tolerates up to 8.'
10364
+ uncoveredSegments:
10365
+ type: integer
10366
+ minimum: 0
10367
+ description: >
10368
+ The HONEST SURPLUS at mint time: how many segments of this compound command would still not be
10369
+ allowed by any rule after redeeming this batch, under the coverage snapshot taken then. `0`
10370
+ means "redeem this and the whole command is covered, as of that snapshot" — a statement about
10371
+ the snapshot, not a lasting guarantee (a concurrent rule revocation makes it stale).
10372
+ uncoveredDetail:
10373
+ type: array
10374
+ items: { $ref: '#/components/schemas/RuleOfferUncoveredDetail' }
10375
+ description: >
10376
+ design/382 §3.5, ADDITIVE — the per-segment reason behind `uncoveredSegments`. When present the
10377
+ row count equals `uncoveredSegments`, but THE COUNT IS THE SOURCE OF TRUTH: the server drops a
10378
+ malformed detail seat without dropping the batch, so an absent `uncoveredDetail` is NOT an
10379
+ assertion that nothing is uncovered.
10380
+ discriminator:
10381
+ propertyName: kind
10382
+
10383
+ PersistedRuleAnchor:
10384
+ type: object
10385
+ description: >
10386
+ server >= 7.48.0 (P-39), ADDITIVE — one self-describing attribution anchor for
10387
+ `ToolApprovalRespondAck.persistedRules`, index-aligned with it.
10388
+
10389
+ The disease it closes: per-entry attribution of `persistedRules` used to be inferable only from
10390
+ POSITION, and position was not provable on the wire (the ack order came from the server's RE-MINTED
10391
+ batch walk, matching the documented display order only by coincidence — the anti-drift check is a SET
10392
+ equality, order is not in it).
10393
+
10394
+ 🔴 BOTH INDEXES LIVE IN THE OFFER SPACE, so a consumer needs no mapping: `offerIndex` echoes the
10395
+ request's `persistRule.batchOfferIndex` VERBATIM (correlation: "this ack really is for the redemption
10396
+ I sent"), and `memberIndex` is the position inside that batch's `rules[]` on the frame. Core's
10397
+ candidate space is deliberately kept off the wire.
10398
+ additionalProperties: false
10399
+ required: [offerIndex, memberIndex, rule]
10400
+ properties:
10401
+ offerIndex: { type: integer, minimum: 0 }
10402
+ memberIndex: { type: integer, minimum: 0 }
10403
+ rule: { type: string, maxLength: 512, description: 'The canonical text this member actually landed as (equal to the same-index entry of `persistedRules`).' }
10404
+
10405
+ PersistRuleSelection:
10406
+ description: >
10407
+ The body field `persistRule` of POST /v1/tool-approvals/{id}/respond — TWO MUTUALLY EXCLUSIVE ARMS,
10408
+ matching the server's `ParsedPersistRule` one for one.
10409
+
10410
+ 🔴 THERE IS NO `kind` ON THE WIRE: that is the server's own post-parse discriminator. The arms are told
10411
+ apart by WHICH FIELD IS PRESENT.
10412
+
10413
+ 🔴 MUTUAL EXCLUSION IS A LOUD 400, not "pick one silently" (server constant
10414
+ PERSIST_RULE_BATCH_EXCLUSIVE_ERROR, verbatim): "persistRule.batchOfferIndex is mutually exclusive with
10415
+ persistRule.rule / persistRule.edited — send exactly one arm". Two arms are two DIFFERENT
10416
+ authorisations; guessing between them is deciding for the person.
10417
+
10418
+ 🔴 MALFORMED IS ALSO A 400, never folded (PERSIST_RULE_BATCH_INDEX_ERROR, verbatim):
10419
+ "persistRule.batchOfferIndex must be a non-negative integer when present" — a negative, fractional or
10420
+ non-numeric index that got coerced points at ANOTHER offer. Likewise
10421
+ PERSIST_RULE_EDITED_FLAG_ERROR: "persistRule.edited must be a boolean when present" — a non-boolean is
10422
+ NEVER silently treated as false, because that would quietly fold a "I want to store free text" back
10423
+ into the candidate arm and answer `rule_not_offered`, leaving the person to think their own rule was
10424
+ rejected. And `rule` must be a non-empty string of at most 512 characters.
10425
+
10426
+ 🔴 NO SCOPE IN THE BODY: a top-level `scope` and a `persistRule.scope` are both loud 400s
10427
+ ("this endpoint does not accept a rule scope — the server mints it from where the approval happened").
10428
+ Letting the caller name a scope is letting a consent given inside project A be written as a standing
10429
+ allow over project B (or globally) — the most expensive widening available on this axis.
10430
+
10431
+ ⚠️ ON `decision: "deny"` THE WHOLE FIELD IS NOT PARSED AT ALL (tolerated, ignored, no 400 — same
10432
+ posture as `updatedInput`): having refused the operation, consenting to it persistently is meaningless.
10433
+ So the refusals above apply to the allow family; only the TOP-LEVEL `scope` 400 is decision-independent.
10434
+ oneOf:
10435
+ - type: object
10436
+ title: candidate-or-edited-arm
10437
+ required: [rule]
10438
+ # 🔴 互斥必须 EXPLICIT(codex R2-F1,验真后修):两臂都是开集,所以每一臂都能把对面臂的键
10439
+ # 当「多余键」收下 —— `oneOf` 数的是「几臂命中」,于是 `{rule, batchOfferIndex:-1}` 会因为
10440
+ # 批臂被 `minimum:0` 判假而**恰好命中一臂**,schema 放行、server 报互斥 400。靠「另一臂恰好
10441
+ # 判假」表达互斥是间接后果,不是约束。
10442
+ not: { required: [batchOfferIndex] }
10443
+ properties:
10444
+ rule:
10445
+ type: string
10446
+ minLength: 1
10447
+ maxLength: 512
10448
+ description: >
10449
+ With `edited` absent or false this is the CANDIDATE ARM: echo back, VERBATIM, the `rule` of the
10450
+ chosen `kind: "single"` entry of the frame's `ruleOffers` (or, against a <= 7.45 server, of
10451
+ `ruleSuggestions`). Do not hand-build it, re-case it or append `:*` — the server locates it by
10452
+ text and refuses otherwise (`rule_not_offered`). With `edited: true` this is the FREE-TEXT ARM
10453
+ and the string is whatever the person typed.
10454
+ edited:
10455
+ type: boolean
10456
+ description: >
10457
+ server >= 7.44.0 (#340), ADDITIVE — capability bit `respondFreeFormRules`, PROBE IT FIRST.
10458
+ `true` says the text above was hand-edited on the card, so the candidate-table equality is not
10459
+ applied; the engine judges it instead through three gates (the shared validator, a COVERAGE
10460
+ gate requiring the edited rule to still hold the command being adjudicated, and scope
10461
+ inherited from this card). On success the ack carries `persistedRule` = the canonical spelling
10462
+ that really landed. An older worker drops the unknown key and judges the text AS A CANDIDATE,
10463
+ answering `rule_not_offered` — honest, but easily misread as "I wrote it wrong".
10464
+
10465
+ ⚠️ Together with `updatedInput` the persistence is refused (`rule_input_edited`): the call that
10466
+ takes effect is the edited one while the candidates came from the ORIGINAL command. The
10467
+ decision itself still stands.
10468
+ # 🔴 刻意**不封闭**(codex R1-F2,验真后修;main 的同一格早有同款注释,换形时别把它丢了):
10469
+ # server 的 `parseToolApprovalResponse` 是手写 parser,只读它认识的键,对象里多给的键**忽略**
10470
+ # (与 `updatedInput` 同一条宽收姿势)。写 `additionalProperties:false` 会让照 spec 生成的客户端
10471
+ # 拒发一个 server 其实会受理的请求 —— 例如一个把整只候选对象 `{rule, match, command}` 原样转发的
10472
+ # 存量壳,重新 codegen 之后就发不出去了。那是我们自己造的漂移,不是上游的约束。
10473
+ # 互斥**不靠封闭表达**:`oneOf` 的语义是「恰好命中一臂」,两臂各自 `required` 不同 ⇒
10474
+ # `{rule, batchOfferIndex}` 同时命中两臂 ⇒ 判假。开集与互斥两件事各自成立。
10475
+ - type: object
10476
+ title: batch-arm
10477
+ required: [batchOfferIndex]
10478
+ # 同上一臂:`{batchOfferIndex:0, edited:false}` 曾因为文本臂缺 `rule` 判假而恰好命中本臂。
10479
+ not: { anyOf: [{ required: [rule] }, { required: [edited] }] }
10480
+ properties:
10481
+ batchOfferIndex:
10482
+ type: integer
10483
+ minimum: 0
10484
+ description: >
10485
+ server >= 7.46.0 (design/377), ADDITIVE — capability bit `respondBatchRuleOffers`, PROBE IT
10486
+ FIRST. The person ticked a `kind: "batch"` offer; the value is that offer's INDEX in the
10487
+ frame's `ruleOffers`. A conjunctive batch has no single text to copy, so the index is the only
10488
+ possible selection key.
10489
+
10490
+ 🔴 NARROWLY OPENED TO BATCH ONLY: an index that points nowhere, or points at a `single`, yields
10491
+ `rulePersisted:false` + `rule_not_offered` — what was never rendered can never be redeemed, and
10492
+ the `single` arm's anti-forgery anchor is its TEXT equality, which an index may not bypass.
10493
+ 🔴 An older worker drops the unknown key and then fails the `rule` shape check with a 400
10494
+ — honest, but it cannot tell that a batch was ticked. If the capability bit is absent, do not
10495
+ render a redeemable batch affordance.
10496
+ # 同上一臂:刻意不封闭(理由逐字见那一段)。
10497
+
10166
10498
  RuleRefusalReason:
10167
10499
  type: string
10168
10500
  description: >
10169
10501
  Why a "don't ask again" did NOT get persisted (#154 车二; the server's `RuleRefusalReason`, verbatim).
10170
10502
  A CLOSED set so a shell can branch instead of matching prose. The two naming styles are NOT a typo:
10171
- the four hyphenated members are derived from the consent lane's own refusal reasons, the four
10172
- underscored ones are minted by the server's gates.
10503
+ the SEVEN hyphenated members are derived from the consent lane's own refusal reasons
10504
+ (server `CardRulePersisted`), the SIX underscored ones are minted by the server's gates.
10505
+
10506
+ `edit-unsupported` and `edit-rejected` (server >=7.44.0, #340) belong to the FREE-TEXT arm and ask the
10507
+ shell for OPPOSITE things: the first means the edit affordance does not work on this card (no binding
10508
+ anchor, or the deployment has it off) so STOP RENDERING THE INPUT BOX; the second means the engine's
10509
+ gates refused this particular text, so "fix it and try again" — and since #345 (server >=7.51.0) the
10510
+ independently decidable half of that judgement happens BEFORE the decision settles (a 400 with the card
10511
+ still alive), so trying again really works. `scope-unresolved` (core 7.0.0 #490②) is the death of the
10512
+ silent global: with neither an explicit scope nor a cwd the lane refuses loudly instead of defaulting to
10513
+ global, which makes it a ROUTINE answer on a deployment that has not wired `ruleScopeRootFor`.
10173
10514
 
10174
10515
  🔴 A refusal NEVER flips the decision. The person's "allow this once" has already taken effect and
10175
10516
  reached the engine; turning the whole respond into a 4xx would make a real allow vanish. The honest
@@ -10200,16 +10541,30 @@ components:
10200
10541
  note on `rule_lane_unavailable` above already says never to disable the feature globally on it, and
10201
10542
  the same holds here. The one correct source for "does this worker serve rules at all" is
10202
10543
  `capabilities.permissionRules`. Keep offering "don't ask again" on the next ask; just not on this one.
10544
+ `rule_material_absent` (server >=7.48.0, #363) — DURABLE (PARKED) LATE-DECISION LEG ONLY: THIS ROW
10545
+ carries no material to mint a rule from. Three causes, one handling: the ask never entered the rule
10546
+ lane (governance-forced / no owner / the engine minted no candidate / the command bytes are not
10547
+ readable out of the args), the row predates those columns, or the card on the row will not parse.
10548
+ 🔴 ALSO SPLIT FROM `rule_lane_unavailable` ON PURPOSE, for the same reason as the previous member:
10549
+ that word says "this deployment serves no rules at all" and a shell may stop rendering the affordance
10550
+ on it, whereas the truth here is "the lane is fine, only THIS ROW has no material". Folding them lets
10551
+ one late park answer condemn a healthy deployment's whole lane. Silently ignoring it is worse still
10552
+ (no silent fail-open on the approval axis): a person who ticked "don't ask again" and saw nothing
10553
+ happen will not tick it twice.
10203
10554
  enum:
10204
10555
  - no-candidates
10205
10556
  - unknown-candidate
10206
10557
  - confirm-refused
10207
10558
  - redeem-refused
10559
+ - edit-unsupported
10560
+ - edit-rejected
10561
+ - scope-unresolved
10208
10562
  - rule_lane_unavailable
10209
10563
  - rule_input_edited
10210
10564
  - rule_not_offered
10211
10565
  - rule_store_error
10212
10566
  - rule_governance_forced
10567
+ - rule_material_absent
10213
10568
 
10214
10569
  ApprovalRiskAxes:
10215
10570
  type: object
@@ -11864,6 +12219,44 @@ components:
11864
12219
  next ask" (`rule_input_edited`) from "there was nothing redeemable on this card"
11865
12220
  (`rule_lane_unavailable`). 🔴 `rule_lane_unavailable` covers four causes, two of them per-card — do
11866
12221
  NOT read it as "this deployment has no rule lane" (see the RuleRefusalReason schema).
12222
+ persistedRule:
12223
+ type: string
12224
+ maxLength: 512
12225
+ description: >
12226
+ server >= 7.44.0 (#340), ADDITIVE — FREE-TEXT ARM ONLY: the CANONICAL SPELLING of the rule that
12227
+ actually landed (core may normalise `Bash(adb *)` into `Bash(adb:*)`). Echo THIS back to the
12228
+ person, not the bytes they typed into the box.
12229
+
12230
+ 🔴 ABSENCE HAS THREE MEANINGS, none of them "the rule is gone": (1) this respond used the
12231
+ CANDIDATE arm — the server's `editedArmEcho` is the single mint point for this key, so a candidate
12232
+ respond's ack is byte-for-byte unchanged; (2) nothing landed (read `rulePersisted` / `ruleRefusal`);
12233
+ (3) the respond carried no `persistRule` at all.
12234
+ persistedRules:
12235
+ type: array
12236
+ items: { type: string, maxLength: 512 }
12237
+ description: >
12238
+ server >= 7.46.0 (design/377), ADDITIVE — BATCH ARM ONLY: the canonical text of EVERY member that
12239
+ landed in this one redemption. A conjunctive batch is one yes for the whole set; if any member is
12240
+ refused the ack is `rulePersisted:false` + `ruleRefusal:"redeem-refused"` and the members that DID
12241
+ land are not rolled back (their dots are on record and a re-triggered card dedupes idempotently).
12242
+
12243
+ 🔴 ORDER: from server 7.48.0 on, the DISPLAY order of the requested offer
12244
+ (`ruleOffers[batchOfferIndex].rules`) is a PROMISE, not a coincidence; on 7.46-7.47 it inherited the
12245
+ server's re-minted walk order instead. For per-entry attribution read `persistedRuleAnchors`
12246
+ rather than inferring it from position.
12247
+ 🔴 Not the same thing as `persistedRule`: the singular key belongs to the single/edited arm, the
12248
+ plural one to the batch arm, and the two never appear together.
12249
+ persistedRuleAnchors:
12250
+ type: array
12251
+ items: { $ref: '#/components/schemas/PersistedRuleAnchor' }
12252
+ description: >
12253
+ server >= 7.48.0 (P-39), ADDITIVE and with NO capability bit — the per-entry, self-describing
12254
+ attribution anchors for `persistedRules`, INDEX-ALIGNED with it.
12255
+
12256
+ Minted ONLY on the batch arm (the single/edited arm has no member coordinates to speak of), and
12257
+ omitted WHOLESALE whenever the server cannot prove attribution — an honest absence, never a
12258
+ fabricated index. A consumer that sees it absent knows this ack can only be read positionally, and
12259
+ `persistedRules` itself is unchanged to the byte (older consumers ignore the unknown key).
11867
12260
  noteRecorded:
11868
12261
  type: boolean
11869
12262
  description: >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/sdk",
3
- "version": "8.1.0",
3
+ "version": "8.2.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",