@sema-agent/core 7.25.1 → 7.26.1

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 (83) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/core/ask-origin.d.ts +84 -0
  3. package/dist/core/ask-origin.js +31 -3
  4. package/dist/core/auto-compaction.d.ts +5 -0
  5. package/dist/core/auto-compaction.js +7 -0
  6. package/dist/core/auto-mode-directive.d.ts +45 -0
  7. package/dist/core/auto-mode-directive.js +47 -0
  8. package/dist/core/checkpoint-store.d.ts +10 -1
  9. package/dist/core/engine-notice.d.ts +10 -0
  10. package/dist/core/gate-lanes.js +18 -10
  11. package/dist/core/governance-codes.d.ts +1 -1
  12. package/dist/core/governance-codes.js +3 -0
  13. package/dist/core/hooks.d.ts +3 -0
  14. package/dist/core/mcp-probe.d.ts +122 -0
  15. package/dist/core/mcp-probe.js +132 -0
  16. package/dist/core/memory-engine/engine.d.ts +6 -1
  17. package/dist/core/memory-engine/engine.js +30 -2
  18. package/dist/core/memory-engine/index.d.ts +1 -0
  19. package/dist/core/memory-engine/index.js +1 -0
  20. package/dist/core/memory-engine/shadowing.d.ts +58 -0
  21. package/dist/core/memory-engine/shadowing.js +40 -0
  22. package/dist/core/memory-engine/tools.d.ts +33 -1
  23. package/dist/core/memory-engine/tools.js +136 -31
  24. package/dist/core/memory-engine/types.d.ts +10 -0
  25. package/dist/core/memory.d.ts +60 -0
  26. package/dist/core/memory.js +47 -2
  27. package/dist/core/permission-rule-provider.js +6 -1
  28. package/dist/core/permission-rule-store.d.ts +87 -8
  29. package/dist/core/permission-rule-store.js +223 -76
  30. package/dist/core/permission-rule-sync.js +14 -5
  31. package/dist/core/reminder-mint.d.ts +41 -0
  32. package/dist/core/reminder-mint.js +55 -0
  33. package/dist/core/runner/compaction-restate.d.ts +9 -0
  34. package/dist/core/runner/compaction-restate.js +20 -0
  35. package/dist/core/runner/contracts.d.ts +18 -1
  36. package/dist/core/runner/denial-limit-arms.js +1 -1
  37. package/dist/core/runner/permission-rule-lanes.d.ts +5 -17
  38. package/dist/core/runner/permission-rule-lanes.js +5 -23
  39. package/dist/core/runner/prepare-ask-lane.js +2 -0
  40. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  41. package/dist/core/runner/prepare-context-lane.js +7 -2
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -2
  43. package/dist/core/runner/prepare-inherited-gate.js +6 -3
  44. package/dist/core/runner/prepare-memory.js +47 -6
  45. package/dist/core/runner/prepare-park-ask.js +1 -0
  46. package/dist/core/runner/prepare-policy-chain.js +8 -4
  47. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -0
  48. package/dist/core/runner/prepare-suspend-saga.js +3 -1
  49. package/dist/core/runner/prepare-task.js +7 -4
  50. package/dist/core/runner/run-leg.js +9 -1
  51. package/dist/core/runner/run-recovery-lanes.js +2 -2
  52. package/dist/core/runner/run-terminal-adoption.js +2 -2
  53. package/dist/core/runner/run-turn-boundary.js +2 -2
  54. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  55. package/dist/core/runner/session-rule-policy.js +6 -7
  56. package/dist/core/session-policy-store.d.ts +18 -0
  57. package/dist/core/session-policy-store.js +3 -0
  58. package/dist/core/store-contracts/permission-rule-write-contract.d.ts +20 -0
  59. package/dist/core/store-contracts/permission-rule-write-contract.js +90 -0
  60. package/dist/core/store-contracts/read-back-shape-contract.d.ts +56 -0
  61. package/dist/core/store-contracts/read-back-shape-contract.js +78 -0
  62. package/dist/core/task-result.d.ts +11 -0
  63. package/dist/core/task-spec.d.ts +31 -0
  64. package/dist/core/tool-catalog-entries.js +3 -3
  65. package/dist/core/tool-policy.d.ts +38 -0
  66. package/dist/core/tool-policy.js +5 -1
  67. package/dist/core/types.d.ts +1 -1
  68. package/dist/index.d.ts +11 -2
  69. package/dist/index.js +10 -1
  70. package/dist/stores/file/adoption/adopt.js +4 -10
  71. package/dist/stores/file/permission-rule-store.d.ts +5 -1
  72. package/dist/stores/file/permission-rule-store.js +52 -31
  73. package/dist/stores/file/session-policy-store.d.ts +32 -0
  74. package/dist/stores/file/session-policy-store.js +72 -50
  75. package/dist/tools/fs/shell-read-check.d.ts +58 -1
  76. package/dist/tools/fs/shell-read-check.js +47 -33
  77. package/dist/tools/fs/shell-read-establish.js +19 -8
  78. package/dist/tools/fs/shell-read-judge.d.ts +3 -0
  79. package/dist/tools/fs/shell-read-judge.js +3 -1
  80. package/package.json +1 -1
  81. package/test/export-surface.snapshot.json +79 -1
  82. package/wire-changes/core@7.26.0.json +695 -0
  83. package/wire-changes/core@7.26.1.json +605 -0
@@ -79,7 +79,7 @@ function stampPlacementRootSessionId(placementRootResolved) {
79
79
  return placementValueOrAbsent(placementRootResolved);
80
80
  }
81
81
  export function prepareSuspendSaga(input) {
82
- const { gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter } = input;
82
+ const { gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, autoModeIntent, approverPosture, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter } = input;
83
83
  if (!gateMachineryActive)
84
84
  return { saga: undefined };
85
85
  const inFlightSpendMicroUsd = () => {
@@ -139,6 +139,7 @@ export function prepareSuspendSaga(input) {
139
139
  return ancestorRulesAtMint !== undefined ||
140
140
  inheritedShellGate !== undefined ||
141
141
  autoModeIntentCarried ||
142
+ approverPosture !== undefined ||
142
143
  inheritedAdmittedOrgScopes !== undefined ||
143
144
  ownOrgVerdictRef.current !== undefined ||
144
145
  orgGovernedProvenance ||
@@ -147,6 +148,7 @@ export function prepareSuspendSaga(input) {
147
148
  ...(ancestorRulesAtMint !== undefined ? { ancestorRules: structuredClone(ancestorRulesAtMint) } : {}),
148
149
  ...(inheritedShellGate !== undefined ? { shellGate: inheritedShellGate } : {}),
149
150
  ...(autoModeIntentCarried ? { autoModeRequested: true } : {}),
151
+ ...(approverPosture !== undefined ? { approverPosture } : {}),
150
152
  ...(inheritedAdmittedOrgScopes !== undefined ? { admittedOrgScopes: [...inheritedAdmittedOrgScopes] } : {}),
151
153
  ...(ownOrgVerdictRef.current !== undefined
152
154
  ? { ownAdmittedOrgScopes: [...ownOrgVerdictRef.current.scopes], ownAdmittedOrgWriteScope: ownOrgVerdictRef.current.writeScope }
@@ -9,6 +9,7 @@ import { hasSessionFork } from "../session.js";
9
9
  import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
10
10
  import { loadAnnounceOnceLedger } from "./announce-once-ledger.js";
11
11
  import { isValidReminderMark, mintReminderMark } from "../reminder-mint.js";
12
+ import { refuseInvalidApproverPosture } from "../auto-mode-directive.js";
12
13
  import { emitTrace } from "../trace.js";
13
14
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
14
15
  import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGuardrail } from "../../brain/timeout.js";
@@ -265,6 +266,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
265
266
  spec = doors.spec;
266
267
  const checkpointStore = resolveCheckpointStore(spec, deps), checkpointSeat = checkpointSeatOf(spec, checkpointStore);
267
268
  const autoModeSeat = refuseInvalidAutoModeSeat(spec);
269
+ const approverPostureSeat = refuseInvalidApproverPosture(spec);
268
270
  const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, microCompactKnob, resolvedRole, model, thinking, compModel, fableMitigations, memoryDelegationEvidence, memoryProvenance, memoryCapturePolicy, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
269
271
  announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
270
272
  const modelGateManifest = toolModelGateManifest(doors.modelGate);
@@ -376,7 +378,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
376
378
  let readDenyAdditionsNormalized = [];
377
379
  let handsLessResolvedFace;
378
380
  let handsLessResolvedWriteFace;
379
- const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, ancestorSessionRulesRef, ancestorRulesNow, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, writeFaceCheckpointSection, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, parentSpecSeat, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, carrierWriteFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, checkpointSeat, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun: () => { abortController.abort(); void harness.abort(); }, runtimeCaps: () => runtimeCaps, hooks: () => hooks, preToolUseObservational: () => preToolUseObservational, hookTimeoutMs: () => hookTimeoutMs, hookEnvFace: () => hookEnvFace, autoModeDecider: () => autoModeDecider, autoModeDenialTracking: () => autoModeDenialTracking, autoModeArming: () => autoModeArming, agentForkDenial: () => agentForkDenial, observersActive: () => observersActive, resolvedReadFace: () => resolvedReadFace, resolvedWriteFace: () => resolvedWriteFace, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, handsLessResolvedWriteFace: () => handsLessResolvedWriteFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
381
+ const { liveInheritedGate, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, inheritedParentConstraints, autoModeIntent, approverPosture: approverPostureResolved, inheritedAdmittedOrgScopes, priorOwnOrgVerdict, orgGovernedProvenance, effectiveShellGate, ownSessionRulesRef, ancestorSessionRulesRef, ancestorRulesNow, memoryAdmittedOrgScopesRef, ownOrgVerdictRef, orgAdmissionCheckpointState, faceCheckpointSection, faceCheckpointState, writeFaceCheckpointSection, f012CheckpointState, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, frozenOnQuestion, parentSpecSeat, provenanceForChildrenRef, delegationSettlementRef, inheritedGateForChildren, carrierReadFace, carrierWriteFace, fullShellReachable, gateStopRef, stopForDenialLimit, enrichSpecToolCtx } = prepareInheritedGate({ spec, deps, internals, resume, sessions, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, ownedEnv, lockedPreflight, checkpointSeat, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, autoModeSeat, approverPostureSeat, handsEnabled, reportUsage, harnessRef, skillScope, forwardEvent, subagentRetain, worktreeIsolation, requestReview, requestStopAfterTurn, enterPlanMode, reminderMark, reminderDisclosureCounts, fileHistoryEnabled, historyScope, historyRoot, historyFs, abortRun: () => { abortController.abort(); void harness.abort(); }, runtimeCaps: () => runtimeCaps, hooks: () => hooks, preToolUseObservational: () => preToolUseObservational, hookTimeoutMs: () => hookTimeoutMs, hookEnvFace: () => hookEnvFace, autoModeDecider: () => autoModeDecider, autoModeDenialTracking: () => autoModeDenialTracking, autoModeArming: () => autoModeArming, agentForkDenial: () => agentForkDenial, observersActive: () => observersActive, resolvedReadFace: () => resolvedReadFace, resolvedWriteFace: () => resolvedWriteFace, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized, handsLessResolvedFace: () => handsLessResolvedFace, handsLessResolvedWriteFace: () => handsLessResolvedWriteFace, centerAdoption: () => centerAdoptionRef.current, memoryEngineSession: () => memoryEngineSession });
380
382
  const { roster, blockedRef, runtimeCaps, runtimeCapsFaulted, complianceDenies, complianceDegraded, agentForkDenial, observersActive, autoModeArmReason, autoModeDecider, autoModeDenialTracking, autoModeArming, selfOrchestrationActive, workflowToolsActive, workflowSizeGuideline } = await prepareCapsAndWorkflow({ spec, deps, internals, session, sessionId, runId, hostTaskId, taskScope, taskRootFinal, model, thinking, lockedPreflight, checkpointSeat, toolEffects, toolFaceSnapshot, promptProfile, resolvedInteractionPosture, harnessRef, frozenOnAsk, parentSpecSeat, carrierReadFace, carrierWriteFace, forwardEvent, inheritedGateForChildren, enrichSpecToolCtx, maybeOffload, requestReview, enterPlanMode, autoModeIntent, peerLaneActive, peerSendMessageBuiltIn, runnerSelf, memoryEngineSession: () => memoryEngineSession, centerAdoption: () => centerAdoptionRef.current, readDenyAdditionsNormalized: () => readDenyAdditionsNormalized });
381
383
  const tools = roster.tools;
382
384
  const pausedRef = {};
@@ -700,15 +702,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
700
702
  });
701
703
  }
702
704
  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 });
703
- const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
705
+ const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, autoModeIntent, approverPosture: approverPostureResolved, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
704
706
  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 });
705
707
  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 });
706
708
  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, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
707
- const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
709
+ const postureFrameRef = {};
710
+ const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
708
711
  const preparedHolder = {};
709
712
  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, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, carrierWriteFace, readDenyAdditionsNormalized, preparedHolder });
710
713
  await onceLedger.settle(session, announcedListingsRef);
711
- const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, 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, ...(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, undeliveredContinuationRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, 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 } } : {}), planModeRef, 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 }, gitStatusRef, listBackgroundTasks, hookIdentity, ...(sessionBoxBound !== undefined ? { sessionBox: sessionBoxBound } : {}), hookTimeoutMs, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}), ...(shellReadCheck !== undefined ? { shellReadCheck } : {}), ...(toolDisclosureSeat !== undefined ? { toolDisclosure: toolDisclosureSeat } : {}) });
714
+ const buildPrepared = () => ({ harness, session, sessionId, runId, reminderMark, reminderDisclosureCounts, approvalPosture: { autoMode: autoModeIntent, ...(approverPostureResolved !== undefined ? { approverPosture: approverPostureResolved } : {}) }, 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, ...(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, undeliveredContinuationRef, suspendProgressRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, workCutRef, brainCallGuardrailRef, gateStopRef, 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 } } : {}), planModeRef, 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 }, 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 } : {}) });
712
715
  rollback.commit();
713
716
  const prepared = buildPrepared();
714
717
  preparedHolder.current = prepared;
@@ -8,7 +8,8 @@ import { buildHumanInputEvent, projectHumanInput } from "../human-input-projecti
8
8
  import { ORG_ADJUDICATION_TIMEOUT_MS, settleOrgVerdictWithin } from "../permission-rule-org.js";
9
9
  import { applyPersistedTightening, disclosedRuleSet } from "../persisted-rule-arms.js";
10
10
  import { computeCostMicroUsd } from "../pricing.js";
11
- import { mintSystemReminder } from "../reminder-mint.js";
11
+ import { composeApprovalPostureDirective } from "../auto-mode-directive.js";
12
+ import { mintSystemReminder, statedReminderBodiesOf } from "../reminder-mint.js";
12
13
  import { hasDestroy } from "../remote-env.js";
13
14
  import { advertisedWritableRootsSeat, refuseOutOfContractDecision } from "../tool-policy.js";
14
15
  import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
@@ -375,6 +376,13 @@ export async function runLeg(input) {
375
376
  effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body, attachmentEnvelopeTags(f.source)), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
376
377
  }
377
378
  }
379
+ if (composeApprovalPostureDirective(prepared.approvalPosture) !== undefined) {
380
+ const postureDirective = composeApprovalPostureDirective(prepared.approvalPosture, await statedReminderBodiesOf(prepared.session, prepared.reminderMark));
381
+ if (postureDirective !== undefined) {
382
+ prepared.postureFrameRef.protectedText = mintSystemReminder(postureDirective, prepared.reminderMark);
383
+ effectiveObjective = `${prepared.postureFrameRef.protectedText}\n${effectiveObjective}`;
384
+ }
385
+ }
378
386
  const gitQueuedChars = gitLegDelivered !== undefined && gitLegDelivered.standalone ? gitLegDelivered.wrapped.length : 0;
379
387
  const precallMicroUsd = rs.budget.maxCostMicroUsd === undefined
380
388
  ? 0
@@ -6,7 +6,7 @@ import { emitTrace } from "../trace.js";
6
6
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
7
7
  import { rebaseCadenceWindows } from "./turn-attachments.js";
8
8
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
9
- import { gitRestateOption } from "./git-leg-delivery.js";
9
+ import { compactionRestateOptions } from "./compaction-restate.js";
10
10
  import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
11
11
  import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
12
12
  import { asThrownError, thrownText } from "../thrown-value.js";
@@ -41,7 +41,7 @@ export function runRecoveryLanes(input) {
41
41
  ...contextInstructionFilesOption(prepared),
42
42
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
43
43
  ...seamCCompactionOptions(runner, prepared),
44
- ...gitRestateOption(prepared),
44
+ ...compactionRestateOptions(prepared),
45
45
  ...windowSafetyOptions(prepared.harness.getModel()),
46
46
  ...compactionHookOptions(runner, spec, prepared.sessionId, "forced", prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: passSignal }),
47
47
  });
@@ -7,7 +7,7 @@ import { answerSettled, errorCodeOf } from "./assemble-result.js";
7
7
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow } from "./clock-and-limits.js";
8
8
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
9
9
  import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
10
- import { gitRestateOption } from "./git-leg-delivery.js";
10
+ import { compactionRestateOptions } from "./compaction-restate.js";
11
11
  import { MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
12
12
  import { reconciledToolEndBody } from "./tool-end-body.js";
13
13
  import { asThrownError, thrownText } from "../thrown-value.js";
@@ -149,7 +149,7 @@ async function finish(spec, prepared, opts, runner, sessions) {
149
149
  ...contextInstructionFilesOption(prepared),
150
150
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
151
151
  ...seamCCompactionOptions(runner, prepared),
152
- ...gitRestateOption(prepared),
152
+ ...compactionRestateOptions(prepared),
153
153
  ...compactionHookOptions(runner, spec, prepared.sessionId, "auto", prepared.hookIdentity, {
154
154
  timeoutMs: prepared.hookTimeoutMs,
155
155
  ...(opts?.cancelled === true ? { signal: prepared.abortController.signal } : {}),
@@ -13,7 +13,7 @@ import { droppedToolDigest, withAnnounceDigests } from "./announce-once-ledger.j
13
13
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
14
14
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
15
15
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow, ENV_DUE_GOVERNANCE_READ_BUDGET_MS, GOVERNANCE_READ_STALLED, LIMIT_APPROACH_DEFAULT_THRESHOLDS, limitApproachFrames, platformLimitTerminal, raceUntilDeadline } from "./clock-and-limits.js";
16
- import { gitRestateOption } from "./git-leg-delivery.js";
16
+ import { compactionRestateOptions } from "./compaction-restate.js";
17
17
  import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
18
18
  import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
19
19
  import { contextUsageSections } from "../../prompt-assembly/section-usage.js";
@@ -583,7 +583,7 @@ export function createTurnBoundary(input) {
583
583
  ...contextInstructionFilesOption(prepared),
584
584
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
585
585
  ...seamCCompactionOptions(runner, prepared),
586
- ...gitRestateOption(prepared),
586
+ ...compactionRestateOptions(prepared),
587
587
  ...windowSafetyOptions(event.model),
588
588
  ...compactionHookOptions(runner, spec, prepared.sessionId, passTrigger, prepared.hookIdentity, { timeoutMs: prepared.hookTimeoutMs, signal: prepared.abortController.signal }),
589
589
  });
@@ -1,7 +1,7 @@
1
1
  import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { ToolEffect } from "../types.js";
3
3
  import { type NamedToolPolicy } from "../tool-policy.js";
4
- import type { SessionPermissionRules, SessionPolicyStore, StoredSessionRules } from "../session-policy-store.js";
4
+ import { type SessionPermissionRules, type SessionPolicyStore, type StoredSessionRules } from "../session-policy-store.js";
5
5
  /** Tool names whose write target is the `file_path`/`path` arg ({@link fileArgPath}).
6
6
  *
7
7
  * REF-B3 — the ONE definition of that narrower set. Production consumer today:
@@ -1,5 +1,6 @@
1
1
  import { canonicalizeTarget, isWinFormPath, pathFamilyOf, writeTargetPath } from "../../tools/fs/safety.js";
2
2
  import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
3
+ import { continuesLineage } from "../session-policy-store.js";
3
4
  import { thrownText } from "../thrown-value.js";
4
5
  import { ruleDenialSentence } from "../rule-denial-sentence.js";
5
6
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
@@ -61,13 +62,11 @@ export function createLiveSessionRules(store, floor, hooks) {
61
62
  return enforced;
62
63
  }
63
64
  if (enforced.rev > 0) {
64
- if (live.gen !== undefined && enforced.gen !== undefined) {
65
- if (live.gen !== enforced.gen) {
66
- announce(`session ${JSON.stringify(floor.sessionId)} has a rules row of a new lineage (generation ${live.gen}, rev ${live.rev}), which does not continue the row this leg enforces (generation ${enforced.gen})`);
67
- return enforced;
68
- }
65
+ if (!continuesLineage(live, enforced)) {
66
+ announce(`session ${JSON.stringify(floor.sessionId)} has a rules row of a new lineage (generation ${live.gen}, rev ${live.rev}), which does not continue the row this leg enforces (generation ${enforced.gen})`);
67
+ return enforced;
69
68
  }
70
- else if (vanished) {
69
+ if (vanished && live.gen === undefined) {
71
70
  announce(`session ${JSON.stringify(floor.sessionId)} has a new rules row at rev ${live.rev} after the row this leg enforced was deleted, and its backend carries no lineage to continue it by`);
72
71
  return enforced;
73
72
  }
@@ -89,7 +88,7 @@ export function createLiveSessionRules(store, floor, hooks) {
89
88
  if (handedRead !== undefined) {
90
89
  handed.delete(toolCallId);
91
90
  const claimed = await handedRead;
92
- if (claimed.rev >= enforced.rev)
91
+ if (continuesLineage(claimed, enforced) && claimed.rev >= enforced.rev)
93
92
  return claimed;
94
93
  }
95
94
  const p = read(signal);
@@ -131,6 +131,24 @@ export declare function normalizeRules(rules: SessionPermissionRules): SessionPe
131
131
  * allowDirs: removing confinement, or adding a dir not within some prior dir, loosens (coarse raw containment).
132
132
  */
133
133
  export declare function loosenReasons(prior: SessionPermissionRules, next: SessionPermissionRules): string[];
134
+ /**
135
+ * Does row `a` belong to the SAME lineage as row `b` — the one question every reader asks before it compares
136
+ * two rows' `rev`, asked in ONE place so a `gen` that is ABSENT cannot mean three different things at three
137
+ * stations. It means what {@link StoredSessionRules.gen} says it means: generation ZERO. A row that spells
138
+ * `gen: 0` and a row that omits it are therefore the same row's generation, and a row of generation 1 does not
139
+ * continue either of them — which is the case the readings used to disagree on, each treating absence as
140
+ * "unknown, so do not judge" and letting a recreated row pass for a continuation of the one it enforced.
141
+ *
142
+ * ABSENCE STILL CARRIES ONE OTHER FACT, and it is a different question: a backend that stamps no lineage at
143
+ * ALL gives its reader no recycle evidence, so a reader that has one of its own (it watched the row vanish)
144
+ * keeps using it. That test asks whether the FIELD is there, never what it compares to, and it is the caller's
145
+ * to make — this predicate answers only "same lineage or not".
146
+ */
147
+ export declare function continuesLineage(a: {
148
+ gen?: number;
149
+ }, b: {
150
+ gen?: number;
151
+ }): boolean;
134
152
  /** strip any caller-passed `rev` from a rules object so the store is the sole authority on it. */
135
153
  export declare function stripRev(rules: SessionPermissionRules): SessionPermissionRules;
136
154
  /**
@@ -69,6 +69,9 @@ export function loosenReasons(prior, next) {
69
69
  }
70
70
  return reasons;
71
71
  }
72
+ export function continuesLineage(a, b) {
73
+ return (a.gen ?? 0) === (b.gen ?? 0);
74
+ }
72
75
  export function stripRev(rules) {
73
76
  const { ...rest } = rules;
74
77
  delete rest.rev;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The permission-rule WRITE-FACE contract: what every backend behind `PermissionRuleWriter.apply` owes
3
+ * its caller. The sibling kit (`permission-rule-sync-contract.ts`) pins the pure JOIN; this one pins the
4
+ * DOOR it arrives through. A delta is DATA — a foreign caller, a replayed record, a deserialized join —
5
+ * so "the type system says it cannot happen" is not an answer a door may give, and a kind outside the
6
+ * union must be refused rather than folded as whichever arm a dispatch chain happens to end with. The
7
+ * positive control rides the same run on purpose: a backend that throws at everything refuses the
8
+ * unnamed kind too, so a suite without the control proves nothing.
9
+ *
10
+ * KIT form: vitest-free (`node:assert`), runner seam via {@link ContractAssertionRunner}, no skip hook.
11
+ */
12
+ import type { WritableDurableRulePartition } from "../permission-rule-store.js";
13
+ import { type ContractAssertionRunner } from "./contract-harness.js";
14
+ export interface PermissionRuleWriteContractHooks {
15
+ /** Open ONE fresh, EMPTY partition with a write face — once per case; teardown stays with the caller. */
16
+ make: () => WritableDurableRulePartition | Promise<WritableDurableRulePartition>;
17
+ runAssertion?: ContractAssertionRunner;
18
+ }
19
+ /** Run the full conformance suite. Throws (assert) on the first violated clause. */
20
+ export declare function permissionRuleWriteContract(hooks: PermissionRuleWriteContractHooks): Promise<void>;
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert";
2
+ import { PERMISSION_RULE_WRITER, isRuleRevision } from "../permission-rule-store.js";
3
+ import { beginContract } from "./contract-harness.js";
4
+ const GLOBAL = { kind: "global" };
5
+ function add(actor, counter) {
6
+ return { dot: { actor, counter }, origin: "user", createdAt: "2026-01-01T00:00:00.000Z" };
7
+ }
8
+ const IDENTITY = { behavior: "deny", rule: "Bash(curl:*)", scope: GLOBAL, tool: "Bash", match: "prefix", command: "curl" };
9
+ function tombstoneFor(removed, deletedBy) {
10
+ return { behavior: IDENTITY.behavior, rule: IDENTITY.rule, scope: GLOBAL, removedDots: removed.map((a) => a.dot), deletedBy };
11
+ }
12
+ function liveDots(rules) {
13
+ return new Set(rules.flatMap((r) => r.adds.map((a) => `${r.behavior}|${r.rule}|${a.dot.actor}#${a.dot.counter}`)));
14
+ }
15
+ async function rawShape(part) {
16
+ const raw = await part[PERMISSION_RULE_WRITER].readRaw();
17
+ const dots = [...liveDots(raw.rules)].sort();
18
+ const tombs = raw.tombstones.map((t) => `${t.behavior}|${t.rule}|${t.deletedBy.actor}#${t.deletedBy.counter}`).sort();
19
+ return JSON.stringify({ rev: raw.rev, dots, tombs });
20
+ }
21
+ function assertDecline(outcome, what) {
22
+ const answer = (outcome ?? {});
23
+ assert.equal(answer.conflict, true, `${what} must answer \`conflict: true\` — a decline tagged with anything else is an answer the engine reads as unreadable, so a kit accepting it certifies a backend the engine cannot use`);
24
+ assert.equal(answer.refused, undefined, `${what} must not wear the refusal tag as well — an answer that is two arms is an answer about neither`);
25
+ }
26
+ function assertRevision(outcome, what) {
27
+ const rev = outcome?.rev;
28
+ assert.ok(isRuleRevision(rev), `${what} must carry a revision — a non-negative safe integer — and this answer carried ${rev === undefined ? "none at all" : `a ${typeof rev}`}`);
29
+ }
30
+ export async function permissionRuleWriteContract(hooks) {
31
+ const { run, settle } = beginContract(hooks.runAssertion);
32
+ run("the named arms land, a landing ADVANCES the revision, and a spent revision is ANSWERED (positive control)", async () => {
33
+ const part = await hooks.make();
34
+ const writer = part[PERMISSION_RULE_WRITER];
35
+ const a1 = add("kit", 1);
36
+ const held = (await part.list()).rev;
37
+ const first = await writer.apply({ kind: "tighten-add", ...IDENTITY, add: a1 }, { expectedRev: held });
38
+ assert.ok(!("conflict" in first) && !("refused" in first), "a well-formed tighten-add must land");
39
+ assertRevision(first, "the tighten-add landing");
40
+ const afterAdd = await part.list();
41
+ assert.deepEqual(liveDots(afterAdd.rules), new Set([`deny|${IDENTITY.rule}|kit#1`]), "the added dot must be visible through list()");
42
+ assert.ok(afterAdd.rev > held, "a landed write must ADVANCE the revision — a constant one is not a fence, it is a number");
43
+ assert.equal((await part[PERMISSION_RULE_WRITER].readRaw()).rev, afterAdd.rev, "every face must report ONE revision");
44
+ const rawBefore = await rawShape(part);
45
+ const stale = await writer.apply({ kind: "tighten-add", ...IDENTITY, add: add("kit", 2) }, { expectedRev: held });
46
+ assertDecline(stale, "a spent expectedRev");
47
+ assertRevision(stale, "the declining conflict");
48
+ const staleDelete = await writer.apply({ kind: "tighten-delete", tombstone: tombstoneFor([a1], { actor: "kit", counter: 8 }) }, { expectedRev: held });
49
+ assertDecline(staleDelete, "a spent expectedRev on the delete arm");
50
+ assertRevision(staleDelete, "the declining conflict on the delete arm");
51
+ assert.equal(await rawShape(part), rawBefore, "a declined write must move nothing — rows, tombstones or revision");
52
+ const beforeDelete = (await part.list()).rev;
53
+ const removed = await writer.apply({ kind: "tighten-delete", tombstone: tombstoneFor([a1], { actor: "kit", counter: 9 }) }, { expectedRev: beforeDelete });
54
+ assert.ok(!("conflict" in removed) && !("refused" in removed), "a well-formed tighten-delete must land");
55
+ assertRevision(removed, "the tighten-delete landing");
56
+ assert.deepEqual(liveDots((await part.list()).rules), new Set(), "the tombstoned dot must leave the live view");
57
+ assert.ok((await part.list()).rev > beforeDelete, "a landed delete must advance the revision too");
58
+ const raw = await part[PERMISSION_RULE_WRITER].readRaw();
59
+ assert.ok(liveDots(raw.rules).has(`deny|${IDENTITY.rule}|kit#1`), "the raw face must RETAIN a tombstoned add (list() filters; the evidence may not be filtered too)");
60
+ const tomb = raw.tombstones.find((t) => t.deletedBy.actor === "kit" && t.deletedBy.counter === 9);
61
+ assert.ok(tomb !== undefined, "the raw face must retain the tombstone a landed delete wrote");
62
+ assert.deepEqual({ behavior: tomb.behavior, rule: tomb.rule, scope: tomb.scope, removedDots: tomb.removedDots }, { behavior: IDENTITY.behavior, rule: IDENTITY.rule, scope: GLOBAL, removedDots: [a1.dot] }, "the retained tombstone must be the WHOLE one — a partial one identifies nothing");
63
+ });
64
+ run("a delta whose kind is outside the union is REFUSED — not folded as the arm it resembles", async () => {
65
+ for (const [label, payload] of [
66
+ ["① a legal tombstone under an unnamed kind", { kind: "tighten-purge", tombstone: tombstoneFor([add("kit", 1)], { actor: "kit", counter: 9 }) }],
67
+ ["② a legal add payload under an unnamed kind", { kind: "import-add", ...IDENTITY, add: add("kit", 2) }],
68
+ ]) {
69
+ const part = await hooks.make();
70
+ const writer = part[PERMISSION_RULE_WRITER];
71
+ const seeded = add("kit", 7);
72
+ await writer.apply({ kind: "tighten-add", ...IDENTITY, add: seeded }, { expectedRev: (await part.list()).rev });
73
+ const before = await part.list();
74
+ const rawShapeBefore = await rawShape(part);
75
+ let threw = false;
76
+ try {
77
+ await writer.apply(payload, { expectedRev: before.rev });
78
+ }
79
+ catch {
80
+ threw = true;
81
+ }
82
+ assert.equal(threw, true, `${label}: the write face must refuse an unnamed delta kind`);
83
+ const after = await part.list();
84
+ assert.deepEqual(liveDots(after.rules), liveDots(before.rules), `${label}: a refused delta must not change the rows`);
85
+ assert.equal(after.rev, before.rev, `${label}: a refused delta must not advance the revision`);
86
+ assert.equal(await rawShape(part), rawShapeBefore, `${label}: a refused delta must move nothing in the RAW state either`);
87
+ }
88
+ });
89
+ await settle();
90
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The CROSS-STORE read-back rule (`@contract store.read_back_shape_judged`, quoted for integrators in
3
+ * `docs/INTEGRATION-CORE.md` §5.0) and the acceptance suite that pins it. Kit form as the sibling kits
4
+ * (see `permission-rule-write-contract.ts`): no test framework, no skip hook.
5
+ */
6
+ import { type ContractAssertionRunner } from "./contract-harness.js";
7
+ /**
8
+ * @contract store.read_back_shape_judged — every read a store answers, a single get and an enumeration
9
+ * alike, judges the SHAPE of what it read before answering: a body that is not the shape this store
10
+ * writes is CORRUPT, and a corrupt body is answered in the store's own corrupt vocabulary — a loud
11
+ * refusal, or an absence that carries a disclosure naming the loss — never as an empty set, an empty
12
+ * object or a default value, because an answer a caller cannot tell apart from "there is nothing here"
13
+ * turns a recoverable incident into a silent one. Which read-back failures a store TOLERATES (an
14
+ * enumeration that skips one unreadable row and serves the rest, a torn trailing write read as absent)
15
+ * is a per-store fact registered beside that store's own contract, never assumed from another store's.
16
+ */
17
+ export declare const CORRUPT_BODY_FORMS: readonly ["not-an-object", "missing-required", "wrong-type", "torn-json"];
18
+ /** One way a persisted body can fail to be the shape its store writes. */
19
+ export type CorruptBodyForm = (typeof CORRUPT_BODY_FORMS)[number];
20
+ export interface ReadBackShapeSubject {
21
+ /** Declared ONCE per subject, and only by a backend that keeps no bytes a foreign writer could damage
22
+ * (a process-local map holding objects it minted itself). Never per attempt: a planter that cannot
23
+ * find bytes it knows exist must FAIL rather than exempt its backend. */
24
+ readonly unplantable?: string;
25
+ /** Plant ONE corrupt body of `form`, with `sentinel` inside the planted bytes wherever the form allows
26
+ * (that is how clause 3 detects a body handed straight back). No declining return: a backend with no
27
+ * bodies declares that at the subject, and a planter that cannot find bytes it knows exist THROWS. */
28
+ plant: (form: CorruptBodyForm, sentinel: string) => Promise<void>;
29
+ /** Perform the read under test. THROWING IS AN ANSWER — the loud refusal — and is not a suite failure. */
30
+ read: () => Promise<unknown>;
31
+ /** Write ONE well-formed row — clause 1's subject. */
32
+ seed: () => Promise<void>;
33
+ close?: () => void;
34
+ }
35
+ export interface StoreReadBackShapeHooks {
36
+ /** The face under test, named as a reader would name it (`"FileCheckpointStore.get"`). Failure text only. */
37
+ face: string;
38
+ /** Open a FRESH, EMPTY store and return its subject. Called once per observation; teardown is `close`. */
39
+ open: () => Promise<ReadBackShapeSubject>;
40
+ runAssertion?: ContractAssertionRunner;
41
+ }
42
+ /**
43
+ * Run the read-back shape suite over ONE read face. Throws (assert) on the first violated clause.
44
+ *
45
+ * Clauses: (1) a seeded row and an empty store both ANSWER, answer differently, and the empty answer is
46
+ * REPRODUCIBLE — a face that throws at everything, answers everything alike, or carries incidental data
47
+ * (a read id, a timestamp) proves nothing by differing under corruption;
48
+ * (2) each of the four {@link CORRUPT_BODY_FORMS}, planted, is answered differently from an empty store;
49
+ * (3) and the answer never contains the planted bytes.
50
+ *
51
+ * CEILING: clause 2 proves the corrupt case is DISTINGUISHABLE, not that the distinguishing answer is
52
+ * right — a face answering one invalid row where a good read answers rows passes. Whether the store then
53
+ * named the loss correctly is judged by its own section in §5.0, by a person; the four forms are a
54
+ * fixture set, not the space of corrupt bodies.
55
+ */
56
+ export declare function storeReadBackShapeContract(hooks: StoreReadBackShapeHooks): Promise<void>;
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert";
2
+ import { thrownText } from "../thrown-value.js";
3
+ import { beginContract } from "./contract-harness.js";
4
+ export const CORRUPT_BODY_FORMS = ["not-an-object", "missing-required", "wrong-type", "torn-json"];
5
+ async function observe(subject) {
6
+ try {
7
+ const value = await subject.read();
8
+ return { threw: false, body: safeStringify(value) };
9
+ }
10
+ catch (err) {
11
+ return { threw: true, message: thrownText(err) };
12
+ }
13
+ }
14
+ function safeStringify(value) {
15
+ const seen = new WeakSet();
16
+ try {
17
+ return (JSON.stringify(value, (_k, v) => {
18
+ if (typeof v === "object" && v !== null) {
19
+ if (seen.has(v))
20
+ return "[circular]";
21
+ seen.add(v);
22
+ }
23
+ return v;
24
+ }) ?? String(value));
25
+ }
26
+ catch {
27
+ return String(value);
28
+ }
29
+ }
30
+ function render(o) {
31
+ return o.threw ? `threw(${o.message})` : `answered ${o.body}`;
32
+ }
33
+ async function withSubject(hooks, fn) {
34
+ const subject = await hooks.open();
35
+ try {
36
+ return await fn(subject);
37
+ }
38
+ finally {
39
+ subject.close?.();
40
+ }
41
+ }
42
+ export async function storeReadBackShapeContract(hooks) {
43
+ const { run, settle } = beginContract(hooks.runAssertion);
44
+ const where = hooks.face;
45
+ run(`${where}: the read face tells a present row from an absent one (positive control)`, async () => {
46
+ const empty = await withSubject(hooks, observe);
47
+ const seeded = await withSubject(hooks, async (s) => {
48
+ await s.seed();
49
+ return observe(s);
50
+ });
51
+ assert.equal(empty.threw, false, `${where}: reading an EMPTY store must answer, not throw (${render(empty)}) — absence is not damage`);
52
+ assert.equal(seeded.threw, false, `${where}: reading a well-formed row must answer, not throw (${render(seeded)})`);
53
+ const again = await withSubject(hooks, observe);
54
+ assert.deepEqual(again, empty, `${where}: two reads of two EMPTY stores answered differently (${render(empty)} then ${render(again)}) — this suite's oracle is "the corrupt answer differs from the empty one", which is free for a face carrying incidental data; project it away in the binding`);
55
+ assert.notDeepEqual(seeded, empty, `${where}: a seeded store and an empty one answered identically (${render(empty)}) — this face reports nothing a corrupt body could differ from, so every clause below would pass without meaning anything`);
56
+ });
57
+ for (const form of CORRUPT_BODY_FORMS) {
58
+ run(`${where}: a ${form} body is not read as an absent row`, async () => {
59
+ const sentinel = `sema-corrupt-sentinel-${form}`;
60
+ const empty = await withSubject(hooks, observe);
61
+ const corrupt = await withSubject(hooks, async (s) => {
62
+ if (s.unplantable !== undefined) {
63
+ assert.ok(s.unplantable.length > 0, `${where}: a subject declaring no plantable body must say WHY`);
64
+ return undefined;
65
+ }
66
+ await s.plant(form, sentinel);
67
+ return observe(s);
68
+ });
69
+ if (corrupt === undefined)
70
+ return;
71
+ assert.notDeepEqual(corrupt, empty, `${where}: a ${form} body was answered exactly as an absent row is (${render(empty)}) — a corrupt body must reach the caller as a refusal or as an absence that names the loss, never as the store's empty answer`);
72
+ if (!corrupt.threw) {
73
+ assert.ok(!corrupt.body.includes(sentinel), `${where}: a ${form} body was handed back through the read face (the planted marker is in the answer) — the read judged nothing`);
74
+ }
75
+ });
76
+ }
77
+ await settle();
78
+ }
@@ -121,11 +121,22 @@ export type EffectiveMemoryScopes = {
121
121
  state: "mounted";
122
122
  reason?: never;
123
123
  contract: "v2" | "legacy";
124
+ /** `shadowed` = how many of this scope's entries the session's write home retires from the
125
+ * DEFAULT read face by name (they stay reachable by id and through `includeShadowed`).
126
+ * PRESENT-IFF non-zero, and never on the write home's own row — absence is "none", which is
127
+ * why zero is not spelled. A mount-time snapshot: the tool faces re-derive it per call, so a
128
+ * later challenge or tombstone can make the live answer smaller than this number. */
124
129
  scopes: Array<{
125
130
  scope: string;
126
131
  origin: "deployment" | "request";
132
+ shadowed?: number;
127
133
  }>;
128
134
  writeScope: string | null;
135
+ /** The project identity the assembly point DECLARED for this task, echoed verbatim
136
+ * ({@link import("./memory.js").MemoryProjectKey}). PRESENT-IFF the spec carried one:
137
+ * absence means the deployment said nothing, which is not the same as `{ absent }`, which
138
+ * means it looked and found none. */
139
+ projectKey?: import("./memory.js").MemoryProjectKey;
129
140
  materializedResidue?: never;
130
141
  } | {
131
142
  state: "memoryless";
@@ -35,6 +35,11 @@ export type ImageInput = {
35
35
  * shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and folds across a delegation chain (max-rank: `off` <
36
36
  * `classify` < `always`). One closed set; every seat that names a doctrine spells it through this alias. */
37
37
  export type ShellGateDoctrine = "off" | "classify" | "always";
38
+ /** The approval POSTURE a host declares for a task — {@link TaskSpec.approverPosture}'s value set: a
39
+ * statement about who answers this run's approval cards, never an instruction to the gate. One word
40
+ * today (`"bypass"` — the host answers every card itself, automatically). The engine only RENDERS it,
41
+ * so adding a word here changes what the model is told and nothing else. */
42
+ export type ApproverPosture = "bypass";
38
43
  export interface TaskSpec {
39
44
  taskId?: string;
40
45
  /** The instruction / latest user message for this turn. */
@@ -924,6 +929,32 @@ export interface TaskSpec {
924
929
  * deployment's face and the resuming principal's deny bit are judged afresh on every leg.
925
930
  */
926
931
  autoModeRequested?: true;
932
+ /**
933
+ * The host's declared APPROVER POSTURE for this task — a typed fact seat, the same ownership split
934
+ * as {@link shellGate} and {@link autoModeRequested}: the service layer declares it (it is the layer
935
+ * that knows who sits at the cards), core only CONSUMES it. `"bypass"` says the host answers every
936
+ * approval card itself, automatically.
937
+ *
938
+ * **The gate does not read it.** Cards are raised, routed and recorded byte-identically with and
939
+ * without this seat — no rule is relaxed, no ask is skipped, no decision is pre-empted. The seat's
940
+ * one effect is that the ENGINE tells the model which posture the run is under, in its own marked
941
+ * `<system-reminder>` on the first turn. That is the whole point of the seat: a posture is a fact of
942
+ * the run, and a host that instead splices the sentence into the objective hands the model
943
+ * reminder-shaped text with no engine mark, which the mark declaration tells it to treat as data and
944
+ * flag. One fact, one seat, one mint.
945
+ *
946
+ * Absent ⇒ no posture declared, zero bytes rendered (byte-identical to the pre-seat shape). Only the
947
+ * literal `"bypass"` is a value: any other present value is refused at the door
948
+ * (`config.approver_posture_invalid`) rather than folded back to absence — a marshalled typo must not
949
+ * silently un-declare a posture the host meant to declare.
950
+ *
951
+ * **Inheritance / resume:** session-wide, exactly like {@link autoModeRequested} — an engine-spawned
952
+ * child inherits it through the trusted constraint chain (`InheritedGate.approverPosture`), and the
953
+ * resolved value is recorded on the checkpoint's data half so a redemption in another process still
954
+ * knows the posture without the seat being re-passed. Re-passing it is honoured; the sources fold
955
+ * seat-first.
956
+ */
957
+ approverPosture?: ApproverPosture;
927
958
  /** Task-scoped MCP servers, materialized into tools then disposed. */
928
959
  mcp?: McpServerSpec[];
929
960
  /** Task-scoped A2A peers (remote agents), whose advertised skills mount as `a2a__<peer>__<skill>`
@@ -123,9 +123,9 @@ export const TOOL_CATALOG_ENTRIES = [
123
123
  { id: "exit-plan-mode", name: "ExitPlanMode", source: "builtin", definedIn: "src/core/present-plan-tool.ts", mountedBy: ["optional"], cards: ["exit-plan-mode"], face: { family: "plan", effect: "read", contract: c("core.exit_plan_mode@1"), renderHints: { userFacingName: "", approvalCard: "plan" } } },
124
124
  { id: "enter-plan-mode", name: "EnterPlanMode", source: "builtin", definedIn: "src/core/present-plan-tool.ts", mountedBy: ["optional"], cards: ["enter-plan-mode"], face: { family: "plan", effect: "read", contract: c("core.enter_plan_mode@1") } },
125
125
  { id: "refresh-mcp-tools", name: REFRESH_MCP_TOOLS_TOOL_NAME, source: "builtin", definedIn: "src/core/runner/prepare-protocol-tools.ts", mountedBy: ["optional"], cards: [], face: { family: "protocol", effect: "read" } },
126
- { id: "memory-search", name: "memory_search", source: "builtin", definedIn: "src/core/memory-engine/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contentOrigin: "local", contract: c("core.memory_search@1", "6"), offload: false } },
127
- { id: "memory-get", name: "memory_get", source: "builtin", definedIn: "src/core/memory-engine/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contentOrigin: "local", contract: c("core.memory_get@1", "6"), offload: false } },
128
- { id: "memory-index", name: "memory_index", source: "builtin", definedIn: "src/core/memory-engine/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contentOrigin: "local", contract: c("core.memory_index@1"), offload: false } },
126
+ { id: "memory-search", name: "memory_search", source: "builtin", definedIn: "src/core/memory-engine/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contentOrigin: "local", contract: c("core.memory_search@1", "7"), offload: false } },
127
+ { id: "memory-get", name: "memory_get", source: "builtin", definedIn: "src/core/memory-engine/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contentOrigin: "local", contract: c("core.memory_get@1", "7"), offload: false } },
128
+ { id: "memory-index", name: "memory_index", source: "builtin", definedIn: "src/core/memory-engine/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contentOrigin: "local", contract: c("core.memory_index@1", "2"), offload: false } },
129
129
  { id: "memory-list", name: "memory_list", source: "builtin", definedIn: "src/core/shared-memory/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contract: c("core.memory_list@1"), offload: false } },
130
130
  { id: "memory-read", name: "memory_read", source: "builtin", definedIn: "src/core/shared-memory/tools.ts", mountedBy: ["memory"], cards: [], face: { family: "memory", effect: "read", contract: c("core.memory_read@1"), offload: false } },
131
131
  { id: "cron-create", name: "CronCreate", source: "builtin", definedIn: "src/tools/scheduler-tools.ts", mountedBy: ["scheduler"], cards: ["cron-create"], face: { family: "other", effect: "write", contract: c("core.cron_create@1") } },