@sema-agent/core 7.20.0 → 7.21.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.
- package/CHANGELOG.md +84 -0
- package/dist/core/ask-origin.d.ts +95 -8
- package/dist/core/ask-origin.js +37 -5
- package/dist/core/engine-notice.d.ts +9 -0
- package/dist/core/gate-bounded-await.d.ts +6 -1
- package/dist/core/gate-bounded-await.js +13 -3
- package/dist/core/gate-fold.js +18 -5
- package/dist/core/gate-lanes.js +6 -12
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/memory-engine/consolidation-driver.d.ts +3 -0
- package/dist/core/memory-engine/consolidation-driver.js +20 -1
- package/dist/core/memory-engine/consolidation.d.ts +7 -0
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +26 -1
- package/dist/core/memory-engine/distiller.js +9 -2
- package/dist/core/memory-engine/tools.js +6 -0
- package/dist/core/path-identity.d.ts +13 -0
- package/dist/core/path-identity.js +14 -0
- package/dist/core/runner/env-capability-census.d.ts +3 -1
- package/dist/core/runner/env-capability-census.js +24 -7
- package/dist/core/runner/gate-exit.d.ts +7 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +1 -0
- package/dist/core/runner/inherited-ask-grants.js +5 -2
- package/dist/core/runner/memory-consolidation-driver.d.ts +72 -2
- package/dist/core/runner/memory-consolidation-driver.js +57 -0
- package/dist/core/runner/prepare-config-doors.js +2 -0
- package/dist/core/runner/prepare-gate-stations.d.ts +6 -8
- package/dist/core/runner/prepare-gate-stations.js +1 -7
- package/dist/core/runner/prepare-policy-chain.d.ts +1 -0
- package/dist/core/runner/prepare-policy-chain.js +52 -20
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +8 -1
- package/dist/core/runner/prepare-wiring-manifest.js +7 -2
- package/dist/core/runner/run-settle-and-teardown.js +5 -0
- package/dist/core/runner/write-protection-station.d.ts +59 -0
- package/dist/core/runner/write-protection-station.js +32 -0
- package/dist/core/runner-deps.d.ts +21 -0
- package/dist/core/sensitive-path-policy.js +13 -0
- package/dist/core/wiring-manifest.d.ts +47 -0
- package/dist/core/wiring-manifest.js +2 -0
- package/dist/core/write-protect.d.ts +41 -3
- package/dist/core/write-protect.js +40 -14
- package/dist/engine/execution-env/node-execution-env.d.ts +10 -0
- package/dist/engine/execution-env/node-execution-env.js +29 -1
- package/dist/engine/harness/types.d.ts +10 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/tools/fs/bash-readonly-classifier.js +36 -7
- package/dist/tools/fs/read-deny.d.ts +4 -2
- package/dist/tools/fs/read-deny.js +5 -0
- package/dist/tools/fs/safety.d.ts +12 -0
- package/dist/tools/fs/safety.js +43 -38
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +9 -1
- package/wire-changes/core@7.20.1.json +183 -0
- package/wire-changes/core@7.21.0.json +452 -0
|
@@ -152,7 +152,7 @@ export async function preparePolicyChain(input) {
|
|
|
152
152
|
}, "mandate_unreconstructible", creq);
|
|
153
153
|
const markerChannelFullDeny = (creq) => attestAskUnresolvableCause({ action: "deny", message: `no approver is reachable for the inherited approval of "${creq.toolName}" and the marker channel is at capacity — denied fail-closed` }, "approver_unavailable", creq);
|
|
154
154
|
const inheritedAskGrants = new Map();
|
|
155
|
-
const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs) => {
|
|
155
|
+
const recordInheritedAskGrant = (toolCallId, approver, presented, waitMs, settled) => {
|
|
156
156
|
if (typeof approver !== "function")
|
|
157
157
|
return;
|
|
158
158
|
if (presented === undefined)
|
|
@@ -162,7 +162,7 @@ export async function preparePolicyChain(input) {
|
|
|
162
162
|
const argsJson = askGrantShapeOf(presented);
|
|
163
163
|
if (argsJson === undefined)
|
|
164
164
|
return;
|
|
165
|
-
inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs });
|
|
165
|
+
inheritedAskGrants.set(toolCallId, { approver, argsJson, presented, waitMs, settled });
|
|
166
166
|
};
|
|
167
167
|
const sandboxDeclared = isIsolated(executionEnv);
|
|
168
168
|
const sandboxAdmissionArmed = (() => {
|
|
@@ -277,37 +277,51 @@ export async function preparePolicyChain(input) {
|
|
|
277
277
|
cwdRef: handsCwdRef,
|
|
278
278
|
deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
|
|
279
279
|
});
|
|
280
|
-
const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider, ancestorTracker) => {
|
|
280
|
+
const recheckApprovedEdit = async (pol, onAskOf, creq, edit, approved, csignal, ancestorDecider, ancestorTracker) => {
|
|
281
281
|
let editArgs = edit;
|
|
282
|
+
let settledBy = approved;
|
|
283
|
+
const vetoed = (d) => {
|
|
284
|
+
const out = { ...d };
|
|
285
|
+
carryEngineAttestations(d, out);
|
|
286
|
+
carryPrecedingSettlement(settledBy, out);
|
|
287
|
+
return out;
|
|
288
|
+
};
|
|
282
289
|
for (let round = 0;; round++) {
|
|
283
290
|
if (round >= 3) {
|
|
284
|
-
return {
|
|
291
|
+
return vetoed({
|
|
285
292
|
action: "deny",
|
|
286
293
|
message: `the approval-edit chain for "${creq.toolName}" exceeded the re-approval cap (inherited parent policy) — denied fail-closed; re-submit the edited action directly`,
|
|
287
|
-
};
|
|
294
|
+
});
|
|
288
295
|
}
|
|
289
296
|
let re;
|
|
290
297
|
try {
|
|
291
298
|
re = await pol.check({ ...creq, args: editArgs }, csignal ?? abortController.signal);
|
|
292
299
|
}
|
|
293
300
|
catch (err) {
|
|
294
|
-
return {
|
|
301
|
+
return vetoed({
|
|
295
302
|
action: "deny",
|
|
296
303
|
message: `inherited parent policy re-check of the approved edit errored for "${creq.toolName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
297
|
-
};
|
|
304
|
+
});
|
|
298
305
|
}
|
|
299
306
|
if (re.action === "deny")
|
|
300
|
-
return re;
|
|
307
|
+
return vetoed(re);
|
|
301
308
|
if (re.action === "allow") {
|
|
302
|
-
|
|
309
|
+
const out = { action: "allow", updatedInput: re.updatedInput !== undefined ? re.updatedInput : editArgs };
|
|
310
|
+
carryEngineAttestations(re, out);
|
|
311
|
+
carryPrecedingSettlement(settledBy, out);
|
|
312
|
+
return out;
|
|
303
313
|
}
|
|
304
314
|
if (re.updatedInput !== undefined)
|
|
305
315
|
editArgs = re.updatedInput;
|
|
306
316
|
const judged = await judgeInheritedClassifier({ autoMode: ancestorDecider !== undefined ? { decider: ancestorDecider, denialTracking: ancestorTracker } : undefined, ask: re, req: { ...creq, args: editArgs }, signal: csignal ?? abortController.signal, subject: "the approved edit", allowRewrite: editArgs });
|
|
307
|
-
if (judged.kind === "allow")
|
|
308
|
-
|
|
317
|
+
if (judged.kind === "allow") {
|
|
318
|
+
const cleared = { ...judged.result };
|
|
319
|
+
carryEngineAttestations(judged.result, cleared);
|
|
320
|
+
carryPrecedingSettlement(settledBy, cleared);
|
|
321
|
+
return cleared;
|
|
322
|
+
}
|
|
309
323
|
if (judged.kind === "deny")
|
|
310
|
-
return judged.result;
|
|
324
|
+
return vetoed(judged.result);
|
|
311
325
|
const { ask: editAsk, fallback: editFallback, mintedHere: editMintedHere } = judged;
|
|
312
326
|
re = editAsk;
|
|
313
327
|
const rr = await resolveAsk({
|
|
@@ -326,8 +340,12 @@ export async function preparePolicyChain(input) {
|
|
|
326
340
|
settleDenialLimitFallback({ fallback: editFallback, mintedHere: editMintedHere, tracker: ancestorTracker, resolved: rr, headless: headlessDenyAtRecheck, stop: stopForDenialLimit, toolName: creq.toolName, toolCallId: creq.toolCallId });
|
|
327
341
|
if (rr.action !== "allow")
|
|
328
342
|
return rr;
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
settledBy = rr;
|
|
344
|
+
if (rr.updatedInput === undefined) {
|
|
345
|
+
const out = { action: "allow", updatedInput: editArgs };
|
|
346
|
+
carryPrecedingSettlement(rr, out);
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
331
349
|
editArgs = rr.updatedInput;
|
|
332
350
|
}
|
|
333
351
|
};
|
|
@@ -404,9 +422,9 @@ export async function preparePolicyChain(input) {
|
|
|
404
422
|
if (resolved.action !== "allow")
|
|
405
423
|
return resolved;
|
|
406
424
|
if (resolved.updatedInput !== undefined) {
|
|
407
|
-
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
|
|
425
|
+
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
|
|
408
426
|
}
|
|
409
|
-
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
|
|
427
|
+
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
|
|
410
428
|
return { action: "allow" };
|
|
411
429
|
},
|
|
412
430
|
}
|
|
@@ -473,12 +491,16 @@ export async function preparePolicyChain(input) {
|
|
|
473
491
|
if (resolved.action !== "allow")
|
|
474
492
|
return resolved;
|
|
475
493
|
if (resolved.updatedInput !== undefined) {
|
|
476
|
-
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
|
|
494
|
+
return recheckApprovedEdit(pc.policy, pc.onAsk, creq, resolved.updatedInput, resolved, csignal, pc.autoMode?.decider, pc.autoMode?.denialTracking);
|
|
477
495
|
}
|
|
478
496
|
if (pc.preToolUse === undefined) {
|
|
479
|
-
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs);
|
|
497
|
+
recordInheritedAskGrant(creq.toolCallId, pc.onAsk, resolved.presentedInput, askWaitMs, resolved);
|
|
480
498
|
}
|
|
481
|
-
|
|
499
|
+
if (decision.updatedInput === undefined)
|
|
500
|
+
return resolved;
|
|
501
|
+
const approvedWithRewrite = { ...resolved, updatedInput: decision.updatedInput };
|
|
502
|
+
carryEngineAttestations(resolved, approvedWithRewrite);
|
|
503
|
+
return approvedWithRewrite;
|
|
482
504
|
},
|
|
483
505
|
});
|
|
484
506
|
const rewriteCapableLayers = policy !== undefined || parentConstraintWrappers.length > 0;
|
|
@@ -540,6 +562,7 @@ export async function preparePolicyChain(input) {
|
|
|
540
562
|
const recheckLayers = [...recheckPrefix, ...postEmitterSameInstanceAdapters];
|
|
541
563
|
const finalReq = { ...req, args: decision.updatedInput };
|
|
542
564
|
let asked;
|
|
565
|
+
const revalidated = {};
|
|
543
566
|
try {
|
|
544
567
|
for (const layer of recheckLayers) {
|
|
545
568
|
const d = await layer.check(finalReq, signal);
|
|
@@ -558,13 +581,22 @@ export async function preparePolicyChain(input) {
|
|
|
558
581
|
}
|
|
559
582
|
if (d.action === "ask" && asked === undefined)
|
|
560
583
|
asked = d;
|
|
584
|
+
carryEngineAttestations(d, revalidated);
|
|
561
585
|
}
|
|
562
586
|
}
|
|
563
587
|
finally {
|
|
564
588
|
rewriteEmitterIndex.delete(req.toolCallId);
|
|
565
589
|
}
|
|
566
590
|
if (asked !== undefined && decision.action === "allow") {
|
|
567
|
-
|
|
591
|
+
const surfaced = { ...asked, updatedInput: decision.updatedInput };
|
|
592
|
+
carryEngineAttestations(asked, surfaced);
|
|
593
|
+
return surfaced;
|
|
594
|
+
}
|
|
595
|
+
if (decision.action === "allow") {
|
|
596
|
+
const owned = { ...decision };
|
|
597
|
+
carryEngineAttestations(decision, owned);
|
|
598
|
+
carryEngineAttestations(revalidated, owned);
|
|
599
|
+
return owned;
|
|
568
600
|
}
|
|
569
601
|
return decision;
|
|
570
602
|
},
|
|
@@ -690,7 +690,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
692
|
const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, envHandToolNames, handsCwdRef, readDenyMatcher, questionToolMounted, runtimeCaps, inheritedAncestorRules, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
|
|
693
|
-
const { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, durableApproval, wiringManifest, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace } = prepareWiringManifest({ spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, lspManagerWired, lspDiagnosticsLive: lspDiagnostics !== undefined });
|
|
693
|
+
const { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, durableApproval, wiringManifest, writeProtectionCheck, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace } = prepareWiringManifest({ spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, lspManagerWired, lspDiagnosticsLive: lspDiagnostics !== undefined });
|
|
694
694
|
if (fileHistoryBoundary !== undefined) {
|
|
695
695
|
harness.on("tool_call", async () => {
|
|
696
696
|
await fileHistoryBoundary.settle();
|
|
@@ -701,7 +701,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
701
701
|
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, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
|
|
702
702
|
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 });
|
|
703
703
|
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 });
|
|
704
|
-
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,
|
|
704
|
+
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, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
|
|
705
705
|
const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef });
|
|
706
706
|
const preparedHolder = {};
|
|
707
707
|
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 });
|
|
@@ -28,6 +28,7 @@ import { type ToolResultStore } from "../tool-result-store.js";
|
|
|
28
28
|
import { type OnAsk, type ToolPolicy } from "../tool-policy.js";
|
|
29
29
|
import type { GateOutcome } from "../gate-outcome.js";
|
|
30
30
|
import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect, ToolExecuteContext } from "../types.js";
|
|
31
|
+
import type { WriteProtectionCheck } from "../write-protect.js";
|
|
31
32
|
import { type UsageWindow } from "../usage-window-store.js";
|
|
32
33
|
import type { ToolRoster } from "../tool-roster.js";
|
|
33
34
|
import { type AutoModeArmFact, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
|
|
@@ -41,7 +42,8 @@ export interface PrepareWiringManifestInput {
|
|
|
41
42
|
/** borrowed-readonly — the deployment seats, whole: `onAsk` (the seat's deps half), `onError` (the warning), `executionEnv`
|
|
42
43
|
* (the hook-env predicate), `checkpointStore` (durable infra), the manifest's dozen wiring facts (`backgroundAgentStore`,
|
|
43
44
|
* `permissionRuleStore`, `permissionRuleSyncWired`, `onBackgroundChildEvent`, `lockedConfig`, `compliancePostureResolver`,
|
|
44
|
-
* `memoryScopeAdmission`, `retentionPolicy`, `onElicit`), `onNotice`,
|
|
45
|
+
* `memoryScopeAdmission`, `retentionPolicy`, `onElicit`), `onNotice`, the write-protection judge's compile seats
|
|
46
|
+
* (`writeProtectedPaths`, `memoryEngineDir`), and the usage-governance seats (`usageWindowStore`,
|
|
45
47
|
* `now`, `onUsageGovernance`…) the helper reads through the whole record. */
|
|
46
48
|
deps: RunnerDeps;
|
|
47
49
|
/** borrowed-readonly — the trusted spawn-side channel: `questionFaceStripped`, `isDelegatedChild`, `onTaskNotification`,
|
|
@@ -67,6 +69,8 @@ export interface PrepareWiringManifestInput {
|
|
|
67
69
|
effectivePolicy: ToolPolicy | undefined;
|
|
68
70
|
/** borrowed-readonly — whether an execution env is wired (the hand band exists at all). */
|
|
69
71
|
handsEnabled: boolean;
|
|
72
|
+
/** borrowed-readonly — the resolved task root: the base a `base: "root"` write tool's relative spelling resolves against. */
|
|
73
|
+
taskRootFinal: string;
|
|
70
74
|
/** borrowed-readonly — the materialized MCP handle: `tools` (the destructive-ungated scan) and `statuses` (the manifest face). */
|
|
71
75
|
mcp: Pick<MaterializedMcp, "tools" | "statuses">;
|
|
72
76
|
/** borrowed-readonly — the assembled safety vocabulary (the ungated scans and the manifest's armed cell). Read only. */
|
|
@@ -158,6 +162,9 @@ export interface PrepareWiringManifestResult {
|
|
|
158
162
|
durableApproval: TaskSpec["durableApproval"];
|
|
159
163
|
/** owned — this leg's EFFECTIVE wiring manifest (`Prepared.wiringManifest`). */
|
|
160
164
|
wiringManifest: WiringManifest;
|
|
165
|
+
/** owned — the leg's write-protection gate input, or undefined when the table was emptied (no judge, no
|
|
166
|
+
* tighten mounted); handed to the gate station rather than compiled a second time there. */
|
|
167
|
+
writeProtectionCheck: WriteProtectionCheck | undefined;
|
|
161
168
|
/** owned — THE park predicate as one named const (`parkLane.effective === true`). */
|
|
162
169
|
parkLaneArmed: boolean;
|
|
163
170
|
/** owned — THE gate-machinery activation predicate as one named const: a policy, a screening hook, an egress or
|
|
@@ -7,6 +7,8 @@ import { isVolatileOffloadStore } from "../tool-result-store.js";
|
|
|
7
7
|
import { durableMandateSourceOf, isLiveApproverSeat } from "../tool-policy.js";
|
|
8
8
|
import { deliverEngineNotice } from "../types.js";
|
|
9
9
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
10
|
+
import { mountWriteProtection } from "./write-protection-station.js";
|
|
11
|
+
import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
|
|
10
12
|
import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
|
|
11
13
|
import { LSP_CONTRACT_ID, countElicitOptIns, deriveLspSeam, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
12
14
|
import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
|
|
@@ -111,7 +113,7 @@ function announceDurableGateUnavailable(args) {
|
|
|
111
113
|
});
|
|
112
114
|
}
|
|
113
115
|
export function prepareWiringManifest(input) {
|
|
114
|
-
const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, lspManagerWired, lspDiagnosticsLive } = input;
|
|
116
|
+
const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, lspManagerWired, lspDiagnosticsLive } = input;
|
|
115
117
|
const onAsk = spec.onAsk ?? deps.onAsk;
|
|
116
118
|
const handWriteTools = toolRoster.entries.filter((e) => e.mountedBy === "hands" && (e.effect === "write" || e.effect === "idempotent")).map((e) => e.name);
|
|
117
119
|
const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
|
|
@@ -168,6 +170,7 @@ export function prepareWiringManifest(input) {
|
|
|
168
170
|
const platformSuspendArmed = durableSuspendInfraReady && (envLifetimeSuspendAt !== undefined || usageGovernance !== undefined);
|
|
169
171
|
const durableApproval = spec.durableApproval ??
|
|
170
172
|
(runtimeCaps?.forceDurableGate ? { scope: spec.principal || DEFAULT_IRREVERSIBLE_SCOPE } : undefined);
|
|
173
|
+
const writeProtection = mountWriteProtection({ deps, env: handsEnabled ? executionEnv : undefined, rootPath: taskRootFinal, roster: toolRoster, sessionId, runId });
|
|
171
174
|
const wiringQuestionSeam = resolveQuestionSeam(spec, deps);
|
|
172
175
|
const wiringQuestionStripped = internals?.questionFaceStripped === true;
|
|
173
176
|
const wiringAskSeam = resolveAskSeamForm(spec, deps);
|
|
@@ -204,6 +207,8 @@ export function prepareWiringManifest(input) {
|
|
|
204
207
|
...(modelGateManifest !== undefined ? { modelGate: modelGateManifest } : {}),
|
|
205
208
|
autoMode: { ...autoModeArmReason },
|
|
206
209
|
mcp: mcpManifestEntries(lockedPreflight.mcp, mcp.statuses),
|
|
210
|
+
...(writeProtection !== undefined ? { writeProtectionTargetView: writeProtection.targetView } : {}),
|
|
211
|
+
autoConsolidationOnRecommendation: autoRunOnRecommendationEffective(deps),
|
|
207
212
|
tools: toolRoster,
|
|
208
213
|
hooks: wiredHookEntries(hooks, spec.hooks !== undefined ? "spec" : "deps", preToolUseObservational),
|
|
209
214
|
...(lspManagerWired ? { lsp: deriveLspSeam(toolRoster.entries.some((e) => e.contract.contractId === LSP_CONTRACT_ID), lspDiagnosticsLive) } : {}),
|
|
@@ -242,5 +247,5 @@ export function prepareWiringManifest(input) {
|
|
|
242
247
|
});
|
|
243
248
|
const hookContextConsumerWired = hooks?.preToolUse !== undefined || hooks?.postToolUse !== undefined || hooks?.postToolUseFailure !== undefined;
|
|
244
249
|
const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
|
|
245
|
-
return { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, platformSuspendArmed, durableApproval, wiringManifest, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace };
|
|
250
|
+
return { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, platformSuspendArmed, durableApproval, wiringManifest, writeProtectionCheck: writeProtection?.check, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace };
|
|
246
251
|
}
|
|
@@ -18,6 +18,7 @@ import { resumeDecisionWasNegative } from "./decide-continuation.js";
|
|
|
18
18
|
import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
|
|
19
19
|
import { generatePromptSuggestions } from "./prompt-suggestions.js";
|
|
20
20
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
21
|
+
import { autoRunConsolidationFromRecommendation, autoRunOnRecommendationEffective, scopeHasUnconsumedRecommendation } from "./memory-consolidation-driver.js";
|
|
21
22
|
import { amendTerminal, terminalProjection } from "./terminal-projection.js";
|
|
22
23
|
export async function runSettleAndTeardown(input) {
|
|
23
24
|
const { spec, queue, prepared, resume, internals, rs, stats, ident, parentToolCallId, subagentName, loopLatch, timeout, ownCommittedTailRef, emitDelegationLifecycle, taskIdRef, manualCompactRef, drainManualCompact, final, threw, abortedLive, userInterruptedLive, answerSettled, strandedHumanAnswers, reasoningResolution, comp, setResult, onSuggestions, onSuspend, runner, sessions } = input;
|
|
@@ -240,6 +241,10 @@ export async function runSettleAndTeardown(input) {
|
|
|
240
241
|
if (prepared.memoryEngineSession) {
|
|
241
242
|
const mes = prepared.memoryEngineSession;
|
|
242
243
|
await settleTeardownLeg(() => mes.harvest(), "memoryEngineSession.harvest (settle leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
|
|
244
|
+
const scope = mes.handle.writeScope;
|
|
245
|
+
if (scope !== null && autoRunOnRecommendationEffective(runner.deps) && scopeHasUnconsumedRecommendation(mes.engine, scope)) {
|
|
246
|
+
void autoRunConsolidationFromRecommendation({ deps: runner.deps, engine: mes.engine, scope, sessionId: prepared.sessionId, onNotice: runner.deps.onNotice, onError: runner.deps.onError });
|
|
247
|
+
}
|
|
243
248
|
}
|
|
244
249
|
emitTrace(rs.telemetry.tracer, () => ({
|
|
245
250
|
kind: "task.end",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The leg's write-protection MOUNT — one compile, and the loud outlet for the reading it could not get.
|
|
3
|
+
*
|
|
4
|
+
* The always-on write-protection judge re-judges a table-missing spelling on its RESOLVED real target,
|
|
5
|
+
* which needs a filesystem to resolve in. A deployment that wires no execution environment (a tool-only
|
|
6
|
+
* or MCP-only host) hands it none, so the spelling becomes the whole judgment and a covered write that
|
|
7
|
+
* reaches a protected row only through an alias is cleared. That verdict is the judge's documented
|
|
8
|
+
* absence law and is deliberately unchanged here: the announcement dialect applies (announce, do not
|
|
9
|
+
* refuse) — refusing every covered write instead would break exactly the deployments this shape exists
|
|
10
|
+
* for, and the reference implementation falls back to the same spelling-only set when resolution is
|
|
11
|
+
* unavailable. What this seat removes is the SILENCE.
|
|
12
|
+
*
|
|
13
|
+
* Two readers, one compile: the gate station mounts `check`, and the leg's wiring manifest reports
|
|
14
|
+
* `targetView`. Compiling twice would be two judges free to disagree about the seats they were handed.
|
|
15
|
+
*/
|
|
16
|
+
import type { ExecutionEnv } from "../../internal/harness.js";
|
|
17
|
+
import type { RunnerDeps } from "../runner-deps.js";
|
|
18
|
+
import type { ToolRoster } from "../tool-roster.js";
|
|
19
|
+
import { type WriteProtectionMount } from "../write-protect.js";
|
|
20
|
+
/**
|
|
21
|
+
* Compile the leg's write-protection judge and announce a missing target view.
|
|
22
|
+
*
|
|
23
|
+
* Returns the mount, or `undefined` when the deployment emptied the table (`[]`): no judge, no tighten
|
|
24
|
+
* mounted at all, and nothing announced — an explicit opt-out is not a degrade. A garbage table refuses
|
|
25
|
+
* prepare loudly from here, as it did from the gate station before, so an unusable table cannot lie
|
|
26
|
+
* dormant on a write-tool-less task.
|
|
27
|
+
*
|
|
28
|
+
* The notice fires exactly once per prepared leg and only when a judge with NO target view sits beside at
|
|
29
|
+
* least one tool whose writes it covers. Coverage runs the judge's OWN per-call test ({@link
|
|
30
|
+
* protectivePathTargetOf}) rather than a copy of it, over every name a row DISPATCHES under — its own and
|
|
31
|
+
* its aliases, because the gate hands the judge the name the call was made with. A row that declares a
|
|
32
|
+
* path target is covered by that declaration under every name; a row that declares none is covered under
|
|
33
|
+
* whichever of its names the catalog gives a non-read slot to. Both halves are load-bearing: a tool named
|
|
34
|
+
* `Write` that declares nothing, and a tool aliased to `Write` that declares nothing, are each judged by
|
|
35
|
+
* spelling alone on a hands-less leg, and each would otherwise go unnamed by the very line that exists to
|
|
36
|
+
* name them. Reported by the ROW's own name — what the deployment mounted.
|
|
37
|
+
*
|
|
38
|
+
* SNAPSHOT BOUNDARY, deliberate and disclosed: the roster read here is the LEG-START one, the same one
|
|
39
|
+
* the wiring manifest reports and for the same reason — this is one announcement per prepared leg, not a
|
|
40
|
+
* subscription. A run that gains its FIRST covered write tool after prepare (a mid-run roster delta) is
|
|
41
|
+
* judged by the same spelling-only judge and is not announced; the leg's `writeProtection.targetView`
|
|
42
|
+
* still reports the judge's reading correctly, because the judge never gains an environment mid-run —
|
|
43
|
+
* only the covered SET can widen.
|
|
44
|
+
*/
|
|
45
|
+
export declare function mountWriteProtection(input: {
|
|
46
|
+
/** borrowed-readonly — the deployment seats: the table (`writeProtectedPaths`; absent ⇒ the default
|
|
47
|
+
* table, `[]` ⇒ opted out), the data root the memory library is materialized under
|
|
48
|
+
* (`memoryEngineDir`; absent ⇒ the resolver's env/default chain) and the operator sink. */
|
|
49
|
+
deps: Pick<RunnerDeps, "writeProtectedPaths" | "memoryEngineDir" | "onNotice">;
|
|
50
|
+
/** The HANDS' environment, or `undefined` with no hands — the driver's own predicate, not a second
|
|
51
|
+
* reading of the env seats, so the judge sees exactly the environment the write tools do. */
|
|
52
|
+
env: ExecutionEnv | undefined;
|
|
53
|
+
/** The base a `base: "root"` write tool's relative spelling resolves against. */
|
|
54
|
+
rootPath: string;
|
|
55
|
+
/** The leg's minted roster — what actually MOUNTED. */
|
|
56
|
+
roster: ToolRoster;
|
|
57
|
+
sessionId: string;
|
|
58
|
+
runId: string;
|
|
59
|
+
}): WriteProtectionMount | undefined;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
2
|
+
import { protectivePathTargetOf } from "../tool-registry.js";
|
|
3
|
+
import { createWriteProtectionCheck } from "../write-protect.js";
|
|
4
|
+
export function mountWriteProtection(input) {
|
|
5
|
+
const mount = createWriteProtectionCheck(input.deps.writeProtectedPaths, {
|
|
6
|
+
...(input.deps.memoryEngineDir !== undefined ? { dataRoot: input.deps.memoryEngineDir } : {}),
|
|
7
|
+
...(input.env !== undefined ? { env: input.env } : {}),
|
|
8
|
+
rootPath: input.rootPath,
|
|
9
|
+
});
|
|
10
|
+
if (mount === undefined || mount.targetView !== "spelling-only")
|
|
11
|
+
return mount;
|
|
12
|
+
const covered = input.roster.entries
|
|
13
|
+
.filter((e) => [e.name, ...e.aliases].some((name) => {
|
|
14
|
+
const target = protectivePathTargetOf({ toolName: name, args: {}, face: e.pathTarget !== undefined ? { pathTarget: e.pathTarget } : {} });
|
|
15
|
+
return target !== undefined && target.access !== "read";
|
|
16
|
+
}))
|
|
17
|
+
.map((e) => e.name)
|
|
18
|
+
.sort();
|
|
19
|
+
if (covered.length === 0)
|
|
20
|
+
return mount;
|
|
21
|
+
deliverEngineNotice(input.deps.onNotice, {
|
|
22
|
+
code: "config.write_protection_target_view_absent",
|
|
23
|
+
message: `This run mounted path-confinable write tools [${covered.join(", ")}] while its write-protection judge has NO execution environment, ` +
|
|
24
|
+
`so the judge compares path SPELLINGS against its table and nothing else. A write whose spelling misses the table but whose real target ` +
|
|
25
|
+
`resolves onto a protected path — through a symlink, a bind mount or a ".." walk — is cleared, and cleared silently: the resolved-target ` +
|
|
26
|
+
`half of the judgment needs a filesystem to resolve in. Nothing about this run changed. Wire an execution environment (RunnerDeps.executionEnv) ` +
|
|
27
|
+
`to get the target view back, or accept the spelling-only floor knowingly — the leg's wiring manifest reports which one is in force at ` +
|
|
28
|
+
`writeProtection.targetView.`,
|
|
29
|
+
detail: { tools: covered, sessionId: input.sessionId, runId: input.runId },
|
|
30
|
+
});
|
|
31
|
+
return mount;
|
|
32
|
+
}
|
|
@@ -391,6 +391,27 @@ export interface RunnerDeps {
|
|
|
391
391
|
maxCycles?: number;
|
|
392
392
|
mintRepairBudget?: number;
|
|
393
393
|
};
|
|
394
|
+
/**
|
|
395
|
+
* The advisory-to-execution BRIDGE: when `true`, a terminal harvest that crosses
|
|
396
|
+
* the consolidation threshold (the same edge that mints `memory.consolidation_recommended`) fires
|
|
397
|
+
* ONE host consolidation run for the recommended scope at the post-task tail — fire-and-forget,
|
|
398
|
+
* result-first, never on the harvest tail itself. Explicit `false` = OFF (the deployment's own off
|
|
399
|
+
* switch; it beats the default). ABSENT = the named default `AUTO_RUN_ON_RECOMMENDATION_DEFAULT`
|
|
400
|
+
* (`memory-consolidation-driver.ts`, read through `autoRunOnRecommendationEffective` — the ONE
|
|
401
|
+
* resolver), applied ONLY where a run could actually happen (the protocol switch wired AND a model
|
|
402
|
+
* seat resolvable): a default-on value never arms a deployment that wired no consolidation.
|
|
403
|
+
*
|
|
404
|
+
* DEPLOYMENT seat ONLY (no TaskSpec twin), the same law as {@link RunnerDeps.memoryConsolidation} /
|
|
405
|
+
* {@link RunnerDeps.memoryConsolidationDriver}: arming this is an EGRESS decision — memory content crosses
|
|
406
|
+
* to the consolidation model automatically after every recommending task, never a task author's call.
|
|
407
|
+
*
|
|
408
|
+
* `true` with nothing to run (no {@link RunnerDeps.memoryConsolidation}, or no resolvable driver model
|
|
409
|
+
* seat), or a non-boolean, is refused at prepare (`config.auto_consolidation`) — never folded to off.
|
|
410
|
+
*
|
|
411
|
+
* Reported on the effective wiring manifest as `WiringManifest.autoConsolidation`, PRESENT IFF armed
|
|
412
|
+
* (absent and fingerprint-identical when off).
|
|
413
|
+
*/
|
|
414
|
+
autoRunOnRecommendation?: boolean;
|
|
394
415
|
/**
|
|
395
416
|
* design/199 件A — the DEPLOYMENT's read-face declaration
|
|
396
417
|
* ({@link import("../tools/fs/containment-face.js").ContainmentFace}; see {@link TaskSpec.readFace} for the
|
|
@@ -4,6 +4,7 @@ import { canonicalizeTarget, expandHomeTilde, isAbsoluteForFamily, isWinFormPath
|
|
|
4
4
|
import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TABLE } from "../tools/fs/read-deny.js";
|
|
5
5
|
import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
|
|
6
6
|
import { pathTargetBaseOf } from "./effective-path-target.js";
|
|
7
|
+
import { symlinkChainHops } from "./path-identity.js";
|
|
7
8
|
export function agentConfigDenyPatterns() {
|
|
8
9
|
return READ_FACE_BUILTIN_DENY_TABLE.filter((row) => row.tier === "agent-config").map((row) => row.pattern);
|
|
9
10
|
}
|
|
@@ -147,6 +148,18 @@ export function createSensitivePathPolicy(opts) {
|
|
|
147
148
|
decisionReason: "safety",
|
|
148
149
|
};
|
|
149
150
|
}
|
|
151
|
+
if (relative === null) {
|
|
152
|
+
for (const hop of await symlinkChainHops(opts.env, path, signal, pathFamilyOf({ root: opts.rootPath, cwd: req.cwd ?? opts.env.cwd }), pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }))) {
|
|
153
|
+
const viaHop = matchSensitive(hop, compiled, false);
|
|
154
|
+
if (viaHop) {
|
|
155
|
+
return {
|
|
156
|
+
action: "deny",
|
|
157
|
+
message: `write to "${path}" is blocked: its symlink chain passes through the guarded sensitive path "${viaHop}"`,
|
|
158
|
+
decisionReason: "safety",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
150
163
|
return { action: "allow" };
|
|
151
164
|
},
|
|
152
165
|
};
|
|
@@ -24,6 +24,7 @@ import type { RunnerDeps, TaskSpec } from "./types.js";
|
|
|
24
24
|
import { type StoreDurability } from "./checkpoint-store.js";
|
|
25
25
|
import type { WiringManifestHookEntry } from "./hook-seats.js";
|
|
26
26
|
import type { McpDelivered, McpFailureKind } from "./mcp-failure.js";
|
|
27
|
+
import type { WriteProtectionTargetView } from "./write-protect.js";
|
|
27
28
|
export type { StoreDurability };
|
|
28
29
|
/** The manifest-face projection of {@link StoreDurability}: `"declared_durable"` (the name says it —
|
|
29
30
|
* a DECLARATION relayed, not a verification) vs `"process_local"` (declared so, or fail-closed
|
|
@@ -319,6 +320,46 @@ export interface WiringManifest {
|
|
|
319
320
|
mounted: boolean;
|
|
320
321
|
lane?: LspLane;
|
|
321
322
|
};
|
|
323
|
+
/**
|
|
324
|
+
* EFFECTIVE half only, PRESENT IFF this leg compiled a write-protection judge — a deployment that
|
|
325
|
+
* replaced the table with `[]` opted the layer out entirely, and reports nothing here rather than a
|
|
326
|
+
* judge-shaped word about a judge that does not exist (absence = "no judge on this leg", which is a
|
|
327
|
+
* different fact from either value below, and strictly weaker than both).
|
|
328
|
+
*
|
|
329
|
+
* `targetView` is the JUDGE'S OWN word (`WriteProtectionMatcher.targetView`, relayed verbatim), and it
|
|
330
|
+
* is how an operator tells a hands-less deployment's write floor from a hands-ful one without inspecting
|
|
331
|
+
* the engine's seats — the same fact `config.write_protection_target_view_absent` announces once per run
|
|
332
|
+
* when a covered write tool is mounted beside a `"spelling-only"` judge.
|
|
333
|
+
*
|
|
334
|
+
* IN the fingerprint below (unlike `tools`/`lsp`/`hooks`): which view the judge got is an ASSEMBLY fact
|
|
335
|
+
* — it follows the table seat and whether the deployment wired hands at all — so legs of one assembly
|
|
336
|
+
* agree on it, which is exactly the equality the fingerprint advertises.
|
|
337
|
+
*
|
|
338
|
+
* @contract write_protection.target_view_disclosed — WHICH reading of a write's target the leg's
|
|
339
|
+
* write-protection judge holds is DISCLOSED, never inferred: the judge self-reports one of two words at
|
|
340
|
+
* its compile (`"target"` — it holds an execution environment and re-judges a table-missing spelling on
|
|
341
|
+
* its resolved real target; `"spelling-only"` — it holds none, so the spelling is the whole judgment and
|
|
342
|
+
* an alias onto a protected path is cleared), the leg's wiring manifest relays that word verbatim, and a
|
|
343
|
+
* `"spelling-only"` judge with a covered write tool mounted also announces
|
|
344
|
+
* `config.write_protection_target_view_absent` exactly once per run; the disclosure changes no verdict —
|
|
345
|
+
* the absence law it makes readable is the judge's own and stands unaltered.
|
|
346
|
+
*/
|
|
347
|
+
writeProtection?: {
|
|
348
|
+
targetView: WriteProtectionTargetView;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* EFFECTIVE half only. PRESENT IFF auto-consolidation is EFFECTIVELY armed on this
|
|
352
|
+
* deployment (`autoRunOnRecommendationEffective`: an explicit `true`, or an absent seat under the
|
|
353
|
+
* named default `AUTO_RUN_ON_RECOMMENDATION_DEFAULT` where both consolidation seats are wired): a
|
|
354
|
+
* terminal harvest that crosses the threshold auto-runs one host consolidation — memory content
|
|
355
|
+
* auto-egresses to the configured model. ABSENT (and the manifest, including `configFingerprint`,
|
|
356
|
+
* byte-identical) when not armed — an explicit `false`, or a default that does not arm it.
|
|
357
|
+
* IN the fingerprint (an assembly fact, the same class as {@link WiringManifest.writeProtection}); always
|
|
358
|
+
* `{ onRecommendation: true }` when present — the prepare door refuses an armed-but-unrunnable state.
|
|
359
|
+
*/
|
|
360
|
+
autoConsolidation?: {
|
|
361
|
+
onRecommendation: true;
|
|
362
|
+
};
|
|
322
363
|
/** EFFECTIVE half only — a short, non-sensitive fingerprint (sha256 prefix over the canonical
|
|
323
364
|
* JSON of this manifest's own resolved facts; every field here is an enum/boolean/count, no
|
|
324
365
|
* secrets) so an operator can correlate legs that ran under the same resolved assembly. */
|
|
@@ -397,6 +438,12 @@ export interface WiringFacts {
|
|
|
397
438
|
mounted: boolean;
|
|
398
439
|
lane?: LspLane;
|
|
399
440
|
};
|
|
441
|
+
/** Effective half only — the compiled write-protection judge's OWN target-view word
|
|
442
|
+
* ({@link WiringManifest.writeProtection}); absent ⇒ this leg compiled no judge (an emptied table). */
|
|
443
|
+
writeProtectionTargetView?: WriteProtectionTargetView;
|
|
444
|
+
/** Effective half only — `RunnerDeps.autoRunOnRecommendation === true`
|
|
445
|
+
* ({@link WiringManifest.autoConsolidation}); false ⇒ no section (byte-identical manifest). */
|
|
446
|
+
autoConsolidationOnRecommendation?: boolean;
|
|
400
447
|
}
|
|
401
448
|
/** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
|
|
402
449
|
* single-source shapes, no parallel hand-copied interface). */
|
|
@@ -141,6 +141,8 @@ export function deriveWiringManifest(facts) {
|
|
|
141
141
|
...(facts.half === "effective" && facts.tools !== undefined ? { tools: facts.tools } : {}),
|
|
142
142
|
...(facts.half === "effective" && facts.hooks !== undefined && facts.hooks.length > 0 ? { hooks: facts.hooks.map((h) => ({ ...h })) } : {}),
|
|
143
143
|
...(facts.half === "effective" && facts.lsp !== undefined ? { lsp: { ...facts.lsp } } : {}),
|
|
144
|
+
...(facts.half === "effective" && facts.writeProtectionTargetView !== undefined ? { writeProtection: { targetView: facts.writeProtectionTargetView } } : {}),
|
|
145
|
+
...(facts.half === "effective" && facts.autoConsolidationOnRecommendation === true ? { autoConsolidation: { onRecommendation: true } } : {}),
|
|
144
146
|
};
|
|
145
147
|
if (facts.half === "effective") {
|
|
146
148
|
const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, ...assembly } = manifest;
|
|
@@ -51,7 +51,8 @@ export declare function resolveWriteProtectedTable(entries?: readonly WriteProte
|
|
|
51
51
|
* The judge's answer for ONE covered write ({@link WriteProtectionMatcher.judge}):
|
|
52
52
|
* · `clear` — no row on either view: the write is not this layer's business;
|
|
53
53
|
* · `hit` — a row matched; `via` says which view named it: `"spelling"` (the anchored spelling, no
|
|
54
|
-
* filesystem consulted)
|
|
54
|
+
* filesystem consulted), `"chain"` (a spelling the leaf symlink chain passes through, judged as a
|
|
55
|
+
* spelling) or `"target"` (the resolved real target, the spelling being innocent);
|
|
55
56
|
* · `unresolvable` — the real target could not be resolved (an unfollowable symlink, an errored probe, a
|
|
56
57
|
* namespace spelling the tree refuses); `cause` is the canonicalizer's own sentence and the consumer
|
|
57
58
|
* REFUSES, an unjudged target never being vouched.
|
|
@@ -61,7 +62,7 @@ export type WriteProtectionVerdict = {
|
|
|
61
62
|
} | {
|
|
62
63
|
readonly kind: "hit";
|
|
63
64
|
readonly hit: WriteProtectedHit;
|
|
64
|
-
readonly via: "spelling" | "target";
|
|
65
|
+
readonly via: "spelling" | "chain" | "target";
|
|
65
66
|
} | {
|
|
66
67
|
readonly kind: "unresolvable";
|
|
67
68
|
readonly cause: string;
|
|
@@ -78,10 +79,24 @@ export interface WriteProtectionJudgeContext {
|
|
|
78
79
|
/** Bounds the filesystem probes of the target view. */
|
|
79
80
|
readonly signal?: AbortSignal;
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* WHICH reading of a write's TARGET a compiled judge has — its own word, and the vocabulary the wiring
|
|
84
|
+
* manifest discloses it with. `"target"`: the judge holds an execution environment, so a spelling that
|
|
85
|
+
* misses the table is judged a SECOND time on its resolved real target (an alias, a symlink, a `..` walk
|
|
86
|
+
* that lands on a protected row is seen). `"spelling-only"`: no environment, so the spelling IS the whole
|
|
87
|
+
* judgment and a path reaching a row only through an alias is cleared — the module header's absence law,
|
|
88
|
+
* unchanged by this word, which only makes it READABLE. Never derived by a reader from the judge's seats.
|
|
89
|
+
*/
|
|
90
|
+
export declare const WRITE_PROTECTION_TARGET_VIEWS: readonly ["spelling-only", "target"];
|
|
91
|
+
/** One of {@link WRITE_PROTECTION_TARGET_VIEWS}. */
|
|
92
|
+
export type WriteProtectionTargetView = (typeof WRITE_PROTECTION_TARGET_VIEWS)[number];
|
|
81
93
|
/** The compiled judge over one resolved table. `matchPath` is the SPELLING view alone (literal folded
|
|
82
94
|
* comparisons, no filesystem access — the admin preview face); `judge` is the whole judgment (see the
|
|
83
95
|
* module header for the two views). */
|
|
84
96
|
export interface WriteProtectionMatcher {
|
|
97
|
+
/** WHICH reading of a target this judge HAS — minted at compile and read verbatim by everyone who needs
|
|
98
|
+
* the fact; a reader re-deriving it from the seats is a second opinion, free to drift. */
|
|
99
|
+
readonly targetView: WriteProtectionTargetView;
|
|
85
100
|
/** The resolved rows this judge was compiled from (canonical names — the disclosure face). */
|
|
86
101
|
readonly rows: readonly WriteProtectedRow[];
|
|
87
102
|
/** Judge ONE path SPELLING. Returns the first matching row (basename rows first, then segment,
|
|
@@ -147,6 +162,17 @@ export declare function compileWriteProtection(entries?: readonly WriteProtected
|
|
|
147
162
|
* · `clear` ⇒ `undefined`: the surviving decision stands.
|
|
148
163
|
*/
|
|
149
164
|
export declare function writeProtectionTightenDecision(toolName: string, verdict: WriteProtectionVerdict): PermissionResult | undefined;
|
|
165
|
+
/**
|
|
166
|
+
* @contract write_protection.verdict_is_the_mandate — the judge's two non-clear verdicts are the two facts
|
|
167
|
+
* a standing allow rule may not retire, and `clear` is neither: a MATCHED row is the mandate a live decider
|
|
168
|
+
* may still clear, a target it could not RESOLVE is the mandate no automatic lane may. These two predicates
|
|
169
|
+
* are the ONE reading of a verdict as either fact, beside the decision mint, so the fold records what the
|
|
170
|
+
* judge said and never re-derives "was this write protection?" from the decision it got back.
|
|
171
|
+
*/
|
|
172
|
+
export declare function writeProtectionMandated(verdict: WriteProtectionVerdict): boolean;
|
|
173
|
+
/** The second half of {@link writeProtectionMandated}'s contract: the judge could not name this target
|
|
174
|
+
* (the arm that owns the ACTION denies on the same verdict). */
|
|
175
|
+
export declare function writeProtectionUnresolved(verdict: WriteProtectionVerdict): boolean;
|
|
150
176
|
/**
|
|
151
177
|
* The ENGINE-FILLED gate input (`ToolGateInput.writeProtectionCheck`). Returns `null` SYNCHRONOUSLY when the
|
|
152
178
|
* call is not this layer's business — not a covered write, or no resolvable target string — and otherwise
|
|
@@ -154,6 +180,18 @@ export declare function writeProtectionTightenDecision(toolName: string, verdict
|
|
|
154
180
|
* a call the judge does not cover must not cost the fold a suspension point.
|
|
155
181
|
*/
|
|
156
182
|
export type WriteProtectionCheck = (toolName: string, args: unknown, face?: ToolCallLike["face"], cwd?: string, signal?: AbortSignal) => Promise<WriteProtectionVerdict> | null;
|
|
183
|
+
/**
|
|
184
|
+
* ONE leg's write-protection mount: the gate input and the judge's own {@link WriteProtectionTargetView},
|
|
185
|
+
* handed back together so the two readers of this compile — the gate station that mounts the check and the
|
|
186
|
+
* disclosure face that reports which reading it got — read ONE compile. `undefined` in place of the whole
|
|
187
|
+
* record is "no judge" (an emptied table), a different fact from a judge whose target view is absent.
|
|
188
|
+
*/
|
|
189
|
+
export interface WriteProtectionMount {
|
|
190
|
+
/** The gate input (`ToolGateInput.writeProtectionCheck`). */
|
|
191
|
+
readonly check: WriteProtectionCheck;
|
|
192
|
+
/** The compiled judge's own word for the reading it has of a write's target. */
|
|
193
|
+
readonly targetView: WriteProtectionTargetView;
|
|
194
|
+
}
|
|
157
195
|
/**
|
|
158
196
|
* Build the ENGINE-FILLED gate input (`ToolGateInput.writeProtectionCheck`): covered tools are the
|
|
159
197
|
* path-confinable write set (Write/Edit/NotebookEdit — the shared spelling), the judged target is
|
|
@@ -167,4 +205,4 @@ export type WriteProtectionCheck = (toolName: string, args: unknown, face?: Tool
|
|
|
167
205
|
* Returns `undefined` when the resolved table is empty — nothing to judge, mount nothing.
|
|
168
206
|
* `opts` are the judge's own seats, passed straight through ({@link compileWriteProtection}).
|
|
169
207
|
*/
|
|
170
|
-
export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[], opts?: WriteProtectionCompileOptions):
|
|
208
|
+
export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[], opts?: WriteProtectionCompileOptions): WriteProtectionMount | undefined;
|