@sema-agent/core 7.43.0 → 7.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +245 -0
  2. package/dist/agents/subagent.d.ts +2 -1
  3. package/dist/agents/subagent.js +33 -14
  4. package/dist/agents/tool-filter.d.ts +65 -4
  5. package/dist/agents/tool-filter.js +127 -4
  6. package/dist/brain/errors.d.ts +23 -2
  7. package/dist/brain/errors.js +26 -2
  8. package/dist/brain/stream-engine.d.ts +2 -1
  9. package/dist/brain/stream-engine.js +10 -9
  10. package/dist/core/agent-definition.d.ts +2 -2
  11. package/dist/core/allow-entry.d.ts +25 -1
  12. package/dist/core/allow-entry.js +29 -4
  13. package/dist/core/ask-origin.d.ts +23 -4
  14. package/dist/core/ask-origin.js +10 -4
  15. package/dist/core/checkpoint-store.d.ts +28 -15
  16. package/dist/core/checkpoint-store.js +4 -13
  17. package/dist/core/decision-receipt.js +1 -1
  18. package/dist/core/effective-path-target.d.ts +7 -6
  19. package/dist/core/effective-path-target.js +3 -7
  20. package/dist/core/engine-notice.d.ts +33 -14
  21. package/dist/core/engine-notice.js +14 -7
  22. package/dist/core/gate-lanes.js +2 -0
  23. package/dist/core/gate-outcome.d.ts +5 -0
  24. package/dist/core/gate-outcome.js +2 -0
  25. package/dist/core/governance-codes.d.ts +4 -3
  26. package/dist/core/governance-codes.js +1 -0
  27. package/dist/core/hooks.d.ts +2 -0
  28. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  29. package/dist/core/memory-engine/content-origin.js +9 -4
  30. package/dist/core/memory-engine/engine.d.ts +2 -2
  31. package/dist/core/memory-engine/engine.js +7 -14
  32. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  33. package/dist/core/memory-engine/file-backend.js +5 -7
  34. package/dist/core/memory-engine/layout.d.ts +41 -4
  35. package/dist/core/memory-engine/layout.js +35 -5
  36. package/dist/core/permission-rules.d.ts +4 -0
  37. package/dist/core/permission-rules.js +5 -2
  38. package/dist/core/real-approval.d.ts +9 -6
  39. package/dist/core/real-approval.js +5 -5
  40. package/dist/core/runner/child-tree.d.ts +13 -0
  41. package/dist/core/runner/child-tree.js +3 -0
  42. package/dist/core/runner/contracts.d.ts +9 -18
  43. package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
  44. package/dist/core/runner/prepare-ask-lane.js +2 -2
  45. package/dist/core/runner/prepare-config-doors.js +3 -1
  46. package/dist/core/runner/prepare-execution-env.js +2 -1
  47. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  48. package/dist/core/runner/prepare-gate-stations.js +2 -1
  49. package/dist/core/runner/prepare-listings.d.ts +4 -4
  50. package/dist/core/runner/prepare-listings.js +2 -22
  51. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  52. package/dist/core/runner/prepare-park-ask.js +2 -2
  53. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  54. package/dist/core/runner/prepare-policy-chain.js +4 -2
  55. package/dist/core/runner/prepare-task.js +10 -6
  56. package/dist/core/runner/resume-policy-outcome.js +2 -1
  57. package/dist/core/runner/run-leg.js +1 -1
  58. package/dist/core/runner/run-notification-lane.js +2 -2
  59. package/dist/core/runner/runtask.d.ts +2 -0
  60. package/dist/core/runner/runtask.js +3 -5
  61. package/dist/core/runner/steer-admission.d.ts +10 -11
  62. package/dist/core/runner/steer-admission.js +6 -8
  63. package/dist/core/runner/stream-steer-verb.d.ts +5 -4
  64. package/dist/core/runner/stream-steer-verb.js +8 -10
  65. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  66. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  67. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  68. package/dist/core/runner/synthetic-tools.js +3 -2
  69. package/dist/core/session-policy-store.d.ts +6 -2
  70. package/dist/core/session-policy-store.js +2 -1
  71. package/dist/core/skill-grant.d.ts +10 -2
  72. package/dist/core/skill-grant.js +20 -4
  73. package/dist/core/steer-replay.d.ts +46 -0
  74. package/dist/core/steer-replay.js +22 -0
  75. package/dist/core/task-event.d.ts +3 -2
  76. package/dist/core/task-notification.d.ts +12 -1
  77. package/dist/core/task-notification.js +13 -1
  78. package/dist/core/task-spec.d.ts +5 -3
  79. package/dist/core/task-stream.d.ts +12 -6
  80. package/dist/core/tool-errors.d.ts +2 -2
  81. package/dist/core/tool-policy.d.ts +15 -4
  82. package/dist/core/tool-policy.js +9 -13
  83. package/dist/core/tool-spec.d.ts +20 -2
  84. package/dist/core/types.d.ts +2 -2
  85. package/dist/core/types.js +1 -1
  86. package/dist/index.d.ts +7 -4
  87. package/dist/index.js +7 -4
  88. package/dist/orchestration/workflow-types.d.ts +5 -0
  89. package/dist/orchestration/workflow.d.ts +3 -2
  90. package/dist/orchestration/workflow.js +16 -7
  91. package/dist/tools/fs/removal-targets.d.ts +82 -0
  92. package/dist/tools/fs/removal-targets.js +236 -0
  93. package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
  94. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  95. package/package.json +1 -1
  96. package/test/export-surface.snapshot.json +43 -5
  97. package/wire-changes/core@7.44.0.json +469 -0
  98. package/wire-changes/core@7.45.0.json +1084 -0
@@ -170,6 +170,7 @@ export const ASK_CLASSES = Object.freeze([
170
170
  "content_question",
171
171
  "hook",
172
172
  "ancestor",
173
+ "dangerous_delete",
173
174
  "unverifiable_delete",
174
175
  "transcript_write",
175
176
  "org_rule",
@@ -200,6 +201,7 @@ export const MODE_ARBITRATION = {
200
201
  content_question: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
201
202
  hook: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
202
203
  ancestor: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
204
+ dangerous_delete: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
203
205
  unverifiable_delete: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
204
206
  transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
205
207
  org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
@@ -222,7 +224,8 @@ export const MODE_ARBITRATION = {
222
224
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
223
225
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
224
226
  };
225
- export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
227
+ const CLASSIFIER_WITHHELD_ON_ROOTS = new Set(["probe"]);
228
+ export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
226
229
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
227
230
  export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
228
231
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
@@ -243,6 +246,8 @@ export function screenAskClassMembers(askClass, classes) {
243
246
  }
244
247
  export function decisionAskClassesOf(d) {
245
248
  const out = new Set();
249
+ if (d.guard === "dangerous_delete")
250
+ out.add("dangerous_delete");
246
251
  if (d.guard === "unverifiable_delete")
247
252
  out.add("unverifiable_delete");
248
253
  if (d.guard === "transcript_write")
@@ -273,10 +278,11 @@ export function decisionAskClassesOf(d) {
273
278
  out.add(c);
274
279
  return out;
275
280
  }
276
- export function foldArbitrationRow(classes) {
281
+ export function foldArbitrationRow(classes, readFace) {
277
282
  if (classes.size === 0)
278
283
  return MODE_ARBITRATION.ordinary;
279
- let bypassImmune = false, classifierEligible = true, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
284
+ const withheld = readFace === "roots" && [...classes].some((c) => CLASSIFIER_WITHHELD_ON_ROOTS.has(c));
285
+ let bypassImmune = false, classifierEligible = !withheld, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
280
286
  for (const c of classes) {
281
287
  const row = MODE_ARBITRATION[c];
282
288
  bypassImmune ||= row.bypassImmune;
@@ -334,7 +340,7 @@ export function classifyAsk(d, facts) {
334
340
  if (classes.size === 0)
335
341
  classes.add("ordinary");
336
342
  const askClass = ASK_CLASSES.find((c) => classes.has(c));
337
- return { classes, askClass, row: foldArbitrationRow(classes) };
343
+ return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace) };
338
344
  }
339
345
  export function headlessDenialMessage(toolName, askMessage) {
340
346
  return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
@@ -1,7 +1,8 @@
1
- import type { GateOutcome } from "./gate-outcome.js";
1
+ import type { GateOutcome, HostDecider } from "./gate-outcome.js";
2
2
  import { type ExecutionOutcomeRecordWord } from "./checkpoint-execution-record.js";
3
3
  import type { CheckpointPause } from "./pause-registry.js";
4
- import { type SystemInjectionPriority } from "./task-notification.js";
4
+ import type { SystemInjectionPriority } from "./task-notification.js";
5
+ import { type SteerReplayFacet } from "./steer-replay.js";
5
6
  import { type QuestionAnswer } from "./ask-question.js";
6
7
  import type { ReadEntry } from "../tools/fs/safety.js";
7
8
  import type { RepairBundle } from "../agents/repair-loop.js";
@@ -624,7 +625,7 @@ export type ResumeOutcome = {
624
625
  * authorization; a replay states its own facts.
625
626
  */
626
627
  hostDecision: {
627
- decidedBy: "person" | "sla_timeout" | "policy";
628
+ decidedBy: HostDecider;
628
629
  approver?: string;
629
630
  };
630
631
  /**
@@ -2130,10 +2131,12 @@ export declare class CheckpointError extends Error {
2130
2131
  * the caller delivers or removes a parked steer first. Nothing is written on this path. */
2131
2132
  | "steering.queue_full"
2132
2133
  /** design/171 §6.3 (falsification review adoption): the append reused an `inputId` already parked on
2133
- * this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL payload stays the
2134
- * idempotent no-op a retry needs; a differing one is refused, because swallowing it would lose the
2135
- * second operator's instruction — the exact silent loss the queue replaced. Retry with a fresh id.
2136
- * Nothing is written on this path. */
2134
+ * this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL steer stays the idempotent
2135
+ * no-op a retry needs; a differing one is refused, because swallowing it would lose the second
2136
+ * operator's instruction — the exact silent loss the queue replaced. `detail.differs` names the facets
2137
+ * that differ: with `text` among them it is another instruction (retry it under a fresh id); without,
2138
+ * it is the parked one re-sent at another tier / trust / attribution (a fresh id would park it twice —
2139
+ * replay it with its original values). Nothing is written on this path. */
2137
2140
  | "steering.duplicate_input_id"
2138
2141
  /** design/373 §4.3 (D2): the deployment's `userPromptSubmit` hook refused a wake resume's
2139
2142
  * `message` at the entrance screen (block verdict, or a fail-closed timeout/crash — the
@@ -2217,8 +2220,8 @@ export declare class CheckpointError extends Error {
2217
2220
  * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
2218
2221
  * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
2219
2222
  | "resume.constraint_unprojectable";
2220
- /** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
2221
- * members. `field` names WHICH part of the decision payload was rejected on a
2223
+ /** Structured discrimination a caller can branch on where the code alone is ambiguous. `field`
2224
+ * names WHICH part of the decision payload was rejected on a
2222
2225
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
2223
2226
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
2224
2227
  * action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
@@ -2248,6 +2251,10 @@ export declare class CheckpointError extends Error {
2248
2251
  * (`code` remains the only REQUIRED discriminant), which is exactly why the obligation to SPELL it
2249
2252
  * is a gate rather than a type: see the closed set's own doc. */
2250
2253
  reason?: CheckpointErrorReason;
2254
+ /** On `steering.duplicate_input_id` — the facets the repeat differs from the parked steer in (closed
2255
+ * {@link SteerReplayFacet}, in that set's order; always present on that code, never empty). Absent on
2256
+ * every other code. The live verb's refusal carries the same value. */
2257
+ differs?: readonly SteerReplayFacet[];
2251
2258
  } | undefined;
2252
2259
  constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
2253
2260
  /** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
@@ -2322,10 +2329,12 @@ export declare class CheckpointError extends Error {
2322
2329
  * the caller delivers or removes a parked steer first. Nothing is written on this path. */
2323
2330
  | "steering.queue_full"
2324
2331
  /** design/171 §6.3 (falsification review adoption): the append reused an `inputId` already parked on
2325
- * this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL payload stays the
2326
- * idempotent no-op a retry needs; a differing one is refused, because swallowing it would lose the
2327
- * second operator's instruction — the exact silent loss the queue replaced. Retry with a fresh id.
2328
- * Nothing is written on this path. */
2332
+ * this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL steer stays the idempotent
2333
+ * no-op a retry needs; a differing one is refused, because swallowing it would lose the second
2334
+ * operator's instruction — the exact silent loss the queue replaced. `detail.differs` names the facets
2335
+ * that differ: with `text` among them it is another instruction (retry it under a fresh id); without,
2336
+ * it is the parked one re-sent at another tier / trust / attribution (a fresh id would park it twice —
2337
+ * replay it with its original values). Nothing is written on this path. */
2329
2338
  | "steering.duplicate_input_id"
2330
2339
  /** design/373 §4.3 (D2): the deployment's `userPromptSubmit` hook refused a wake resume's
2331
2340
  * `message` at the entrance screen (block verdict, or a fail-closed timeout/crash — the
@@ -2409,8 +2418,8 @@ export declare class CheckpointError extends Error {
2409
2418
  * computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
2410
2419
  * checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
2411
2420
  | "resume.constraint_unprojectable", message: string,
2412
- /** Structured discrimination a caller can branch on where the code alone is ambiguous. Two
2413
- * members. `field` names WHICH part of the decision payload was rejected on a
2421
+ /** Structured discrimination a caller can branch on where the code alone is ambiguous. `field`
2422
+ * names WHICH part of the decision payload was rejected on a
2414
2423
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
2415
2424
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
2416
2425
  * action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
@@ -2440,6 +2449,10 @@ export declare class CheckpointError extends Error {
2440
2449
  * (`code` remains the only REQUIRED discriminant), which is exactly why the obligation to SPELL it
2441
2450
  * is a gate rather than a type: see the closed set's own doc. */
2442
2451
  reason?: CheckpointErrorReason;
2452
+ /** On `steering.duplicate_input_id` — the facets the repeat differs from the parked steer in (closed
2453
+ * {@link SteerReplayFacet}, in that set's order; always present on that code, never empty). Absent on
2454
+ * every other code. The live verb's refusal carries the same value. */
2455
+ differs?: readonly SteerReplayFacet[];
2443
2456
  } | undefined);
2444
2457
  }
2445
2458
  /**
@@ -8,7 +8,7 @@ import { readBoundBinding } from "./skill-grant-binding.js";
8
8
  import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
9
9
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
10
10
  import { redactSecrets } from "./untrusted-egress.js";
11
- import { steerPriorityOf } from "./task-notification.js";
11
+ import { steerReplayRefusal } from "./steer-replay.js";
12
12
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
13
13
  import { isPermissionMode } from "./ask-origin.js";
14
14
  import { wireOf } from "./tool-name-readings.js";
@@ -548,22 +548,13 @@ export function readPendingSteerQueue(state) {
548
548
  }
549
549
  return [...(state.pendingSteerQueue ?? [])].sort((a, b) => a.seq - b.seq);
550
550
  }
551
- function samePendingSteerPayload(a, b) {
552
- return (a.text === b.text &&
553
- a.trusted === b.trusted &&
554
- steerPriorityOf(a) === steerPriorityOf(b) &&
555
- a.actor?.id === b.actor?.id &&
556
- a.actor?.hostAsserted === b.actor?.hostAsserted &&
557
- a.actor?.issuer === b.actor?.issuer);
558
- }
559
551
  export function appendPendingSteer(state, entry) {
560
552
  const current = readPendingSteerQueue(state);
561
553
  const collision = current.find((e) => e.inputId === entry.inputId);
562
554
  if (collision !== undefined) {
563
- if (!samePendingSteerPayload(collision, entry)) {
564
- throw new CheckpointError("steering.duplicate_input_id", `a different steering instruction is already parked under inputId "${entry.inputId}" — ` +
565
- `re-issue this one with a fresh inputId (an identical payload would have been an idempotent retry)`);
566
- }
555
+ const refusal = steerReplayRefusal(collision, entry, { seat: "parked", inputId: entry.inputId });
556
+ if (refusal !== undefined)
557
+ throw new CheckpointError("steering.duplicate_input_id", refusal.message, refusal.detail);
567
558
  return current;
568
559
  }
569
560
  const nextSeq = current.reduce((max, e) => Math.max(max, e.seq), 0) + 1;
@@ -11,7 +11,7 @@ export const refusedOutOfContract = (message) => {
11
11
  export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
12
12
  const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"]);
13
13
  const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
14
- const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
14
+ const GUARDS = Object.keys({ dangerous_delete: 0, unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
15
15
  const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
16
16
  const AUTHORED_BY_WORDS = Object.keys({ deployment: 0 });
17
17
  const memberOf = (words) => {
@@ -28,8 +28,8 @@
28
28
  *
29
29
  * Nothing here judges anything: it answers "which absolute path is this call about". Containment, the
30
30
  * sensitive-path deny face and the rule precedence walk are unchanged and still run where they ran.
31
- * `undefined` means "this call has no path this reading can locate" — the documented fail-toward-asking
32
- * posture of both lanes (no refusal on the tightening side, no grant on the allow side).
31
+ * `undefined` means "this call names no path" (no declaration, a non-string slot, an absent slot that targets
32
+ * nothing); a path the call does name whose base this reading lacks is `unreadable` — never a guess, never silence.
33
33
  *
34
34
  * RepoMap shares the search tools' root-relative `path` semantics but declares NO path target at all (no
35
35
  * rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no
@@ -71,8 +71,8 @@ export declare function splitAbsoluteGlobPattern(pattern: string, family: PathFa
71
71
  } | undefined;
72
72
  /**
73
73
  * The reading of a call's path slot under ONE declaration: `path` = the absolute target's identity; `unreadable`
74
- * = the slot spells a target but the base it needs is not a usable base of this tree (fed to TIGHTENING rows
75
- * only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
74
+ * = the slot spells a target (or targets its base) but the base it needs is not supplied or not a usable base of
75
+ * this tree (fed to TIGHTENING rows only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
76
76
  * spelling itself (a rule speaks about no such spelling: no verdict, no ask — the fs throat delivers the
77
77
  * refusal with the same word); `undefined` = no target in this reading.
78
78
  */
@@ -97,8 +97,9 @@ export type PathTargetReading = {
97
97
  * ③ otherwise a `patternParam`'s ABSOLUTE pattern supplies its own base directory;
98
98
  * ④ otherwise the base itself (the tool searches all of it).
99
99
  *
100
- * A base the deployment did not supply yields `undefined`: a relative spelling with nothing to resolve
101
- * against names no file, and claiming it names `/<spelling>` was a fabrication no fence should judge.
100
+ * A base the caller did not supply yields `unreadable`: a relative spelling with nothing to resolve against
101
+ * names a file this reading cannot locate — claiming it names `/<spelling>` would be a fabrication, and
102
+ * claiming it names nothing would let every rule about that file stay silent.
102
103
  */
103
104
  export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): PathTargetReading;
104
105
  /**
@@ -31,10 +31,8 @@ export function effectivePathTargetOf(call, pt, bases) {
31
31
  if (spelling.spelled !== undefined)
32
32
  return resolveSpelling(spelling.spelled, spelling.base, bases);
33
33
  const declaredBase = spelling.base;
34
- if (declaredBase === undefined)
35
- return undefined;
36
34
  const family = pathFamilyOf(bases);
37
- return isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
35
+ return declaredBase !== undefined && isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
38
36
  }
39
37
  export function effectivePathSpellingOf(call, pt, bases) {
40
38
  if (pt === undefined)
@@ -63,9 +61,7 @@ function resolveSpelling(spelled, declaredBase, bases) {
63
61
  const tree = bases.root ?? bases.cwd;
64
62
  if (isAbsoluteForFamily(family, screened.spelled))
65
63
  return readingOf(pathIdentityOf(screened.spelled, family, tree));
66
- if (declaredBase === undefined || family === undefined)
67
- return undefined;
68
- if (!isAbsoluteForFamily(family, declaredBase))
64
+ if (declaredBase === undefined || family === undefined || !isAbsoluteForFamily(family, declaredBase))
69
65
  return unreadableBase(declaredBase);
70
66
  return readingOf(pathIdentityOf(joinForFamily(family, declaredBase, screened.spelled), family, tree));
71
67
  }
@@ -75,5 +71,5 @@ function readingOf(r) {
75
71
  return r.reason === "relative" || r.reason === "unanchored" ? undefined : { kind: "refused", reason: r.reason };
76
72
  }
77
73
  function unreadableBase(base) {
78
- return { kind: "unreadable", reason: `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
74
+ return { kind: "unreadable", reason: base === undefined ? "the call's path is relative to a base that was not supplied" : `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
79
75
  }
@@ -394,16 +394,18 @@ export interface EngineNotice {
394
394
  * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
395
395
  * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
396
396
  * #257 contract's "accepted = enqueued, not consumed" sentence;
397
- * `detail: { steer, cause, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
398
- * (`cause` on the steer row only, closed `USER_STEER_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
397
+ * `detail: { <countKey>, cause, taskId?, sessionId?, runId? }` (the `code` and the `countKey` of each family are the
398
+ * table {@link USER_INPUT_UNDRAINED_FAMILIES}, keyed by the lane the input was queued on — `injectionLaneOf` — and the
399
+ * minter writes exactly those; `cause` on both rows, closed `USER_INPUT_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
399
400
  * leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
400
- * session's next run, for steers it had accepted whose consumption was not confirmed before the process died: their queue was the
401
- * serving layer's, so only it can count them. One code for both, so a consumer de-duplicating on `code` keeps
401
+ * session's next run, for inputs it had accepted whose consumption was not confirmed before the process died: their queue was the
402
+ * serving layer's, so only it can count them, and it picks the family by the LANE the input was queued on (a `later`
403
+ * steer rides the follow-up lane). One code per family for both arms, so a consumer de-duplicating on `code` keeps
402
404
  * one row; `sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
403
405
  * per-run identity, the key this account is actually about — see
404
406
  * {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated). A delegated child
405
407
  * leg is announced on its PARENT's session as well — the direct parent only — as a second row per family,
406
- * `detail: { steer | followUp, cause?, taskId?, sessionId: <the parent's>, childSessionId }` (no `runId`: nothing
408
+ * `detail: { <countKey>, cause, taskId?, sessionId: <the parent's>, childSessionId }` (no `runId`: nothing
407
409
  * carries the parent's), message tail `(a child leg, <childSessionId>)`; `childSessionId` present = a child reported it.
408
410
  * Per-run, at most once per family (the terminal sweep is a single site) — which is why
409
411
  * `taskId`/`sessionId` cannot key it: both answer the same value for two runs of one session.
@@ -959,16 +961,33 @@ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowRe
959
961
  parent?: ParentLegIds | undefined;
960
962
  }, onNotice: ((notice: EngineNotice) => void) | undefined): void;
961
963
  /**
962
- * Why a person's steer is reported as not consumed — `task.user_steer_undrained`'s `detail.cause`. Closed:
963
- * · `leg_end` — the leg ended with the steer still owed (this engine mints it, at the leg's settlement);
964
- * · `crashed` — the engine process ended before the leg settled, and a steer the serving layer had accepted for it was
965
- * not confirmed consumed before the process died (a live leg gives the serving layer no evidence that a steer reached
966
- * the model, so such a steer MAY have been consumed). Only the serving layer can count these (the queue was its own), so the serving layer
967
- * mints this arm, before the next run of the session starts — the one member-level exception to
968
- * {@link import("./governance-codes.js").NOTICE_MINTER}'s row for the code, which stays `"engine"`.
964
+ * Why a person's input is reported as not consumed — the `detail.cause` of BOTH undrained families
965
+ * (`task.user_steer_undrained` / `task.user_followup_undrained`; the family is the lane the input was queued on). Closed:
966
+ * · `leg_end` — the leg ended with the input still owed (this engine mints it, at the leg's settlement);
967
+ * · `crashed` — the engine process ended before the leg settled, and an input the serving layer had accepted for it was
968
+ * not confirmed consumed before the process died (a live leg gives the serving layer no evidence that an input reached
969
+ * the model, so such an input MAY have been consumed). Only the serving layer can count these (the queue was its own), so the serving layer
970
+ * mints this arm, choosing the family by the lane the input was queued on, before the next run of the session starts —
971
+ * the one member-level exception to {@link import("./governance-codes.js").NOTICE_MINTER}'s rows for the two codes, which stay `"engine"`.
969
972
  */
970
- export declare const USER_STEER_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
971
- export type UserSteerUndrainedCause = (typeof USER_STEER_UNDRAINED_CAUSES)[number];
973
+ export declare const USER_INPUT_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
974
+ export type UserInputUndrainedCause = (typeof USER_INPUT_UNDRAINED_CAUSES)[number];
975
+ /**
976
+ * The two families of the stranded-input account, keyed by the lane the input was queued on (`injectionLaneOf` maps a steer's
977
+ * tier to it): the notice `code` the family is announced under, and the `detail` key that carries its count
978
+ * (`detail[countKey]`, beside `cause`). {@link undrainedUserInputNotices} writes its rows with exactly these two spellings, so a
979
+ * serving layer that mints the `crashed` arm picks its family by lane and spells code and count key the way the engine does.
980
+ */
981
+ export declare const USER_INPUT_UNDRAINED_FAMILIES: Readonly<{
982
+ readonly steer: Readonly<{
983
+ readonly code: "task.user_steer_undrained";
984
+ readonly countKey: "steer";
985
+ }>;
986
+ readonly followUp: Readonly<{
987
+ readonly code: "task.user_followup_undrained";
988
+ readonly countKey: "followUp";
989
+ }>;
990
+ }>;
972
991
  /** Where a child leg's account is ALSO announced: its spawner's session, and that run's task id when the spawner carried one
973
992
  * (it never carries the run id, and the row never invents one). */
974
993
  interface ParentLegIds {
@@ -51,7 +51,13 @@ export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
51
51
  }
52
52
  }
53
53
  const SETTLED_OWED_ACCOUNTS = new WeakSet();
54
- export const USER_STEER_UNDRAINED_CAUSES = Object.freeze(["leg_end", "crashed"]);
54
+ export const USER_INPUT_UNDRAINED_CAUSES = Object.freeze(["leg_end", "crashed"]);
55
+ const STEER_UNDRAINED = "task.user_steer_undrained";
56
+ const FOLLOWUP_UNDRAINED = "task.user_followup_undrained";
57
+ export const USER_INPUT_UNDRAINED_FAMILIES = Object.freeze({
58
+ steer: Object.freeze({ code: STEER_UNDRAINED, countKey: "steer" }),
59
+ followUp: Object.freeze({ code: FOLLOWUP_UNDRAINED, countKey: "followUp" }),
60
+ });
55
61
  export function parentLegIds(internals) {
56
62
  const parentSessionId = internals?.parentSessionId;
57
63
  if (typeof parentSessionId !== "string" || parentSessionId === "")
@@ -70,19 +76,20 @@ export function undrainedUserInputNotices(counts, taskId, sessionId, runId, pare
70
76
  const copyTail = ` (a child leg, ${sessionId})`;
71
77
  const own = [];
72
78
  const copies = [];
79
+ const cause = "leg_end";
73
80
  if (counts.steer > 0) {
74
81
  const message = `${counts.steer} user steer(s) ${tail}`;
75
- const count = { steer: counts.steer, cause: "leg_end" };
76
- own.push({ code: "task.user_steer_undrained", message, detail: { ...count, ...tid, ...sid, ...rid } });
82
+ const count = { [USER_INPUT_UNDRAINED_FAMILIES.steer.countKey]: counts.steer, cause };
83
+ own.push({ code: STEER_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
77
84
  if (toParent)
78
- copies.push({ code: "task.user_steer_undrained", message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
85
+ copies.push({ code: STEER_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
79
86
  }
80
87
  if (counts.followUp > 0) {
81
88
  const message = `${counts.followUp} user follow-up(s) ${tail}`;
82
- const count = { followUp: counts.followUp };
83
- own.push({ code: "task.user_followup_undrained", message, detail: { ...count, ...tid, ...sid, ...rid } });
89
+ const count = { [USER_INPUT_UNDRAINED_FAMILIES.followUp.countKey]: counts.followUp, cause };
90
+ own.push({ code: FOLLOWUP_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
84
91
  if (toParent)
85
- copies.push({ code: "task.user_followup_undrained", message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
92
+ copies.push({ code: FOLLOWUP_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
86
93
  }
87
94
  return [...own, ...copies];
88
95
  }
@@ -40,6 +40,7 @@ export async function runGateLanes(pass) {
40
40
  writeProtected: pass.writeProtected === true,
41
41
  writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
42
42
  });
43
+ const readFaceNow = input.readFace?.();
43
44
  const askOriginFacts = (org, ruleCallUnreadable) => ({
44
45
  contentQuestion: heldBy("lanes.contentQuestionClass", QUESTION_TOOL_NAMES, pass.req),
45
46
  markedUnresolvable: markedUnresolvable(),
@@ -49,6 +50,7 @@ export async function runGateLanes(pass) {
49
50
  mandateMarks: mandateMarksNow(),
50
51
  ...(input.toolMarkSources !== undefined ? { toolMarkSources: input.toolMarkSources } : {}),
51
52
  ...(input.a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
53
+ ...(readFaceNow !== undefined ? { readFace: readFaceNow } : {}),
52
54
  });
53
55
  const classifyStanding = (unstamped, facts) => (pass.askClassification = classifyAsk(unstamped, facts));
54
56
  let orgTightenCount = 0;
@@ -125,6 +125,11 @@ export type SettlementParty = (typeof SETTLEMENT_PARTIES)[number];
125
125
  export type SettlementPartiesMatchTheRecord = AssertAllKeysHandled<Exclude<Settlement["who"]["party"], SettlementParty> | Exclude<SettlementParty, Settlement["who"]["party"]>>;
126
126
  /** Membership test for {@link SettlementParty}. */
127
127
  export declare function isSettlementParty(v: unknown): v is SettlementParty;
128
+ /** Who REPORTED a durable decide (`hostDecision.decidedBy`): a person, the host's own SLA sweep at the row's deadline, or the
129
+ * deployment's policy — the closed set; a host's decide door mirrors the words it accepts from this one. */
130
+ export declare const HOST_DECIDERS: readonly ["person", "sla_timeout", "policy"];
131
+ export type HostDecider = (typeof HOST_DECIDERS)[number];
132
+ export declare const isHostDecider: (word: unknown) => word is HostDecider;
128
133
  /** The DECIDER arms of {@link Settlement} — a person's and the deployment policy's, the two parties that
129
134
  * answer an ask either way. Named so a site that both composes one and quotes its note can read the seat
130
135
  * off the record instead of re-deriving it. */
@@ -25,6 +25,8 @@ const SETTLEMENT_PARTY_SET = new Set(SETTLEMENT_PARTIES);
25
25
  export function isSettlementParty(v) {
26
26
  return SETTLEMENT_PARTY_SET.has(v);
27
27
  }
28
+ export const HOST_DECIDERS = Object.freeze(["person", "sla_timeout", "policy"]);
29
+ export const isHostDecider = (word) => HOST_DECIDERS.some((w) => w === word);
28
30
  export function decidersNote(text) {
29
31
  return text === "" ? undefined : text;
30
32
  }
@@ -259,9 +259,10 @@ export declare function noticeAudienceOf(code: string): NoticeAudience;
259
259
  * {@link NOTICE_AUDIENCE} like every other row, and NEVER minted by this package (the catalog gate reds a mint site for
260
260
  * one in src/). One code, one minter: a code two processes mint would be two facts under one spelling.
261
261
  *
262
- * Member-level exception, stated where a consumer reads the minter: `task.user_steer_undrained` is an engine row, and
263
- * its `detail.cause: "crashed"` arm is minted by the server — the engine's process is gone when that loss is counted
264
- * (see `USER_STEER_UNDRAINED_CAUSES`). The code keeps one row here and one spelling for both arms.
262
+ * Member-level exception, stated where a consumer reads the minter: `task.user_steer_undrained` and
263
+ * `task.user_followup_undrained` are engine rows, and their `detail.cause: "crashed"` arm is minted by the server — the
264
+ * engine's process is gone when that loss is counted (see `USER_INPUT_UNDRAINED_CAUSES`); the server picks the code by the
265
+ * lane the input was queued on, as this engine does. Each code keeps one row here and one spelling for both arms.
265
266
  */
266
267
  export type NoticeMinter = "engine" | "server";
267
268
  declare const NOTICE_MINTER_TABLE: {
@@ -84,6 +84,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
84
84
  "memory.capture_optout_unavailable",
85
85
  "memory.capture_optout_sweep_failed",
86
86
  "memory.target_outside_store",
87
+ "memory.scope_home_not_directory",
87
88
  ]);
88
89
  export function governanceRetryClass(code) {
89
90
  if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
@@ -1466,6 +1466,8 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1466
1466
  permissionMode?: () => import("./ask-origin.js").PermissionMode;
1467
1467
  /** The leg's approval-seat form (`WiringManifest.ask.form`): the deny exit's way-out clause names a declaration, never a missing approver. */
1468
1468
  askSeatForm?: import("./wiring-manifest.js").AskSeamForm;
1469
+ /** The read face the leg resolved, read per call into the class derivation (`roots` withholds the classifier from a `probe` question); absent ⇒ none resolved. */
1470
+ readFace?: () => import("../tools/fs/containment-face.js").ContainmentFace | undefined;
1469
1471
  /** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
1470
1472
  * absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
1471
1473
  toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
@@ -1,37 +1,3 @@
1
- /**
2
- * design/178 §3 — the tool content-origin classification (three tiers by CAPABILITY, fail-closed).
3
- *
4
- * The question this face answers per mounted tool: can invoking it bring content from OUTSIDE the
5
- * deployment's trust boundary into the session — and therefore, via the automatic harvest path,
6
- * into long-term memory? Three tiers:
7
- *
8
- * - "external" — network-retrieval families and external protocol channels. Invocation marks the
9
- * session's memory EXTERNALLY EXPOSED (one-way, durable; what the mark does at
10
- * harvest is the provenance mode's question — design/336: tag-admit with an origin
11
- * marker under "carry", full quarantine under "off").
12
- * - "execution" — general execution (shell/exec). A NAMED residual channel: it can reach external
13
- * content indirectly, but the two industry anchors this tier is calibrated against
14
- * gate on network/protocol families only, and excluding every shell session would
15
- * disable memory for the main coding scenario. Not polluting by default; the
16
- * `execIsExternalContent` strict knob upgrades it to external.
17
- * - "local" — purely local reads/computation.
18
- *
19
- * WHO CLASSIFIES (single-source rule): core built-ins declare `contentOrigin` on their own ToolSpec
20
- * (the classification lives with the tool definition); protocol tools (MCP/A2A) are structurally
21
- * external (the channel itself crosses the boundary); a HOST tool declares its own — and when it
22
- * does not, the default is EXTERNAL (fail-closed: unknown = external), overridable only through the
23
- * explicit `TaskSpec.memory.trustedTools` allowlist. A core-mounted built-in without a declaration
24
- * defaults local: core owns those definitions, and the fail-closed default exists for tools core
25
- * CANNOT vouch for, not for its own. A declaration is honored only when it SPELLS a member of the
26
- * closed vocabulary — an unreadable one is not a declaration.
27
- *
28
- * The three tier names are BOUNDARY-RELATIVE, not statements about topology: `"local"` means "does
29
- * not bring content from outside the deployment's trust boundary", which a unix socket to another
30
- * process the deployment itself runs satisfies and a loopback HTTP call to somebody else's service
31
- * does not. Distance is not the axis; whose content it is, is.
32
- *
33
- * There is deliberately no "gate inactive" state: every mounted tool gets a class.
34
- */
35
1
  import type { ToolContentOrigin } from "../types.js";
36
2
  export interface ClassifyToolContentOriginInput {
37
3
  /** The tool's own `contentOrigin` declaration (ToolSpec/AgentTool carry) — wins when present AND
@@ -1,3 +1,4 @@
1
+ import { allowedToolNames, readDeniedEntry, readToolNameList } from "../../agents/tool-filter.js";
1
2
  const TOOL_CONTENT_ORIGIN_SET = { external: true, execution: true, local: true };
2
3
  export const TOOL_CONTENT_ORIGINS = Object.keys(TOOL_CONTENT_ORIGIN_SET);
3
4
  export function isToolContentOrigin(value) {
@@ -27,10 +28,14 @@ export function delegationCallIsExternal(input) {
27
28
  if (face === undefined)
28
29
  return true;
29
30
  const selectedFor = (f) => {
30
- const d = f.denyTools ?? [];
31
- const a = f.allowTools;
32
- const isNarrowed = a !== undefined && !a.includes("*");
33
- return input.pool.filter((t) => (!isNarrowed || a.some((x) => selectedBy(x, t))) && !d.some((x) => selectedBy(x, t)));
31
+ const deniedList = readToolNameList(f.denyTools);
32
+ const d = deniedList.kind === "entries" ? deniedList.entries : [];
33
+ const a = allowedToolNames(f.allowTools);
34
+ const denies = (t) => d.some((x) => {
35
+ const read = readDeniedEntry(x);
36
+ return read.kind === "name" && selectedBy(read.name, t);
37
+ });
38
+ return input.pool.filter((t) => (a === undefined || [...a].some((x) => selectedBy(x, t))) && !denies(t));
34
39
  };
35
40
  if (selectedFor(face).some((t) => input.isPolluting(t)))
36
41
  return true;
@@ -1789,7 +1789,7 @@ export declare class MemoryEngine {
1789
1789
  */
1790
1790
  private gateDerivedIndex;
1791
1791
  /** The file backend's verdict at an act the engine itself performs on a store path ({@link
1792
- * storeTargetEscape}): throws `memory.target_outside_store` when `path` would land outside `root`;
1792
+ * landsOutsideStore}): throws `memory.target_outside_store` when `path` would land outside `root`;
1793
1793
  * each caller's existing failure lane reports it. `root` is the mount's canonical root where a
1794
1794
  * handle exists, so a root re-pointed after the mount is judged against the one it mounted. */
1795
1795
  private refuseOutsideStore;
@@ -1798,7 +1798,7 @@ export declare class MemoryEngine {
1798
1798
  * is never a supported layout (the harvest scan refuses symlinked entry files outright), and this
1799
1799
  * site already reports write failures by throwing, so ELOOP joins the failures it already had. A
1800
1800
  * target whose directory resolves outside `root` (a directory on the path is a link) is refused
1801
- * the same way, before anything is read or written ({@link storeTargetEscape}). */
1801
+ * the same way, before anything is read or written ({@link landsOutsideStore}). */
1802
1802
  private writeIfChanged;
1803
1803
  /** Persist the canonical projection back to the session file (id minting / frontmatter completion).
1804
1804
  * C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
@@ -18,7 +18,7 @@ import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCand
18
18
  import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
19
19
  import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
20
20
  import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
21
- import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, storeTargetEscape, TARGET_OUTSIDE_STORE, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, 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, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
21
+ import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, 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, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
22
22
  import { createSessionIncarnationSeat, recalledForAnyIdentity, recalledForIdentity, rememberForIdentity } from "./session-incarnation.js";
23
23
  import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
24
24
  import { writableRootOf } from "./types.js";
@@ -1416,11 +1416,8 @@ export class MemoryEngine {
1416
1416
  ensureDirExists(this.memoryDir);
1417
1417
  const physicalRoot = landingPathOf(this.memoryDir);
1418
1418
  this.mountedRoot ??= physicalRoot;
1419
- if (physicalRoot !== this.mountedRoot) {
1420
- const e = new Error(`${TARGET_OUTSIDE_STORE}: the memory dir ${this.memoryDir} now resolves to ${physicalRoot}, not ${this.mountedRoot} where this engine first mounted it — refused; open a new engine on the directory you mean`);
1421
- e.code = TARGET_OUTSIDE_STORE;
1422
- throw e;
1423
- }
1419
+ if (physicalRoot !== this.mountedRoot)
1420
+ throw storeRootMovedRefusal("engine", this.memoryDir);
1424
1421
  ensureDirExists(this.controlDir);
1425
1422
  await this.adoptLayoutBeforeMount(writeScope);
1426
1423
  const restricted = opts?.adoptionRestricted === true;
@@ -2216,7 +2213,7 @@ export class MemoryEngine {
2216
2213
  if (content === undefined)
2217
2214
  continue;
2218
2215
  try {
2219
- this.refuseOutsideStore(handle.memoryDir, m.path, "restoring");
2216
+ this.refuseOutsideStore(handle.memoryDir, m.path, "restoring a memory file");
2220
2217
  mkdirSync(dirname(m.path), { recursive: true });
2221
2218
  writeFileNoFollow(m.path, content);
2222
2219
  report.restored.push(m.relPath);
@@ -4947,15 +4944,11 @@ export class MemoryEngine {
4947
4944
  };
4948
4945
  }
4949
4946
  refuseOutsideStore(root, path, act) {
4950
- const escape = storeTargetEscape(root, path);
4951
- if (escape === undefined)
4952
- return;
4953
- const e = new Error(`${TARGET_OUTSIDE_STORE}: ${act} ${path} was refused — ${escape}; not performed`);
4954
- e.code = TARGET_OUTSIDE_STORE;
4955
- throw e;
4947
+ if (landsOutsideStore(root, path))
4948
+ throw storeActRefusal(act, path);
4956
4949
  }
4957
4950
  writeIfChanged(target, text, root) {
4958
- this.refuseOutsideStore(root, target, "the projection write of");
4951
+ this.refuseOutsideStore(root, target, "the projection write");
4959
4952
  const current = readNoFollowSafe(target);
4960
4953
  if (current === text)
4961
4954
  return;