@sema-agent/core 7.17.0 → 7.17.2

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 (43) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/core/fs-write-gate-policy.js +4 -0
  3. package/dist/core/memory-engine/engine.js +2 -1
  4. package/dist/core/memory-engine/layout.d.ts +18 -6
  5. package/dist/core/memory-engine/layout.js +40 -21
  6. package/dist/core/physical-path.d.ts +37 -0
  7. package/dist/core/physical-path.js +30 -0
  8. package/dist/core/runner/contracts.d.ts +3 -1
  9. package/dist/core/runner/prepare-artifact.d.ts +4 -5
  10. package/dist/core/runner/prepare-artifact.js +2 -16
  11. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  12. package/dist/core/runner/prepare-ask-lane.js +3 -3
  13. package/dist/core/runner/prepare-park-ask.d.ts +3 -0
  14. package/dist/core/runner/prepare-park-ask.js +3 -2
  15. package/dist/core/runner/prepare-policy-chain.js +2 -2
  16. package/dist/core/runner/prepare-question-face.js +2 -1
  17. package/dist/core/runner/prepare-task.js +5 -5
  18. package/dist/core/runner/run-harness-handlers.js +4 -1
  19. package/dist/core/runner/run-leg.js +4 -1
  20. package/dist/core/sensitive-path-policy.js +7 -8
  21. package/dist/core/skills-directory.js +4 -3
  22. package/dist/core/spec-contract.js +5 -4
  23. package/dist/core/task-registry-shared.d.ts +5 -1
  24. package/dist/core/task-registry-shared.js +1 -0
  25. package/dist/core/tool-catalog-entries.js +1 -1
  26. package/dist/core/tool-policy.d.ts +56 -0
  27. package/dist/core/tool-policy.js +6 -0
  28. package/dist/engine/execution-env/node-execution-env.js +4 -3
  29. package/dist/index.d.ts +1 -0
  30. package/dist/index.js +1 -0
  31. package/dist/orchestration/workflow-script-store.js +9 -25
  32. package/dist/orchestration/workflow.js +6 -3
  33. package/dist/stores/cc/task-list-store.js +2 -10
  34. package/dist/stores/file/fs-atomic.d.ts +15 -18
  35. package/dist/stores/file/fs-atomic.js +4 -14
  36. package/dist/stores/file/mailbox-store.d.ts +7 -11
  37. package/dist/stores/file/mailbox-store.js +4 -11
  38. package/dist/tools/artifact/local-stub.js +4 -3
  39. package/dist/tools/fs/bash-readonly-classifier.d.ts +19 -1
  40. package/dist/tools/fs/bash-readonly-classifier.js +413 -12
  41. package/dist/tools/fs/fs-bash.js +42 -17
  42. package/package.json +1 -1
  43. package/test/export-surface.snapshot.json +5 -1
@@ -1,7 +1,7 @@
1
- import { realpathSync } from "node:fs";
2
1
  import { resolve as resolveFsPath } from "node:path";
3
2
  import { AgentHarness, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, summaryOutputBudgetTokens } from "../../internal/harness.js";
4
3
  import { sanitizeCompactionSettings } from "../auto-compaction.js";
4
+ import { physicalPathOf } from "../physical-path.js";
5
5
  import { projectStaleToolResults, resolveStaleToolResultOffload } from "./compaction-call-options.js";
6
6
  import { uuidv7 } from "../../engine/session/uuid.js";
7
7
  import { brainToRuntime } from "../runtime.js";
@@ -346,7 +346,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
346
346
  else {
347
347
  const canonical = (p, what) => {
348
348
  try {
349
- return realpathSync.native(resolveFsPath(p));
349
+ return physicalPathOf(resolveFsPath(p));
350
350
  }
351
351
  catch (err) {
352
352
  return failIsolation(`cannot canonicalize ${what} (${err instanceof Error ? err.message : String(err)}) — an unverifiable path is treated as not isolated`);
@@ -690,16 +690,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
690
690
  return undefined;
691
691
  });
692
692
  }
693
- const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash });
693
+ const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash });
694
694
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
695
695
  const { suspendForResource, suspendForPlatformLimit, suspendForReview } = prepareBoundaryParks({ saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, pausedRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter });
696
- const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
696
+ const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
697
697
  prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
698
698
  const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef });
699
699
  const preparedHolder = {};
700
700
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder });
701
701
  await onceLedger.settle(session, announcedListingsRef);
702
- const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
702
+ const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolRoster, toolRosterDeltas: rosterSeat, structuredProjector, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, microCompact, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
703
703
  rollback.commit();
704
704
  const prepared = buildPrepared();
705
705
  preparedHolder.current = prepared;
@@ -287,7 +287,10 @@ export function createHarnessHandlers(input) {
287
287
  prepared.workCutRef.abortedBeforeTurnEnd = true;
288
288
  const stopExtra = rs.turn.turnStopReason !== undefined ? { stopReason: rs.turn.turnStopReason } : {};
289
289
  const usage = rs.turn.turnUsage ?? { inputTokens: 0, totalInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, costMicroUsd: 0 };
290
- queue.push({ type: "turn_end", usage, ...(rs.turn.turnUsage === undefined || rs.turn.turnUsageMissing ? { usageMissing: true } : {}), ...stopExtra, ...ident() });
290
+ const turnUsageUnknown = rs.turn.turnUsage === undefined || rs.turn.turnUsageMissing;
291
+ if (turnUsageUnknown)
292
+ stats.usageMissing = true;
293
+ queue.push({ type: "turn_end", usage, ...(turnUsageUnknown ? { usageMissing: true } : {}), ...stopExtra, ...ident() });
291
294
  rs.turn.turnUsageMissing = false;
292
295
  rs.turn.turnStopReason = undefined;
293
296
  if (parentToolCallId !== undefined) {
@@ -10,7 +10,7 @@ import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-ar
10
10
  import { computeCostMicroUsd } from "../pricing.js";
11
11
  import { mintSystemReminder } from "../reminder-mint.js";
12
12
  import { hasDestroy } from "../remote-env.js";
13
- import { refuseOutOfContractDecision } from "../tool-policy.js";
13
+ import { advertisedWritableRootsSeat, refuseOutOfContractDecision } from "../tool-policy.js";
14
14
  import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
15
15
  import { deliverEngineNotice } from "../types.js";
16
16
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
@@ -571,6 +571,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
571
571
  toolCallId: pendingAction.toolCallId,
572
572
  budget: beltBudget,
573
573
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
574
+ ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
574
575
  ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
575
576
  }, prepared.abortController.signal));
576
577
  const beltUnsafe = rechecked.action === "deny" ||
@@ -632,6 +633,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
632
633
  ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
633
634
  budget: beltBudget,
634
635
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
636
+ ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
635
637
  }, prepared.abortController.signal));
636
638
  if (narrowed.action === "deny") {
637
639
  const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`, prepared.reminderMark);
@@ -651,6 +653,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
651
653
  ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
652
654
  budget: beltBudget,
653
655
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
656
+ ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
654
657
  }))
655
658
  .then((answer) => normalizePersistedRuleHit(answer)), unreadable, { signal: prepared.abortController.signal, timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
656
659
  const tightened = applyPersistedTightening({ action: "allow" }, read);
@@ -104,14 +104,13 @@ export function createSensitivePathPolicy(opts) {
104
104
  return { action: "allow" };
105
105
  const canon = await canonicalizeTarget(opts.env, path, signal, pathFamilyOf({ root: opts.rootPath, cwd: req.cwd ?? opts.env.cwd }), pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }));
106
106
  if (!canon.ok) {
107
- if (canon.unresolvedSymlink) {
108
- return {
109
- action: "deny",
110
- message: `write to "${path}" is blocked: it is a symlink whose real target could not be resolved (it could point onto a guarded sensitive path)`,
111
- decisionReason: "safety",
112
- };
113
- }
114
- return { action: "allow" };
107
+ return {
108
+ action: "deny",
109
+ message: canon.unresolvedSymlink
110
+ ? `write to "${path}" is blocked: it is a symlink whose real target could not be resolved (it could point onto a guarded sensitive path)`
111
+ : `write to "${path}" is blocked: its target could not be resolved on this tree (${canon.refused ?? canon.message}), so the sensitive-path guard cannot judge it`,
112
+ decisionReason: "safety",
113
+ };
115
114
  }
116
115
  let relative = null;
117
116
  for (const rootKey of await dataRootKeys(signal)) {
@@ -1,5 +1,6 @@
1
- import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
1
+ import { readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { isAbsolute, join, relative } from "node:path";
3
+ import { physicalPathOf } from "./physical-path.js";
3
4
  import { isSkillSpecifierEnforced, parseSkillToolEntry } from "./skill-tool-specifier.js";
4
5
  const SKILL_FILE = "SKILL.md";
5
6
  const RESOURCE_DIRS = ["assets", "references", "scripts"];
@@ -200,7 +201,7 @@ function resolveInsideSkill(abs, rel, walk) {
200
201
  let real;
201
202
  let stats;
202
203
  try {
203
- real = realpathSync(abs);
204
+ real = physicalPathOf(abs);
204
205
  stats = statSync(abs);
205
206
  }
206
207
  catch (err) {
@@ -220,7 +221,7 @@ function resolveInsideSkill(abs, rel, walk) {
220
221
  function readAttachments(skillDir, skillName, budgetBytes, warn) {
221
222
  let realRoot;
222
223
  try {
223
- realRoot = realpathSync(skillDir);
224
+ realRoot = physicalPathOf(skillDir);
224
225
  }
225
226
  catch (err) {
226
227
  warn({ code: "read_failed", skill: skillName, detail: `the skill directory could not be resolved (${errText(err)}) — attachments skipped` });
@@ -1,7 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { randomBytes } from "node:crypto";
3
- import { lstatSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { lstatSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, isAbsolute, resolve, sep } from "node:path";
5
+ import { physicalPathOf } from "./physical-path.js";
5
6
  const DEFAULT_GATE_TIMEOUT_MS = 60_000;
6
7
  const GATE_MAX_BUFFER = 16 * 1024 * 1024;
7
8
  const OUTPUT_TAIL_CHARS = 1500;
@@ -139,8 +140,8 @@ export async function runOracle(gates, cwdRoot, opts) {
139
140
  continue;
140
141
  }
141
142
  try {
142
- const physRoot = realpathSync(root);
143
- const phys = realpathSync(candidate);
143
+ const physRoot = physicalPathOf(root);
144
+ const phys = physicalPathOf(candidate);
144
145
  if (phys !== physRoot && !phys.startsWith(physRoot + sep)) {
145
146
  results.push(err(gate, `gate cwd physically escapes the spec root (symlink): ${gate.cwd}`));
146
147
  continue;
@@ -247,7 +248,7 @@ function restoreOneAtomic(p, bytes) {
247
248
  }
248
249
  function safeRealpath(p) {
249
250
  try {
250
- return realpathSync(p);
251
+ return physicalPathOf(p);
251
252
  }
252
253
  catch {
253
254
  return undefined;
@@ -40,7 +40,11 @@ export interface UnifiedTaskOutput {
40
40
  code?: string;
41
41
  /** RB-386② — machine-readable failure code on a FAILED background_agent row's poll
42
42
  * details (the child's TaskResult.errorCode taxonomy — brain codes / `limit.*` / `budget.*` …).
43
- * Additive; absent on non-failed rows, on rows whose failure carried no code, and on other lanes. */
43
+ * Additive; absent on non-failed rows, on rows whose failure carried no code, and on other lanes.
44
+ * The same key also carries a failed WORKFLOW run's `WorkflowRun.errorCode` (
45
+ * `workflow.*` for the orchestration's own refusals / a failed child's terminal code), populated
46
+ * by `formatWorkflowRun`: one field name for "why this poll answer is a failure", whichever lane
47
+ * minted the failure. */
44
48
  errorCode?: string;
45
49
  /** RB-386② — retryability verdict for a FAILED background_agent row, derived at settle time by the
46
50
  * same classifier the sync sub-agent report's `error_kind (retryable: …)` line uses. Additive. */
@@ -259,6 +259,7 @@ export function formatWorkflowRun(run) {
259
259
  status: run.status,
260
260
  retrieval_status: "success",
261
261
  ...(run.completionId !== undefined ? { completionId: run.completionId } : {}),
262
+ ...(run.errorCode !== undefined ? { errorCode: run.errorCode } : {}),
262
263
  details: summary,
263
264
  },
264
265
  };
@@ -165,7 +165,7 @@ export function toolFace(id) {
165
165
  const e = byId.get(id);
166
166
  if (e === undefined)
167
167
  throw new Error(`tool catalog: no definition ${JSON.stringify(id)}`);
168
- return structuredClone(e.face);
168
+ return { contentOrigin: "local", ...structuredClone(e.face) };
169
169
  }
170
170
  function deepFreeze(value) {
171
171
  if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
@@ -58,6 +58,30 @@ export interface ToolCallRequest {
58
58
  * window — at that declaration, not here.
59
59
  */
60
60
  cwd?: string;
61
+ /**
62
+ * The CANONICAL directories THIS RUN'S PROMPT ADVERTISES AS WRITABLE — the admitted list of the root
63
+ * fence's advertised-writable-directory seat (the memory engine's write home, the deployment's
64
+ * scratchpad), in admission order, deduplicated. Stamped by the Runner on every policy adjudication of
65
+ * the run, read live at adjudication time (a directory admitted by a later phase is on the list the
66
+ * next call carries).
67
+ *
68
+ * WHY IT EXISTS: the fence and the write gate were judging one fact twice. The prompt tells the model
69
+ * "write to it directly with the Write tool" and the fence opens the hole for exactly the directories
70
+ * this list names — but a deployment-authored write gate knew nothing of them, so every advertised
71
+ * write still asked (a memory entry plus its index = two questions the person was told would not be
72
+ * asked). Reading THIS field makes the gate's exemption domain the same list the fence admitted,
73
+ * instead of a second copy the deployment maintains by hand.
74
+ *
75
+ * A containment gate reads it EXACTLY as {@link import("./fs-write-gate-policy.js").FsWriteGatePolicyOptions.exemptDirs}:
76
+ * a covered write whose canonical target lands inside one of these directories is allowed and not
77
+ * asked. It grants nothing else — a DENY composed alongside (sensitive-path, an org rule) still
78
+ * outranks it under the deny > ask > allow fold, and an unresolvable target still fails closed.
79
+ *
80
+ * ABSENT outside a Runner, or when the run has no hands, or when nothing was advertised. Absence is NO
81
+ * exemption, never a wildcard (an empty list reads the same). Read-only — a policy cannot advertise a
82
+ * directory by writing one back.
83
+ */
84
+ advertisedWritableRoots?: readonly string[];
61
85
  /**
62
86
  * design/80 D-E-core: read-only per-run budget snapshot so a STATELESS policy can self-limit against
63
87
  * state that survives suspend/resume (a fresh in-memory counter would reset every leg — the supervisor
@@ -104,6 +128,23 @@ export interface ToolCallRequest {
104
128
  /** `read_only` (#619): the shell tool's command was PROVABLY read-only by the engine's transcribed upstream
105
129
  * tables (`readOnlyShellVerdict`) and the allow layer let it run without a question — after every
106
130
  * deny/ask lane and the person's own allow rules, never over a mandated ask. */
131
+ /**
132
+ * Stamp {@link ToolCallRequest.advertisedWritableRoots} from the run's advertised-writable-directory seat
133
+ * — the ONE place the seat becomes a request field, so every adjudication station stamps it alike (the
134
+ * `callFaceSeat` shape).
135
+ *
136
+ * The list is COPIED AND FROZEN here, and that is load-bearing rather than tidy. The seat's `roots` getter
137
+ * hands back its own admission ledger, and {@link combinePolicies} walks every layer with ONE request
138
+ * object: a layer handed the live array could append to the very domain the write gate exempts, widening
139
+ * it for that call and for every later one. `readonly` is a compile-time word; this is the runtime one.
140
+ * (What a frozen array does not close is a layer REPLACING the whole field on the shared request — the
141
+ * pre-existing shape that `cwd`, `budget` and `face` live with; it belongs to the fold, not to this seat.)
142
+ */
143
+ export declare function advertisedWritableRootsSeat(seat: {
144
+ readonly roots: readonly string[];
145
+ } | undefined): {
146
+ advertisedWritableRoots?: readonly string[];
147
+ };
107
148
  declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
108
149
  export type DecisionReason = (typeof DECISION_REASONS)[number];
109
150
  /** Carry every engine attestation (settlement, classifier cause) from a decision onto a reconstruction of it — the
@@ -1419,6 +1460,21 @@ export declare function durableMandateSourceOf(args: {
1419
1460
  durableApprovalDeclared: boolean;
1420
1461
  onAsk: OnAsk | undefined;
1421
1462
  }): DurableMandateSource | undefined;
1463
+ /**
1464
+ * Has this leg DECLARED a durable park opt-in, from EITHER source (the per-principal
1465
+ * entitlement or the task's own `durableApproval`) — with NO live-seat narrowing. Deliberately
1466
+ * NOT {@link durableMandateSourceOf}: that one asks "is every ask MANDATED to gate durably" (a
1467
+ * live approver seat beside a mere `durableApproval` opt-in defeats the mandate), while this one
1468
+ * asks "does this leg have a durable park FACILITY at all" — the question-face mount decision and
1469
+ * the inherited-mandate float both need the bare declaration, seat or no seat, and had each spelled
1470
+ * `forceDurableGate === true || durableApprovalDeclared` by hand (two call sites, `prepare-
1471
+ * question-face.ts` and `prepare-policy-chain.ts`, one drifting from the other silently if either
1472
+ * grows a third source or a typo'd operator).
1473
+ */
1474
+ export declare function durableParkOptInDeclared(args: {
1475
+ forceDurableGate: boolean;
1476
+ durableApprovalDeclared: boolean;
1477
+ }): boolean;
1422
1478
  /** EXACT structural clone for the approval boundary (alias isolation) — fail-closed, never lossy,
1423
1479
  * never identity: a JSON round-trip would silently DROP unclonable members (presented ≠ executing =
1424
1480
  * consent misrepresentation) and an identity fallback would hand the approver the live executing
@@ -10,6 +10,9 @@ import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./unt
10
10
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
11
11
  import { protocolOf } from "./protocol-table.js";
12
12
  import { isAbsoluteForFamily, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
13
+ export function advertisedWritableRootsSeat(seat) {
14
+ return seat === undefined ? {} : { advertisedWritableRoots: Object.freeze([...seat.roots]) };
15
+ }
13
16
  const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
14
17
  const DECISION_REASON_SET = new Set(DECISION_REASONS);
15
18
  function settledByNobody(kind) {
@@ -914,6 +917,9 @@ export function durableMandateSourceOf(args) {
914
917
  return "durable_approval";
915
918
  return undefined;
916
919
  }
920
+ export function durableParkOptInDeclared(args) {
921
+ return args.forceDurableGate || args.durableApprovalDeclared;
922
+ }
917
923
  export function tryCloneArgs(v) {
918
924
  try {
919
925
  const value = structuredClone(v);
@@ -1,13 +1,14 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { closeSync, constants, createReadStream, mkdtempSync, openSync, readSync, statSync, truncateSync, unlinkSync } from "node:fs";
4
- import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, realpath, rename, rm, unlink, writeFile, } from "node:fs/promises";
4
+ import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, rename, rm, unlink, writeFile, } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { isAbsolute, join, resolve } from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
9
9
  import { killProcessTree, shutdownDebug } from "./kill-tree.js";
10
10
  import { scrubSecretEnv } from "../../core/secret-env.js";
11
+ import { physicalPathOfAsync } from "../../core/physical-path.js";
11
12
  import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
12
13
  import { BackgroundShellError } from "../../core/background-shell.js";
13
14
  import { SchedulerError } from "../../core/scheduler.js";
@@ -802,7 +803,7 @@ export class NodeExecutionEnv {
802
803
  let hop;
803
804
  try {
804
805
  const link = await readlink(resolved);
805
- const parentReal = await realpath(resolve(resolved, "..")).catch(() => resolve(resolved, ".."));
806
+ const parentReal = await physicalPathOfAsync(resolve(resolved, "..")).catch(() => resolve(resolved, ".."));
806
807
  hop = isAbsolute(link) ? link : resolve(parentReal, link);
807
808
  }
808
809
  catch {
@@ -1004,7 +1005,7 @@ export class NodeExecutionEnv {
1004
1005
  async canonicalPath(path) {
1005
1006
  const resolved = resolvePath(this.cwd, path);
1006
1007
  try {
1007
- return ok(await realpath(resolved));
1008
+ return ok(await physicalPathOfAsync(resolved));
1008
1009
  }
1009
1010
  catch (error) {
1010
1011
  return err(toFileError(error, resolved));
package/dist/index.d.ts CHANGED
@@ -83,6 +83,7 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
83
83
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
84
84
  export type { SecretEnvFinding, SecretEnvFindingKind } from "./core/secret-env.js";
85
85
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
86
+ export { physicalPathOf, physicalPathOfExisting } from "./core/physical-path.js";
86
87
  export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode, WriteExpectation, WriteReceipt } from "./internal/harness.js";
87
88
  export type { ExecResult } from "./internal/harness.js";
88
89
  export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
package/dist/index.js CHANGED
@@ -68,6 +68,7 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
68
68
  export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
69
69
  export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
70
70
  export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
71
+ export { physicalPathOf, physicalPathOfExisting } from "./core/physical-path.js";
71
72
  export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
72
73
  export { withRetry } from "./core/with-retry.js";
73
74
  export { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "./core/git-worktree-env.js";
@@ -1,5 +1,6 @@
1
- import { mkdirSync, readFileSync, realpathSync, writeFileSync, existsSync } from "node:fs";
2
- import { basename, dirname, join, resolve, sep } from "node:path";
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join, resolve, sep } from "node:path";
3
+ import { physicalPathOfExisting } from "../core/physical-path.js";
3
4
  import { sanitizeScope } from "../stores/file/fs-atomic.js";
4
5
  function isPlainObject(v) {
5
6
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -33,23 +34,6 @@ function safeFileStem(id) {
33
34
  function containedBy(root, p) {
34
35
  return p === root || p.startsWith(root + sep);
35
36
  }
36
- function canonicalizeDeepestExisting(p) {
37
- let cur = p;
38
- const missingTail = [];
39
- for (;;) {
40
- try {
41
- const real = realpathSync(cur);
42
- return missingTail.length === 0 ? real : join(real, ...[...missingTail].reverse());
43
- }
44
- catch {
45
- const parent = dirname(cur);
46
- if (parent === cur)
47
- return undefined;
48
- missingTail.push(basename(cur));
49
- cur = parent;
50
- }
51
- }
52
- }
53
37
  export function createFileWorkflowScriptStore(dir) {
54
38
  const root = resolve(dir);
55
39
  const scopeDir = (scope) => join(root, sanitizeScope(scope));
@@ -71,17 +55,17 @@ export function createFileWorkflowScriptStore(dir) {
71
55
  if (!containedBy(base, full)) {
72
56
  throw new Error(`workflow script store: scriptPath is outside this scope's script partition`);
73
57
  }
74
- const physRoot = canonicalizeDeepestExisting(root);
75
- const physBase = canonicalizeDeepestExisting(base);
76
- const physFull = canonicalizeDeepestExisting(full);
58
+ const physRoot = physicalPathOfExisting(root);
59
+ const physBase = physicalPathOfExisting(base);
60
+ const physFull = physicalPathOfExisting(full);
77
61
  const refusePhysical = () => {
78
62
  throw new Error(`workflow script store: scriptPath is outside this scope's script partition after symlink resolution`);
79
63
  };
80
- if (physRoot !== undefined && physBase !== undefined && !containedBy(physRoot, physBase))
64
+ if (!containedBy(physRoot, physBase))
81
65
  refusePhysical();
82
- if (physBase !== undefined && physFull !== undefined && !containedBy(physBase, physFull))
66
+ if (!containedBy(physBase, physFull))
83
67
  refusePhysical();
84
- return readFileSync(physFull ?? full, "utf-8");
68
+ return readFileSync(physFull, "utf-8");
85
69
  },
86
70
  resolveName(name) {
87
71
  if (/^wf_/i.test(name))
@@ -460,6 +460,9 @@ export const workflowResumeClaimFallback = {
460
460
  table.delete(key);
461
461
  },
462
462
  };
463
+ function parkLaneArmed(opts) {
464
+ return opts.store !== undefined && opts.checkpointStore !== undefined;
465
+ }
463
466
  export function startWorkflow(runner, fn, opts = {}, internals) {
464
467
  const alsDepth = currentWorkflowDepth();
465
468
  const internalDepth = internals?.workflowDepth;
@@ -472,10 +475,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
472
475
  if (opts.resumeFromRunId !== undefined && opts.journalStore === undefined) {
473
476
  throw new Error("runWorkflow: resumeFromRunId requires a journalStore to load the prior run's journal");
474
477
  }
475
- if (opts.checkpointStore !== undefined && opts.store === undefined) {
478
+ if (opts.checkpointStore !== undefined && !parkLaneArmed(opts)) {
476
479
  throw new WorkflowParkRefusal("workflow.park_requires_run_store", "runWorkflow: checkpointStore (the store this run's children park in) requires a WorkflowRunStore — a workflow child's park is recorded on its wa* row and proven against the checkpoint store at the next resume, and without a run store there is no durable row for a host to route the parked approval by", {});
477
480
  }
478
- if (opts.defaultDurableApproval !== undefined && (opts.store === undefined || opts.checkpointStore === undefined)) {
481
+ if (opts.defaultDurableApproval !== undefined && !parkLaneArmed(opts)) {
479
482
  throw new WorkflowParkRefusal("workflow.park_requires_run_store", "runWorkflow: defaultDurableApproval requires a WorkflowRunStore and a checkpointStore — a workflow child's park is recorded on its wa* row and read back from the checkpoint store at the next resume; without both there is no durable row for a host to route the parked approval by, or no way for a resume to prove the park", {});
480
483
  }
481
484
  if (opts.parkedResume !== undefined && opts.resumeFromRunId === undefined) {
@@ -917,7 +920,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
917
920
  const seat = s.checkpointStore;
918
921
  if (seat === undefined || seat === "disabled")
919
922
  return;
920
- if (parkSeat === undefined) {
923
+ if (!parkLaneArmed(opts)) {
921
924
  throw new WorkflowParkRefusal("workflow.park_requires_run_store", "workflow agent: the child's checkpointStore would let it park where this workflow cannot read the park back — a workflow whose children can park needs a WorkflowRunStore and that checkpoint store as its read seat (RunWorkflowOptions.store + checkpointStore); wire both, or leave the child's seat unset.", {});
922
925
  }
923
926
  if (seat !== parkSeat) {
@@ -2,21 +2,13 @@ import { readFileSync, readdirSync, mkdirSync, unlinkSync } from "node:fs";
2
2
  import { join, resolve } from "node:path";
3
3
  import { assertJsonMetadata, normalizeTaskShape } from "../../tools/task-list.js";
4
4
  import { atomicWriteFile } from "../file/fs-atomic.js";
5
- import { realpathSync } from "node:fs";
6
- function realpathSyncSafe(p) {
7
- try {
8
- return realpathSync(p);
9
- }
10
- catch {
11
- return p;
12
- }
13
- }
5
+ import { physicalPathOf } from "../../core/physical-path.js";
14
6
  import { withCcLock } from "./lockfile.js";
15
7
  export function createCcFileTaskListStore(listDir) {
16
8
  const dir = resolve(listDir);
17
9
  mkdirSync(dir, { recursive: true });
18
10
  const hwmPath = join(dir, ".highwatermark");
19
- const lockTarget = realpathSyncSafe(dir);
11
+ const lockTarget = physicalPathOf(dir);
20
12
  const CC_TASK_ID_RE = /^[A-Za-z0-9_-]+$/;
21
13
  const validId = (id) => CC_TASK_ID_RE.test(id);
22
14
  const taskPath = (id) => {
@@ -11,10 +11,8 @@ export declare function sanitizePathComponent(raw: string): string;
11
11
  * on-disk dir name, never the stored data. Never `path.join` a raw scope.
12
12
  */
13
13
  export declare function sanitizeScope(scope: string): string;
14
- /**
15
- * Resolve the data root (CC `getClaudeConfigHomeDir` analog): `$AGENT_DATA_DIR ?? ~/.ai-agent`,
16
- * NFC-normalized, and `realpath`-canonicalized once the dir exists. Creates the dir (0o700) if absent.
17
- */
14
+ /** Resolve the data root (CC `getClaudeConfigHomeDir` analog): `$AGENT_DATA_DIR ?? ~/.ai-agent`,
15
+ * NFC-normalized, canonicalized once the dir exists. Creates the dir (0o700) if absent. */
18
16
  export declare function resolveDataRoot(explicit?: string): string;
19
17
  /** Ensure a directory exists with 0o700 perms (idempotent). */
20
18
  /**
@@ -59,25 +57,24 @@ export declare function readJsonlRecords<T>(path: string, onCorrupt?: (info: {
59
57
  * append (the checkpoint commit point ALWAYS fsyncs; session/memory cadence is the caller's `fsyncEvery`).
60
58
  */
61
59
  /**
62
- * RB-144 (2026-07-25, 按面收口): THE canonical key for "one authority per physical location".
63
- *
64
- * Every file backend keeps a module-level table so that N instances over one directory collapse into one
65
- * CAS authority. Getting the KEY wrong reopens the exact defect the table exists to close — and this repo
66
- * has now paid for that four separate times: RB-62 taught the mailbox to realpath, RB-101 taught the
67
- * session store, RB-119 added case folding there, and a defect hunt then found the agent store, the run store
68
- * and the task-list still keying on a lexical `resolve()` while two of them cite the mailbox as the
69
- * precedent they copied. Point fixes kept missing siblings, so the rule now lives in ONE place that all of
70
- * them call.
60
+ * RB-144: THE canonical key for "one authority per physical location". Every file backend keeps a
61
+ * module-level table so that N instances over one directory collapse into one CAS authority; getting the
62
+ * KEY wrong reopens the defect the table exists to close, which this repo paid for four times (mailbox,
63
+ * session store, then the agent/run/task-list stores still keying on a lexical `resolve()`). Point fixes
64
+ * kept missing siblings, so the rule lives in ONE place they all call.
71
65
  *
72
66
  * Two normalizations, each for a demonstrated failure:
73
- * - REALPATH — a symlinked data dir (`/var` → `/private/var` on macOS, a container bind-mount, a linked
67
+ * - PHYSICAL PATH — a symlinked data dir (`/var` → `/private/var` on macOS, a bind-mount, a linked
74
68
  * `~/.ai-agent`) otherwise yields two authorities for one directory, and both writers win the CAS.
75
- * - CASE FOLD — on a case-insensitive filesystem (macOS/Windows default) `Foo/` and `foo/` are the same
76
- * directory; `realpath` does NOT fold case there, so realpath alone is not enough (measured).
69
+ * - CASE FOLD — on a case-insensitive filesystem `Foo/` and `foo/` are the same directory. The mint
70
+ * answers with the on-disk spelling, which folds the two only where the entry EXISTS; the fold here
71
+ * also covers a not-yet-created tail, and folding a PATH would name a different directory on a
72
+ * case-SENSITIVE volume (see the cc task-list store's lock target, which must not fold) — so it stays
73
+ * at this call site, which keys, rather than inside the mint, which answers about paths.
77
74
  *
78
75
  * The key is used ONLY for table lookup — never for I/O, so the on-disk name stays verbatim. A path that
79
- * does not exist yet resolves through its parent directory; if even that fails, the lexical form is used
80
- * (a store must not fail to construct because canonicalization is unavailable).
76
+ * does not exist yet resolves through its deepest existing ancestor; a path with no resolvable ancestor at
77
+ * all keys on its lexical form (a store must not fail to construct because canonicalization is unavailable).
81
78
  */
82
79
  export declare function canonicalStoreKey(p: string): string;
83
80
  export declare class AppendLog {
@@ -1,9 +1,10 @@
1
- import { closeSync, constants as FS, existsSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, readSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync, writeSync, fsyncSync, } from "node:fs";
1
+ import { closeSync, constants as FS, existsSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, readSync, renameSync, truncateSync, unlinkSync, writeFileSync, writeSync, fsyncSync, } from "node:fs";
2
2
  import { homedir, hostname, uptime } from "node:os";
3
3
  import { hrtime } from "node:process";
4
4
  import { basename, dirname, join, resolve as resolvePath } from "node:path";
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { execFileSync } from "node:child_process";
7
+ import { physicalPathOf, physicalPathOfExisting } from "../../core/physical-path.js";
7
8
  const SAFE_COMPONENT = /^[A-Za-z0-9_.-]+$/;
8
9
  export function sanitizePathComponent(raw) {
9
10
  if (raw === "" || raw === "." || raw === ".." || !SAFE_COMPONENT.test(raw)) {
@@ -18,7 +19,7 @@ export function sanitizeScope(scope) {
18
19
  export function resolveDataRoot(explicit) {
19
20
  const raw = (explicit ?? process.env.AGENT_DATA_DIR ?? join(homedir(), ".ai-agent")).normalize("NFC");
20
21
  mkdirSync(raw, { recursive: true, mode: 0o700 });
21
- return realpathSync(raw);
22
+ return physicalPathOf(raw);
22
23
  }
23
24
  export function writeThenLink(target, content) {
24
25
  ensureDir(dirnameOf(target));
@@ -134,18 +135,7 @@ export function readJsonlRecords(path, onCorrupt) {
134
135
  return out;
135
136
  }
136
137
  export function canonicalStoreKey(p) {
137
- const abs = resolvePath(p);
138
- try {
139
- return realpathSync(abs).toLowerCase();
140
- }
141
- catch {
142
- try {
143
- return join(realpathSync(dirname(abs)), basename(abs)).toLowerCase();
144
- }
145
- catch {
146
- return abs.toLowerCase();
147
- }
148
- }
138
+ return physicalPathOfExisting(p).toLowerCase();
149
139
  }
150
140
  export class AppendLog {
151
141
  fd;
@@ -81,17 +81,13 @@ export declare class FileMailboxStore implements MailboxStore {
81
81
  /**
82
82
  * The canonical mutex / shared-state key — instance-independent (X-5).
83
83
  *
84
- * RB-162 (2026-07-26): this went through `realpath` (RB-62) but NOT the case fold (RB-67/RB-119), even
85
- * though the module header above claims "the shared-state KEY goes through `canonicalStoreKey`, which
86
- * also folds case". It did not that name appeared exactly once in this file, inside that sentence.
87
- * The handle component was folded by `boxPath`, but the ROOT was not: two spellings of the same
88
- * directory on a case-insensitive filesystem produced two shared-state slots over one physical file, and
89
- * each allocated its own `seq` the very contract violation the realpath comment above describes,
90
- * reached by the other half of the same canonicalization.
91
- *
92
- * Worse for the record: the enumerative guard written for this exact family tested
93
- * `src.includes("canonicalStoreKey")`, which that one comment satisfied. The guard was green because the
94
- * file talked about the rule.
84
+ * RB-162: this went through the physical path but NOT the case fold, even though the module header
85
+ * claimed otherwise. The handle component was folded by `boxPath` while the ROOT was not, so two
86
+ * spellings of one directory on a case-insensitive filesystem produced two shared-state slots over one
87
+ * physical file, each allocating its own `seq` the same contract violation the RB-62 comment above
88
+ * describes, reached by the other half of the same canonicalization. The enumerative guard written for
89
+ * this family tested `src.includes("canonicalStoreKey")`, which the CLAIM satisfied: a guard that reads
90
+ * prose is green while the code diverges.
95
91
  */
96
92
  private lockKey;
97
93
  /**