@sema-agent/core 5.53.0 → 5.55.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 +150 -0
- package/dist/agents/cumulative-stats.d.ts +26 -0
- package/dist/agents/cumulative-stats.js +56 -0
- package/dist/agents/observer.d.ts +11 -7
- package/dist/agents/observer.js +2 -4
- package/dist/agents/subagent.d.ts +4 -0
- package/dist/agents/subagent.js +1 -1
- package/dist/agents/verify.d.ts +27 -3
- package/dist/agents/verify.js +7 -2
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp-diagnostics.d.ts +19 -17
- package/dist/core/lsp-diagnostics.js +11 -5
- package/dist/core/mcp.d.ts +214 -5
- package/dist/core/mcp.js +315 -30
- package/dist/core/memory-engine/consolidation.d.ts +378 -0
- package/dist/core/memory-engine/consolidation.js +342 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +237 -4
- package/dist/core/memory-engine/engine.js +1111 -4
- package/dist/core/memory-engine/export-bundle.js +9 -0
- package/dist/core/memory-engine/file-backend.js +27 -1
- package/dist/core/memory-engine/frontmatter.d.ts +20 -1
- package/dist/core/memory-engine/frontmatter.js +111 -0
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +3 -1
- package/dist/core/memory-engine/memory-backend-contract.js +131 -0
- package/dist/core/memory-engine/sync-client.js +26 -0
- package/dist/core/memory-engine/tools.d.ts +9 -0
- package/dist/core/memory-engine/tools.js +57 -13
- package/dist/core/memory-engine/types.d.ts +99 -0
- package/dist/core/memory-recall.js +4 -3
- package/dist/core/memory.d.ts +33 -3
- package/dist/core/memory.js +6 -4
- package/dist/core/permission-rule-model.d.ts +140 -21
- package/dist/core/permission-rule-model.js +76 -17
- package/dist/core/permission-rule-org.d.ts +4 -3
- package/dist/core/permission-rule-org.js +12 -3
- package/dist/core/permission-rules.d.ts +22 -0
- package/dist/core/permission-rules.js +60 -6
- package/dist/core/reminder-disclosure.d.ts +29 -4
- package/dist/core/reminder-disclosure.js +60 -12
- package/dist/core/runner/prepare-memory.js +7 -2
- package/dist/core/runner/prepare-task.d.ts +31 -1
- package/dist/core/runner/prepare-task.js +31 -14
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +12 -10
- package/dist/core/runner/session-rule-policy.js +5 -3
- package/dist/core/runner/synthetic-tools.js +4 -2
- package/dist/core/runner/turn-attachments.d.ts +16 -6
- package/dist/core/runner/turn-attachments.js +34 -20
- package/dist/core/tool-policy.d.ts +18 -0
- package/dist/core/tool-policy.js +19 -8
- package/dist/core/types.d.ts +89 -6
- package/dist/core/untrusted-egress.js +12 -2
- package/dist/core/untrusted-text.d.ts +189 -3
- package/dist/core/untrusted-text.js +416 -6
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/workflow-types.d.ts +16 -6
- package/dist/orchestration/workflow-types.js +10 -4
- package/dist/orchestration/workflow.js +32 -6
- package/dist/prompts/default.d.ts +12 -2
- package/dist/stores/file/background-agent-store.js +1 -0
- package/dist/stores/file/checkpoint-store.d.ts +6 -2
- package/dist/stores/file/checkpoint-store.js +1 -0
- package/dist/stores/file/fs-atomic.d.ts +151 -10
- package/dist/stores/file/fs-atomic.js +208 -32
- package/dist/stores/file/index.d.ts +26 -3
- package/dist/stores/file/index.js +25 -2
- package/dist/stores/file/shared-ledger.d.ts +40 -5
- package/dist/stores/file/shared-ledger.js +24 -8
- package/dist/stores/file/workflow-run-store.d.ts +8 -1
- package/dist/stores/file/workflow-run-store.js +1 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +71 -0
- package/dist/tools/fs/bash-readonly-classifier.js +58 -47
- package/dist/tools/fs/index.d.ts +3 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +30 -1
|
@@ -483,13 +483,22 @@ export interface Prepared {
|
|
|
483
483
|
ownedEnv?: ExecutionEnv;
|
|
484
484
|
/** design/45: a mutable holder the durable-suspend gate writes when a policy `ask` was checkpointed
|
|
485
485
|
* (capture + abort). The run loop reads it to assemble `status:"suspended"`. Empty unless a suspension
|
|
486
|
-
* fired this run.
|
|
486
|
+
* fired this run.
|
|
487
|
+
*
|
|
488
|
+
* `gatedCallId` is the id of the tool call the committed gate is holding — read straight off the
|
|
489
|
+
* committed checkpoint's `tool_approval` pendingAction, so the id a contaminated sibling frame names
|
|
490
|
+
* and the id the checkpoint parks on are the SAME value by construction, not by convention. ABSENT
|
|
491
|
+
* (never guessed) for a park that binds no tool call at all — a `resource_limit` slice and a
|
|
492
|
+
* `plan_review` pause both have pendingActions with no tool call, so there is no causal id to name.
|
|
493
|
+
* Written unconditionally by the commit-side publisher precisely so a later park cannot inherit an
|
|
494
|
+
* earlier one's id. */
|
|
487
495
|
suspendRef: {
|
|
488
496
|
token?: CheckpointToken;
|
|
489
497
|
checkpointId?: string;
|
|
490
498
|
gate?: CheckpointGate;
|
|
491
499
|
scope?: string;
|
|
492
500
|
restoreMode?: "snapshot" | "park_only";
|
|
501
|
+
gatedCallId?: string;
|
|
493
502
|
};
|
|
494
503
|
/** Ruled 2026-08-05 (matrix ruling arm A): set true by the resume engine when this leg EXECUTES the
|
|
495
504
|
* approved pending call — the restart-loop cap then counts from a fresh base (consecutive
|
|
@@ -511,6 +520,7 @@ export interface Prepared {
|
|
|
511
520
|
gate?: CheckpointGate;
|
|
512
521
|
scope?: string;
|
|
513
522
|
restoreMode?: "snapshot" | "park_only";
|
|
523
|
+
gatedCallId?: string;
|
|
514
524
|
};
|
|
515
525
|
/** RB-439-a: the remote-workspace lifecycle failures this run hit, appended in call order and echoed
|
|
516
526
|
* verbatim on `TaskResult.remoteEnvFailures`. A shared array (not a per-call return) because a suspend
|
|
@@ -859,6 +869,26 @@ export interface Prepared {
|
|
|
859
869
|
droppedMessages: boolean;
|
|
860
870
|
};
|
|
861
871
|
}
|
|
872
|
+
/**
|
|
873
|
+
* WHICH tool call a committed durable park is holding this run — `undefined` when nothing parked, or
|
|
874
|
+
* when the park that did commit holds no call (a resource slice, a plan review).
|
|
875
|
+
*
|
|
876
|
+
* SINGLE derivation on purpose. Two consumers need this answer: the abort-classification seam, which
|
|
877
|
+
* stamps it into the `details` of the results the loop mints for the contaminated siblings, and the
|
|
878
|
+
* `tool_end` projection, which puts it on the wire. Reading it from one function keeps the two from
|
|
879
|
+
* disagreeing about WHICH HOLDER WINS or about what a holder with no call means. They are not otherwise
|
|
880
|
+
* interchangeable: the frame face applies strictly narrower conditions on top of this answer (see
|
|
881
|
+
* `tool_end.gatedCallId`), so a frame may omit an id this function returns — never the reverse.
|
|
882
|
+
*
|
|
883
|
+
* The frame side must not read the id back out of a tool RESULT even though the marker is there: a
|
|
884
|
+
* result's `details` is written by the tool (and replaceable by post-tool hooks), so lifting a
|
|
885
|
+
* cross-call attribution from it would let any failing tool name an arbitrary call and put a phantom
|
|
886
|
+
* approval wait on someone else's frame. Same rule, same reason, as `settledBy`.
|
|
887
|
+
*
|
|
888
|
+
* The two holders are never both set (the commit-side discriminant writes exactly one); they are read
|
|
889
|
+
* here in assemble-result's slot order so the winner is the same one the terminal status is built from.
|
|
890
|
+
*/
|
|
891
|
+
export declare function gatedCallIdOf(p: Pick<Prepared, "suspendRef" | "reviewRef">): string | undefined;
|
|
862
892
|
/**
|
|
863
893
|
* design/45 resume inputs threaded into {@link prepareTask} to continue a suspended task. The Runner
|
|
864
894
|
* builds it from the persisted {@link Checkpoint}: rewind the branch to the suspension leaf, skip the
|
|
@@ -39,6 +39,7 @@ import { STALL_CONNECT_MS, STALL_FIRST_TOKEN_MS, STALL_IDLE_MS, withBrainCallGua
|
|
|
39
39
|
import { defineTool, isDefineToolProduct } from "../tools.js";
|
|
40
40
|
import { RETIRED_TOOL_NAMES } from "../tool-name-aliases.js";
|
|
41
41
|
import { protocolOf } from "../protocol-table.js";
|
|
42
|
+
import { isMcpCoveringRuleName, mcpRuleNameCovers } from "../permission-rules.js";
|
|
42
43
|
import { pathToUri } from "../lsp-protocol.js";
|
|
43
44
|
import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOffloadPolicy, InMemoryToolResultStore, RunnerSharedToolResultStore, ScopedToolResultStore, isVolatileOffloadStore, OFFLOAD_TOOL_NAME, createReadToolResultTool, withToolResultOffload, } from "../tool-result-store.js";
|
|
44
45
|
import { OUTPUT_TOOL_NAME, REPORT_FINDINGS_TOOL_NAME, SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createOutputTool, createReportBlockedTool, createReportFindingsTool, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
|
|
@@ -240,6 +241,30 @@ class ParkRefusal extends Error {
|
|
|
240
241
|
export { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
241
242
|
export { isFableFamilyModelId, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
|
|
242
243
|
export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
|
|
244
|
+
export function gatedCallIdOf(p) {
|
|
245
|
+
if (p.suspendRef.token !== undefined)
|
|
246
|
+
return p.suspendRef.gatedCallId;
|
|
247
|
+
if (p.reviewRef.token !== undefined)
|
|
248
|
+
return p.reviewRef.gatedCallId;
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
function parkContaminationMarker(refs) {
|
|
252
|
+
if (refs.suspendRef.token === undefined && refs.reviewRef.token === undefined)
|
|
253
|
+
return undefined;
|
|
254
|
+
const gatedCallId = gatedCallIdOf(refs);
|
|
255
|
+
return { code: "gate.parked", ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
|
|
256
|
+
}
|
|
257
|
+
function publishCommittedSuspend(refs, token, gate, scope, remoteHandle, checkpointId, pendingAction) {
|
|
258
|
+
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? refs.reviewRef : refs.suspendRef;
|
|
259
|
+
ref.token = token;
|
|
260
|
+
if (checkpointId !== undefined)
|
|
261
|
+
ref.checkpointId = checkpointId;
|
|
262
|
+
ref.gate = gate;
|
|
263
|
+
ref.gatedCallId = pendingAction?.kind === "tool_approval" ? pendingAction.toolCallId : undefined;
|
|
264
|
+
if (remoteHandle !== undefined)
|
|
265
|
+
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
266
|
+
ref.scope = scope;
|
|
267
|
+
}
|
|
243
268
|
function hasConversationContent(branch) {
|
|
244
269
|
return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
|
|
245
270
|
}
|
|
@@ -2611,7 +2636,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2611
2636
|
stream: withBrainCallGuardrail((m, c, o) => deps.brain.stream(m, c, o), brainCallGuardrailMs, brainCallGuardrailRef),
|
|
2612
2637
|
};
|
|
2613
2638
|
const harness = new AgentHarness({
|
|
2614
|
-
abortResultDetails: () => suspendRef
|
|
2639
|
+
abortResultDetails: () => parkContaminationMarker({ suspendRef, reviewRef }),
|
|
2615
2640
|
...(spec.limits?.maxOutputTokens !== undefined && spec.limits.maxOutputTokens > 0
|
|
2616
2641
|
? { maxOutputTokens: spec.limits.maxOutputTokens }
|
|
2617
2642
|
: {}),
|
|
@@ -2686,6 +2711,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2686
2711
|
for (const n of list ?? []) {
|
|
2687
2712
|
if (known.has(n))
|
|
2688
2713
|
continue;
|
|
2714
|
+
if (isMcpCoveringRuleName(n) && [...known].some((k) => mcpRuleNameCovers(n, k)))
|
|
2715
|
+
continue;
|
|
2689
2716
|
const retired = RETIRED_TOOL_NAMES.get(n);
|
|
2690
2717
|
if (retired !== undefined) {
|
|
2691
2718
|
const err = new Error(`tool policy ${kind}-list entry "${n}" is a RETIRED tool name (${retired}) and matches nothing in this run's roster — legacy-name normalization was removed (RB-476-A), so this entry would silently guard nothing. Update the deployment's rule to the current name.`);
|
|
@@ -3748,16 +3775,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3748
3775
|
return { ok: false, reason };
|
|
3749
3776
|
}
|
|
3750
3777
|
};
|
|
3751
|
-
const publishCommittedSuspend = (token, gate, scope, remoteHandle, checkpointId) => {
|
|
3752
|
-
const ref = gate.kind === "needs_review" || gate.kind === "plan_review" ? reviewRef : suspendRef;
|
|
3753
|
-
ref.token = token;
|
|
3754
|
-
if (checkpointId !== undefined)
|
|
3755
|
-
ref.checkpointId = checkpointId;
|
|
3756
|
-
ref.gate = gate;
|
|
3757
|
-
if (remoteHandle !== undefined)
|
|
3758
|
-
ref.restoreMode = remoteHandle.restoreMode === "park_only" ? "park_only" : "snapshot";
|
|
3759
|
-
ref.scope = scope;
|
|
3760
|
-
};
|
|
3761
3778
|
const suspendLoopCapHit = (count, cap, detail) => {
|
|
3762
3779
|
if (cap === undefined)
|
|
3763
3780
|
return false;
|
|
@@ -3838,7 +3855,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3838
3855
|
};
|
|
3839
3856
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
|
|
3840
3857
|
return false;
|
|
3841
|
-
publishCommittedSuspend(token, gate, scope, remoteHandle, cp.checkpointId);
|
|
3858
|
+
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
3842
3859
|
try {
|
|
3843
3860
|
await sessions.pin?.(sessionId);
|
|
3844
3861
|
}
|
|
@@ -3924,7 +3941,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3924
3941
|
};
|
|
3925
3942
|
if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
|
|
3926
3943
|
return false;
|
|
3927
|
-
publishCommittedSuspend(token, gate, scope, remoteHandle, cp.checkpointId);
|
|
3944
|
+
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
3928
3945
|
try {
|
|
3929
3946
|
await sessions.pin?.(sessionId);
|
|
3930
3947
|
}
|
|
@@ -4219,7 +4236,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4219
4236
|
if (!committed.ok) {
|
|
4220
4237
|
return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
|
|
4221
4238
|
}
|
|
4222
|
-
publishCommittedSuspend(token, gate, cp.scope, remoteHandle, cp.checkpointId);
|
|
4239
|
+
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, cp.scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
4223
4240
|
try {
|
|
4224
4241
|
await sessions.pin?.(sessionId);
|
|
4225
4242
|
}
|
|
@@ -87,12 +87,19 @@ settledBy?: ApprovalSettledBy,
|
|
|
87
87
|
approver?: string,
|
|
88
88
|
/** The ask resolver's deny-arm classification — same caller, same engine-owned channel and the same
|
|
89
89
|
* never-derived-from-`result` posture as the two above. Omitted ⇒ not an ask-resolution deny. */
|
|
90
|
-
resolution?: import("../tool-policy.js").AskDenyResolution
|
|
90
|
+
resolution?: import("../tool-policy.js").AskDenyResolution,
|
|
91
|
+
/** WHICH call this run's committed durable park is holding ({@link gatedCallIdOf}), for the frames the
|
|
92
|
+
* abort short-circuits. Same never-derived-from-`result` posture as the three above, and for the
|
|
93
|
+
* sharpest version of that reason: this one is an assertion about a DIFFERENT call, so a tool able to
|
|
94
|
+
* author it could point an approval UI at a call nobody is waiting on. Omitted ⇒ no park is holding a
|
|
95
|
+
* call (nothing parked, or the park that did holds none), and the frame then carries no id at all. */
|
|
96
|
+
gatedCallIdOfRun?: string): {
|
|
91
97
|
output?: unknown;
|
|
92
98
|
truncated?: boolean;
|
|
93
99
|
totalChars?: number;
|
|
94
100
|
structured?: unknown;
|
|
95
101
|
errorCode?: string;
|
|
102
|
+
gatedCallId?: string;
|
|
96
103
|
settledBy?: ApprovalSettledBy;
|
|
97
104
|
approver?: string;
|
|
98
105
|
resolution?: import("../tool-policy.js").AskDenyResolution;
|
|
@@ -31,9 +31,9 @@ import { toImageContent } from "./image.js";
|
|
|
31
31
|
import { OUTPUT_TOOL_NAME, SKILLS_LISTING_PROBE_HEADER, resolveOutputRetries } from "./synthetic-tools.js";
|
|
32
32
|
import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
|
|
33
33
|
import { assembleResult, errorCodeOf } from "./assemble-result.js";
|
|
34
|
-
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
|
|
34
|
+
import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
|
|
35
35
|
import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
|
|
36
|
-
import { prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
36
|
+
import { gatedCallIdOf, prepareTask, resolveCheckpointStore } from "./prepare-task.js";
|
|
37
37
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
38
38
|
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
39
39
|
import { hasVerifiableStructureSignal } from "./grounding-signal.js";
|
|
@@ -41,7 +41,7 @@ import { hasDestroy, isIsolated } from "../remote-env.js";
|
|
|
41
41
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
42
42
|
import { cloneObserverInput, formatHookFeedback } from "../hooks.js";
|
|
43
43
|
import { buildHumanInputEvent, projectHumanInput } from "../human-input-projection.js";
|
|
44
|
-
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
|
|
44
|
+
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
45
45
|
import { reconcileInterruptedSession } from "../session-reconcile.js";
|
|
46
46
|
import { RunnerSharedToolResultStore } from "../tool-result-store.js";
|
|
47
47
|
import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
|
|
@@ -141,17 +141,19 @@ function resumeDecisionWasNegative(resume) {
|
|
|
141
141
|
}
|
|
142
142
|
const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
|
|
143
143
|
"NOT executed on resume. If you still need it, issue it again now.";
|
|
144
|
-
function toolEndBodyFrom(result, isError, settledBy, approver, resolution) {
|
|
144
|
+
function toolEndBodyFrom(result, isError, settledBy, approver, resolution, gatedCallIdOfRun) {
|
|
145
145
|
const o = toolOutputFrom(result);
|
|
146
146
|
const st = structuredFrom(result);
|
|
147
147
|
const det = isError ? result?.details : undefined;
|
|
148
148
|
const codeRaw = det?.code;
|
|
149
149
|
const kindRaw = det?.errorKind;
|
|
150
150
|
const code = typeof codeRaw === "string" ? codeRaw : typeof kindRaw === "string" ? kindRaw : undefined;
|
|
151
|
+
const gatedCallId = code === "gate.parked" ? gatedCallIdOfRun : undefined;
|
|
151
152
|
return {
|
|
152
153
|
...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
|
|
153
154
|
...(st !== undefined ? { structured: st } : {}),
|
|
154
155
|
...(typeof code === "string" ? { errorCode: code } : {}),
|
|
156
|
+
...(gatedCallId !== undefined ? { gatedCallId } : {}),
|
|
155
157
|
...(settledBy !== undefined ? { settledBy } : {}),
|
|
156
158
|
...(approver !== undefined ? { approver } : {}),
|
|
157
159
|
...(resolution !== undefined ? { resolution } : {}),
|
|
@@ -543,7 +545,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
543
545
|
const files = prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent);
|
|
544
546
|
if (files.length > 0) {
|
|
545
547
|
queue.push({ type: "diagnostics", files, isNew: true, ...ident() });
|
|
546
|
-
const block = formatDiagnosticsBlock(files);
|
|
548
|
+
const block = mintSystemReminder(formatDiagnosticsBlock(files), prepared.reminderMark);
|
|
547
549
|
void prepared.harness.steer(block, { provenance: "engine-note" }).catch(() => undefined);
|
|
548
550
|
boundarySteered = true;
|
|
549
551
|
}
|
|
@@ -746,7 +748,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
746
748
|
}
|
|
747
749
|
}
|
|
748
750
|
if (due.length > 0) {
|
|
749
|
-
attachmentsPayload = due.map((a) => mintSystemReminder(sanitizeUntrustedText(a.body), prepared.reminderMark)).join("\n");
|
|
751
|
+
attachmentsPayload = due.map((a) => mintSystemReminder(sanitizeUntrustedText(a.body, attachmentEnvelopeTags(a.source)), prepared.reminderMark)).join("\n");
|
|
750
752
|
for (const a of due)
|
|
751
753
|
boundaryAttachmentBytes += Buffer.byteLength(a.body, "utf8");
|
|
752
754
|
rs.attach.attachmentsInjected += due.length;
|
|
@@ -767,7 +769,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
767
769
|
try {
|
|
768
770
|
const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity });
|
|
769
771
|
if (r?.additionalContext && injectedThisTurn === undefined) {
|
|
770
|
-
const body = sanitizeUntrustedText(r.additionalContext);
|
|
772
|
+
const body = sanitizeUntrustedText(r.additionalContext, SHELLED_BODY_ENVELOPE_TAGS);
|
|
771
773
|
const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
|
|
772
774
|
if (budget >= BATCH_CONTEXT_MIN_KEEP_BYTES) {
|
|
773
775
|
const clipped = clipToBytes(body, budget) === body
|
|
@@ -1008,7 +1010,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
|
|
|
1008
1010
|
return onTurnBoundary;
|
|
1009
1011
|
}
|
|
1010
1012
|
function wrapGitFrame(body, mark) {
|
|
1011
|
-
return mintSystemReminder(sanitizeUntrustedText(body), mark);
|
|
1013
|
+
return mintSystemReminder(sanitizeUntrustedText(body, SHELLED_BODY_ENVELOPE_TAGS), mark);
|
|
1012
1014
|
}
|
|
1013
1015
|
async function resolveGitLegDelivery(prepared, cpMirror, report) {
|
|
1014
1016
|
const ref = prepared.gitStatusRef;
|
|
@@ -1344,7 +1346,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
|
|
|
1344
1346
|
toolName: event.toolName,
|
|
1345
1347
|
...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
|
|
1346
1348
|
isError: event.isError,
|
|
1347
|
-
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver, settlement?.resolution),
|
|
1349
|
+
...toolEndBodyFrom(event.result, event.isError, settlement?.settledBy, settlement?.approver, settlement?.resolution, prepared.abortController.signal.aborted ? gatedCallIdOf(prepared) : undefined),
|
|
1348
1350
|
...ident(),
|
|
1349
1351
|
});
|
|
1350
1352
|
announceWorkspaceMove();
|
|
@@ -3258,7 +3260,7 @@ export class Runner {
|
|
|
3258
3260
|
}
|
|
3259
3261
|
}
|
|
3260
3262
|
if (firstFrames.length > 0) {
|
|
3261
|
-
effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
|
|
3263
|
+
effectiveObjective = `${firstFrames.map((f) => mintSystemReminder(sanitizeUntrustedText(f.body, attachmentEnvelopeTags(f.source)), prepared.reminderMark)).join("\n")}\n${effectiveObjective}`;
|
|
3262
3264
|
}
|
|
3263
3265
|
}
|
|
3264
3266
|
const gitQueuedChars = gitLegDelivered !== undefined && gitLegDelivered.standalone ? gitLegDelivered.wrapped.length : 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
|
|
2
2
|
import { isWinFormPath } from "../../tools/fs/safety.js";
|
|
3
|
-
import { createCoarseCommandNamePolicy } from "../tool-policy.js";
|
|
3
|
+
import { createCoarseCommandNamePolicy, mcpCoveringEntries, mcpCoveringHit } from "../tool-policy.js";
|
|
4
4
|
export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
|
|
5
5
|
export const PATH_CONFINABLE_WRITE_TOOLS = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
|
|
6
6
|
export function isWithin(root, p) {
|
|
@@ -24,6 +24,8 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
24
24
|
const { env, rootPath, toolEffects } = opts;
|
|
25
25
|
const toolDeny = new Set(rules.toolDeny ?? []);
|
|
26
26
|
const toolAllow = rules.toolAllow ? new Set(rules.toolAllow) : undefined;
|
|
27
|
+
const toolDenyCovering = mcpCoveringEntries(rules.toolDeny);
|
|
28
|
+
const toolAllowCovering = mcpCoveringEntries(rules.toolAllow);
|
|
27
29
|
const cmdPolicy = rules.commandAllow || rules.commandDeny
|
|
28
30
|
? createCoarseCommandNamePolicy({
|
|
29
31
|
...(rules.commandAllow ? { allow: rules.commandAllow } : {}),
|
|
@@ -36,9 +38,9 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
36
38
|
nameSets: [{ ...(rules.toolDeny?.length ? { deny: [...rules.toolDeny] } : {}), ...(rules.toolAllow?.length ? { allow: [...rules.toolAllow] } : {}) }],
|
|
37
39
|
async check(req, signal) {
|
|
38
40
|
const toolName = req.toolName;
|
|
39
|
-
if (toolDeny.has(toolName))
|
|
41
|
+
if (toolDeny.has(toolName) || mcpCoveringHit(toolDenyCovering, toolName))
|
|
40
42
|
return deny(`tool "${req.toolName}" is denied by a session rule`);
|
|
41
|
-
if (toolAllow && !toolAllow.has(toolName)) {
|
|
43
|
+
if (toolAllow && !toolAllow.has(toolName) && !mcpCoveringHit(toolAllowCovering, toolName)) {
|
|
42
44
|
return deny(`tool "${req.toolName}" is not in the session-rule allowlist`);
|
|
43
45
|
}
|
|
44
46
|
if (cmdPolicy) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool } from "../tools.js";
|
|
3
|
+
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
3
4
|
export const OUTPUT_TOOL_NAME = "StructuredOutput";
|
|
4
5
|
export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
|
|
5
6
|
export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
|
|
@@ -123,7 +124,8 @@ export function skillListingLine(s) {
|
|
|
123
124
|
const desc = s.description.length > SKILL_DESC_MAX_CHARS
|
|
124
125
|
? `${s.description.slice(0, SKILL_DESC_MAX_CHARS - 1)}…`
|
|
125
126
|
: s.description;
|
|
126
|
-
|
|
127
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
128
|
+
return `- ${safe(s.name)}: ${safe(desc)}${safe(skillFilesSuffix(s.files))}`;
|
|
127
129
|
}
|
|
128
130
|
const SKILLS_BLOCK_CAP_MARKER = "(block size cap reached — lower-priority skills are listed name-only; descriptions via the tool)";
|
|
129
131
|
export function buildSkillsBlock(skills) {
|
|
@@ -141,7 +143,7 @@ export function buildSkillsBlock(skills) {
|
|
|
141
143
|
const lineCost = (l) => bytes(l) + 1;
|
|
142
144
|
const budget = SKILLS_BLOCK_MAX_BYTES - bytes(render([]));
|
|
143
145
|
const fullLines = skills.map((s) => skillListingLine(s));
|
|
144
|
-
const nameLines = skills.map((s) => `- ${s.name}`);
|
|
146
|
+
const nameLines = skills.map((s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`);
|
|
145
147
|
const fullTotal = fullLines.reduce((acc, l) => acc + lineCost(l), 0);
|
|
146
148
|
if (fullTotal <= budget)
|
|
147
149
|
return render(fullLines);
|
|
@@ -65,6 +65,7 @@ export interface BackgroundTaskSnapshot {
|
|
|
65
65
|
description?: string;
|
|
66
66
|
status: string;
|
|
67
67
|
}
|
|
68
|
+
export declare function attachmentEnvelopeTags(source: AttachmentSource): string[];
|
|
68
69
|
/** One due reminder. `body` is the inner text — the steerer adds the `<system-reminder>` wrapper (F3). */
|
|
69
70
|
export interface TurnAttachment {
|
|
70
71
|
source: AttachmentSource;
|
|
@@ -638,7 +639,9 @@ export declare function replayAnnouncedListing(texts: readonly string[], headers
|
|
|
638
639
|
}): Set<string> | undefined;
|
|
639
640
|
/** Removal-frame header for the agent-type roster (single-sourced for renderer + replay parser). */
|
|
640
641
|
export declare const AGENT_LISTING_REMOVED_HEADER = "The following agent types are no longer available:";
|
|
641
|
-
/** CC-verbatim roster-drift header — same single-source rationale as {@link agentListingInitialHeader}.
|
|
642
|
+
/** CC-verbatim roster-drift header — same single-source rationale as {@link agentListingInitialHeader}.
|
|
643
|
+
* The interpolated tool name is neutralized on both headers: a caller renames the delegation tool, so
|
|
644
|
+
* the name is deployment text arriving on the marked-reminder lane like every other field here. */
|
|
642
645
|
export declare function agentListingDeltaHeader(toolName: string): string;
|
|
643
646
|
/**
|
|
644
647
|
* G1 续批 — agent-type roster announce (CC 2.1.198 `agent_listing_delta`, bundle pretty.js:599199-599223,
|
|
@@ -745,11 +748,18 @@ export declare const MCP_INSTRUCTIONS_MAX_CHARS: number;
|
|
|
745
748
|
* — fenced on the delta lane, RAW-INTERPOLATED (no fence, no bound) into the stable prompt. A single
|
|
746
749
|
* source makes the divergence unrepresentable.
|
|
747
750
|
*
|
|
748
|
-
* Truncation is DISCLOSED (§25 honesty):
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
751
|
+
* Truncation is DISCLOSED (§25 honesty): the fence clips silently with an ellipsis, which reads to the
|
|
752
|
+
* model as the server's own text. The trailing note lives OUTSIDE the fence, on a trusted line, so the
|
|
753
|
+
* model can tell "the server said less" from "we cut it".
|
|
754
|
+
*
|
|
755
|
+
* The clip verdict comes FROM the fence (`delimitUntrustedWithClip`), never from a length test
|
|
756
|
+
* re-derived here. The earlier shape did re-derive it — a local `sanitizeUntrustedText` pre-pass with
|
|
757
|
+
* the DEFAULT (reminder-only) tag set, measured against the cap — and that mirror silently stopped
|
|
758
|
+
* matching once the fence widened its neutralization to the whole authority-envelope family: each
|
|
759
|
+
* widened-family tag occurrence adds a ZWSP the pre-count never saw, so a server could land its text
|
|
760
|
+
* in the `(cap − N, cap]` window and get the body clipped with the honesty note withheld, N being its
|
|
761
|
+
* own choice of tag count. One measurement, taken where the cut happens, is the only shape that cannot
|
|
762
|
+
* drift.
|
|
753
763
|
*/
|
|
754
764
|
export declare function fenceMcpServerInstructions(server: string, text: string): string;
|
|
755
765
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PRESENT_PLAN_TOOL_NAME } from "../present-plan-tool.js";
|
|
2
|
-
import {
|
|
2
|
+
import { delimitUntrustedWithClip, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
3
3
|
import { protocolOf } from "../protocol-table.js";
|
|
4
4
|
import { buildSkillsBlock, skillListingLine } from "./synthetic-tools.js";
|
|
5
5
|
import { TOOL_SEARCH_NAME as TOOL_SEARCH_TOOL_NAME } from "./tool-disclosure.js";
|
|
@@ -20,6 +20,11 @@ export const CHANGED_FILES_MTIME_EPS_MS = 2000;
|
|
|
20
20
|
export const ATTACHMENT_BYTE_CAP = 8 * 1024;
|
|
21
21
|
const PROJECTION_ITEMS_MAX = 50;
|
|
22
22
|
const PROJECTION_CONTENT_MAX = 80;
|
|
23
|
+
const ATTACHMENT_TAGS_DEFAULT = [...SHELLED_BODY_ENVELOPE_TAGS];
|
|
24
|
+
const ATTACHMENT_TAGS_SKILLS_OWNER = SHELLED_BODY_ENVELOPE_TAGS.filter((t) => t !== "skills");
|
|
25
|
+
export function attachmentEnvelopeTags(source) {
|
|
26
|
+
return source === "skills_listing" ? ATTACHMENT_TAGS_SKILLS_OWNER : ATTACHMENT_TAGS_DEFAULT;
|
|
27
|
+
}
|
|
23
28
|
export const INSTRUCTIONS_CHANGE_BYTE_CAP = 512;
|
|
24
29
|
export function createAttachmentState() {
|
|
25
30
|
return {
|
|
@@ -358,8 +363,9 @@ export function renderBudgetUsd(used, total) {
|
|
|
358
363
|
return `USD budget: $${used}/$${total}; $${total - used} remaining`;
|
|
359
364
|
}
|
|
360
365
|
export function renderOrphanedBackgroundTasks(tasks) {
|
|
366
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
361
367
|
return (`The container was restarted. The following background tasks were running and are now stopped:\n` +
|
|
362
|
-
tasks.map((t) => `- ${t.description || "(no description)"} (task ${t.id})`).join("\n") +
|
|
368
|
+
tasks.map((t) => `- ${safe(t.description || "(no description)")} (task ${safe(t.id)})`).join("\n") +
|
|
363
369
|
`\nRe-create them if still needed.`);
|
|
364
370
|
}
|
|
365
371
|
function renderChangedFiles(paths) {
|
|
@@ -449,7 +455,7 @@ export const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
|
|
|
449
455
|
export const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
|
|
450
456
|
export const AMBIENT_CONTEXT_NOTE = "This is ambient context — do not narrate it to the user unless they ask or it is directly relevant to their request.";
|
|
451
457
|
export function agentListingInitialHeader(toolName) {
|
|
452
|
-
return `Available agent types for the ${toolName} tool:`;
|
|
458
|
+
return `Available agent types for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
453
459
|
}
|
|
454
460
|
export function replayAnnouncedListing(texts, headers) {
|
|
455
461
|
let announced;
|
|
@@ -492,13 +498,17 @@ export function replayAnnouncedListing(texts, headers) {
|
|
|
492
498
|
return announced;
|
|
493
499
|
}
|
|
494
500
|
export const AGENT_LISTING_REMOVED_HEADER = "The following agent types are no longer available:";
|
|
501
|
+
function listingIdentity(name) {
|
|
502
|
+
return sanitizeUntrustedText(name, SHELLED_BODY_ENVELOPE_TAGS);
|
|
503
|
+
}
|
|
495
504
|
export function agentListingDeltaHeader(toolName) {
|
|
496
|
-
return `New agent types are now available for the ${toolName} tool:`;
|
|
505
|
+
return `New agent types are now available for the ${sanitizeUntrustedText(toolName, SHELLED_BODY_ENVELOPE_TAGS)} tool:`;
|
|
497
506
|
}
|
|
498
507
|
export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
508
|
+
const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
|
|
499
509
|
const line = (e) => {
|
|
500
|
-
const base = e.description ? `- ${e.name}: ${e.description}` : `- ${e.name}`;
|
|
501
|
-
return `${base} (Tools: ${e.tools ?? AGENT_TOOLS_NOTE_DEFAULT})`;
|
|
510
|
+
const base = e.description ? `- ${safe(e.name)}: ${safe(e.description)}` : `- ${safe(e.name)}`;
|
|
511
|
+
return `${base} (Tools: ${safe(e.tools ?? AGENT_TOOLS_NOTE_DEFAULT)})`;
|
|
502
512
|
};
|
|
503
513
|
const announced = state.announcedAgentTypes;
|
|
504
514
|
if (announced === undefined) {
|
|
@@ -511,18 +521,22 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
511
521
|
blocks.push(modelsAvailableLine(models));
|
|
512
522
|
return blocks.join("\n\n");
|
|
513
523
|
}
|
|
514
|
-
const
|
|
515
|
-
const
|
|
524
|
+
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
525
|
+
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name))).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
526
|
+
const removed = [...announced.keys()]
|
|
527
|
+
.filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)))
|
|
528
|
+
.sort();
|
|
516
529
|
const modelsDrifted = state.announcedModels !== undefined &&
|
|
517
530
|
models !== undefined &&
|
|
518
|
-
(state.announcedModels.length !== models.length ||
|
|
531
|
+
(state.announcedModels.length !== models.length ||
|
|
532
|
+
state.announcedModels.some((m, i) => listingIdentity(m) !== listingIdentity(models[i])));
|
|
519
533
|
if (added.length === 0 && removed.length === 0 && !modelsDrifted)
|
|
520
534
|
return undefined;
|
|
521
535
|
const blocks = [];
|
|
522
536
|
if (added.length > 0)
|
|
523
537
|
blocks.push(`${agentListingDeltaHeader(toolName)}\n${added.map(line).join("\n")}`);
|
|
524
538
|
if (removed.length > 0) {
|
|
525
|
-
blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${n}`).join("\n")}`);
|
|
539
|
+
blocks.push(`${AGENT_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${safe(n)}`).join("\n")}`);
|
|
526
540
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
527
541
|
}
|
|
528
542
|
if (modelsDrifted)
|
|
@@ -531,7 +545,8 @@ export function renderAgentListingDelta(state, entries, toolName, models) {
|
|
|
531
545
|
}
|
|
532
546
|
export const MODELS_AVAILABLE_PREFIX = "Models available for the 'model' parameter: ";
|
|
533
547
|
function modelsAvailableLine(models) {
|
|
534
|
-
|
|
548
|
+
const named = models.map((m) => sanitizeUntrustedText(m, SHELLED_BODY_ENVELOPE_TAGS));
|
|
549
|
+
return `${MODELS_AVAILABLE_PREFIX}${named.length > 0 ? named.join(", ") : "(none)"}`;
|
|
535
550
|
}
|
|
536
551
|
export function replayAnnouncedModels(texts) {
|
|
537
552
|
let last;
|
|
@@ -563,15 +578,16 @@ export function renderSkillsListingDelta(state, entries) {
|
|
|
563
578
|
return undefined;
|
|
564
579
|
return buildSkillsBlock(entries);
|
|
565
580
|
}
|
|
566
|
-
const
|
|
567
|
-
const
|
|
581
|
+
const announcedIds = new Set([...announced.keys()].map(listingIdentity));
|
|
582
|
+
const added = entries.filter((e) => !announcedIds.has(listingIdentity(e.name)));
|
|
583
|
+
const removed = [...announced.keys()].filter((n) => !entries.some((e) => listingIdentity(e.name) === listingIdentity(n)));
|
|
568
584
|
if (added.length === 0 && removed.length === 0)
|
|
569
585
|
return undefined;
|
|
570
586
|
const blocks = [];
|
|
571
587
|
if (added.length > 0)
|
|
572
588
|
blocks.push(`${SKILLS_LISTING_DELTA_HEADER}\n${added.map((e) => skillListingLine(e)).join("\n")}`);
|
|
573
589
|
if (removed.length > 0) {
|
|
574
|
-
blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${n}`).join("\n")}`);
|
|
590
|
+
blocks.push(`${SKILLS_LISTING_REMOVED_HEADER}\n${removed.map((n) => `- ${sanitizeUntrustedText(n, SHELLED_BODY_ENVELOPE_TAGS)}`).join("\n")}`);
|
|
575
591
|
blocks.push(AMBIENT_CONTEXT_NOTE);
|
|
576
592
|
}
|
|
577
593
|
return blocks.join("\n\n");
|
|
@@ -581,12 +597,10 @@ export function commitSkillsListing(state, entries) {
|
|
|
581
597
|
}
|
|
582
598
|
export const MCP_INSTRUCTIONS_MAX_CHARS = 8 * 1024;
|
|
583
599
|
export function fenceMcpServerInstructions(server, text) {
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
? `${fenced}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
|
|
589
|
-
: fenced;
|
|
600
|
+
const fence = delimitUntrustedWithClip(`MCP server "${server}" instructions`, text, MCP_INSTRUCTIONS_MAX_CHARS);
|
|
601
|
+
return fence.clipped
|
|
602
|
+
? `${fence.text}\n(Truncated by the agent runtime: this server's instructions exceeded ${MCP_INSTRUCTIONS_MAX_CHARS} characters.)`
|
|
603
|
+
: fence.text;
|
|
590
604
|
}
|
|
591
605
|
export function renderMcpInstructionsDelta(added, removed) {
|
|
592
606
|
const blocks = [];
|
|
@@ -383,6 +383,24 @@ export interface ToolPolicyProjection {
|
|
|
383
383
|
*/
|
|
384
384
|
readonly requiresLiveRemainder: boolean;
|
|
385
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* The entries of a tool-NAME list whose reach is a set of names rather than one name — the MCP
|
|
388
|
+
* covering spellings (`mcp__<server>`, `mcp__<server>__<glob>`) that CC's rule matcher resolves and
|
|
389
|
+
* an exact-membership test silently cannot. Every name-keyed lane in this file consults these
|
|
390
|
+
* ALONGSIDE its exact set: a covering spelling can never equal a minted tool name (`*` is outside the
|
|
391
|
+
* minted charset and a minted name always carries a tool segment), so leaving it in the exact set too
|
|
392
|
+
* costs nothing and keeps the projection/audit faces reporting the operator's own spelling.
|
|
393
|
+
*
|
|
394
|
+
* The list is usually empty, which is why every call site tests it before scanning.
|
|
395
|
+
*
|
|
396
|
+
* Exported for the OTHER name-keyed lane in this engine — the persisted session/ancestor rules in
|
|
397
|
+
* `session-rule-policy.ts`. Not part of the public API (`src/index.ts` re-exports by name and does not
|
|
398
|
+
* list these): all four publishers of {@link ToolPolicyNameSets} must resolve a covering entry the same
|
|
399
|
+
* way, because the prepare-time audit's exemption for these spellings speaks for all of them at once.
|
|
400
|
+
*/
|
|
401
|
+
export declare function mcpCoveringEntries(entries: readonly string[] | undefined): readonly string[];
|
|
402
|
+
/** Does any covering entry reach `toolName`? See {@link mcpCoveringEntries}. */
|
|
403
|
+
export declare function mcpCoveringHit(covering: readonly string[], toolName: string): boolean;
|
|
386
404
|
/**
|
|
387
405
|
* Execute a persisted {@link ToolPolicyProjection} against a call (F-012 L1): returns the first
|
|
388
406
|
* component's deny, or `undefined` when the projection has no opinion (it is deny-only by
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -5,7 +5,7 @@ import { join, normalize as normalizePath, posix as posixPath, sep, win32 as win
|
|
|
5
5
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
6
6
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
7
7
|
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
8
|
-
import { parsePermissionRule } from "./permission-rules.js";
|
|
8
|
+
import { isMcpCoveringRuleName, mcpRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
|
|
9
9
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
10
10
|
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
11
11
|
const DECISION_REASON_SET = new Set(DECISION_REASONS);
|
|
@@ -36,16 +36,22 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
|
|
|
36
36
|
export function decisionText(d) {
|
|
37
37
|
return d.message;
|
|
38
38
|
}
|
|
39
|
+
export function mcpCoveringEntries(entries) {
|
|
40
|
+
return (entries ?? []).filter(isMcpCoveringRuleName);
|
|
41
|
+
}
|
|
42
|
+
export function mcpCoveringHit(covering, toolName) {
|
|
43
|
+
return covering.length > 0 && covering.some((e) => mcpRuleNameCovers(e, toolName));
|
|
44
|
+
}
|
|
39
45
|
export function checkToolPolicyProjection(projection, req) {
|
|
40
46
|
for (const c of projection.components) {
|
|
41
47
|
if (c.kind === "tool_deny") {
|
|
42
|
-
if (c.names.includes(req.toolName)) {
|
|
48
|
+
if (c.names.includes(req.toolName) || mcpCoveringHit(mcpCoveringEntries(c.names), req.toolName)) {
|
|
43
49
|
return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
|
|
44
50
|
}
|
|
45
51
|
continue;
|
|
46
52
|
}
|
|
47
53
|
if (c.kind === "tool_allowlist") {
|
|
48
|
-
if (!c.names.includes(req.toolName)) {
|
|
54
|
+
if (!c.names.includes(req.toolName) && !mcpCoveringHit(mcpCoveringEntries(c.names), req.toolName)) {
|
|
49
55
|
return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
|
|
50
56
|
}
|
|
51
57
|
continue;
|
|
@@ -218,6 +224,8 @@ export function createAllowDenyPolicy(opts) {
|
|
|
218
224
|
opts = { ...opts, ...(screenedAllow ? { allow: screenedAllow } : {}), ...(screenedDeny ? { deny: screenedDeny } : {}) };
|
|
219
225
|
const allow = opts.allow ? new Set(opts.allow) : undefined;
|
|
220
226
|
const deny = new Set(opts.deny ?? []);
|
|
227
|
+
const denyCovering = mcpCoveringEntries(opts.deny);
|
|
228
|
+
const allowCovering = mcpCoveringEntries(opts.allow);
|
|
221
229
|
return {
|
|
222
230
|
projection: {
|
|
223
231
|
components: [
|
|
@@ -229,10 +237,10 @@ export function createAllowDenyPolicy(opts) {
|
|
|
229
237
|
nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
|
|
230
238
|
check(req) {
|
|
231
239
|
const toolName = req.toolName;
|
|
232
|
-
if (deny.has(toolName)) {
|
|
240
|
+
if (deny.has(toolName) || mcpCoveringHit(denyCovering, toolName)) {
|
|
233
241
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
234
242
|
}
|
|
235
|
-
if (allow && !allow.has(toolName)) {
|
|
243
|
+
if (allow && !allow.has(toolName) && !mcpCoveringHit(allowCovering, toolName)) {
|
|
236
244
|
return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
|
|
237
245
|
}
|
|
238
246
|
return ALLOW;
|
|
@@ -249,6 +257,9 @@ export function createApprovalPolicy(opts) {
|
|
|
249
257
|
const need = new Set(opts.requireApproval);
|
|
250
258
|
const deny = new Set(opts.deny ?? []);
|
|
251
259
|
const auto = new Set(opts.autoAllow ?? []);
|
|
260
|
+
const needCovering = mcpCoveringEntries(opts.requireApproval);
|
|
261
|
+
const denyCovering = mcpCoveringEntries(opts.deny);
|
|
262
|
+
const autoCovering = mcpCoveringEntries(opts.autoAllow);
|
|
252
263
|
return {
|
|
253
264
|
projection: {
|
|
254
265
|
components: [
|
|
@@ -266,10 +277,10 @@ export function createApprovalPolicy(opts) {
|
|
|
266
277
|
],
|
|
267
278
|
async check(req, signal) {
|
|
268
279
|
const toolName = req.toolName;
|
|
269
|
-
if (deny.has(toolName)) {
|
|
280
|
+
if (deny.has(toolName) || mcpCoveringHit(denyCovering, toolName)) {
|
|
270
281
|
return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
|
|
271
282
|
}
|
|
272
|
-
if (need.has(toolName)) {
|
|
283
|
+
if (need.has(toolName) || mcpCoveringHit(needCovering, toolName)) {
|
|
273
284
|
if (signal?.aborted) {
|
|
274
285
|
return withCoreMintedResolution({ action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, settledBy: "aborted" }, "task_aborted", req);
|
|
275
286
|
}
|
|
@@ -303,7 +314,7 @@ export function createApprovalPolicy(opts) {
|
|
|
303
314
|
}
|
|
304
315
|
return withCoreMintedResolution({ action: "deny", message: `approval denied for "${req.toolName}"`, settledBy: "human" }, "human_refused", req);
|
|
305
316
|
}
|
|
306
|
-
if (opts.denyByDefault && !auto.has(toolName)) {
|
|
317
|
+
if (opts.denyByDefault && !auto.has(toolName) && !mcpCoveringHit(autoCovering, toolName)) {
|
|
307
318
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
308
319
|
}
|
|
309
320
|
return ALLOW;
|