@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.
- package/CHANGELOG.md +51 -0
- package/dist/core/fs-write-gate-policy.js +4 -0
- package/dist/core/memory-engine/engine.js +2 -1
- package/dist/core/memory-engine/layout.d.ts +18 -6
- package/dist/core/memory-engine/layout.js +40 -21
- package/dist/core/physical-path.d.ts +37 -0
- package/dist/core/physical-path.js +30 -0
- package/dist/core/runner/contracts.d.ts +3 -1
- package/dist/core/runner/prepare-artifact.d.ts +4 -5
- package/dist/core/runner/prepare-artifact.js +2 -16
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
- package/dist/core/runner/prepare-ask-lane.js +3 -3
- package/dist/core/runner/prepare-park-ask.d.ts +3 -0
- package/dist/core/runner/prepare-park-ask.js +3 -2
- package/dist/core/runner/prepare-policy-chain.js +2 -2
- package/dist/core/runner/prepare-question-face.js +2 -1
- package/dist/core/runner/prepare-task.js +5 -5
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-leg.js +4 -1
- package/dist/core/sensitive-path-policy.js +7 -8
- package/dist/core/skills-directory.js +4 -3
- package/dist/core/spec-contract.js +5 -4
- package/dist/core/task-registry-shared.d.ts +5 -1
- package/dist/core/task-registry-shared.js +1 -0
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-policy.d.ts +56 -0
- package/dist/core/tool-policy.js +6 -0
- package/dist/engine/execution-env/node-execution-env.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/orchestration/workflow-script-store.js +9 -25
- package/dist/orchestration/workflow.js +6 -3
- package/dist/stores/cc/task-list-store.js +2 -10
- package/dist/stores/file/fs-atomic.d.ts +15 -18
- package/dist/stores/file/fs-atomic.js +4 -14
- package/dist/stores/file/mailbox-store.d.ts +7 -11
- package/dist/stores/file/mailbox-store.js +4 -11
- package/dist/tools/artifact/local-stub.js +4 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +19 -1
- package/dist/tools/fs/bash-readonly-classifier.js +413 -12
- package/dist/tools/fs/fs-bash.js +42 -17
- package/package.json +1 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 =
|
|
143
|
-
const phys =
|
|
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
|
|
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
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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,
|
|
2
|
-
import {
|
|
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 =
|
|
75
|
-
const physBase =
|
|
76
|
-
const physFull =
|
|
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 (
|
|
64
|
+
if (!containedBy(physRoot, physBase))
|
|
81
65
|
refusePhysical();
|
|
82
|
-
if (
|
|
66
|
+
if (!containedBy(physBase, physFull))
|
|
83
67
|
refusePhysical();
|
|
84
|
-
return readFileSync(physFull
|
|
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
|
|
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
|
|
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 (
|
|
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 {
|
|
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 =
|
|
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
|
-
*
|
|
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
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
* -
|
|
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
|
|
76
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
/**
|