@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
|
@@ -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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { closeSync, constants as fsConstants, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
1
|
+
import { closeSync, constants as fsConstants, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
2
|
const { O_WRONLY, O_CREAT, O_TRUNC, O_NOFOLLOW } = fsConstants;
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
@@ -984,9 +984,10 @@ function coerceChallenges(raw) {
|
|
|
984
984
|
}
|
|
985
985
|
return raw;
|
|
986
986
|
}
|
|
987
|
+
export const CHALLENGE_LEDGER_MAX_EVENTS = 10_000;
|
|
987
988
|
export function appendChallengeEvents(controlDir, events, now) {
|
|
988
989
|
if (events.length === 0)
|
|
989
|
-
return [];
|
|
990
|
+
return { assignments: [], eventCount: undefined };
|
|
990
991
|
return lockedStrictUpdate(controlDir, CHALLENGES_FILE, "memory challenge ledger", coerceChallenges, (rec) => {
|
|
991
992
|
const byEventId = new Map(rec.events.map((e) => [e.eventId, e]));
|
|
992
993
|
const maxGen = new Map();
|
|
@@ -1020,17 +1021,17 @@ export function appendChallengeEvents(controlDir, events, now) {
|
|
|
1020
1021
|
out.push({ entryId: ev.entryId, eventId: ev.eventId, generation, at: ev.at, replayed: false });
|
|
1021
1022
|
changed = true;
|
|
1022
1023
|
}
|
|
1023
|
-
return { ...(changed ? { next: rec } : {}), result: out };
|
|
1024
|
+
return { ...(changed ? { next: rec } : {}), result: { assignments: out, eventCount: rec.events.length } };
|
|
1024
1025
|
});
|
|
1025
1026
|
}
|
|
1026
1027
|
export function resolveChallengeEvent(controlDir, entryId, generation, reason, now, requestId) {
|
|
1027
1028
|
return lockedStrictUpdate(controlDir, CHALLENGES_FILE, "memory challenge ledger", coerceChallenges, (rec) => {
|
|
1028
1029
|
const opened = rec.events.some((e) => e.kind === "challenge" && e.entryId === entryId && e.generation === generation);
|
|
1029
1030
|
if (!opened)
|
|
1030
|
-
return { result: false };
|
|
1031
|
+
return { result: { resolved: false, eventCount: undefined } };
|
|
1031
1032
|
const alreadyResolved = rec.events.some((e) => e.kind === "resolve" && e.entryId === entryId && e.generation === generation);
|
|
1032
1033
|
if (alreadyResolved)
|
|
1033
|
-
return { result: true };
|
|
1034
|
+
return { result: { resolved: true, eventCount: undefined } };
|
|
1034
1035
|
rec.events.push({
|
|
1035
1036
|
eventId: requestId !== undefined && requestId !== "" ? `${requestId}:${entryId}` : `resolve:${entryId}:${generation}`,
|
|
1036
1037
|
entryId,
|
|
@@ -1039,7 +1040,7 @@ export function resolveChallengeEvent(controlDir, entryId, generation, reason, n
|
|
|
1039
1040
|
at: now(),
|
|
1040
1041
|
reason,
|
|
1041
1042
|
});
|
|
1042
|
-
return { next: rec, result: true };
|
|
1043
|
+
return { next: rec, result: { resolved: true, eventCount: rec.events.length } };
|
|
1043
1044
|
});
|
|
1044
1045
|
}
|
|
1045
1046
|
export function challengedEntryIds(controlDir) {
|
|
@@ -1061,6 +1062,74 @@ export function challengedEntryIds(controlDir) {
|
|
|
1061
1062
|
export function readChallengeEvents(controlDir) {
|
|
1062
1063
|
return coerceChallenges(readStrictSidecar(controlDir, CHALLENGES_FILE, "memory challenge ledger")).events;
|
|
1063
1064
|
}
|
|
1065
|
+
export function isStrictControlPlaneLedgerCorrupt(controlDir, ledger) {
|
|
1066
|
+
const fileName = ledger === "lineage" ? LINEAGE_FILE : CHALLENGES_FILE;
|
|
1067
|
+
const what = ledger === "lineage" ? "memory lineage ledger" : "memory challenge ledger";
|
|
1068
|
+
const coerce = ledger === "lineage" ? coerceLineage : coerceChallenges;
|
|
1069
|
+
try {
|
|
1070
|
+
coerce(readStrictSidecar(controlDir, fileName, what));
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
catch (err) {
|
|
1074
|
+
if (err instanceof ControlPlaneCorruptError)
|
|
1075
|
+
return true;
|
|
1076
|
+
throw err;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
export function rebuildStrictControlPlaneLedger(controlDir, ledger, now) {
|
|
1080
|
+
const fileName = ledger === "lineage" ? LINEAGE_FILE : CHALLENGES_FILE;
|
|
1081
|
+
const what = ledger === "lineage" ? "memory lineage ledger" : "memory challenge ledger";
|
|
1082
|
+
const coerce = ledger === "lineage" ? coerceLineage : coerceChallenges;
|
|
1083
|
+
const empty = ledger === "lineage" ? { version: 1, committed: {}, pending: {} } : { version: 1, events: [] };
|
|
1084
|
+
ensureDirExists(controlDir);
|
|
1085
|
+
const file = join(controlDir, fileName);
|
|
1086
|
+
const journal = `${file}.journal`;
|
|
1087
|
+
const lock = `${file}.lock`;
|
|
1088
|
+
const token = acquireSidecarLock(lock, { onDeadline: "throw" });
|
|
1089
|
+
try {
|
|
1090
|
+
let healthy = false;
|
|
1091
|
+
try {
|
|
1092
|
+
coerce(readStrictSidecar(controlDir, fileName, what));
|
|
1093
|
+
healthy = true;
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
if (!(err instanceof ControlPlaneCorruptError))
|
|
1097
|
+
throw err;
|
|
1098
|
+
}
|
|
1099
|
+
if (healthy) {
|
|
1100
|
+
const e = new Error(`${what} 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.`);
|
|
1101
|
+
e.code = "memory.control_plane_not_corrupt";
|
|
1102
|
+
throw e;
|
|
1103
|
+
}
|
|
1104
|
+
const at = now();
|
|
1105
|
+
const quarantinedTo = [];
|
|
1106
|
+
assertSidecarLockOwnership(lock, token, what);
|
|
1107
|
+
for (const path of [file, journal]) {
|
|
1108
|
+
if (!existsSync(path))
|
|
1109
|
+
continue;
|
|
1110
|
+
let dest = `${path}.corrupt-${at}`;
|
|
1111
|
+
for (let n = 2;; n++) {
|
|
1112
|
+
try {
|
|
1113
|
+
copyFileSync(path, dest, fsConstants.COPYFILE_EXCL);
|
|
1114
|
+
break;
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
if (err.code !== "EEXIST")
|
|
1118
|
+
throw err;
|
|
1119
|
+
dest = `${path}.corrupt-${at}-${n}`;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
quarantinedTo.push(dest);
|
|
1123
|
+
}
|
|
1124
|
+
assertSidecarLockOwnership(lock, token, what);
|
|
1125
|
+
atomicWriteFileSync(file, `${JSON.stringify(empty, null, 2)}\n`);
|
|
1126
|
+
rmSync(journal, { force: true });
|
|
1127
|
+
return { ledger, quarantinedTo, at };
|
|
1128
|
+
}
|
|
1129
|
+
finally {
|
|
1130
|
+
releaseSidecarLock(lock, token);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1064
1133
|
function coerceChallengedHistory(raw) {
|
|
1065
1134
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
1066
1135
|
return {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { parseAllowRuleText, suggestRulesForCommand } from "./permission-rule-model.js";
|
|
2
3
|
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
3
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
@@ -28,7 +29,7 @@ function nowIso(deps) {
|
|
|
28
29
|
function mintId(deps, prefix) {
|
|
29
30
|
if (deps.newId !== undefined)
|
|
30
31
|
return deps.newId();
|
|
31
|
-
return `${prefix}_${Date.now().toString(36)}_${
|
|
32
|
+
return `${prefix}_${Date.now().toString(36)}_${randomBytes(9).toString("base64url")}`;
|
|
32
33
|
}
|
|
33
34
|
function requirePrincipal(principal, entry) {
|
|
34
35
|
if (typeof principal !== "string" || principal === "") {
|
|
@@ -80,8 +80,42 @@ export interface OrgRuleStatePersistence {
|
|
|
80
80
|
/** Duck-typed discovery of the persistence face on a store backend, `writerOf` style. */
|
|
81
81
|
export declare function orgRuleStatePersistenceOf(store: PermissionRuleStore): OrgRuleStatePersistence | undefined;
|
|
82
82
|
/** The `decisionReason` the consuming gate stamps on the synthetic asks it mints while org adjudication
|
|
83
|
-
* is unavailable. Exported so the gate integration and its tests share one spelling
|
|
84
|
-
|
|
83
|
+
* is unavailable. Exported so the gate integration and its tests share one spelling — and TYPED as the
|
|
84
|
+
* member of {@link import("./tool-policy.js").DecisionReason} it must equal, so the constant and the
|
|
85
|
+
* vocabulary cannot drift into two spellings of one word. */
|
|
86
|
+
export declare const ORG_UNAVAILABLE_DECISION_REASON: Extract<import("./tool-policy.js").DecisionReason, "org_unavailable">;
|
|
87
|
+
/**
|
|
88
|
+
* How long a single org adjudication may take before the consuming gate stops waiting and reads the
|
|
89
|
+
* answer as UNAVAILABLE (its fail-closed word).
|
|
90
|
+
*
|
|
91
|
+
* A bound is MANDATORY rather than optional because of where this call sits: on the hot path of every
|
|
92
|
+
* governed tool call, and — on the durable resume leg — after the checkpoint has been consumed and the
|
|
93
|
+
* start frame emitted. A provider that never settles would otherwise wedge the call past any task
|
|
94
|
+
* deadline (tasks carry no walltime by default), leaving the frame unpaired and the approval spent with
|
|
95
|
+
* nothing retryable. A hang is the one outcome worse than either verdict.
|
|
96
|
+
*
|
|
97
|
+
* The value matches the auto-mode classifier's per-round-trip cap — the other model/network call the
|
|
98
|
+
* permission path makes — because the reasoning is the same: a governance lookup that takes longer than
|
|
99
|
+
* this is indistinguishable from an outage, and an outage is exactly what the unavailable arm is for.
|
|
100
|
+
* The task's own abort signal still applies in parallel; whichever fires first ends the wait.
|
|
101
|
+
*/
|
|
102
|
+
export declare const ORG_ADJUDICATION_TIMEOUT_MS = 15000;
|
|
103
|
+
/**
|
|
104
|
+
* Await `p`, but settle with `fallback` if the deadline elapses or the signal fires first.
|
|
105
|
+
*
|
|
106
|
+
* Shared by both org consumption sites so one bound cannot drift into two. Deliberately NOT `.unref()`
|
|
107
|
+
* on the timer: this is a foreground rescue timer, and the caller is awaiting it inside the control
|
|
108
|
+
* flow — an unref'd timer stops firing in exactly the situation it exists for (an otherwise idle loop).
|
|
109
|
+
* A rejecting `p` also lands on the fallback: the callers' fail-closed word is the same either way, and
|
|
110
|
+
* an escaping rejection here would turn a dependency failure into a task crash.
|
|
111
|
+
*/
|
|
112
|
+
export declare function settleOrgVerdictWithin<T>(p: Promise<T>, fallback: T, opts: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
timeoutMs: number;
|
|
115
|
+
}): Promise<T>;
|
|
116
|
+
/** The `decisionReason` of a decision an ORG RULE produced (a deny, or a non-dismissable ask). Same
|
|
117
|
+
* single-spelling contract as {@link ORG_UNAVAILABLE_DECISION_REASON}. */
|
|
118
|
+
export declare const ORG_RULE_DECISION_REASON: Extract<import("./tool-policy.js").DecisionReason, "org_rule">;
|
|
85
119
|
/** How far into the future a snapshot's `fetchedAtMs` may sit before it is refused — ordinary NTP-level
|
|
86
120
|
* clock skew passes; a far-future timestamp (which would satisfy the staleness bound INDEFINITELY,
|
|
87
121
|
* turning fail-closed into evergreen freshness) does not. */
|
|
@@ -7,6 +7,29 @@ export function orgRuleStatePersistenceOf(store) {
|
|
|
7
7
|
: undefined;
|
|
8
8
|
}
|
|
9
9
|
export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
|
|
10
|
+
export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
11
|
+
export function settleOrgVerdictWithin(p, fallback, opts) {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const finish = (v) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
clearTimeout(timer);
|
|
19
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
20
|
+
resolve(v);
|
|
21
|
+
};
|
|
22
|
+
const onAbort = () => finish(fallback);
|
|
23
|
+
const timer = setTimeout(() => finish(fallback), opts.timeoutMs);
|
|
24
|
+
if (opts.signal?.aborted === true) {
|
|
25
|
+
finish(fallback);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
29
|
+
p.then(finish, () => finish(fallback));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export const ORG_RULE_DECISION_REASON = "org_rule";
|
|
10
33
|
export const ORG_FETCHED_AT_SKEW_ALLOWANCE_MS = 5 * 60_000;
|
|
11
34
|
export function createOrgRuleOverlay(cfg) {
|
|
12
35
|
if (cfg.governed !== true) {
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* design/179 §8 — the persisted allow-rule store seam, its
|
|
2
|
+
* design/179 §8 — the persisted allow-rule store seam, its backend write face, and the removal entry.
|
|
3
3
|
*
|
|
4
4
|
* ## Two faces, deliberately unequal
|
|
5
5
|
*
|
|
6
|
-
* A deployment sees a READ face: `list()`, scoped to one verified principal by a factory.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A deployment sees a READ face: `list()`, scoped to one verified principal by a factory. The write face
|
|
7
|
+
* is a BACKEND CONTRACT (ruled 2026-08-10: exported so an out-of-repo store twin builds against the same
|
|
8
|
+
* definitions instead of mirroring them), not a host write API — the only way a rule enters the store
|
|
9
|
+
* through the ENGINE is the redemption path (`permission-rule-consent.ts`), which requires an approved
|
|
10
|
+
* durable approval record; that invariant lives in the engine's wiring and is pinned by the
|
|
11
|
+
* writer-caller registry test, not in type visibility.
|
|
9
12
|
* `expectedRev` is concurrency control, not authorization, so "hold a put API and skip the ticket" is not
|
|
10
13
|
* a shape that exists here rather than a rule someone must remember.
|
|
11
14
|
*
|
|
@@ -156,15 +159,19 @@ export interface RuleSyncJoinDelta {
|
|
|
156
159
|
* What a write may say. Authorization discriminates on the DELTA SHAPE, not on a full snapshot: only the
|
|
157
160
|
* add arm can introduce a dot, and the delete arm carries a tombstone and no adds. A backend additionally
|
|
158
161
|
* REFUSES at runtime any delete that would introduce a new add dot — structure and runtime check together,
|
|
159
|
-
* so "pick the delete arm and smuggle an add" is neither expressible nor accepted. The `sync-join` arm
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* not at the
|
|
162
|
+
* so "pick the delete arm and smuggle an add" is neither expressible nor accepted. The `sync-join` arm's
|
|
163
|
+
* safety does not rest on hiding the type (ruled 2026-08-10: the backend contract IS exported for
|
|
164
|
+
* out-of-repo store twins) — it rests on every inbound record inside it passing the single validator
|
|
165
|
+
* again AT THE BACKEND: the fifth door of design/179 §4's validator list closes here, not at the
|
|
166
|
+
* calling layer, and closes identically for every caller.
|
|
163
167
|
*/
|
|
164
168
|
export type RuleWriteDelta = RuleAddDelta | RuleDeleteDelta | RuleSyncJoinDelta;
|
|
165
169
|
/**
|
|
166
|
-
* The
|
|
167
|
-
*
|
|
170
|
+
* The backend write face. Exported as part of the BACKEND CONTRACT (ruled 2026-08-10) so an
|
|
171
|
+
* out-of-repo store implementation hangs the same face the file backend does, instead of mirroring the
|
|
172
|
+
* types. The consent boundary is unchanged by the export: the ENGINE reaches a writer only through the
|
|
173
|
+
* consent protocol's redemption (and the sync client's join) — a property of the engine's wiring —
|
|
174
|
+
* and a deployment always owned its own storage bytes, so type visibility grants nothing new.
|
|
168
175
|
*/
|
|
169
176
|
export interface PermissionRuleWriter {
|
|
170
177
|
/** Mint the next dot for this replica. Counters need only be unique and monotonic, so a dot minted for
|
|
@@ -198,11 +205,11 @@ export interface RawRuleSyncState {
|
|
|
198
205
|
quarantined?: QuarantinedRuleAdd[];
|
|
199
206
|
}
|
|
200
207
|
/**
|
|
201
|
-
* The
|
|
202
|
-
*
|
|
208
|
+
* The handle a writable backend hangs its write face on (exported with the backend contract, ruled 2026-08-10;
|
|
209
|
+
* the consent boundary lives in the engine's wiring, not in this key's visibility).
|
|
203
210
|
*/
|
|
204
211
|
export declare const PERMISSION_RULE_WRITER = "__semaPermissionRuleWriter";
|
|
205
|
-
/** A store that also carries the
|
|
212
|
+
/** A store that also carries the backend write face. */
|
|
206
213
|
export interface WritablePermissionRuleStore extends PermissionRuleStore {
|
|
207
214
|
readonly [PERMISSION_RULE_WRITER]: PermissionRuleWriter;
|
|
208
215
|
}
|
|
@@ -419,7 +426,11 @@ export type RemoveResult =
|
|
|
419
426
|
export declare function removePersistedRule(opts: {
|
|
420
427
|
rule: string;
|
|
421
428
|
scope: RuleScope;
|
|
422
|
-
principal
|
|
429
|
+
/** Whose bucket. A bare string stays the principal shorthand (unchanged callers); the structural
|
|
430
|
+
* {@link RuleOwner} form adds the local-owner bucket (downstream request, 2026-08-10 — design/182 §4.5's
|
|
431
|
+
* `forLocalOwner()` face existed, but removal could not name it, so a local-owner rule was
|
|
432
|
+
* unrevokable through this entry). Same observed-remove/add-wins/stillLive semantics either way. */
|
|
433
|
+
principal: string | RuleOwner;
|
|
423
434
|
provider: PermissionRuleStoreProvider;
|
|
424
435
|
}): Promise<RemoveResult>;
|
|
425
436
|
export declare function errText(err: unknown): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { canonicalize } from "./canonical-json.js";
|
|
2
3
|
import { isRuleLive, parseAllowRuleText } from "./permission-rule-model.js";
|
|
3
4
|
export function sameRuleOwner(a, b) {
|
|
@@ -326,7 +327,11 @@ export async function ruleStoreChecksum(payload) {
|
|
|
326
327
|
}
|
|
327
328
|
const REMOVE_MAX_ATTEMPTS = 8;
|
|
328
329
|
export async function removePersistedRule(opts) {
|
|
329
|
-
const
|
|
330
|
+
const owner = typeof opts.principal === "string" ? { kind: "principal", principal: opts.principal } : opts.principal;
|
|
331
|
+
if (owner.kind === "local-owner" && opts.provider.forLocalOwner === undefined) {
|
|
332
|
+
return { status: "failed", error: "this provider has no local-owner bucket (forLocalOwner is not implemented) — a local-owner rule cannot be removed through it" };
|
|
333
|
+
}
|
|
334
|
+
const store = owner.kind === "local-owner" ? opts.provider.forLocalOwner() : opts.provider.forPrincipal(owner.principal);
|
|
330
335
|
const writer = writerOf(store);
|
|
331
336
|
if (writer === undefined) {
|
|
332
337
|
return { status: "failed", error: "the resolved permission-rule store has no write face — rules cannot be removed through it" };
|
|
@@ -396,7 +401,7 @@ export class InMemoryPermissionRuleStore {
|
|
|
396
401
|
observedVector;
|
|
397
402
|
rev = 0;
|
|
398
403
|
counter = 0;
|
|
399
|
-
constructor(actor = `mem-${
|
|
404
|
+
constructor(actor = `mem-${randomBytes(6).toString("base64url")}`, now = Date.now) {
|
|
400
405
|
this.actor = actor;
|
|
401
406
|
this.now = now;
|
|
402
407
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import type { PersistedAllowRule, RuleDot, RuleScope, RuleTombstone } from "./permission-rule-model.js";
|
|
29
29
|
import type { PermissionRuleStoreProvider, RuleOwner, RuleSyncDrop, RuleSyncFrontier, RuleSyncState } from "./permission-rule-store.js";
|
|
30
|
+
import { type TracerHook } from "./trace.js";
|
|
30
31
|
/** The injected HTTP seam — core never bundles a fetch. The deployment owns base URL, auth, TLS and
|
|
31
32
|
* retries; a non-2xx / network failure should THROW (the round then aborts with zero local effects —
|
|
32
33
|
* the transport runs before any local write). */
|
|
@@ -103,6 +104,13 @@ export declare function syncPermissionRules(opts: {
|
|
|
103
104
|
transport: PermissionRuleSyncTransport;
|
|
104
105
|
/** Injected clock — observation timestamps only, never adjudication input. */
|
|
105
106
|
now?: () => number;
|
|
107
|
+
/** design/182 §4.6 — optional trace sink for the round's DISCLOSURES: one
|
|
108
|
+
* `permission.rule_sync_resurrected` per removed→live transition and one
|
|
109
|
+
* `permission.rule_sync_dropped` per refused/quarantined row. The same facts are already on the
|
|
110
|
+
* returned result; this is the operator channel for a deployment that drives sync outside a task
|
|
111
|
+
* (where there is no `RunnerDeps.tracer` in scope) and does not read the result itself. Absent ⇒ no
|
|
112
|
+
* events, and the round is byte-identical. */
|
|
113
|
+
tracer?: TracerHook;
|
|
106
114
|
}): Promise<PermissionRuleSyncResult>;
|
|
107
115
|
interface ParsedRuleSyncResponse {
|
|
108
116
|
principal: string;
|