@sema-agent/core 7.14.0 → 7.15.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 +30 -0
- package/dist/core/ask-origin.d.ts +2 -2
- package/dist/core/checkpoint-store.d.ts +14 -15
- package/dist/core/hooks.d.ts +4 -3
- package/dist/core/runner/contracts.d.ts +2 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +3 -1
- package/dist/core/runner/permission-rule-lanes.js +1 -1
- package/dist/core/runner/prepare-defer-classify.d.ts +1 -1
- package/dist/core/runner/prepare-defer-classify.js +12 -10
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -4
- package/dist/core/runner/prepare-gate-stations.js +2 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +8 -8
- package/dist/core/runner/prepare-hands-readface.js +27 -29
- package/dist/core/runner/prepare-inherited-gate.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.d.ts +5 -6
- package/dist/core/runner/prepare-park-ask.js +3 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +2 -2
- package/dist/core/runner/prepare-policy-chain.js +2 -2
- package/dist/core/runner/prepare-task.js +5 -7
- package/dist/core/runner/tool-defer-gate.d.ts +86 -0
- package/dist/core/runner/tool-defer-gate.js +57 -0
- package/dist/core/runner/tool-disclosure.d.ts +0 -36
- package/dist/core/runner/tool-disclosure.js +0 -43
- package/dist/core/runner-deps.d.ts +16 -9
- package/dist/core/task-spec.d.ts +8 -2
- package/dist/core/tool-spec.d.ts +7 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tools/fs/fs-bash.d.ts +41 -0
- package/dist/tools/fs/fs-bash.js +86 -29
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +5 -1
|
@@ -265,8 +265,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
265
265
|
announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
|
|
266
266
|
const modelGateManifest = toolModelGateManifest(doors.modelGate);
|
|
267
267
|
const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
|
|
268
|
-
let
|
|
269
|
-
let shellGatedMonitor = false;
|
|
268
|
+
let shellGatedTools = new Set();
|
|
270
269
|
refuseRequireExistingWithoutSession(spec);
|
|
271
270
|
const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(internals?.sessionPlacement !== undefined ? { placement: internals.sessionPlacement } : {}), ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
|
|
272
271
|
const sessionId = acquired.sessionId;
|
|
@@ -512,8 +511,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
512
511
|
resolvedReadFace = handsReadFace.resolvedReadFace;
|
|
513
512
|
readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
|
|
514
513
|
handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
|
|
515
|
-
|
|
516
|
-
shellGatedMonitor = handsReadFace.shellGatedMonitor;
|
|
514
|
+
shellGatedTools = handsReadFace.shellGatedTools;
|
|
517
515
|
const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
|
|
518
516
|
const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
|
|
519
517
|
if (spec.handsReadOnly !== true) {
|
|
@@ -683,7 +681,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
683
681
|
releaseSignal = () => sig.removeEventListener("abort", onExternalAbort);
|
|
684
682
|
}
|
|
685
683
|
}
|
|
686
|
-
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, ownToolNames,
|
|
684
|
+
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, ownToolNames, shellGatedTools, handsCwdRef, readDenyMatcher, questionToolMounted, runtimeCaps, inheritedAncestorRules, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
|
|
687
685
|
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 });
|
|
688
686
|
if (fileHistoryBoundary !== undefined) {
|
|
689
687
|
harness.on("tool_call", async () => {
|
|
@@ -694,8 +692,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
694
692
|
const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash });
|
|
695
693
|
const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, inheritedParentConstraints, seedInheritedGate, inheritedAncestorRules, inheritedShellGate, autoModeIntent, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
|
|
696
694
|
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 });
|
|
697
|
-
const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures,
|
|
698
|
-
prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController,
|
|
695
|
+
const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
|
|
696
|
+
prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, planModeRef, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, hookEnvFace, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider, autoModeDenialTracking, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash });
|
|
699
697
|
const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef });
|
|
700
698
|
const preparedHolder = {};
|
|
701
699
|
const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Model } from "../../internal/llm.js";
|
|
2
|
+
import type { ToolSpec } from "../types.js";
|
|
3
|
+
import type { ToolFingerprintInput } from "../cache-break-detector.js";
|
|
4
|
+
/**
|
|
5
|
+
* The deferral GATE (design/36): which mounted tool names ship as placeholders on a request. Pure
|
|
6
|
+
* registry logic over names and sizes — no harness, no I/O. `tool-disclosure.ts` owns the placeholder
|
|
7
|
+
* and ToolSearch mechanics that act on the set this module decides; `prepare-defer-classify.ts` is the
|
|
8
|
+
* phase that assembles the inputs from the mounted face.
|
|
9
|
+
*/
|
|
10
|
+
/** The deferral gate's threshold: the engine's candidate set is deferred when its inlined size reaches
|
|
11
|
+
* this fraction of the model's context window (that set measured, never the rest of the face). */
|
|
12
|
+
export declare const DEFER_THRESHOLD_FRACTION = 0.1;
|
|
13
|
+
/** The typed refusal a `deferMode` outside its declared domain (`"auto"` or absent) raises at the classifier —
|
|
14
|
+
* a bad value never selects an arm silently. */
|
|
15
|
+
export declare const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
|
|
16
|
+
/** The deferral gate's verdict — the two deferral lists, what the gate measured, and whether it opened. */
|
|
17
|
+
export interface DeferralGate {
|
|
18
|
+
/** The caller's LITERAL list: `TaskSpec.deferTools` names mounted on this face, minus the inline pins.
|
|
19
|
+
* Deferred on every request, whatever the gate says — an intentional deferral is not a size question. */
|
|
20
|
+
explicit: Set<string>;
|
|
21
|
+
/** The engine's SELF-SELECTED candidates: `ToolSpec.defer === true`, every remote-protocol name, built-ins
|
|
22
|
+
* that declare their own `defer`, and — under `deferMode: "auto"` — every caller spec; minus the inline
|
|
23
|
+
* pins and minus `explicit`. Deferred iff the gate opens. */
|
|
24
|
+
gated: Set<string>;
|
|
25
|
+
/** Σ inlined chars (name + description + serialized schema) over `gated` — the bytes the gate could reclaim. */
|
|
26
|
+
gatedChars: number;
|
|
27
|
+
/** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. */
|
|
28
|
+
thresholdChars: number;
|
|
29
|
+
/** Whether `gated` is deferred on this request: `gatedChars ≥ thresholdChars`. */
|
|
30
|
+
open: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Deferral has TWO rules, and only two.
|
|
34
|
+
*
|
|
35
|
+
* ① THE CALLER'S LIST IS LITERAL. `TaskSpec.deferTools` (arriving as `callerDeferNames`, pre-filtered to
|
|
36
|
+
* mounted wire names, built-ins included) is deferred on every request; `alwaysLoad` (`ToolSpec.alwaysLoad`,
|
|
37
|
+
* `alwaysLoadNames`) is inlined on every request and is judged FIRST — a pinned name is in neither list
|
|
38
|
+
* and is never measured. Neither depends on the gate: they are the deployment's stated intent.
|
|
39
|
+
*
|
|
40
|
+
* ② THE ENGINE'S OWN CANDIDATES SIT BEHIND ONE THRESHOLD GATE. The candidates are declared: explicit
|
|
41
|
+
* `ToolSpec.defer === true` on a caller spec, every REMOTE-PROTOCOL tool name (MCP servers AND A2A peers —
|
|
42
|
+
* the caller unions them), and `builtinDeferNames` (mounted BUILT-INS that declare their own `defer`; a
|
|
43
|
+
* built-in ToolSpec never reaches `specs`, so its declaration arrives as a mounted name). `deferMode:
|
|
44
|
+
* "auto"` WIDENS the candidate set to every caller spec — undeclared caller tools included — and nothing
|
|
45
|
+
* else: it is not a second gate, and engine built-ins the deployment did not name are candidates under no
|
|
46
|
+
* setting. The gate decides the set all or nothing: deferred iff `gatedChars ≥ thresholdChars`, where the
|
|
47
|
+
* threshold is {@link DEFER_THRESHOLD_FRACTION} of the model's context window (`contextTokens ??
|
|
48
|
+
* contextWindow`, in chars at {@link CHARS_PER_TOKEN}). The measure is the candidate set ONLY — the bytes
|
|
49
|
+
* the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose weight
|
|
50
|
+
* sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A window
|
|
51
|
+
* of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
|
|
52
|
+
*
|
|
53
|
+
* Invariants: `explicit ∩ gated = ∅`; `deferred = explicit ∪ (open ? gated : ∅)`; a pinned name is in no
|
|
54
|
+
* list; the same inputs always yield the same verdict (a pure function of the request).
|
|
55
|
+
*/
|
|
56
|
+
export declare function deferralGate(opts: {
|
|
57
|
+
specs: ReadonlyArray<ToolSpec>;
|
|
58
|
+
/** Every namespaced tool a remote protocol materialized for this task (MCP servers AND A2A peers —
|
|
59
|
+
* the caller unions them). The remote-roster shape (many tools, schemas the deployment does not
|
|
60
|
+
* control, drift between tasks) is the same for every protocol, so every protocol's names are
|
|
61
|
+
* candidates by being remote. Only an inline pin (or the caller's own list) takes a name out. */
|
|
62
|
+
protocolToolNames: ReadonlyArray<string>;
|
|
63
|
+
/** The MOUNTED face — the tools whose full definitions the request would carry. The measure reads
|
|
64
|
+
* each candidate's inlined size from here; a candidate absent from it weighs nothing. */
|
|
65
|
+
fullTools: ReadonlyArray<ToolFingerprintInput>;
|
|
66
|
+
/** `"auto"` widens the candidate set to every caller spec (see above). Any other value refuses. */
|
|
67
|
+
deferMode?: "auto";
|
|
68
|
+
model: Model;
|
|
69
|
+
/** Rule ① — `TaskSpec.deferTools`: ALREADY-MOUNTED wire names (built-ins included; `ToolSpec.defer`
|
|
70
|
+
* only reaches caller specs) the caller defers on every request. Caller pre-filters to mounted names. */
|
|
71
|
+
callerDeferNames?: ReadonlyArray<string>;
|
|
72
|
+
/** Rule ② — mounted BUILT-INS that declare their own `defer` (the engine's declaration, not the
|
|
73
|
+
* caller's), so they are candidates. Caller pre-filters to mounted names. */
|
|
74
|
+
builtinDeferNames?: ReadonlyArray<string>;
|
|
75
|
+
/** Rule ① — inline-pin names (`TaskSpec.alwaysLoadTools` + the MCP tools whose server declared
|
|
76
|
+
* `_meta["anthropic/alwaysLoad"]`, caller-collected; A2A has no such per-skill declaration, so the
|
|
77
|
+
* task-level list is a peer's only inline-keep channel): exempt from BOTH lists.
|
|
78
|
+
* `ToolSpec.alwaysLoad` rides the specs directly and needs no entry here. */
|
|
79
|
+
alwaysLoadNames?: ReadonlyArray<string>;
|
|
80
|
+
}): DeferralGate;
|
|
81
|
+
/**
|
|
82
|
+
* Decide which tool names are deferred: the caller's literal list, plus the gated candidates when the gate
|
|
83
|
+
* opens. THIS CONTRACT (with {@link deferralGate}) IS THE SINGLE AUTHORITY on the deferral sources and the
|
|
84
|
+
* gate; the `TaskSpec` / `RunnerDeps` JSDoc points here.
|
|
85
|
+
*/
|
|
86
|
+
export declare function classifyDeferred(opts: Parameters<typeof deferralGate>[0]): Set<string>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
|
|
2
|
+
export const DEFER_THRESHOLD_FRACTION = 0.1;
|
|
3
|
+
const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
|
|
4
|
+
function inlinedChars(t) {
|
|
5
|
+
let schema = "";
|
|
6
|
+
try {
|
|
7
|
+
schema = JSON.stringify(t.parameters) ?? "";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
schema = "";
|
|
11
|
+
}
|
|
12
|
+
return t.name.length + t.description.length + schema.length;
|
|
13
|
+
}
|
|
14
|
+
export const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
|
|
15
|
+
export function deferralGate(opts) {
|
|
16
|
+
if (opts.deferMode !== undefined && opts.deferMode !== "auto") {
|
|
17
|
+
const e = new Error(`RunnerDeps.deferMode: invalid value ${JSON.stringify(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
|
|
18
|
+
e.code = DEFER_MODE_INVALID_CODE;
|
|
19
|
+
throw e;
|
|
20
|
+
}
|
|
21
|
+
const pinned = new Set(opts.alwaysLoadNames ?? []);
|
|
22
|
+
for (const s of opts.specs) {
|
|
23
|
+
if (s.alwaysLoad === true)
|
|
24
|
+
pinned.add(s.name);
|
|
25
|
+
}
|
|
26
|
+
const explicit = new Set();
|
|
27
|
+
for (const name of opts.callerDeferNames ?? [])
|
|
28
|
+
if (!pinned.has(name))
|
|
29
|
+
explicit.add(name);
|
|
30
|
+
const gated = new Set();
|
|
31
|
+
const select = (name) => {
|
|
32
|
+
if (!pinned.has(name) && !explicit.has(name))
|
|
33
|
+
gated.add(name);
|
|
34
|
+
};
|
|
35
|
+
for (const s of opts.specs)
|
|
36
|
+
if (s.defer === true || opts.deferMode === "auto")
|
|
37
|
+
select(s.name);
|
|
38
|
+
for (const name of opts.protocolToolNames)
|
|
39
|
+
select(name);
|
|
40
|
+
for (const name of opts.builtinDeferNames ?? [])
|
|
41
|
+
select(name);
|
|
42
|
+
let gatedChars = 0;
|
|
43
|
+
for (const t of opts.fullTools)
|
|
44
|
+
if (gated.has(t.name))
|
|
45
|
+
gatedChars += inlinedChars(t);
|
|
46
|
+
const windowTokens = Math.max(0, opts.model.contextTokens ?? opts.model.contextWindow ?? 0);
|
|
47
|
+
const thresholdChars = Math.floor(windowTokens * DEFER_THRESHOLD_FRACTION) * CHARS_PER_TOKEN;
|
|
48
|
+
return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars };
|
|
49
|
+
}
|
|
50
|
+
export function classifyDeferred(opts) {
|
|
51
|
+
const gate = deferralGate(opts);
|
|
52
|
+
const deferred = new Set(gate.explicit);
|
|
53
|
+
if (gate.open)
|
|
54
|
+
for (const n of gate.gated)
|
|
55
|
+
deferred.add(n);
|
|
56
|
+
return deferred;
|
|
57
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { type TSchema } from "typebox";
|
|
2
2
|
import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
|
|
3
|
-
import type { Model } from "../../internal/llm.js";
|
|
4
|
-
import type { ToolSpec } from "../types.js";
|
|
5
|
-
import type { ToolFingerprintInput } from "../cache-break-detector.js";
|
|
6
3
|
/**
|
|
7
4
|
* Deferred-tool dynamic disclosure (design/36). A deployment with hundreds of tools / several MCP
|
|
8
5
|
* servers can't inline every full JSON Schema into every request — turn-1 cache-creation tokens
|
|
@@ -56,39 +53,6 @@ export declare function deferHint(description: string, max?: number): string;
|
|
|
56
53
|
* announcement, anywhere. A bare interpolation is a prompt-injection hole (council BUG, design/36).
|
|
57
54
|
*/
|
|
58
55
|
export declare function safeName(name: string): string;
|
|
59
|
-
/**
|
|
60
|
-
* Decide which tool names are deferred. THIS CONTRACT IS THE SINGLE AUTHORITY on the default-mode
|
|
61
|
-
* source list (TaskSpec/RunnerDeps JSDoc points here), so every arm the body implements is named:
|
|
62
|
-
* deterministic by default = explicit `ToolSpec.defer === true`, plus every REMOTE-PROTOCOL tool name
|
|
63
|
-
* (MCP servers AND A2A peers — most numerous / most drift-prone), plus `deferNames`, i.e. the
|
|
64
|
-
* already-mounted wire names from `TaskSpec.deferTools` and from BUILT-INS that declare their own
|
|
65
|
-
* `defer` (a built-in ToolSpec never reaches `specs`, so its declared deferral has to arrive here as a
|
|
66
|
-
* mounted name — this is why the shared-memory pair defaults to deferred). With `deferMode: "auto"`, ADDITIONALLY
|
|
67
|
-
* defer the remaining user tools when their combined inlined schema would exceed ~10% of the model's
|
|
68
|
-
* context window — an explicit opt-in (council minor #6), because an implicit threshold makes the same
|
|
69
|
-
* TaskSpec yield different tools[] across models/contexts. Explicit `defer` always wins — except
|
|
70
|
-
* against an alwaysLoad pin (RB-400-a), which exempts a name from every deferral source.
|
|
71
|
-
*/
|
|
72
|
-
export declare function classifyDeferred(opts: {
|
|
73
|
-
specs: ReadonlyArray<ToolSpec>;
|
|
74
|
-
/** Every namespaced tool a remote protocol materialized for this task (MCP servers AND A2A peers —
|
|
75
|
-
* the caller unions them). The constant-defer arm is about the SHAPE of a remote roster (many tools,
|
|
76
|
-
* schemas the deployment does not control, drift between tasks), which is the same for every protocol
|
|
77
|
-
* in the table: naming one protocol here left the next one inlined by omission, with nobody deciding
|
|
78
|
-
* that. Only an alwaysLoad pin exempts a name. */
|
|
79
|
-
protocolToolNames: ReadonlyArray<string>;
|
|
80
|
-
fullTools: ReadonlyArray<ToolFingerprintInput>;
|
|
81
|
-
deferMode?: "auto";
|
|
82
|
-
model: Model;
|
|
83
|
-
/** TaskSpec.deferTools — defer ALREADY-MOUNTED tools by wire name (built-ins included;
|
|
84
|
-
* `ToolSpec.defer` only reaches caller specs). Caller pre-filters to mounted names. */
|
|
85
|
-
deferNames?: ReadonlyArray<string>;
|
|
86
|
-
/** RB-400-a — inline-pin names (`TaskSpec.alwaysLoadTools` + the MCP tools whose server declared
|
|
87
|
-
* `_meta["anthropic/alwaysLoad"]`, caller-collected; A2A has no such per-skill declaration, so the
|
|
88
|
-
* task-level list is a peer's only inline-keep channel): exempt from EVERY deferral source below.
|
|
89
|
-
* `ToolSpec.alwaysLoad` rides the specs directly and needs no entry here. */
|
|
90
|
-
alwaysLoadNames?: ReadonlyArray<string>;
|
|
91
|
-
}): Set<string>;
|
|
92
56
|
/** Build the registry the search ranks over (deferred tools only — non-deferred are already visible). */
|
|
93
57
|
export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, tools: ReadonlyArray<{
|
|
94
58
|
name: string;
|
|
@@ -2,11 +2,8 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { defineTool, errorResult } from "../tools.js";
|
|
4
4
|
import { formatZodValidationError, truncateError } from "../tool-errors.js";
|
|
5
|
-
import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
|
|
6
5
|
import { toolFace } from "../tool-catalog-entries.js";
|
|
7
6
|
export const TOOL_SEARCH_NAME = "ToolSearch";
|
|
8
|
-
const DEFER_AUTO_FRACTION = 0.1;
|
|
9
|
-
const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
|
|
10
7
|
export const TOOL_SEARCH_DEFAULT_MAX_RESULTS = 5;
|
|
11
8
|
const MAX_QUERY_RESULTS = 25;
|
|
12
9
|
export const DEFERRED_NO_PROGRESS_LIMIT = 3;
|
|
@@ -19,46 +16,6 @@ export function deferHint(description, max = 120) {
|
|
|
19
16
|
export function safeName(name) {
|
|
20
17
|
return name.replace(/[\x00-\x1f<>"`]/g, "");
|
|
21
18
|
}
|
|
22
|
-
function inlinedChars(t) {
|
|
23
|
-
let schema = "";
|
|
24
|
-
try {
|
|
25
|
-
schema = JSON.stringify(t.parameters) ?? "";
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
schema = "";
|
|
29
|
-
}
|
|
30
|
-
return t.description.length + schema.length;
|
|
31
|
-
}
|
|
32
|
-
export function classifyDeferred(opts) {
|
|
33
|
-
const pinned = new Set(opts.alwaysLoadNames ?? []);
|
|
34
|
-
for (const s of opts.specs) {
|
|
35
|
-
if (s.alwaysLoad === true)
|
|
36
|
-
pinned.add(s.name);
|
|
37
|
-
}
|
|
38
|
-
const deferred = new Set();
|
|
39
|
-
for (const s of opts.specs) {
|
|
40
|
-
if (s.defer === true && !pinned.has(s.name))
|
|
41
|
-
deferred.add(s.name);
|
|
42
|
-
}
|
|
43
|
-
for (const name of opts.protocolToolNames)
|
|
44
|
-
if (!pinned.has(name))
|
|
45
|
-
deferred.add(name);
|
|
46
|
-
for (const name of opts.deferNames ?? [])
|
|
47
|
-
if (!pinned.has(name))
|
|
48
|
-
deferred.add(name);
|
|
49
|
-
if (opts.deferMode === "auto") {
|
|
50
|
-
const callerNames = new Set(opts.specs.map((s) => s.name));
|
|
51
|
-
const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name) && callerNames.has(t.name));
|
|
52
|
-
const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
|
|
53
|
-
const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
|
|
54
|
-
const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
|
|
55
|
-
if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
|
|
56
|
-
for (const t of candidates)
|
|
57
|
-
deferred.add(t.name);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return deferred;
|
|
61
|
-
}
|
|
62
19
|
export function buildDeferredRegistry(deferred, tools) {
|
|
63
20
|
const reg = new Map();
|
|
64
21
|
for (const t of tools) {
|
|
@@ -1370,15 +1370,22 @@ export interface RunnerDeps {
|
|
|
1370
1370
|
removedBytes: number;
|
|
1371
1371
|
}) => void;
|
|
1372
1372
|
/**
|
|
1373
|
-
* Deferred-tool disclosure policy (design/36).
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1373
|
+
* Deferred-tool disclosure policy (design/36). Two rules govern deferral. ① The caller's list is
|
|
1374
|
+
* literal: {@link TaskSpec.deferTools} names defer on every request, `ToolSpec.alwaysLoad` /
|
|
1375
|
+
* {@link TaskSpec.alwaysLoadTools} pins inline on every request (judged first) — neither reads this
|
|
1376
|
+
* knob. ② The engine's own candidates — `ToolSpec.defer === true`, every REMOTE-PROTOCOL tool name
|
|
1377
|
+
* (MCP **and** A2A peer tools), and built-in tools that declare their own `defer` — sit behind ONE
|
|
1378
|
+
* threshold gate, all or nothing: deferred iff their inlined size (name + description + schema; the
|
|
1379
|
+
* candidate set only — the caller's list, pins and non-candidates are not measured) reaches 10% of the
|
|
1380
|
+
* model's context window; below that they ship inline in full and no ToolSearch is mounted.
|
|
1381
|
+
*
|
|
1382
|
+
* `"auto"` is a CANDIDATE-WIDENING knob, not a second gate: it adds every caller spec — undeclared
|
|
1383
|
+
* caller tools included — to the candidate set, measured and decided by the same threshold. Engine
|
|
1384
|
+
* built-ins the deployment did not name are candidates under no setting. An explicit opt-in (council
|
|
1385
|
+
* minor #6) because it makes `tools[]` depend on the caller roster's total weight, which a deployment
|
|
1386
|
+
* with many thin tools may not want. Any other value refuses at prepare (`config.defer_mode_invalid`).
|
|
1387
|
+
* `classifyDeferred` / `deferralGate` in `src/core/runner/tool-disclosure.ts` are the single authority
|
|
1388
|
+
* if this text drifts.
|
|
1382
1389
|
*/
|
|
1383
1390
|
deferMode?: "auto";
|
|
1384
1391
|
/**
|
package/dist/core/task-spec.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export type ImageInput = {
|
|
|
31
31
|
url: string;
|
|
32
32
|
};
|
|
33
33
|
/** A single self-contained task with its full configuration. */
|
|
34
|
+
/** The shell-gate doctrine vocabulary — {@link TaskSpec.shellGate}'s value set, and the word the engine persists on a
|
|
35
|
+
* shell-gated ask (`RiskDescriptor.shellGateDoctrine`) and folds across a delegation chain (max-rank: `off` <
|
|
36
|
+
* `classify` < `always`). One closed set; every seat that names a doctrine spells it through this alias. */
|
|
37
|
+
export type ShellGateDoctrine = "off" | "classify" | "always";
|
|
34
38
|
export interface TaskSpec {
|
|
35
39
|
taskId?: string;
|
|
36
40
|
/** The instruction / latest user message for this turn. */
|
|
@@ -400,6 +404,8 @@ export interface TaskSpec {
|
|
|
400
404
|
* included — `ToolSpec.defer` only covers caller specs): wire names listed here ship as
|
|
401
405
|
* lightweight placeholders (name + one-line hint, schema bytes OFF the cache prefix) and are
|
|
402
406
|
* activated on demand via the injected ToolSearch (design/36 materialization — prefix untouched).
|
|
407
|
+
* This list is LITERAL — an intentional deferral, deferred on every request regardless of the
|
|
408
|
+
* deferral gate ({@link RunnerDeps.deferMode}) and not counted toward its threshold.
|
|
403
409
|
* "Default-on but not exposed" = the deployment lists the tool here on requests where no
|
|
404
410
|
* activation source fired (e.g. Workflow unless the user said "workflow"/"ultracode"); omitting
|
|
405
411
|
* the name on a later request mounts it fully (activation = not deferring). Unknown names are
|
|
@@ -445,7 +451,7 @@ export interface TaskSpec {
|
|
|
445
451
|
* direction; CC 220 `alwaysLoad` 对位): names listed here are NEVER deferred — not by
|
|
446
452
|
* `ToolSpec.defer`, not by the MCP constant-defer arm (besides the server's own
|
|
447
453
|
* `_meta["anthropic/alwaysLoad"]` declaration this list is the only channel that keeps a chosen
|
|
448
|
-
* MCP tool's full schema inlined), not by `deferTools`, not by `deferMode
|
|
454
|
+
* MCP tool's full schema inlined), not by `deferTools`, not by the deferral gate under any `deferMode`. Unknown names
|
|
449
455
|
* are ignored (the valve only subtracts). Names also in `excludeTools` stay excluded (exclusion
|
|
450
456
|
* wins — an unmounted tool has no schema to keep inline).
|
|
451
457
|
*/
|
|
@@ -862,7 +868,7 @@ export interface TaskSpec {
|
|
|
862
868
|
* task config — a resume that omits it leaves the resumed run's SUBSEQUENT `bash` calls ungated (the approved
|
|
863
869
|
* pending call itself runs once, having been adjudicated). Re-pass the same value the original task used.
|
|
864
870
|
*/
|
|
865
|
-
shellGate?:
|
|
871
|
+
shellGate?: ShellGateDoctrine;
|
|
866
872
|
/**
|
|
867
873
|
* The caller's AUTO-MODE INTENT for this task — the "user turned auto on" half of the classifier
|
|
868
874
|
* arming (CC 2.1.250 polarity: auto mode is something the USER enables at the permission-mode
|
package/dist/core/tool-spec.d.ts
CHANGED
|
@@ -363,9 +363,12 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
|
363
363
|
* for this tool — it is not a "threshold so low that everything offloads". */
|
|
364
364
|
offloadThresholdChars?: number;
|
|
365
365
|
/**
|
|
366
|
-
* Deferred disclosure (design/36): when `true`, this tool is
|
|
367
|
-
*
|
|
368
|
-
*
|
|
366
|
+
* Deferred disclosure (design/36): when `true`, this tool is a CANDIDATE of the engine's deferral gate.
|
|
367
|
+
* When the gate opens (the candidate set's inlined size reaching 10% of the model's context window; see
|
|
368
|
+
* {@link RunnerDeps.deferMode}) it is NOT inlined with its full JSON Schema: it appears as a lightweight
|
|
369
|
+
* placeholder (`{name, one-line hint, empty params}`) so the model knows it exists; when the gate stays
|
|
370
|
+
* shut it ships inline in full. (A caller that wants a tool deferred on every request names it in
|
|
371
|
+
* {@link TaskSpec.deferTools} — that list is literal.) Activation is a DISCLOSURE fact (has the model seen the schema?), not an
|
|
369
372
|
* execution precondition: the model activates the tool via the injected ToolSearch, or by making a call
|
|
370
373
|
* whose arguments already match the real parameters (see {@link TaskSpec.deferSelfResolve}, on by
|
|
371
374
|
* default). Where the schema then lands depends on {@link TaskSpec.toolMaterializeStrategy} — the next
|
|
@@ -386,7 +389,7 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
|
386
389
|
* `alwaysLoad`, declared per MCP tool via `_meta["anthropic/alwaysLoad"]`, whose `isDeferredTool`
|
|
387
390
|
* checks it before every deferral arm): `true` pins this tool's full schema inline in every
|
|
388
391
|
* request — it is never deferred, regardless of its own {@link defer}, {@link TaskSpec.deferTools},
|
|
389
|
-
* or
|
|
392
|
+
* or the deferral gate. The deferral sources only ever ADD; this is the explicit subtract valve.
|
|
390
393
|
* Exclusion still wins ({@link TaskSpec.excludeTools} unmounts — nothing left to keep inline).
|
|
391
394
|
*/
|
|
392
395
|
alwaysLoad?: boolean;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import type { AgentTool, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict } from "../internal/harness.js";
|
|
13
13
|
import type { Model, StreamFn } from "../internal/llm.js";
|
|
14
14
|
export type { ModelRef, ModelRole, RoleSpec, ModelRoles, Brain } from "./model-seat.js";
|
|
15
|
-
export type { StaleToolResultOffloadOptions, ImageInput, TaskSpec } from "./task-spec.js";
|
|
15
|
+
export type { StaleToolResultOffloadOptions, ImageInput, TaskSpec, ShellGateDoctrine } from "./task-spec.js";
|
|
16
16
|
export type { ToolEffect, ToolContentOrigin, ReversibilityVerdict, ToolSpec, ToolReturn, NestedUsage, NestedUsageAccum, ToolExecuteContext } from "./tool-spec.js";
|
|
17
17
|
export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook, FileEditedNotice, FileEditedHook, HandsBandOptions } from "./hands-band.js";
|
|
18
18
|
export type { AgentDefinition, SkillManifest, SkillSpec } from "./agent-definition.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core
|
|
|
93
93
|
export { mintReminderMark, isValidReminderMark, openSystemReminder, mintSystemReminder, reminderMarkDeclaration } from "./core/reminder-mint.js";
|
|
94
94
|
export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
|
|
95
95
|
export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, CheckResult, } from "./core/property-harness.js";
|
|
96
|
-
export { bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
|
|
96
|
+
export { bashReversibilityProbe, bashReadBoundaryProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
|
|
97
97
|
export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, type BashReadonlyRootBoundary, type CompoundReadonlyVerdict, } from "./tools/fs/index.js";
|
|
98
98
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
99
99
|
export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, type ReadDenyEntry, type ReadDenyMatcher, type NormalizedReadDenyEntry, type ReadDenyBuiltinTier, type ReadDenyBuiltinRow, type ReadDenyBuiltinConfig, } from "./tools/fs/index.js";
|
|
@@ -318,7 +318,7 @@ export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } f
|
|
|
318
318
|
export { type BrainTimeoutConfig } from "./brain/timeout.js";
|
|
319
319
|
export { createAssistantMessageEventStream } from "./internal/llm.js";
|
|
320
320
|
export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
321
|
-
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, FileEditedHook, FileEditedNotice, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, TerminalCause, PausedCause, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolInputVerdict, ToolInputValidationContext, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
|
|
321
|
+
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, FileEditedHook, FileEditedNotice, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, TerminalCause, PausedCause, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, ShellGateDoctrine, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolInputVerdict, ToolInputValidationContext, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
|
|
322
322
|
export { Type } from "typebox";
|
|
323
323
|
export type { TSchema, Static } from "typebox";
|
|
324
324
|
export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
|
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ export { runExecGate } from "./core/exec-gate.js";
|
|
|
72
72
|
export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core/untrusted-text.js";
|
|
73
73
|
export { mintReminderMark, isValidReminderMark, openSystemReminder, mintSystemReminder, reminderMarkDeclaration } from "./core/reminder-mint.js";
|
|
74
74
|
export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
|
|
75
|
-
export { bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
|
|
75
|
+
export { bashReversibilityProbe, bashReadBoundaryProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
|
|
76
76
|
export { classifyCompoundReadonlyDetailed, formatOutOfRootReadApprovalOption, } from "./tools/fs/index.js";
|
|
77
77
|
export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
|
|
78
78
|
export { READ_FACE_DEFAULT_DENY_ENTRIES, READ_FACE_BUILTIN_DENY_TABLE, READ_DENY_BUILTIN_TIERS, READ_DENY_DEFAULT_TIERS, resolveReadDenyBuiltins, compileReadDeny, } from "./tools/fs/index.js";
|
|
@@ -56,6 +56,47 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
|
|
|
56
56
|
* that adds nothing the reader cannot already see.
|
|
57
57
|
*/
|
|
58
58
|
export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
|
|
59
|
+
/**
|
|
60
|
+
* The READ-BOUNDARY seat of the full shell: a `ToolSpec.reversibilityProbe` that answers exactly ONE
|
|
61
|
+
* question — does the deployment's read boundary object to where this command reads? — and nothing about
|
|
62
|
+
* the command's shape. It is the shell face of the rule every read face obeys: the structured readers
|
|
63
|
+
* (Read/Grep/Glob) refuse a deny-listed or out-of-root target; the shell reader gets a MANDATED ask for the
|
|
64
|
+
* same target. The engine installs it on Bash and Monitor whenever a real writable shell is mounted and the
|
|
65
|
+
* shellGate doctrine adds no residual-risk classifier of its own (`shellGate:"off"`, the default); under
|
|
66
|
+
* `"classify"` the doctrine installs {@link bashReversibilityProbe} instead, which composes THIS judgment
|
|
67
|
+
* with the read-only shape classifier, and under `"always"` the tier itself is the mandate and no probe runs.
|
|
68
|
+
* The boundary is therefore judged under every doctrine; the doctrine decides only what happens to the
|
|
69
|
+
* residual shell risk.
|
|
70
|
+
*
|
|
71
|
+
* The verdict, in the probe vocabulary the gate reads (`reversible` here means "the boundary raised no
|
|
72
|
+
* question — the seat vouches the call may run"):
|
|
73
|
+
* · the boundary DECLARED an objection — a deny-listed operand, a recursive walk under the deny judge, an
|
|
74
|
+
* operand outside the roots — ⇒ `{ reversible: false, mandated: true }` (+ the recursive-reach `cause`);
|
|
75
|
+
* · the boundary could NOT READ where the command reads — an unexpanded glob operand (the compound face
|
|
76
|
+
* reports it as undecided), a `~`-rooted operand with no home declared, a working-directory move the walk
|
|
77
|
+
* cannot model — ⇒ `{ reversible: false }` (fail-closed; a plain ask, which a stored allow rule may retire —
|
|
78
|
+
* the seat did not declare anything, it declined to guess);
|
|
79
|
+
* · otherwise ⇒ `{ reversible: true }`. This includes every command whose SHAPE the read faces refuse whole
|
|
80
|
+
* (an unlisted program, a redirection, a substitution, a segment that is not a reader): those shapes carry
|
|
81
|
+
* no read-face verdict, so this seat has no question to ask — the residual is the doctrine's, and under
|
|
82
|
+
* `"off"` the doctrine asks nothing. `run_in_background` is likewise not this seat's question (it changes
|
|
83
|
+
* WHEN the command runs, never WHERE it reads).
|
|
84
|
+
*
|
|
85
|
+
* Reach = the read faces' reach, and nothing wider: the compound face walks the operands of a command whose
|
|
86
|
+
* every segment is a listed reader, and the bounded poll-loop face walks a loop body of listed readers. A
|
|
87
|
+
* read hidden in a shape the faces refuse whole (`cat x | <unlisted>`, `$(cat x)`, `cat $DIR/x`, a launcher
|
|
88
|
+
* `env cat x`, a path-prefixed `/bin/cat x`) is not seen here — deny-listed or out-of-root alike — exactly
|
|
89
|
+
* the shapes the `"classify"` doctrine exists to ask about. The text-level containment gate the classify
|
|
90
|
+
* seat consults on its demotion arms is deliberately not consulted here ({@link boundaryDeclared}: it may
|
|
91
|
+
* add a mandate to an ask, never originate one). The poll-loop face reports its refusal as one sentence,
|
|
92
|
+
* so the boundary's share of it is read by removing the boundary's judging seats
|
|
93
|
+
* ({@link pollLoopBoundaryVerdict}).
|
|
94
|
+
*
|
|
95
|
+
* The reader set is the engine's classify set ({@link BASH_CLASSIFY_DEFAULT_ALLOW}) — here it is NOT an
|
|
96
|
+
* auto-allow list but the set of programs whose operands the faces read; a wider set means more reads are
|
|
97
|
+
* judged, never that more runs are vouched.
|
|
98
|
+
*/
|
|
99
|
+
export declare function bashReadBoundaryProbe(boundary: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
|
|
59
100
|
/**
|
|
60
101
|
* design/199 D-6 — the FULL shell's contract id, single-sourced: both shell faces share the wire
|
|
61
102
|
* name "Bash", so this id is the ONE structural discriminator between the write-capable shell and
|