@sema-agent/core 5.23.0 → 5.25.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 +192 -1
- package/dist/core/checkpoint-store.d.ts +84 -10
- package/dist/core/checkpoint-store.js +3 -1
- package/dist/core/hooks.d.ts +18 -5
- package/dist/core/hooks.js +28 -4
- package/dist/core/memory-engine/engine.js +18 -3
- package/dist/core/permission-rule-org.d.ts +9 -0
- package/dist/core/permission-rule-org.js +12 -5
- package/dist/core/permission-rule-store.d.ts +25 -14
- package/dist/core/permission-rule-store.js +5 -1
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +40 -9
- package/dist/core/runner/runtask.js +49 -6
- package/dist/core/runner/session-file-state-replay.js +3 -0
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/tool-policy.js +17 -15
- package/dist/core/tool-result-store.d.ts +8 -0
- package/dist/core/tool-result-store.js +77 -3
- package/dist/core/types.d.ts +7 -5
- package/dist/index.d.ts +13 -7
- package/dist/index.js +2 -2
- package/dist/stores/file/adoption/adopt.d.ts +23 -3
- package/dist/stores/file/adoption/adopt.js +4 -8
- package/dist/stores/file/adoption/marker.d.ts +41 -18
- package/dist/stores/file/adoption/marker.js +14 -7
- package/dist/stores/file/permission-rule-store.d.ts +15 -1
- package/dist/stores/file/permission-rule-store.js +4 -1
- package/dist/stores/file/session-policy-store.d.ts +11 -1
- package/dist/stores/file/session-policy-store.js +8 -3
- package/dist/stores/file/task-list-store.d.ts +15 -1
- package/dist/stores/file/task-list-store.js +2 -2
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/fs/fs-shared.js +23 -7
- package/dist/tools/monitor.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,175 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.25.0 — 2026-08-10
|
|
4
|
+
|
|
5
|
+
No API-BREAKING changes (exports grow only; every new member is optional/additive). One
|
|
6
|
+
behavior-surface narrowing, called out below.
|
|
7
|
+
|
|
8
|
+
### Narrowed (behavior, ruled)
|
|
9
|
+
|
|
10
|
+
- **The persisted-rule lane gains its mandate boundary (#144, dual-source measured).** *Allow rules
|
|
11
|
+
silence the classifier's questions, never a mandated one.* A persisted allow rule used to
|
|
12
|
+
short-circuit every surviving non-governance ask — including an operator's `shellGate:"always"`
|
|
13
|
+
(per-call confirmation mandated by deployment config) and a tool's own egress/irreversibility
|
|
14
|
+
marks (the non-budgetable family). The boundary is a single-source provenance predicate: the
|
|
15
|
+
classify-doctrine bash ask (coarse tier `"maybe"`) stays the rule lane's home turf (the
|
|
16
|
+
don't-ask-again main case is deliberately preserved); doctrine-installed `"always"` and
|
|
17
|
+
tool-declared marks are not clearable by rule. When a rule MATCHES but cannot clear, the surviving
|
|
18
|
+
ask discloses it on both channels: a message note naming the rule and the mandate, and the new
|
|
19
|
+
additive `PermissionResult.ask.persistedRuleShadowed` member (the matched rule text) — a consumer
|
|
20
|
+
renders "your rule is alive, just outranked". **Consumer note**: deployments under
|
|
21
|
+
`shellGate:"always"` or with marked tools will see asks their users' rules used to clear.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`CheckpointError.detail.reason`** (closed set, additive; downstream-requested): discriminates
|
|
26
|
+
the pre-CAS refusal arms one code used to cover — `version_newer` / `env_factory_missing` /
|
|
27
|
+
`governed_unwired` on `unsupported_version` (each retryable on a differently-capable worker), and
|
|
28
|
+
`real_approval_damaged` / `real_approval_forged` / `constraint_chain_missing` on the
|
|
29
|
+
`invalid_outcome` row-integrity arms (terminal for the row's bytes). A deployment retry policy can
|
|
30
|
+
now tell "a capable worker can redeem this" from "no worker ever will".
|
|
31
|
+
- **`REAL_APPROVAL_CHECKPOINT_VERSION` exported** from the package root (joins the other five ladder
|
|
32
|
+
constants).
|
|
33
|
+
- **The I6 adoption boot gate reaches two more store faces** via an optional `dataRoot` anchor on
|
|
34
|
+
`FilePermissionRuleStoreProvider` and `createFileTaskListStore` (additive; the adoption arc's own
|
|
35
|
+
nested rule leg keeps its exemption, pinned). The freeze header states its real coverage — two
|
|
36
|
+
doors and the seams between them — instead of over-claiming.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **The disclosure channel is real end-to-end (pre-release rescan on this very batch).**
|
|
41
|
+
`persistedRuleShadowed` was write-only as first landed: `AskRequest` gains the member (all four
|
|
42
|
+
mint sites thread it), and the durable park mint carries it as `RiskDescriptor.shadowedRule`
|
|
43
|
+
(`inlineUntrusted`-capped) — the mandated population's normal route now discloses like the
|
|
44
|
+
synchronous one. The mandate predicate judges egress FIRST (the tool's own declaration is not
|
|
45
|
+
shadowed by the coarse doctrine sharing the seat). The secret-scrub quarantine capture stops
|
|
46
|
+
treating a name collision as a receipt (family suffix form; the polluted-index warning claims a
|
|
47
|
+
capture only when its write landed). The last silent timeout discard (a legal cap below the
|
|
48
|
+
resolved default) is announced; an empty env string is a written silence exception (the unset
|
|
49
|
+
idiom). The materialize-strategy announcements gain a per-value process ledger and a reset seam.
|
|
50
|
+
- **The loud-bad-value law lands on its two founding cases (#123, ruled).** `BASH_*_TIMEOUT_MS`
|
|
51
|
+
discards stop exempting the garbage/0/negative classes (every discarded value names the knob, what
|
|
52
|
+
arrived — env legs show the original string — and the value in force);
|
|
53
|
+
`SEMA_TOOL_MATERIALIZE_STRATEGY`'s refusal now matches the seat the bad value occupies: shadowed
|
|
54
|
+
by an explicit legal spec ⇒ loud discard (the documented "spec wins" precedence finally holds),
|
|
55
|
+
would-be-in-force ⇒ the closed-set refusal stands, dormant (no deferred tools) ⇒ announced once
|
|
56
|
+
per process instead of lying in wait.
|
|
57
|
+
- **A v7+ checkpoint declaring parent constraints must carry both the frozen chain and its digest**
|
|
58
|
+
(they are one write with the version stamp) — a row carrying neither is refused pre-CAS as damaged
|
|
59
|
+
instead of silently falling back to the count-only contract. Rows v6 and below keep the historic
|
|
60
|
+
contract.
|
|
61
|
+
- **The org resume belt's refusal carries the overlay's disclosure lines** (the resume path has no
|
|
62
|
+
`onUnavailable` seat) and tells a cancelled wait apart from unreadable governance. The verdict is
|
|
63
|
+
unchanged; the account is not.
|
|
64
|
+
- The sandbox-admission registry/decision domain split is documented and pinned (the two
|
|
65
|
+
un-instrumented fold families are safe for structural reasons — a family that gains an ask arm
|
|
66
|
+
reds instead of arriving unrecorded); frozen projections' fold position is disclosed; the adoption
|
|
67
|
+
report declares the memory-engine tree `action:"none"` explicitly; two engine quarantine captures
|
|
68
|
+
go `wx`-exclusive (no symlink at the final name, no overwrite of earlier evidence under a coarse
|
|
69
|
+
clock); the abort-races-timeout test pin gets real load headroom; the v8 stamp note carries its
|
|
70
|
+
erratum (v8 rows appear on ANY durable deployment via the always-mounted integrity policies — what
|
|
71
|
+
stays true: rows without the bit keep their historic stamps).
|
|
72
|
+
|
|
73
|
+
## 5.24.0 — 2026-08-10
|
|
74
|
+
|
|
75
|
+
No API-BREAKING changes (exports grow only; `suspendAsk` gains an optional fifth parameter;
|
|
76
|
+
`removePersistedRule` widens its `principal` input to a union). Behavior-surface narrowings and two
|
|
77
|
+
deliberate loosenings are called out below; checkpoint **v8** is a compatibility disclosure, not a
|
|
78
|
+
breaking change (rows without the new gate bit keep their historic stamps).
|
|
79
|
+
|
|
80
|
+
### Narrowed (behavior)
|
|
81
|
+
|
|
82
|
+
- **The real-approval bit survives the durability boundary (#130/#120).** An ask carrying
|
|
83
|
+
`requiresRealApproval` (an org ASK rule, the org-unavailable tighten, or a policy that minted the
|
|
84
|
+
bit) now parks as the NON-BUDGETABLE `irreversible_ask` gate kind — it used to park as plain
|
|
85
|
+
`human`, the one kind a network budget resolver may auto-approve. The gate carries
|
|
86
|
+
`RealApprovalGateBit` (`origin: "org_rule" | "org_unavailable" | "policy"`), the row stamps
|
|
87
|
+
`REAL_APPROVAL_CHECKPOINT_VERSION = 8` (`MAX_SUPPORTED` raised to 8 — a pre-5.24 worker rejects a
|
|
88
|
+
v8 row pre-CAS as `unsupported_version`, the row stays pending). Resume adds three guards: a
|
|
89
|
+
governed row (org origin) is refused PRE-CAS on a worker with no `permissionRuleOrg` wiring (the
|
|
90
|
+
human decision stays unspent, redeemable on an org-wired worker; a post-CAS belt remains as
|
|
91
|
+
defense in depth); a v8 row whose bit was stripped in storage, and a sub-v8 row carrying a bit no
|
|
92
|
+
release minted, are both refused pre-CAS (corruption/forgery guards).
|
|
93
|
+
- **`run_in_background: true` is judged by the backgrounding doctrine (#125, P0).** On a
|
|
94
|
+
`shellGate:"classify"` deployment, `bashReversibilityProbe` used to read only the command text, so
|
|
95
|
+
`ls` + the parameter form auto-admitted exactly what `ls &` asks for. The parameter spelling now
|
|
96
|
+
gets the textual spelling's verdict (not reversible ⇒ ask).
|
|
97
|
+
- **Oversized strings inside `details` are offloaded (#116).** `withToolResultOffload` used to
|
|
98
|
+
replace only `res.content`; a Bash call's 140k-char `details.stdout` sailed into session
|
|
99
|
+
persistence and every wire projection. Every oversized string inside `details` (deep walk, plain
|
|
100
|
+
objects/arrays, cycle-cut with a completed-transform memo, identity-preserving when untouched) is
|
|
101
|
+
now offloaded to the same store and replaced by a bounded head naming its ref; judged
|
|
102
|
+
independently of content size. Sequenced after the server tool-results read face (7.11.0) so full
|
|
103
|
+
payloads always have a host-side egress. `Write` joins `Read`'s structural offload exemption: its
|
|
104
|
+
`details.content` is the session-continuation replay's authorization source, and the replay
|
|
105
|
+
additionally refuses to seed a content string carrying the offload-replacement notice
|
|
106
|
+
(`isOffloadedDetailReplacement`, exported). An own `__proto__` key in `details` survives the
|
|
107
|
+
rebuild as data (never installed as the rebuilt object's prototype), and below head+notice size a
|
|
108
|
+
string rides untouched (no replacement that grows).
|
|
109
|
+
- **A local-owner adoption rebinds the anonymous session-policy estate (#132).** The
|
|
110
|
+
session-policy leg now runs for BOTH adoption shapes: the local-owner shape rewrites `[sid, null]`
|
|
111
|
+
rows to the adopted principal (a foreign principal's row stays), and the terminal report says
|
|
112
|
+
`row-rewrite` with the real count. On such a root the anonymous lane ALIASES to the adopted
|
|
113
|
+
principal (reads and writes), so the window between terminal adoption and the operator landing the
|
|
114
|
+
principal wiring cannot drop tighten-only deny rules; unadopted and principal-adopted roots are
|
|
115
|
+
untouched. Narrow disclosed window: a pre-5.24 in-flight local-owner marker (a stage-3 claim with
|
|
116
|
+
no session-policy evidence) reads as corrupt — recovery is an operator action (remove the root's
|
|
117
|
+
adoption.json and adopt afresh).
|
|
118
|
+
|
|
119
|
+
### Loosened (deliberate, ruled)
|
|
120
|
+
|
|
121
|
+
- **A park minted under org UNAVAILABILITY honors the approval it solicited (#131).** The gate's
|
|
122
|
+
own message promises "every allow tightens to a real approval until [the org] can [be read]" — but
|
|
123
|
+
the resume belt refused that very approval and burned it, livelocking a park-only deployment
|
|
124
|
+
(ask → park → approved → still unavailable → refused, forever). A row with
|
|
125
|
+
`origin:"org_unavailable"` now EXECUTES on an approved resume even while the org stays unreadable
|
|
126
|
+
(the durable row — origin plus the human outcome — is the audit record); `org_rule` rows and
|
|
127
|
+
pre-v8 rows keep the strict fail-closed posture, and a published org deny blocks regardless of
|
|
128
|
+
origin.
|
|
129
|
+
- **The adoption terminal record's read validator checks shape, not this build's constants
|
|
130
|
+
(#133).** The stored report snapshots the by-design-not-migrated set and the config table AS RULED
|
|
131
|
+
AT ADOPTION TIME; both may grow by ruling, and the old exact-equality read marked every
|
|
132
|
+
already-adopted root corrupt on the first release after any growth (and again on rollback), across
|
|
133
|
+
13 store constructors at once. Read time now enforces non-empty/well-formed/no-duplicate-identity;
|
|
134
|
+
the writer-emits-the-constant drift guard moves to the test grid, where a change is a deliberate,
|
|
135
|
+
reviewable edit. Empty, duplicate-carrying and mangled reports are refused exactly as before.
|
|
136
|
+
|
|
137
|
+
### Added
|
|
138
|
+
|
|
139
|
+
- **The permission-rule BACKEND CONTRACT is exported** (ruled 2026-08-10): `PERMISSION_RULE_WRITER`,
|
|
140
|
+
`writerOf`, `foldDelta`, `addDotsOf`, the delta/writer/store types and the two consent-boundary
|
|
141
|
+
assertions — an out-of-repo store twin builds against the same definitions the file backend
|
|
142
|
+
implements instead of mirroring them. The consent boundary is unchanged and now PINNED: the engine
|
|
143
|
+
reaches `writer.apply` only on the consent redemption, tighten-delete and sync-join lanes
|
|
144
|
+
(registered-caller scan + a second net over every file naming the writer handle).
|
|
145
|
+
- **`removePersistedRule` accepts a structural `RuleOwner`** — the string shorthand is unchanged;
|
|
146
|
+
`{ kind: "local-owner" }` resolves the local-owner bucket (previously unrevokable through this
|
|
147
|
+
entry), and a provider without `forLocalOwner` fails loudly instead of no-oping against the
|
|
148
|
+
zero-rule store.
|
|
149
|
+
- `RealApprovalGateBit` exported (the supervisor-inbox / SQL-twin read of the new gate member).
|
|
150
|
+
|
|
151
|
+
### Fixed
|
|
152
|
+
|
|
153
|
+
- **The background time wall is stated as a budget, never a kill threat (#121).** Under deadline
|
|
154
|
+
pressure a model read "auto-terminates if still running after 600s" as "better TaskStop it myself
|
|
155
|
+
first" (a measured 6.3s self-stop where waiting was optimal). All three Bash minting arms and both
|
|
156
|
+
Monitor siblings now say "may run up to Ns"; the kill explanation stays with the terminal
|
|
157
|
+
timed-out wording, which fires only when a timeout actually happens. Retired-dialect tripwires on
|
|
158
|
+
both faces.
|
|
159
|
+
- **`createAllowDenyPolicy`'s invalid-name refusal teaches each lesson once.** A CC-migrated
|
|
160
|
+
settings file with 39 content-form entries repeated the same three-line lesson 39 times; the
|
|
161
|
+
thrown string now groups by lesson (entries listed under it). The structured `issues` face stays
|
|
162
|
+
per-entry and gains a `lesson` member (additive).
|
|
163
|
+
- Three public comment surfaces stopped recommending prefix recomposition over `buildToolResultRef`
|
|
164
|
+
(#134 — the encoding is not injective; ownership is exact-equality against a recomposed ref), the
|
|
165
|
+
`RunnerDeps.permissionRuleStore` docstring states the ruled backend-contract boundary instead of
|
|
166
|
+
"the write face is core-private", and dated errata landed for two past releases (5.22.0: the
|
|
167
|
+
design/182 rule-sync core half shipped unlisted; 5.13.0: safety asks going sync-first under a live
|
|
168
|
+
approver shipped unlisted and superseded a written park promise).
|
|
169
|
+
- Docs: `docs/sdk/09` gains the sandbox-admission section (every exclusion conjunct, from source)
|
|
170
|
+
and an org-rule-layer account stated in terms of the mechanism that actually holds the line
|
|
171
|
+
(`requiresRealApproval` excluded at every loosening seam, now including the durability boundary).
|
|
172
|
+
|
|
3
173
|
## 5.23.0 — 2026-08-10
|
|
4
174
|
|
|
5
175
|
No BREAKING changes.
|
|
@@ -92,7 +262,10 @@ No BREAKING changes.
|
|
|
92
262
|
delegation-provenance aggregate rides the durable checkpoint; a resumed leg missing the state
|
|
93
263
|
reads as `unknown` (static floor), never as `clean`.
|
|
94
264
|
- **`buildToolResultRef` exported from the package root** — a host wiring an HTTP tool-result read
|
|
95
|
-
face
|
|
265
|
+
face composes refs through the single source instead of reimplementing the segment escaping.
|
|
266
|
+
(Erratum 2026-08-10: this entry originally said a read face binds a ref to its owning task "by
|
|
267
|
+
prefix recomputation". It must not — the encoding is not injective over the two segments, so
|
|
268
|
+
ownership is an exact-equality test against a recomposed ref, never a prefix match.)
|
|
96
269
|
- **Content mandate split from the approval mandate (#94).** A delegated child's question routes on
|
|
97
270
|
the QUESTION seat, not the approver seat. Behavior widening, called out explicitly: under
|
|
98
271
|
`durableApproval` with a live `onQuestion`, an absent or string approver seat used to leave child
|
|
@@ -103,6 +276,13 @@ No BREAKING changes.
|
|
|
103
276
|
`TaskRegistry.reviveBackgroundAgent`'s refusal union gains the `recycling` member — exhaustive
|
|
104
277
|
consumers add one arm.
|
|
105
278
|
|
|
279
|
+
_Addendum (2026-08-10, #135 erratum): the design/182 rule-sync **core half also shipped in this
|
|
280
|
+
release** and was not listed. It added the sync client and its wire contract to the public surface:
|
|
281
|
+
`syncPermissionRules` (full-state join, screened in both directions), `PERMISSION_RULE_SYNC_PATH`,
|
|
282
|
+
`RuleSyncRequestBody` / `RuleSyncResponseBody` / `PermissionRuleSyncResult`, `parseRuleSyncResponse`,
|
|
283
|
+
and `LOCAL_OWNER_UNSYNCABLE_CODE` (a local-owner bucket refuses to sync, loudly) — the definitions a
|
|
284
|
+
server-side rule store builds against. Recorded as a dated erratum rather than a silent rewrite._
|
|
285
|
+
|
|
106
286
|
## 5.21.1 — 2026-08-09
|
|
107
287
|
|
|
108
288
|
- **Fix: every `hands: none` deployment failed at the door on 5.21.0** (P0). `HAND_TOOL_EFFECTS`
|
|
@@ -1105,6 +1285,17 @@ mysterious runtime.
|
|
|
1105
1285
|
|
|
1106
1286
|
- **`finalVerification` mechanism hardening (four of the seven field failure modes).** ① The injection headroom guard read only the turn axis — it now also reads the tightest caller-armed budget axis (tokens/cost/walltime, same axes as the limit-approach frames); ≥90% full skips the injection, so the gate can no longer convert an externally-passing run into `limits.*_exceeded`. ② Both injections emit the standard `steering_injected` echo (**new closed-set member: `source: "final_verification"`** — switches over the source union add an arm), closing the stats-vs-stream observability gap. ③ The nudge licenses cleaning up residue the model's own testing created (the state-harmless clause no longer reads as "leave your test residue in place"). ④ The nudge forbids laundering pre-existing uncertainty into "confirmed". The remaining modes are design-bounded: the engine never judges check semantics, and self-grading is the deployment's verifier-role wiring.
|
|
1107
1287
|
|
|
1288
|
+
_Addendum to 5.13.0 (2026-08-10, erratum — the entry below was missing at release): **SAFETY asks go
|
|
1289
|
+
sync-first when a live approver is wired.** With a FUNCTION-valued `onAsk` present, an
|
|
1290
|
+
egress/irreversibility-tightened ask now resolves in-stream through that approver (same turn, no
|
|
1291
|
+
checkpoint) instead of always parking as `irreversible_ask`; the durable park remains the headless /
|
|
1292
|
+
no-live-approver / `forceDurableGate` / live-face-answered-unavailable path, where the gate kind,
|
|
1293
|
+
`safetyAxis` and risk descriptor are unchanged. This superseded the earlier written promise that
|
|
1294
|
+
"safety asks … still park" with a live approver (the 1.37x sync-approval entry). The change shipped
|
|
1295
|
+
in 5.13.0 with no changelog entry — recorded here as a dated erratum rather than a silent rewrite;
|
|
1296
|
+
consumer flips: a probe pinning "an `irreversibility:'always'` tool always suspends" reds whenever a
|
|
1297
|
+
live `onAsk` is wired — drop the approver (or arm `forceDurableGate`) to test the park leg._
|
|
1298
|
+
|
|
1108
1299
|
## 5.12.0 — 2026-08-05
|
|
1109
1300
|
|
|
1110
1301
|
### BREAKING
|
|
@@ -135,6 +135,11 @@ export interface RiskDescriptor {
|
|
|
135
135
|
* attempted — over-reaching a shell parse risks a wrong/forgeable path). Each path `inlineUntrusted`-capped.
|
|
136
136
|
* Omitted when none derivable. */
|
|
137
137
|
touchedPaths?: string[];
|
|
138
|
+
/** #144 (additive): a persisted allow rule MATCHED this call but could not clear the mandated ask —
|
|
139
|
+
* the matched rule text (`inlineUntrusted`-capped), threaded to the mint so the durable-park route
|
|
140
|
+
* carries the same disclosure the synchronous ask does. The inbox renders "their rule is alive,
|
|
141
|
+
* just outranked" instead of the person concluding their rule silently broke. */
|
|
142
|
+
shadowedRule?: string;
|
|
138
143
|
}
|
|
139
144
|
/**
|
|
140
145
|
* design/80 §D-E: the DETERMINISTIC severity tier (1..5) for an escalation checkpoint, a PURE function of the
|
|
@@ -266,7 +271,31 @@ export declare function buildRiskDescriptor(input: {
|
|
|
266
271
|
shellGated?: boolean;
|
|
267
272
|
/** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
|
|
268
273
|
shellGateDoctrine?: "classify" | "always";
|
|
274
|
+
/** #144: the matched-but-outranked persisted rule to persist (see {@link RiskDescriptor.shadowedRule}). */
|
|
275
|
+
shadowedRule?: string;
|
|
269
276
|
}): RiskDescriptor;
|
|
277
|
+
/**
|
|
278
|
+
* #130/#131/#120 (2026-08-10) — the durable record of an ask's `requiresRealApproval` bit, which used
|
|
279
|
+
* to DIE at the park: the mint keyed the gate kind on the static tool marks only, so an org-governed
|
|
280
|
+
* ask on an unmarked tool minted a plain `{kind:"human"}` — the one kind a network budget resolver may
|
|
281
|
+
* auto-approve — and the org's "only judgment clears this" demand was silently budgetable after the
|
|
282
|
+
* durability boundary. Present ⇒ the mint escalates to `irreversible_ask` (non-budgetable) and stamps
|
|
283
|
+
* {@link REAL_APPROVAL_CHECKPOINT_VERSION}. `origin` additionally records WHY, for the resume belts:
|
|
284
|
+
* · `"org_rule"` — an org ASK rule fired (governance was READABLE at mint). Resume keeps the strict
|
|
285
|
+
* posture: org unavailable at resume still refuses (newly-blind ⇒ fail-closed).
|
|
286
|
+
* · `"org_unavailable"` — governance could NOT be read at mint and the gate's own message promised
|
|
287
|
+
* "every allow tightens to a real approval until it can". A person approving THIS park IS that real
|
|
288
|
+
* approval — so a resume that finds org still unavailable executes instead of refusing and burning
|
|
289
|
+
* the approval (the park-only livelock #131 closed). The durable row itself — this origin plus the
|
|
290
|
+
* recorded human outcome — is the audit record of that passage; the resume emits no extra notice.
|
|
291
|
+
* · `"policy"` — a policy/hook minted the bit (e.g. the always-on classifier-parity rule); no org
|
|
292
|
+
* semantics, the resume belts treat it as a plain non-budgetable approval.
|
|
293
|
+
* Both org origins double as the #120 governed mark: a resuming worker with NO org adjudication wiring
|
|
294
|
+
* refuses to redeem such a row (the governed boundary must not vanish with a deployment's wiring).
|
|
295
|
+
*/
|
|
296
|
+
export interface RealApprovalGateBit {
|
|
297
|
+
origin: "org_rule" | "org_unavailable" | "policy";
|
|
298
|
+
}
|
|
270
299
|
export type CheckpointGate =
|
|
271
300
|
/** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
|
|
272
301
|
* carries an OPTIONAL display-only {@link RiskDescriptor} (severity/axes/summary) for the supervisor
|
|
@@ -292,6 +321,7 @@ export type CheckpointGate =
|
|
|
292
321
|
reason: string;
|
|
293
322
|
toolName: string;
|
|
294
323
|
safetyAxis?: SafetyAxis;
|
|
324
|
+
realApproval?: RealApprovalGateBit;
|
|
295
325
|
riskDescriptor?: RiskDescriptor;
|
|
296
326
|
}
|
|
297
327
|
/** design/74: a resource slice limit (budget/walltime/turns) was reached — suspend (resumable) instead of
|
|
@@ -782,8 +812,11 @@ export interface CheckpointState {
|
|
|
782
812
|
* (extra deny-narrowing layers + the pre-CAS edit re-adjudication) rather than trusting a
|
|
783
813
|
* re-supplied policy's execution half for the frozen deny data — a re-supplied closure that
|
|
784
814
|
* matches the digest but behaves differently cannot un-deny what the suspend froze. Present iff
|
|
785
|
-
* `requiresParentConstraint` on new mints; absent on
|
|
786
|
-
* the count-only contract governs, and an EDIT resume is refused for want of a validatable chain).
|
|
815
|
+
* `requiresParentConstraint` on new mints; absent on PRE-F-012 checkpoints (⇒ no frozen deny data —
|
|
816
|
+
* the count-only contract governs, and an EDIT resume is refused for want of a validatable chain).
|
|
817
|
+
* "Older" is version-decidable, not guessed: chain, digest and the {@link F012_CHECKPOINT_VERSION}
|
|
818
|
+
* stamp are one write, so on a v7+ row the pair is REQUIRED — its absence there is a damaged row and
|
|
819
|
+
* the resume ladder refuses it pre-CAS rather than falling back to the count-only contract. */
|
|
787
820
|
constraintChain?: ReadonlyArray<import("./tool-policy.js").ConstraintChainEntry>;
|
|
788
821
|
/**
|
|
789
822
|
* F-012 L1 — the content digest over `constraintChain` ({@link import("./tool-policy.js").constraintChainDigest}):
|
|
@@ -791,7 +824,8 @@ export interface CheckpointState {
|
|
|
791
824
|
* content. The resume pre-CAS ladder recomputes the digest over the RE-SUPPLIED chain's
|
|
792
825
|
* projections and rejects a mismatch (`resume.parent_constraint_mismatch`) — replacing the
|
|
793
826
|
* count-only shape check with a content contract (a same-length chain of different frozen deny
|
|
794
|
-
* data no longer passes). Present iff `constraintChain` is
|
|
827
|
+
* data no longer passes). Present iff `constraintChain` is — and on a v7+ row both are required
|
|
828
|
+
* (see {@link constraintChain}: the pair and the version stamp are minted together). */
|
|
795
829
|
constraintDigest?: string;
|
|
796
830
|
};
|
|
797
831
|
/**
|
|
@@ -909,13 +943,29 @@ export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
|
|
|
909
943
|
* state keeps the historic stamps — deployments not delegating see zero version movement.
|
|
910
944
|
*/
|
|
911
945
|
export declare const F012_CHECKPOINT_VERSION = 7;
|
|
946
|
+
/**
|
|
947
|
+
* #130/#120 (2026-08-10 — the F012_CHECKPOINT_VERSION precedent replayed a sixth time): the schema
|
|
948
|
+
* version a suspend stamps when its gate carries {@link RealApprovalGateBit}. The enforcement lives
|
|
949
|
+
* ENTIRELY in the resuming worker (the no-org-wiring refusal, the org_unavailable resume-belt
|
|
950
|
+
* distinction): a pre-131 worker (MAX_SUPPORTED=7) would accept the row, ignore `realApproval`, and
|
|
951
|
+
* either redeem a governed row with no org wiring at all (#120's exact hole, replayed through version
|
|
952
|
+
* skew) or burn the approval on the unavailable belt the bit exists to soften. Stamping v8 forces it
|
|
953
|
+
* to reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A gate
|
|
954
|
+
* with NO `realApproval` keeps the historic stamps. NOTE (erratum 2026-08-10, downstream-measured):
|
|
955
|
+
* "zero version movement for ungoverned deployments" is TOO WIDE — the two always-mounted integrity
|
|
956
|
+
* policies (transcript-integrity, unverifiable-delete) mint `requiresRealApproval` asks, so a park on
|
|
957
|
+
* one of THOSE stamps v8 with `origin:"policy"` on ANY durable deployment, org-governed or not. What
|
|
958
|
+
* stays true: rows whose gate carries no realApproval bit keep their historic stamps everywhere.
|
|
959
|
+
*/
|
|
960
|
+
export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
|
|
912
961
|
/** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
|
|
913
962
|
* {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
|
|
914
|
-
* Raised to
|
|
963
|
+
* Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
|
|
915
964
|
* (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
|
|
916
965
|
* wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
|
|
917
|
-
* state)
|
|
918
|
-
|
|
966
|
+
* state), v7 (F-012 constraint-chain / delegation-provenance enforcement state) and v8 (a
|
|
967
|
+
* non-budgetable `realApproval` gate bit with its org-origin resume semantics). */
|
|
968
|
+
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
|
|
919
969
|
/**
|
|
920
970
|
* Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
|
|
921
971
|
* written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
|
|
@@ -1428,8 +1478,8 @@ export declare class CheckpointError extends Error {
|
|
|
1428
1478
|
* computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
|
|
1429
1479
|
* checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
|
|
1430
1480
|
| "resume.constraint_unprojectable";
|
|
1431
|
-
/** Structured discrimination a caller can branch on where the code alone is ambiguous.
|
|
1432
|
-
*
|
|
1481
|
+
/** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
|
|
1482
|
+
* members. `field` names WHICH part of the decision payload was rejected on a
|
|
1433
1483
|
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
|
|
1434
1484
|
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
|
|
1435
1485
|
* action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
@@ -1441,6 +1491,18 @@ export declare class CheckpointError extends Error {
|
|
|
1441
1491
|
* discriminant. */
|
|
1442
1492
|
readonly detail?: {
|
|
1443
1493
|
field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
|
|
1494
|
+
/** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
|
|
1495
|
+
* deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
|
|
1496
|
+
* damaged/forged and no worker ever will" — blanket-retriable and gate-shape heuristics were
|
|
1497
|
+
* both refuted downstream, so the throw site carries the fact it always knew). Closed set;
|
|
1498
|
+
* additive and optional like `field` — `code` remains the only REQUIRED discriminant.
|
|
1499
|
+
* · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
|
|
1500
|
+
* `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
|
|
1501
|
+
* (retryable on an org-wired worker).
|
|
1502
|
+
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
|
|
1503
|
+
* `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
1504
|
+
* current bytes — no worker version redeems a damaged row). */
|
|
1505
|
+
reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
|
|
1444
1506
|
} | undefined;
|
|
1445
1507
|
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
1446
1508
|
/** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
|
|
@@ -1537,8 +1599,8 @@ export declare class CheckpointError extends Error {
|
|
|
1537
1599
|
* computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
|
|
1538
1600
|
* checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
|
|
1539
1601
|
| "resume.constraint_unprojectable", message: string,
|
|
1540
|
-
/** Structured discrimination a caller can branch on where the code alone is ambiguous.
|
|
1541
|
-
*
|
|
1602
|
+
/** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
|
|
1603
|
+
* members. `field` names WHICH part of the decision payload was rejected on a
|
|
1542
1604
|
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
|
|
1543
1605
|
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
|
|
1544
1606
|
* action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
@@ -1550,6 +1612,18 @@ export declare class CheckpointError extends Error {
|
|
|
1550
1612
|
* discriminant. */
|
|
1551
1613
|
detail?: {
|
|
1552
1614
|
field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
|
|
1615
|
+
/** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
|
|
1616
|
+
* deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
|
|
1617
|
+
* damaged/forged and no worker ever will" — blanket-retriable and gate-shape heuristics were
|
|
1618
|
+
* both refuted downstream, so the throw site carries the fact it always knew). Closed set;
|
|
1619
|
+
* additive and optional like `field` — `code` remains the only REQUIRED discriminant.
|
|
1620
|
+
* · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
|
|
1621
|
+
* `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
|
|
1622
|
+
* (retryable on an org-wired worker).
|
|
1623
|
+
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
|
|
1624
|
+
* `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
|
|
1625
|
+
* current bytes — no worker version redeems a damaged row). */
|
|
1626
|
+
reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "constraint_chain_missing";
|
|
1553
1627
|
} | undefined);
|
|
1554
1628
|
}
|
|
1555
1629
|
/**
|
|
@@ -120,6 +120,7 @@ export function buildRiskDescriptor(input) {
|
|
|
120
120
|
axes,
|
|
121
121
|
toolName,
|
|
122
122
|
...(input.shellGated && input.shellGateDoctrine !== undefined ? { shellGateDoctrine: input.shellGateDoctrine } : {}),
|
|
123
|
+
...(input.shadowedRule !== undefined ? { shadowedRule: inlineUntrusted(input.shadowedRule, 200) } : {}),
|
|
123
124
|
...(summary !== undefined ? { summary } : {}),
|
|
124
125
|
...(touchedPaths !== undefined ? { touchedPaths } : {}),
|
|
125
126
|
};
|
|
@@ -134,7 +135,8 @@ export const BINDING_CHECKPOINT_VERSION = 3;
|
|
|
134
135
|
export const TOKEN_CHECKPOINT_VERSION = 5;
|
|
135
136
|
export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
|
|
136
137
|
export const F012_CHECKPOINT_VERSION = 7;
|
|
137
|
-
export const
|
|
138
|
+
export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
|
|
139
|
+
export const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
|
|
138
140
|
export function checkpointVersionOf(cp) {
|
|
139
141
|
return cp.version ?? 0;
|
|
140
142
|
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -602,7 +602,16 @@ export interface ToolGateInput {
|
|
|
602
602
|
* prefer, so the closure must NOT take its sync-first decline and should park durably. All other
|
|
603
603
|
* decline/pre-commit-failure paths keep their existing `undefined` fallbacks (the gate then keeps the
|
|
604
604
|
* fail-closed deny / typed refusal the caller carries). */
|
|
605
|
-
liveFaceUnavailable?: boolean
|
|
605
|
+
liveFaceUnavailable?: boolean,
|
|
606
|
+
/** #130: present ⇒ the surviving ask carries `requiresRealApproval` and the mint must escalate to
|
|
607
|
+
* the non-budgetable `irreversible_ask` kind carrying this bit (a budget resolver must never
|
|
608
|
+
* auto-approve what only judgment may clear). `origin` records whether the bit came from an org
|
|
609
|
+
* ASK rule, from the org-unavailable tighten (whose resume semantics differ — see
|
|
610
|
+
* {@link import("./checkpoint-store.js").RealApprovalGateBit}), or from a policy/hook. */
|
|
611
|
+
realApproval?: import("./checkpoint-store.js").RealApprovalGateBit,
|
|
612
|
+
/** #144: the matched-but-outranked persisted rule (the surviving ask's `persistedRuleShadowed`)
|
|
613
|
+
* — threaded so the park mint's risk descriptor carries the disclosure on the durable route. */
|
|
614
|
+
shadowedRule?: string) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
606
615
|
/**
|
|
607
616
|
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
608
617
|
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
|
@@ -722,10 +731,14 @@ export interface ToolGateInput {
|
|
|
722
731
|
* · the reserved question tool and a call MARKED unresolvable are excluded for the same reason the
|
|
723
732
|
* classifier excludes them: both contracts require that no synchronous decision-maker stands between
|
|
724
733
|
* the ask and the park / content route, and this lane is one.
|
|
725
|
-
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
734
|
+
* · #144 (ruled): a MANDATED ask is never consumed — an operator's shellGate:"always" tier and a
|
|
735
|
+
* tool's own egress/irreversibility marks are structural requirements, not classifier hesitation
|
|
736
|
+
* ("allow rules silence the classifier's questions, never a mandated one"). When a rule matches
|
|
737
|
+
* but cannot clear, the surviving ask discloses it (message + `persistedRuleShadowed`).
|
|
738
|
+
* What it DOES consume is the classify-DOCTRINE shell ask (the coarse tier "maybe" — the
|
|
739
|
+
* don't-ask-again main case this feature exists for) and unmarked bare asks. Consuming such an ask
|
|
740
|
+
* also skips the park it would have minted; that IS what a standing approval means, and the
|
|
741
|
+
* bullets above are what keep integrity/hook/mandated asks out of that set.
|
|
729
742
|
*/
|
|
730
743
|
persistedRules?: {
|
|
731
744
|
/** The canonical text of the rule that admits this call, or `undefined`. Must not throw: a store that
|
package/dist/core/hooks.js
CHANGED
|
@@ -248,6 +248,7 @@ export async function runToolGate(input) {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
let orgRealApprovalRequired = false;
|
|
251
|
+
let orgAskOrigin;
|
|
251
252
|
let orgTightenCount = 0;
|
|
252
253
|
const applyOrgLayer = async (current, args) => {
|
|
253
254
|
if (input.orgRules === undefined)
|
|
@@ -266,6 +267,7 @@ export async function runToolGate(input) {
|
|
|
266
267
|
}, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
267
268
|
if (answer.status === "unavailable") {
|
|
268
269
|
orgRealApprovalRequired = true;
|
|
270
|
+
orgAskOrigin = "unavailable";
|
|
269
271
|
await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
|
|
270
272
|
if (decided.action === "allow") {
|
|
271
273
|
decided = {
|
|
@@ -297,6 +299,7 @@ export async function runToolGate(input) {
|
|
|
297
299
|
orgTightenCount += 1;
|
|
298
300
|
denySource = "org";
|
|
299
301
|
orgRealApprovalRequired = true;
|
|
302
|
+
orgAskOrigin = "rule";
|
|
300
303
|
return decided.action === "ask"
|
|
301
304
|
? { ...decided, requiresRealApproval: true }
|
|
302
305
|
: {
|
|
@@ -312,6 +315,15 @@ export async function runToolGate(input) {
|
|
|
312
315
|
currentInput = policyRewrite;
|
|
313
316
|
req.args = policyRewrite;
|
|
314
317
|
}
|
|
318
|
+
const persistedRuleMandate = input.egress === true
|
|
319
|
+
? "tool_marks"
|
|
320
|
+
: input.shellGated === true
|
|
321
|
+
? input.irreversibility === "always"
|
|
322
|
+
? "operator_always"
|
|
323
|
+
: undefined
|
|
324
|
+
: input.irreversibility === "always" || input.irreversibility === "maybe"
|
|
325
|
+
? "tool_marks"
|
|
326
|
+
: undefined;
|
|
315
327
|
if (input.persistedRules &&
|
|
316
328
|
!orgRealApprovalRequired &&
|
|
317
329
|
decision.action === "ask" &&
|
|
@@ -320,7 +332,7 @@ export async function runToolGate(input) {
|
|
|
320
332
|
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
321
333
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
322
334
|
const hit = await input.persistedRules.admits(req).catch(() => undefined);
|
|
323
|
-
if (hit !== undefined) {
|
|
335
|
+
if (hit !== undefined && persistedRuleMandate === undefined) {
|
|
324
336
|
decision = {
|
|
325
337
|
action: "allow",
|
|
326
338
|
message: `a persisted allow rule (${hit}) covers this call`,
|
|
@@ -329,6 +341,14 @@ export async function runToolGate(input) {
|
|
|
329
341
|
};
|
|
330
342
|
await notifier.notifyAsync(() => input.persistedRules?.onResolved?.({ toolName: req.toolName, toolCallId, rule: hit }), "toolGate.persistedRuleResolved");
|
|
331
343
|
}
|
|
344
|
+
else if (hit !== undefined) {
|
|
345
|
+
const mandateNoun = persistedRuleMandate === "operator_always" ? "this deployment mandates per-call confirmation for shell commands (shellGate: always)" : "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
|
|
346
|
+
decision = {
|
|
347
|
+
...decision,
|
|
348
|
+
persistedRuleShadowed: hit,
|
|
349
|
+
message: `${decision.message !== undefined ? `${decision.message} ` : ""}(a persisted allow rule (${hit}) matches this call but does not clear the ask — ${mandateNoun})`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
332
352
|
}
|
|
333
353
|
if (input.autoMode &&
|
|
334
354
|
!orgRealApprovalRequired &&
|
|
@@ -383,8 +403,11 @@ export async function runToolGate(input) {
|
|
|
383
403
|
const egressTool = input.egress === true;
|
|
384
404
|
const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
|
|
385
405
|
const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
|
|
406
|
+
const realApprovalOf = (d) => d.action === "ask" && d.requiresRealApproval === true
|
|
407
|
+
? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
|
|
408
|
+
: undefined;
|
|
386
409
|
if (suspendAsk && decision.action === "ask") {
|
|
387
|
-
const suspended = await suspendAsk(req, currentInput, safety);
|
|
410
|
+
const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined);
|
|
388
411
|
if (suspended) {
|
|
389
412
|
if ("parkFailed" in suspended)
|
|
390
413
|
parkFailed = suspended.parkFailed;
|
|
@@ -403,7 +426,7 @@ export async function runToolGate(input) {
|
|
|
403
426
|
req.args = outcome.presentedInput;
|
|
404
427
|
}
|
|
405
428
|
if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
|
|
406
|
-
const suspended = await suspendAsk(req, currentInput, safety, true);
|
|
429
|
+
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined);
|
|
407
430
|
if (suspended) {
|
|
408
431
|
if ("parkFailed" in suspended)
|
|
409
432
|
parkFailed = suspended.parkFailed;
|
|
@@ -427,10 +450,11 @@ export async function runToolGate(input) {
|
|
|
427
450
|
}
|
|
428
451
|
}
|
|
429
452
|
if (decision.action === "ask") {
|
|
453
|
+
const askBeforeResolve = decision;
|
|
430
454
|
const resolved = await resolveAsk(decision, req);
|
|
431
455
|
decision = resolved;
|
|
432
456
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
433
|
-
const suspended = await suspendAsk(req, currentInput, safety, true);
|
|
457
|
+
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined);
|
|
434
458
|
if (suspended) {
|
|
435
459
|
if ("parkFailed" in suspended)
|
|
436
460
|
parkFailed = suspended.parkFailed;
|
|
@@ -656,16 +656,18 @@ export class MemoryEngine {
|
|
|
656
656
|
const indexNow = readSafe(pollutedIndexPath);
|
|
657
657
|
if (indexNow === undefined || indexNow === handle.indexText)
|
|
658
658
|
return;
|
|
659
|
+
let indexCaptureLanded = false;
|
|
659
660
|
try {
|
|
660
661
|
const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-polluted-${MEMORY_INDEX_FILENAME}`);
|
|
661
662
|
ensureDirExists(dirname(dest));
|
|
662
|
-
writeFileSync(dest, indexNow, "utf8");
|
|
663
|
+
writeFileSync(dest, indexNow, { encoding: "utf8", flag: "wx" });
|
|
664
|
+
indexCaptureLanded = true;
|
|
663
665
|
}
|
|
664
666
|
catch {
|
|
665
667
|
}
|
|
666
668
|
try {
|
|
667
669
|
writeFileNoFollow(pollutedIndexPath, handle.indexText);
|
|
668
|
-
report.warnings.push(
|
|
670
|
+
report.warnings.push(`memory index restored to its pre-session state — this session's index additions were not retained (session polluted; ${indexCaptureLanded ? "the removed text was captured to quarantine" : "the quarantine capture did NOT land — the removed text is gone"})`);
|
|
669
671
|
}
|
|
670
672
|
catch (err) {
|
|
671
673
|
report.warnings.push(`memory index could NOT be restored to its pre-session state: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1225,7 +1227,20 @@ export class MemoryEngine {
|
|
|
1225
1227
|
try {
|
|
1226
1228
|
const dest = join(this.controlDir, QUARANTINE_DIR, `${this.now()}-${MEMORY_INDEX_FILENAME}`);
|
|
1227
1229
|
ensureDirExists(dirname(dest));
|
|
1228
|
-
|
|
1230
|
+
let landed = false;
|
|
1231
|
+
for (let attempt = 0; !landed && attempt < 10; attempt++) {
|
|
1232
|
+
const candidate = attempt === 0 ? dest : `${dest}.${attempt}`;
|
|
1233
|
+
try {
|
|
1234
|
+
writeFileSync(candidate, text, { encoding: "utf8", flag: "wx" });
|
|
1235
|
+
landed = true;
|
|
1236
|
+
}
|
|
1237
|
+
catch (err) {
|
|
1238
|
+
if (!(err instanceof Error && "code" in err && err.code === "EEXIST"))
|
|
1239
|
+
throw err;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (!landed)
|
|
1243
|
+
writeFileSync(`${dest}.${process.pid}`, text, { encoding: "utf8", flag: "wx" });
|
|
1229
1244
|
captured = true;
|
|
1230
1245
|
}
|
|
1231
1246
|
catch (err) {
|
|
@@ -112,6 +112,15 @@ export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
|
|
|
112
112
|
export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
|
|
113
113
|
signal?: AbortSignal;
|
|
114
114
|
timeoutMs: number;
|
|
115
|
+
/**
|
|
116
|
+
* backlog #136④ — which arm WON, reported at the moment it won. A consumer that wants to tell a
|
|
117
|
+
* cancelled wait from an unreadable provider cannot get that by sampling `signal.aborted` after
|
|
118
|
+
* the await: a cancellation queued between the settlement and the continuation reads identically
|
|
119
|
+
* to one that actually ended the wait, and the operator-facing account would name the wrong
|
|
120
|
+
* cause. Called at most once, before `p` resolves, and only for the two non-provider arms —
|
|
121
|
+
* absent call ⇒ the awaited promise itself settled.
|
|
122
|
+
*/
|
|
123
|
+
onFallback?: (cause: "aborted" | "timeout") => void;
|
|
115
124
|
}): Promise<T>;
|
|
116
125
|
/** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
|
|
117
126
|
* single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */
|