@sema-agent/core 5.23.0 → 5.24.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 +122 -1
- package/dist/core/checkpoint-store.d.ts +38 -3
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/hooks.d.ts +7 -1
- package/dist/core/hooks.js +10 -3
- 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.js +9 -6
- package/dist/core/runner/runtask.js +28 -2
- package/dist/core/runner/session-file-state-replay.js +3 -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 +1 -1
- package/dist/stores/file/adoption/adopt.js +3 -8
- package/dist/stores/file/adoption/marker.d.ts +15 -7
- package/dist/stores/file/adoption/marker.js +14 -7
- package/dist/stores/file/session-policy-store.d.ts +11 -1
- package/dist/stores/file/session-policy-store.js +8 -3
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/monitor.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.24.0 — 2026-08-10
|
|
4
|
+
|
|
5
|
+
No API-BREAKING changes (exports grow only; `suspendAsk` gains an optional fifth parameter;
|
|
6
|
+
`removePersistedRule` widens its `principal` input to a union). Behavior-surface narrowings and two
|
|
7
|
+
deliberate loosenings are called out below; checkpoint **v8** is a compatibility disclosure, not a
|
|
8
|
+
breaking change (rows without the new gate bit keep their historic stamps).
|
|
9
|
+
|
|
10
|
+
### Narrowed (behavior)
|
|
11
|
+
|
|
12
|
+
- **The real-approval bit survives the durability boundary (#130/#120).** An ask carrying
|
|
13
|
+
`requiresRealApproval` (an org ASK rule, the org-unavailable tighten, or a policy that minted the
|
|
14
|
+
bit) now parks as the NON-BUDGETABLE `irreversible_ask` gate kind — it used to park as plain
|
|
15
|
+
`human`, the one kind a network budget resolver may auto-approve. The gate carries
|
|
16
|
+
`RealApprovalGateBit` (`origin: "org_rule" | "org_unavailable" | "policy"`), the row stamps
|
|
17
|
+
`REAL_APPROVAL_CHECKPOINT_VERSION = 8` (`MAX_SUPPORTED` raised to 8 — a pre-5.24 worker rejects a
|
|
18
|
+
v8 row pre-CAS as `unsupported_version`, the row stays pending). Resume adds three guards: a
|
|
19
|
+
governed row (org origin) is refused PRE-CAS on a worker with no `permissionRuleOrg` wiring (the
|
|
20
|
+
human decision stays unspent, redeemable on an org-wired worker; a post-CAS belt remains as
|
|
21
|
+
defense in depth); a v8 row whose bit was stripped in storage, and a sub-v8 row carrying a bit no
|
|
22
|
+
release minted, are both refused pre-CAS (corruption/forgery guards).
|
|
23
|
+
- **`run_in_background: true` is judged by the backgrounding doctrine (#125, P0).** On a
|
|
24
|
+
`shellGate:"classify"` deployment, `bashReversibilityProbe` used to read only the command text, so
|
|
25
|
+
`ls` + the parameter form auto-admitted exactly what `ls &` asks for. The parameter spelling now
|
|
26
|
+
gets the textual spelling's verdict (not reversible ⇒ ask).
|
|
27
|
+
- **Oversized strings inside `details` are offloaded (#116).** `withToolResultOffload` used to
|
|
28
|
+
replace only `res.content`; a Bash call's 140k-char `details.stdout` sailed into session
|
|
29
|
+
persistence and every wire projection. Every oversized string inside `details` (deep walk, plain
|
|
30
|
+
objects/arrays, cycle-cut with a completed-transform memo, identity-preserving when untouched) is
|
|
31
|
+
now offloaded to the same store and replaced by a bounded head naming its ref; judged
|
|
32
|
+
independently of content size. Sequenced after the server tool-results read face (7.11.0) so full
|
|
33
|
+
payloads always have a host-side egress. `Write` joins `Read`'s structural offload exemption: its
|
|
34
|
+
`details.content` is the session-continuation replay's authorization source, and the replay
|
|
35
|
+
additionally refuses to seed a content string carrying the offload-replacement notice
|
|
36
|
+
(`isOffloadedDetailReplacement`, exported). An own `__proto__` key in `details` survives the
|
|
37
|
+
rebuild as data (never installed as the rebuilt object's prototype), and below head+notice size a
|
|
38
|
+
string rides untouched (no replacement that grows).
|
|
39
|
+
- **A local-owner adoption rebinds the anonymous session-policy estate (#132).** The
|
|
40
|
+
session-policy leg now runs for BOTH adoption shapes: the local-owner shape rewrites `[sid, null]`
|
|
41
|
+
rows to the adopted principal (a foreign principal's row stays), and the terminal report says
|
|
42
|
+
`row-rewrite` with the real count. On such a root the anonymous lane ALIASES to the adopted
|
|
43
|
+
principal (reads and writes), so the window between terminal adoption and the operator landing the
|
|
44
|
+
principal wiring cannot drop tighten-only deny rules; unadopted and principal-adopted roots are
|
|
45
|
+
untouched. Narrow disclosed window: a pre-5.24 in-flight local-owner marker (a stage-3 claim with
|
|
46
|
+
no session-policy evidence) reads as corrupt — recovery is an operator action (remove the root's
|
|
47
|
+
adoption.json and adopt afresh).
|
|
48
|
+
|
|
49
|
+
### Loosened (deliberate, ruled)
|
|
50
|
+
|
|
51
|
+
- **A park minted under org UNAVAILABILITY honors the approval it solicited (#131).** The gate's
|
|
52
|
+
own message promises "every allow tightens to a real approval until [the org] can [be read]" — but
|
|
53
|
+
the resume belt refused that very approval and burned it, livelocking a park-only deployment
|
|
54
|
+
(ask → park → approved → still unavailable → refused, forever). A row with
|
|
55
|
+
`origin:"org_unavailable"` now EXECUTES on an approved resume even while the org stays unreadable
|
|
56
|
+
(the durable row — origin plus the human outcome — is the audit record); `org_rule` rows and
|
|
57
|
+
pre-v8 rows keep the strict fail-closed posture, and a published org deny blocks regardless of
|
|
58
|
+
origin.
|
|
59
|
+
- **The adoption terminal record's read validator checks shape, not this build's constants
|
|
60
|
+
(#133).** The stored report snapshots the by-design-not-migrated set and the config table AS RULED
|
|
61
|
+
AT ADOPTION TIME; both may grow by ruling, and the old exact-equality read marked every
|
|
62
|
+
already-adopted root corrupt on the first release after any growth (and again on rollback), across
|
|
63
|
+
13 store constructors at once. Read time now enforces non-empty/well-formed/no-duplicate-identity;
|
|
64
|
+
the writer-emits-the-constant drift guard moves to the test grid, where a change is a deliberate,
|
|
65
|
+
reviewable edit. Empty, duplicate-carrying and mangled reports are refused exactly as before.
|
|
66
|
+
|
|
67
|
+
### Added
|
|
68
|
+
|
|
69
|
+
- **The permission-rule BACKEND CONTRACT is exported** (ruled 2026-08-10): `PERMISSION_RULE_WRITER`,
|
|
70
|
+
`writerOf`, `foldDelta`, `addDotsOf`, the delta/writer/store types and the two consent-boundary
|
|
71
|
+
assertions — an out-of-repo store twin builds against the same definitions the file backend
|
|
72
|
+
implements instead of mirroring them. The consent boundary is unchanged and now PINNED: the engine
|
|
73
|
+
reaches `writer.apply` only on the consent redemption, tighten-delete and sync-join lanes
|
|
74
|
+
(registered-caller scan + a second net over every file naming the writer handle).
|
|
75
|
+
- **`removePersistedRule` accepts a structural `RuleOwner`** — the string shorthand is unchanged;
|
|
76
|
+
`{ kind: "local-owner" }` resolves the local-owner bucket (previously unrevokable through this
|
|
77
|
+
entry), and a provider without `forLocalOwner` fails loudly instead of no-oping against the
|
|
78
|
+
zero-rule store.
|
|
79
|
+
- `RealApprovalGateBit` exported (the supervisor-inbox / SQL-twin read of the new gate member).
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **The background time wall is stated as a budget, never a kill threat (#121).** Under deadline
|
|
84
|
+
pressure a model read "auto-terminates if still running after 600s" as "better TaskStop it myself
|
|
85
|
+
first" (a measured 6.3s self-stop where waiting was optimal). All three Bash minting arms and both
|
|
86
|
+
Monitor siblings now say "may run up to Ns"; the kill explanation stays with the terminal
|
|
87
|
+
timed-out wording, which fires only when a timeout actually happens. Retired-dialect tripwires on
|
|
88
|
+
both faces.
|
|
89
|
+
- **`createAllowDenyPolicy`'s invalid-name refusal teaches each lesson once.** A CC-migrated
|
|
90
|
+
settings file with 39 content-form entries repeated the same three-line lesson 39 times; the
|
|
91
|
+
thrown string now groups by lesson (entries listed under it). The structured `issues` face stays
|
|
92
|
+
per-entry and gains a `lesson` member (additive).
|
|
93
|
+
- Three public comment surfaces stopped recommending prefix recomposition over `buildToolResultRef`
|
|
94
|
+
(#134 — the encoding is not injective; ownership is exact-equality against a recomposed ref), the
|
|
95
|
+
`RunnerDeps.permissionRuleStore` docstring states the ruled backend-contract boundary instead of
|
|
96
|
+
"the write face is core-private", and dated errata landed for two past releases (5.22.0: the
|
|
97
|
+
design/182 rule-sync core half shipped unlisted; 5.13.0: safety asks going sync-first under a live
|
|
98
|
+
approver shipped unlisted and superseded a written park promise).
|
|
99
|
+
- Docs: `docs/sdk/09` gains the sandbox-admission section (every exclusion conjunct, from source)
|
|
100
|
+
and an org-rule-layer account stated in terms of the mechanism that actually holds the line
|
|
101
|
+
(`requiresRealApproval` excluded at every loosening seam, now including the durability boundary).
|
|
102
|
+
|
|
3
103
|
## 5.23.0 — 2026-08-10
|
|
4
104
|
|
|
5
105
|
No BREAKING changes.
|
|
@@ -92,7 +192,10 @@ No BREAKING changes.
|
|
|
92
192
|
delegation-provenance aggregate rides the durable checkpoint; a resumed leg missing the state
|
|
93
193
|
reads as `unknown` (static floor), never as `clean`.
|
|
94
194
|
- **`buildToolResultRef` exported from the package root** — a host wiring an HTTP tool-result read
|
|
95
|
-
face
|
|
195
|
+
face composes refs through the single source instead of reimplementing the segment escaping.
|
|
196
|
+
(Erratum 2026-08-10: this entry originally said a read face binds a ref to its owning task "by
|
|
197
|
+
prefix recomputation". It must not — the encoding is not injective over the two segments, so
|
|
198
|
+
ownership is an exact-equality test against a recomposed ref, never a prefix match.)
|
|
96
199
|
- **Content mandate split from the approval mandate (#94).** A delegated child's question routes on
|
|
97
200
|
the QUESTION seat, not the approver seat. Behavior widening, called out explicitly: under
|
|
98
201
|
`durableApproval` with a live `onQuestion`, an absent or string approver seat used to leave child
|
|
@@ -103,6 +206,13 @@ No BREAKING changes.
|
|
|
103
206
|
`TaskRegistry.reviveBackgroundAgent`'s refusal union gains the `recycling` member — exhaustive
|
|
104
207
|
consumers add one arm.
|
|
105
208
|
|
|
209
|
+
_Addendum (2026-08-10, #135 erratum): the design/182 rule-sync **core half also shipped in this
|
|
210
|
+
release** and was not listed. It added the sync client and its wire contract to the public surface:
|
|
211
|
+
`syncPermissionRules` (full-state join, screened in both directions), `PERMISSION_RULE_SYNC_PATH`,
|
|
212
|
+
`RuleSyncRequestBody` / `RuleSyncResponseBody` / `PermissionRuleSyncResult`, `parseRuleSyncResponse`,
|
|
213
|
+
and `LOCAL_OWNER_UNSYNCABLE_CODE` (a local-owner bucket refuses to sync, loudly) — the definitions a
|
|
214
|
+
server-side rule store builds against. Recorded as a dated erratum rather than a silent rewrite._
|
|
215
|
+
|
|
106
216
|
## 5.21.1 — 2026-08-09
|
|
107
217
|
|
|
108
218
|
- **Fix: every `hands: none` deployment failed at the door on 5.21.0** (P0). `HAND_TOOL_EFFECTS`
|
|
@@ -1105,6 +1215,17 @@ mysterious runtime.
|
|
|
1105
1215
|
|
|
1106
1216
|
- **`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
1217
|
|
|
1218
|
+
_Addendum to 5.13.0 (2026-08-10, erratum — the entry below was missing at release): **SAFETY asks go
|
|
1219
|
+
sync-first when a live approver is wired.** With a FUNCTION-valued `onAsk` present, an
|
|
1220
|
+
egress/irreversibility-tightened ask now resolves in-stream through that approver (same turn, no
|
|
1221
|
+
checkpoint) instead of always parking as `irreversible_ask`; the durable park remains the headless /
|
|
1222
|
+
no-live-approver / `forceDurableGate` / live-face-answered-unavailable path, where the gate kind,
|
|
1223
|
+
`safetyAxis` and risk descriptor are unchanged. This superseded the earlier written promise that
|
|
1224
|
+
"safety asks … still park" with a live approver (the 1.37x sync-approval entry). The change shipped
|
|
1225
|
+
in 5.13.0 with no changelog entry — recorded here as a dated erratum rather than a silent rewrite;
|
|
1226
|
+
consumer flips: a probe pinning "an `irreversibility:'always'` tool always suspends" reds whenever a
|
|
1227
|
+
live `onAsk` is wired — drop the approver (or arm `forceDurableGate`) to test the park leg._
|
|
1228
|
+
|
|
1108
1229
|
## 5.12.0 — 2026-08-05
|
|
1109
1230
|
|
|
1110
1231
|
### BREAKING
|
|
@@ -267,6 +267,28 @@ export declare function buildRiskDescriptor(input: {
|
|
|
267
267
|
/** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
|
|
268
268
|
shellGateDoctrine?: "classify" | "always";
|
|
269
269
|
}): RiskDescriptor;
|
|
270
|
+
/**
|
|
271
|
+
* #130/#131/#120 (2026-08-10) — the durable record of an ask's `requiresRealApproval` bit, which used
|
|
272
|
+
* to DIE at the park: the mint keyed the gate kind on the static tool marks only, so an org-governed
|
|
273
|
+
* ask on an unmarked tool minted a plain `{kind:"human"}` — the one kind a network budget resolver may
|
|
274
|
+
* auto-approve — and the org's "only judgment clears this" demand was silently budgetable after the
|
|
275
|
+
* durability boundary. Present ⇒ the mint escalates to `irreversible_ask` (non-budgetable) and stamps
|
|
276
|
+
* {@link REAL_APPROVAL_CHECKPOINT_VERSION}. `origin` additionally records WHY, for the resume belts:
|
|
277
|
+
* · `"org_rule"` — an org ASK rule fired (governance was READABLE at mint). Resume keeps the strict
|
|
278
|
+
* posture: org unavailable at resume still refuses (newly-blind ⇒ fail-closed).
|
|
279
|
+
* · `"org_unavailable"` — governance could NOT be read at mint and the gate's own message promised
|
|
280
|
+
* "every allow tightens to a real approval until it can". A person approving THIS park IS that real
|
|
281
|
+
* approval — so a resume that finds org still unavailable executes instead of refusing and burning
|
|
282
|
+
* the approval (the park-only livelock #131 closed). The durable row itself — this origin plus the
|
|
283
|
+
* recorded human outcome — is the audit record of that passage; the resume emits no extra notice.
|
|
284
|
+
* · `"policy"` — a policy/hook minted the bit (e.g. the always-on classifier-parity rule); no org
|
|
285
|
+
* semantics, the resume belts treat it as a plain non-budgetable approval.
|
|
286
|
+
* Both org origins double as the #120 governed mark: a resuming worker with NO org adjudication wiring
|
|
287
|
+
* refuses to redeem such a row (the governed boundary must not vanish with a deployment's wiring).
|
|
288
|
+
*/
|
|
289
|
+
export interface RealApprovalGateBit {
|
|
290
|
+
origin: "org_rule" | "org_unavailable" | "policy";
|
|
291
|
+
}
|
|
270
292
|
export type CheckpointGate =
|
|
271
293
|
/** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
|
|
272
294
|
* carries an OPTIONAL display-only {@link RiskDescriptor} (severity/axes/summary) for the supervisor
|
|
@@ -292,6 +314,7 @@ export type CheckpointGate =
|
|
|
292
314
|
reason: string;
|
|
293
315
|
toolName: string;
|
|
294
316
|
safetyAxis?: SafetyAxis;
|
|
317
|
+
realApproval?: RealApprovalGateBit;
|
|
295
318
|
riskDescriptor?: RiskDescriptor;
|
|
296
319
|
}
|
|
297
320
|
/** design/74: a resource slice limit (budget/walltime/turns) was reached — suspend (resumable) instead of
|
|
@@ -909,13 +932,25 @@ export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
|
|
|
909
932
|
* state keeps the historic stamps — deployments not delegating see zero version movement.
|
|
910
933
|
*/
|
|
911
934
|
export declare const F012_CHECKPOINT_VERSION = 7;
|
|
935
|
+
/**
|
|
936
|
+
* #130/#120 (2026-08-10 — the F012_CHECKPOINT_VERSION precedent replayed a sixth time): the schema
|
|
937
|
+
* version a suspend stamps when its gate carries {@link RealApprovalGateBit}. The enforcement lives
|
|
938
|
+
* ENTIRELY in the resuming worker (the no-org-wiring refusal, the org_unavailable resume-belt
|
|
939
|
+
* distinction): a pre-131 worker (MAX_SUPPORTED=7) would accept the row, ignore `realApproval`, and
|
|
940
|
+
* either redeem a governed row with no org wiring at all (#120's exact hole, replayed through version
|
|
941
|
+
* skew) or burn the approval on the unavailable belt the bit exists to soften. Stamping v8 forces it
|
|
942
|
+
* to reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A gate
|
|
943
|
+
* with NO `realApproval` keeps the historic stamps — ungoverned deployments see zero version movement.
|
|
944
|
+
*/
|
|
945
|
+
export declare const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
|
|
912
946
|
/** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
|
|
913
947
|
* {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
|
|
914
|
-
* Raised to
|
|
948
|
+
* Raised to 8 for realApproval-bearing gates — this worker reads v1 (legacy human), v2 (resource), v3
|
|
915
949
|
* (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the retired
|
|
916
950
|
* wall-clock allocation), v5 (token-allocation-bearing resource/approval), v6 (org-admission freeze
|
|
917
|
-
* state)
|
|
918
|
-
|
|
951
|
+
* state), v7 (F-012 constraint-chain / delegation-provenance enforcement state) and v8 (a
|
|
952
|
+
* non-budgetable `realApproval` gate bit with its org-origin resume semantics). */
|
|
953
|
+
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
|
|
919
954
|
/**
|
|
920
955
|
* Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
|
|
921
956
|
* written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
|
|
@@ -134,7 +134,8 @@ export const BINDING_CHECKPOINT_VERSION = 3;
|
|
|
134
134
|
export const TOKEN_CHECKPOINT_VERSION = 5;
|
|
135
135
|
export const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
|
|
136
136
|
export const F012_CHECKPOINT_VERSION = 7;
|
|
137
|
-
export const
|
|
137
|
+
export const REAL_APPROVAL_CHECKPOINT_VERSION = 8;
|
|
138
|
+
export const MAX_SUPPORTED_CHECKPOINT_VERSION = 8;
|
|
138
139
|
export function checkpointVersionOf(cp) {
|
|
139
140
|
return cp.version ?? 0;
|
|
140
141
|
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -602,7 +602,13 @@ 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) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
|
|
606
612
|
/**
|
|
607
613
|
* design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
|
|
608
614
|
* before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
|
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
|
: {
|
|
@@ -383,8 +386,11 @@ export async function runToolGate(input) {
|
|
|
383
386
|
const egressTool = input.egress === true;
|
|
384
387
|
const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
|
|
385
388
|
const safety = egressTool || irreversibleTool ? { egress: egressTool, irreversible: irreversibleTool } : undefined;
|
|
389
|
+
const realApprovalOf = (d) => d.action === "ask" && d.requiresRealApproval === true
|
|
390
|
+
? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
|
|
391
|
+
: undefined;
|
|
386
392
|
if (suspendAsk && decision.action === "ask") {
|
|
387
|
-
const suspended = await suspendAsk(req, currentInput, safety);
|
|
393
|
+
const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision));
|
|
388
394
|
if (suspended) {
|
|
389
395
|
if ("parkFailed" in suspended)
|
|
390
396
|
parkFailed = suspended.parkFailed;
|
|
@@ -403,7 +409,7 @@ export async function runToolGate(input) {
|
|
|
403
409
|
req.args = outcome.presentedInput;
|
|
404
410
|
}
|
|
405
411
|
if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
|
|
406
|
-
const suspended = await suspendAsk(req, currentInput, safety, true);
|
|
412
|
+
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision));
|
|
407
413
|
if (suspended) {
|
|
408
414
|
if ("parkFailed" in suspended)
|
|
409
415
|
parkFailed = suspended.parkFailed;
|
|
@@ -427,10 +433,11 @@ export async function runToolGate(input) {
|
|
|
427
433
|
}
|
|
428
434
|
}
|
|
429
435
|
if (decision.action === "ask") {
|
|
436
|
+
const askBeforeResolve = decision;
|
|
430
437
|
const resolved = await resolveAsk(decision, req);
|
|
431
438
|
decision = resolved;
|
|
432
439
|
if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
|
|
433
|
-
const suspended = await suspendAsk(req, currentInput, safety, true);
|
|
440
|
+
const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve));
|
|
434
441
|
if (suspended) {
|
|
435
442
|
if ("parkFailed" in suspended)
|
|
436
443
|
parkFailed = suspended.parkFailed;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* design/179 §8 — the persisted allow-rule store seam, its
|
|
2
|
+
* design/179 §8 — the persisted allow-rule store seam, its backend write face, and the removal entry.
|
|
3
3
|
*
|
|
4
4
|
* ## Two faces, deliberately unequal
|
|
5
5
|
*
|
|
6
|
-
* A deployment sees a READ face: `list()`, scoped to one verified principal by a factory.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A deployment sees a READ face: `list()`, scoped to one verified principal by a factory. The write face
|
|
7
|
+
* is a BACKEND CONTRACT (ruled 2026-08-10: exported so an out-of-repo store twin builds against the same
|
|
8
|
+
* definitions instead of mirroring them), not a host write API — the only way a rule enters the store
|
|
9
|
+
* through the ENGINE is the redemption path (`permission-rule-consent.ts`), which requires an approved
|
|
10
|
+
* durable approval record; that invariant lives in the engine's wiring and is pinned by the
|
|
11
|
+
* writer-caller registry test, not in type visibility.
|
|
9
12
|
* `expectedRev` is concurrency control, not authorization, so "hold a put API and skip the ticket" is not
|
|
10
13
|
* a shape that exists here rather than a rule someone must remember.
|
|
11
14
|
*
|
|
@@ -156,15 +159,19 @@ export interface RuleSyncJoinDelta {
|
|
|
156
159
|
* What a write may say. Authorization discriminates on the DELTA SHAPE, not on a full snapshot: only the
|
|
157
160
|
* add arm can introduce a dot, and the delete arm carries a tombstone and no adds. A backend additionally
|
|
158
161
|
* REFUSES at runtime any delete that would introduce a new add dot — structure and runtime check together,
|
|
159
|
-
* so "pick the delete arm and smuggle an add" is neither expressible nor accepted. The `sync-join` arm
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* not at the
|
|
162
|
+
* so "pick the delete arm and smuggle an add" is neither expressible nor accepted. The `sync-join` arm's
|
|
163
|
+
* safety does not rest on hiding the type (ruled 2026-08-10: the backend contract IS exported for
|
|
164
|
+
* out-of-repo store twins) — it rests on every inbound record inside it passing the single validator
|
|
165
|
+
* again AT THE BACKEND: the fifth door of design/179 §4's validator list closes here, not at the
|
|
166
|
+
* calling layer, and closes identically for every caller.
|
|
163
167
|
*/
|
|
164
168
|
export type RuleWriteDelta = RuleAddDelta | RuleDeleteDelta | RuleSyncJoinDelta;
|
|
165
169
|
/**
|
|
166
|
-
* The
|
|
167
|
-
*
|
|
170
|
+
* The backend write face. Exported as part of the BACKEND CONTRACT (ruled 2026-08-10) so an
|
|
171
|
+
* out-of-repo store implementation hangs the same face the file backend does, instead of mirroring the
|
|
172
|
+
* types. The consent boundary is unchanged by the export: the ENGINE reaches a writer only through the
|
|
173
|
+
* consent protocol's redemption (and the sync client's join) — a property of the engine's wiring —
|
|
174
|
+
* and a deployment always owned its own storage bytes, so type visibility grants nothing new.
|
|
168
175
|
*/
|
|
169
176
|
export interface PermissionRuleWriter {
|
|
170
177
|
/** Mint the next dot for this replica. Counters need only be unique and monotonic, so a dot minted for
|
|
@@ -198,11 +205,11 @@ export interface RawRuleSyncState {
|
|
|
198
205
|
quarantined?: QuarantinedRuleAdd[];
|
|
199
206
|
}
|
|
200
207
|
/**
|
|
201
|
-
* The
|
|
202
|
-
*
|
|
208
|
+
* The handle a writable backend hangs its write face on (exported with the backend contract, ruled 2026-08-10;
|
|
209
|
+
* the consent boundary lives in the engine's wiring, not in this key's visibility).
|
|
203
210
|
*/
|
|
204
211
|
export declare const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter";
|
|
205
|
-
/** A store that also carries the
|
|
212
|
+
/** A store that also carries the backend write face. */
|
|
206
213
|
export interface WritablePermissionRuleStore extends PermissionRuleStore {
|
|
207
214
|
readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
|
|
208
215
|
}
|
|
@@ -419,7 +426,11 @@ export type RemoveResult =
|
|
|
419
426
|
export declare function removePersistedRule(opts: {
|
|
420
427
|
rule: string;
|
|
421
428
|
scope: RuleScope;
|
|
422
|
-
principal
|
|
429
|
+
/** Whose bucket. A bare string stays the principal shorthand (unchanged callers); the structural
|
|
430
|
+
* {@link RuleOwner} form adds the local-owner bucket (downstream request, 2026-08-10 — design/182 §4.5's
|
|
431
|
+
* `forLocalOwner()` face existed, but removal could not name it, so a local-owner rule was
|
|
432
|
+
* unrevokable through this entry). Same observed-remove/add-wins/stillLive semantics either way. */
|
|
433
|
+
principal: string | RuleOwner;
|
|
423
434
|
provider: PermissionRuleStoreProvider;
|
|
424
435
|
}): Promise<RemoveResult>;
|
|
425
436
|
export declare function errText(err: unknown): string;
|
|
@@ -327,7 +327,11 @@ export async function ruleStoreChecksum(payload) {
|
|
|
327
327
|
}
|
|
328
328
|
const REMOVE_MAX_ATTEMPTS = 8;
|
|
329
329
|
export async function removePersistedRule(opts) {
|
|
330
|
-
const
|
|
330
|
+
const owner = typeof opts.principal === "string" ? { kind: "principal", principal: opts.principal } : opts.principal;
|
|
331
|
+
if (owner.kind === "local-owner" && opts.provider.forLocalOwner === undefined) {
|
|
332
|
+
return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it" };
|
|
333
|
+
}
|
|
334
|
+
const store = owner.kind === "local-owner" ? opts.provider.forLocalOwner() : opts.provider.forPrincipal(owner.principal);
|
|
331
335
|
const writer = writerOf(store);
|
|
332
336
|
if (writer === undefined) {
|
|
333
337
|
return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be removed through it" };
|
|
@@ -85,7 +85,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
|
|
|
85
85
|
import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
|
|
86
86
|
import { resolveKey } from "../../tools/fs/safety.js";
|
|
87
87
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
88
|
-
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
88
|
+
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
89
89
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
90
90
|
import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
91
91
|
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
@@ -4236,7 +4236,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4236
4236
|
}
|
|
4237
4237
|
};
|
|
4238
4238
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4239
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable) => {
|
|
4239
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval) => {
|
|
4240
4240
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4241
4241
|
if (syncFirstEligible &&
|
|
4242
4242
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4345,12 +4345,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4345
4345
|
...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
|
|
4346
4346
|
});
|
|
4347
4347
|
gate =
|
|
4348
|
-
safety !== undefined
|
|
4348
|
+
safety !== undefined || realApproval !== undefined
|
|
4349
4349
|
? {
|
|
4350
4350
|
kind: "irreversible_ask",
|
|
4351
|
-
reason:
|
|
4351
|
+
reason: safety !== undefined
|
|
4352
|
+
? `human approval required before safety-tightened tool "${req.toolName}"`
|
|
4353
|
+
: `real human approval required for tool "${req.toolName}" (non-budgetable: ${realApproval.origin})`,
|
|
4352
4354
|
toolName: req.toolName,
|
|
4353
|
-
safetyAxis: safety,
|
|
4355
|
+
...(safety !== undefined ? { safetyAxis: safety } : {}),
|
|
4356
|
+
...(realApproval !== undefined ? { realApproval } : {}),
|
|
4354
4357
|
riskDescriptor,
|
|
4355
4358
|
}
|
|
4356
4359
|
: durableApproval
|
|
@@ -4373,7 +4376,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4373
4376
|
const mintedAt = Date.now();
|
|
4374
4377
|
cp = {
|
|
4375
4378
|
token,
|
|
4376
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4379
|
+
version: realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4377
4380
|
scope,
|
|
4378
4381
|
sessionId,
|
|
4379
4382
|
leafId,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
2
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
3
|
-
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
3
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
4
4
|
import { engineVersion } from "../version.js";
|
|
5
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -3704,6 +3704,21 @@ export class Runner {
|
|
|
3704
3704
|
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3705
3705
|
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`);
|
|
3706
3706
|
}
|
|
3707
|
+
const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
|
|
3708
|
+
const preCasBitWellFormed = preCasGateBit !== undefined &&
|
|
3709
|
+
typeof preCasGateBit === "object" &&
|
|
3710
|
+
(preCasGateBit.origin === "org_rule" ||
|
|
3711
|
+
preCasGateBit.origin === "org_unavailable" ||
|
|
3712
|
+
preCasGateBit.origin === "policy");
|
|
3713
|
+
if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
|
|
3714
|
+
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3715
|
+
? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3716
|
+
: `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`);
|
|
3717
|
+
}
|
|
3718
|
+
if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
|
|
3719
|
+
this.deps.permissionRuleOrg === undefined) {
|
|
3720
|
+
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`);
|
|
3721
|
+
}
|
|
3707
3722
|
const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
|
|
3708
3723
|
if (retiredWalltimeTotal !== undefined) {
|
|
3709
3724
|
throw new CheckpointError("checkpoint.walltime_axis_retired", "this checkpoint's ledger carries a cross-slice WALL-CLOCK allocation (resourceLedger.totalWalltimeSec), an axis this engine version retired — " +
|
|
@@ -4009,13 +4024,24 @@ export class Runner {
|
|
|
4009
4024
|
return;
|
|
4010
4025
|
}
|
|
4011
4026
|
}
|
|
4027
|
+
const gateRealApproval = resume.cp.gate.kind === "irreversible_ask" ? resume.cp.gate.realApproval : undefined;
|
|
4028
|
+
const gateOrgGoverned = gateRealApproval?.origin === "org_rule" || gateRealApproval?.origin === "org_unavailable";
|
|
4029
|
+
if (gateOrgGoverned && prepared.permissionRuleOrg === undefined) {
|
|
4030
|
+
const unwiredDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its approval was minted under organization governance (${gateRealApproval.origin}), and this worker has no org adjudication wiring — a governed approval may only be redeemed where governance can be enforced. This approval is spent; re-issue the call on an org-wired worker.`);
|
|
4031
|
+
emitEnd(true, { content: unwiredDenial });
|
|
4032
|
+
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, unwiredDenial, true));
|
|
4033
|
+
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
4034
|
+
return;
|
|
4035
|
+
}
|
|
4012
4036
|
if (prepared.permissionRuleOrg !== undefined) {
|
|
4013
4037
|
const orgVerdict = prepared.permissionRuleOrg
|
|
4014
4038
|
.adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
|
|
4015
4039
|
.catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
|
|
4016
4040
|
const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
4017
4041
|
const blocked = org.status === "unavailable"
|
|
4018
|
-
?
|
|
4042
|
+
? gateRealApproval?.origin === "org_unavailable"
|
|
4043
|
+
? undefined
|
|
4044
|
+
: "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
|
|
4019
4045
|
: org.verdict?.behavior === "deny"
|
|
4020
4046
|
? `an organization policy rule (${org.verdict.rule}) denies it`
|
|
4021
4047
|
: undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isAbsolutePathForm } from "../../tools/fs/safety.js";
|
|
2
|
+
import { isOffloadedDetailReplacement } from "../tool-result-store.js";
|
|
2
3
|
const READ_TOOL = "Read";
|
|
3
4
|
const WRITE_TOOL = "Write";
|
|
4
5
|
const RETRACTING_RESULTS = [
|
|
@@ -50,6 +51,8 @@ export function wholeFileRecordsFromTranscript(messages) {
|
|
|
50
51
|
const content = isRead ? wholeFileFromReadCard(rest) : typeof rest.content === "string" ? rest.content : undefined;
|
|
51
52
|
if (content === undefined)
|
|
52
53
|
continue;
|
|
54
|
+
if (isOffloadedDetailReplacement(content))
|
|
55
|
+
continue;
|
|
53
56
|
byPath.set(filePath, { path: filePath, content, at: m.timestamp });
|
|
54
57
|
}
|
|
55
58
|
return [...byPath.values()];
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -122,12 +122,9 @@ export function createAllowDenyPolicy(opts) {
|
|
|
122
122
|
if (entry.startsWith("mcp__")) {
|
|
123
123
|
const segments = entry.slice("mcp__".length).split("__");
|
|
124
124
|
if (segments.some((seg) => seg.length === 0)) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
message: `"${entry}" is a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
129
|
-
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`,
|
|
130
|
-
});
|
|
125
|
+
const lesson = `a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
126
|
+
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`;
|
|
127
|
+
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
131
128
|
continue;
|
|
132
129
|
}
|
|
133
130
|
}
|
|
@@ -135,14 +132,11 @@ export function createAllowDenyPolicy(opts) {
|
|
|
135
132
|
kept.push(entry);
|
|
136
133
|
continue;
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
`and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
|
|
144
|
-
`createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`,
|
|
145
|
-
});
|
|
135
|
+
const lesson = `a rule CONTENT form, not a tool name — a name set matches raw tool names, so this entry ` +
|
|
136
|
+
`can never match any mounted tool (in an allow list it removes the tool entirely). Use the tool NAME here, ` +
|
|
137
|
+
`and route the narrowing through the lane that speaks this grammar: parameter rules via ` +
|
|
138
|
+
`createPermissionRulePolicy, Bash command prefixes via the persisted allow-rule lane.`;
|
|
139
|
+
invalid.push({ entry, list, message: `"${entry}" is ${lesson}`, lesson });
|
|
146
140
|
}
|
|
147
141
|
return kept;
|
|
148
142
|
};
|
|
@@ -150,8 +144,16 @@ export function createAllowDenyPolicy(opts) {
|
|
|
150
144
|
const screenedDeny = screen(opts.deny, "deny");
|
|
151
145
|
if (invalid.length > 0) {
|
|
152
146
|
if ((opts.onInvalidName ?? "throw") === "throw") {
|
|
147
|
+
const byLesson = new Map();
|
|
148
|
+
for (const i of invalid) {
|
|
149
|
+
const group = byLesson.get(i.lesson) ?? [];
|
|
150
|
+
group.push({ entry: i.entry, list: i.list });
|
|
151
|
+
byLesson.set(i.lesson, group);
|
|
152
|
+
}
|
|
153
153
|
const e = new Error(`createAllowDenyPolicy: ${invalid.length} entr${invalid.length === 1 ? "y is" : "ies are"} not tool name(s):\n` +
|
|
154
|
-
|
|
154
|
+
[...byLesson.entries()]
|
|
155
|
+
.map(([lesson, group]) => ` Each of the following is ${lesson}\n` + group.map((g) => ` [${g.list}] "${g.entry}"`).join("\n"))
|
|
156
|
+
.join("\n"));
|
|
155
157
|
e.code = "config.invalid_tool_name_set";
|
|
156
158
|
e.issues = invalid;
|
|
157
159
|
throw e;
|
|
@@ -240,5 +240,13 @@ export declare function withToolResultOffload(tool: AgentTool, store: ToolResult
|
|
|
240
240
|
* ⇒ byte-identical historic preview. An accessor (not a snapshot) because the wrap happens at
|
|
241
241
|
* prepare time, before the deferred classification exists and before any activation can. */
|
|
242
242
|
reachableTools?: () => ReadonlySet<string> | undefined): AgentTool;
|
|
243
|
+
/** The machine-recognizable start of an offloaded-detail replacement notice. Exported for consumers
|
|
244
|
+
* that treat a `details` string as SEMANTIC INPUT (not display) — e.g. the session-continuation
|
|
245
|
+
* read-state replay — so they can tell "this member was offloaded, the bytes here are a preview"
|
|
246
|
+
* and degrade honestly instead of consuming preview bytes as the real value. */
|
|
247
|
+
export declare const OFFLOADED_DETAIL_NOTICE_PREFIX = "\u2026[offloaded \u2014 ";
|
|
248
|
+
/** True when `s` carries an offloaded-detail replacement notice (the newline-anchored prefix form the
|
|
249
|
+
* walk emits). A semantic consumer treats such a string as NOT the member's real bytes. */
|
|
250
|
+
export declare function isOffloadedDetailReplacement(s: string): boolean;
|
|
243
251
|
/** The injected `read_tool_result` tool: pages through an offloaded result. `effect:"read"` (verifier/reconcile-safe). */
|
|
244
252
|
export declare function createReadToolResultTool(store: ToolResultStore): AgentTool;
|
|
@@ -121,6 +121,8 @@ export function firstPartyOffloadPolicy(toolName) {
|
|
|
121
121
|
switch (toolName) {
|
|
122
122
|
case "Read":
|
|
123
123
|
return { offload: false };
|
|
124
|
+
case "Write":
|
|
125
|
+
return { offload: false };
|
|
124
126
|
case "Bash":
|
|
125
127
|
return { offloadThresholdChars: 30_000 };
|
|
126
128
|
case "Grep":
|
|
@@ -164,21 +166,93 @@ export function withToolResultOffload(tool, store, thresholdChars, sessionId, re
|
|
|
164
166
|
return tool;
|
|
165
167
|
const wrappedExecute = async (toolCallId, params, signal, onUpdate) => {
|
|
166
168
|
const res = await tool.execute(toolCallId, params, signal, onUpdate);
|
|
169
|
+
const offloadedDetails = res.details === undefined ? undefined : await offloadOversizedDetailStrings(res.details, store, thresholdChars, sessionId, toolCallId);
|
|
170
|
+
const withDetails = (r) => offloadedDetails === undefined || offloadedDetails.value === res.details ? r : { ...r, details: offloadedDetails.value };
|
|
167
171
|
if (totalTextChars(res.content) <= thresholdChars)
|
|
168
|
-
return res;
|
|
172
|
+
return withDetails(res);
|
|
169
173
|
const full = res.content
|
|
170
174
|
.filter((b) => b.type === "text")
|
|
171
175
|
.map((b) => b.text)
|
|
172
176
|
.join("\n");
|
|
173
177
|
if (full.length <= PREVIEW_HEAD_CHARS + PREVIEW_TAIL_CHARS)
|
|
174
|
-
return res;
|
|
178
|
+
return withDetails(res);
|
|
175
179
|
const ref = buildToolResultRef(sessionId, toolCallId);
|
|
176
180
|
await store.put(ref, full);
|
|
177
181
|
const images = res.content.filter((b) => b.type !== "text");
|
|
178
|
-
return { ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] };
|
|
182
|
+
return withDetails({ ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] });
|
|
179
183
|
};
|
|
180
184
|
return { ...tool, execute: wrappedExecute };
|
|
181
185
|
}
|
|
186
|
+
const OFFLOADED_DETAIL_HEAD_CHARS = 2_000;
|
|
187
|
+
const OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS = 400;
|
|
188
|
+
export const OFFLOADED_DETAIL_NOTICE_PREFIX = "…[offloaded — ";
|
|
189
|
+
export function isOffloadedDetailReplacement(s) {
|
|
190
|
+
return s.includes(`\n${OFFLOADED_DETAIL_NOTICE_PREFIX}`);
|
|
191
|
+
}
|
|
192
|
+
async function offloadOversizedDetailStrings(details, store, thresholdChars, sessionId, toolCallId) {
|
|
193
|
+
const puts = [];
|
|
194
|
+
const onStack = new Set();
|
|
195
|
+
const isPlainObject = (v) => {
|
|
196
|
+
if (typeof v !== "object" || v === null)
|
|
197
|
+
return false;
|
|
198
|
+
const p = Object.getPrototypeOf(v);
|
|
199
|
+
return p === Object.prototype || p === null;
|
|
200
|
+
};
|
|
201
|
+
const replace = (full, path) => {
|
|
202
|
+
const detailRef = buildToolResultRef(sessionId, toolCallId + " " + path);
|
|
203
|
+
puts.push(Promise.resolve(store.put(detailRef, full)));
|
|
204
|
+
return (`${full.slice(0, OFFLOADED_DETAIL_HEAD_CHARS)}\n` +
|
|
205
|
+
`${OFFLOADED_DETAIL_NOTICE_PREFIX}${full.length} chars total; ref "${detailRef}"; the remainder is retained in this run's tool-result store and reads back through the deployment's tool-results face]`);
|
|
206
|
+
};
|
|
207
|
+
const memo = new Map();
|
|
208
|
+
const walk = (v, segs) => {
|
|
209
|
+
if (typeof v === "string")
|
|
210
|
+
return v.length > thresholdChars && v.length > OFFLOADED_DETAIL_HEAD_CHARS + OFFLOADED_DETAIL_NOTICE_ALLOWANCE_CHARS ? replace(v, JSON.stringify(segs)) : v;
|
|
211
|
+
if (Array.isArray(v)) {
|
|
212
|
+
if (onStack.has(v))
|
|
213
|
+
return v;
|
|
214
|
+
const done = memo.get(v);
|
|
215
|
+
if (done !== undefined)
|
|
216
|
+
return done;
|
|
217
|
+
onStack.add(v);
|
|
218
|
+
let changed = false;
|
|
219
|
+
const next = v.map((item, i) => {
|
|
220
|
+
const w = walk(item, [...segs, i]);
|
|
221
|
+
if (w !== item)
|
|
222
|
+
changed = true;
|
|
223
|
+
return w;
|
|
224
|
+
});
|
|
225
|
+
onStack.delete(v);
|
|
226
|
+
const result = changed ? next : v;
|
|
227
|
+
memo.set(v, result);
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
if (isPlainObject(v)) {
|
|
231
|
+
if (onStack.has(v))
|
|
232
|
+
return v;
|
|
233
|
+
const done = memo.get(v);
|
|
234
|
+
if (done !== undefined)
|
|
235
|
+
return done;
|
|
236
|
+
onStack.add(v);
|
|
237
|
+
let changed = false;
|
|
238
|
+
const next = {};
|
|
239
|
+
for (const [k, item] of Object.entries(v)) {
|
|
240
|
+
const w = walk(item, [...segs, k]);
|
|
241
|
+
if (w !== item)
|
|
242
|
+
changed = true;
|
|
243
|
+
Object.defineProperty(next, k, { value: w, enumerable: true, writable: true, configurable: true });
|
|
244
|
+
}
|
|
245
|
+
onStack.delete(v);
|
|
246
|
+
const result = changed ? next : v;
|
|
247
|
+
memo.set(v, result);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
return v;
|
|
251
|
+
};
|
|
252
|
+
const value = walk(details, []);
|
|
253
|
+
await Promise.all(puts);
|
|
254
|
+
return { value };
|
|
255
|
+
}
|
|
182
256
|
export function createReadToolResultTool(store) {
|
|
183
257
|
return defineTool({
|
|
184
258
|
name: OFFLOAD_TOOL_NAME,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4188,11 +4188,13 @@ export interface RunnerDeps {
|
|
|
4188
4188
|
* they confirmed once is not asked about again.
|
|
4189
4189
|
*
|
|
4190
4190
|
* This is the one seam in this file that LOOSENS, and it is shaped so it can only do so within limits
|
|
4191
|
-
* the engine holds. The provider hands out a READ face anchored to one verified principal
|
|
4192
|
-
* face is
|
|
4193
|
-
*
|
|
4194
|
-
*
|
|
4195
|
-
*
|
|
4191
|
+
* the engine holds. The provider hands out a READ face anchored to one verified principal. The write
|
|
4192
|
+
* face is an exported BACKEND CONTRACT (ruled 2026-08-10: an out-of-repo store twin builds against the
|
|
4193
|
+
* same definitions instead of mirroring them), but the ENGINE reaches it only on the consent lanes —
|
|
4194
|
+
* redemption of a confirmed human decision, the tighten-delete, and the sync join — a wiring invariant
|
|
4195
|
+
* pinned by a registered-caller scan (test/permission-rule-writer-callers); no exported convenience
|
|
4196
|
+
* mints a rule around consent. Rules are consumed post-fold, in the gate's ask branch, and never
|
|
4197
|
+
* resolve an ask carrying `requiresRealApproval` or one a PreToolUse hook raised.
|
|
4196
4198
|
*
|
|
4197
4199
|
* Omitted ⇒ the lane does not exist: no rules are read, no field is added to any ask, and the decision
|
|
4198
4200
|
* path is byte-identical to a build without it. An unauthenticated task (no `principal`) resolves to
|
package/dist/index.d.ts
CHANGED
|
@@ -88,7 +88,7 @@ export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW,
|
|
|
88
88
|
export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
|
|
89
89
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
90
90
|
export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, assertSafeToolResultRef, buildToolResultRef, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
|
|
91
|
-
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
|
|
91
|
+
export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingTokens, winnerFromOutcome, validatePendingSteer, readPendingSteerQueue, appendPendingSteer, MAX_PENDING_STEER_CHARS, MAX_PENDING_STEER_ENTRIES, PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES, PENDING_STEER_FROZEN_FIELDS, ACTOR_ASSERTION_FROZEN_FIELDS, MAX_ACTOR_FIELD_CHARS, MAX_STEER_INPUT_ID_CHARS, LEGACY_PENDING_STEER_INPUT_ID, type ActorAssertion, type PendingSteerEntry, type PendingSteerInput, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type RealApprovalGateBit, type ResourceLedger, type ResourceLimitReason, type PlatformLimitReason, } from "./core/checkpoint-store.js";
|
|
92
92
|
export { InMemoryUsageWindowStore, GLOBAL_USAGE_KEY, EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord, usageRetryAfterMs, resolveUsageWindows, type UsageWindow, type UsageWindowStore, type UsageWindowReading, type UsageWindowRecord, type UsageSlot, type UsageBucketRow, } from "./core/usage-window-store.js";
|
|
93
93
|
export { FileUsageWindowStore } from "./stores/file/usage-window-store.js";
|
|
94
94
|
export { ENV_LIFETIME_SUSPEND_MARGIN_MS, USAGE_WINDOW_REAP_MARGIN_MS } from "./core/runner/prepare-task.js";
|
|
@@ -136,14 +136,20 @@ export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRul
|
|
|
136
136
|
/**
|
|
137
137
|
* design/179 — persisted ALLOW rules: the standing form of approvals a person already gave.
|
|
138
138
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* and
|
|
139
|
+
* The consent boundary, restated precisely (ruled 2026-08-10): the ENGINE's only write
|
|
140
|
+
* path into a rule store is the consent protocol below (a durable approval record, an authenticated
|
|
141
|
+
* confirmation, a principal-bound redemption). That fact is a property of the engine's wiring, not of
|
|
142
|
+
* what this file exports — a deployment always owned its own storage bytes. The BACKEND CONTRACT
|
|
143
|
+
* (writer types, the writer handle key, and the fold pure functions) is therefore exported further
|
|
144
|
+
* down, so an out-of-repo store implementation (the server's SQL twin) is built against the same
|
|
145
|
+
* definitions as the file backend instead of mirroring them — mirrored types are how comment/contract
|
|
146
|
+
* drift starts. What stays deliberately absent is any convenience that mints rules AROUND consent:
|
|
147
|
+
* exporting the contract gives a store implementer the same powers it already had over its own bytes,
|
|
148
|
+
* and nothing more. Removal is exported without ceremony, because narrowing on a user's behalf is
|
|
149
|
+
* allowed and widening is not.
|
|
144
150
|
*/
|
|
145
151
|
export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedAllowRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleSuggestion, type RuleReject, type RuleRejectCode, type ParsedAllowRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
|
|
146
|
-
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, } from "./core/permission-rule-store.js";
|
|
152
|
+
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, type RuleSyncState, type RuleSyncFrontier, type RuleSyncDrop, type RuleSyncLandingReport, type RuleOwner, type QuarantinedRuleAdd, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, type PermissionRuleWriter, type WritablePermissionRuleStore, type RuleWriteDelta, type RuleAddDelta, type RuleDeleteDelta, type RuleSyncJoinDelta, type RawRuleSyncState, type RedemptionAuthorization, } from "./core/permission-rule-store.js";
|
|
147
153
|
export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, type PermissionRuleSyncTransport, type PermissionRuleSyncResult, type RuleSyncRequestBody, type RuleSyncResponseBody, } from "./core/permission-rule-sync.js";
|
|
148
154
|
export { createOrgRuleOverlay, orgRuleVerdictFor, effectivePermissionRules, orgRuleStatePersistenceOf, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, type OrgPermissionRule, type OrgRuleSnapshot, type OrgRuleSnapshotProvider, type OrgRuleStatePersistence, type PersistedOrgRuleState, type OrgRuleOverlay, type OrgOverlayResolution, type OrgOverlayStatus, type EffectivePermissionRule, } from "./core/permission-rule-org.js";
|
|
149
155
|
export { RULE_SYNC_DROP_CODES, type RuleSyncDropReason, type RuleQuarantineReason } from "./core/governance-codes.js";
|
package/dist/index.js
CHANGED
|
@@ -112,7 +112,7 @@ export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_M
|
|
|
112
112
|
export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
|
|
113
113
|
export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, } from "./core/permission-rules.js";
|
|
114
114
|
export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, } from "./core/permission-rule-model.js";
|
|
115
|
-
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, } from "./core/permission-rule-store.js";
|
|
115
|
+
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, joinRuleStates, screenRuleSyncState, collectBelowFrontier, ruleSyncVector, joinFrontiers, dotAtOrBelowFrontier, sameRuleOwner, PERMISSION_RULE_WRITER, writerOf, foldDelta, addDotsOf, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, } from "./core/permission-rule-store.js";
|
|
116
116
|
export { syncPermissionRules, parseRuleSyncResponse, PERMISSION_RULE_SYNC_PATH, LOCAL_OWNER_UNSYNCABLE_CODE, } from "./core/permission-rule-sync.js";
|
|
117
117
|
export { createOrgRuleOverlay, orgRuleVerdictFor, effectivePermissionRules, orgRuleStatePersistenceOf, ORG_UNAVAILABLE_DECISION_REASON, ORG_RULE_DECISION_REASON, ORG_ADJUDICATION_TIMEOUT_MS, } from "./core/permission-rule-org.js";
|
|
118
118
|
export { RULE_SYNC_DROP_CODES } from "./core/governance-codes.js";
|
|
@@ -500,8 +500,8 @@ export async function adoptLocalDataRoot(opts) {
|
|
|
500
500
|
};
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
|
-
if (
|
|
504
|
-
const rows = rewriteSessionPolicyRows(opts.root, opts.from.principal, opts.toPrincipal);
|
|
503
|
+
if (legBits[SESSION_POLICY_LEG]?.done !== true) {
|
|
504
|
+
const rows = rewriteSessionPolicyRows(opts.root, opts.from.kind === "principal" ? opts.from.principal : null, opts.toPrincipal);
|
|
505
505
|
legBits = { ...legBits, [SESSION_POLICY_LEG]: { done: true, rows } };
|
|
506
506
|
publishMarker(opts.root, adoptionId, opts.from, opts.toPrincipal, 2, plan, legBits);
|
|
507
507
|
}
|
|
@@ -557,12 +557,7 @@ export async function adoptLocalDataRoot(opts) {
|
|
|
557
557
|
}
|
|
558
558
|
if (plan.rulesDir !== undefined)
|
|
559
559
|
legs.push({ store: "permission-rule", action: "bucket-rebind" });
|
|
560
|
-
|
|
561
|
-
legs.push({ store: SESSION_POLICY_LEG, action: "row-rewrite", rows: legBits[SESSION_POLICY_LEG]?.rows ?? 0 });
|
|
562
|
-
}
|
|
563
|
-
else {
|
|
564
|
-
legs.push({ store: SESSION_POLICY_LEG, action: "none" });
|
|
565
|
-
}
|
|
560
|
+
legs.push({ store: SESSION_POLICY_LEG, action: "row-rewrite", rows: legBits[SESSION_POLICY_LEG]?.rows ?? 0 });
|
|
566
561
|
const carried = Object.entries(legBits)
|
|
567
562
|
.filter(([k]) => k.startsWith(CARRIAGE_BIT_PREFIX))
|
|
568
563
|
.map(([k, bit]) => ({
|
|
@@ -153,18 +153,26 @@ export type RootAdoptionFile = {
|
|
|
153
153
|
export declare const ROOT_ADOPTION_FILE = "adoption.json";
|
|
154
154
|
/**
|
|
155
155
|
* design/183 §6 (r4) — the CLOSED SET of by-design-not-migrated assets, verbatim from the design's
|
|
156
|
-
* written list.
|
|
157
|
-
*
|
|
158
|
-
*
|
|
156
|
+
* written list. The WRITER emits exactly this list and the drift pin (§10.5 ① — the design-vs-code
|
|
157
|
+
* two-cell guard) lives in the TEST GRID, where a ruled growth of the set is a deliberate edit.
|
|
158
|
+
*
|
|
159
|
+
* #133: the terminal READ validator deliberately does NOT compare a stored report against this
|
|
160
|
+
* constant. A persisted report is a snapshot of the set AS RULED AT ADOPTION TIME; the set is
|
|
161
|
+
* allowed to grow by ruling (§10.5 ① names that road), so an exact-equality read check would mark
|
|
162
|
+
* every already-adopted root corrupt on the release after any growth (and again on rollback) — 13
|
|
163
|
+
* construction sites all refusing to serve. The read validator checks SHAPE and integrity
|
|
164
|
+
* (non-empty, well-formed entries, no duplicate assets — what an empty/mangled account rebuild
|
|
165
|
+
* actually needs), never version-crossed content equality.
|
|
159
166
|
*/
|
|
160
167
|
export declare const NOT_MIGRATED_BY_DESIGN: ReadonlyArray<{
|
|
161
168
|
asset: string;
|
|
162
169
|
ruling: string;
|
|
163
170
|
}>;
|
|
164
|
-
/** The five baseline obligation IDENTITIES (design/183 §6 minimum face).
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
171
|
+
/** The five baseline obligation IDENTITIES (design/183 §6 minimum face). Since #133 the terminal READ
|
|
172
|
+
* validator deliberately does NOT compare a stored report against this table (the stored report is a
|
|
173
|
+
* snapshot of the set as ruled at adoption time; see the validator's own note) — the enforcement that
|
|
174
|
+
* the WRITER emits exactly this set lives in the test grid (test/file-adoption-root.test.ts pins the
|
|
175
|
+
* fresh report's identities against this constant), where a drift is a deliberate, reviewable edit. */
|
|
168
176
|
export declare const BASELINE_CONFIG_IDENTITIES: ReadonlyArray<{
|
|
169
177
|
deployment: string;
|
|
170
178
|
key: string;
|
|
@@ -74,7 +74,7 @@ function isRootAdoptionFileShape(v) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
const legs = (m.legs ?? {});
|
|
77
|
-
if (m.phase >= 3 &&
|
|
77
|
+
if (m.phase >= 3 && legs["session-policy"]?.done !== true)
|
|
78
78
|
return false;
|
|
79
79
|
if (m.phase === 5) {
|
|
80
80
|
for (const store of p.carriage) {
|
|
@@ -123,14 +123,21 @@ function isRootAdoptionFileShape(v) {
|
|
|
123
123
|
return false;
|
|
124
124
|
configIds.add(id);
|
|
125
125
|
return true;
|
|
126
|
-
})
|
|
127
|
-
BASELINE_CONFIG_IDENTITIES.every((b) => configIds.has(JSON.stringify([b.deployment, b.key])));
|
|
126
|
+
});
|
|
128
127
|
const nmd = r.notMigratedByDesign;
|
|
128
|
+
const nmdAssets = new Set();
|
|
129
129
|
const closedSetOk = Array.isArray(nmd) &&
|
|
130
|
-
nmd.length
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
nmd.length > 0 &&
|
|
131
|
+
nmd.every((e) => {
|
|
132
|
+
if (e === null || typeof e !== "object")
|
|
133
|
+
return false;
|
|
134
|
+
const m = e;
|
|
135
|
+
if (typeof m.asset !== "string" || m.asset === "" || typeof m.ruling !== "string" || m.ruling === "")
|
|
136
|
+
return false;
|
|
137
|
+
if (nmdAssets.has(m.asset))
|
|
138
|
+
return false;
|
|
139
|
+
nmdAssets.add(m.asset);
|
|
140
|
+
return true;
|
|
134
141
|
});
|
|
135
142
|
const legsOk = Array.isArray(r.legs) &&
|
|
136
143
|
r.legs.every((l) => {
|
|
@@ -31,6 +31,15 @@ export interface FileSessionPolicyStoreOptions {
|
|
|
31
31
|
export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
32
32
|
private readonly dir;
|
|
33
33
|
private readonly onCorruptRead;
|
|
34
|
+
/** #132 — the ADOPTED identity of a local-owner root's anonymous lane, or undefined on an
|
|
35
|
+
* unadopted (or principal-adopted) root. A local-owner adoption rebinds the anonymous
|
|
36
|
+
* `[sid,null]` estate rows to `toPrincipal`; until the deployment's principal wiring lands
|
|
37
|
+
* (`REQUIRE_PRINCIPAL` et al. — recorded `migrated:false` in the config account, an operator
|
|
38
|
+
* action), the runtime still queries anonymously, and ENOENT there would silently drop the very
|
|
39
|
+
* tighten-only deny rules the rebind moved. Adoption is a transfer of the whole anonymous
|
|
40
|
+
* identity, so on such a root the anonymous key IS the adopted principal — reads and writes
|
|
41
|
+
* alike (a window-era anonymous write must not mint a fresh orphan row). */
|
|
42
|
+
private readonly anonymousAlias;
|
|
34
43
|
constructor(root: string, opts?: FileSessionPolicyStoreOptions);
|
|
35
44
|
/** The one delivery point for {@link onCorruptRead}. Swallow-guarded here so no caller has to remember. */
|
|
36
45
|
private disclose;
|
|
@@ -41,7 +50,8 @@ export declare class FileSessionPolicyStore implements SessionPolicyStore {
|
|
|
41
50
|
* (swallow-guarded; never on a plain ENOENT, which really is absence). */
|
|
42
51
|
private discloseCorrupt;
|
|
43
52
|
/** `(sessionId, principal)` → a safe, INJECTIVE filename (sanitizeScope appends the full sha256 of the raw
|
|
44
|
-
* composite key, so distinct keys never collide on disk).
|
|
53
|
+
* composite key, so distinct keys never collide on disk). On a local-owner-adopted root the
|
|
54
|
+
* anonymous lane resolves to the adopted principal's key (#132 — see {@link anonymousAlias}). */
|
|
45
55
|
private pathFor;
|
|
46
56
|
private read;
|
|
47
57
|
getRules(sessionId: string, principal?: string): Promise<StoredSessionRules | null>;
|
|
@@ -2,15 +2,19 @@ import { readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { loosenReasons, normalizeRules, stripRev, SessionPolicyError, } from "../../core/session-policy-store.js";
|
|
4
4
|
import { atomicWriteFile, ensureDir, sanitizeScope } from "./fs-atomic.js";
|
|
5
|
-
import { assertAdoptionBootGate } from "./adoption/marker.js";
|
|
5
|
+
import { assertAdoptionBootGate, readRootAdoptionFile } from "./adoption/marker.js";
|
|
6
6
|
export class FileSessionPolicyStore {
|
|
7
7
|
dir;
|
|
8
8
|
onCorruptRead;
|
|
9
|
+
anonymousAlias;
|
|
9
10
|
constructor(root, opts) {
|
|
10
11
|
assertAdoptionBootGate(root, "FileSessionPolicyStore");
|
|
11
12
|
this.dir = join(root, "session-policy");
|
|
12
13
|
ensureDir(this.dir);
|
|
13
14
|
this.onCorruptRead = opts?.onCorruptRead;
|
|
15
|
+
const adoption = readRootAdoptionFile(root);
|
|
16
|
+
this.anonymousAlias =
|
|
17
|
+
adoption !== undefined && "adopted" in adoption && adoption.adopted.from.kind === "local-owner" ? adoption.adopted.toPrincipal : undefined;
|
|
14
18
|
}
|
|
15
19
|
disclose(info) {
|
|
16
20
|
try {
|
|
@@ -23,7 +27,8 @@ export class FileSessionPolicyStore {
|
|
|
23
27
|
this.disclose({ sessionId, ...(principal !== undefined ? { principal } : {}), path: this.pathFor(sessionId, principal), reason });
|
|
24
28
|
}
|
|
25
29
|
pathFor(sessionId, principal) {
|
|
26
|
-
const
|
|
30
|
+
const effective = principal ?? this.anonymousAlias;
|
|
31
|
+
const composite = JSON.stringify([sessionId, effective ?? null]);
|
|
27
32
|
return join(this.dir, `${sanitizeScope(composite)}.json`);
|
|
28
33
|
}
|
|
29
34
|
read(sessionId, principal) {
|
|
@@ -78,7 +83,7 @@ export class FileSessionPolicyStore {
|
|
|
78
83
|
}
|
|
79
84
|
}
|
|
80
85
|
const next = { ...clean, rev: (prior?.rev ?? 0) + 1 };
|
|
81
|
-
atomicWriteFile(join(this.dir, "tmp"), this.pathFor(sessionId, principal), JSON.stringify({ ...next, __sid: sessionId, __principal: principal ?? null }));
|
|
86
|
+
atomicWriteFile(join(this.dir, "tmp"), this.pathFor(sessionId, principal), JSON.stringify({ ...next, __sid: sessionId, __principal: (principal ?? this.anonymousAlias) ?? null }));
|
|
82
87
|
return next;
|
|
83
88
|
}
|
|
84
89
|
async listBySession(sessionId) {
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -14,9 +14,12 @@ import { BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadon
|
|
|
14
14
|
export function bashReversibilityProbe(allow, boundary) {
|
|
15
15
|
const allowSet = new Set(allow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
16
16
|
return (args) => {
|
|
17
|
-
const
|
|
17
|
+
const a = args;
|
|
18
|
+
const command = a?.command;
|
|
18
19
|
if (typeof command !== "string")
|
|
19
20
|
return { reversible: false };
|
|
21
|
+
if (a?.run_in_background === true)
|
|
22
|
+
return { reversible: false };
|
|
20
23
|
const resolved = typeof boundary === "function" ? boundary() : boundary;
|
|
21
24
|
if (classifyCompoundReadonly(command, allowSet, resolved) === undefined)
|
|
22
25
|
return { reversible: true };
|
|
@@ -555,8 +558,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
555
558
|
const bgCappedByMax = appliedTimeoutSec !== undefined && requestedTimeoutSec !== undefined && typeof bgCaps.maxBgTimeoutSec === "number" && requestedTimeoutSec > bgCaps.maxBgTimeoutSec;
|
|
556
559
|
const budgetNote = appliedTimeoutSec !== undefined
|
|
557
560
|
? bgCappedByMax
|
|
558
|
-
? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) —
|
|
559
|
-
: ` Time budget:
|
|
561
|
+
? ` Time budget: requested ${requestedTimeoutSec}s, capped at ${bgCaps.maxBgTimeoutSec}s (env ceiling: requests above ${bgCaps.maxBgTimeoutSec}s are reduced to it) — the process may run up to ${appliedTimeoutSec}s.`
|
|
562
|
+
: ` Time budget: the process may run up to ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).`
|
|
560
563
|
: "";
|
|
561
564
|
const lifetimeNote = backgroundLifetimeNote(bgCaps, bgSessionScoped, bgEnvIsolatedOwned);
|
|
562
565
|
const registry = taskOpts.taskRegistry;
|
|
@@ -655,7 +658,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
655
658
|
const tail = stdoutSoFar.slice(-1_000);
|
|
656
659
|
const adoptCaps = detachEnv.backgroundCapabilities;
|
|
657
660
|
const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
|
|
658
|
-
? ` Time budget:
|
|
661
|
+
? ` Time budget: the process may run up to ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
|
|
659
662
|
: "";
|
|
660
663
|
return {
|
|
661
664
|
content: (cause === "timeout"
|
package/dist/tools/monitor.js
CHANGED
|
@@ -121,7 +121,7 @@ export function createMonitorTool(env, opts) {
|
|
|
121
121
|
const envWallMs = bgTimeout !== undefined ? bgTimeout * 1000 : undefined;
|
|
122
122
|
const envWallWins = !isPersistent && envWallMs !== undefined && envWallMs < timeoutMs;
|
|
123
123
|
const persistentBudget = bgTimeout !== undefined
|
|
124
|
-
? `the execution environment
|
|
124
|
+
? `the execution environment's background time budget ends the watch after ${bgTimeout}s`
|
|
125
125
|
: `the execution environment's background time budget still applies`;
|
|
126
126
|
const retainedByEnv = env.backgroundCapabilities.retainBackgroundProcesses === true;
|
|
127
127
|
const retainedBySpec = opts.retainBackgroundProcesses === true;
|
|
@@ -142,8 +142,8 @@ export function createMonitorTool(env, opts) {
|
|
|
142
142
|
const lifetime = isPersistent
|
|
143
143
|
? `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but ${persistentBudget}${persistentAnchor}`
|
|
144
144
|
: envWallWins
|
|
145
|
-
? `
|
|
146
|
-
: `
|
|
145
|
+
? `The watch may run up to ${envWallMs}ms — the execution environment's background time budget (${bgTimeout}s) is shorter than the ${timeoutMs}ms watch timeout${clampNote}, and the shorter of the two wins.`
|
|
146
|
+
: `The watch may run up to ${timeoutMs}ms${clampNote}.`;
|
|
147
147
|
const content = onNotify !== undefined
|
|
148
148
|
? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
|
|
149
149
|
`~200ms are batched); you'll get a final notification with the exit code when it ends — do not poll. ` +
|