@sema-agent/core 5.36.0 → 5.38.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 +124 -0
- package/dist/agents/subagent.d.ts +10 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +3 -0
- package/dist/agents/team.d.ts +7 -1
- package/dist/agents/team.js +11 -9
- package/dist/agents/verify.js +3 -0
- package/dist/core/auto-mode-prompt-assets.d.ts +5 -3
- package/dist/core/auto-mode-prompt-assets.js +1 -1
- package/dist/core/checkpoint-store.d.ts +26 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +129 -2
- package/dist/core/hooks.js +20 -3
- package/dist/core/memory-engine/engine.d.ts +142 -0
- package/dist/core/memory-engine/engine.js +264 -2
- package/dist/core/memory-engine/file-backend.d.ts +490 -16
- package/dist/core/memory-engine/file-backend.js +1099 -36
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/layout.d.ts +42 -2
- package/dist/core/memory-engine/layout.js +76 -12
- package/dist/core/memory-engine/memory-backend-contract.d.ts +13 -0
- package/dist/core/memory-engine/memory-backend-contract.js +89 -0
- package/dist/core/protocol-table.d.ts +4 -4
- package/dist/core/runner/assemble-result.d.ts +5 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-config-doors.d.ts +17 -0
- package/dist/core/runner/prepare-config-doors.js +33 -2
- package/dist/core/runner/prepare-memory.d.ts +11 -1
- package/dist/core/runner/prepare-memory.js +48 -2
- package/dist/core/runner/prepare-task.d.ts +22 -2
- package/dist/core/runner/prepare-task.js +125 -42
- package/dist/core/runner/runtask.js +50 -11
- package/dist/core/tool-model-gate.d.ts +125 -0
- package/dist/core/tool-model-gate.js +303 -0
- package/dist/core/tool-policy.d.ts +1 -1
- package/dist/core/types.d.ts +284 -1
- package/dist/core/types.js +21 -0
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/orchestration/builtin-workflows.d.ts +68 -6
- package/dist/orchestration/builtin-workflows.js +26 -9
- package/dist/orchestration/run-workflow-tool.d.ts +10 -1
- package/dist/orchestration/run-workflow-tool.js +70 -27
- package/dist/orchestration/workflow-script-store.d.ts +8 -3
- package/dist/prompts/coordinator.d.ts +4 -1
- package/dist/prompts/coordinator.js +8 -0
- package/dist/prompts/default.d.ts +14 -4
- package/dist/prompts/default.js +2 -1
- package/dist/scenarios/full-body.d.ts +5 -0
- package/dist/scenarios/full-body.js +8 -4
- package/dist/tools/fs/fs-shared.d.ts +3 -2
- package/dist/tools/fs/fs-shared.js +19 -9
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +24 -1
package/dist/core/hooks.js
CHANGED
|
@@ -78,6 +78,18 @@ export function createHookEnvCapabilities(env) {
|
|
|
78
78
|
}
|
|
79
79
|
return Object.freeze(face);
|
|
80
80
|
}
|
|
81
|
+
const IDENTITY_AGENT_NAME_MAX = 80;
|
|
82
|
+
export function mintHookInvocationIdentity(facts) {
|
|
83
|
+
return Object.freeze(Object.assign(Object.create(null), {
|
|
84
|
+
sessionId: facts.sessionId,
|
|
85
|
+
taskId: facts.taskId,
|
|
86
|
+
legKind: facts.legKind,
|
|
87
|
+
isDelegatedChild: facts.isDelegatedChild,
|
|
88
|
+
...(facts.insideFork === true ? { insideFork: true } : {}),
|
|
89
|
+
...(facts.agentName !== undefined ? { agentName: inlineUntrusted(facts.agentName.slice(0, 320), IDENTITY_AGENT_NAME_MAX) } : {}),
|
|
90
|
+
...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
81
93
|
export function formatHookFeedback(text) {
|
|
82
94
|
return `<system-reminder>\n${text}\n</system-reminder>`;
|
|
83
95
|
}
|
|
@@ -270,7 +282,12 @@ export function persistedRuleMandateOf(marks) {
|
|
|
270
282
|
export async function runToolGate(input) {
|
|
271
283
|
const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
|
|
272
284
|
const { toolCallId, toolName } = event;
|
|
273
|
-
const hookCtx = () => ({
|
|
285
|
+
const hookCtx = () => ({
|
|
286
|
+
toolCallId,
|
|
287
|
+
toolName,
|
|
288
|
+
...(input.hookEnv !== undefined ? { env: input.hookEnv } : {}),
|
|
289
|
+
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
290
|
+
});
|
|
274
291
|
let currentInput = event.input;
|
|
275
292
|
const preToolContext = [];
|
|
276
293
|
let hookAsk;
|
|
@@ -285,7 +302,7 @@ export async function runToolGate(input) {
|
|
|
285
302
|
const reason = preToolUseCrashReason(`this call to "${toolName}"`, err);
|
|
286
303
|
traceHookCrash(input, err, notifier);
|
|
287
304
|
if (input.permissionDenied) {
|
|
288
|
-
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook" }), "toolGate.permissionDenied");
|
|
305
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
289
306
|
}
|
|
290
307
|
return { block: true, reason: formatHookFeedback(reason), preToolContext };
|
|
291
308
|
}
|
|
@@ -741,7 +758,7 @@ export async function runToolGate(input) {
|
|
|
741
758
|
currentInput = decision.updatedInput;
|
|
742
759
|
}
|
|
743
760
|
if (input.permissionDenied) {
|
|
744
|
-
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource }), "toolGate.permissionDenied");
|
|
761
|
+
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason: denyReason, source: denySource, ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
745
762
|
}
|
|
746
763
|
const denySettledBy = decision.settledBy;
|
|
747
764
|
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CommittedBinding, type EraseMemoryEntriesInput, type MemoryErasureAttestation, type TransferEvidence } from "./file-backend.js";
|
|
1
2
|
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
3
|
import type { HarvestReport, MemoryAnnouncement, MemoryBackend, MemorySessionHandle, ScanFinding } from "./types.js";
|
|
3
4
|
/**
|
|
@@ -187,6 +188,72 @@ export interface MemoryInjection {
|
|
|
187
188
|
content: string;
|
|
188
189
|
};
|
|
189
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* design/178 v2-a §1.3 — one entry's ASSEMBLED provenance account: the machine-readable answer to
|
|
193
|
+
* "who wrote this in, what state is it in, has it been moved" for an auditor holding an entry id.
|
|
194
|
+
* ASSEMBLY, not a second account: every field is read from an authority that already exists
|
|
195
|
+
* (lineage/challenge/pollution = the engine's control plane; binding/content/custody = the
|
|
196
|
+
* backend's committed account + evidence chain) — a "provenance store" would be a drift surface.
|
|
197
|
+
*
|
|
198
|
+
* Field law:
|
|
199
|
+
* - `v` — version envelope: a consumer reading `v > 1` must refuse, never reinterpret.
|
|
200
|
+
* - `binding` — the committed-account row state, FOUR states without collapse: `bound`/`unbound`
|
|
201
|
+
* ({@link CommittedBinding}, shared tagged form) / `absent` (no row) / `unknown` (the backend
|
|
202
|
+
* has no audit-snapshot capability — "cannot judge" is never folded into "does not exist").
|
|
203
|
+
* - `contributors` — the committed lineage BY-ENTRY projection, each row joined with its
|
|
204
|
+
* session's pollution record where one exists. Pending (unsettled) lineage rows are never
|
|
205
|
+
* contributions; they surface as the `lineage_pending` exclusion instead.
|
|
206
|
+
* - `exclusion` — the model-visible read faces' withholding state for this id, when any.
|
|
207
|
+
* - `contentState` — the content half, LINKED to `binding` by construction: `absent` ⇔ binding
|
|
208
|
+
* `absent`; `capability-absent` ⇔ binding `unknown`; a row answers `present` (committed content
|
|
209
|
+
* servable; only then may `ingest` ride) or `unavailable` (row facts answered, no committed
|
|
210
|
+
* carrier — "cannot read it" stays distinct from "does not exist").
|
|
211
|
+
* - `ingest` — the typed repo-file ingest provenance carried by the committed content's
|
|
212
|
+
* frontmatter (`provenance` + the un-whitewashable `trust` marker), present only with
|
|
213
|
+
* `contentState: "present"`.
|
|
214
|
+
* - `custody` — the per-id evidence-chain read (`transfers.jsonl`), `damaged` = the chain's
|
|
215
|
+
* integrity is impeached while a sound reading still answers (torn tail / evidence loss /
|
|
216
|
+
* ev-identity contradiction — `reason` carries the detail when one is known; see
|
|
217
|
+
* {@link EntryCustodyReport}), `capability-absent` = the backend has no custody face (events
|
|
218
|
+
* then empty — never fabricated).
|
|
219
|
+
*/
|
|
220
|
+
export interface EntryProvenanceAccount {
|
|
221
|
+
v: 1;
|
|
222
|
+
id: string;
|
|
223
|
+
binding: CommittedBinding | {
|
|
224
|
+
state: "absent";
|
|
225
|
+
} | {
|
|
226
|
+
state: "unknown";
|
|
227
|
+
reason: "audit-snapshot-capability-absent";
|
|
228
|
+
};
|
|
229
|
+
contributors: Array<{
|
|
230
|
+
sessionId: string;
|
|
231
|
+
lastRev: string;
|
|
232
|
+
lastAt?: number;
|
|
233
|
+
polluted?: {
|
|
234
|
+
at: number;
|
|
235
|
+
reason: string;
|
|
236
|
+
};
|
|
237
|
+
}>;
|
|
238
|
+
exclusion?: {
|
|
239
|
+
code: "challenged" | "lineage_pending";
|
|
240
|
+
generation?: number;
|
|
241
|
+
at?: number;
|
|
242
|
+
};
|
|
243
|
+
contentState: "present" | "unavailable" | "capability-absent" | "absent";
|
|
244
|
+
ingest?: {
|
|
245
|
+
kind: "repo_file";
|
|
246
|
+
path: string;
|
|
247
|
+
contentHash: string;
|
|
248
|
+
ingestedAt: number;
|
|
249
|
+
trust?: "untrusted";
|
|
250
|
+
};
|
|
251
|
+
custody: {
|
|
252
|
+
state: "complete" | "damaged" | "capability-absent";
|
|
253
|
+
events: TransferEvidence[];
|
|
254
|
+
reason?: string;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
190
257
|
export declare class MemoryEngine {
|
|
191
258
|
private readonly backend;
|
|
192
259
|
private readonly memoryDir;
|
|
@@ -296,6 +363,81 @@ export declare class MemoryEngine {
|
|
|
296
363
|
* so one batched request stays per-entry attributable.
|
|
297
364
|
*/
|
|
298
365
|
challengeEntries(ids: readonly string[], reason: string, requestId: string): ChallengeAssignment[];
|
|
366
|
+
/**
|
|
367
|
+
* design/178 v2-b §3.3(b) — the HOST erasure API: explicit, named, evidenced, loud. This is a
|
|
368
|
+
* host-plane DECLARED deletion, not a second deletion throat: it executes through the same
|
|
369
|
+
* `op:"delete"` lane every deletion takes, and the model-visible rule is untouched (a missing
|
|
370
|
+
* file is still never a delete; the frontmatter tombstone is still the only model-side channel).
|
|
371
|
+
* The mass-deletion fuse guards the ACCIDENT shape (files silently missing at harvest) and does
|
|
372
|
+
* not apply here — an explicitly authorized erasure self-discloses by enumerating every id in
|
|
373
|
+
* its attestation, and each selector form is bounded (there is no whole-store selector).
|
|
374
|
+
*
|
|
375
|
+
* Thin membrane by design (§7.4 r1): with the backend's evidence capability present
|
|
376
|
+
* (`eraseWithEvidence` — File implements it), validation is the ONLY thing that happens outside
|
|
377
|
+
* the store's transaction lock; resolution, lineage capture, planning, and evidence all run
|
|
378
|
+
* inside it. Capability ABSENT ⇒ loud refusal (`memory.erasure_evidence_unavailable`) unless
|
|
379
|
+
* `allowUnevidenced: true` opts into the DEGRADED lane, whose contract is deliberately weaker
|
|
380
|
+
* and stated (§5): same requestId = a NEW request (re-resolved — replay convergence is not
|
|
381
|
+
* promised), selector reuse is undetectable (no anchor to compare), `erasedPreviously` and
|
|
382
|
+
* `evidenceEv` never appear, every notFound row carries `historyUnknown` ("never existed" vs
|
|
383
|
+
* "deleted without evidence" is not decidable), resolution is the contract read faces'
|
|
384
|
+
* best-effort set (shadow-only rows invisible), execution is per-id CAS'd delete patches
|
|
385
|
+
* (conflicts reported, replay converges), and the attestation carries
|
|
386
|
+
* `evidenceCapability: "none"` + `custodyState: "capability-absent"` throughout — a silent
|
|
387
|
+
* downgrade is the forbidden shape. Residual enumeration is unavailable on the degraded lane
|
|
388
|
+
* (no quarantine face): its `quarantineHits`/`quarantineOpaque` answer empty/zero as
|
|
389
|
+
* "unenumerable", never as a verified absence. Applied deletes close their lineage rows (the
|
|
390
|
+
* same space hygiene the organic tombstone path takes).
|
|
391
|
+
*
|
|
392
|
+
* BOTH lanes finish with the derived-index sweep ({@link sweepErasedIndexLines}): the erased
|
|
393
|
+
* entries' MEMORY.md pointer lines — name + description, the model-visible distillation — are
|
|
394
|
+
* cleared from disk in the same call, and a sweep failure is disclosed on
|
|
395
|
+
* `residuals.indexUncleared`, never silently absorbed into a clean residual set.
|
|
396
|
+
*/
|
|
397
|
+
eraseMemoryEntries(input: EraseMemoryEntriesInput): Promise<MemoryErasureAttestation>;
|
|
398
|
+
/**
|
|
399
|
+
* design/178 v2-b (rescan hardening) — the erase-time DERIVED-INDEX sweep, both lanes. MEMORY.md
|
|
400
|
+
* is the engine's model-visible distillation of entry frontmatter (name + description — routinely
|
|
401
|
+
* a restatement of exactly what an erasure request targets), and it is a derived projection the
|
|
402
|
+
* BACKEND knows nothing about: the store lane deletes rows/projections/shadows while the index
|
|
403
|
+
* line survives on disk (git working tree included) and rides the next injection that prefers the
|
|
404
|
+
* live on-disk index. The only other cleaner is the next materialize's rebuild — which a one-off
|
|
405
|
+
* compliance erasure cannot count on — so the erase itself clears the pointer lines.
|
|
406
|
+
*
|
|
407
|
+
* LINE-TARGETED, never a full rebuild (no session handle exists here; prose/headers and every
|
|
408
|
+
* unrelated line stay verbatim): a line is dropped iff its link target resolves to an erased
|
|
409
|
+
* row's BOUND projection path. Failures are DISCLOSED on the attestation's
|
|
410
|
+
* `residuals.indexUncleared` seat, never silent (坏值响亮度): a non-file at the index path, a
|
|
411
|
+
* non-ENOENT read failure, or a refused write leaves the path listed — the caller archiving the
|
|
412
|
+
* attestation knows the model-visible index may still name the erased entry. Honest bounds,
|
|
413
|
+
* stated: an UNBOUND erased row has no projection address to match (its dangling line — if a
|
|
414
|
+
* model ever wrote one — falls to the rebuild's orphan clearing); a session handle minted
|
|
415
|
+
* BEFORE the erase still carries its materialize-time index text (a handle is a snapshot; the
|
|
416
|
+
* next materialize re-derives); a REPO-INGESTED entry's index line targets the repo source file
|
|
417
|
+
* (recorded at materialize), which is not derivable from the deleted row — that line is a
|
|
418
|
+
* pointer to a still-existing user-owned file (L8 keeps it; the repo bytes were never the
|
|
419
|
+
* store's to erase — `propagation: "local-store-only"`); and the write is the same unlocked
|
|
420
|
+
* read-filter-write every writer performs on this MODEL-EDITABLE plane file (a concurrent
|
|
421
|
+
* session's line landing inside the window is last-writer-wins — the plane's standing posture,
|
|
422
|
+
* not a channel this sweep adds).
|
|
423
|
+
*/
|
|
424
|
+
private sweepErasedIndexLines;
|
|
425
|
+
/**
|
|
426
|
+
* design/178 v2-a §1.3 — the per-entry provenance ANSWER (host audit API; see
|
|
427
|
+
* {@link EntryProvenanceAccount} for the field law). The read is held to the three audit axes:
|
|
428
|
+
* SIDE-EFFECT-FREE (no adoption, no healing, no migration trigger — the backend half goes through
|
|
429
|
+
* the audit-snapshot capability faces, never the adopting/serving read paths), COMMITTED (the
|
|
430
|
+
* account answers, never uncommitted plane bytes), and ACCOUNT-COMPLETE (the backend half is
|
|
431
|
+
* ledger-driven, so a row whose projection vanished out-of-band still answers).
|
|
432
|
+
*
|
|
433
|
+
* Failure posture is the control plane's existing fail-closed law: a corrupt lineage or challenge
|
|
434
|
+
* ledger THROWS (`ControlPlaneCorruptError` — an answer is never assembled over an account whose
|
|
435
|
+
* integrity is unknown), and the backend's own refusals (v1-compat store, contended snapshot
|
|
436
|
+
* fence) propagate. CAPABILITY ABSENCE is not failure: a backend without the audit faces answers
|
|
437
|
+
* `binding: "unknown"` / `contentState: "capability-absent"` / `custody: "capability-absent"` —
|
|
438
|
+
* reported honestly, never fabricated and never a throw (#196 absence-reports-not-silent-green).
|
|
439
|
+
*/
|
|
440
|
+
provenanceOf(entryId: string): Promise<EntryProvenanceAccount>;
|
|
299
441
|
/** Host API: challenge every entry the lineage ledger attributes to `sessionId` (post-hoc source
|
|
300
442
|
* falsification — trustedTools misconfigured, a tool re-classified, late delegation evidence).
|
|
301
443
|
* Same requestId contract as {@link challengeEntries}. */
|
|
@@ -5,8 +5,8 @@ import { MAX_MEMORY_BYTES, composeMemoryBlock, firstSentence } from "../memory.j
|
|
|
5
5
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
6
6
|
import { formatMemoryAge } from "../memory-recall.js";
|
|
7
7
|
import { computeEntryRev, parseEntryFile, serializeEntryFile } from "./frontmatter.js";
|
|
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, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, resolveChallengeEvent, stageLineagePending, } from "./layout.js";
|
|
8
|
+
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, 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, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, 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
|
|
|
@@ -259,6 +259,268 @@ export class MemoryEngine {
|
|
|
259
259
|
}
|
|
260
260
|
return this.challengeAndAnnounce(ids.map((entryId) => ({ eventId: `${requestId}:${entryId}`, entryId, reason })));
|
|
261
261
|
}
|
|
262
|
+
async eraseMemoryEntries(input) {
|
|
263
|
+
const captured = captureErasureInput(input);
|
|
264
|
+
const bad = erasureRequestInvalid(captured);
|
|
265
|
+
if (bad !== undefined) {
|
|
266
|
+
const e = new Error(`eraseMemoryEntries: ${bad}`);
|
|
267
|
+
e.code = "config.memory_erasure_request";
|
|
268
|
+
throw e;
|
|
269
|
+
}
|
|
270
|
+
const requestId = captured.requestId;
|
|
271
|
+
const face = this.backend.eraseWithEvidence;
|
|
272
|
+
if (typeof face === "function") {
|
|
273
|
+
const att = await face.call(this.backend, captured);
|
|
274
|
+
this.sweepErasedIndexLines(att);
|
|
275
|
+
return att;
|
|
276
|
+
}
|
|
277
|
+
if (captured.allowUnevidenced !== true) {
|
|
278
|
+
const e = new Error("eraseMemoryEntries: this backend has no erasure-evidence capability (eraseWithEvidence) — an unevidenced erasure is refused by default. " +
|
|
279
|
+
"Pass allowUnevidenced: true to run the documented degraded lane (no replay convergence, no selector-reuse detection, no erasure history, best-effort resolution).");
|
|
280
|
+
e.code = "memory.erasure_evidence_unavailable";
|
|
281
|
+
throw e;
|
|
282
|
+
}
|
|
283
|
+
const at = this.now();
|
|
284
|
+
const select = captured.select;
|
|
285
|
+
let resolvedIds;
|
|
286
|
+
if ("ids" in select) {
|
|
287
|
+
resolvedIds = [...select.ids];
|
|
288
|
+
}
|
|
289
|
+
else if ("scope" in select) {
|
|
290
|
+
const headers = await this.backend.listHeaders([select.scope]);
|
|
291
|
+
resolvedIds = [...new Set(headers.map((h) => h.id))].sort();
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
resolvedIds = lineageContributionsOfSession(this.controlDir, select.sessionId)
|
|
295
|
+
.map((c) => c.entryId)
|
|
296
|
+
.sort();
|
|
297
|
+
}
|
|
298
|
+
const present = await this.backend.getByIds(resolvedIds);
|
|
299
|
+
const byId = new Map(present.map((e) => [e.id, e]));
|
|
300
|
+
const patches = [...byId.values()].map((e) => ({ op: "delete", id: e.id, baseRev: e.rev }));
|
|
301
|
+
const report = patches.length > 0 ? await this.backend.applyPatches(patches) : { applied: [], conflicts: [] };
|
|
302
|
+
const appliedDeletes = new Set(report.applied.filter((a) => a.op === "delete").map((a) => a.id));
|
|
303
|
+
if (appliedDeletes.size > 0)
|
|
304
|
+
clearLineageForEntries(this.controlDir, [...appliedDeletes]);
|
|
305
|
+
const erased = [...appliedDeletes].map((id) => {
|
|
306
|
+
const e = byId.get(id);
|
|
307
|
+
return {
|
|
308
|
+
id,
|
|
309
|
+
rev: e?.rev ?? "",
|
|
310
|
+
binding: e !== undefined ? { state: "bound", scope: e.scope, slug: e.slug } : { state: "unbound" },
|
|
311
|
+
projectionsRemoved: 1,
|
|
312
|
+
sessions: [],
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
const notFound = resolvedIds.filter((id) => !byId.has(id)).map((id) => ({ id, historyUnknown: true }));
|
|
316
|
+
const conflicts = report.conflicts.map((c) => ({ id: c.id, reason: c.reason }));
|
|
317
|
+
const status = conflicts.length === 0 && resolvedIds.every((id) => appliedDeletes.has(id)) ? "complete" : "partial";
|
|
318
|
+
const att = {
|
|
319
|
+
v: 1,
|
|
320
|
+
requestId,
|
|
321
|
+
at,
|
|
322
|
+
status,
|
|
323
|
+
evidenceCapability: "none",
|
|
324
|
+
custodyState: "capability-absent",
|
|
325
|
+
select,
|
|
326
|
+
selectHash: erasureSelectHash(select),
|
|
327
|
+
resolvedIds,
|
|
328
|
+
erased,
|
|
329
|
+
notFound,
|
|
330
|
+
conflicts,
|
|
331
|
+
residuals: { quarantineHits: [], quarantineOpaque: 0, propagation: "local-store-only" },
|
|
332
|
+
};
|
|
333
|
+
this.sweepErasedIndexLines(att);
|
|
334
|
+
return att;
|
|
335
|
+
}
|
|
336
|
+
sweepErasedIndexLines(att) {
|
|
337
|
+
const uncleared = [];
|
|
338
|
+
try {
|
|
339
|
+
const erasedPaths = new Set();
|
|
340
|
+
const boundScopes = new Set();
|
|
341
|
+
for (const row of att.erased) {
|
|
342
|
+
if (row.binding.state !== "bound")
|
|
343
|
+
continue;
|
|
344
|
+
boundScopes.add(row.binding.scope);
|
|
345
|
+
erasedPaths.add(canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, row.binding.scope), `${row.binding.slug}.md`)));
|
|
346
|
+
}
|
|
347
|
+
for (const row of att.erasedPreviously ?? []) {
|
|
348
|
+
if (row.from === undefined)
|
|
349
|
+
continue;
|
|
350
|
+
boundScopes.add(row.from.scope);
|
|
351
|
+
erasedPaths.add(canonicalize(join(scopeDirFor(this.memoryDir, this.controlDir, row.from.scope), `${row.from.slug}.md`)));
|
|
352
|
+
}
|
|
353
|
+
if (erasedPaths.size === 0)
|
|
354
|
+
return;
|
|
355
|
+
const dirs = new Set([this.memoryDir]);
|
|
356
|
+
for (const scope of Object.keys(registeredScopes(this.controlDir)))
|
|
357
|
+
dirs.add(scopeDirFor(this.memoryDir, this.controlDir, scope));
|
|
358
|
+
for (const scope of boundScopes)
|
|
359
|
+
dirs.add(scopeDirFor(this.memoryDir, this.controlDir, scope));
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
for (const dir of dirs) {
|
|
362
|
+
const indexPath = join(dir, MEMORY_INDEX_FILENAME);
|
|
363
|
+
const canon = canonicalize(indexPath);
|
|
364
|
+
if (seen.has(canon))
|
|
365
|
+
continue;
|
|
366
|
+
seen.add(canon);
|
|
367
|
+
let isFile;
|
|
368
|
+
try {
|
|
369
|
+
isFile = lstatSync(indexPath).isFile();
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
if (err.code === "ENOENT")
|
|
373
|
+
continue;
|
|
374
|
+
uncleared.push(indexPath);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!isFile) {
|
|
378
|
+
uncleared.push(indexPath);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const text = readNoFollowSafe(indexPath);
|
|
382
|
+
if (text === undefined) {
|
|
383
|
+
uncleared.push(indexPath);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const lines = text.split("\n");
|
|
387
|
+
const kept = lines.filter((line) => {
|
|
388
|
+
const target = indexLineTarget(line);
|
|
389
|
+
if (target === undefined)
|
|
390
|
+
return true;
|
|
391
|
+
return !erasedPaths.has(canonicalize(join(dir, target)));
|
|
392
|
+
});
|
|
393
|
+
if (kept.length === lines.length)
|
|
394
|
+
continue;
|
|
395
|
+
try {
|
|
396
|
+
writeFileNoFollow(indexPath, kept.join("\n"));
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
uncleared.push(indexPath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
uncleared.push(`${join(this.memoryDir, MEMORY_INDEX_FILENAME)} (index sweep failed: ${err instanceof Error ? err.message : String(err)})`);
|
|
405
|
+
}
|
|
406
|
+
if (uncleared.length > 0) {
|
|
407
|
+
try {
|
|
408
|
+
att.residuals.indexUncleared = [...new Set(uncleared)].sort();
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
try {
|
|
412
|
+
const detail = `memory erasure: the attestation could not carry its index-residue disclosure ` +
|
|
413
|
+
`(${inlineUntrusted(String(err instanceof Error ? err.message : err), 200)}) — MEMORY.md files that may still name erased entries: ` +
|
|
414
|
+
`${uncleared.map((p) => inlineUntrusted(String(p), 300)).join(", ")}`;
|
|
415
|
+
try {
|
|
416
|
+
enqueueMemoryAnnouncement(this.controlDir, { kind: "external", at: this.now(), items: [detail] });
|
|
417
|
+
}
|
|
418
|
+
catch (enqueueErr) {
|
|
419
|
+
this.discloseAnnounceFailure("erasure index-residue enqueue", enqueueErr);
|
|
420
|
+
}
|
|
421
|
+
const sink = this.onIncident;
|
|
422
|
+
if (sink !== undefined) {
|
|
423
|
+
try {
|
|
424
|
+
const e = new Error(detail);
|
|
425
|
+
e.code = "memory.erasure_index_residue";
|
|
426
|
+
sink(e);
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async provenanceOf(entryId) {
|
|
438
|
+
const snapshotFn = this.backend.committedSnapshotOf;
|
|
439
|
+
const custodyFn = this.backend.custodyOf;
|
|
440
|
+
const readEnginePlane = () => {
|
|
441
|
+
const challenged = challengedEntryIds(this.controlDir).get(entryId);
|
|
442
|
+
const lineage = lineageAccountOfEntry(this.controlDir, entryId);
|
|
443
|
+
return {
|
|
444
|
+
contributors: lineage.contributors.map((c) => {
|
|
445
|
+
const polluted = this.sessionPollution(c.sessionId);
|
|
446
|
+
return {
|
|
447
|
+
sessionId: c.sessionId,
|
|
448
|
+
lastRev: c.lastRev,
|
|
449
|
+
lastAt: c.lastAt,
|
|
450
|
+
...(polluted !== undefined ? { polluted: { at: polluted.at, reason: polluted.reason } } : {}),
|
|
451
|
+
};
|
|
452
|
+
}),
|
|
453
|
+
exclusion: challenged !== undefined
|
|
454
|
+
? { code: "challenged", generation: challenged.generation, at: challenged.at }
|
|
455
|
+
: lineage.pendingLatched
|
|
456
|
+
? { code: "lineage_pending" }
|
|
457
|
+
: undefined,
|
|
458
|
+
};
|
|
459
|
+
};
|
|
460
|
+
{
|
|
461
|
+
let snap;
|
|
462
|
+
let custody;
|
|
463
|
+
if (typeof snapshotFn !== "function") {
|
|
464
|
+
custody = typeof custodyFn !== "function" ? { state: "capability-absent", events: [] } : await custodyFn.call(this.backend, entryId);
|
|
465
|
+
}
|
|
466
|
+
else if (typeof custodyFn !== "function") {
|
|
467
|
+
snap = await snapshotFn.call(this.backend, entryId);
|
|
468
|
+
custody = { state: "capability-absent", events: [] };
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
const accountKey = (s) => JSON.stringify(s.state === "row" ? { state: s.state, rev: s.rev, binding: s.binding } : { state: s.state });
|
|
472
|
+
const chainKey = (c) => canonicalJsonStringify(JSON.parse(JSON.stringify([c.state, c.reason ?? null, c.events])));
|
|
473
|
+
snap = await snapshotFn.call(this.backend, entryId);
|
|
474
|
+
custody = await custodyFn.call(this.backend, entryId);
|
|
475
|
+
let stable = false;
|
|
476
|
+
for (let bracket = 0; bracket < 4; bracket++) {
|
|
477
|
+
const snapAgain = await snapshotFn.call(this.backend, entryId);
|
|
478
|
+
if (accountKey(snapAgain) !== accountKey(snap)) {
|
|
479
|
+
snap = snapAgain;
|
|
480
|
+
custody = await custodyFn.call(this.backend, entryId);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
const custodyAgain = await custodyFn.call(this.backend, entryId);
|
|
484
|
+
if (chainKey(custodyAgain) !== chainKey(custody)) {
|
|
485
|
+
custody = custodyAgain;
|
|
486
|
+
snap = await snapshotFn.call(this.backend, entryId);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
stable = true;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
if (!stable) {
|
|
493
|
+
throw new Error(`memory provenance read refused: the committed account for ${JSON.stringify(entryId)} kept moving across ` +
|
|
494
|
+
`every snapshot/custody bracket — the two faces cannot be joined without tearing while the store is this hot; retry later.`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const plane = readEnginePlane();
|
|
498
|
+
let binding;
|
|
499
|
+
let contentState;
|
|
500
|
+
let ingest;
|
|
501
|
+
if (snap === undefined) {
|
|
502
|
+
binding = { state: "unknown", reason: "audit-snapshot-capability-absent" };
|
|
503
|
+
contentState = "capability-absent";
|
|
504
|
+
}
|
|
505
|
+
else if (snap.state === "absent") {
|
|
506
|
+
binding = { state: "absent" };
|
|
507
|
+
contentState = "absent";
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
binding = snap.binding;
|
|
511
|
+
if (snap.content.state === "present") {
|
|
512
|
+
contentState = "present";
|
|
513
|
+
const fm = snap.content.entry.frontmatter;
|
|
514
|
+
if (fm.provenance !== undefined)
|
|
515
|
+
ingest = { ...fm.provenance, ...(fm.trust !== undefined ? { trust: fm.trust } : {}) };
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
contentState = "unavailable";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { v: 1, id: entryId, binding, contributors: plane.contributors, ...(plane.exclusion !== undefined ? { exclusion: plane.exclusion } : {}), contentState, ...(ingest !== undefined ? { ingest } : {}), custody };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
262
524
|
challengeSession(sessionId, reason, requestId) {
|
|
263
525
|
if (typeof requestId !== "string" || requestId === "") {
|
|
264
526
|
const e = new Error("challengeSession: requestId is required (idempotency identity — retries must reuse it; the engine does not mint one)");
|