@sema-agent/core 5.22.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +169 -1
  2. package/dist/agents/subagent.js +3 -2
  3. package/dist/core/checkpoint-store.d.ts +38 -3
  4. package/dist/core/checkpoint-store.js +2 -1
  5. package/dist/core/governance-codes.js +3 -0
  6. package/dist/core/hooks.d.ts +69 -2
  7. package/dist/core/hooks.js +100 -15
  8. package/dist/core/memory-engine/engine.d.ts +28 -1
  9. package/dist/core/memory-engine/engine.js +62 -3
  10. package/dist/core/memory-engine/index.d.ts +1 -1
  11. package/dist/core/memory-engine/index.js +1 -1
  12. package/dist/core/memory-engine/layout.d.ts +69 -3
  13. package/dist/core/memory-engine/layout.js +75 -6
  14. package/dist/core/permission-rule-consent.js +2 -1
  15. package/dist/core/permission-rule-org.d.ts +36 -2
  16. package/dist/core/permission-rule-org.js +23 -0
  17. package/dist/core/permission-rule-store.d.ts +25 -14
  18. package/dist/core/permission-rule-store.js +7 -2
  19. package/dist/core/permission-rule-sync.d.ts +8 -0
  20. package/dist/core/permission-rule-sync.js +35 -6
  21. package/dist/core/runner/prepare-task.d.ts +10 -2
  22. package/dist/core/runner/prepare-task.js +120 -11
  23. package/dist/core/runner/runtask.js +46 -1
  24. package/dist/core/runner/session-file-state-replay.js +3 -0
  25. package/dist/core/tool-policy.d.ts +37 -4
  26. package/dist/core/tool-policy.js +49 -19
  27. package/dist/core/tool-result-store.d.ts +17 -1
  28. package/dist/core/tool-result-store.js +79 -4
  29. package/dist/core/trace.d.ts +47 -0
  30. package/dist/core/types.d.ts +45 -5
  31. package/dist/core/wiring-manifest.d.ts +16 -1
  32. package/dist/core/wiring-manifest.js +7 -1
  33. package/dist/index.d.ts +18 -10
  34. package/dist/index.js +5 -3
  35. package/dist/orchestration/goal.d.ts +10 -0
  36. package/dist/orchestration/goal.js +6 -5
  37. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  38. package/dist/stores/file/adoption/adopt.js +611 -0
  39. package/dist/stores/file/adoption/marker.d.ts +202 -0
  40. package/dist/stores/file/adoption/marker.js +205 -0
  41. package/dist/stores/file/background-agent-store.js +2 -0
  42. package/dist/stores/file/checkpoint-store.js +2 -0
  43. package/dist/stores/file/file-snapshot-store.js +2 -0
  44. package/dist/stores/file/index.d.ts +2 -0
  45. package/dist/stores/file/index.js +4 -0
  46. package/dist/stores/file/mailbox-store.js +2 -0
  47. package/dist/stores/file/memory-store.js +2 -0
  48. package/dist/stores/file/session-policy-store.d.ts +11 -1
  49. package/dist/stores/file/session-policy-store.js +9 -2
  50. package/dist/stores/file/session-store.js +2 -0
  51. package/dist/stores/file/task-list-store.js +2 -0
  52. package/dist/stores/file/tool-result-store.js +2 -0
  53. package/dist/stores/file/usage-window-store.js +2 -0
  54. package/dist/stores/file/workflow-journal-store.js +2 -0
  55. package/dist/stores/file/workflow-run-store.js +2 -0
  56. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  57. package/dist/tools/fs/fs-bash.js +7 -4
  58. package/dist/tools/monitor.js +3 -3
  59. package/package.json +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,152 @@
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
+
103
+ ## 5.23.0 — 2026-08-10
104
+
105
+ No BREAKING changes.
106
+
107
+ ### Added
108
+
109
+ - **design/183 adoption protocol, core file half.** Root adoption marker + six-phase
110
+ `adoptLocalDataRoot` state machine + `readAdoptionStatus` standing introspection + config account
111
+ (witness/ack) + quarantine listing; 23 new exports. The I6 boot gate is a new refusal surface: every
112
+ file-store constructor under a root whose `adoption.json` marker is IN FLIGHT throws
113
+ `AdoptionError("adoption_in_flight")` (corrupt marker: `adoption_marker_corrupt`, fail-closed).
114
+ Roots without a marker are untouched. The terminal report carries `notMigratedByDesign` — the
115
+ machine-readable closed set of assets ruled to stay local, sharded per migration form.
116
+ - **design/182 org layer wired into the decision chain** (org-governed deployments only): org deny
117
+ blocks on both the main gate and the durable-resume leg; an unavailable org snapshot past
118
+ `ORG_ADJUDICATION_TIMEOUT_MS` (15s, mandatory at both call sites) downgrades every terminal allow
119
+ to a human ask, fail-closed. Closed-set additions consumers should re-pin: `DecisionReason`
120
+ +`org_rule`/`org_unavailable`; `PermissionDeniedSource` +`org`; `TraceEvent`
121
+ +`rule_sync_resurrected`/`rule_sync_dropped`/`org_snapshot_unavailable`.
122
+ - `AskOutcome` terminal arms accept an optional `settledBy` (host self-report; `allow` × `"timeout"`
123
+ is loudly refused); `GoalResult.budgetCause` names which ceiling ended a budget run;
124
+ `NON_GOVERNANCE_MEMORY_CODES` +3 members (`memory.challenge_ledger_oversize`,
125
+ `memory.control_plane_rebuilt`, `memory.control_plane_not_corrupt`);
126
+ `MemoryEngine.rebuildCorruptControlPlaneLedger` (host-only, refuses a healthy ledger, quarantines
127
+ corrupt bytes) and `CHALLENGE_LEDGER_MAX_EVENTS` disclosure threshold.
128
+
129
+ ### Fixed / narrowed
130
+
131
+ - **#109**: separated option values (`head -c 5`, `cut -f 1`, `grep -A 3 pat`, …) are no longer
132
+ counted as file operands by the read-only shell classifier — fileless forms of these commands now
133
+ ask instead of auto-passing.
134
+ - **#110**: a checkpoint `put` that errors but whose row is durably present (read-back claims it)
135
+ suspends instead of running on; `put`+`get` both failing aborts the run fail-closed.
136
+ - **#114**: `settledBy:"timeout"` documented promise narrowed to the two core-managed wait windows.
137
+ - **#115**: the offload truncation notice states where the remainder IS instead of implying any
138
+ caller can fetch it.
139
+ - Consent-record ids and the in-memory CRDT actor default now take real entropy (`randomBytes`);
140
+ format is opaque, do not parse.
141
+
142
+ ### Tooling
143
+
144
+ - New `gate:error-surface` (four invariants over model-visible error minting: no `.stack`,
145
+ >400-char templates ledgered, uncapped-arm ratchet, verbatim-guidance dedup) wired into CI.
146
+ - The environment-knob surface (25 names) is under an exhaustive AST-driven liveness registry;
147
+ `docs/CONFIG-CATALOG-GUIDE.md` gained the environment-knobs chapter (two comment-only pseudo-knobs
148
+ called out: `MAX_TOOL_CONCURRENCY`, `APPROVAL_TTL_MS` — setting them has no effect).
149
+
3
150
  ## 5.22.0 — 2026-08-09
4
151
 
5
152
  ### BREAKING
@@ -45,7 +192,10 @@
45
192
  delegation-provenance aggregate rides the durable checkpoint; a resumed leg missing the state
46
193
  reads as `unknown` (static floor), never as `clean`.
47
194
  - **`buildToolResultRef` exported from the package root** — a host wiring an HTTP tool-result read
48
- face binds an incoming ref to its owning task by prefix recomputation.
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.)
49
199
  - **Content mandate split from the approval mandate (#94).** A delegated child's question routes on
50
200
  the QUESTION seat, not the approver seat. Behavior widening, called out explicitly: under
51
201
  `durableApproval` with a live `onQuestion`, an absent or string approver seat used to leave child
@@ -56,6 +206,13 @@
56
206
  `TaskRegistry.reviveBackgroundAgent`'s refusal union gains the `recycling` member — exhaustive
57
207
  consumers add one arm.
58
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
+
59
216
  ## 5.21.1 — 2026-08-09
60
217
 
61
218
  - **Fix: every `hands: none` deployment failed at the door on 5.21.0** (P0). `HAND_TOOL_EFFECTS`
@@ -1058,6 +1215,17 @@ mysterious runtime.
1058
1215
 
1059
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.
1060
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
+
1061
1229
  ## 5.12.0 — 2026-08-05
1062
1230
 
1063
1231
  ### BREAKING
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { isAbsolute } from "node:path";
3
3
  import { withDelegationProvenance } from "../core/tool-policy.js";
4
+ import { isHighSurrogate, isLowSurrogate } from "../core/surrogate-safe-slice.js";
4
5
  import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from "../core/memory-engine/delegation-provenance.js";
5
6
  import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
6
7
  import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
@@ -65,8 +66,8 @@ function defuseGapMarkers(slice) {
65
66
  return slice.replace(/\[…/g, `${REVIEW_MARKER_SCAR}…`);
66
67
  }
67
68
  function surrogateSafeSlice(text, from, width, anchorEnd = false) {
68
- const isLow = (i) => text.charCodeAt(i) >= 0xdc00 && text.charCodeAt(i) <= 0xdfff;
69
- const isHigh = (i) => text.charCodeAt(i) >= 0xd800 && text.charCodeAt(i) <= 0xdbff;
69
+ const isLow = (i) => isLowSurrogate(text.charCodeAt(i));
70
+ const isHigh = (i) => isHighSurrogate(text.charCodeAt(i));
70
71
  let start = from;
71
72
  if (start > 0 && start < text.length && isLow(start) && isHigh(start - 1))
72
73
  start--;
@@ -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 7 for F-012-bearing checkpoints — this worker reads v1 (legacy human), v2 (resource), v3
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) and v7 (F-012 constraint-chain / delegation-provenance enforcement state). */
918
- export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 7;
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 MAX_SUPPORTED_CHECKPOINT_VERSION = 7;
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
  }
@@ -17,6 +17,9 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
17
17
  "memory.challenge_sweep_failed",
18
18
  "memory.lineage_settle_failed",
19
19
  "memory.challenge_ledger_corrupt",
20
+ "memory.challenge_ledger_oversize",
21
+ "memory.control_plane_rebuilt",
22
+ "memory.control_plane_not_corrupt",
20
23
  ]);
21
24
  export function governanceRetryClass(code) {
22
25
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
@@ -161,8 +161,13 @@ export interface Hooks {
161
161
  * - `"shellGate"` — same tighten-deny, but the tool's irreversibility tier was installed by the COARSE
162
162
  * `TaskSpec.shellGate` doctrine (design/80 D-E), not an explicit per-tool mark.
163
163
  * - `"planMode"` — the design/108 plan-mode write-deny short-circuit (a read-only fidelity gate).
164
+ * - `"org"` (design/182 §7) — the ORG layer: an organization deny rule, or the fail-closed tighten an
165
+ * org-governed deployment applies while it cannot adjudicate against a snapshot (that tighten's ask
166
+ * then resolving to deny). Its own word rather than `"policy"` because the authority is the ORG's
167
+ * published snapshot, not this deployment's `ToolPolicy` — an operator reading "policy" on a call
168
+ * their own policy allowed would be looking in the wrong place.
164
169
  */
165
- export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier";
170
+ export type PermissionDeniedSource = "policy" | "hook" | "safety" | "shellGate" | "planMode" | "classifier" | "org";
166
171
  /** The payload a {@link Hooks.permissionDenied} callback observes (CC-exact fields + `source`). */
167
172
  export interface PermissionDeniedPayload {
168
173
  toolName: string;
@@ -520,6 +525,26 @@ export interface ParkAttemptFailed {
520
525
  /** Why the park could not be minted — a sentence, already worded for a reader. */
521
526
  parkFailed: string;
522
527
  }
528
+ /**
529
+ * design/182 §7 — what the ORG layer answers for ONE call (see `ToolGateInput.orgRules`).
530
+ *
531
+ * The two arms are distinguished by `status`, NEVER by the absence of a verdict: "the org published no
532
+ * rule for this call" and "this deployment cannot see the org's rules" are opposite facts, and reading
533
+ * an empty answer as the first one is exactly the governance fail-open the availability contract exists
534
+ * to prevent.
535
+ */
536
+ export type OrgGateVerdict = {
537
+ status: "available";
538
+ /** The org rule that speaks for this call, if any. Deny outranks ask; there is no allow bucket. */
539
+ verdict?: {
540
+ behavior: "deny" | "ask";
541
+ rule: string;
542
+ };
543
+ } | {
544
+ status: "unavailable";
545
+ /** The overlay's own disclosure lines (engine/deployment-authored), for the operator channel. */
546
+ disclosures: readonly string[];
547
+ };
523
548
  /** Inputs to the two-phase tool gate. `adjudicate`/`resolveAsk` are pre-bound to the task abort signal. */
524
549
  export interface ToolGateInput {
525
550
  /** RB-473 batch 2b — bounded disclosure for a HOST callback that throws inside this gate call's
@@ -577,7 +602,13 @@ export interface ToolGateInput {
577
602
  * prefer, so the closure must NOT take its sync-first decline and should park durably. All other
578
603
  * decline/pre-commit-failure paths keep their existing `undefined` fallbacks (the gate then keeps the
579
604
  * fail-closed deny / typed refusal the caller carries). */
580
- liveFaceUnavailable?: boolean) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
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>;
581
612
  /**
582
613
  * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
583
614
  * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
@@ -713,6 +744,42 @@ export interface ToolGateInput {
713
744
  rule: string;
714
745
  }) => void;
715
746
  };
747
+ /**
748
+ * design/182 §7 — the ORG layer. Present ONLY when the deployment DECLARED org governance (the
749
+ * overlay constructor is the boot gate: a governed declaration with no snapshot provider refuses to
750
+ * boot). Absent ⇒ the whole step does not exist and the decision path is byte-identical.
751
+ *
752
+ * The org layer is the outermost TIGHTENING authority, so it runs after every fold and every engine
753
+ * tighten and before the ask-resolution chain — "org deny > org ask (non-dismissable) > personal
754
+ * allow rule > bare ask". It never loosens: there is structurally no org allow bucket.
755
+ *
756
+ * The `unavailable` arm is the load-bearing one. An org-governed deployment that cannot reach a
757
+ * snapshot fails the WHOLE decision boundary closed — every terminal allow (a deployment-policy
758
+ * allow and the no-policy default allow included) becomes an ask carrying
759
+ * `decisionReason: "org_unavailable"` AND `requiresRealApproval: true`, and both ask→allow seams (the
760
+ * personal-rule lane and the auto-mode classifier) are disarmed. Tightening only the two seams was
761
+ * refuted during design: a call that is ALREADY allow never passes through either seam, so it would
762
+ * have sailed past the org deny that could not be read. The `requiresRealApproval` bit closes the
763
+ * third loosening path — a blanket `onAsk: "allow"` would otherwise clear the synthetic ask.
764
+ */
765
+ orgRules?: {
766
+ /** Adjudicate this call against the current org overlay. MUST NOT throw — a throw is read as
767
+ * `unavailable` (fail-closed), never as "no org rule spoke". */
768
+ adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
769
+ /** True iff the reserved question-tool NAME on this leg resolves to the ENGINE's own content-ask
770
+ * tool. It is the exemption's real predicate: the name alone is not, because a caller may mount
771
+ * its own tool under that name when the first-party one is not mounted, and such a tool has
772
+ * arbitrary side effects (codex review, confirmed MED). Absent ⇒ `false` ⇒ the call is governed
773
+ * like any other, which is the fail-closed default. */
774
+ contentAskToolMounted?: boolean;
775
+ /** Disclosure sink for the unavailable arm (trace `permission.org_snapshot_unavailable`). Never
776
+ * affects the outcome. */
777
+ onUnavailable?: (info: {
778
+ toolName: string;
779
+ toolCallId: string;
780
+ message: string;
781
+ }) => void;
782
+ };
716
783
  /**
717
784
  * F-012 L2 — the SANDBOX ADMISSION leg. Present ONLY when the runner armed the mode: the execution
718
785
  * env DECLARES isolation (`RemoteExecutionEnv.capabilities.isolation === true` — a Runner-resolved
@@ -3,6 +3,7 @@ import { brandPolicyAskClass } from "./ask-class.js";
3
3
  import { inlineUntrusted } from "./untrusted-text.js";
4
4
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
5
5
  import { createSafeNotifier } from "./safe-notify.js";
6
+ import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
6
7
  export function cloneObserverInput(input) {
7
8
  try {
8
9
  return structuredClone(input);
@@ -246,12 +247,76 @@ export async function runToolGate(input) {
246
247
  denySource = input.shellGated === true ? "shellGate" : "safety";
247
248
  }
248
249
  }
250
+ let orgRealApprovalRequired = false;
251
+ let orgAskOrigin;
252
+ let orgTightenCount = 0;
253
+ const applyOrgLayer = async (current, args) => {
254
+ if (input.orgRules === undefined)
255
+ return current;
256
+ if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
257
+ return current;
258
+ let decided = current;
259
+ const answer = await settleOrgVerdictWithin(input.orgRules
260
+ .adjudicate({ toolName, args, toolCallId })
261
+ .catch((err) => ({
262
+ status: "unavailable",
263
+ disclosures: [`the org adjudication face threw: ${err instanceof Error ? err.message : String(err)}`],
264
+ })), {
265
+ status: "unavailable",
266
+ disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`],
267
+ }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
268
+ if (answer.status === "unavailable") {
269
+ orgRealApprovalRequired = true;
270
+ orgAskOrigin = "unavailable";
271
+ await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
272
+ if (decided.action === "allow") {
273
+ decided = {
274
+ action: "ask",
275
+ message: `tool "${toolName}" needs approval: this deployment is org-governed and cannot currently adjudicate ` +
276
+ `against an organization policy snapshot — every allow tightens to a real approval until it can`,
277
+ decisionReason: ORG_UNAVAILABLE_DECISION_REASON,
278
+ requiresRealApproval: true,
279
+ };
280
+ orgTightenCount += 1;
281
+ denySource = "org";
282
+ }
283
+ else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
284
+ decided = { ...decided, requiresRealApproval: true };
285
+ orgTightenCount += 1;
286
+ }
287
+ return decided;
288
+ }
289
+ if (answer.verdict === undefined)
290
+ return decided;
291
+ const { behavior, rule } = answer.verdict;
292
+ if (behavior === "deny") {
293
+ orgTightenCount += 1;
294
+ denySource = "org";
295
+ return { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON };
296
+ }
297
+ if (decided.action === "deny")
298
+ return decided;
299
+ orgTightenCount += 1;
300
+ denySource = "org";
301
+ orgRealApprovalRequired = true;
302
+ orgAskOrigin = "rule";
303
+ return decided.action === "ask"
304
+ ? { ...decided, requiresRealApproval: true }
305
+ : {
306
+ action: "ask",
307
+ message: `an organization policy rule (${rule}) requires approval for this call`,
308
+ decisionReason: ORG_RULE_DECISION_REASON,
309
+ requiresRealApproval: true,
310
+ };
311
+ };
312
+ decision = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
249
313
  if (decision.action === "ask") {
250
314
  if (policyRewrite !== undefined) {
251
315
  currentInput = policyRewrite;
252
316
  req.args = policyRewrite;
253
317
  }
254
318
  if (input.persistedRules &&
319
+ !orgRealApprovalRequired &&
255
320
  decision.action === "ask" &&
256
321
  decision.requiresRealApproval !== true &&
257
322
  decision.decisionReason !== "hook" &&
@@ -269,6 +334,7 @@ export async function runToolGate(input) {
269
334
  }
270
335
  }
271
336
  if (input.autoMode &&
337
+ !orgRealApprovalRequired &&
272
338
  decision.action === "ask" &&
273
339
  req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
274
340
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
@@ -320,8 +386,11 @@ export async function runToolGate(input) {
320
386
  const egressTool = input.egress === true;
321
387
  const irreversibleTool = input.irreversibility === "always" || input.irreversibility === "maybe";
322
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;
323
392
  if (suspendAsk && decision.action === "ask") {
324
- const suspended = await suspendAsk(req, currentInput, safety);
393
+ const suspended = await suspendAsk(req, currentInput, safety, undefined, realApprovalOf(decision));
325
394
  if (suspended) {
326
395
  if ("parkFailed" in suspended)
327
396
  parkFailed = suspended.parkFailed;
@@ -340,7 +409,7 @@ export async function runToolGate(input) {
340
409
  req.args = outcome.presentedInput;
341
410
  }
342
411
  if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
343
- const suspended = await suspendAsk(req, currentInput, safety, true);
412
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(decision));
344
413
  if (suspended) {
345
414
  if ("parkFailed" in suspended)
346
415
  parkFailed = suspended.parkFailed;
@@ -364,10 +433,11 @@ export async function runToolGate(input) {
364
433
  }
365
434
  }
366
435
  if (decision.action === "ask") {
436
+ const askBeforeResolve = decision;
367
437
  const resolved = await resolveAsk(decision, req);
368
438
  decision = resolved;
369
439
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
370
- const suspended = await suspendAsk(req, currentInput, safety, true);
440
+ const suspended = await suspendAsk(req, currentInput, safety, true, realApprovalOf(askBeforeResolve));
371
441
  if (suspended) {
372
442
  if ("parkFailed" in suspended)
373
443
  parkFailed = suspended.parkFailed;
@@ -418,23 +488,34 @@ export async function runToolGate(input) {
418
488
  editArgs = hr.updatedInput;
419
489
  }
420
490
  }
421
- if (!adjudicate)
491
+ if (!adjudicate && input.orgRules === undefined)
422
492
  break;
423
- let recheck;
424
- try {
425
- recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
426
- }
427
- catch (err) {
428
- recheck = {
429
- action: "deny",
430
- message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
431
- };
493
+ let recheck = { action: "allow" };
494
+ if (adjudicate) {
495
+ try {
496
+ recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
497
+ }
498
+ catch (err) {
499
+ recheck = {
500
+ action: "deny",
501
+ message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
502
+ };
503
+ }
432
504
  }
433
505
  if (recheck.action === "deny") {
434
506
  editDenied = recheck;
435
507
  denySource = "policy";
436
508
  break;
437
509
  }
510
+ if (recheck.updatedInput !== undefined)
511
+ editArgs = recheck.updatedInput;
512
+ const orgTightensBefore = orgTightenCount;
513
+ recheck = await applyOrgLayer(recheck, editArgs);
514
+ const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
515
+ if (recheck.action === "deny") {
516
+ editDenied = recheck;
517
+ break;
518
+ }
438
519
  if (recheck.action === "allow") {
439
520
  if (recheck.updatedInput !== undefined)
440
521
  editArgs = recheck.updatedInput;
@@ -445,11 +526,15 @@ export async function runToolGate(input) {
445
526
  const rr = await resolveAsk(recheck, { toolName, args: editArgs, toolCallId });
446
527
  if (rr.action !== "allow") {
447
528
  editDenied = rr;
448
- denySource = "policy";
529
+ if (!orgRaisedThisRound)
530
+ denySource = "policy";
449
531
  break;
450
532
  }
451
- if (rr.updatedInput === undefined)
533
+ if (rr.updatedInput === undefined) {
534
+ if (rr.presentedInput !== undefined)
535
+ editArgs = rr.presentedInput;
452
536
  break;
537
+ }
453
538
  editArgs = rr.updatedInput;
454
539
  }
455
540
  decision = editDenied ?? { ...decision, updatedInput: editArgs };
@@ -1,4 +1,4 @@
1
- import { type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
1
+ import { type ChallengeAssignment, type ChallengeEvent, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, type MemoryPartitionIncidentSink, type RetrievedAccountRow, type SessionPollutionRecord } from "./layout.js";
2
2
  import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
3
3
  /**
4
4
  * The CC `# Memory` instruction section — VERBATIM from the live capture
@@ -220,6 +220,33 @@ export declare class MemoryEngine {
220
220
  * generation only — the announcement is a model-visible face, so no name/description/free reason
221
221
  * rides it; content stays on the host audit face). Failures are disclosed, never thrown past. */
222
222
  private challengeAndAnnounce;
223
+ /** design/180 §10 — one disclosure per PROCESS per engine, not one per append: the size is a
224
+ * standing property of the account, and repeating it on every challenge is the spam that gets
225
+ * advisories muted (the same argument the durable-park disclosures are built on). */
226
+ private challengeLedgerSizeDisclosed;
227
+ /**
228
+ * design/180 §10 — the challenge ledger's size bound is a DISCLOSURE threshold, never a cap: the
229
+ * ledger cannot be rotated (the exclusion predicate is derived from the events, so dropping the
230
+ * oldest rows would un-withhold their entries), so crossing {@link CHALLENGE_LEDGER_MAX_EVENTS}
231
+ * changes nothing about what is withheld — it tells the deployment that an account on the
232
+ * per-tool-call read path has grown to where it costs the session latency.
233
+ */
234
+ private discloseChallengeLedgerSize;
235
+ /**
236
+ * design/180 §10 — the RECOVERY channel for a corrupt fail-closed ledger (lineage / challenges).
237
+ *
238
+ * The fail-closed family refuses every read on corruption so a silent "start fresh" cannot launder
239
+ * taint; permanent refusal is the point, and it left a deployment with genuinely corrupt bytes no way
240
+ * back except deleting engine files by hand. This is that way back, and it is explicit in every
241
+ * dimension: only a host call reaches it (nothing in the engine self-heals), a HEALTHY ledger is
242
+ * refused (`memory.control_plane_not_corrupt`), the corrupt bytes are quarantined beside the file
243
+ * rather than deleted, and the rebuild ANNOUNCES itself — resetting the challenge ledger un-withholds
244
+ * whatever it was withholding, which the model's next session must not learn about silently.
245
+ *
246
+ * `reason` is required and recorded in the announcement's shape (not its text — the announcement is a
247
+ * model-visible face and stays opaque); it is the operator's own note on the host error face.
248
+ */
249
+ rebuildCorruptControlPlaneLedger(ledger: StrictControlPlaneLedger, reason: string): ControlPlaneRebuildReceipt;
223
250
  /**
224
251
  * Host API (advisory, control-plane trust level — the same plane that can mark pollution):
225
252
  * challenge entries by id. `requestId` is REQUIRED (the generation ledger's idempotency identity;