@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.
- package/CHANGELOG.md +169 -1
- package/dist/agents/subagent.js +3 -2
- package/dist/core/checkpoint-store.d.ts +38 -3
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +69 -2
- package/dist/core/hooks.js +100 -15
- package/dist/core/memory-engine/engine.d.ts +28 -1
- package/dist/core/memory-engine/engine.js +62 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +69 -3
- package/dist/core/memory-engine/layout.js +75 -6
- package/dist/core/permission-rule-consent.js +2 -1
- package/dist/core/permission-rule-org.d.ts +36 -2
- package/dist/core/permission-rule-org.js +23 -0
- package/dist/core/permission-rule-store.d.ts +25 -14
- package/dist/core/permission-rule-store.js +7 -2
- package/dist/core/permission-rule-sync.d.ts +8 -0
- package/dist/core/permission-rule-sync.js +35 -6
- package/dist/core/runner/prepare-task.d.ts +10 -2
- package/dist/core/runner/prepare-task.js +120 -11
- package/dist/core/runner/runtask.js +46 -1
- package/dist/core/runner/session-file-state-replay.js +3 -0
- package/dist/core/tool-policy.d.ts +37 -4
- package/dist/core/tool-policy.js +49 -19
- package/dist/core/tool-result-store.d.ts +17 -1
- package/dist/core/tool-result-store.js +79 -4
- package/dist/core/trace.d.ts +47 -0
- package/dist/core/types.d.ts +45 -5
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +18 -10
- package/dist/index.js +5 -3
- package/dist/orchestration/goal.d.ts +10 -0
- package/dist/orchestration/goal.js +6 -5
- package/dist/stores/file/adoption/adopt.d.ts +146 -0
- package/dist/stores/file/adoption/adopt.js +611 -0
- package/dist/stores/file/adoption/marker.d.ts +202 -0
- package/dist/stores/file/adoption/marker.js +205 -0
- package/dist/stores/file/background-agent-store.js +2 -0
- package/dist/stores/file/checkpoint-store.js +2 -0
- package/dist/stores/file/file-snapshot-store.js +2 -0
- package/dist/stores/file/index.d.ts +2 -0
- package/dist/stores/file/index.js +4 -0
- package/dist/stores/file/mailbox-store.js +2 -0
- package/dist/stores/file/memory-store.js +2 -0
- package/dist/stores/file/session-policy-store.d.ts +11 -1
- package/dist/stores/file/session-policy-store.js +9 -2
- package/dist/stores/file/session-store.js +2 -0
- package/dist/stores/file/task-list-store.js +2 -0
- package/dist/stores/file/tool-result-store.js +2 -0
- package/dist/stores/file/usage-window-store.js +2 -0
- package/dist/stores/file/workflow-journal-store.js +2 -0
- package/dist/stores/file/workflow-run-store.js +2 -0
- package/dist/tools/fs/bash-readonly-classifier.js +59 -10
- package/dist/tools/fs/fs-bash.js +7 -4
- package/dist/tools/monitor.js +3 -3
- 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
|
|
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
|
package/dist/agents/subagent.js
CHANGED
|
@@ -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) =>
|
|
69
|
-
const isHigh = (i) =>
|
|
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
|
|
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
|
}
|
|
@@ -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)) {
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/core/hooks.js
CHANGED
|
@@ -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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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;
|