@sema-agent/core 7.44.0 → 7.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/dist/agents/subagent.js +33 -14
  3. package/dist/agents/tool-filter.d.ts +65 -4
  4. package/dist/agents/tool-filter.js +127 -4
  5. package/dist/core/agent-definition.d.ts +2 -2
  6. package/dist/core/allow-entry.d.ts +25 -1
  7. package/dist/core/allow-entry.js +29 -4
  8. package/dist/core/ask-origin.d.ts +23 -4
  9. package/dist/core/ask-origin.js +10 -4
  10. package/dist/core/checkpoint-store.d.ts +2 -2
  11. package/dist/core/decision-receipt.js +1 -1
  12. package/dist/core/effective-path-target.d.ts +7 -6
  13. package/dist/core/effective-path-target.js +3 -7
  14. package/dist/core/engine-notice.d.ts +20 -3
  15. package/dist/core/engine-notice.js +12 -6
  16. package/dist/core/gate-lanes.js +2 -0
  17. package/dist/core/gate-outcome.d.ts +5 -0
  18. package/dist/core/gate-outcome.js +2 -0
  19. package/dist/core/hooks.d.ts +2 -0
  20. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  21. package/dist/core/memory-engine/content-origin.js +9 -4
  22. package/dist/core/memory-engine/layout.d.ts +5 -1
  23. package/dist/core/memory-engine/layout.js +1 -0
  24. package/dist/core/permission-rules.d.ts +4 -0
  25. package/dist/core/permission-rules.js +5 -2
  26. package/dist/core/real-approval.d.ts +9 -6
  27. package/dist/core/real-approval.js +5 -5
  28. package/dist/core/runner/child-tree.d.ts +13 -0
  29. package/dist/core/runner/child-tree.js +3 -0
  30. package/dist/core/runner/contracts.d.ts +8 -2
  31. package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
  32. package/dist/core/runner/prepare-ask-lane.js +2 -2
  33. package/dist/core/runner/prepare-config-doors.js +3 -1
  34. package/dist/core/runner/prepare-execution-env.js +2 -1
  35. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  36. package/dist/core/runner/prepare-gate-stations.js +2 -1
  37. package/dist/core/runner/prepare-listings.d.ts +4 -4
  38. package/dist/core/runner/prepare-listings.js +2 -22
  39. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  40. package/dist/core/runner/prepare-park-ask.js +2 -2
  41. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  42. package/dist/core/runner/prepare-policy-chain.js +4 -2
  43. package/dist/core/runner/prepare-task.js +10 -6
  44. package/dist/core/runner/resume-policy-outcome.js +2 -1
  45. package/dist/core/runner/run-leg.js +1 -1
  46. package/dist/core/runner/run-notification-lane.js +2 -2
  47. package/dist/core/runner/runtask.d.ts +2 -0
  48. package/dist/core/runner/runtask.js +2 -0
  49. package/dist/core/runner/stream-steer-verb.js +2 -2
  50. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  51. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  52. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  53. package/dist/core/runner/synthetic-tools.js +3 -2
  54. package/dist/core/session-policy-store.d.ts +6 -2
  55. package/dist/core/session-policy-store.js +2 -1
  56. package/dist/core/skill-grant.d.ts +10 -2
  57. package/dist/core/skill-grant.js +20 -4
  58. package/dist/core/task-notification.d.ts +12 -1
  59. package/dist/core/task-notification.js +13 -1
  60. package/dist/core/task-spec.d.ts +5 -3
  61. package/dist/core/tool-policy.d.ts +15 -4
  62. package/dist/core/tool-policy.js +9 -13
  63. package/dist/core/tool-spec.d.ts +20 -2
  64. package/dist/core/types.d.ts +1 -1
  65. package/dist/core/types.js +1 -1
  66. package/dist/index.d.ts +5 -3
  67. package/dist/index.js +5 -3
  68. package/dist/orchestration/workflow.js +2 -1
  69. package/dist/tools/fs/removal-targets.d.ts +82 -0
  70. package/dist/tools/fs/removal-targets.js +236 -0
  71. package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
  72. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  73. package/package.json +1 -1
  74. package/test/export-surface.snapshot.json +33 -1
  75. package/wire-changes/core@7.45.0.json +1084 -0
@@ -26,6 +26,7 @@ import { AllowEntryTable, screenAllowedToolsValue } from "../allow-entry.js";
26
26
  import { compileOutputSchema } from "./strict-output-schema.js";
27
27
  import { MEMORY_ENGINE_TOOL_NAMES } from "../memory-engine/tools.js";
28
28
  import { foldAdmissionFreeze } from "../memory-admission.js";
29
+ import { deploymentCanRerootChild } from "./child-tree.js";
29
30
  import { prepareMemory } from "./prepare-memory.js";
30
31
  import { prepareMemoryEngineSession } from "./prepare-memory-engine-session.js";
31
32
  import { prepareDeferClassify } from "./prepare-defer-classify.js";
@@ -363,9 +364,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
363
364
  e.code = "isolation.unavailable";
364
365
  throw e;
365
366
  };
366
- if (ownedEnvSeat.current === undefined) {
367
+ if (!deploymentCanRerootChild(deps)) {
367
368
  failIsolation("no executionEnvFactory is configured (a static/shared execution env cannot mint a per-agent worktree)");
368
369
  }
370
+ else if (ownedEnvSeat.current === undefined) {
371
+ failIsolation("the executionEnvFactory returned no env instead of a worktree-rooted env");
372
+ }
369
373
  else if (ownedEnvSeat.current === deps.executionEnv) {
370
374
  ownedEnvSeat.current = undefined;
371
375
  failIsolation("the executionEnvFactory returned the shared static env instead of a worktree-rooted env");
@@ -733,7 +737,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
733
737
  releaseSignal = () => sig.removeEventListener("abort", onExternalAbort);
734
738
  }
735
739
  }
736
- const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, skillGrantLane, allowEntryLanes, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, grantBindingOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, skillSpecs, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider: () => autoModeDeciderRef.current, permissionMode, scratchpadDir: spec.envFacts?.scratchpadDir, toolRosterDeltas: rosterSeat, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
740
+ const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, skillGrantLane, allowEntryLanes, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, grantBindingOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, skillGrants, skillSpecs, allowedToolTables, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider: () => autoModeDeciderRef.current, readFace: () => resolvedReadFace, permissionMode, scratchpadDir: spec.envFacts?.scratchpadDir, toolRosterDeltas: rosterSeat, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
737
741
  const { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, durableApproval, wiringManifest, writeProtection, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace, sessionBoxBound } = prepareWiringManifest({ spec, deps, askSeat: doors.askSeat, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, shellReadStationArmed: handsReadFace.shellReadStationArmed, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, harness, pausedRef, liveInheritedGate, delegation, toolRoster, readDenyBuiltinTiersActive: handsReadFace.readDenyBuiltinTiersActive, lspManagerWired, lspDiagnosticsLive: lspDiagnostics !== undefined, fileHistoryFact, sessionBox });
738
742
  const writeProtectionCheck = writeProtection?.check;
739
743
  writeProtectionJudgeRef.current = writeProtection?.judgeTarget;
@@ -743,18 +747,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
743
747
  return undefined;
744
748
  });
745
749
  }
746
- const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking: autoModeDenialTrackingRef, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot: handsReadFace.grantReadRoot });
750
+ const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, handsRoot: attachmentRootCanonical, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, grantBindingOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking: autoModeDenialTrackingRef, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot: handsReadFace.grantReadRoot });
747
751
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, skillGrantCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, permissionMode, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
748
752
  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 });
749
- 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, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef });
753
+ const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, handsRoot: attachmentRootCanonical, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, grantBindingOf, now, humanReviewRef, abortController, harness, pausedRef });
750
754
  const seatInputsFor = skillGateWiring({ tools, skillToolName: SKILL_TOOL_NAME, seatOf: skillSeatOf, mode: () => permissionMode.current, lane: skillGrantLane, onAllowed: (info) => emitTrace(deps.tracer, () => ({ kind: "permission.skill_allowed", version: 1, taskId: hostTaskId, ...info, entries: [...info.entries], ts: Date.now() })), allowEntries: { deploymentAllows: { ...allowEntryLanes.deployment, onAllowed: (info) => emitTrace(deps.tracer, () => ({ kind: "permission.allowed_tools_allowed", version: 1, taskId: hostTaskId, source: "deployment", ...info, entries: [...info.entries], ts: Date.now() })) }, requestAllows: { ...allowEntryLanes.request, onAllowed: (info) => emitTrace(deps.tracer, () => ({ kind: "permission.allowed_tools_allowed", version: 1, taskId: hostTaskId, source: "request", ...info, entries: [...info.entries], ts: Date.now() })) } } });
751
- const { approvedCallScreen } = 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, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, readDenyInForce: () => ({ tiers: handsReadFace.readDenyBuiltinTiersActive, rows: [...readDenyAdditionsNormalized.map((e) => e.pattern), ...handsReadFaceInput.pathDenyRules.filter((r) => r.faces.includes("read")).map((r) => r.rule)] }), hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider: autoModeDeciderRef, autoModeDenialTracking: autoModeDenialTrackingRef, askRoute: { ...wiringManifest.ask, effective: askSeatEffective(wiringManifest) }, permissionMode, toolMarkSources, ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}), ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, seatInputsFor, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement });
755
+ const { approvedCallScreen } = 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, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, readDenyInForce: () => ({ tiers: handsReadFace.readDenyBuiltinTiersActive, rows: [...readDenyAdditionsNormalized.map((e) => e.pattern), ...handsReadFaceInput.pathDenyRules.filter((r) => r.faces.includes("read")).map((r) => r.rule)] }), hookEnvFace, writeProtectionCheck, shellReadCheck, readFace: () => resolvedReadFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider: autoModeDeciderRef, autoModeDenialTracking: autoModeDenialTrackingRef, askRoute: { ...wiringManifest.ask, effective: askSeatEffective(wiringManifest) }, permissionMode, toolMarkSources, ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}), ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, seatInputsFor, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement });
752
756
  const postureFrameRef = { report: (err) => deps.onError?.(err, { phase: "degraded", sessionId }) };
753
757
  const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, toolRosterDeltas: rosterSeat, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, contextOverlay: legContextOverlay, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
754
758
  const preparedHolder = {};
755
759
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveWriteFaceObserved, 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, skillGrants: skillGrantLane, callerPolicy: lockedPreflight.toolPolicy, inheritedPolicies: (inheritedParentConstraints ?? []).map((c) => c.policy), persistedRuleLane: permissionRuleLane, orgRuleLane: permissionRuleOrgLane, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, carrierWriteFace, readDenyAdditionsNormalized, preparedHolder });
756
760
  await onceLedger.settle(session, announcedListingsRef);
757
- const buildPrepared = () => ({ harness, session, sessionId, runId, registryAccess: { owner: hostTaskId, scope: taskScope }, reminderMark, reminderDisclosureCounts, fullShellReachable, approvalPosture: { get mode() { return permissionMode.current; }, askEffective: askSeatEffective(wiringManifest) }, ...(outputStyleFrame !== undefined ? { outputStyleFrame } : {}), permissionMode, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(mcpRedial !== undefined ? { mcpRedial } : {}), ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved, effectiveWriteFace: effectiveWriteFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, toolProgressRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, skillGrants: skillGrantLane, skillSpecs, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, endModeCeiling, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, undeliveredContinuationRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, stopForDenialLimit, suspendForReview, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}), 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 } } : {}), stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, frameReceipts: { pending: [], listingSnapshotOwed: false }, frameSettlement, ...(peerLaneRefs.boundaryRound !== undefined ? { peerBoundaryRound: peerLaneRefs.boundaryRound } : {}), gitStatusRef, postureFrameRef, listBackgroundTasks, hookIdentity, ...(sessionBoxBound !== undefined ? { sessionBox: sessionBoxBound } : {}), hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}), ...(shellReadCheck !== undefined ? { shellReadCheck } : {}), ...(toolDisclosureSeat !== undefined ? { toolDisclosure: toolDisclosureSeat } : {}), ...(approvedCallScreen !== undefined ? { approvedCallScreen } : {}) });
761
+ const buildPrepared = () => ({ harness, session, sessionId, runId, registryAccess: { owner: hostTaskId, scope: taskScope }, reminderMark, reminderDisclosureCounts, fullShellReachable, approvalPosture: { get mode() { return permissionMode.current; }, askEffective: askSeatEffective(wiringManifest) }, ...(outputStyleFrame !== undefined ? { outputStyleFrame } : {}), permissionMode, editedFilesSnapshot, taskRootPath: taskRootFinal, model, thinking, compModel: effectiveCompModel, mcp, ...(mcpRedial !== undefined ? { mcpRedial } : {}), ...(a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, gateOutcomes, batchHaltRef, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(fileHistoryBoundary !== undefined ? { fileHistoryBoundary } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved, effectiveWriteFace: effectiveWriteFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(attachmentRootCanonical !== undefined ? { handsRoot: attachmentRootCanonical } : {}), toolProgressRef, ...(advertisedWritableDirs !== undefined ? { advertisedWritableDirs } : {}), ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), ...(sealReadStateSeat !== undefined ? { sealReadStateSeat } : {}), denyNarrowingPolicy, skillGrants: skillGrantLane, skillSpecs, ...(permissionRuleLane !== undefined ? { persistedRuleLane: permissionRuleLane } : {}), ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, endModeCeiling, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, pausedRef, undeliveredContinuationRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, stopForDenialLimit, suspendForReview, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}), 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 } } : {}), stopRequestedRef, announcedSnapshotRecovered: onceLedger.recovered, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(projectInstructionContent !== undefined ? { projectInstructionContent } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, frameReceipts: { pending: [], listingSnapshotOwed: false }, frameSettlement, ...(peerLaneRefs.boundaryRound !== undefined ? { peerBoundaryRound: peerLaneRefs.boundaryRound } : {}), gitStatusRef, postureFrameRef, listBackgroundTasks, hookIdentity, ...(sessionBoxBound !== undefined ? { sessionBox: sessionBoxBound } : {}), hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}), ...(shellReadCheck !== undefined ? { shellReadCheck } : {}), ...(toolDisclosureSeat !== undefined ? { toolDisclosure: toolDisclosureSeat } : {}), ...(approvedCallScreen !== undefined ? { approvedCallScreen } : {}) });
758
762
  rollback.commit();
759
763
  commitModeSeats(abortController.signal);
760
764
  const prepared = buildPrepared();
@@ -1,4 +1,5 @@
1
1
  import { canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace } from "../ask-question.js";
2
+ import { isHostDecider } from "../gate-outcome.js";
2
3
  import { CHECKPOINT_FEATURES, CheckpointError, checkpointVersionOf, winnerFromOutcome } from "../checkpoint-store.js";
3
4
  import { policyCannotAnswer, screenApproverAttribution } from "../tool-policy.js";
4
5
  import { sanitizeUntrustedText } from "../untrusted-text.js";
@@ -52,7 +53,7 @@ export function resumePolicyOutcome(input) {
52
53
  throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a policy_ask decide names who decided it ({ decidedBy: "person" | "sla_timeout" | "policy", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
53
54
  }
54
55
  const decidedBy = hostDecisionRaw.decidedBy;
55
- if (decidedBy !== "person" && decidedBy !== "sla_timeout" && decidedBy !== "policy") {
56
+ if (!isHostDecider(decidedBy)) {
56
57
  throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "${describeSuppliedValue(decidedBy)}" is outside the closed set — it is exactly "person", "sla_timeout" or "policy"; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
57
58
  }
58
59
  const cannotAllow = decision !== "allow"
@@ -614,7 +614,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
614
614
  }
615
615
  const executedArgs = humanRewrite !== undefined ? resolvedArgs : screened !== undefined ? screened.args : resolvedArgs;
616
616
  const unseen = !deepJsonEqual(executedArgs, pendingAction.args);
617
- const beltReq = () => ({ toolName: pendingAction.toolName, args: executedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)), budget: beltBudget, ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}), ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs) });
617
+ const beltReq = () => ({ toolName: pendingAction.toolName, args: executedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)), budget: beltBudget, ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}), ...(prepared.handsRoot !== undefined ? { root: prepared.handsRoot } : {}), ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs) });
618
618
  const unjudgeable = unseen ? chainCannotJudge(resume.cp.state.inheritedGate) : undefined;
619
619
  if (unjudgeable !== undefined) {
620
620
  const chainDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: its input changed after the approval and the inherited ancestor chain cannot judge the changed input (${unjudgeable}). This approval is spent — the call has to be re-issued.`, prepared.reminderMark);
@@ -1,6 +1,6 @@
1
1
  import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
2
2
  import { boundInputHashOf } from "../canonical-json.js";
3
- import { createLiveNotificationLane, DurableAppendObligations, isDelegatedAgentTerminal, isTerminalTaskNotification, renderTaskNotificationXml, routeOrParkNotification, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
3
+ import { createLiveNotificationLane, DurableAppendObligations, injectionLaneOf, isDelegatedAgentTerminal, isTerminalTaskNotification, renderTaskNotificationXml, routeOrParkNotification, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
4
4
  import { ToolDetachHub } from "../tool-detach.js";
5
5
  import { pendingContentAskCallId } from "./decide-continuation.js";
6
6
  export function runNotificationLane(input) {
@@ -76,7 +76,7 @@ async function runNotificationLaneAs(input, obligations) {
76
76
  engineNoteKey: taskNotificationDedupKey(item.payload),
77
77
  ...(item.payload.task_type !== "external" && isTerminalTaskNotification(item.payload) ? { capPreferred: true } : {}),
78
78
  };
79
- const deliver = item.priority === "later"
79
+ const deliver = injectionLaneOf(item.priority) === "followUp"
80
80
  ? notificationHarness.followUp(xml, noteOptions)
81
81
  : notificationHarness.steer(xml, { ...noteOptions, ...(item.priority === "now" ? { immediate: true } : {}) });
82
82
  void deliver.then(() => item.onDisposition?.("queued"), () => {
@@ -68,6 +68,8 @@ export declare class Runner implements RunnerSelfSeat {
68
68
  * thin compositions OVER the Runner (`runGoal`) can emit at their terminal state without reaching
69
69
  * into private deps. */
70
70
  emitTaskOutcome(outcome: TaskOutcome): void;
71
+ /** Whether a child this Runner runs can be placed in another tree: {@link RunnerSelfSeat.canRerootChild}. */
72
+ canRerootChild(): boolean;
71
73
  /** The deployment agent catalog (RunnerDeps.agents/builtinAgents), exposed READ-ONLY
72
74
  * so the Agent-tool lane (`createSubagentTool`) defaults to the SAME registry the workflow lane
73
75
  * consumes (single source; the types.ts "pass the same array" follow-on made structural). The array
@@ -62,6 +62,7 @@ import { runCompactionMachinery } from "./run-compaction-machinery.js";
62
62
  import { runStopAndFinalVerify } from "./run-stop-and-final-verify.js";
63
63
  import { runRecoveryLanes } from "./run-recovery-lanes.js";
64
64
  import { runGitLane } from "./run-git-lane.js";
65
+ import { deploymentCanRerootChild } from "./child-tree.js";
65
66
  import { thrownText } from "../thrown-value.js";
66
67
  export class Runner {
67
68
  deps;
@@ -125,6 +126,7 @@ export class Runner {
125
126
  emitTaskOutcome(outcome) {
126
127
  emitTaskOutcome(this.deps, outcome);
127
128
  }
129
+ canRerootChild() { return deploymentCanRerootChild(this.deps); }
128
130
  get agentCatalog() {
129
131
  return {
130
132
  ...(this.deps.agents ? { agents: [...this.deps.agents] } : {}),
@@ -3,7 +3,7 @@ import { snapshotActorAssertion } from "../../internal/llm.js";
3
3
  import { MAX_STEER_INPUT_ID_CHARS } from "../checkpoint-store.js";
4
4
  import { formatHookFeedback, hookSeatExpiredError, runHookSeat } from "../hooks.js";
5
5
  import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
6
- import { isSystemInjectionPriority, steerPriorityOf, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
6
+ import { injectionLaneOf, isSystemInjectionPriority, steerPriorityOf, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js";
7
7
  import { deliverEngineNotice } from "../types.js";
8
8
  import { inlineUntrusted, sanitizeUntrustedText } from "../untrusted-text.js";
9
9
  import { announceAcceptance, armAcceptance, nextHumanInputSeq } from "./steer-admission.js";
@@ -133,7 +133,7 @@ export function streamSteerVerb(input) {
133
133
  const noteOptions = { provenance: "engine-note", callerAuthored: true, parkRecord, ...(actor !== undefined ? { actor } : {}) };
134
134
  if (mintsAFrame)
135
135
  armAcceptance(parkRecord, () => publishQueued(h));
136
- if (priority === "later") {
136
+ if (injectionLaneOf(priority) === "followUp") {
137
137
  await h.harness.followUp(payload, noteOptions);
138
138
  noteAccepted();
139
139
  return;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The sub-agent tool lists of one prepared task: the allow advisory for `TaskSpec.agents` and THE judgment of every denied-tools
3
+ * list a deployment writes — the task's own definitions (`TaskSpec.agents`), the definitions a mounted delegation tool carries
4
+ * (the `authored` faces — the engine's built-ins name tools of other vocabularies and are not judged) and its tool-level list.
5
+ * Universe = the task's assembled roster plus every mounted delegation tool's pool, names and aliases; a pool the engine cannot
6
+ * list (an `extraTools` factory) turns the refusals that need a roster into host notices. A tool's own lists are judged where
7
+ * the tool is mounted: only a tool a deployment mounted (depth 0) publishes them (`authored`, `agentToolLevelAllowTools`, `agentToolLevelDenyTools`), the
8
+ * engine's nested copy for a delegated child carries neither.
9
+ *
10
+ * Throws `config.agent.unknown_tool` (`refused`, the one site) — synchronous, like the phase that calls it.
11
+ */
12
+ import type { AgentTool } from "../../internal/harness.js";
13
+ import type { TaskSpec } from "../types.js";
14
+ import type { AnnounceOnceSink } from "./contracts.js";
15
+ /**
16
+ * The names a definition's denied-tools entries stand for, for a lane that has neither a roster nor a notice sink at the point
17
+ * it builds its own policy (a workflow leg's agentType): each entry is read by the one reading, so `Tool(content)` denies the
18
+ * whole tool there too, and an entry that names no tool is refused by the same refusal. The stale-name and told dispositions
19
+ * need the leg's roster and a sink; the leg's own policy-name audit is what reads them.
20
+ */
21
+ export declare function deniedToolNamesOf(where: string, value: unknown): string[];
22
+ export interface SubAgentToolListsInput {
23
+ /** borrowed-readonly — the REBOUND spec: `agents` (the task's definitions) and `tools` (the mounted delegation tools). */
24
+ spec: Pick<TaskSpec, "agents" | "tools">;
25
+ /** borrowed-readonly — the FULLY ASSEMBLED mount array (names + aliases = the roster half of the universe). */
26
+ tools: ReadonlyArray<AgentTool>;
27
+ /** borrowed-readonly — the once-per-session ledger whose operator sink carries the notices. */
28
+ onceLedger: Pick<AnnounceOnceSink, "onError">;
29
+ /** borrowed-readonly — the acquired session's id (operator-line context). */
30
+ sessionId: string;
31
+ }
32
+ export declare function auditSubAgentToolLists(input: SubAgentToolListsInput): void;
@@ -0,0 +1,121 @@
1
+ import { notAListSentence, notAStringSentence, readDeniedEntry, readToolNameList, valueTypeLabel } from "../../agents/tool-filter.js";
2
+ import { compileAllowList, inertEntryPhrase, renderListEntry } from "../allow-entry.js";
3
+ import { inlineUntrusted } from "../untrusted-text.js";
4
+ const DENY_LIST_NAMES_ONLY = "To block by path or command, write a deny rule in the caller's permission policy: a denied-tools list only takes tool names (it narrows which tools the agent is handed).";
5
+ function refused(message) {
6
+ const e = new Error(message);
7
+ e.code = "config.agent.unknown_tool";
8
+ return e;
9
+ }
10
+ function refusal(where, flaw) {
11
+ return refused(`${where} declares tool ${inertEntryPhrase(flaw)} in its denied tools, but no tool of that spelling exists in this deployment — fix the entry or mount the tool.${flaw.reason === "grammar" ? ` ${DENY_LIST_NAMES_ONLY}` : ""}`);
12
+ }
13
+ function refusalNotAString(where, side, index, type) {
14
+ return refused(notAStringSentence(where, side, index, type));
15
+ }
16
+ function refusalNotAList(where, side, list) {
17
+ return refused(notAListSentence(where, side, list));
18
+ }
19
+ export function deniedToolNamesOf(where, value) {
20
+ const list = readToolNameList(value);
21
+ if (list.kind !== "entries")
22
+ throw refusalNotAList(where, "denied", list);
23
+ return list.entries.map((entry, index) => {
24
+ if (entry === "*")
25
+ return entry;
26
+ const read = readDeniedEntry(entry);
27
+ if (read.kind === "not_a_string")
28
+ throw refusalNotAString(where, "denied", index, read.type);
29
+ const flaw = compileAllowList([read.name], "deny").inert[0];
30
+ if (flaw !== undefined && flaw.reason === "grammar")
31
+ throw refusal(where, { ...flaw, entry: read.name });
32
+ return read.name;
33
+ });
34
+ }
35
+ function judgeDeniedToolNames(where, value, universe, advise) {
36
+ if (value === undefined || value === null)
37
+ return;
38
+ const list = readToolNameList(value);
39
+ if (list.kind !== "entries")
40
+ throw refusalNotAList(where, "denied", list);
41
+ let index = -1;
42
+ for (const entry of list.entries) {
43
+ index += 1;
44
+ if (entry === "*")
45
+ continue;
46
+ const read = readDeniedEntry(entry);
47
+ if (read.kind === "not_a_string")
48
+ throw refusalNotAString(where, "denied", index, read.type);
49
+ const { name, content } = read;
50
+ const raw = entry;
51
+ const { rows, inert } = compileAllowList([name], "deny", universe.known);
52
+ const flaw = inert[0] === undefined ? undefined : { ...inert[0], entry: raw };
53
+ const stale = flaw !== undefined && (flaw.reason === "tool_name" || flaw.reason === "near_miss");
54
+ if (flaw !== undefined && (flaw.reason === "grammar" || (stale && universe.complete)))
55
+ throw refusal(where, flaw);
56
+ if (flaw === undefined && universe.known.has(name)) {
57
+ if (content)
58
+ advise(new Error(`${where} declares ${renderListEntry(raw)} in its denied tools: this list reads tool names, not content, so the whole tool "${inlineUntrusted(name, 80)}" is denied to the agent. ${DENY_LIST_NAMES_ONLY}`));
59
+ continue;
60
+ }
61
+ advise(new Error(`${where} declares tool ${flaw !== undefined ? inertEntryPhrase(flaw) : renderListEntry(raw)} in its denied tools, a name this task's roster and delegation pools do not carry — the entry hides nothing here. Advisory only: a list shared across deployments is not wrong because one lacks a tool, but a misspelling guards nothing.` +
62
+ (rows[0]?.compiled.kind === "namespace" ? " This list matches tool names exactly: a server-level entry hides none of that server's tools; name them, or write a deny rule in the caller's permission policy." : "") +
63
+ (universe.complete ? "" : " A delegation tool this task mounts injects tools the engine cannot enumerate (extraTools), so the name cannot be checked here.")));
64
+ }
65
+ }
66
+ function judgeAllowedToolList(where, value, known, advise) {
67
+ if (value === undefined || value === null)
68
+ return;
69
+ const list = readToolNameList(value);
70
+ if (list.kind !== "entries")
71
+ throw refusalNotAList(where, "allowed", list);
72
+ const unknownNames = [];
73
+ let index = -1;
74
+ for (const entry of list.entries) {
75
+ index += 1;
76
+ if (typeof entry !== "string")
77
+ throw refusalNotAString(where, "allowed", index, valueTypeLabel(entry));
78
+ if (known !== undefined && entry !== "*" && !known.has(entry))
79
+ unknownNames.push(entry);
80
+ }
81
+ if (unknownNames.length === 0)
82
+ return;
83
+ advise(new Error(`${where} allows tool(s) ${unknownNames.map(renderListEntry).join(", ")} not present in this task's assembled roster — likely a typo (the entry would be item-filtered at spawn; the agent stays usable). Advisory only: the delegation pool can differ from this roster, so a tool mounted only on the delegation tool or produced by per-spawn extraTools makes this spurious.`));
84
+ }
85
+ export function auditSubAgentToolLists(input) {
86
+ const { spec, tools, onceLedger, sessionId } = input;
87
+ const advise = (err) => {
88
+ try {
89
+ onceLedger.onError(err, { phase: "config", sessionId });
90
+ }
91
+ catch {
92
+ }
93
+ };
94
+ const delegationTools = (spec.tools ?? []).filter((t) => t.agentToolFaces !== undefined);
95
+ const taskAgents = spec.agents ?? [];
96
+ if (taskAgents.length === 0 && delegationTools.length === 0)
97
+ return;
98
+ const known = new Set(tools.flatMap((t) => [t.name, ...(t.aliases ?? [])]));
99
+ const denyKnown = new Set(known);
100
+ for (const t of delegationTools)
101
+ for (const p of t.agentToolPool ?? [])
102
+ for (const n of [p.name, ...(p.aliases ?? [])])
103
+ denyKnown.add(n);
104
+ const universe = { known: denyKnown, complete: delegationTools.every((t) => t.agentToolPool !== undefined) };
105
+ for (const def of taskAgents) {
106
+ judgeAllowedToolList(`TaskSpec.agents: agent "${def.name}"`, def.allowTools, known, advise);
107
+ judgeDeniedToolNames(`TaskSpec.agents: agent "${def.name}"`, def.denyTools, universe, advise);
108
+ }
109
+ const replaced = new Set(taskAgents.map((d) => d.name));
110
+ for (const t of delegationTools) {
111
+ for (const face of t.agentToolFaces ?? []) {
112
+ if (face.authored === true && !replaced.has(face.name)) {
113
+ const faceWhere = `delegation tool "${t.name}": agent definition "${face.name}"`;
114
+ judgeAllowedToolList(faceWhere, face.allowTools, undefined, advise);
115
+ judgeDeniedToolNames(faceWhere, face.denyTools, universe, advise);
116
+ }
117
+ }
118
+ judgeAllowedToolList(`delegation tool "${t.name}": the tool-level list`, t.agentToolLevelAllowTools, undefined, advise);
119
+ judgeDeniedToolNames(`delegation tool "${t.name}": the tool-level list`, t.agentToolLevelDenyTools, universe, advise);
120
+ }
121
+ }
@@ -110,7 +110,7 @@ export interface SkillListingEntry {
110
110
  * and the tool, so what the prompt promises is exactly what the tool serves. Each field is READ ONCE, BY NAME, and
111
111
  * both consumers see the copy: the gate screens this list while the invoke substitutes from it, so a `get baseDir()`
112
112
  * answering those two reads differently passed a screen that measured a value the model never received — and a rest
113
- * copy would silently drop an INHERITED or non-enumerable list, i.e. lose a skill's grant or its refusals. */
113
+ * copy would silently drop an INHERITED or non-enumerable list. The two tables are read by {@link readSkillTable}. */
114
114
  export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[];
115
115
  /** [c209-C] The skills listing's lead line — single-sourced so the run loop's durable-resume
116
116
  * transcript probe (MED-3② skills arm, runtask) matches EXACTLY what the initial frame carries
@@ -2,7 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool } from "../tools.js";
3
3
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
4
  import { SKILL_LISTING_DEFAULT_BUDGET, SKILL_LISTING_DESC_MAX_CHARS_DEFAULT } from "../skill-listing-budget.js";
5
- import { skillNamedBy } from "../skill-grant.js";
5
+ import { readSkillTable, skillNamedBy } from "../skill-grant.js";
6
6
  import { toolFace } from "../tool-catalog-entries.js";
7
7
  import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
8
8
  import { foldListingWithin } from "./listing-fold.js";
@@ -116,7 +116,8 @@ export function normalizeSkills(skills) {
116
116
  seen.add(s.name);
117
117
  const { name, description, content, allowedTools, disallowedTools, files, baseDir, ...rest } = s;
118
118
  void rest;
119
- out.push({ name, description, content, ...(allowedTools !== undefined ? { allowedTools: [...allowedTools] } : {}), ...(disallowedTools !== undefined ? { disallowedTools: [...disallowedTools] } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
119
+ const [allowed, denied] = [readSkillTable(allowedTools), readSkillTable(disallowedTools)];
120
+ out.push({ name, description, content, ...(allowed !== undefined ? { allowedTools: allowed } : {}), ...(denied !== undefined ? { disallowedTools: denied } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
120
121
  }
121
122
  }
122
123
  return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -52,12 +52,16 @@ export interface PutRulesOptions {
52
52
  * this from a verified, role-checked principal (mirrors `steer.trusted`). */
53
53
  operator?: boolean;
54
54
  }
55
- /** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, and the
56
- * spelling to write instead when the judgment has one (a retired name's 1:1 successor, an unmintable segment's minted name). */
55
+ /** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, the judgment's
56
+ * one-line sentence when it has one, and the spelling to write instead when the judgment has one (a retired name's 1:1
57
+ * successor, an unmintable segment's minted name). */
57
58
  export interface SessionRuleNameRefusal {
58
59
  field: SessionRuleToolNameField;
59
60
  name: string;
60
61
  verdict: Exclude<RuleToolNameVerdict["kind"], "current">;
62
+ /** The `guidance` of the judgment ({@link judgeRuleToolName}), byte for byte — the same string the refusal's sentence quotes. Present for
63
+ * `retired` and `unmintable_segment`, the two verdicts that carry one; absent for `unprefixed_namespace`, whose judgment has no sentence of its own. */
64
+ guidance?: string;
61
65
  currentName?: string;
62
66
  }
63
67
  /** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
@@ -41,6 +41,7 @@ export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLA
41
41
  continue;
42
42
  const quoted = JSON.stringify(name);
43
43
  const successor = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
44
+ const guidance = verdict.kind === "unprefixed_namespace" ? undefined : verdict.guidance;
44
45
  let message;
45
46
  switch (verdict.kind) {
46
47
  case "retired":
@@ -57,7 +58,7 @@ export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLA
57
58
  return exhaustive;
58
59
  }
59
60
  }
60
- throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(successor !== undefined ? { currentName: successor } : {}) });
61
+ throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(guidance !== undefined ? { guidance } : {}), ...(successor !== undefined ? { currentName: successor } : {}) });
61
62
  }
62
63
  }
63
64
  }
@@ -76,8 +76,16 @@ export declare function parkedSkillApprovalRefusal(row: {
76
76
  readonly args: unknown;
77
77
  readonly boundGrantDigest?: string;
78
78
  }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string, site?: "claim" | "execution"): string | undefined;
79
- /** A resume's skills read ONCE: each definition copied with its two lists, so the parked Skill approval's binding and the
80
- * resumed leg's prepare read one value — a later edit to the caller's own objects or arrays reaches neither. */
79
+ /**
80
+ * A skill's table (`allowed-tools` / `disallowed-tools`) READ ONCE, the one reading every later reader takes instead of the host's
81
+ * value: absent (`undefined`, or `null` — a frontmatter's empty key) is no table; a list is its dense copy (by index, no iterator
82
+ * of the host's runs; a member that is not a string stays as it is); a value that is not a list, or a list whose read throws, is
83
+ * ONE entry that reads as nothing ({@link nonListValueLabel}), so each table applies the law it already has to an entry that reads
84
+ * as nothing — a deny table keeps its Skill from activating, an allow table is announced and grants nothing. A string is not spread.
85
+ */
86
+ export declare function readSkillTable(value: unknown): string[] | undefined;
87
+ /** A resume's skills read ONCE: each definition copied with its two tables ({@link readSkillTable}), so the parked Skill approval's
88
+ * binding and the resumed leg's prepare read one value — a later edit to the caller's own objects or arrays reaches neither. */
81
89
  export declare function detachSkillLists<T extends SkillGrantSource>(skills: readonly T[]): T[];
82
90
  /** Does this skill declare a list (either, non-empty)? The one test of "the Skill card is asked for this skill". */
83
91
  export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowedTools" | "disallowedTools">): boolean;
@@ -3,7 +3,8 @@ import { parsePermissionRule } from "./permission-rules.js";
3
3
  import { mountedToolPermissionSeat } from "./mode-policy.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { boundInputHashOf } from "./canonical-json.js";
6
- import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries } from "./allow-entry.js";
6
+ import { readToolNameList } from "../agents/tool-filter.js";
7
+ import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries, nonListValueLabel, nonStringMemberLabel, renderListEntry } from "./allow-entry.js";
7
8
  export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
8
9
  const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
9
10
  export function skillNamedBy(args, skills) {
@@ -12,6 +13,8 @@ export function skillNamedBy(args, skills) {
12
13
  }
13
14
  function skillGrantBinding(skill) {
14
15
  const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
16
+ if (typeof e !== "string")
17
+ return nonStringMemberLabel(e);
15
18
  const p = parsePermissionRule(e);
16
19
  return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
17
20
  }))].sort();
@@ -44,8 +47,21 @@ export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, sit
44
47
  ? `${why} — refusing pre-CAS (nothing ran; the checkpoint stays pending). Decide this checkpoint as deny and re-activate it: the new card shows the current lists.`
45
48
  : `The approved Skill call was not executed: ${why}. This approval is spent — to use "${chosen}", have the model activate it again: the new card shows its lists.`;
46
49
  }
50
+ export function readSkillTable(value) {
51
+ if (value === undefined || value === null)
52
+ return undefined;
53
+ const read = readToolNameList(value);
54
+ return read.kind === "entries" ? read.entries : [nonListValueLabel(read)];
55
+ }
47
56
  export function detachSkillLists(skills) {
48
- return skills.map((s) => (typeof s === "object" && s !== null ? { ...s, ...(Array.isArray(s.allowedTools) ? { allowedTools: [...s.allowedTools] } : {}), ...(Array.isArray(s.disallowedTools) ? { disallowedTools: [...s.disallowedTools] } : {}) } : s));
57
+ return skills.map((s) => {
58
+ if (typeof s !== "object" || s === null)
59
+ return s;
60
+ const { allowedTools: rawAllowed, disallowedTools: rawDenied, ...rest } = s;
61
+ const allowedTools = readSkillTable(rawAllowed);
62
+ const disallowedTools = readSkillTable(rawDenied);
63
+ return { ...rest, ...(allowedTools !== undefined ? { allowedTools } : {}), ...(disallowedTools !== undefined ? { disallowedTools } : {}) };
64
+ });
49
65
  }
50
66
  export function declaresSkillLists(skill) {
51
67
  return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
@@ -74,7 +90,7 @@ export function skillDenyDecision(hit) {
74
90
  action: "deny",
75
91
  message: hit.readsAsNothing !== undefined
76
92
  ? skillDenyUnreadable(hit.skill, hit.readsAsNothing, "activation").message
77
- : `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
93
+ : `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map(renderListEntry).join(", ")}) — refused for the rest of this run`,
78
94
  decisionReason: "skill",
79
95
  };
80
96
  }
@@ -88,7 +104,7 @@ export function skillRowRefusal(lane, req) {
88
104
  export function skillAllowDecision(hit, updatedInput) {
89
105
  return {
90
106
  action: "allow",
91
- message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in allowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")})`,
107
+ message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in allowed-tools (${hit.entries.map(renderListEntry).join(", ")})`,
92
108
  decisionReason: "skill",
93
109
  ...(updatedInput !== undefined ? { updatedInput } : {}),
94
110
  };
@@ -32,10 +32,21 @@ export declare const SYSTEM_INJECTION_PRIORITIES: readonly SystemInjectionPriori
32
32
  export declare function isSystemInjectionPriority(value: unknown): value is SystemInjectionPriority;
33
33
  /** The ONE reading of a steer's priority: a parked row or an accepted input that carries none means `"next"`
34
34
  * (the default the live admission has always applied) — so a retry that spells the default and a row parked
35
- * without it are the same instruction, whichever side normalized. */
35
+ * without it are the same instruction, whichever side normalized. A tier outside the closed set throws
36
+ * (`steering.invalid_content`, the sentence names the set); `null` is such a tier, not an absent one. */
36
37
  export declare function steerPriorityOf(x: {
37
38
  readonly priority?: SystemInjectionPriority;
38
39
  }): SystemInjectionPriority;
40
+ /** The two queues a person's input rides on a live run: the `steer` lane (the running turn's next boundary) and the
41
+ * `followUp` lane (the run's would-otherwise-stop seat). They are also the two families of the stranded-input account — an
42
+ * input's account is the one of the lane it was queued on (`USER_INPUT_UNDRAINED_FAMILIES`). */
43
+ export type UserInputLane = "steer" | "followUp";
44
+ /** The ONE reading of "which lane carries this tier": `"later"` rides the follow-up lane (never folded into work in
45
+ * progress), `"next"` and `"now"` ride the steer lane (`"now"` additionally cuts the running turn). Both injection
46
+ * seats — the steer verb and the notification lane — branch on it. A parked row that carries no tier reads through
47
+ * {@link steerPriorityOf} first: `injectionLaneOf(steerPriorityOf(row))`. A tier outside the closed set throws
48
+ * (`steering.invalid_content`, the same check and sentence as {@link steerPriorityOf}). */
49
+ export declare function injectionLaneOf(priority: SystemInjectionPriority): UserInputLane;
39
50
  export interface TaskNotificationPayload {
40
51
  task_id: string;
41
52
  /** design/144 §2: `"external"` is CORE-MINTED ONLY — `TaskStream.notify()` casts it onto every
@@ -10,8 +10,20 @@ export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
10
10
  export function isSystemInjectionPriority(value) {
11
11
  return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
12
12
  }
13
+ function requireSystemInjectionPriority(value) {
14
+ if (isSystemInjectionPriority(value))
15
+ return value;
16
+ const shown = typeof value === "string" ? JSON.stringify(value.length > 24 ? `${value.slice(0, 24)}…` : value) : value === null ? "null" : typeof value;
17
+ const refusal = new Error(`a steer priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")}, got ${shown}`);
18
+ refusal.code = "steering.invalid_content";
19
+ throw refusal;
20
+ }
13
21
  export function steerPriorityOf(x) {
14
- return x.priority ?? "next";
22
+ return x.priority === undefined ? "next" : requireSystemInjectionPriority(x.priority);
23
+ }
24
+ const LANE_OF_PRIORITY = { now: "steer", next: "steer", later: "followUp" };
25
+ export function injectionLaneOf(priority) {
26
+ return LANE_OF_PRIORITY[requireSystemInjectionPriority(priority)];
15
27
  }
16
28
  const TASK_NOTIFICATION_TAGS = [
17
29
  "task-notification",
@@ -551,9 +551,11 @@ export interface TaskSpec {
551
551
  * upstream): an `allowTools` entry naming a tool that does not exist is ITEM-FILTERED at spawn (the
552
552
  * agent stays listed and delegable; the only host disclosure is a prepare-time HEURISTIC advisory
553
553
  * via `RunnerDeps.onError` phase `"config"` for entries unknown to the parent's assembled roster —
554
- * zero model-facing warning, the verified upstream shape); a `denyTools` entry naming an
555
- * unknown tool is REJECTED at task start (`config.agent.unknown_tool` — a deny-typo silently WIDENS,
556
- * the opposite failure direction, sema extension with no upstream deny-list to defer to); and
554
+ * zero model-facing warning, the verified upstream shape); a `denyTools` entry is read as the
555
+ * reference reads it (`Tool(content)` hides the whole `Tool`, with a host notice) and judged once, here and for the
556
+ * delegation tool's own lists alike: a spelling that names no tool, or a stale one (another case of a mounted tool, a
557
+ * retired name), is REJECTED at task start (`config.agent.unknown_tool`), any other name no tool carries is told to the
558
+ * host and hides nothing; and
557
559
  * providing this field with NO delegation tool mounted is rejected too
558
560
  * (`config.agents.no_delegation_tool`) — never a silent no-op. POLICY-NEUTRAL: a
559
561
  * roster listing only; the 1.294 tighten-only parent-policy inheritance is applied at spawn time,
@@ -4,6 +4,7 @@ import { type Settlement } from "./gate-outcome.js";
4
4
  export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
5
5
  import { type DecisionReason } from "./decision-receipt.js";
6
6
  export { isEngineRefusal } from "./decision-receipt.js";
7
+ import { type RemovalGuardSeat } from "../tools/fs/removal-targets.js";
7
8
  /** A tool call presented to a policy before it executes. */
8
9
  /** design/388 B6 — the per-call face (see {@link ToolCallRequest.face}). */
9
10
  export interface ToolCallFace {
@@ -69,6 +70,15 @@ export interface ToolCallRequest {
69
70
  * window — at that declaration, not here.
70
71
  */
71
72
  cwd?: string;
73
+ /**
74
+ * The root this leg's tools resolve a ROOT-relative spelling against (a search tool's relative `path`, a
75
+ * pattern with no `path`), stamped beside {@link cwd} at every Runner adjudication seat — the same
76
+ * canonical spelling as `cwd` (the hands' own root, the value `cwd` starts at). Present whenever the leg
77
+ * mounts file hands, read-only hands included; absent on a hands-less leg and on a bare request. A path
78
+ * guard whose deployment supplied no base of its own resolves the call's target against `root` / `cwd`.
79
+ * Read-only, like `cwd`.
80
+ */
81
+ root?: string;
72
82
  /**
73
83
  * The CANONICAL directories THIS RUN'S PROMPT ADVERTISES AS WRITABLE — the admitted list of the root
74
84
  * fence's advertised-writable-directory seat (the memory engine's write home, the deployment's
@@ -397,12 +407,12 @@ export type PermissionResult = {
397
407
  /** The ask's whole class set, in {@link import("./ask-origin.js").ASK_CLASSES} order (see
398
408
  * {@link AskRequest.classes}). Stamped beside {@link askClass}, under the same rule. */
399
409
  classes?: readonly import("./ask-origin.js").AskClass[];
400
- /** WHICH integrity guard minted this ask — the unverifiable recursive delete or the session-transcript
401
- * write. ENGINE-STAMPED by the two guard policies; a fold-internal fact the class derivation reads
402
- * ({@link import("./ask-origin.js").classifyAsk}) so the two guards, which mint the same
410
+ /** WHICH integrity guard minted this ask — a removal of a protected location, an unverifiable recursive delete
411
+ * or the session-transcript write. ENGINE-STAMPED by the guard policies; a fold-internal fact the class
412
+ * derivation reads ({@link import("./ask-origin.js").classifyAsk}) so the guards, which mint the same
403
413
  * `requiresRealApproval` ask, are told apart by a fact and not by their prose. Never copied onto the
404
414
  * approval request or the park row. */
405
- guard?: "unverifiable_delete" | "transcript_write" | "artifact_shared_read";
415
+ guard?: "dangerous_delete" | "unverifiable_delete" | "transcript_write" | "artifact_shared_read";
406
416
  /** The call could not be READ against a rule that may reach it — a content-form deny row, a content-form
407
417
  * ask row, or a persisted-rule row (the lexer's word). ENGINE-STAMPED at the three unreadable mint sites;
408
418
  * the class derivation reads it as `rule_unreadable` (a mode may release it, a person may clear it, no
@@ -1082,6 +1092,7 @@ export declare function createUnverifiableDeletePolicy(opts?: {
1082
1092
  safeVars?: readonly string[];
1083
1093
  /** Override the shell tool names this gate parses (default `Bash`). */
1084
1094
  tools?: string[];
1095
+ removalSeat?: RemovalGuardSeat;
1085
1096
  }): ToolPolicy;
1086
1097
  export declare function createTranscriptIntegrityPolicy(opts?: {
1087
1098
  /** Transcript directories to protect. Default: `($AGENT_DATA_DIR ?? ~/.ai-agent)/sessions`. */