@sema-agent/sdk 8.1.0 → 8.3.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/README.md +309 -0
- package/dist/events.d.ts +5 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/resources/tool-approvals.d.ts +407 -16
- package/dist/resources/tool-approvals.d.ts.map +1 -1
- package/dist/resources/tool-approvals.js +175 -1
- package/dist/resources/tool-approvals.js.map +1 -1
- package/dist/types.d.ts +293 -0
- package/dist/types.d.ts.map +1 -1
- package/openapi.yaml +745 -27
- package/package.json +1 -1
package/openapi.yaml
CHANGED
|
@@ -1453,7 +1453,7 @@ paths:
|
|
|
1453
1453
|
required: [sessionId, leafId]
|
|
1454
1454
|
properties:
|
|
1455
1455
|
sessionId: { type: string }
|
|
1456
|
-
leafId: { type: string,
|
|
1456
|
+
leafId: { type: ["string", "null"], description: "null = session exists but has no entries yet." }
|
|
1457
1457
|
'304': { description: 'Leaf unchanged since the presented ETag (no body).' }
|
|
1458
1458
|
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
1459
1459
|
'404': { $ref: '#/components/responses/NotFound' }
|
|
@@ -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
|
-
|
|
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
|
-
|
|
3370
|
-
`rule`
|
|
3371
|
-
the
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
|
@@ -6661,6 +6684,217 @@ components:
|
|
|
6661
6684
|
and the history PULL answers the no-oracle 404 `not_found.blob`, while the REST of the sync
|
|
6662
6685
|
surface keeps working.
|
|
6663
6686
|
|
|
6687
|
+
# ─── S-139(sdk 8.3.0):对 server 7.60.0 的整体重对账补齐的 19 位 ───────────────────────
|
|
6688
|
+
# 行号 = server 7.60.0 树的 `src/http/routes/capabilities.ts`。语义与谓词的全文在
|
|
6689
|
+
# `packages/sdk/src/types.ts` 的 `Capabilities` 同名成员上(逐条亲读 server 源码转述);
|
|
6690
|
+
# 这里只留 codegen 需要的形与一句定性。机械对账门 = `test/server-keyset-parity.test.ts`。
|
|
6691
|
+
deviceExecutor:
|
|
6692
|
+
description: >
|
|
6693
|
+
device lane (capabilities.ts:162). Object OR `false` — same tri-state family as `fleet`.
|
|
6694
|
+
Predicate = `deps.deviceHub` present, byte-identical to the `GET /v1/device/ws` upgrade
|
|
6695
|
+
handler's mount condition. `wsPath` is deliberately on the wire (hard-coding it downstream
|
|
6696
|
+
would turn a path change into a cross-repo breaking change). Device COUNT / online state are
|
|
6697
|
+
deliberately NOT advertised (that is the `/v1/devices/*` admin face).
|
|
6698
|
+
oneOf:
|
|
6699
|
+
- type: boolean
|
|
6700
|
+
enum: [false]
|
|
6701
|
+
- type: object
|
|
6702
|
+
required: [enabled, protocolVersion, maxInflightPerDevice, wsPath]
|
|
6703
|
+
additionalProperties: true
|
|
6704
|
+
properties:
|
|
6705
|
+
enabled: { type: boolean, enum: [true] }
|
|
6706
|
+
protocolVersion: { type: integer }
|
|
6707
|
+
maxInflightPerDevice: { type: integer }
|
|
6708
|
+
wsPath: { type: string }
|
|
6709
|
+
memoryEngine:
|
|
6710
|
+
description: >
|
|
6711
|
+
Memory ENGINE posture (capabilities.ts:210 = `projectMemoryEngineCapability`). Object = the
|
|
6712
|
+
memory face is really lit, `false` = dark. NOT the same thing as `memory`/`memoryWrite` (those
|
|
6713
|
+
advertise the RETIRED MemoryStore HTTP verbs and stay false forever) — the engine has NO HTTP
|
|
6714
|
+
verbs, so a true here does NOT mean there are memory read/write endpoints to call.
|
|
6715
|
+
`vectorMode: null` = this leg has no rung indicator (the `file` engine), NOT "the rung is null".
|
|
6716
|
+
oneOf:
|
|
6717
|
+
- type: boolean
|
|
6718
|
+
enum: [false]
|
|
6719
|
+
- type: object
|
|
6720
|
+
required: [backend, vectorMode]
|
|
6721
|
+
additionalProperties: true
|
|
6722
|
+
properties:
|
|
6723
|
+
backend: { type: string, enum: [file, pg, tidb] }
|
|
6724
|
+
# 🔴 openapi **3.1**:可空用 `type: [T, "null"]`,`nullable` 不是 3.1 关键字(会被静默忽略)。
|
|
6725
|
+
# `null` 也必须进 enum,否则真校验器把 file 引擎的常态答判非法。
|
|
6726
|
+
vectorMode: { type: ["string", "null"], enum: [native, portable, lexical, null] }
|
|
6727
|
+
sql:
|
|
6728
|
+
type: ["object", "null"]
|
|
6729
|
+
additionalProperties: true
|
|
6730
|
+
description: >
|
|
6731
|
+
This deployment's SQL transaction read semantics (capabilities.ts:216 =
|
|
6732
|
+
`projectSqlEngineCapability`), read back AFTER connection initialization set it — same source as
|
|
6733
|
+
the operator `GET /v1/diagnostics/wiring` `sqlEngine` section. `null` = NO SQL backend here
|
|
6734
|
+
(env-only worker / local file backend), NOT "could not read it". `txnMode: null` = this engine
|
|
6735
|
+
has no such indicator, NOT "optimistic".
|
|
6736
|
+
required: [engine, isolation, txnMode]
|
|
6737
|
+
properties:
|
|
6738
|
+
engine: { type: string, enum: [tidb, innodb, pg] }
|
|
6739
|
+
isolation: { type: string }
|
|
6740
|
+
txnMode: { type: ["string", "null"], enum: [pessimistic, null] }
|
|
6741
|
+
sessionBackgroundable:
|
|
6742
|
+
type: boolean
|
|
6743
|
+
description: >
|
|
6744
|
+
The REMOTE lane can detach (capabilities.ts:233 = `Boolean(deps.runStore)`): `POST
|
|
6745
|
+
/v1/tasks/stream` with `x-detach-on-disconnect: true` survives client disconnect; come back via
|
|
6746
|
+
the `X-Task-Id` response header and `GET /v1/runs/{id}` + `/events`. Deployment half only — the
|
|
6747
|
+
per-request half (caller must pass `sessionId`) is not in this bit. Scope is the REMOTE lane;
|
|
6748
|
+
never repurpose it to disable local backgrounding.
|
|
6749
|
+
runMemoryCaptureOptOut:
|
|
6750
|
+
type: boolean
|
|
6751
|
+
description: >
|
|
6752
|
+
`POST /v1/runs/{id}/memory/capture-optout` mid-run flip verb is present (capabilities.ts:237 =
|
|
6753
|
+
`Boolean(deps.runStore)`). Absent ⇒ do not render the affordance. True only promises the
|
|
6754
|
+
ENDPOINT exists, not that THIS run is live (non-live ⇒ 409 with a signposting body).
|
|
6755
|
+
memoryBundle:
|
|
6756
|
+
type: boolean
|
|
6757
|
+
description: >
|
|
6758
|
+
Governance carry-out bundle pair `POST /v1/memory/{export,import}` (operator lane) —
|
|
6759
|
+
capabilities.ts:261, two conjuncts: both engine seams wired AND `operatorPrincipals` NON-EMPTY
|
|
6760
|
+
(an empty roster makes those routes 403 for every identity). True does not promise the backend
|
|
6761
|
+
has the bundle composite face (core then refuses loudly with `memory.export_incomplete`).
|
|
6762
|
+
memoryCompliance:
|
|
6763
|
+
type: boolean
|
|
6764
|
+
description: >
|
|
6765
|
+
Provenance / erasure compliance pair `GET /v1/memory/entries/{entryId}/provenance` and
|
|
6766
|
+
`POST /v1/memory/erase` (operator lane) — capabilities.ts:273, same two-conjunct family as
|
|
6767
|
+
`memoryBundle`. Deliberately NOT the same source as `memoryBundle` (a deployment can have the
|
|
6768
|
+
bundle composite face without control-plane ownership, and vice versa).
|
|
6769
|
+
memoryOrigin:
|
|
6770
|
+
type: boolean
|
|
6771
|
+
description: >
|
|
6772
|
+
External-origin marking face (three routes under `/v1/memory/origin/*`, operator lane) —
|
|
6773
|
+
capabilities.ts:287, same two-conjunct family. Same predicate as `memoryCompliance` TODAY but a
|
|
6774
|
+
deliberately separate bit (two products; a deployment may want only one). True does not promise
|
|
6775
|
+
the `GET …/external` answer is complete — there is no scope enumeration face, so an empty answer
|
|
6776
|
+
must never be read as "the store is clean".
|
|
6777
|
+
memoryConsolidationDriver:
|
|
6778
|
+
type: boolean
|
|
6779
|
+
description: >
|
|
6780
|
+
Consolidation valve pair (`POST /v1/admin/memory/consolidation/run`, `GET
|
|
6781
|
+
/v1/admin/memory/consolidation`, operator lane) — capabilities.ts:301. FALSE ⇒ those routes are
|
|
6782
|
+
**404** (whole domain unmounted), NOT 501: do not branch on 501 here. Contrast
|
|
6783
|
+
`memoryOptOutGrant`, whose false arm IS a 501.
|
|
6784
|
+
memoryOptOutGrant:
|
|
6785
|
+
type: boolean
|
|
6786
|
+
description: >
|
|
6787
|
+
memory-capture opt-out GRANT table admin (four routes under `/v1/admin/memory-optout`,
|
|
6788
|
+
operator-only) — capabilities.ts:307. FALSE ⇒ **501** `capability.memory_optout_grant_required`
|
|
6789
|
+
(change the deployment shape), as opposed to `memoryConsolidationDriver`'s 404 (turn the knob on).
|
|
6790
|
+
permissionModeAuto:
|
|
6791
|
+
type: object
|
|
6792
|
+
additionalProperties: true
|
|
6793
|
+
description: >
|
|
6794
|
+
Default-state disclosure for `permissionMode: "auto"` (capabilities.ts:463; adjudicated by
|
|
6795
|
+
`src/auto-mode-face.ts`). The parts are deliberately NOT folded into one boolean — "this binary
|
|
6796
|
+
does not know auto" and "it knows auto but this box has no entitlement source" would collapse to
|
|
6797
|
+
the same false, and a shell must do different things for those. `reason` is present ONLY when
|
|
6798
|
+
`armed` is false; read its vocabulary as an OPEN set (server already grew it from four words to
|
|
6799
|
+
six). `model` is omitted when the classifier route cannot be resolved (the server logs the
|
|
6800
|
+
engine's refusal rather than 500-ing the read face). `?permissionMode=<five words>` folds the
|
|
6801
|
+
caller's INTENT into the verdict; anything outside the five words is a 400
|
|
6802
|
+
`request.field_invalid`.
|
|
6803
|
+
# 🔴 required 只有**三位**:武装三键(intentArming / armed / reason)是 server >= 7.57.0 (S-80)
|
|
6804
|
+
# 才有的,而本包的支持地板是 3.0.0 => every 7.x <= 7.56.0 server is inside the promised face and
|
|
6805
|
+
# emits ONLY these three (verified verbatim against server tags v7.50.0 / v7.56.0). Requiring the
|
|
6806
|
+
# arming triple would make a SUPPORTED server's legitimate response invalid.
|
|
6807
|
+
required: [accepted, classifierSeat, entitlementSource]
|
|
6808
|
+
properties:
|
|
6809
|
+
accepted: { type: boolean, enum: [true] }
|
|
6810
|
+
classifierSeat: { type: boolean }
|
|
6811
|
+
entitlementSource: { type: boolean }
|
|
6812
|
+
intentArming: { type: boolean, description: 'server >= 7.57.0 only. ABSENT together with `armed`/`reason` on <= 7.56.0.' }
|
|
6813
|
+
armed:
|
|
6814
|
+
type: boolean
|
|
6815
|
+
description: >
|
|
6816
|
+
server >= 7.57.0 only. Read as THREE states, not two: true = will arm; false = will not, and
|
|
6817
|
+
`reason` is then present; ABSENT = an older server that does not report arming at all — never
|
|
6818
|
+
fold that absence into false (it would render a 7.50 box that IS arming as "auto == default").
|
|
6819
|
+
reason:
|
|
6820
|
+
type: string
|
|
6821
|
+
enum: [mode_not_auto, deployment_incapable, org_denied, local_denied, settings_denied, resolver_fault]
|
|
6822
|
+
model: { type: string }
|
|
6823
|
+
readFace:
|
|
6824
|
+
type: ["string", "null"]
|
|
6825
|
+
enum: [open, roots, null]
|
|
6826
|
+
description: >
|
|
6827
|
+
The EFFECTIVE READ containment rung this deployment explicitly declared (capabilities.ts:484 =
|
|
6828
|
+
`deps.config.readFace ?? null`; env `READ_FACE` or config-center's `readFace.face` — same config
|
|
6829
|
+
key, so this bit does not distinguish org-pushed from local). `null` = this deployment pinned
|
|
6830
|
+
NOTHING and the engine default takes over; the server deliberately does NOT fold `null` into
|
|
6831
|
+
`roots` (copying an upstream default onto the wire becomes a lie the day core changes it).
|
|
6832
|
+
Minimal disclosure — the deny table itself is never on the capability face. There is NO
|
|
6833
|
+
per-request `readFace` field on TaskRequest.
|
|
6834
|
+
callerCwd:
|
|
6835
|
+
type: boolean
|
|
6836
|
+
description: >
|
|
6837
|
+
Whether a caller-supplied `cwd` is actually HONORED (capabilities.ts:541 =
|
|
6838
|
+
`cwdHonored(config) || deviceCwdHonored(config)`) — single-user `host` lane OR the `device` lane.
|
|
6839
|
+
Byte-identical predicate to the submit-side gate in `boot/resolve-spec.ts`. Prefer this bit;
|
|
6840
|
+
when absent (older worker) fall back to `projectContext` — that is the verbatim pre-bit behavior.
|
|
6841
|
+
a2a:
|
|
6842
|
+
type: boolean
|
|
6843
|
+
description: >
|
|
6844
|
+
The A2A CLIENT read face `GET /v1/sessions/{id}/a2a` exists (capabilities.ts:555, unconditional
|
|
6845
|
+
`true` — like `mcp`): with no peers configured it returns an honest empty panel, so there is no
|
|
6846
|
+
501 path to gate on. Deliberately a DIFFERENT predicate from `a2aInjection`.
|
|
6847
|
+
a2aInjection:
|
|
6848
|
+
type: boolean
|
|
6849
|
+
description: >
|
|
6850
|
+
This deployment HONORS a caller-supplied `body.a2aPeers` (capabilities.ts:556 =
|
|
6851
|
+
`a2aInjectionHonored(config)`; three vetoes owned by `task-a2a.ts`: single-user AND `a2a`
|
|
6852
|
+
unlocked AND compliance permits). FALSE is a FIELD gate, not a 4xx: the request still 200s and
|
|
6853
|
+
the field is warned + ignored — EXCEPT under a config lock, where submit refuses 400
|
|
6854
|
+
`config.locked_key`.
|
|
6855
|
+
a2aServe:
|
|
6856
|
+
type: boolean
|
|
6857
|
+
description: >
|
|
6858
|
+
Server-as-peer knob (capabilities.ts:564 = `config.a2aServe !== undefined`): true ⟺
|
|
6859
|
+
`GET /.well-known/agent-card.json` and `POST /v1/a2a` exist; false ⟺ both 404. Scope is ROUTE
|
|
6860
|
+
EXISTENCE, not "every method runs" — `message/send` / `tasks/get` additionally need a durable run
|
|
6861
|
+
store (without it they answer a named JSON-RPC `-32004`).
|
|
6862
|
+
agentRoster:
|
|
6863
|
+
type: boolean
|
|
6864
|
+
description: >
|
|
6865
|
+
`GET /v1/agents/roster` background-agent roster read face (capabilities.ts:640 =
|
|
6866
|
+
`Boolean(deps.backgroundAgentStore)`, byte-identical to that route's 501 gate). Scope is the
|
|
6867
|
+
ENUMERATION face (content-free projection); subagent OUTPUT bodies go through
|
|
6868
|
+
`/v1/runs/{id}/subagents/{handle}/output`, gated by `subagentOutput`.
|
|
6869
|
+
retention:
|
|
6870
|
+
type: ["object", "null"]
|
|
6871
|
+
additionalProperties: true
|
|
6872
|
+
description: >
|
|
6873
|
+
Managed-retention deployment facts (capabilities.ts:654). `null` = the sweep lane is off
|
|
6874
|
+
(default). `mode: audit-only` = it runs and judges but calls NO destructive method; `enforce` =
|
|
6875
|
+
it really deletes. The predicate looks only at config because a box with `intervalSec > 0` and no
|
|
6876
|
+
real executor CANNOT BOOT (`boot/retention-lane.ts` refuses at startup).
|
|
6877
|
+
required: [mode, maxAgeDays]
|
|
6878
|
+
properties:
|
|
6879
|
+
mode: { type: string, enum: [audit-only, enforce] }
|
|
6880
|
+
maxAgeDays: { type: integer }
|
|
6881
|
+
workflowsGate:
|
|
6882
|
+
type: object
|
|
6883
|
+
additionalProperties: true
|
|
6884
|
+
description: >
|
|
6885
|
+
Self-orchestration denial disclosure (capabilities.ts:739). The two parts are deliberately not
|
|
6886
|
+
folded: `engineCan` = core's `workflowsCapability`; `denial` = the DEPLOYMENT-level admission
|
|
6887
|
+
refusal (closed set, today's only member `entitlement_resolver_absent`), `null` = this layer does
|
|
6888
|
+
not refuse. Non-null means the engine is there but this deployment can grant it to NO principal
|
|
6889
|
+
(half-configured) — signpost the OPERATOR, do not tell the user to retry. `denial: null` does NOT
|
|
6890
|
+
mean "you are authorized" (that is per-principal). `workflows` is the PRODUCT of the two factors.
|
|
6891
|
+
required: [engineCan, denial]
|
|
6892
|
+
properties:
|
|
6893
|
+
engineCan: { type: boolean }
|
|
6894
|
+
denial:
|
|
6895
|
+
type: ["string", "null"]
|
|
6896
|
+
enum: [entitlement_resolver_absent, null]
|
|
6897
|
+
|
|
6664
6898
|
SkillSpec:
|
|
6665
6899
|
type: object
|
|
6666
6900
|
description: A per-request skill (passed as an object; core-native TaskSpec.skills shape).
|
|
@@ -8023,6 +8257,27 @@ components:
|
|
|
8023
8257
|
display order; an EXACT entry, when present, is always first. core contract caps candidates at 2;
|
|
8024
8258
|
the server enforces a tolerance cap of 4 on read-back — lay out for 4. Absent = pre-column row
|
|
8025
8259
|
(parked before the column existed) OR no candidates; render both as "no candidates", never an error.
|
|
8260
|
+
ruleOffers:
|
|
8261
|
+
type: array
|
|
8262
|
+
minItems: 1
|
|
8263
|
+
items: { $ref: '#/components/schemas/RuleOffer' }
|
|
8264
|
+
description: >
|
|
8265
|
+
server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union successor of the retired
|
|
8266
|
+
`ruleSuggestions` above on the DURABLE queue row (core `PendingAction.ruleOffers`, the same
|
|
8267
|
+
contract as the sync leg's `AskRequest.ruleOffers`). A >= 7.46.0 row carries only the new key;
|
|
8268
|
+
pre-5.58 rows fail the new narrow read entry by entry and land as "no supply" (no compatibility
|
|
8269
|
+
read — a stale schema means re-ask).
|
|
8270
|
+
|
|
8271
|
+
🔴 THE INDEX ON THIS LEG IS NOT A SELECTION KEY. This projection drops malformed offers one by one
|
|
8272
|
+
and COMPACTS, so an excision shifts every later index forward. That is only sound because this leg
|
|
8273
|
+
has NO redemption mouth (POST /v1/approvals/{sessionId}/decide takes no rule field). Using it as
|
|
8274
|
+
`persistRule.batchOfferIndex` would redeem a different rule than the one the person clicked. The
|
|
8275
|
+
live card leg is the opposite (pure prefix truncation, no per-entry drops) and is where redemption
|
|
8276
|
+
actually happens.
|
|
8277
|
+
|
|
8278
|
+
🔴 DISPLAY / TRIAGE ONLY, and ABSENT MEANS THERE REALLY IS NO SUPPLY (rule lane unarmed, the
|
|
8279
|
+
command yields no rule, no rule could silence this ask, or an old row) — never "the projection
|
|
8280
|
+
dropped it", which is precisely the bug this key exists to close. Never `null`, never `[]`.
|
|
8026
8281
|
governanceForced:
|
|
8027
8282
|
type: boolean
|
|
8028
8283
|
enum: [true]
|
|
@@ -9889,6 +10144,38 @@ components:
|
|
|
9889
10144
|
replica ⇒ redeemable; after a restart / on another replica that `approvalId` 404s and the person
|
|
9890
10145
|
must use the durable decide leg, which carries NO rule field in v1 (its body is strict — an extra
|
|
9891
10146
|
`persistRule` is a loud 400, not a silent drop).
|
|
10147
|
+
ruleOffers:
|
|
10148
|
+
type: array
|
|
10149
|
+
minItems: 1
|
|
10150
|
+
items: { $ref: '#/components/schemas/RuleOffer' }
|
|
10151
|
+
description: >
|
|
10152
|
+
server >= 7.46.0 (core 5.58.0, design/375), "tool_approval" only — the ENGINE-minted "don't ask
|
|
10153
|
+
again" OPTIONS for this ask, as a DISCRIMINATED UNION (`single` | `batch`). It is the BREAKING
|
|
10154
|
+
REPLACEMENT of `ruleSuggestions` above: a >= 7.46.0 engine mints the old key nowhere, there is no
|
|
10155
|
+
alias, and the two keys are declared side by side only so one type surface covers both generations.
|
|
10156
|
+
|
|
10157
|
+
🔴 READ `kind` TO TELL THE ARMS APART, never a positional assumption. ORDER IS CONTRACT: a
|
|
10158
|
+
whole-string exact `single` is index 0 when present and a `batch` is always last; core's own
|
|
10159
|
+
cardinality is <= 2 while the server enforces a tolerance cap of 4.
|
|
10160
|
+
|
|
10161
|
+
🔴 INDEX SEMANTICS DIFFER PER LEG, and only one leg can redeem. On THIS leg (and on the
|
|
10162
|
+
`approval_request` card and the durable replay of both) the server prefix-truncates and never drops
|
|
10163
|
+
an entry, so the index equals core's offer index and IS a legal selection key for
|
|
10164
|
+
`persistRule.batchOfferIndex`. On the durable queue row (PendingCheckpoint.ruleOffers) the server
|
|
10165
|
+
drops malformed entries and COMPACTS, so indexes shift there and that leg has no redemption mouth
|
|
10166
|
+
at all. A consumer that drops entries itself must keep every survivor's ORIGINAL wire index, or
|
|
10167
|
+
suppress its persistence actions entirely (core's normative clause) — a compacted index means the
|
|
10168
|
+
k-th option the person clicked is not the k-th rule the server redeems.
|
|
10169
|
+
|
|
10170
|
+
🔴 PRESENCE IS THE PROMISE (same as `ruleSuggestions`): present only where a permission-rule store
|
|
10171
|
+
is really wired. The converse does not hold — a wired deployment still omits it for mandated /
|
|
10172
|
+
requiresRealApproval / shadowed / hook-produced / inherited-unresolved / ancestor-resolved /
|
|
10173
|
+
anonymous asks. Absence reads as "THIS CARD has no such option", never as "this deployment has no
|
|
10174
|
+
rule lane" (that is `capabilities.permissionRules`) and never as "your rule broke" (that is
|
|
10175
|
+
`persistedRuleShadowed`).
|
|
10176
|
+
|
|
10177
|
+
⚠️ REDEMPTION SCOPE is the live respond leg keyed by this frame's `approvalId` only; a replayed
|
|
10178
|
+
offer on another replica is not a promise that it is still redeemable.
|
|
9892
10179
|
persistedRuleShadowed:
|
|
9893
10180
|
type: string
|
|
9894
10181
|
description: >
|
|
@@ -10013,6 +10300,53 @@ components:
|
|
|
10013
10300
|
parentToolCallId: { type: string }
|
|
10014
10301
|
depth: { type: integer }
|
|
10015
10302
|
agentName: { type: string }
|
|
10303
|
+
# ─── S-139(sdk 8.3.0):对 server 7.60.0 的整体重对账补齐的 9 位 ────────────────────────
|
|
10304
|
+
# 长 description 的单一真源在 `packages/sdk/src/resources/tool-approvals.ts` 的同名成员上;
|
|
10305
|
+
# 这里只留 codegen 需要的形与硬条款一句。机械对账门 = `test/server-keyset-parity.test.ts`。
|
|
10306
|
+
requiresRealApproval:
|
|
10307
|
+
type: boolean
|
|
10308
|
+
enum: [true]
|
|
10309
|
+
description: >
|
|
10310
|
+
server >= 7.30.0 (core 5.37, #283), "tool_approval" only, ADDITIVE: the SAFETY-class provenance
|
|
10311
|
+
bit core stamps at the gate. Present (true) or ABSENT — never encoded as false. Deliberately a
|
|
10312
|
+
DIFFERENT contract from the card's `risk.requiresRealApproval` (which is an always-present
|
|
10313
|
+
boolean): the card form needs the durable ask store, and this key is what lets a LIVE-ONLY
|
|
10314
|
+
deployment read the bit at all. Present ⇒ every automatic allowance (remembered rules,
|
|
10315
|
+
bypassPermissions posture) stands down.
|
|
10316
|
+
origin: { $ref: '#/components/schemas/AskOrigin' }
|
|
10317
|
+
inputHasBidi:
|
|
10318
|
+
type: boolean
|
|
10319
|
+
enum: [true]
|
|
10320
|
+
description: >
|
|
10321
|
+
server >= 7.45.0 (E-14), "tool_approval" only, ADDITIVE: the TO-BE-EXECUTED input (`args`)
|
|
10322
|
+
contains Unicode bidi control characters — what the eye reads may not be the byte order that
|
|
10323
|
+
runs. DISCLOSURE bit, bytes unchanged (sanitising would change the bytes about to execute, which
|
|
10324
|
+
is WORSE than not disclosing); rendering is the shell's. Present (true) or ABSENT; absence !=
|
|
10325
|
+
"verified clean". Computed BEFORE the byte cap, so it can ride a frame whose `args` were omitted.
|
|
10326
|
+
expiresAtMs:
|
|
10327
|
+
type: integer
|
|
10328
|
+
description: >
|
|
10329
|
+
server >= 7.34.0 (#288), "tool_approval" only, ADDITIVE: window triple (with `expiresInMs` and
|
|
10330
|
+
`serverNowMs`, same names/semantics as on ApprovalRequestFrame) so the LEGACY frame family can
|
|
10331
|
+
render a countdown too. `expiresAtMs` = this ask's absolute deadline on the server clock. The
|
|
10332
|
+
three keys are BORN AND ABSENT TOGETHER — any frame that gets emitted carries all three.
|
|
10333
|
+
expiresInMs:
|
|
10334
|
+
type: integer
|
|
10335
|
+
description: 'Window triple (see expiresAtMs): max(0, expiresAtMs - serverNowMs), computed at mint time.'
|
|
10336
|
+
serverNowMs:
|
|
10337
|
+
type: integer
|
|
10338
|
+
description: 'Window triple (see expiresAtMs): the server clock at FRAME MINT time — the countdown anchor; use it to correct local clock skew.'
|
|
10339
|
+
ruleOffersAbsence: { $ref: '#/components/schemas/RuleOffersAbsence' }
|
|
10340
|
+
denialLimitFallback: { $ref: '#/components/schemas/DenialLimitFallback' }
|
|
10341
|
+
parked:
|
|
10342
|
+
type: boolean
|
|
10343
|
+
enum: [true]
|
|
10344
|
+
description: >
|
|
10345
|
+
server >= 7.42.0 (#329), "tool_approval_complete" ONLY, ADDITIVE: the explicit discriminator for
|
|
10346
|
+
the PARK sense of `outcome: "expired"` (which is one word with three landings). Present ⇔ THIS ask
|
|
10347
|
+
settled through the park route ⇒ render "moved to the background queue", not "the card died".
|
|
10348
|
+
🔴 ABSENCE MUST NOT BE READ AS "really denied": a store-less deployment, a deny policy, and a
|
|
10349
|
+
sibling VOIDed in a batch all emit no such key — absence only means "no park evidence".
|
|
10016
10350
|
outcome:
|
|
10017
10351
|
type: string
|
|
10018
10352
|
enum: [allowed, denied, expired]
|
|
@@ -10123,6 +10457,15 @@ components:
|
|
|
10123
10457
|
the PRESENCE-IS-THE-PROMISE clause (present only where a rule store is wired) and the redemption
|
|
10124
10458
|
scope are stated verbatim on ToolApprovalFrame.ruleSuggestions — the live frame, the stored
|
|
10125
10459
|
`card_json` and the replayed frame carry the SAME material.
|
|
10460
|
+
ruleOffers:
|
|
10461
|
+
type: array
|
|
10462
|
+
minItems: 1
|
|
10463
|
+
items: { $ref: '#/components/schemas/RuleOffer' }
|
|
10464
|
+
description: >
|
|
10465
|
+
server >= 7.46.0 (core 5.58.0, design/375) — the discriminated-union replacement of
|
|
10466
|
+
`ruleSuggestions` above. Same material as the live frame and the stored `card_json` (the server
|
|
10467
|
+
projects all three through one `copyRuleOffer`), so semantics, order-is-contract, presence-is-the-
|
|
10468
|
+
promise and the per-leg index rules are stated verbatim on ToolApprovalFrame.ruleOffers.
|
|
10126
10469
|
persistedRuleShadowed:
|
|
10127
10470
|
type: string
|
|
10128
10471
|
description: >
|
|
@@ -10141,6 +10484,10 @@ components:
|
|
|
10141
10484
|
probeCause: { type: object, additionalProperties: true, description: 'server >= 7.17.0: structured reversibility-probe tightening cause — same shape and semantics as ToolApprovalFrame.probeCause ({code, roots:{shown,total}, further?:{shown,total}}); frame and card carry the SAME value (one narrow-read function server-side). See that key for the full contract.' }
|
|
10142
10485
|
ruleEvidence: { type: object, additionalProperties: true, description: 'server >= 7.23.0: rule-provenance evidence — same shape and semantics as ToolApprovalFrame.ruleEvidence (each member a value or a NAMED absence word; display/reconciliation metadata, never adjudication input). Frame and card carry the SAME value. See that key for the full contract.' }
|
|
10143
10486
|
delegation: { $ref: '#/components/schemas/ApprovalCardDelegation' }
|
|
10487
|
+
# S-139(sdk 8.3.0):同 A-057.④ 的理由 —— server 早随卡投出(帧与卡同一只窄读函数 ⇒ 同值),
|
|
10488
|
+
# 但本 schema 此前只在帧上公示过。长 description 的单一真源在 ToolApprovalFrame 的同名键。
|
|
10489
|
+
ruleOffersAbsence: { $ref: '#/components/schemas/RuleOffersAbsence' }
|
|
10490
|
+
denialLimitFallback: { $ref: '#/components/schemas/DenialLimitFallback' }
|
|
10144
10491
|
|
|
10145
10492
|
RuleSuggestion:
|
|
10146
10493
|
type: object
|
|
@@ -10163,13 +10510,332 @@ components:
|
|
|
10163
10510
|
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
10511
|
command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
|
|
10165
10512
|
|
|
10513
|
+
# ─── S-139(sdk 8.3.0):对 server 7.60.0 整体重对账带进来的四张型面 ───────────────────────
|
|
10514
|
+
AskOrigin:
|
|
10515
|
+
type: string
|
|
10516
|
+
enum: [content_question, unresolvable, org_unavailable, org_rule, hook, ask_rule, denial_limit_fallback, policy]
|
|
10517
|
+
description: >
|
|
10518
|
+
WHO raised an ask (`ToolApprovalFrame.origin`; server >= 7.57.0 / core 7.5.0, S-125③/#564) — core's
|
|
10519
|
+
`ASK_ORIGINS`, ENGINE-STAMPED at the gate (core's words: "engine-stamped at the gate, never a policy's
|
|
10520
|
+
claim"), transcribed verbatim by the server, which deliberately mints no second eligibility table of
|
|
10521
|
+
its own.
|
|
10522
|
+
|
|
10523
|
+
🔴 READ AS AN OPEN SET. The vocabulary's sole owner is core and it has grown before; branch with a
|
|
10524
|
+
default arm and treat an unknown word as "unknown origin", never as "no origin".
|
|
10525
|
+
🔴 NOT interchangeable with `governanceForced`: this says which KIND of authority asked, that one says
|
|
10526
|
+
whether THIS DEPLOYMENT's ops governance layer is the gate — `origin: policy` covers any deployment
|
|
10527
|
+
ToolPolicy's ask, so a governance-produced ask and an ordinary one look identical in this one word.
|
|
10528
|
+
|
|
10529
|
+
RuleOffersAbsence:
|
|
10530
|
+
type: string
|
|
10531
|
+
enum: [mandated, shadowed, lane_cannot_speak]
|
|
10532
|
+
description: >
|
|
10533
|
+
WHY `ruleOffers` is absent while the rule lane IS present (`ToolApprovalFrame.ruleOffersAbsence` and
|
|
10534
|
+
`ApprovalCard.ruleOffersAbsence`; server >= 7.55.0 / core #490 fix ②, S-15). The engine names which
|
|
10535
|
+
door is shut: `mandated` = this ask must be confirmed every time (🔴 this arm must NOT point the user
|
|
10536
|
+
at writing a rule — a rule would not silence it, and saying so sends them into a loop); `shadowed` = a
|
|
10537
|
+
rule matched but could not clear it (the machine-readable twin of `persistedRuleShadowed`);
|
|
10538
|
+
`lane_cannot_speak` = the engine could mint no coverable form for this command.
|
|
10539
|
+
|
|
10540
|
+
Engine-side MUTUALLY EXCLUSIVE with `ruleOffers`. 🔴 ABSENCE IS NOT AN ASSERTION — it covers both
|
|
10541
|
+
"there are offers" and the three structural doors, and one frame cannot tell them apart. ADVISORY
|
|
10542
|
+
display metadata, NEVER adjudication input. The frame and the stored `card_json` go through the SAME
|
|
10543
|
+
server-side narrow read, so both faces carry identical values; a word outside the set is treated as
|
|
10544
|
+
malformed and the key is not minted at all (the server never passes an unknown word through).
|
|
10545
|
+
|
|
10546
|
+
DenialLimitKind:
|
|
10547
|
+
type: string
|
|
10548
|
+
enum: [consecutive, total]
|
|
10549
|
+
description: 'Which bound tripped in a `DenialLimitFallback` — the consecutive-refusal limit or the cumulative one.'
|
|
10550
|
+
|
|
10551
|
+
DenialLimitFallback:
|
|
10552
|
+
type: object
|
|
10553
|
+
additionalProperties: false
|
|
10554
|
+
required: [consecutive, total, limit, autoDenyAfterMs]
|
|
10555
|
+
description: >
|
|
10556
|
+
The auto-mode classifier's DENIAL-LIMIT FALLBACK card (`ToolApprovalFrame.denialLimitFallback` and
|
|
10557
|
+
`ApprovalCard.denialLimitFallback`; server >= 7.57.0 / core 7.4.0 #548, S-114): the call that hit the
|
|
10558
|
+
consecutive (default 3) or cumulative (default 20) refusal limit is no longer silently denied — it
|
|
10559
|
+
becomes a card a HUMAN must decide. The same mint also stamps `requiresRealApproval`; the two are twins.
|
|
10560
|
+
|
|
10561
|
+
🔴 ALL FOUR MEMBERS ARE REQUIRED: core's shape has no optional member, so "one missing" is a bad value
|
|
10562
|
+
rather than an older core, and the server drops the whole key instead of half-minting a card whose
|
|
10563
|
+
counts would be misread.
|
|
10564
|
+
🔴 `autoDenyAfterMs` (ms; `0` = not armed — the deployment turned the knob off, or a TOTAL-tier card
|
|
10565
|
+
waits for a person) is for RENDERING THE COUNTDOWN ONLY. Never start a second timer from it: the window
|
|
10566
|
+
is executed by the ENGINE, and two overlapping windows are worse than the original defect and silent.
|
|
10567
|
+
🔴 ABSENCE IS NOT AN ASSERTION — the vast majority of asks are not fallback cards.
|
|
10568
|
+
⚠️ The durable (parked) leg does NOT carry this key today; the card's copy rides `card_json`, which is
|
|
10569
|
+
a different leg from the parked row's `pendingAction`.
|
|
10570
|
+
properties:
|
|
10571
|
+
consecutive: { type: integer, description: 'Consecutive refusals at the moment the limit tripped.' }
|
|
10572
|
+
total: { type: integer, description: 'Cumulative refusals at the moment the limit tripped.' }
|
|
10573
|
+
limit: { $ref: '#/components/schemas/DenialLimitKind' }
|
|
10574
|
+
autoDenyAfterMs: { type: integer, description: 'This card''s own auto-deny window in ms; 0 = not armed. Countdown rendering only.' }
|
|
10575
|
+
|
|
10576
|
+
RuleOfferMatch:
|
|
10577
|
+
type: string
|
|
10578
|
+
description: >
|
|
10579
|
+
The MATCH FORM of a persisted rule — the server's `PERSISTED_RULE_MATCHES`, verbatim (the vocabulary
|
|
10580
|
+
is OWNED BY CORE; the server only transcribes it and pins set equality, so core adding a member turns
|
|
10581
|
+
the server red at compile time).
|
|
10582
|
+
|
|
10583
|
+
`exact` = this one command only (`Bash(git status)`). `prefix` = the historical colon-star
|
|
10584
|
+
word-boundary prefix (`Bash(git status:*)`) and the ONLY spelling for a compound prefix form; it stays
|
|
10585
|
+
a permanent compatibility read. `wildcard` = the trailing space-star form (`Bash(npm run *)`), the
|
|
10586
|
+
same predicate as `prefix` on a single command body, minted by core's suggestion face since
|
|
10587
|
+
design/382 / #510. `subpath` = the `Read(//abs-dir/**)` directory form, which never holds a command.
|
|
10588
|
+
|
|
10589
|
+
🔴 FOUR MEMBERS, not "the three today's miner emits": this type describes what a consumer may RECEIVE,
|
|
10590
|
+
and the server's wire validator accepts all four. A consumer type narrower than the mint point is a
|
|
10591
|
+
lie — it lets a `switch` claim exhaustiveness the wire does not honour. (The retired `RuleSuggestion`
|
|
10592
|
+
declared only two, which is exactly why a legal `wildcard` candidate used to be dropped as malformed.)
|
|
10593
|
+
enum: [exact, prefix, wildcard, subpath]
|
|
10594
|
+
|
|
10595
|
+
RuleOfferUncoveredReason:
|
|
10596
|
+
type: string
|
|
10597
|
+
description: >
|
|
10598
|
+
Why one segment of a compound command is STILL not covered after redeeming the batch (design/382
|
|
10599
|
+
§3.5; the server's `UNCOVERED_SEGMENT_REASONS`, core-owned). `redirection` = a redirection is asked
|
|
10600
|
+
every time, permanently. `no_rule_form` = there is no rule form that could cover this segment.
|
|
10601
|
+
`cap_overflow` = deduplication plus the batch cap pushed it out.
|
|
10602
|
+
enum: [redirection, no_rule_form, cap_overflow]
|
|
10603
|
+
|
|
10604
|
+
RuleOfferUncoveredDetail:
|
|
10605
|
+
type: object
|
|
10606
|
+
description: >
|
|
10607
|
+
design/382 §3.5, ADDITIVE — one "why is this segment still uncovered" row. `segment` is the folded
|
|
10608
|
+
segment's ORIGINAL BYTES (command family: UNTRUSTED for display, and subject to the same redaction /
|
|
10609
|
+
truncation discipline as a member's `segment`).
|
|
10610
|
+
additionalProperties: false
|
|
10611
|
+
required: [segment, reason]
|
|
10612
|
+
properties:
|
|
10613
|
+
segment: { type: string, maxLength: 512 }
|
|
10614
|
+
reason: { $ref: '#/components/schemas/RuleOfferUncoveredReason' }
|
|
10615
|
+
|
|
10616
|
+
RuleOfferBatchMember:
|
|
10617
|
+
description: >
|
|
10618
|
+
ONE MEMBER of a `batch` offer — the rule folded out of one segment of a compound command
|
|
10619
|
+
(design/382 §2.3 B3; discriminated on `kind`, core-owned closed vocabulary).
|
|
10620
|
+
|
|
10621
|
+
`segment` records WHICH folded segment this member came from. Core states it explicitly as a RENDERING
|
|
10622
|
+
SEAT that never participates in adjudication, and it is UNTRUSTED for display exactly like
|
|
10623
|
+
`rule` / `command` / `directory`. ⚠️ Only `segment` may be TRUNCATED by the server (an honest
|
|
10624
|
+
elision); `rule` / `command` / `directory` sit on the left-hand side of the redemption equality and
|
|
10625
|
+
are NEVER truncated.
|
|
10626
|
+
|
|
10627
|
+
🔴 AN UNRECOGNISED MEMBER `kind` DROPS THE WHOLE BATCH, never the single member (core's normative
|
|
10628
|
+
degradation clause): a batch missing one member renders "yes to N" as "yes to N-1", which is worse
|
|
10629
|
+
than not rendering it. Any `single` offers beside it are complete and honest, so they stay.
|
|
10630
|
+
oneOf:
|
|
10631
|
+
- type: object
|
|
10632
|
+
additionalProperties: false
|
|
10633
|
+
required: [kind, rule, match, command, segment]
|
|
10634
|
+
properties:
|
|
10635
|
+
kind: { type: string, enum: [command] }
|
|
10636
|
+
rule: { type: string, maxLength: 512 }
|
|
10637
|
+
match: { $ref: '#/components/schemas/RuleOfferMatch' }
|
|
10638
|
+
command: { type: string, maxLength: 512 }
|
|
10639
|
+
segment: { type: string, maxLength: 512 }
|
|
10640
|
+
- type: object
|
|
10641
|
+
additionalProperties: false
|
|
10642
|
+
required: [kind, rule, directory, segment]
|
|
10643
|
+
properties:
|
|
10644
|
+
kind: { type: string, enum: [directoryRead] }
|
|
10645
|
+
rule: { type: string, maxLength: 512, description: 'The `Read(//abs-dir/**)` directory-read grant minted from a `cd` segment.' }
|
|
10646
|
+
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`.' }
|
|
10647
|
+
segment: { type: string, maxLength: 512 }
|
|
10648
|
+
discriminator:
|
|
10649
|
+
propertyName: kind
|
|
10650
|
+
|
|
10651
|
+
RuleOffer:
|
|
10652
|
+
description: >
|
|
10653
|
+
ONE "don't ask again" OPTION on an approval card (core 5.58.0 design/375 §3.1; server >= 7.46.0 emits
|
|
10654
|
+
the same shape on THREE legs — the live `tool_approval` frame, the stored `card_json`, and the durable
|
|
10655
|
+
park row). It is the BREAKING replacement of `RuleSuggestion`, with no alias.
|
|
10656
|
+
|
|
10657
|
+
🔴 READ `kind`. ORDER IS CONTRACT: a whole-string exact `single` is index 0 when present, a `batch` is
|
|
10658
|
+
always last, core's cardinality is <= 2 and the server's enforcement cap is 4.
|
|
10659
|
+
|
|
10660
|
+
🔴 TWO ARMS, TWO SELECTION KEYS (see PersistRuleSelection). The `single` arm is redeemed by echoing its
|
|
10661
|
+
`rule` text VERBATIM — the server locates it BY TEXT, so a hand-built string is refused
|
|
10662
|
+
(`rule_not_offered`) by construction. The `batch` arm has no single text to copy (it is ONE yes for
|
|
10663
|
+
every member, with no per-member ticking), so it is redeemed BY INDEX, and the server additionally
|
|
10664
|
+
checks an anti-drift equality before honouring it: the batch re-minted from the adjudicated command
|
|
10665
|
+
must have the SAME member text set as the one the person saw, or the redemption is refused and the way
|
|
10666
|
+
forward is to re-trigger the command for a fresh card.
|
|
10667
|
+
|
|
10668
|
+
🔴 INDEX SEMANTICS DIFFER PER LEG — the live card leg's index is a legal selection key, the durable
|
|
10669
|
+
queue row's is not (that projection compacts). Both statements are on the two `ruleOffers` properties.
|
|
10670
|
+
oneOf:
|
|
10671
|
+
- type: object
|
|
10672
|
+
additionalProperties: false
|
|
10673
|
+
required: [kind, rule, match, command]
|
|
10674
|
+
properties:
|
|
10675
|
+
kind: { type: string, enum: [single] }
|
|
10676
|
+
rule: { type: string, maxLength: 512, description: 'The canonical rule text to echo back verbatim on redemption (server MAX_RULE_TEXT_CHARS = 512).' }
|
|
10677
|
+
match: { $ref: '#/components/schemas/RuleOfferMatch' }
|
|
10678
|
+
command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
|
|
10679
|
+
- type: object
|
|
10680
|
+
additionalProperties: false
|
|
10681
|
+
required: [kind, rules, uncoveredSegments]
|
|
10682
|
+
properties:
|
|
10683
|
+
kind: { type: string, enum: [batch] }
|
|
10684
|
+
rules:
|
|
10685
|
+
type: array
|
|
10686
|
+
minItems: 1
|
|
10687
|
+
items: { $ref: '#/components/schemas/RuleOfferBatchMember' }
|
|
10688
|
+
description: 'The per-segment members (segment order, deduplicated by rule text). Core contract 1..5; the server tolerates up to 8.'
|
|
10689
|
+
uncoveredSegments:
|
|
10690
|
+
type: integer
|
|
10691
|
+
minimum: 0
|
|
10692
|
+
description: >
|
|
10693
|
+
The HONEST SURPLUS at mint time: how many segments of this compound command would still not be
|
|
10694
|
+
allowed by any rule after redeeming this batch, under the coverage snapshot taken then. `0`
|
|
10695
|
+
means "redeem this and the whole command is covered, as of that snapshot" — a statement about
|
|
10696
|
+
the snapshot, not a lasting guarantee (a concurrent rule revocation makes it stale).
|
|
10697
|
+
uncoveredDetail:
|
|
10698
|
+
type: array
|
|
10699
|
+
items: { $ref: '#/components/schemas/RuleOfferUncoveredDetail' }
|
|
10700
|
+
description: >
|
|
10701
|
+
design/382 §3.5, ADDITIVE — the per-segment reason behind `uncoveredSegments`. When present the
|
|
10702
|
+
row count equals `uncoveredSegments`, but THE COUNT IS THE SOURCE OF TRUTH: the server drops a
|
|
10703
|
+
malformed detail seat without dropping the batch, so an absent `uncoveredDetail` is NOT an
|
|
10704
|
+
assertion that nothing is uncovered.
|
|
10705
|
+
discriminator:
|
|
10706
|
+
propertyName: kind
|
|
10707
|
+
|
|
10708
|
+
PersistedRuleAnchor:
|
|
10709
|
+
type: object
|
|
10710
|
+
description: >
|
|
10711
|
+
server >= 7.48.0 (P-39), ADDITIVE — one self-describing attribution anchor for
|
|
10712
|
+
`ToolApprovalRespondAck.persistedRules`, index-aligned with it.
|
|
10713
|
+
|
|
10714
|
+
The disease it closes: per-entry attribution of `persistedRules` used to be inferable only from
|
|
10715
|
+
POSITION, and position was not provable on the wire (the ack order came from the server's RE-MINTED
|
|
10716
|
+
batch walk, matching the documented display order only by coincidence — the anti-drift check is a SET
|
|
10717
|
+
equality, order is not in it).
|
|
10718
|
+
|
|
10719
|
+
🔴 BOTH INDEXES LIVE IN THE OFFER SPACE, so a consumer needs no mapping: `offerIndex` echoes the
|
|
10720
|
+
request's `persistRule.batchOfferIndex` VERBATIM (correlation: "this ack really is for the redemption
|
|
10721
|
+
I sent"), and `memberIndex` is the position inside that batch's `rules[]` on the frame. Core's
|
|
10722
|
+
candidate space is deliberately kept off the wire.
|
|
10723
|
+
additionalProperties: false
|
|
10724
|
+
required: [offerIndex, memberIndex, rule]
|
|
10725
|
+
properties:
|
|
10726
|
+
offerIndex: { type: integer, minimum: 0 }
|
|
10727
|
+
memberIndex: { type: integer, minimum: 0 }
|
|
10728
|
+
rule: { type: string, maxLength: 512, description: 'The canonical text this member actually landed as (equal to the same-index entry of `persistedRules`).' }
|
|
10729
|
+
|
|
10730
|
+
PersistRuleSelection:
|
|
10731
|
+
description: >
|
|
10732
|
+
The body field `persistRule` of POST /v1/tool-approvals/{id}/respond — TWO MUTUALLY EXCLUSIVE ARMS,
|
|
10733
|
+
matching the server's `ParsedPersistRule` one for one.
|
|
10734
|
+
|
|
10735
|
+
🔴 THERE IS NO `kind` ON THE WIRE: that is the server's own post-parse discriminator. The arms are told
|
|
10736
|
+
apart by WHICH FIELD IS PRESENT.
|
|
10737
|
+
|
|
10738
|
+
🔴 MUTUAL EXCLUSION IS A LOUD 400, not "pick one silently" (server constant
|
|
10739
|
+
PERSIST_RULE_BATCH_EXCLUSIVE_ERROR, verbatim): "persistRule.batchOfferIndex is mutually exclusive with
|
|
10740
|
+
persistRule.rule / persistRule.edited — send exactly one arm". Two arms are two DIFFERENT
|
|
10741
|
+
authorisations; guessing between them is deciding for the person.
|
|
10742
|
+
|
|
10743
|
+
🔴 MALFORMED IS ALSO A 400, never folded (PERSIST_RULE_BATCH_INDEX_ERROR, verbatim):
|
|
10744
|
+
"persistRule.batchOfferIndex must be a non-negative integer when present" — a negative, fractional or
|
|
10745
|
+
non-numeric index that got coerced points at ANOTHER offer. Likewise
|
|
10746
|
+
PERSIST_RULE_EDITED_FLAG_ERROR: "persistRule.edited must be a boolean when present" — a non-boolean is
|
|
10747
|
+
NEVER silently treated as false, because that would quietly fold a "I want to store free text" back
|
|
10748
|
+
into the candidate arm and answer `rule_not_offered`, leaving the person to think their own rule was
|
|
10749
|
+
rejected. And `rule` must be a non-empty string of at most 512 characters.
|
|
10750
|
+
|
|
10751
|
+
🔴 NO SCOPE IN THE BODY: a top-level `scope` and a `persistRule.scope` are both loud 400s
|
|
10752
|
+
("this endpoint does not accept a rule scope — the server mints it from where the approval happened").
|
|
10753
|
+
Letting the caller name a scope is letting a consent given inside project A be written as a standing
|
|
10754
|
+
allow over project B (or globally) — the most expensive widening available on this axis.
|
|
10755
|
+
|
|
10756
|
+
⚠️ ON `decision: "deny"` THE WHOLE FIELD IS NOT PARSED AT ALL (tolerated, ignored, no 400 — same
|
|
10757
|
+
posture as `updatedInput`): having refused the operation, consenting to it persistently is meaningless.
|
|
10758
|
+
So the refusals above apply to the allow family; only the TOP-LEVEL `scope` 400 is decision-independent.
|
|
10759
|
+
oneOf:
|
|
10760
|
+
- type: object
|
|
10761
|
+
title: candidate-or-edited-arm
|
|
10762
|
+
required: [rule]
|
|
10763
|
+
# 🔴 互斥必须 EXPLICIT(codex R2-F1,验真后修):两臂都是开集,所以每一臂都能把对面臂的键
|
|
10764
|
+
# 当「多余键」收下 —— `oneOf` 数的是「几臂命中」,于是 `{rule, batchOfferIndex:-1}` 会因为
|
|
10765
|
+
# 批臂被 `minimum:0` 判假而**恰好命中一臂**,schema 放行、server 报互斥 400。靠「另一臂恰好
|
|
10766
|
+
# 判假」表达互斥是间接后果,不是约束。
|
|
10767
|
+
not: { required: [batchOfferIndex] }
|
|
10768
|
+
properties:
|
|
10769
|
+
rule:
|
|
10770
|
+
type: string
|
|
10771
|
+
minLength: 1
|
|
10772
|
+
maxLength: 512
|
|
10773
|
+
description: >
|
|
10774
|
+
With `edited` absent or false this is the CANDIDATE ARM: echo back, VERBATIM, the `rule` of the
|
|
10775
|
+
chosen `kind: "single"` entry of the frame's `ruleOffers` (or, against a <= 7.45 server, of
|
|
10776
|
+
`ruleSuggestions`). Do not hand-build it, re-case it or append `:*` — the server locates it by
|
|
10777
|
+
text and refuses otherwise (`rule_not_offered`). With `edited: true` this is the FREE-TEXT ARM
|
|
10778
|
+
and the string is whatever the person typed.
|
|
10779
|
+
edited:
|
|
10780
|
+
type: boolean
|
|
10781
|
+
description: >
|
|
10782
|
+
server >= 7.44.0 (#340), ADDITIVE — capability bit `respondFreeFormRules`, PROBE IT FIRST.
|
|
10783
|
+
`true` says the text above was hand-edited on the card, so the candidate-table equality is not
|
|
10784
|
+
applied; the engine judges it instead through three gates (the shared validator, a COVERAGE
|
|
10785
|
+
gate requiring the edited rule to still hold the command being adjudicated, and scope
|
|
10786
|
+
inherited from this card). On success the ack carries `persistedRule` = the canonical spelling
|
|
10787
|
+
that really landed. An older worker drops the unknown key and judges the text AS A CANDIDATE,
|
|
10788
|
+
answering `rule_not_offered` — honest, but easily misread as "I wrote it wrong".
|
|
10789
|
+
|
|
10790
|
+
⚠️ Together with `updatedInput` the persistence is refused (`rule_input_edited`): the call that
|
|
10791
|
+
takes effect is the edited one while the candidates came from the ORIGINAL command. The
|
|
10792
|
+
decision itself still stands.
|
|
10793
|
+
# 🔴 刻意**不封闭**(codex R1-F2,验真后修;main 的同一格早有同款注释,换形时别把它丢了):
|
|
10794
|
+
# server 的 `parseToolApprovalResponse` 是手写 parser,只读它认识的键,对象里多给的键**忽略**
|
|
10795
|
+
# (与 `updatedInput` 同一条宽收姿势)。写 `additionalProperties:false` 会让照 spec 生成的客户端
|
|
10796
|
+
# 拒发一个 server 其实会受理的请求 —— 例如一个把整只候选对象 `{rule, match, command}` 原样转发的
|
|
10797
|
+
# 存量壳,重新 codegen 之后就发不出去了。那是我们自己造的漂移,不是上游的约束。
|
|
10798
|
+
# 互斥**不靠封闭表达**:`oneOf` 的语义是「恰好命中一臂」,两臂各自 `required` 不同 ⇒
|
|
10799
|
+
# `{rule, batchOfferIndex}` 同时命中两臂 ⇒ 判假。开集与互斥两件事各自成立。
|
|
10800
|
+
- type: object
|
|
10801
|
+
title: batch-arm
|
|
10802
|
+
required: [batchOfferIndex]
|
|
10803
|
+
# 同上一臂:`{batchOfferIndex:0, edited:false}` 曾因为文本臂缺 `rule` 判假而恰好命中本臂。
|
|
10804
|
+
not: { anyOf: [{ required: [rule] }, { required: [edited] }] }
|
|
10805
|
+
properties:
|
|
10806
|
+
batchOfferIndex:
|
|
10807
|
+
type: integer
|
|
10808
|
+
minimum: 0
|
|
10809
|
+
description: >
|
|
10810
|
+
server >= 7.46.0 (design/377), ADDITIVE — capability bit `respondBatchRuleOffers`, PROBE IT
|
|
10811
|
+
FIRST. The person ticked a `kind: "batch"` offer; the value is that offer's INDEX in the
|
|
10812
|
+
frame's `ruleOffers`. A conjunctive batch has no single text to copy, so the index is the only
|
|
10813
|
+
possible selection key.
|
|
10814
|
+
|
|
10815
|
+
🔴 NARROWLY OPENED TO BATCH ONLY: an index that points nowhere, or points at a `single`, yields
|
|
10816
|
+
`rulePersisted:false` + `rule_not_offered` — what was never rendered can never be redeemed, and
|
|
10817
|
+
the `single` arm's anti-forgery anchor is its TEXT equality, which an index may not bypass.
|
|
10818
|
+
🔴 An older worker drops the unknown key and then fails the `rule` shape check with a 400
|
|
10819
|
+
— honest, but it cannot tell that a batch was ticked. If the capability bit is absent, do not
|
|
10820
|
+
render a redeemable batch affordance.
|
|
10821
|
+
# 同上一臂:刻意不封闭(理由逐字见那一段)。
|
|
10822
|
+
|
|
10166
10823
|
RuleRefusalReason:
|
|
10167
10824
|
type: string
|
|
10168
10825
|
description: >
|
|
10169
10826
|
Why a "don't ask again" did NOT get persisted (#154 车二; the server's `RuleRefusalReason`, verbatim).
|
|
10170
10827
|
A CLOSED set so a shell can branch instead of matching prose. The two naming styles are NOT a typo:
|
|
10171
|
-
the
|
|
10172
|
-
underscored ones are minted by the server's gates.
|
|
10828
|
+
the SEVEN hyphenated members are derived from the consent lane's own refusal reasons
|
|
10829
|
+
(server `CardRulePersisted`), the SIX underscored ones are minted by the server's gates.
|
|
10830
|
+
|
|
10831
|
+
`edit-unsupported` and `edit-rejected` (server >=7.44.0, #340) belong to the FREE-TEXT arm and ask the
|
|
10832
|
+
shell for OPPOSITE things: the first means the edit affordance does not work on this card (no binding
|
|
10833
|
+
anchor, or the deployment has it off) so STOP RENDERING THE INPUT BOX; the second means the engine's
|
|
10834
|
+
gates refused this particular text, so "fix it and try again" — and since #345 (server >=7.51.0) the
|
|
10835
|
+
independently decidable half of that judgement happens BEFORE the decision settles (a 400 with the card
|
|
10836
|
+
still alive), so trying again really works. `scope-unresolved` (core 7.0.0 #490②) is the death of the
|
|
10837
|
+
silent global: with neither an explicit scope nor a cwd the lane refuses loudly instead of defaulting to
|
|
10838
|
+
global, which makes it a ROUTINE answer on a deployment that has not wired `ruleScopeRootFor`.
|
|
10173
10839
|
|
|
10174
10840
|
🔴 A refusal NEVER flips the decision. The person's "allow this once" has already taken effect and
|
|
10175
10841
|
reached the engine; turning the whole respond into a 4xx would make a real allow vanish. The honest
|
|
@@ -10200,16 +10866,30 @@ components:
|
|
|
10200
10866
|
note on `rule_lane_unavailable` above already says never to disable the feature globally on it, and
|
|
10201
10867
|
the same holds here. The one correct source for "does this worker serve rules at all" is
|
|
10202
10868
|
`capabilities.permissionRules`. Keep offering "don't ask again" on the next ask; just not on this one.
|
|
10869
|
+
`rule_material_absent` (server >=7.48.0, #363) — DURABLE (PARKED) LATE-DECISION LEG ONLY: THIS ROW
|
|
10870
|
+
carries no material to mint a rule from. Three causes, one handling: the ask never entered the rule
|
|
10871
|
+
lane (governance-forced / no owner / the engine minted no candidate / the command bytes are not
|
|
10872
|
+
readable out of the args), the row predates those columns, or the card on the row will not parse.
|
|
10873
|
+
🔴 ALSO SPLIT FROM `rule_lane_unavailable` ON PURPOSE, for the same reason as the previous member:
|
|
10874
|
+
that word says "this deployment serves no rules at all" and a shell may stop rendering the affordance
|
|
10875
|
+
on it, whereas the truth here is "the lane is fine, only THIS ROW has no material". Folding them lets
|
|
10876
|
+
one late park answer condemn a healthy deployment's whole lane. Silently ignoring it is worse still
|
|
10877
|
+
(no silent fail-open on the approval axis): a person who ticked "don't ask again" and saw nothing
|
|
10878
|
+
happen will not tick it twice.
|
|
10203
10879
|
enum:
|
|
10204
10880
|
- no-candidates
|
|
10205
10881
|
- unknown-candidate
|
|
10206
10882
|
- confirm-refused
|
|
10207
10883
|
- redeem-refused
|
|
10884
|
+
- edit-unsupported
|
|
10885
|
+
- edit-rejected
|
|
10886
|
+
- scope-unresolved
|
|
10208
10887
|
- rule_lane_unavailable
|
|
10209
10888
|
- rule_input_edited
|
|
10210
10889
|
- rule_not_offered
|
|
10211
10890
|
- rule_store_error
|
|
10212
10891
|
- rule_governance_forced
|
|
10892
|
+
- rule_material_absent
|
|
10213
10893
|
|
|
10214
10894
|
ApprovalRiskAxes:
|
|
10215
10895
|
type: object
|
|
@@ -11864,6 +12544,44 @@ components:
|
|
|
11864
12544
|
next ask" (`rule_input_edited`) from "there was nothing redeemable on this card"
|
|
11865
12545
|
(`rule_lane_unavailable`). 🔴 `rule_lane_unavailable` covers four causes, two of them per-card — do
|
|
11866
12546
|
NOT read it as "this deployment has no rule lane" (see the RuleRefusalReason schema).
|
|
12547
|
+
persistedRule:
|
|
12548
|
+
type: string
|
|
12549
|
+
maxLength: 512
|
|
12550
|
+
description: >
|
|
12551
|
+
server >= 7.44.0 (#340), ADDITIVE — FREE-TEXT ARM ONLY: the CANONICAL SPELLING of the rule that
|
|
12552
|
+
actually landed (core may normalise `Bash(adb *)` into `Bash(adb:*)`). Echo THIS back to the
|
|
12553
|
+
person, not the bytes they typed into the box.
|
|
12554
|
+
|
|
12555
|
+
🔴 ABSENCE HAS THREE MEANINGS, none of them "the rule is gone": (1) this respond used the
|
|
12556
|
+
CANDIDATE arm — the server's `editedArmEcho` is the single mint point for this key, so a candidate
|
|
12557
|
+
respond's ack is byte-for-byte unchanged; (2) nothing landed (read `rulePersisted` / `ruleRefusal`);
|
|
12558
|
+
(3) the respond carried no `persistRule` at all.
|
|
12559
|
+
persistedRules:
|
|
12560
|
+
type: array
|
|
12561
|
+
items: { type: string, maxLength: 512 }
|
|
12562
|
+
description: >
|
|
12563
|
+
server >= 7.46.0 (design/377), ADDITIVE — BATCH ARM ONLY: the canonical text of EVERY member that
|
|
12564
|
+
landed in this one redemption. A conjunctive batch is one yes for the whole set; if any member is
|
|
12565
|
+
refused the ack is `rulePersisted:false` + `ruleRefusal:"redeem-refused"` and the members that DID
|
|
12566
|
+
land are not rolled back (their dots are on record and a re-triggered card dedupes idempotently).
|
|
12567
|
+
|
|
12568
|
+
🔴 ORDER: from server 7.48.0 on, the DISPLAY order of the requested offer
|
|
12569
|
+
(`ruleOffers[batchOfferIndex].rules`) is a PROMISE, not a coincidence; on 7.46-7.47 it inherited the
|
|
12570
|
+
server's re-minted walk order instead. For per-entry attribution read `persistedRuleAnchors`
|
|
12571
|
+
rather than inferring it from position.
|
|
12572
|
+
🔴 Not the same thing as `persistedRule`: the singular key belongs to the single/edited arm, the
|
|
12573
|
+
plural one to the batch arm, and the two never appear together.
|
|
12574
|
+
persistedRuleAnchors:
|
|
12575
|
+
type: array
|
|
12576
|
+
items: { $ref: '#/components/schemas/PersistedRuleAnchor' }
|
|
12577
|
+
description: >
|
|
12578
|
+
server >= 7.48.0 (P-39), ADDITIVE and with NO capability bit — the per-entry, self-describing
|
|
12579
|
+
attribution anchors for `persistedRules`, INDEX-ALIGNED with it.
|
|
12580
|
+
|
|
12581
|
+
Minted ONLY on the batch arm (the single/edited arm has no member coordinates to speak of), and
|
|
12582
|
+
omitted WHOLESALE whenever the server cannot prove attribution — an honest absence, never a
|
|
12583
|
+
fabricated index. A consumer that sees it absent knows this ack can only be read positionally, and
|
|
12584
|
+
`persistedRules` itself is unchanged to the byte (older consumers ignore the unknown key).
|
|
11867
12585
|
noteRecorded:
|
|
11868
12586
|
type: boolean
|
|
11869
12587
|
description: >
|