@sema-agent/core 5.22.0 → 5.23.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 +47 -0
- package/dist/agents/subagent.js +3 -2
- package/dist/core/governance-codes.js +3 -0
- package/dist/core/hooks.d.ts +62 -1
- package/dist/core/hooks.js +90 -12
- 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.js +2 -1
- 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 +111 -5
- package/dist/core/runner/runtask.js +19 -0
- package/dist/core/tool-policy.d.ts +37 -4
- package/dist/core/tool-policy.js +32 -4
- package/dist/core/tool-result-store.d.ts +9 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +47 -0
- package/dist/core/types.d.ts +38 -0
- package/dist/core/wiring-manifest.d.ts +16 -1
- package/dist/core/wiring-manifest.js +7 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- 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 +616 -0
- package/dist/stores/file/adoption/marker.d.ts +194 -0
- package/dist/stores/file/adoption/marker.js +198 -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.js +2 -0
- 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/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.23.0 — 2026-08-10
|
|
4
|
+
|
|
5
|
+
No BREAKING changes.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **design/183 adoption protocol, core file half.** Root adoption marker + six-phase
|
|
10
|
+
`adoptLocalDataRoot` state machine + `readAdoptionStatus` standing introspection + config account
|
|
11
|
+
(witness/ack) + quarantine listing; 23 new exports. The I6 boot gate is a new refusal surface: every
|
|
12
|
+
file-store constructor under a root whose `adoption.json` marker is IN FLIGHT throws
|
|
13
|
+
`AdoptionError("adoption_in_flight")` (corrupt marker: `adoption_marker_corrupt`, fail-closed).
|
|
14
|
+
Roots without a marker are untouched. The terminal report carries `notMigratedByDesign` — the
|
|
15
|
+
machine-readable closed set of assets ruled to stay local, sharded per migration form.
|
|
16
|
+
- **design/182 org layer wired into the decision chain** (org-governed deployments only): org deny
|
|
17
|
+
blocks on both the main gate and the durable-resume leg; an unavailable org snapshot past
|
|
18
|
+
`ORG_ADJUDICATION_TIMEOUT_MS` (15s, mandatory at both call sites) downgrades every terminal allow
|
|
19
|
+
to a human ask, fail-closed. Closed-set additions consumers should re-pin: `DecisionReason`
|
|
20
|
+
+`org_rule`/`org_unavailable`; `PermissionDeniedSource` +`org`; `TraceEvent`
|
|
21
|
+
+`rule_sync_resurrected`/`rule_sync_dropped`/`org_snapshot_unavailable`.
|
|
22
|
+
- `AskOutcome` terminal arms accept an optional `settledBy` (host self-report; `allow` × `"timeout"`
|
|
23
|
+
is loudly refused); `GoalResult.budgetCause` names which ceiling ended a budget run;
|
|
24
|
+
`NON_GOVERNANCE_MEMORY_CODES` +3 members (`memory.challenge_ledger_oversize`,
|
|
25
|
+
`memory.control_plane_rebuilt`, `memory.control_plane_not_corrupt`);
|
|
26
|
+
`MemoryEngine.rebuildCorruptControlPlaneLedger` (host-only, refuses a healthy ledger, quarantines
|
|
27
|
+
corrupt bytes) and `CHALLENGE_LEDGER_MAX_EVENTS` disclosure threshold.
|
|
28
|
+
|
|
29
|
+
### Fixed / narrowed
|
|
30
|
+
|
|
31
|
+
- **#109**: separated option values (`head -c 5`, `cut -f 1`, `grep -A 3 pat`, …) are no longer
|
|
32
|
+
counted as file operands by the read-only shell classifier — fileless forms of these commands now
|
|
33
|
+
ask instead of auto-passing.
|
|
34
|
+
- **#110**: a checkpoint `put` that errors but whose row is durably present (read-back claims it)
|
|
35
|
+
suspends instead of running on; `put`+`get` both failing aborts the run fail-closed.
|
|
36
|
+
- **#114**: `settledBy:"timeout"` documented promise narrowed to the two core-managed wait windows.
|
|
37
|
+
- **#115**: the offload truncation notice states where the remainder IS instead of implying any
|
|
38
|
+
caller can fetch it.
|
|
39
|
+
- Consent-record ids and the in-memory CRDT actor default now take real entropy (`randomBytes`);
|
|
40
|
+
format is opaque, do not parse.
|
|
41
|
+
|
|
42
|
+
### Tooling
|
|
43
|
+
|
|
44
|
+
- New `gate:error-surface` (four invariants over model-visible error minting: no `.stack`,
|
|
45
|
+
>400-char templates ledgered, uncapped-arm ratchet, verbatim-guidance dedup) wired into CI.
|
|
46
|
+
- The environment-knob surface (25 names) is under an exhaustive AST-driven liveness registry;
|
|
47
|
+
`docs/CONFIG-CATALOG-GUIDE.md` gained the environment-knobs chapter (two comment-only pseudo-knobs
|
|
48
|
+
called out: `MAX_TOOL_CONCURRENCY`, `APPROVAL_TTL_MS` — setting them has no effect).
|
|
49
|
+
|
|
3
50
|
## 5.22.0 — 2026-08-09
|
|
4
51
|
|
|
5
52
|
### 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--;
|
|
@@ -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
|
|
@@ -713,6 +738,42 @@ export interface ToolGateInput {
|
|
|
713
738
|
rule: string;
|
|
714
739
|
}) => void;
|
|
715
740
|
};
|
|
741
|
+
/**
|
|
742
|
+
* design/182 §7 — the ORG layer. Present ONLY when the deployment DECLARED org governance (the
|
|
743
|
+
* overlay constructor is the boot gate: a governed declaration with no snapshot provider refuses to
|
|
744
|
+
* boot). Absent ⇒ the whole step does not exist and the decision path is byte-identical.
|
|
745
|
+
*
|
|
746
|
+
* The org layer is the outermost TIGHTENING authority, so it runs after every fold and every engine
|
|
747
|
+
* tighten and before the ask-resolution chain — "org deny > org ask (non-dismissable) > personal
|
|
748
|
+
* allow rule > bare ask". It never loosens: there is structurally no org allow bucket.
|
|
749
|
+
*
|
|
750
|
+
* The `unavailable` arm is the load-bearing one. An org-governed deployment that cannot reach a
|
|
751
|
+
* snapshot fails the WHOLE decision boundary closed — every terminal allow (a deployment-policy
|
|
752
|
+
* allow and the no-policy default allow included) becomes an ask carrying
|
|
753
|
+
* `decisionReason: "org_unavailable"` AND `requiresRealApproval: true`, and both ask→allow seams (the
|
|
754
|
+
* personal-rule lane and the auto-mode classifier) are disarmed. Tightening only the two seams was
|
|
755
|
+
* refuted during design: a call that is ALREADY allow never passes through either seam, so it would
|
|
756
|
+
* have sailed past the org deny that could not be read. The `requiresRealApproval` bit closes the
|
|
757
|
+
* third loosening path — a blanket `onAsk: "allow"` would otherwise clear the synthetic ask.
|
|
758
|
+
*/
|
|
759
|
+
orgRules?: {
|
|
760
|
+
/** Adjudicate this call against the current org overlay. MUST NOT throw — a throw is read as
|
|
761
|
+
* `unavailable` (fail-closed), never as "no org rule spoke". */
|
|
762
|
+
adjudicate: (req: ToolCallRequest) => Promise<OrgGateVerdict>;
|
|
763
|
+
/** True iff the reserved question-tool NAME on this leg resolves to the ENGINE's own content-ask
|
|
764
|
+
* tool. It is the exemption's real predicate: the name alone is not, because a caller may mount
|
|
765
|
+
* its own tool under that name when the first-party one is not mounted, and such a tool has
|
|
766
|
+
* arbitrary side effects (codex review, confirmed MED). Absent ⇒ `false` ⇒ the call is governed
|
|
767
|
+
* like any other, which is the fail-closed default. */
|
|
768
|
+
contentAskToolMounted?: boolean;
|
|
769
|
+
/** Disclosure sink for the unavailable arm (trace `permission.org_snapshot_unavailable`). Never
|
|
770
|
+
* affects the outcome. */
|
|
771
|
+
onUnavailable?: (info: {
|
|
772
|
+
toolName: string;
|
|
773
|
+
toolCallId: string;
|
|
774
|
+
message: string;
|
|
775
|
+
}) => void;
|
|
776
|
+
};
|
|
716
777
|
/**
|
|
717
778
|
* F-012 L2 — the SANDBOX ADMISSION leg. Present ONLY when the runner armed the mode: the execution
|
|
718
779
|
* 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,73 @@ export async function runToolGate(input) {
|
|
|
246
247
|
denySource = input.shellGated === true ? "shellGate" : "safety";
|
|
247
248
|
}
|
|
248
249
|
}
|
|
250
|
+
let orgRealApprovalRequired = false;
|
|
251
|
+
let orgTightenCount = 0;
|
|
252
|
+
const applyOrgLayer = async (current, args) => {
|
|
253
|
+
if (input.orgRules === undefined)
|
|
254
|
+
return current;
|
|
255
|
+
if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
|
|
256
|
+
return current;
|
|
257
|
+
let decided = current;
|
|
258
|
+
const answer = await settleOrgVerdictWithin(input.orgRules
|
|
259
|
+
.adjudicate({ toolName, args, toolCallId })
|
|
260
|
+
.catch((err) => ({
|
|
261
|
+
status: "unavailable",
|
|
262
|
+
disclosures: [`the org adjudication face threw: ${err instanceof Error ? err.message : String(err)}`],
|
|
263
|
+
})), {
|
|
264
|
+
status: "unavailable",
|
|
265
|
+
disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`],
|
|
266
|
+
}, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
|
|
267
|
+
if (answer.status === "unavailable") {
|
|
268
|
+
orgRealApprovalRequired = true;
|
|
269
|
+
await notifier.notifyAsync(() => input.orgRules?.onUnavailable?.({ toolName, toolCallId, message: answer.disclosures.join("; ") }), "toolGate.orgSnapshotUnavailable");
|
|
270
|
+
if (decided.action === "allow") {
|
|
271
|
+
decided = {
|
|
272
|
+
action: "ask",
|
|
273
|
+
message: `tool "${toolName}" needs approval: this deployment is org-governed and cannot currently adjudicate ` +
|
|
274
|
+
`against an organization policy snapshot — every allow tightens to a real approval until it can`,
|
|
275
|
+
decisionReason: ORG_UNAVAILABLE_DECISION_REASON,
|
|
276
|
+
requiresRealApproval: true,
|
|
277
|
+
};
|
|
278
|
+
orgTightenCount += 1;
|
|
279
|
+
denySource = "org";
|
|
280
|
+
}
|
|
281
|
+
else if (decided.action === "ask" && decided.requiresRealApproval !== true) {
|
|
282
|
+
decided = { ...decided, requiresRealApproval: true };
|
|
283
|
+
orgTightenCount += 1;
|
|
284
|
+
}
|
|
285
|
+
return decided;
|
|
286
|
+
}
|
|
287
|
+
if (answer.verdict === undefined)
|
|
288
|
+
return decided;
|
|
289
|
+
const { behavior, rule } = answer.verdict;
|
|
290
|
+
if (behavior === "deny") {
|
|
291
|
+
orgTightenCount += 1;
|
|
292
|
+
denySource = "org";
|
|
293
|
+
return { action: "deny", message: `an organization policy rule (${rule}) denies this call`, decisionReason: ORG_RULE_DECISION_REASON };
|
|
294
|
+
}
|
|
295
|
+
if (decided.action === "deny")
|
|
296
|
+
return decided;
|
|
297
|
+
orgTightenCount += 1;
|
|
298
|
+
denySource = "org";
|
|
299
|
+
orgRealApprovalRequired = true;
|
|
300
|
+
return decided.action === "ask"
|
|
301
|
+
? { ...decided, requiresRealApproval: true }
|
|
302
|
+
: {
|
|
303
|
+
action: "ask",
|
|
304
|
+
message: `an organization policy rule (${rule}) requires approval for this call`,
|
|
305
|
+
decisionReason: ORG_RULE_DECISION_REASON,
|
|
306
|
+
requiresRealApproval: true,
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
decision = await applyOrgLayer(decision, policyRewrite !== undefined ? policyRewrite : currentInput);
|
|
249
310
|
if (decision.action === "ask") {
|
|
250
311
|
if (policyRewrite !== undefined) {
|
|
251
312
|
currentInput = policyRewrite;
|
|
252
313
|
req.args = policyRewrite;
|
|
253
314
|
}
|
|
254
315
|
if (input.persistedRules &&
|
|
316
|
+
!orgRealApprovalRequired &&
|
|
255
317
|
decision.action === "ask" &&
|
|
256
318
|
decision.requiresRealApproval !== true &&
|
|
257
319
|
decision.decisionReason !== "hook" &&
|
|
@@ -269,6 +331,7 @@ export async function runToolGate(input) {
|
|
|
269
331
|
}
|
|
270
332
|
}
|
|
271
333
|
if (input.autoMode &&
|
|
334
|
+
!orgRealApprovalRequired &&
|
|
272
335
|
decision.action === "ask" &&
|
|
273
336
|
req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
|
|
274
337
|
input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
|
|
@@ -418,23 +481,34 @@ export async function runToolGate(input) {
|
|
|
418
481
|
editArgs = hr.updatedInput;
|
|
419
482
|
}
|
|
420
483
|
}
|
|
421
|
-
if (!adjudicate)
|
|
484
|
+
if (!adjudicate && input.orgRules === undefined)
|
|
422
485
|
break;
|
|
423
|
-
let recheck;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
486
|
+
let recheck = { action: "allow" };
|
|
487
|
+
if (adjudicate) {
|
|
488
|
+
try {
|
|
489
|
+
recheck = await adjudicate({ toolName, args: editArgs, toolCallId });
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
recheck = {
|
|
493
|
+
action: "deny",
|
|
494
|
+
message: `policy re-check of the approved edit for "${toolName}" errored: ${err instanceof Error ? err.message : String(err)}`,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
432
497
|
}
|
|
433
498
|
if (recheck.action === "deny") {
|
|
434
499
|
editDenied = recheck;
|
|
435
500
|
denySource = "policy";
|
|
436
501
|
break;
|
|
437
502
|
}
|
|
503
|
+
if (recheck.updatedInput !== undefined)
|
|
504
|
+
editArgs = recheck.updatedInput;
|
|
505
|
+
const orgTightensBefore = orgTightenCount;
|
|
506
|
+
recheck = await applyOrgLayer(recheck, editArgs);
|
|
507
|
+
const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
|
|
508
|
+
if (recheck.action === "deny") {
|
|
509
|
+
editDenied = recheck;
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
438
512
|
if (recheck.action === "allow") {
|
|
439
513
|
if (recheck.updatedInput !== undefined)
|
|
440
514
|
editArgs = recheck.updatedInput;
|
|
@@ -445,11 +519,15 @@ export async function runToolGate(input) {
|
|
|
445
519
|
const rr = await resolveAsk(recheck, { toolName, args: editArgs, toolCallId });
|
|
446
520
|
if (rr.action !== "allow") {
|
|
447
521
|
editDenied = rr;
|
|
448
|
-
|
|
522
|
+
if (!orgRaisedThisRound)
|
|
523
|
+
denySource = "policy";
|
|
449
524
|
break;
|
|
450
525
|
}
|
|
451
|
-
if (rr.updatedInput === undefined)
|
|
526
|
+
if (rr.updatedInput === undefined) {
|
|
527
|
+
if (rr.presentedInput !== undefined)
|
|
528
|
+
editArgs = rr.presentedInput;
|
|
452
529
|
break;
|
|
530
|
+
}
|
|
453
531
|
editArgs = rr.updatedInput;
|
|
454
532
|
}
|
|
455
533
|
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;
|
|
@@ -6,7 +6,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
6
6
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
7
|
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
8
8
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./file-backend.js";
|
|
9
|
-
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
9
|
+
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, readIndexRevs, writeIndexRevs, bumpScanFuse, canonicalize, claimRootScope, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, isContainedIn, markSessionPolluted, readSessionPollution, recordRetrievedAccount, writeFileNoFollow, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
10
10
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
11
11
|
export const MEMORY_INSTRUCTION_TEMPLATE = `# Memory
|
|
12
12
|
|
|
@@ -173,7 +173,9 @@ export class MemoryEngine {
|
|
|
173
173
|
challengeAndAnnounce(events) {
|
|
174
174
|
if (events.length === 0)
|
|
175
175
|
return [];
|
|
176
|
-
const assigned = appendChallengeEvents(this.controlDir, events, this.now);
|
|
176
|
+
const { assignments: assigned, eventCount } = appendChallengeEvents(this.controlDir, events, this.now);
|
|
177
|
+
if (eventCount !== undefined)
|
|
178
|
+
this.discloseChallengeLedgerSize(eventCount);
|
|
177
179
|
const fresh = assigned.filter((a) => !a.replayed);
|
|
178
180
|
if (fresh.length > 0) {
|
|
179
181
|
try {
|
|
@@ -189,6 +191,60 @@ export class MemoryEngine {
|
|
|
189
191
|
}
|
|
190
192
|
return assigned;
|
|
191
193
|
}
|
|
194
|
+
challengeLedgerSizeDisclosed = false;
|
|
195
|
+
discloseChallengeLedgerSize(eventCount) {
|
|
196
|
+
if (this.challengeLedgerSizeDisclosed || eventCount <= CHALLENGE_LEDGER_MAX_EVENTS)
|
|
197
|
+
return;
|
|
198
|
+
this.challengeLedgerSizeDisclosed = true;
|
|
199
|
+
const sink = this.onIncident;
|
|
200
|
+
if (sink === undefined)
|
|
201
|
+
return;
|
|
202
|
+
try {
|
|
203
|
+
const err = new Error(`memory challenge ledger holds ${eventCount} events, past the calibrated bound of ${CHALLENGE_LEDGER_MAX_EVENTS}: ` +
|
|
204
|
+
`the whole file is rewritten on every challenge and parsed on every model-visible read (index rebuild, memory tools), ` +
|
|
205
|
+
`so this account now costs the session latency. It is NOT rotated — dropping events would un-withhold their entries — ` +
|
|
206
|
+
`so the deployment resolves the settled generations or archives the account deliberately.`);
|
|
207
|
+
err.code = "memory.challenge_ledger_oversize";
|
|
208
|
+
sink(err);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
rebuildCorruptControlPlaneLedger(ledger, reason) {
|
|
214
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
215
|
+
const e = new Error("rebuildCorruptControlPlaneLedger: a non-empty reason is required (this call drops taint evidence — it is recorded, never implicit)");
|
|
216
|
+
e.code = "config.memory_rebuild_reason";
|
|
217
|
+
throw e;
|
|
218
|
+
}
|
|
219
|
+
if (!isStrictControlPlaneLedgerCorrupt(this.controlDir, ledger)) {
|
|
220
|
+
const e = new Error(`${ledger === "challenges" ? "memory challenge ledger" : "memory lineage ledger"} reads cleanly — refusing to rebuild it. This call exists to recover a ledger whose corruption already refuses every read; resetting a healthy one would drop taint evidence that is still doing its job.`);
|
|
221
|
+
e.code = "memory.control_plane_not_corrupt";
|
|
222
|
+
throw e;
|
|
223
|
+
}
|
|
224
|
+
enqueueMemoryAnnouncement(this.controlDir, {
|
|
225
|
+
kind: "gate",
|
|
226
|
+
at: this.now(),
|
|
227
|
+
items: [
|
|
228
|
+
ledger === "challenges"
|
|
229
|
+
? `memory control plane: the challenge ledger was found CORRUPT and a deployment-initiated reset to empty was requested — entries that were withheld by an unresolved challenge may no longer be withheld. Treat facts recalled from memory as unverified until confirmed with the user.`
|
|
230
|
+
: `memory control plane: the harvest lineage ledger was found CORRUPT and a deployment-initiated reset to empty was requested — which session contributed which entry may no longer be recorded for anything written before this point.`,
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
const receipt = rebuildStrictControlPlaneLedger(this.controlDir, ledger, this.now);
|
|
234
|
+
const sink = this.onIncident;
|
|
235
|
+
if (sink !== undefined) {
|
|
236
|
+
try {
|
|
237
|
+
const err = new Error(`memory control plane: ${ledger} ledger rebuilt after corruption (reason: ${reason}). ` +
|
|
238
|
+
`The corrupt bytes are quarantined at ${receipt.quarantinedTo.join(", ") || "(no file existed)"} — ` +
|
|
239
|
+
`they are the only remaining record of what this account held.`);
|
|
240
|
+
err.code = "memory.control_plane_rebuilt";
|
|
241
|
+
sink(err);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return receipt;
|
|
247
|
+
}
|
|
192
248
|
challengeEntries(ids, reason, requestId) {
|
|
193
249
|
if (typeof requestId !== "string" || requestId === "") {
|
|
194
250
|
const e = new Error("challengeEntries: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
|
|
@@ -207,7 +263,10 @@ export class MemoryEngine {
|
|
|
207
263
|
return this.challengeAndAnnounce(contributions.map((c) => ({ eventId: `${requestId}:${c.entryId}`, entryId: c.entryId, reason, challengedRev: c.lastRev })));
|
|
208
264
|
}
|
|
209
265
|
resolveChallenge(entryId, generation, reason, requestId) {
|
|
210
|
-
|
|
266
|
+
const { resolved, eventCount } = resolveChallengeEvent(this.controlDir, entryId, generation, reason, this.now, requestId);
|
|
267
|
+
if (eventCount !== undefined)
|
|
268
|
+
this.discloseChallengeLedgerSize(eventCount);
|
|
269
|
+
return resolved;
|
|
211
270
|
}
|
|
212
271
|
adjudicatePendingLineage(txnId, action) {
|
|
213
272
|
const promoted = adjudicateLineagePending(this.controlDir, txnId, action, this.now);
|
|
@@ -2,7 +2,7 @@ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION
|
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type MemoryGetDetails, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH, type ScannedEntryFile } from "./file-backend.js";
|
|
5
|
-
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
|
|
5
|
+
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAppendResult, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, } from "./layout.js";
|
|
6
6
|
export { readV2HeaderHints, type V2HeaderHints } from "./header-hints.js";
|
|
7
7
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, type ParsedEntryFile } from "./frontmatter.js";
|
|
8
8
|
export type { MemoryBackend, MemoryEntry, MemoryEntryFrontmatter, MemoryEntryHeader, ScoredMemoryEntry, NotePatch, PatchReport, MaterializedFile, MemorySessionHandle, HarvestReport, HarvestRejection, HarvestRejectionCode, MemoryAnnouncement, ScanFinding, } from "./types.js";
|
|
@@ -2,7 +2,7 @@ export { MemoryEngine, buildMemoryInstruction, truncateIndex, MEMORY_INSTRUCTION
|
|
|
2
2
|
export { MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, } from "./tools.js";
|
|
3
3
|
export { scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE } from "./scan.js";
|
|
4
4
|
export { FileMemoryEngineBackend, scanEntryFiles, MEMORY_INDEX_FILENAME, DEFAULT_MAX_ENTRY_DEPTH } from "./file-backend.js";
|
|
5
|
-
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, } from "./layout.js";
|
|
5
|
+
export { ControlPlaneCorruptError, deriveControlPlaneDir, deriveRepoControlPlaneDir, deriveRepoKey, deriveRepoMemoryDir, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, claimRootScope, rootScopeOf, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, } from "./layout.js";
|
|
6
6
|
export { readV2HeaderHints } from "./header-hints.js";
|
|
7
7
|
export { parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile } from "./frontmatter.js";
|
|
8
8
|
export { memoryBackendContract, assertMemoryBackendSearchEquivalence, } from "./memory-backend-contract.js";
|
|
@@ -402,6 +402,33 @@ export interface ChallengeAssignment {
|
|
|
402
402
|
at: number;
|
|
403
403
|
replayed: boolean;
|
|
404
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* design/180 §10 — the challenge ledger's SIZE CALIBRATION.
|
|
407
|
+
*
|
|
408
|
+
* The ledger is append-only and cannot be rotated: the exclusion predicate is DERIVED from the events
|
|
409
|
+
* ("a challenge generation with no matching resolve"), so dropping the oldest rows would silently
|
|
410
|
+
* un-withhold their entries — taint laundering, the exact failure the fail-closed family exists to
|
|
411
|
+
* prevent. The bound is therefore a DISCLOSURE threshold, not a cap: crossing it changes nothing about
|
|
412
|
+
* what the ledger holds, it tells the deployment the account needs attention.
|
|
413
|
+
*
|
|
414
|
+
* Calibrated against what actually degrades, which is not disk: every append REWRITES the whole file
|
|
415
|
+
* under the strict lock, and every model-visible read path ({@link challengedEntryIds}, called on each
|
|
416
|
+
* index rebuild and each memory tool call) PARSES it whole. At ~200 bytes per event, 10k events is a
|
|
417
|
+
* ~2 MB parse on a read path that runs per tool call — the point where "an audit account nobody looks
|
|
418
|
+
* at" becomes a latency the session feels. A deployment reaching it should resolve the settled
|
|
419
|
+
* generations (they stay in the file, and that is the point) or archive the account deliberately.
|
|
420
|
+
*/
|
|
421
|
+
export declare const CHALLENGE_LEDGER_MAX_EVENTS = 10000;
|
|
422
|
+
/** What {@link appendChallengeEvents} returns: the per-request assignments plus the ledger's size
|
|
423
|
+
* AFTER the append — the caller owns disclosure (this module has no sink), and reading the size here
|
|
424
|
+
* costs nothing because the record is already in hand under the lock. */
|
|
425
|
+
export interface ChallengeAppendResult {
|
|
426
|
+
assignments: ChallengeAssignment[];
|
|
427
|
+
/** Total events in the ledger after this append, or `undefined` when nothing was appended — the
|
|
428
|
+
* empty-request fast path takes no lock and reads nothing, so it has no size to report and must not
|
|
429
|
+
* report a made-up `0` (a caller comparing that against the bound would read "tiny", not "unknown"). */
|
|
430
|
+
eventCount: number | undefined;
|
|
431
|
+
}
|
|
405
432
|
/** Append S-grade challenge events (idempotent per eventId, generation allocation inside the strict
|
|
406
433
|
* lock). Every NEW eventId opens a NEW generation for its entry — a resolve of an earlier
|
|
407
434
|
* generation never immunizes the entry against later pollution (design/180 R-8). */
|
|
@@ -410,11 +437,20 @@ export declare function appendChallengeEvents(controlDir: string, events: Readon
|
|
|
410
437
|
entryId: string;
|
|
411
438
|
reason: string;
|
|
412
439
|
challengedRev?: string;
|
|
413
|
-
}>, now: () => number):
|
|
440
|
+
}>, now: () => number): ChallengeAppendResult;
|
|
414
441
|
/** The trusted resolve: closes EXACTLY the named generation (a later challenge's generation stays
|
|
415
442
|
* open — resolves never blanket-clear). Idempotent: resolving an already-resolved generation is a
|
|
416
|
-
* no-op.
|
|
417
|
-
|
|
443
|
+
* no-op. `resolved` is false ⇔ no such open generation existed.
|
|
444
|
+
*
|
|
445
|
+
* `eventCount` rides along for the same reason it does on {@link appendChallengeEvents}: a resolve
|
|
446
|
+
* APPENDS an event too, so it crosses the size bound exactly as a challenge does (independent
|
|
447
|
+
* adversarial review, verified — hanging the disclosure off the challenge writer alone leaves a ledger
|
|
448
|
+
* that crossed the bound on a resolve silent until the next challenge, which may never come).
|
|
449
|
+
* `undefined` when nothing was appended. */
|
|
450
|
+
export declare function resolveChallengeEvent(controlDir: string, entryId: string, generation: number, reason: string, now: () => number, requestId?: string): {
|
|
451
|
+
resolved: boolean;
|
|
452
|
+
eventCount: number | undefined;
|
|
453
|
+
};
|
|
418
454
|
/** The exclusion predicate's input: entry id → its LATEST unresolved challenge generation. An entry
|
|
419
455
|
* appears iff at least one challenge generation has no matching resolve. Throws on corruption —
|
|
420
456
|
* consumers refuse service (integrity unknown ≠ clean). */
|
|
@@ -424,6 +460,36 @@ export declare function challengedEntryIds(controlDir: string): Map<string, {
|
|
|
424
460
|
}>;
|
|
425
461
|
/** Full event read (host audit / tests). Throws on corruption. */
|
|
426
462
|
export declare function readChallengeEvents(controlDir: string): ChallengeEvent[];
|
|
463
|
+
/** Which fail-closed control-plane ledger a rebuild targets. */
|
|
464
|
+
export type StrictControlPlaneLedger = "lineage" | "challenges";
|
|
465
|
+
/** What one rebuild did — the deployment's record of a taint-bearing account it chose to reset. */
|
|
466
|
+
export interface ControlPlaneRebuildReceipt {
|
|
467
|
+
ledger: StrictControlPlaneLedger;
|
|
468
|
+
/** The corrupt file (and its journal, when one existed) moved aside — never deleted. */
|
|
469
|
+
quarantinedTo: string[];
|
|
470
|
+
at: number;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* design/180 §10 — rebuild a CORRUPT fail-closed ledger, explicitly.
|
|
474
|
+
*
|
|
475
|
+
* The fail-closed family refuses service on corruption precisely because a silent "start fresh" would
|
|
476
|
+
* LAUNDER taint (a challenged entry re-enters the prompt; a polluted session's contributions become
|
|
477
|
+
* untraceable). That refusal is permanent by design — which left a deployment whose disk really did
|
|
478
|
+
* corrupt with no way back other than deleting engine files by hand. This is that way back, and its
|
|
479
|
+
* whole shape is "never automatic":
|
|
480
|
+
* · the caller names the ledger — nothing in the engine calls this on its own;
|
|
481
|
+
* · a HEALTHY ledger is REFUSED (`memory.control_plane_not_corrupt`) — a rebuild is only ever the
|
|
482
|
+
* answer to a corruption that is already blocking every read, never a way to clear an account;
|
|
483
|
+
* · the corrupt bytes are QUARANTINED beside the file, never deleted: what the ledger held is
|
|
484
|
+
* exactly the evidence someone will want after a laundering decision;
|
|
485
|
+
* · the caller (the engine's host API) announces the rebuild — a reset of this account changes what
|
|
486
|
+
* the model may see, so it is not a silent repair.
|
|
487
|
+
*/
|
|
488
|
+
/** Lock-less probe: does this fail-closed ledger currently REFUSE reads? The rebuild's own check runs
|
|
489
|
+
* again under the lock (this one is for a caller that must decide something — announce, refuse — before
|
|
490
|
+
* taking the lock). `false` for a ledger that reads cleanly, including an absent one (genuinely empty). */
|
|
491
|
+
export declare function isStrictControlPlaneLedgerCorrupt(controlDir: string, ledger: StrictControlPlaneLedger): boolean;
|
|
492
|
+
export declare function rebuildStrictControlPlaneLedger(controlDir: string, ledger: StrictControlPlaneLedger, now: () => number): ControlPlaneRebuildReceipt;
|
|
427
493
|
/** One challenged-history row: how often a retrieved-then-corrected event hit this entry. */
|
|
428
494
|
export interface ChallengedHistoryRow {
|
|
429
495
|
count: number;
|