@sema-agent/core 7.36.0 → 7.37.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/CHANGELOG.md +81 -0
- package/dist/core/ask-origin.d.ts +35 -5
- package/dist/core/ask-origin.js +23 -4
- package/dist/core/checkpoint-store.d.ts +13 -3
- package/dist/core/decision-receipt.d.ts +21 -0
- package/dist/core/decision-receipt.js +63 -0
- package/dist/core/engine-notice.d.ts +8 -5
- package/dist/core/gate-fold.js +2 -1
- package/dist/core/gate-lanes.js +8 -13
- package/dist/core/gate-outcome.d.ts +7 -5
- package/dist/core/gate-outcome.js +5 -1
- package/dist/core/hooks.d.ts +11 -2
- package/dist/core/hooks.js +3 -1
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/runner/denial-limit-arms.d.ts +28 -18
- package/dist/core/runner/denial-limit-arms.js +11 -7
- package/dist/core/runner/inherited-ask-grants.d.ts +32 -31
- package/dist/core/runner/inherited-ask-grants.js +5 -0
- package/dist/core/runner/prepare-inherited-gate.js +1 -1
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +1 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -8
- package/dist/core/runner/prepare-policy-chain.js +43 -25
- package/dist/core/store-contracts/checkpoint-store-contract.js +18 -0
- package/dist/core/tool-name-readings.d.ts +4 -0
- package/dist/core/tool-name-readings.js +1 -0
- package/dist/core/tool-policy.d.ts +74 -84
- package/dist/core/tool-policy.js +71 -36
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
- package/wire-changes/core@7.37.0.json +405 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
|
+
## 7.37.0 — 2026-10-01
|
|
2
|
+
|
|
3
|
+
**Closed sets, one by one.**
|
|
4
|
+
- `MODE_RELEASE_REASONS` / `ModeReleaseReason` (#1212): a NEW exported closed set, one word (`rule_unreadable`) — the words `tool_end.gate.disposition.released.reason` carries. The type already admitted only that word; it is now a runtime list with a predicate (`isModeReleaseReason`).
|
|
5
|
+
- #1214: no closed set moves — the pair's words are `AskClass` (`ASK_CLASSES`), already a package export.
|
|
6
|
+
- #1221: no exported closed set moves — a decision's `action` was already typed allow / deny / ask (`PermissionResult["action"]`); the out-of-contract screen now holds a decision to it at run time (not exported).
|
|
7
|
+
|
|
8
|
+
**Package exports** (`npm run handoff:diff v7.36.0 HEAD`): +3 / −0 — `MODE_RELEASE_REASONS`, `isModeReleaseReason`, `type ModeReleaseReason` (#1212). #1214 adds no export: `AskClass`, `ASK_CLASSES` and `RELEASABLE_ASK_CLASSES` are already package exports.
|
|
9
|
+
|
|
10
|
+
**The wire list's BREAKING items** (`node scripts/wire-changes.mjs v7.36.0 HEAD --version 7.37.0`): none — 0 BREAKING items. The non-breaking moves it carries: +3 exports; two new closed sets (`MODE_RELEASE_REASONS` and its type `ModeReleaseReason`, `rule_unreadable` each) (#1212); two optional members, `askClass` and `classes`, on four declarations — `AskRequest`, `PendingAction.tool_approval` (the park row), the `PermissionResult` ask arm and `AskCarry` — read as five type rows (`PreToolUseResult` is `PermissionResult` plus a member, so its ask arm gains them too) (#1214). Value moves the list cannot read, from the criteria: F1 (`screenGateOutcome` I7 screens `released.reason`); F2 / F3 (#1217: the inherited lane reads an ancestor's decision once, and screens its ask at receipt); F4 (#1218: an inherited grant covers the classes it answered — the child's gate asks its own protection question; a marked tool's first delegated card is `operator_forced`); F5 (#1221: a decision whose `action` is outside allow / deny / ask is refused, where it ranked as an allow); F6 (#1226: the contract screen is the one receipt of a decision on both lanes — read once into the engine's copy; a host's direct `runToolGate` adjudication and the approved-edit re-check are screened, where they were not).
|
|
11
|
+
|
|
12
|
+
### A mode release's `reason` is an exported closed set, minted from one list (#1212; behavior (tighten: the record screen); +3 exports, +0 wire keys, +0 codes) (@server @client-core @test)
|
|
13
|
+
|
|
14
|
+
- **Before (v7.36.0).** `tool_end.gate.disposition.released.reason` was typed `"rule_unreadable"` and minted as that literal in `arbitrateByMode`. There was no runtime list or predicate, so a consumer screening the word keyed its own table on the type (`ModeRelease["reason"]`), and `screenGateOutcome` I7 screened a release's `mode`, `askClass`, `classes` and `origin` but not its `reason`.
|
|
15
|
+
- **Now.** `MODE_RELEASE_REASONS` (`["rule_unreadable"]`; every member an ask class), `type ModeReleaseReason` and `isModeReleaseReason(v)`, exported beside `RELEASABLE_ASK_CLASSES`. The release arm of `arbitrateByMode` carries the first member of the list its class set holds (none ⇒ no `reason`), so a new reason is one word in the list. The mint and the predicate read one copy of the list taken at load, so a caller writing into the exported array changes neither; `ModeRelease.reason` and the release arm of `Arbitration` are typed by it. `screenGateOutcome` I7 refuses a `released.reason` outside the set on the same exit as an out-of-set `released.origin`: `I7: released.reason "<word>" is outside the closed set`.
|
|
16
|
+
- **Who is hurt.** A caller that hands `screenGateOutcome` a record whose `released.reason` is outside the set now gets one defect. Only a hand-built or cast record can carry one: the engine mints only `rule_unreadable`, and the type admitted nothing else. Inside the engine the gate's mint throws on a defect and the frame screen withholds the outcome, as for every I7 defect. Compensation: mint the word from `MODE_RELEASE_REASONS`, or check it with `isModeReleaseReason`. A consumer whose table is keyed on `ModeRelease["reason"]` needs no change; it may read the list instead.
|
|
17
|
+
- Not registered in `docs/CLOSED-SETS.md`: that registry's scan (`gate:closed-sets`) refuses a set of fewer than two members. This set has no disposition table and no `switch` reads it; `test/closed-sets-literal-members.test.ts` still sweeps it (string-literal members only).
|
|
18
|
+
- Pins: `test/backlog1212-mode-release-reasons.test.ts`:
|
|
19
|
+
- R1: the set equals the reasons `arbitrateByMode` produces over every set of one or two ask classes under `bypassPermissions`, and no other mode releases the class. Writing into the exported array moves neither the mint nor the predicate.
|
|
20
|
+
- R2: every reason is a releasable class.
|
|
21
|
+
- R3: the predicate.
|
|
22
|
+
- R4: I7, three arms. A registered or absent reason screens clean; an outside word is one defect, spelled as the origin arm's; a registered reason the release's `classes` does not hold is one defect (`I7: released.reason "…" is not a member of released.classes`) — the mint takes the word from that set.
|
|
23
|
+
- R5: a real Runner under `bypassPermissions`, over a person's rule row the call cannot be read against. The frame's `released.reason` is `rule_unreadable` and a member.
|
|
24
|
+
- Mutations (`.gate-logs/criteria-7.37.0-mutations.md`, per fixture): D1000 the mint drops the reason (R1 ×2, R5 red); D1001 the mint spells a word outside the list (R1 red); D1002 the list gains an immune class (R1, R2, R3 red); D1003 the predicate answers true (R3, R4 red); D1004 I7 skips `reason` (R4 red); D1006 the mint reads the exported array live (R1, the export-write cell, red); D1018 I7 does not hold `reason` against `classes` (R4's membership cell red; Part E).
|
|
25
|
+
|
|
26
|
+
### The pre-7.34.0 mode-seed migration has a stated window: kept through 7.37.x, removed in 7.38.0 (#1213; docs and the notice's message text; +0 exports, +0 codes, +0 wire keys) (@server @test)
|
|
27
|
+
|
|
28
|
+
- **Before (v7.36.0).** The 7.34.0 entry says rows written by 7.33.x are "read ONCE by the resume leg (`migrateLegacyModeSeed`)" and names no removal. The `resume.legacy_mode_seed_migrated` notice's message said "from 7.35.0 a row still carrying these keys is refused", but 7.35.x and 7.36.0 kept migrating such rows.
|
|
29
|
+
- **Now.** One sentence, in the migration's JSDoc, the checkpoint row member's doc, the notice catalog, `docs/INTEGRATION-CORE.md` and the notice's message: the migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys (`autoModeRequested` / `approverPosture` / a doctrine `shellGate`). Resume or abandon a row parked before 7.34.0 before upgrading to 7.38.0. The window closes one release cycle after the server picks up 7.36.0 (planned: server 7.107.0). "Read ONCE" in the 7.34.0 entry means once per row: the next suspend writes the one key. The notice's code and `detail` are unchanged, and so is the migration.
|
|
30
|
+
- **Who is hurt.** Nobody in this release. At 7.38.0: a deployment still holding a row parked by 7.33.x or earlier that has not been resumed since.
|
|
31
|
+
- Pin: `test/backlog525-auto-intent-persistence.test.ts`. The migrated notice's message states the window and names no 7.35.0 (mutation D1005, the old message: red).
|
|
32
|
+
|
|
33
|
+
### The ask's class rides the request and the park row (#1214; additive: +0 exports, +0 codes, two optional keys on four types) (@server @cli @client-core @test)
|
|
34
|
+
|
|
35
|
+
- **Before (v7.36.0).** An `onAsk` approver received `origin` (which authority raised the ask) but not the ask's class — the `MODE_ARBITRATION` row a permission mode arbitrates it by. A host with nobody at the approval seat that wanted to answer ordinary questions and refuse the protection ones had to infer the class from other members, and `transcript_write`, `rule_unreadable` and the two `tool_marks_*` classes have no request member to infer them from.
|
|
36
|
+
- **Now.** `AskRequest.askClass?: AskClass` and `AskRequest.classes?: readonly AskClass[]`: `classes` is every class the ask belongs to, in `ASK_CLASSES` order, and `askClass` its first member — the classification the gate arbitrated the ask by (the same words `tool_end.gate.disposition` carries when a mode releases or refuses an ask). Every station that fills `origin` fills the pair: the gate's own (both rounds — the first ask and an approved edit's new ask) and the three inherited-lane stations (an ancestor's ask at its frozen approver). The park row carries the same pair (`PendingAction.tool_approval.askClass` / `classes`), written only as a well-formed pair. The `PermissionResult` ask arm (and so a hook's `PreToolUseResult`) and `AskCarry` carry it between the gate's stamp and the request / row: the gate stamps it beside `origin` and replaces a value a policy or hook put on its own ask; an inherited station derives it and never reads a value off the ancestor's decision. The park self-check's representative row carries the pair. `docs/INTEGRATION-CORE.md` §4.6e: an unattended leg's `onAsk` that answers by class — the recommended one answers `ordinary` alone and refuses every other class (the seven protection questions, `mode` — plan's write guard — and every class no mode releases) and a request with no class.
|
|
37
|
+
- **Absent means unclassified.** A request from a station that fills no `origin` — the shared-artifact read confirmation (KL-1214-a) — or one a deployment builds itself; a park row written before 7.37.0 (a resume does not fill it in — KL-1214-b). `CheckpointSummary` does not echo the pair; read the row.
|
|
38
|
+
- **Who is hurt.** Nobody who reads: every key is optional. A store that re-projects the park row field by field drops the pair until it carries the two keys: `probeParkRoundTrip` reports that store `row_not_faithful` (its representative row carries them), and the store-contract kit (`checkpointStoreContract`) fails its new entry `#1214 ask class pair` — the pair must survive `put` → `get` verbatim (`classes` whole, in its order) and an absent pair must stay absent. A test that deep-equals a whole `AskRequest` or park row sees two more keys.
|
|
39
|
+
- **Compensation.** A store that re-projects rows field by field carries `askClass` and `classes` (all four types: optional). A host that answers by class reads `classes` whole and treats absence as a class it does not answer. A host that moves `mode` into its answered set makes a `plan` leg behave like `acceptEdits`.
|
|
40
|
+
- Pins: `test/backlog1214-ask-class-on-request.test.ts` — F1-gate / F1-gate-edit / F1-inherited-shared / F1-inherited-distinct / F1-inherited-edit / F1-inherited-branches (every station that fills `origin`, and the judging station's three branches); F1-census (the resolver's request mints file by file, and the park row's one mint); F1-arbitration (the request's pair equals the pair a `dontAsk` refusal names for the same protected write); F2-row / F2-screen / F2-legacy / F2-selfcheck (the park row); G1-ordinary / G1-write-protection / G1-transcript-write / G1-plan / G1-control-deny / G1-control-allow (a real Runner, a function `onAsk` answering by class: `ordinary` answered; `write_protection`, `transcript_write` and — under `plan` — `mode` refused); G2-gate / G2-hook / G2-inherited / G2-stamp (a policy's or a hook's own value); F1-pair (the pair's shape on every request the file saw). The kit entry: `test/store-contracts-discrimination.test.ts` (a store that drops the pair, and one that re-sorts `classes`, each fail exactly that entry; the reference store passes every entry), run on the in-memory, file and Pg (pg-mem) stores by `test/cross-backend-inmem-file.test.ts` / `test/pg-cross-backend.test.ts`.
|
|
41
|
+
- Mutations (`.gate-logs/criteria-7.37.0-mutations.md`, per fixture): D1010 the gate's stamp writes no class; D1011 the park row does not carry the pair; D1012 a value already on the decision survives the stamp (a policy's, or an earlier stamp's on a re-stamped fallback); D1013 the gate's request carries `askClass` without `classes`; D1014 the inherited-lane carry drops the pair.
|
|
42
|
+
|
|
43
|
+
#### The inherited lane reads an ancestor's decision once, at receipt (#1217; behavior (a stateful or mutated decision is read once; an out-of-contract ancestor decision is refused); +0 exports, +0 codes, +0 wire keys) (@server @test)
|
|
44
|
+
|
|
45
|
+
- **Before (v7.36.0).** The inherited (delegation) lane read an ancestor policy's decision object at every station — the classifier's eligibility, the origin word, the class set, the mode arbitration, the approval request's real-approval and mandate bits, the approved rewrite: 3 to 20 reads per member (measured; 4 to 21 on this release's tree before the fix, once the class set was read on every ask). A decision whose members are getters, or one the policy keeps and changes after returning it, answered each station with its own value. `requiresRealApproval` answering `++reads <= 4` under an ancestor `onAsk: "allow"` was refused on v7.36.0 and ran on the pre-fix tree. A policy that turned its returned ask into `action: "allow"` while its approver answered `"unavailable"` ran the call (a plain object or a function holding the members alike). One that swapped `updatedInput`, or changed a field inside it, after the approval executed the change, not the arguments the approver was shown. An ancestor ASK carrying an out-of-range `decisionReason`, one routed through an accessor, or the retired `reason` field reached the ancestor's approver and ran: the fold's out-of-contract screen only saw what the lane handed on.
|
|
46
|
+
- **Now.** Each call of an ancestor policy's `check` — the distinct-instance wrapper, the shared-instance caller slot, the approved-edit re-check — is received once: one copy of the decision's own enumerable data members, each taken off its own property descriptor (an accessor is refused, never invoked; a Proxy refused before a trap — #1226), their values detached from the policy (a structured clone), the engine's own records on the object carried across (an approval policy's attested settlement, a classifier's cause and model), the object frozen; every station reads that snapshot. A carrier that is not an object — a function holding the members, `null`, `undefined`, a primitive — is refused at receipt, before any approver is consulted. A member of the decision type that a plain read reaches but the copy does not hold — supplied by the prototype, held non-enumerable, or answered by a Proxy for a key it does not list — is refused, fail-closed, and so is a member value the receipt cannot detach (a function inside a rewrite): the copy would drop such a member and a plain read would keep it, and each reading loosened a station that read it the other way. The receipt then runs the fold's out-of-contract screen: its value checks on the snapshot, its carrier checks on the caller's object's property descriptors, never a value. A getter-backed decision is refused (an accessor; #1226); what a policy does to the object, or inside its members, after returning it does not reach the call.
|
|
47
|
+
- **Who is hurt.** An ancestor policy whose decision supplies members through getters: refused at receipt, the getter never invoked (v7.36.0 read it at every station — `++reads <= 4` on `requiresRealApproval` was refused at the approval, `++reads > 2` too). An ancestor policy that reuses and mutates an object it returned: what it held at receipt is what the call sees. An ancestor policy that returns a class instance whose members come from its prototype, a Proxy, an object carrying members on its prototype or as non-enumerable properties, or a rewrite holding a value a structured clone cannot copy: v7.36.0 read such a decision one way at some stations and another way at others; it is now refused at receipt with the fold screen's refusal (`tool_end.gate.disposition` `{ kind: "denied", deniedBy: "policy" }`, `decisionReason: "rule"`) — v7.36.0 denied the shapes measured here too, at the approval (`ask_resolution`) or at the fold, and ran the callable one (the pre-fix tree too; refused now). An ancestor policy whose ask carries an out-of-range `decisionReason` or the retired `reason` field: it reached the ancestor's approver before and is refused now, with the record and text the fold gives the same decision from a task's own caller policy. Compensation: return plain data — a plain object whose contract fields are own enumerable properties, `decisionReason` from the closed set, `message` rather than `reason`, a rewrite that is plain data.
|
|
48
|
+
- Pins: `test/backlog1217-inherited-decision-snapshot.test.ts` — ① the two getter forms refused unread, their plain controls as before; ② a Proxy ancestor decision refused on each receipt arm before a single trap runs (looks 0, gets 0 — the language's `await` probes `then` and nothing else), its ordinary twin riding the lane; ③ an approval policy's settlement rides the snapshot on each arm; ④ the post-return mutations (the object, a field inside the rewrite) and the callable carrier refused at receipt; ⑤ the screen at receipt; ⑥ the refused shapes (a Proxy answering `action`, a prototype `guard` under `bypassPermissions`, a non-enumerable `requiresRealApproval`, an undetachable rewrite, an own getter — its own-data twin the control); ⑦ the member list (its completeness is a compile-time fence over the decision type's keys).
|
|
49
|
+
- Mutations (`.gate-logs/criteria-7.37.0-mutations.md` Part D, per fixture): D1015 the receipt hands on the caller's object (①, ②, ④, ⑤.1–⑤.3, ⑥ red); the record carry dropped (③); the carrier judged on the snapshot (⑤.1, ⑤.4); the own copy without the refusal (②.4, ⑥.1–⑥.3); a plain read materialized instead of the refusal (⑥.1–⑥.3); no detach (④.3, ⑥.4); a callable carrier received as an object instead of refused (④.4; Part E).
|
|
50
|
+
|
|
51
|
+
#### An inherited grant covers the classes it answered, and an inherited station classifies over this task's tool seat (#1218; behavior (tighten: the child's gate asks its own protection question under delegation; a deployment-marked tool's first delegated card is `operator_forced`); +0 exports, +0 codes, +0 wire keys) (@server @cli @client-core @test)
|
|
52
|
+
|
|
53
|
+
- **Before (v7.36.0).** Under delegation (the Agent tool) with the host's `toolPolicy` asking on a tool, the inherited station asked the host's approver once and recorded a grant on the approver's identity and the presented byte shape; the child's own gate reused it class-blind. Its own classification of the same call — `write_protection` (a protected target), `probe` (a shell read outside the session's directories), `operator_forced` (an egress / irreversible mark) — was never put to anyone: a `Write` onto `<root>/.claude/projects/slug/memory/a.md` under an approver answering yes to everything showed one card and wrote the file. The station also spelled every plain ask `ordinary` (its facts named no seat), so a deployment-marked tool's delegated card read `ordinary` beside `riskAxes: { egress: true }`. On this release's tree before the fix, the §4.6e by-class approver (`ordinary` yes, every other class no) therefore answered the one card and the protected write, the out-of-root `cat` and the marked tool all went through unattended.
|
|
54
|
+
- **Now.** One rule: a settlement covers the question it answered. The grant records the request's class set (`InheritedAskGrant.classes`, `ASK_CLASSES` order) and the gate reuses it only when its own stamped `classes` is covered (`askGrantCovers`: `ordinary` — the plain hesitation every other class is a stricter form of — by any card answered; every other class only when it was on the card); otherwise the gate asks its own question — a by-class host refuses the protection class, a person sees the protection card — and the grant is not consumed (the call-site sweep ends it). The inherited station classifies the ancestor's ask over this task's tool seat (the egress / irreversibility / shell-seat marks and their sources, read once with the request's `riskAxes` — `report.markSources` joins the name-readings table), once, before the ancestor's classifier is judged; the sandbox arm, the mode arbitration, the request's `askClass` / `classes` and the grant record all read that one classification. Measured: the protected write under the by-class approver — two cards, `["ordinary"]` then `["write_protection"]`, the second refused, the file not written; under yes-to-everything — the same two cards, then written; the marked tool — one card `["operator_forced"]`, refused by class, executed 0 (a person's yes runs it once, no second card); `cat <outside>` — `["ordinary"]` then `["probe"]`, refused, nothing printed; an ordinary write — one card, written. The `mandate` word is not part of the coverage judgment (it is display, never adjudication input, and the station's word is structurally absent: the ancestor door). A person who answered the station's stricter card — the classifier's bound fallback (`denial_limit_fallback`), a real-approval ask (`policy_rra`), a deployment-authored ask (`operator_forced`), an explicit ask rule (`ask_rule`), a guard — is not asked the plain `ordinary` question again about the same bytes: one card (the first form of this rule, a literal subset, asked two).
|
|
55
|
+
- **Who is hurt.** A deployment that relied on one ancestor-level yes clearing a delegated child's protected write, out-of-root shell read or marked tool silently: the child's gate now asks that question. A host that renders the first card of a delegated deployment-marked tool: under delegation it reads `operator_forced` where it read `ordinary` (the same class the gate has always given the tool's own card), and under `bypassPermissions` / `dontAsk` that inherited ask is bypass-immune at the station (asked at the ancestor's frozen approver, not released or refused by the mode there), as at the gate. `Read` outside the session's directories stays `ordinary` (§4.6e names the coverage). Under `plan`, a delegated call shows one more card than v7.36.0: the station's (`["ordinary"]`, or `["operator_forced"]` for a marked tool) and then the child's gate's `[…, "mode"]` — the mode's own question is a class no ancestor card carried, so it is put to the approver (v7.36.0 reused the grant class-blind and asked it of nobody).
|
|
56
|
+
- **Compensation.** A by-class approver per §4.6e — answer `ordinary`, leave the protection classes to a person — needs no change: the protection question now reaches it and it refuses. A host that wants a marked tool's delegated call to run answers its `operator_forced` card, once.
|
|
57
|
+
- Pins: `test/backlog1218-inherited-grant-coverage.test.ts` — ① the protected write under the by-class host (two cards, refused, not written, one `ask_resolution` refusal); ② the marked tool, both marks (one `operator_forced` card, refused, executed 0; a person's yes: executed once, no second card); ③ the out-of-root `cat` (two cards, `probe`, refused, not printed) and the `Read` control (one `ordinary` card); ④ an ordinary write (one card, covered, written); ⑤ yes-to-everything (`ordinary`, `write_protection`, written); ⑧ the consume half as a unit; ⑨ a stricter card covers the plain question — the bound fallback (`maxConsecutive: 1`) and the four decision-borne classes on the distinct arm, one card each, ran once; the predicate rows; the `write_protection` control (①.1 on the wire). `test/backlog681-auto-mandate-not-classifiable.test.ts`'s lexical anchor follows the sandbox arms to the one classification; `test/name-readings-oracle.test.ts` carries the new row.
|
|
58
|
+
- Mutations (`.gate-logs/criteria-7.37.0-mutations.md` Part E, per fixture): D1016 the consume half reuses without the class comparison (①, ③ shell, ⑤, ⑧.3 red); D1017 the station classifies over the bare station facts (② red, both marks); D1025 (Part G) the coverage predicate back to a literal subset (⑨.1–⑨.5 red: two cards, the second `ordinary`; ⑧ and the control green).
|
|
59
|
+
|
|
60
|
+
### A decision's action is a closed set (#1221; behavior: tighten; +0 exports, +0 codes, +0 wire keys) (@server @test)
|
|
61
|
+
|
|
62
|
+
- **Before (v7.36.0).** The policy fold ranks a decision by testing `action === "deny"` and `action === "ask"`; every other value ranked as an allow. A `ToolPolicy.check` answering `{ action: "bogus" }`, `{ message }` with no `action`, `{ action: "Allow" }`, `{ action: 1 }`, a bare `"deny"` string, or a function holding `action: "allow"` ran the tool: `tool_end.gate.disposition` `{ kind: "allowed" }`, no engine notice. The same held for an ancestor's policy on a delegated child (the inherited lane's receipt runs the same screen, which did not look at `action`).
|
|
63
|
+
- **Now.** The fold's out-of-contract screen holds `action` to allow / deny / ask. Any other value — absent, a case or whitespace variant, a number, an object, a Symbol, or a decision that is not an object at all (a function included) — is refused fail-closed before any approver, with the screen's own refusal: `tool_end.gate.disposition` `{ kind: "denied", deniedBy: "policy" }` (the record the retired `reason` field and an out-of-range `decisionReason` already get), the tool not run, and a model-facing text naming `"action"` and `allow/deny/ask`. It applies wherever the screen runs: every layer of the gate's fold, the inherited lane's receipt, and the direct re-checks (the resume edit, the park re-projection, the changed-files probe). `null` / `undefined` are refused at the same construction point with the screen's text (`not an object`); before, the caller's read of `.action` threw and the refusal read `policy check aborted (task timed out or cancelled)` — a wrong attribution, not a run. A PreToolUse hook's result is not screened for its action: the hook seam reads anything other than deny / ask as no objection, and the policy still decides the call.
|
|
64
|
+
- **Who is hurt.** A policy — the task's own or an ancestor's — that returns an action outside the three words, spells one in another case (`"Allow"`), leaves it out, or returns something other than an object: the tool ran before; it is refused now.
|
|
65
|
+
- **Compensation.** Return `{ action: "allow" | "deny" | "ask" }`, spelled exactly.
|
|
66
|
+
- Pins: `test/backlog1221-decision-action-closed-set.test.ts` — ① the gate lane and ② the inherited lane, six out-of-set shapes each (a function holding `action: "allow"` among them — refused by the carrier arm on the caller's object, on both lanes) (refused before any approver, the retired-`reason` refusal's record); ③ the three words on both lanes; ④ the screen's value shapes (empty, case and whitespace variants, number, boolean, object, null, Symbol, a null-prototype object, a non-object decision, a callable carrier, null / undefined, an unrenderable and an over-long word), the three words handed back value-equal on the engine's frozen receipt (never the caller's object; a receipt handed back is itself — #1226), and the hook seam left to its own mapping. The set is fenced to `PermissionResult["action"]` at compile time, both ways.
|
|
67
|
+
- Mutations (`.gate-logs/criteria-7.37.0-mutations.md` Part F, per fixture): D1018 the screen's action arm removed (①, ②, ④.1–④.3 red); non-objects passed through (①.5, ②.5, ④.2); a function read like an object (①.6, ②.6, ④.2); a case-insensitive test (①.3, ②.3, ④.1); the arm at the hook seam (④.5); a deny without the screen's `decisionReason` (④.1, ④.2). Part G re-runs this file on the receipt (the `snapcarrier` / `callable` rows of Part F describe a `carrierOf` seat the receipt no longer has; Part G supersedes them).
|
|
68
|
+
|
|
69
|
+
#### The contract screen is the one receipt of a policy decision (#1226; behavior (tighten: a host's direct `runToolGate` adjudication and the approved-edit re-check are screened; the screen hands back the engine's copy, never the caller's object); +0 exports, +0 codes, +0 wire keys) (@server @cli @test)
|
|
70
|
+
|
|
71
|
+
- **Before (v7.36.0).** The fold's out-of-contract screen validated a decision and returned the caller's object, and every station after it read that object again: the task's own policy's `action` accessor was read 17 times on the gate lane (measured), so a decision whose accessor changed its answer was decided by the read count. A host driving `runToolGate` itself (its own `adjudicate`), and the approved-edit re-check after a person's edit, never passed the screen: `{ action: "bogus" }`, `{ message }` (no action), the string `"deny"`, `{ action: "" }`, the retired `reason` field and an out-of-range `decisionReason` were allowed at the gate's entry and re-asked up to the edit chain's cap at the re-check. On this release's tree before the fix, the #1221 action arm read the object once more and returned it (19 reads: an accessor answering `deny` 17 times then `allow` ran the tool), and the inherited lane's receipt detached the decision before the screen saw the fallback member, so a retired `autoDenyAfterMs` on the fallback's prototype was erased by the clone and the call ran where v7.36.0 denied.
|
|
72
|
+
- **Now.** One function, one order, both lanes. `refuseOutOfContractDecision` (1) refuses a carrier that is not an object — a primitive, a function, `null`, `undefined` — on the caller's object; (2) reads ONCE, by one function for the decision and for the records it nests — `denialLimitFallback`, the probe account `probeCause` and its `roots` / `further` operand records (one rule at every level): a contract member is absent or an own enumerable DATA property, its value ONE plain read (no caller code runs in it on an ordinary object) that must agree with two looks at the carrier's property descriptors around it (on a Proxy the traps are caller code — what every channel of the carrier agrees on is kept); every other spelling is refused, the member named by path (`denialLimitFallback.consecutive`, `probeCause.roots.total`): an accessor (never invoked — a getter, a lazy getter that replaces itself, a sibling accessor that would rewrite `action`) or a read or shape that disagrees with the looks, and a member the prototype or a non-enumerable property supplies, unless that descriptor spells absence (an `undefined` data value — the typed spelling of an optional member left out, received as on v7.36.0) — judged by DESCRIPTOR, never by a read; and the carrier is an ORDINARY object: a Proxy, as the decision, as a nested record, or anywhere on the prototype chain (a revoked one too), is refused before a trap of its runs (`util.types.isProxy` invokes none). So the receipt runs none of the carrier's code, and nothing of the carrier's can edit a value between the one read and the detach — the review's r4 round found the tree before it probing an absent member with one plain read, which invoked a prototype getter that edited the rewrite the receipt had already taken a reference to (the edited rewrite ran), and an `in`-based fix invoking a Proxy prototype's `has` the same way. A Proxy decision was accepted on v7.36.0 and on this release's review trees when its channels agreed; it is refused now, with the screen's deny and a sentence naming it; a member outside the contract is neither read nor carried (a policy's self-declared `settlement` finds no reader); (3) refuses the retired `reason` as an own property (after the receipt, so a carrier the receipt refuses is never touched by the look); (4) holds the values of that one-read record — never a second read of the caller's object — to the contract: the retired window absent, `action` and `decisionReason` in their closed sets; (5) detaches the contract members (a structured clone; one that cannot be cloned is refused), carries the engine's identity-keyed attestations, freezes the copy AND the records it nests (the fallback, the probe account and its operand records — a wrapper editing a nested record of the receipt it was handed throws where the fast path used to carry the edit through unscreened) and marks the copy — a receipt handed to the screen again comes back as itself, unread. The descriptor map is read by own entry, so a polluted `Object.prototype` names no member; a prototype chain that throws while looked at is a deny naming the throw, not an exception. Every station after the receipt reads the copy. Entries: each layer of `combinePolicies`' fold, the chain instrumentation (the gate lane's first reader of the task's own policy), the gate's adjudication entry and its approved-edit re-check, the inherited lane's three receipts and the final-form re-check, the direct rechecks (resume edit, park re-projection, changed-files probe). Measured: the 17/19-read accessor — 0 reads, refused, ran 0; deny ×2 then `bogus` — 0 reads, refused (v7.36.0: allowed); the prototype window — denied on both lanes, `denialLimitFallback.autoDenyAfterMs` named; the fallback's window behind a getter answering `undefined` then `1` (gate lane through `combinePolicies`, and inherited) — 0 reads, refused, nobody asked (this release's tree before the r3 fix: the window check read `undefined`, the clone read `1`, the call was asked and ran with the window on the engine's copy; v7.36.0 refused it on the gate lane and, on the inherited lane, never read the member and ran the call); the fallback's counts on a class prototype under a real-approval ask — refused, `denialLimitFallback.consecutive` named (v7.36.0: the approver saw 3/3 and allowed; the tree before the r3 fix cloned the counts to `{}` and the approver refused, silently); a Proxy whose descriptor trap rewrites its target during the read, and one whose traps alternate its target so that every descriptor look reports a pair it never holds — refused as a Proxy, not one trap run (the review trees refused them after the looks, the member named); an operand count of the probe account on a prototype or non-enumerable — refused, `probeCause.roots.total` named (the tree before r3 cloned it away: the approver was shown `shown` alone); the six shapes at `runToolGate`'s own entry and the four at the edit re-check — blocked, `{ kind: "denied", deniedBy: "policy" }`, the screen's text; `null` / `undefined` — the same record with a text naming `not an object` (before: `policy check aborted (task timed out or cancelled)`, the caller's read having thrown). A null-prototype object, a frozen decision with frozen nested arguments and a class instance holding the contract members as its own fields are received as before. The hook seam keeps its own reach (the two retired-field arms, the object back unchanged). `snapshotAncestorDecision` (the inherited lane's receipt of 7.37.0's first cut) is retired into the screen; its member list `DECISION_MEMBERS` lives beside it, and the nested records' (`NESTED_CONTRACTS`: the fallback's `consecutive` / `total` / `limit` / `autoDenyAfterMs`, the probe account's `code` / `roots` / `further`, each operand record's `shown` / `total`) beside that. The bare allow `combinePolicies` answers when no layer rewrote and nothing was attested — one object for every such call in the process — is frozen.
|
|
73
|
+
- **Who is hurt.** A host whose `adjudicate` (driving `runToolGate` directly) or approved-edit re-check answers outside the contract — allowed / re-asked before, refused now. A host that relied on the object it returned being the object the engine holds: reference identity across the gate, or rewriting the decision (or the rewrite inside it) after returning it — the engine keeps its own copy now, and what it read is what runs. A policy whose `denialLimitFallback` carries the retired window on its prototype — refused on both lanes (the gate lane already did). A policy that puts members outside the contract on its decision: they were never read; they are no longer carried either. A policy that returns a Proxy as its decision (a logging wrapper, an observable) — accepted before when its traps agreed with each other; refused now, before a trap runs, with the screen's deny naming it: return the plain object. A policy — the task's own on the gate lane as much as an ancestor's — that supplies a contract member through a getter (a class result whose members are prototype getters, a lazy getter that caches itself as data on first read, an own `get action()`), carries one on its prototype, or answers with an `Error` as the carrier (its `message` is an own non-enumerable property): v7.36.0 read the getter at every station, and this release's tree before the r3 fix read it once and kept the first value; each shape either ran or kept its own text — refused now, in the screen's words, the member named. A policy whose `denialLimitFallback` supplies its counts from a prototype (a class instance): v7.36.0 showed the approver the counts; the tree before the r3 fix silently cloned them to `{}`; refused now with `denialLimitFallback.consecutive` named. A policy whose probe account (`probeCause`) holds a member on a prototype, as a non-enumerable property or behind an accessor: refused now, the member named by path. A host that wraps `combinePolicies(...).check` and edits the decision it is handed: the shared bare allow and the engine's receipt are frozen, so the edit throws a `TypeError` in strict code where it used to edit — for the shared allow, every later allow in the process; in sloppy-mode code (a CommonJS wrapper, `new Function`) the write is silently ignored instead, so a wrapper that edited the rewrite in place to redact arguments now runs the call with the model's own arguments — spread a copy (`{ ...d, updatedInput }`) and return that. A policy whose rewrite (`updatedInput`) carries a function or another value a structured clone cannot copy: on the park path it was refused at the park mint before (a `ParkRefusal` on the error face, the store untouched); on the Runner's synchronous path v7.36.0 allowed it and the call failed at execution (`… could not be cloned.`, the tool not run); a host driving `runToolGate` directly got it back unchanged — it is refused at the receipt now on every path, before any park is attempted, the store untouched, the deny quoting the runtime's sentence (a PreToolUse hook's rewrite still rides its seam to the park mint and is refused there, as before). A policy whose park-time re-adjudication (the stored-shape re-check under a JSON-fidelity checkpoint store) answers with a rewrite carrying an own accessor or a class brand whose data equals the filed value: the park guard used to refuse it as not naming that value (its raw-shape arm); the receipt reads the rewrite once into plain data and the guard compares that, so the call parks and the accessor is never read again.
|
|
74
|
+
- **Compensation.** Return plain data — `{ action: "allow" | "deny" | "ask", … }` with the contract members (and the fallback's counts) as own enumerable data properties, no accessors, no retired members — and treat the returned object as consumed. A wrapper that adjusts a decision spreads a copy (`{ ...d, … }`) and edits that.
|
|
75
|
+
- Pins: `test/backlog1226-decision-receipt.test.ts` — ⑪ a prototype getter for an absent contract member that edits the rewrite / the action when invoked (both lanes × three edits, and the screen: refused with `probeCause` named, invoked 0, ran 0 — the r3 tree invoked it once and ran the edited rewrite; v7.36.0 never invoked it and ran the rewrite as returned); ⑫ the ordinary-object law's edges: an `undefined` data property on the prototype / non-enumerable / on the probe account received as absent (both lanes, as v7.36.0); a Proxy on the prototype chain whose `has` / descriptor trap edits the rewrite refused with 0 traps run (the `in` tree ran the edited rewrite); a revoked Proxy as the fallback record refused in the screen's words, no TypeError (the r3 tree threw); a `get`-only Proxy under an ancestor `onAsk: "allow"` refused (v7.36.0 refused at the approval; the `in` tree ran); ④ a Proxy carrier refused before a single trap (looks 0, gets 0) on the lane and at `runToolGate`'s entry, its ordinary twin received once into the frozen copy; ① the 17/19-read accessor (0 reads, refused, both lanes); ② deny ×2 then `bogus` (0 reads, refused, both lanes); ③ the prototype window (both lanes, and the screen); ④ a Proxy carrier on the gate lane and at `runToolGate`'s entry refused before a single trap (looks 0, gets 0), its ordinary twin received once into the frozen copy, the same members behind own getters refused, none invoked — beside `test/backlog564-ask-origin.test.ts` and `test/backlog619-read-only-gate.test.ts` (a stateful Proxy carrier refused unread, the plain twin carrying the stamped member and its word, an accessor refused unread); ⑬ the copy discipline observed from outside: a host editing the object it returned inside `resolveAsk` — the executed arguments and the re-check's request are the receipt's copy — and a wrapper editing the receipt's nested record throws (frozen); ⑤ `runToolGate`'s entry on eight shapes (the six, `null`, `undefined`) and the three-word control; ⑥ the approved-edit re-check and the final-form re-check on the four shapes, with the in-contract control; ⑦ the null-prototype, frozen and class-instance carriers received on both lanes; ⑧ the Proxy's virtual member and the rewriting sibling accessor (both key orders; refused as an accessor, never invoked) refused on both lanes and at the screen; ⑨ the receipt as a unit (value-equal, not the caller's object, frozen, itself on a second pass; the detached rewrite; the unclonable rewrite; `null` / `undefined` on both lanes; the hook seam); ⑩ the fallback record received by the decision's function — the window getter refused unread on the gate lane through `combinePolicies`, on the inherited lane and at the screen; prototype counts refused with `denialLimitFallback.consecutive` named on both lanes; own-data counts, and an own `autoDenyAfterMs: undefined`, received with the approver shown 3/3 and the record carrying the contract's members only; the frozen bare allow (a wrapper's edit throws, a second Runner's allow unedited); a descriptor trap that rewrites its target mid-read, and one that alternates it (every look agreeing on a pair it never holds), refused on both lanes and at the screen; the probe account's prototype / non-enumerable operand count and accessor `code` refused by path on both lanes, its own-data twin reaching the approver whole. `test/backlog1221-decision-action-closed-set.test.ts` ④.2 / ④.4 and `test/policy-out-of-contract-decision.test.ts`'s four same-reference pins read the receipt form. `test/park-mint-unclonable-args.test.ts`: a policy's uncapturable rewrite refused at the receipt (store untouched), a hook's at the park mint (the `cause` by identity). `test/park-mint-backend-fidelity.test.ts`: under a JSON-fidelity store, a re-projection rewrite carrying a class brand, a symbol key, a null prototype, an inextensible or frozen twin, or an own accessor parks with the plain projected value (the accessor read once); key order and a Proxy stay refused.
|
|
76
|
+
- Mutations (`.gate-logs/criteria-7.37.0-mutations.md` Part G, per fixture): D1023 the screen returns the caller's object after validating it (⑨.1 / ⑨.2 / ⑩.8 / ⑩.11 and `backlog1217` ④ red; ①, ②, ④ green by construction since r5 — accessors and Proxies are refused before anything is handed on); D1024 the fallback's window judged after the detach (③ red); D1025 the coverage predicate back to a literal subset (`backlog1218` ⑨.1–⑨.5 red); D1018 the action arm removed (②, ⑤, ⑥ red); D1015 the receipt hands on the caller's object unread; the pre-fix carrier arm (`null` / `undefined` through); a prototype / Proxy member materialized; the descriptor comparison removed; the receipt unmarked; the chain instrumentation reading the layer's object directly (①.1, ④.1 red: n = 2). Part I (the r4 form): the probe read restored (⑪ ×7, ⑩.4 / ⑩.5, ⑧.1 / ⑧.4 / ⑧.7, `backlog1217` ②.4 red — 13); the carrier's Proxy check removed (⑫.3, ⑫.4, ④, ⑧, ⑩.10 / ⑩.12, `backlog1217` ② red — the prototype-chain check still names it); `undefined` no longer spelling absence (⑫.1 red); the prototype-chain Proxy check removed (⑫.2 red). Part H (the r3 form, per fixture): D1026 the window judged by a plain read of the caller's fallback before its receipt (③, ⑩.1–⑩.3 red: the retired-window words in place of the path, the getter read); D1027 the nested records left to the clone alone, no descriptor judgment (③, ⑩.1–⑩.5, ⑩.8, ⑩.11 red); the probe account's rows dropped from the nested table (⑩.11 red); the read's agreement with the looks removed (⑩.12 and `backlog619`'s disagreeing read red); values taken off the first look instead of a read (⑩.12 and the one-read rulers red); one look (the look-count rulers and `backlog564`'s changing-descriptor twin red; ⑩.10 stays refused by the read's agreement); the Part G variants re-run on the r3 screen, the descriptor comparison's row replaced by accessors read and kept (①, ②, ④.3, ⑧'s accessor rows, ⑩.1–⑩.3, ⑩.11, `backlog1217` ①, ⑤.1, ⑤.4, ⑥.5, `backlog564` / `backlog619` r2 and `policy-out-of-contract-decision`'s accessor pin red); the bare allow unfrozen (⑩.9 red).
|
|
77
|
+
|
|
78
|
+
### Documentation
|
|
79
|
+
|
|
80
|
+
- The `peer.held_settled` catalog entry (`EngineNotice.code` JSDoc) lists all nine `detail.reason` words (`PeerHeldSettleReason`). It adds `superseded`: a record another lease holder already settled, sent no receipt from this session. Comment only.
|
|
81
|
+
|
|
1
82
|
## 7.36.0 — 2026-09-30
|
|
2
83
|
|
|
3
84
|
**Hard-breaking changes — the whole release in three sentences.**
|
|
@@ -107,8 +107,9 @@ export interface AskOriginFacts {
|
|
|
107
107
|
/** The MANDATE mark inputs of this call — the tool SEAT's marks plus the gate's own per-call records
|
|
108
108
|
* ({@link MandateMarks}), read from the same two places the lane's own derivation reads them. The
|
|
109
109
|
* per-call PROBE member is deliberately not here: it is engine-stamped on the DECISION, and
|
|
110
|
-
* {@link ruleClearanceOf} reads it there, so one fact cannot arrive twice with two values.
|
|
111
|
-
*
|
|
110
|
+
* {@link ruleClearanceOf} reads it there, so one fact cannot arrive twice with two values. An inherited
|
|
111
|
+
* station passes the tool SEAT's marks of the task it runs in (its per-call records are the gate's, absent
|
|
112
|
+
* there); a station holding no seat at all passes `{}` — an honest empty, since the decision's own
|
|
112
113
|
* members still answer. */
|
|
113
114
|
readonly mandateMarks: Omit<MandateMarks, "probeMandated">;
|
|
114
115
|
}
|
|
@@ -133,12 +134,15 @@ export declare function askOriginOf(decision: Extract<PermissionResult, {
|
|
|
133
134
|
* <out-of-root>` under a standing `Bash(cat:*)` ask rule offered the directory and kept asking after it).
|
|
134
135
|
* It lives at the one derivation rather than the three stamp sites: the withdrawal's input IS the origin
|
|
135
136
|
* word, so a site that stamped without it would be a second, quieter answer to one question.
|
|
137
|
+
* The same stamp writes the ask's CLASS members (`askClass` / `classes`, {@link askClassMembersOf} over the
|
|
138
|
+
* `classification` the caller derived from this decision under these facts): the word and the class leave the
|
|
139
|
+
* gate on one object, and a value a policy put on either is replaced.
|
|
136
140
|
*/
|
|
137
141
|
export declare function stampAskOrigin<T extends Extract<PermissionResult, {
|
|
138
142
|
action: "ask";
|
|
139
|
-
}>>(decision: T, facts: AskOriginFacts): T & {
|
|
143
|
+
}>>(decision: T, facts: AskOriginFacts, classification: AskClassification): T & {
|
|
140
144
|
origin: AskOrigin;
|
|
141
|
-
};
|
|
145
|
+
} & AskClassMembers;
|
|
142
146
|
/**
|
|
143
147
|
* CAN a persisted allow rule clear this ask — and, if not, which door closed? ONE derivation with TWO
|
|
144
148
|
* readers, which is the whole point of the shape: the approval card's `ruleOffersAbsence` word and the
|
|
@@ -610,6 +614,18 @@ export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
|
|
|
610
614
|
/** The classes a permission mode can RELEASE (the `bypassImmune: false` rows) — the only words that can appear on
|
|
611
615
|
* `GateDisposition.released.askClass`; every other class reaches a person under every mode. */
|
|
612
616
|
export declare const RELEASABLE_ASK_CLASSES: readonly AskClass[];
|
|
617
|
+
/**
|
|
618
|
+
* The closed set of `reason` words a mode release carries (`GateDisposition.released.reason`, the `released` arm of
|
|
619
|
+
* {@link Arbitration}): each is a releasable class the release record names on its own — `rule_unreadable`, a release
|
|
620
|
+
* over a person's rule the call could not be read against. A release carries the first member (array order) its class
|
|
621
|
+
* set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
|
|
622
|
+
* of this list taken at load (a caller that writes into the exported array changes neither), so a new reason is one
|
|
623
|
+
* word here.
|
|
624
|
+
*/
|
|
625
|
+
export declare const MODE_RELEASE_REASONS: readonly ["rule_unreadable"];
|
|
626
|
+
export type ModeReleaseReason = (typeof MODE_RELEASE_REASONS)[number];
|
|
627
|
+
/** Membership test for {@link ModeReleaseReason} — the word crosses the wire on `tool_end.gate.disposition.released.reason`. */
|
|
628
|
+
export declare function isModeReleaseReason(v: unknown): v is ModeReleaseReason;
|
|
613
629
|
/**
|
|
614
630
|
* The facts the class derivation reads beside the decision: the origin facts the gate holds, plus the tool
|
|
615
631
|
* seat's mark PROVENANCE ({@link ToolMarkSource}; a mark with no source reads as the deployment's, fail-closed),
|
|
@@ -628,6 +644,20 @@ export interface AskClassification {
|
|
|
628
644
|
readonly askClass: AskClass;
|
|
629
645
|
readonly row: ModeArbitrationRow;
|
|
630
646
|
}
|
|
647
|
+
/** The two members an ask's classification puts on the approval request and the park row: the disclosed word and
|
|
648
|
+
* the whole set, the set as an array in {@link ASK_CLASSES} order (so `classes[0] === askClass`). */
|
|
649
|
+
export interface AskClassMembers {
|
|
650
|
+
readonly askClass: AskClass;
|
|
651
|
+
readonly classes: readonly AskClass[];
|
|
652
|
+
}
|
|
653
|
+
/** The ONE projection of a classification onto {@link AskClassMembers} — every station that carries an ask's class
|
|
654
|
+
* spells it through this, so the word and the set cannot be read from two derivations. The array is a fresh frozen
|
|
655
|
+
* copy per call: an approver that holds it cannot reach the park row's copy or the next card's. */
|
|
656
|
+
export declare function askClassMembersOf(c: AskClassification): AskClassMembers;
|
|
657
|
+
/** The screen a boundary applies before it writes the pair where a host reads it back (the park row): both or
|
|
658
|
+
* neither, and only a well-formed pair — every member of `classes` in the closed set, no repeat, and `askClass` its
|
|
659
|
+
* first member in {@link ASK_CLASSES} order. The copy returned is fresh and frozen, never the caller's array. */
|
|
660
|
+
export declare function screenAskClassMembers(askClass: unknown, classes: unknown): AskClassMembers | undefined;
|
|
631
661
|
/**
|
|
632
662
|
* The DECISION half of the class derivation — what ONE ask says about itself through its own members, read
|
|
633
663
|
* per contributing ask at the policy fold so a member the fold does not copy (a guard mark, the unreadable-rule
|
|
@@ -699,7 +729,7 @@ export type Arbitration = {
|
|
|
699
729
|
classes: readonly AskClass[];
|
|
700
730
|
origin?: AskOrigin;
|
|
701
731
|
mandate?: PersistedRuleMandate;
|
|
702
|
-
reason?:
|
|
732
|
+
reason?: ModeReleaseReason;
|
|
703
733
|
} | {
|
|
704
734
|
kind: "denied_by_mode";
|
|
705
735
|
mode: "bypassPermissions" | "dontAsk";
|
package/dist/core/ask-origin.js
CHANGED
|
@@ -11,15 +11,16 @@ export function askOriginOf(decision, facts) {
|
|
|
11
11
|
return origin;
|
|
12
12
|
}
|
|
13
13
|
const TIGHTEN_ONLY_ORIGINS = new Set(["shell_gate_tighten", "safety_tighten"]);
|
|
14
|
-
export function stampAskOrigin(decision, facts) {
|
|
14
|
+
export function stampAskOrigin(decision, facts, classification) {
|
|
15
15
|
const origin = askOriginOf(decision, facts);
|
|
16
|
+
const classed = askClassMembersOf(classification);
|
|
16
17
|
const { mandate: _selfDeclared, ...unstamped } = decision;
|
|
17
18
|
const clearance = ruleClearanceOf(unstamped, facts.mandateMarks);
|
|
18
19
|
const mandated = { ...unstamped, ...(clearance.mandated ? { mandated: true } : {}), ...(clearance.mandate !== undefined ? { mandate: clearance.mandate } : {}) };
|
|
19
20
|
if (mandated.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
|
|
20
|
-
return { ...mandated, origin };
|
|
21
|
+
return { ...mandated, origin, ...classed };
|
|
21
22
|
const { probeReadRootCandidate: _withdrawn, ...kept } = mandated;
|
|
22
|
-
return { ...kept, origin };
|
|
23
|
+
return { ...kept, origin, ...classed };
|
|
23
24
|
}
|
|
24
25
|
export function ruleClearanceOf(ask, marks) {
|
|
25
26
|
const marksWord = persistedRuleMandateOf({ ...marks, probeMandated: ask?.probeMandated === true });
|
|
@@ -215,6 +216,23 @@ export const MODE_ARBITRATION = {
|
|
|
215
216
|
};
|
|
216
217
|
export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read"]);
|
|
217
218
|
export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
|
|
219
|
+
export const MODE_RELEASE_REASONS = ["rule_unreadable"];
|
|
220
|
+
const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
|
|
221
|
+
export function isModeReleaseReason(v) {
|
|
222
|
+
return MODE_RELEASE_REASON_SET.has(v);
|
|
223
|
+
}
|
|
224
|
+
export function askClassMembersOf(c) {
|
|
225
|
+
return { askClass: c.askClass, classes: Object.freeze(ASK_CLASSES.filter((k) => c.classes.has(k))) };
|
|
226
|
+
}
|
|
227
|
+
export function screenAskClassMembers(askClass, classes) {
|
|
228
|
+
if (!isAskClass(askClass) || !Array.isArray(classes) || classes.length === 0 || !classes.every(isAskClass))
|
|
229
|
+
return undefined;
|
|
230
|
+
const held = new Set(classes);
|
|
231
|
+
const ordered = ASK_CLASSES.filter((k) => held.has(k));
|
|
232
|
+
if (ordered.length !== classes.length || ordered[0] !== askClass)
|
|
233
|
+
return undefined;
|
|
234
|
+
return { askClass, classes: Object.freeze(ordered) };
|
|
235
|
+
}
|
|
218
236
|
export function decisionAskClassesOf(d) {
|
|
219
237
|
const out = new Set();
|
|
220
238
|
if (d.guard === "unverifiable_delete")
|
|
@@ -339,6 +357,7 @@ export function arbitrateByMode(mode, c, disclosure, call) {
|
|
|
339
357
|
return { kind: "denied_by_mode", mode, askClass: c.askClass, classes };
|
|
340
358
|
if (c.row.bypassImmune)
|
|
341
359
|
return { kind: "ask" };
|
|
360
|
+
const reason = [...MODE_RELEASE_REASON_SET].find((r) => has(r));
|
|
342
361
|
return {
|
|
343
362
|
kind: "released",
|
|
344
363
|
mode,
|
|
@@ -346,7 +365,7 @@ export function arbitrateByMode(mode, c, disclosure, call) {
|
|
|
346
365
|
classes,
|
|
347
366
|
...(disclosure?.origin !== undefined ? { origin: disclosure.origin } : {}),
|
|
348
367
|
...(disclosure?.mandate !== undefined ? { mandate: disclosure.mandate } : {}),
|
|
349
|
-
...(
|
|
368
|
+
...(reason !== undefined ? { reason } : {}),
|
|
350
369
|
};
|
|
351
370
|
case "acceptEdits":
|
|
352
371
|
return call?.fileCommand === true && c.classes.size === 1 && c.classes.has("ordinary") ? { kind: "accepted", mode } : { kind: "ask" };
|
|
@@ -834,6 +834,16 @@ export type PendingAction = {
|
|
|
834
834
|
* Written only when it is a member of the set; a pre-existing row reads absent (unreported).
|
|
835
835
|
* Display metadata — the resume belts keep reading the gate's own bit. */
|
|
836
836
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
837
|
+
/** (additive, same precedent) — the PARK twin of `AskRequest.askClass`: the parked ask's class, the first member of
|
|
838
|
+
* {@link classes}, from the same classification the gate arbitrated it by — so a durable approval seat can answer by
|
|
839
|
+
* class exactly as a synchronous `onAsk` does. Written by the park mint only as a well-formed pair with
|
|
840
|
+
* {@link classes} (both or neither). A row parked by an engine before this member existed reads ABSENT
|
|
841
|
+
* (unclassified), and a resume does not fill it in: a seat answering by class treats absence as a class it will
|
|
842
|
+
* not answer. Display / seat metadata — no gate, CAS or resume path reads it. */
|
|
843
|
+
askClass?: import("./ask-origin.js").AskClass;
|
|
844
|
+
/** The PARK twin of `AskRequest.classes`: every class of the parked ask, in `ASK_CLASSES` order
|
|
845
|
+
* (`classes[0] === askClass`). Present exactly when {@link askClass} is. */
|
|
846
|
+
classes?: readonly import("./ask-origin.js").AskClass[];
|
|
837
847
|
/**
|
|
838
848
|
* design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
|
|
839
849
|
* SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
|
|
@@ -1125,7 +1135,7 @@ export interface CheckpointState {
|
|
|
1125
1135
|
* revive the mode where the redeeming deployment denies it, and an `auto` row arms only where a face is
|
|
1126
1136
|
* wired. Absent on a task that declared nothing and inherited nothing (`default`). Rows written before this
|
|
1127
1137
|
* member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; the resume leg migrates
|
|
1128
|
-
* them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. */
|
|
1138
|
+
* them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. The migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys. */
|
|
1129
1139
|
permissionMode?: PermissionMode;
|
|
1130
1140
|
/** Beside a recorded `plan`: the declared word the leg had when a steer or the model's `EnterPlanMode` moved it into
|
|
1131
1141
|
* plan — the mode an approved or edited `plan_review` resumes under ({@link import("./mode-policy.js").modeRequestAfterResume}). */
|
|
@@ -3044,7 +3054,7 @@ export interface LegacyModeSeedMigration {
|
|
|
3044
3054
|
* bypass);
|
|
3045
3055
|
* - `shellGate`: absent ⇒ none; `"always"` ⇒ kept (a live key, not a retired encoding); `"off"` / `"classify"` ⇒
|
|
3046
3056
|
* dropped (no floor); anything else ⇒ refused (`resume.legacy_mode_seed`).
|
|
3047
|
-
* Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates
|
|
3048
|
-
*
|
|
3057
|
+
* Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates nothing ("once" is per row).
|
|
3058
|
+
* Migration precedes any retired-key door, so a window row is read rather than refused. The window: this migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys — resume or abandon a row parked before 7.34.0 before upgrading to 7.38.0.
|
|
3049
3059
|
*/
|
|
3050
3060
|
export declare function migrateLegacyModeSeed(gate: object | undefined): LegacyModeSeedMigration;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PermissionResult } from "./tool-policy.js";
|
|
2
|
+
/** The screen's one refusal — every arm's deny, so an out-of-contract decision is one code whichever arm caught it. */
|
|
3
|
+
export declare const refusedOutOfContract: (message: string) => PermissionResult;
|
|
4
|
+
/** One level of the receipt, the same for the decision and each record it nests. The carrier is an ORDINARY object — it and
|
|
5
|
+
* every object on its prototype chain are looked at for being a Proxy before any descriptor is asked for (a Proxy's traps are
|
|
6
|
+
* the caller's code; the look invokes none of them) — so the receipt runs none of the carrier's code: a contract member is
|
|
7
|
+
* absent or an own enumerable DATA property, its value taken off the descriptor (no read at all). Anything else refuses the
|
|
8
|
+
* carrier, the member named by `path`: an accessor (never invoked), and a member the prototype or a non-enumerable property
|
|
9
|
+
* supplies, unless the nearest descriptor holding it spells absence (an `undefined` data property). Descriptors are looked up
|
|
10
|
+
* as OWN entries of the descriptor map, so a polluted `Object.prototype` names no member. Members outside the contract are
|
|
11
|
+
* not looked at. */
|
|
12
|
+
/** The receipt's own answers carry a `kind` discriminant as an own property: narrowing never asks the prototype chain (`in`) for a
|
|
13
|
+
* word, so a polluted `Object.prototype` cannot answer for the receipt either. */
|
|
14
|
+
export type Received = {
|
|
15
|
+
kind: "held";
|
|
16
|
+
held: Record<string, unknown>;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "refused";
|
|
19
|
+
refused: PermissionResult;
|
|
20
|
+
};
|
|
21
|
+
export declare function receiveContract(carrier: object, members: readonly string[], path: string): Received;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { types } from "node:util";
|
|
2
|
+
import { thrownText } from "./thrown-value.js";
|
|
3
|
+
export const refusedOutOfContract = (message) => ({ action: "deny", message, decisionReason: "rule" });
|
|
4
|
+
const spellsAbsence = (d) => Object.hasOwn(d, "value") && d.value === undefined;
|
|
5
|
+
function ordinaryChain(carrier) {
|
|
6
|
+
const chain = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
try {
|
|
9
|
+
for (let o = carrier; o !== null; o = Object.getPrototypeOf(o)) {
|
|
10
|
+
if (types.isProxy(o))
|
|
11
|
+
return { kind: "not", why: o === carrier ? "is a Proxy" : "has a Proxy on its prototype chain" };
|
|
12
|
+
if (seen.has(o))
|
|
13
|
+
return { kind: "not", why: "has a cyclic prototype chain" };
|
|
14
|
+
seen.add(o);
|
|
15
|
+
chain.push(o);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { kind: "not", why: `threw while its prototype chain was looked at (${thrownText(e)})` };
|
|
20
|
+
}
|
|
21
|
+
return { kind: "chain", chain };
|
|
22
|
+
}
|
|
23
|
+
export function receiveContract(carrier, members, path) {
|
|
24
|
+
const what = path === "" ? "a permission decision" : `a permission decision's "${path.slice(0, -1)}" record`;
|
|
25
|
+
const ordinary = ordinaryChain(carrier);
|
|
26
|
+
if (ordinary.kind === "not") {
|
|
27
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what} ${ordinary.why} — the engine reads a decision off an ordinary object's own property descriptors and runs none of the carrier's code; denied fail-closed`) };
|
|
28
|
+
}
|
|
29
|
+
let own;
|
|
30
|
+
try {
|
|
31
|
+
own = Object.getOwnPropertyDescriptors(carrier);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what} threw while its property descriptors were looked at (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
|
|
35
|
+
}
|
|
36
|
+
const held = Object.create(null);
|
|
37
|
+
for (const m of members) {
|
|
38
|
+
const d = Object.hasOwn(own, m) ? own[m] : undefined;
|
|
39
|
+
if (d !== undefined && !Object.hasOwn(d, "value")) {
|
|
40
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what}'s contract member "${path}${m}" is held by an accessor — contract members must be own data properties, not values supplied through an accessor; denied fail-closed`) };
|
|
41
|
+
}
|
|
42
|
+
if (d !== undefined && d.enumerable === true) {
|
|
43
|
+
held[m] = d.value;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
let reached = d;
|
|
47
|
+
try {
|
|
48
|
+
for (let i = 1; reached === undefined && i < ordinary.chain.length; i++)
|
|
49
|
+
reached = Object.getOwnPropertyDescriptor(ordinary.chain[i], m);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return { kind: "refused", refused: refusedOutOfContract(`${what} threw while "${path}${m}" was looked for on its prototype chain (${thrownText(e)}) — not an ordinary object; denied fail-closed`) };
|
|
53
|
+
}
|
|
54
|
+
if (reached !== undefined && !spellsAbsence(reached)) {
|
|
55
|
+
return {
|
|
56
|
+
kind: "refused",
|
|
57
|
+
refused: refusedOutOfContract(`${what} supplies "${path}${m}" other than as its own enumerable property (its prototype or a non-enumerable property) — ` +
|
|
58
|
+
`contract fields must be own properties, so every reader sees the one value the engine read; denied fail-closed`),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { kind: "held", held };
|
|
63
|
+
}
|
|
@@ -337,7 +337,8 @@ export interface EngineNotice {
|
|
|
337
337
|
* `permissionMode` (`autoModeRequested: true` ⇒ `auto`; a posture is dropped — it never authorized anything;
|
|
338
338
|
* `shellGate` `"off"` / `"classify"` are dropped, `"always"` kept) and the next suspend writes the new member.
|
|
339
339
|
* `detail: { migrated: { permissionMode?, droppedKeys }, sessionId, runId }`. Audience `operator`. A row the
|
|
340
|
-
* migration cannot read refuses the resume instead (`resume.legacy_mode_seed`, the error-code row above).
|
|
340
|
+
* migration cannot read refuses the resume instead (`resume.legacy_mode_seed`, the error-code row above). The
|
|
341
|
+
* migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys.
|
|
341
342
|
* - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
|
|
342
343
|
* checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
|
|
343
344
|
* `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
|
|
@@ -712,10 +713,12 @@ export interface EngineNotice {
|
|
|
712
713
|
* `detail.settlement` names how: `"delivered"` (a re-judgment or an approval released it into
|
|
713
714
|
* this session's model context), `"refused"` (the setting moved to refuse, or an approval met a
|
|
714
715
|
* refusing policy), `"expired"` (the `dialogExpiry` deadline, an eviction when the 100-entry queue
|
|
715
|
-
* was full, a cancelled review,
|
|
716
|
-
* (the injection lane refused the released frame).
|
|
717
|
-
* (`deadline | evicted | shutdown | rejudge |
|
|
718
|
-
*
|
|
716
|
+
* was full, a cancelled review, a graceful shutdown, or a record another lease holder already settled),
|
|
717
|
+
* `"denied"` (the review face), `"dropped"` (the injection lane refused the released frame).
|
|
718
|
+
* `detail.reason` is the trigger (`PeerHeldSettleReason`: `deadline | evicted | shutdown | rejudge |
|
|
719
|
+
* approved | denied | cancelled | inject_failed | superseded`); the sender was sent the matching receipt
|
|
720
|
+
* whenever the record carried a reply address — except on `superseded`, where the lease holder that
|
|
721
|
+
* settled the record sent it.
|
|
719
722
|
* `detail: { settlement, reason, seq, box, fromSession?, heldCount, sessionId, runId }`. Audience
|
|
720
723
|
* `"user"`: it is this session's user who held (or was holding) the message.
|
|
721
724
|
* - `"peer.idle_subscription"` (design/385 §5.2, slice 4) — a peer session asked to be told when
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -6,11 +6,12 @@ import { traceHookCrash } from "./gate-crash-trace.js";
|
|
|
6
6
|
import { writeProtectionMandated, writeProtectionTightenDecision, writeProtectionUnresolved } from "./write-protect.js";
|
|
7
7
|
import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js";
|
|
8
8
|
import { thrownText } from "./thrown-value.js";
|
|
9
|
+
import { refuseOutOfContractDecision } from "./tool-policy.js";
|
|
9
10
|
export async function runGateFold(pass) {
|
|
10
11
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
|
|
11
12
|
const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
|
|
12
13
|
pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
13
|
-
pass.decision = adjudicate ? await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req)) : { action: "allow" };
|
|
14
|
+
pass.decision = adjudicate ? refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req))) : { action: "allow" };
|
|
14
15
|
{
|
|
15
16
|
const refusedBy = ledger.fromDecision(pass.decision);
|
|
16
17
|
if (refusedBy !== undefined)
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown } from "./tool-policy.js";
|
|
1
|
+
import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
2
|
import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, mandateDisclosureOf, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
|
|
3
3
|
import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
4
|
import { classifierUnavailableArm } from "./classifier-unavailable-arm.js";
|
|
@@ -181,9 +181,7 @@ export async function runGateLanes(pass) {
|
|
|
181
181
|
...(input.toolMarkSources !== undefined ? { toolMarkSources: input.toolMarkSources } : {}),
|
|
182
182
|
...(input.a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
|
|
183
183
|
});
|
|
184
|
-
const classifyStanding = (unstamped, facts) =>
|
|
185
|
-
pass.askClassification = classifyAsk(unstamped, facts);
|
|
186
|
-
};
|
|
184
|
+
const classifyStanding = (unstamped, facts) => (pass.askClassification = classifyAsk(unstamped, facts));
|
|
187
185
|
let orgTightenCount = 0;
|
|
188
186
|
let orgEvidence = (() => {
|
|
189
187
|
const w = input.orgRules === undefined ? "not_wired" : "not_adjudicated";
|
|
@@ -355,8 +353,7 @@ export async function runGateLanes(pass) {
|
|
|
355
353
|
const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
|
|
356
354
|
if (pass.decision.action === "ask") {
|
|
357
355
|
const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
358
|
-
classifyStanding(stamped, originFacts);
|
|
359
|
-
pass.decision = stampAskOrigin(stamped, originFacts);
|
|
356
|
+
pass.decision = stampAskOrigin(stamped, originFacts, classifyStanding(stamped, originFacts));
|
|
360
357
|
}
|
|
361
358
|
if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
|
|
362
359
|
const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
|
|
@@ -399,8 +396,7 @@ export async function runGateLanes(pass) {
|
|
|
399
396
|
else if (verdict?.kind === "unavailable") {
|
|
400
397
|
const arm = classifierUnavailableArm({ ask: pass.decision, req: pass.req, cause: verdict.cause, attribution: verdict.classifier, seat: input.autoMode.unavailable, aborted: classifySignal?.aborted === true });
|
|
401
398
|
if (arm.kind === "asked") {
|
|
402
|
-
classifyStanding(arm.ask, originFacts);
|
|
403
|
-
pass.decision = stampAskOrigin(arm.ask, originFacts);
|
|
399
|
+
pass.decision = stampAskOrigin(arm.ask, originFacts, classifyStanding(arm.ask, originFacts));
|
|
404
400
|
pass.classifierRound = undefined;
|
|
405
401
|
}
|
|
406
402
|
else
|
|
@@ -418,8 +414,7 @@ export async function runGateLanes(pass) {
|
|
|
418
414
|
requiresRealApproval: true,
|
|
419
415
|
denialLimitFallback: tracked.fallback,
|
|
420
416
|
};
|
|
421
|
-
classifyStanding(fallbackAsk, originFacts);
|
|
422
|
-
pass.decision = stampAskOrigin(fallbackAsk, originFacts);
|
|
417
|
+
pass.decision = stampAskOrigin(fallbackAsk, originFacts, classifyStanding(fallbackAsk, originFacts));
|
|
423
418
|
}
|
|
424
419
|
else {
|
|
425
420
|
const denied = {
|
|
@@ -517,6 +512,7 @@ export async function runGateLanes(pass) {
|
|
|
517
512
|
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
518
513
|
...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
|
|
519
514
|
...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
|
|
515
|
+
...(d.action === "ask" && d.askClass !== undefined && d.classes !== undefined ? { askClass: d.askClass, classes: d.classes } : {}),
|
|
520
516
|
...(d.action === "ask" && d.mandated === true ? { mandated: true, ...(d.mandate !== undefined ? { mandate: d.mandate } : {}) } : {}),
|
|
521
517
|
...(d.action === "ask" && d.authoredBy === "deployment" ? { authoredBy: "deployment" } : {}),
|
|
522
518
|
});
|
|
@@ -662,7 +658,7 @@ export async function runGateLanes(pass) {
|
|
|
662
658
|
if (adjudicate) {
|
|
663
659
|
try {
|
|
664
660
|
const editReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
665
|
-
recheck = await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq));
|
|
661
|
+
recheck = refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq)));
|
|
666
662
|
}
|
|
667
663
|
catch (err) {
|
|
668
664
|
recheck = {
|
|
@@ -720,8 +716,7 @@ export async function runGateLanes(pass) {
|
|
|
720
716
|
const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
721
717
|
markedThisRound = undefined;
|
|
722
718
|
const editFacts = askOriginFacts(editOrg.org, editRuleCallUnreadable);
|
|
723
|
-
classifyStanding(editAskSnapshot, editFacts);
|
|
724
|
-
const editAskDecision = stampAskOrigin(editAskSnapshot, editFacts);
|
|
719
|
+
const editAskDecision = stampAskOrigin(editAskSnapshot, editFacts, classifyStanding(editAskSnapshot, editFacts));
|
|
725
720
|
if (input.permissionMode !== undefined && pass.askClassification !== undefined) {
|
|
726
721
|
const editVerdict = arbitrateByMode(input.permissionMode(), pass.askClassification, { ...(editAskDecision.origin !== undefined ? { origin: editAskDecision.origin } : {}), ...(editAskDecision.mandate !== undefined ? { mandate: editAskDecision.mandate } : {}) });
|
|
727
722
|
if (editVerdict.kind === "released") {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* policy composes: a policy says allow / ask / deny; this file says how the pass that consumed that
|
|
8
8
|
* verdict ended.
|
|
9
9
|
*/
|
|
10
|
-
import { type AskClass, type AskOrigin, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
10
|
+
import { type AskClass, type AskOrigin, type AssertAllKeysHandled, type ModeReleaseReason, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
|
|
11
11
|
import { type AutoModeClassifierRound } from "./auto-mode.js";
|
|
12
12
|
/**
|
|
13
13
|
* How the wait an ask was in ENDED — the closed set. One word per way a wait can end; the `who` on the
|
|
@@ -303,7 +303,8 @@ export type GateDisposition = {
|
|
|
303
303
|
* A permission MODE released a surviving ask — the allowed disposition's account of the question it did not put
|
|
304
304
|
* to anyone: the mode, the disclosed class word (`askClass`, the first member of the folded set in `ASK_CLASSES`
|
|
305
305
|
* order — one of {@link RELEASABLE_ASK_CLASSES}), the whole set, and the origin / mandate words the card would
|
|
306
|
-
* have shown. `reason
|
|
306
|
+
* have shown. `reason` is a member of {@link import("./ask-origin.js").MODE_RELEASE_REASONS} (`isModeReleaseReason`):
|
|
307
|
+
* `"rule_unreadable"` marks a release over a rule the call could not be read against; absent, the release names none.
|
|
307
308
|
*/
|
|
308
309
|
export interface ModeRelease {
|
|
309
310
|
mode: PermissionMode;
|
|
@@ -311,7 +312,7 @@ export interface ModeRelease {
|
|
|
311
312
|
classes: readonly AskClass[];
|
|
312
313
|
origin?: AskOrigin;
|
|
313
314
|
mandate?: PersistedRuleMandate;
|
|
314
|
-
reason?:
|
|
315
|
+
reason?: ModeReleaseReason;
|
|
315
316
|
}
|
|
316
317
|
/** The two modes that refuse on their own account (`deniedBy: "mode"`): `dontAsk` every non-authority ask,
|
|
317
318
|
* `bypassPermissions` a write whose protection could not be judged. `plan` refuses nothing here — its questions ask. */
|
|
@@ -341,8 +342,9 @@ export interface ModeRefusal {
|
|
|
341
342
|
* {@link import("./auto-mode.js").AUTO_MODE_FALLBACK_CAUSES} — a record whose `to` and `modelUsed`
|
|
342
343
|
* disagree would let a reader answer "which model decided this call?" two ways;
|
|
343
344
|
* I7 `disposition.released` present ⇒ `kind === "allowed"`, no settlement, `mode` is `bypassPermissions`,
|
|
344
|
-
* `askClass` is a releasable class
|
|
345
|
-
*
|
|
345
|
+
* `askClass` is a releasable class, every member of `classes` is an ask class, `origin` / `reason`, when
|
|
346
|
+
* present, are members of their closed sets, and `reason` names a member of `classes` (the mint takes it from
|
|
347
|
+
* that set) — a mode releases nothing a person answered and nothing the table calls immune;
|
|
346
348
|
* I8 `deniedBy === "mode"` ⇔ `mode` present (`bypassPermissions` | `dontAsk`) with `askClass` and `classes`.
|
|
347
349
|
* Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
|
|
348
350
|
* a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAskClass, isAskOrigin, isPermissionMode, RELEASABLE_ASK_CLASSES } from "./ask-origin.js";
|
|
1
|
+
import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, RELEASABLE_ASK_CLASSES } from "./ask-origin.js";
|
|
2
2
|
import { isAutoModeFallbackCause } from "./auto-mode.js";
|
|
3
3
|
export const SETTLEMENT_KINDS = [
|
|
4
4
|
"human_allowed",
|
|
@@ -128,6 +128,10 @@ export function screenGateOutcome(g) {
|
|
|
128
128
|
defects.push("I7: released.classes must be a non-empty list of ask classes");
|
|
129
129
|
if (r.origin !== undefined && !isAskOrigin(r.origin))
|
|
130
130
|
defects.push(`I7: released.origin "${String(r.origin)}" is outside the closed set`);
|
|
131
|
+
if (r.reason !== undefined && !isModeReleaseReason(r.reason))
|
|
132
|
+
defects.push(`I7: released.reason "${String(r.reason)}" is outside the closed set`);
|
|
133
|
+
else if (r.reason !== undefined && Array.isArray(r.classes) && !r.classes.includes(r.reason))
|
|
134
|
+
defects.push(`I7: released.reason "${r.reason}" is not a member of released.classes`);
|
|
131
135
|
}
|
|
132
136
|
if (d.kind === "denied") {
|
|
133
137
|
const modeRefusal = d.deniedBy === "mode";
|