@sema-agent/core 7.6.3 → 7.8.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 +70 -10
- package/dist/agents/agent-transcript-tool.js +2 -2
- package/dist/agents/list-agents-tool.js +2 -3
- package/dist/agents/observer.js +2 -2
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/send-message-tool.js +2 -1
- package/dist/agents/subagent-steps.js +5 -4
- package/dist/agents/subagent.js +2 -3
- package/dist/brain/anthropic.js +8 -2
- package/dist/brain/open-responses.js +5 -3
- package/dist/brain/openai.js +31 -8
- package/dist/brain/reasoning.d.ts +32 -0
- package/dist/brain/reasoning.js +18 -0
- package/dist/core/ask-question.js +2 -2
- package/dist/core/auto-mode-defaults.d.ts +16 -0
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +19 -0
- package/dist/core/auto-mode.js +74 -56
- package/dist/core/checkpoint-execution-record.d.ts +110 -0
- package/dist/core/checkpoint-execution-record.js +49 -0
- package/dist/core/checkpoint-store.d.ts +114 -2
- package/dist/core/checkpoint-store.js +35 -2
- package/dist/core/context-edit.d.ts +0 -22
- package/dist/core/context-edit.js +2 -11
- package/dist/core/engine-notice.d.ts +17 -0
- package/dist/core/fs-write-gate-policy.d.ts +7 -1
- package/dist/core/fs-write-gate-policy.js +7 -7
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +4 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +8 -2
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +17 -0
- package/dist/core/mcp.js +3 -1
- package/dist/core/memory-engine/tools.js +4 -12
- package/dist/core/permission-rules.d.ts +32 -2
- package/dist/core/permission-rules.js +43 -37
- package/dist/core/present-plan-tool.js +3 -4
- package/dist/core/runner/active-skill-scope.js +7 -4
- package/dist/core/runner/clock-and-limits.d.ts +117 -0
- package/dist/core/runner/clock-and-limits.js +118 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/contracts.d.ts +37 -3
- package/dist/core/runner/decide-continuation.d.ts +98 -0
- package/dist/core/runner/decide-continuation.js +133 -0
- package/dist/core/runner/execution-record.d.ts +26 -0
- package/dist/core/runner/execution-record.js +19 -0
- package/dist/core/runner/git-leg-delivery.d.ts +28 -0
- package/dist/core/runner/git-leg-delivery.js +94 -0
- package/dist/core/runner/initial-run-state.d.ts +14 -0
- package/dist/core/runner/initial-run-state.js +11 -0
- package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +33 -11
- package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
- package/dist/core/runner/prepare-defer-classify.js +10 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
- package/dist/core/runner/prepare-delegation-surface.js +15 -17
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-lsp.d.ts +4 -3
- package/dist/core/runner/prepare-lsp.js +3 -2
- package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.js +4 -2
- package/dist/core/runner/prepare-offload-wrappers.js +7 -5
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-project-context.d.ts +5 -23
- package/dist/core/runner/prepare-project-context.js +7 -5
- package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
- package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
- package/dist/core/runner/prepare-protocol-tools.js +38 -21
- package/dist/core/runner/prepare-question-face.d.ts +4 -3
- package/dist/core/runner/prepare-question-face.js +5 -3
- package/dist/core/runner/prepare-safety-scan.js +42 -2
- package/dist/core/runner/prepare-task.js +37 -19
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
- package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +3 -8
- package/dist/core/runner/run-harness-handlers.d.ts +46 -0
- package/dist/core/runner/run-harness-handlers.js +345 -0
- package/dist/core/runner/run-turn-boundary.d.ts +69 -0
- package/dist/core/runner/run-turn-boundary.js +694 -0
- package/dist/core/runner/runtask.d.ts +0 -68
- package/dist/core/runner/runtask.js +66 -1486
- package/dist/core/runner/session-rule-policy.js +9 -4
- package/dist/core/runner/steer-admission.d.ts +17 -0
- package/dist/core/runner/steer-admission.js +17 -0
- package/dist/core/runner/synthetic-tools.js +5 -8
- package/dist/core/runner/tool-disclosure.d.ts +4 -0
- package/dist/core/runner/tool-disclosure.js +9 -5
- package/dist/core/runner/tool-end-body.d.ts +71 -0
- package/dist/core/runner/tool-end-body.js +75 -0
- package/dist/core/runner/tool-face-overlay.d.ts +40 -0
- package/dist/core/runner/tool-face-overlay.js +125 -0
- package/dist/core/runner/tool-output-projection.d.ts +10 -0
- package/dist/core/runner/tool-output-projection.js +33 -10
- package/dist/core/sensitive-path-policy.d.ts +3 -0
- package/dist/core/sensitive-path-policy.js +6 -5
- package/dist/core/shared-memory/tools.js +3 -6
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +108 -0
- package/dist/core/task-event.d.ts +10 -0
- package/dist/core/task-registry.js +4 -5
- package/dist/core/task-spec.d.ts +8 -0
- package/dist/core/tool-catalog-entries.d.ts +48 -0
- package/dist/core/tool-catalog-entries.js +174 -0
- package/dist/core/tool-conformance.d.ts +22 -0
- package/dist/core/tool-conformance.js +37 -0
- package/dist/core/tool-face.d.ts +171 -0
- package/dist/core/tool-face.js +25 -0
- package/dist/core/tool-policy.d.ts +24 -0
- package/dist/core/tool-policy.js +4 -2
- package/dist/core/tool-registry.d.ts +114 -0
- package/dist/core/tool-registry.js +114 -0
- package/dist/core/tool-result-budget.d.ts +0 -3
- package/dist/core/tool-result-budget.js +4 -3
- package/dist/core/tool-result-store.d.ts +0 -18
- package/dist/core/tool-result-store.js +2 -18
- package/dist/core/tool-roster.d.ts +450 -0
- package/dist/core/tool-roster.js +497 -0
- package/dist/core/tool-spec.d.ts +54 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/trace.d.ts +24 -0
- package/dist/core/wiring-manifest.d.ts +13 -0
- package/dist/core/wiring-manifest.js +2 -1
- package/dist/core/write-protect.d.ts +2 -1
- package/dist/core/write-protect.js +5 -4
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +19 -19
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/index.d.ts +20 -8
- package/dist/index.js +19 -7
- package/dist/orchestration/goal.js +2 -1
- package/dist/orchestration/run-spec.js +3 -5
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
- package/dist/stores/file/checkpoint-store.d.ts +7 -0
- package/dist/stores/file/checkpoint-store.js +20 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +7 -11
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +0 -7
- package/dist/tools/fs/fs-search-tools.js +3 -16
- package/dist/tools/fs/fs-write.js +4 -6
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +2 -1
- package/dist/tools/fs/safety.js +6 -5
- package/dist/tools/monitor.js +2 -2
- package/dist/tools/scheduler-tools.js +5 -8
- package/dist/tools/task-list.js +5 -8
- package/dist/tools/todo.js +2 -2
- package/dist/tools/web.js +3 -6
- package/dist/tools/worktree.js +3 -4
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +183 -7
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
|
|
2
2
|
import { isWinFormPath } from "../../tools/fs/safety.js";
|
|
3
3
|
import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
|
|
4
|
-
|
|
5
|
-
export const
|
|
4
|
+
import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames, skillScopeWriteToolNames } from "../tool-registry.js";
|
|
5
|
+
export const PATH_WRITE_TOOLS = skillScopeWriteToolNames();
|
|
6
|
+
export const PATH_CONFINABLE_WRITE_TOOLS = pathConfinableWriteToolNames();
|
|
6
7
|
export function isWithin(root, p) {
|
|
7
8
|
if (!root)
|
|
8
9
|
return false;
|
|
@@ -49,14 +50,18 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
49
50
|
return d;
|
|
50
51
|
}
|
|
51
52
|
if (allowDirs) {
|
|
52
|
-
if (!
|
|
53
|
+
if (!isProtectedWrite(req)) {
|
|
53
54
|
const eff = toolEffects?.get(toolName) ?? "write";
|
|
54
55
|
if (eff !== "read") {
|
|
55
56
|
return deny(`write-capable tool "${req.toolName}" denied: session rule confines writes to allowDirs but this tool cannot be path-confined`);
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
else {
|
|
59
|
-
const
|
|
60
|
+
const declared = declaredPathTargetOf(req);
|
|
61
|
+
if (declared === undefined) {
|
|
62
|
+
return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the tool declares no path target to confine`);
|
|
63
|
+
}
|
|
64
|
+
const path = writeTargetPath(req, declared);
|
|
60
65
|
if (typeof path !== "string" || path.length === 0) {
|
|
61
66
|
return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
|
|
62
67
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admission bookkeeping for HUMAN INPUTS accepted mid-run (design/171 §6.2–§6.3): the per-leg ordinal an
|
|
3
|
+
* accepted input is stamped with, and the field-for-field "is this repeat the same instruction" test the
|
|
4
|
+
* live steer verb applies under an already-used `inputId`. Two accepting arms share both — the stream's
|
|
5
|
+
* `steer` verb and the run body — which is why the ordinal is keyed on the leg's harness in a module-level
|
|
6
|
+
* WeakMap rather than held by either arm: one counter per leg, one ascending sequence across both.
|
|
7
|
+
*
|
|
8
|
+
* Run-loop machinery (layer 1), not a driven lane: nothing here takes an Input or hands back a Result, and
|
|
9
|
+
* nothing here reads host state. Named without the `run-` family prefix for that reason.
|
|
10
|
+
*/
|
|
11
|
+
import type { AcceptedSteerInput } from "./contracts.js";
|
|
12
|
+
export declare function nextHumanInputSeq(key: object): number;
|
|
13
|
+
/** design/171 §6.3 parity, LIVE leg — is a repeat under an already-accepted `inputId` the SAME
|
|
14
|
+
* instruction (an idempotent retry) or a different one wearing a used key? Compared field for field,
|
|
15
|
+
* exactly like the parked leg's replay check: the key alone is never evidence of a replay, and two
|
|
16
|
+
* callers colliding on one id must not silently lose the second instruction. */
|
|
17
|
+
export declare function sameAcceptedSteerInput(a: AcceptedSteerInput, b: AcceptedSteerInput): boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const humanInputOrdinals = new WeakMap();
|
|
2
|
+
export function nextHumanInputSeq(key) {
|
|
3
|
+
let box = humanInputOrdinals.get(key);
|
|
4
|
+
if (box === undefined) {
|
|
5
|
+
box = { n: 0 };
|
|
6
|
+
humanInputOrdinals.set(key, box);
|
|
7
|
+
}
|
|
8
|
+
return ++box.n;
|
|
9
|
+
}
|
|
10
|
+
export function sameAcceptedSteerInput(a, b) {
|
|
11
|
+
return (a.payload === b.payload &&
|
|
12
|
+
a.trusted === b.trusted &&
|
|
13
|
+
a.priority === b.priority &&
|
|
14
|
+
a.actor?.id === b.actor?.id &&
|
|
15
|
+
a.actor?.hostAsserted === b.actor?.hostAsserted &&
|
|
16
|
+
a.actor?.issuer === b.actor?.issuer);
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool } from "../tools.js";
|
|
3
3
|
import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
|
|
4
|
+
import { toolFace } from "../tool-catalog-entries.js";
|
|
4
5
|
export const OUTPUT_TOOL_NAME = "StructuredOutput";
|
|
5
6
|
export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
|
|
6
7
|
export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
|
|
@@ -15,10 +16,9 @@ export function resolveOutputRetries(outputRetries) {
|
|
|
15
16
|
export function createOutputTool(ref, schema, strictSchema) {
|
|
16
17
|
return defineTool({
|
|
17
18
|
name: OUTPUT_TOOL_NAME,
|
|
18
|
-
|
|
19
|
+
...toolFace("structured-output"),
|
|
19
20
|
description: "Call this to submit your FINAL result as a JSON object matching the required schema. The arguments " +
|
|
20
21
|
"ARE the result. Calling this ends the task. Do this once you have the answer (use other tools first if needed).",
|
|
21
|
-
effect: "read",
|
|
22
22
|
parameters: strictSchema ?? schema,
|
|
23
23
|
execute: (args) => {
|
|
24
24
|
ref.value = args;
|
|
@@ -30,9 +30,8 @@ export function createOutputTool(ref, schema, strictSchema) {
|
|
|
30
30
|
export function createReportBlockedTool(ref) {
|
|
31
31
|
return defineTool({
|
|
32
32
|
name: REPORT_BLOCKED_TOOL_NAME,
|
|
33
|
-
|
|
33
|
+
...toolFace("report-blocked"),
|
|
34
34
|
description: "Call this ONLY when you cannot complete the task — missing information, missing permission/credentials, or an ambiguous request you cannot resolve. Provide a concise reason that an external system or operator can act on. Calling this ends the task.",
|
|
35
|
-
effect: "read",
|
|
36
35
|
parameters: Type.Object({ reason: Type.String() }),
|
|
37
36
|
execute: (args) => {
|
|
38
37
|
ref.reason = String(args.reason ?? "").trim() || "blocked";
|
|
@@ -59,14 +58,13 @@ const REPORT_FINDING_SCHEMA = Type.Object({
|
|
|
59
58
|
export function createReportFindingsTool() {
|
|
60
59
|
return defineTool({
|
|
61
60
|
name: REPORT_FINDINGS_TOOL_NAME,
|
|
62
|
-
|
|
61
|
+
...toolFace("report-findings"),
|
|
63
62
|
description: "Report code-review findings as a typed list so the host UI can render them. Use this only when the " +
|
|
64
63
|
"active code-review instructions tell you to report findings with this tool; otherwise follow whatever " +
|
|
65
64
|
"output format those instructions specify. When reporting a review's results, call it once with the " +
|
|
66
65
|
"verified findings ranked most-severe first (empty array if nothing survived verification) and do not " +
|
|
67
66
|
"also print the findings as text. When re-reporting after applying fixes (only if the apply " +
|
|
68
67
|
"instructions ask for it), set `outcome` on each finding to what actually happened.",
|
|
69
|
-
effect: "read",
|
|
70
68
|
parameters: Type.Object({
|
|
71
69
|
level: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), Type.Literal("max")], { description: "Effort level the review ran at" })),
|
|
72
70
|
findings: Type.Array(REPORT_FINDING_SCHEMA, {
|
|
@@ -177,7 +175,7 @@ export function createSkillTool(skills, scope) {
|
|
|
177
175
|
const names = skills.map((s) => s.name).join(", ");
|
|
178
176
|
return defineTool({
|
|
179
177
|
name: SKILL_TOOL_NAME,
|
|
180
|
-
|
|
178
|
+
...toolFace("skill"),
|
|
181
179
|
description: `Invoke a skill.\n\n` +
|
|
182
180
|
`A skill is a packaged set of instructions the user or project has set up for a particular kind of task (deploy steps, a review checklist, a repo-specific workflow). ` +
|
|
183
181
|
`Available skills appear in a system-reminder listing with one-line descriptions. ` +
|
|
@@ -192,7 +190,6 @@ export function createSkillTool(skills, scope) {
|
|
|
192
190
|
skill: Type.String({ description: 'The skill name. E.g., "commit", "review-pr", or "pdf"' }),
|
|
193
191
|
args: Type.Optional(Type.String({ description: "Optional arguments for the skill" })),
|
|
194
192
|
}),
|
|
195
|
-
effect: "read",
|
|
196
193
|
execute: (rawArgs) => {
|
|
197
194
|
const a = (rawArgs ?? {});
|
|
198
195
|
const name = String(a.skill ?? "");
|
|
@@ -39,6 +39,9 @@ export interface DeferredToolInfo {
|
|
|
39
39
|
name: string;
|
|
40
40
|
hint: string;
|
|
41
41
|
description: string;
|
|
42
|
+
/** The real tool's aliases — a placeholder stands for the tool under EVERY name it answers to, so dispatch by
|
|
43
|
+
* alias resolves to the same slot before and after activation (and the roster's name→row resolution agrees). */
|
|
44
|
+
aliases?: readonly string[];
|
|
42
45
|
}
|
|
43
46
|
/** First line of a description, trimmed to a single short hint for a placeholder / announcement. */
|
|
44
47
|
export declare function deferHint(description: string, max?: number): string;
|
|
@@ -90,6 +93,7 @@ export declare function classifyDeferred(opts: {
|
|
|
90
93
|
export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, tools: ReadonlyArray<{
|
|
91
94
|
name: string;
|
|
92
95
|
description: string;
|
|
96
|
+
aliases?: readonly string[];
|
|
93
97
|
}>): Map<string, DeferredToolInfo>;
|
|
94
98
|
/**
|
|
95
99
|
* RB-403 — the direct-call lane a placeholder may carry. The upstream defer gate is SHAPE
|
|
@@ -3,6 +3,7 @@ import { Value } from "typebox/value";
|
|
|
3
3
|
import { defineTool, errorResult } from "../tools.js";
|
|
4
4
|
import { formatZodValidationError, truncateError } from "../tool-errors.js";
|
|
5
5
|
import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
|
|
6
|
+
import { toolFace } from "../tool-catalog-entries.js";
|
|
6
7
|
export const TOOL_SEARCH_NAME = "ToolSearch";
|
|
7
8
|
const DEFER_AUTO_FRACTION = 0.1;
|
|
8
9
|
const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
|
|
@@ -62,7 +63,7 @@ export function buildDeferredRegistry(deferred, tools) {
|
|
|
62
63
|
const reg = new Map();
|
|
63
64
|
for (const t of tools) {
|
|
64
65
|
if (deferred.has(t.name))
|
|
65
|
-
reg.set(t.name, { name: t.name, hint: deferHint(t.description), description: t.description });
|
|
66
|
+
reg.set(t.name, { name: t.name, hint: deferHint(t.description), description: t.description, ...(t.aliases !== undefined && t.aliases.length > 0 ? { aliases: [...t.aliases] } : {}) });
|
|
66
67
|
}
|
|
67
68
|
return reg;
|
|
68
69
|
}
|
|
@@ -127,6 +128,7 @@ export function createPlaceholderTool(info, direct) {
|
|
|
127
128
|
if (direct !== undefined) {
|
|
128
129
|
return {
|
|
129
130
|
name: info.name,
|
|
131
|
+
...(info.aliases !== undefined ? { aliases: [...info.aliases] } : {}),
|
|
130
132
|
label: info.name,
|
|
131
133
|
description: `${info.hint} — deferred: its parameters are not listed here. Call ` +
|
|
132
134
|
`${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load them; a call that already matches this ` +
|
|
@@ -156,6 +158,7 @@ export function createPlaceholderTool(info, direct) {
|
|
|
156
158
|
}
|
|
157
159
|
return defineTool({
|
|
158
160
|
name: info.name,
|
|
161
|
+
...(info.aliases !== undefined ? { aliases: [...info.aliases] } : {}),
|
|
159
162
|
description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
|
|
160
163
|
parameters: EMPTY_PARAMS,
|
|
161
164
|
effect: "read",
|
|
@@ -263,8 +266,10 @@ export function extractDiscoveredToolNames(messages, registry) {
|
|
|
263
266
|
const resolved = resolveToolSearch(part.arguments, registry);
|
|
264
267
|
pendingSearch.set(part.id, prior === undefined ? resolved : [...prior, ...resolved]);
|
|
265
268
|
}
|
|
266
|
-
else
|
|
267
|
-
|
|
269
|
+
else {
|
|
270
|
+
const canonical = registry.has(part.name) ? part.name : [...registry.values()].find((i) => i.aliases?.includes(part.name))?.name;
|
|
271
|
+
if (canonical !== undefined)
|
|
272
|
+
pendingDirect.set(part.id, canonical);
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
275
|
}
|
|
@@ -299,7 +304,7 @@ export function createToolSearchTool(opts) {
|
|
|
299
304
|
});
|
|
300
305
|
return defineTool({
|
|
301
306
|
name: TOOL_SEARCH_NAME,
|
|
302
|
-
|
|
307
|
+
...toolFace("tool-search"),
|
|
303
308
|
description: "Discover and activate deferred tools. " +
|
|
304
309
|
activationPosture +
|
|
305
310
|
"Query forms: " +
|
|
@@ -317,7 +322,6 @@ export function createToolSearchTool(opts) {
|
|
|
317
322
|
})),
|
|
318
323
|
max_results: Type.Optional(Type.Number({ description: "Maximum number of results to return (default: 5)" })),
|
|
319
324
|
}),
|
|
320
|
-
effect: "read",
|
|
321
325
|
execute: async (raw) => {
|
|
322
326
|
const staleSelect = (raw ?? {});
|
|
323
327
|
if (staleSelect.select !== undefined) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the run loop derives from a tool call's END, in every leg that closes one: the `tool_end` frame
|
|
3
|
+
* body (the ONE projection the live loop, the resumed batch and the reconcile-recovered orphan all go
|
|
4
|
+
* through), the `toolResult` transcript message the resume engine closes pending calls with, the thin
|
|
5
|
+
* `response` digest the post-tool-batch observer is handed, and the canonical write-tool → reminder-window
|
|
6
|
+
* family the attachment reducer keys on when a write tool ends.
|
|
7
|
+
*
|
|
8
|
+
* Projections over values the caller already holds, with ONE clock read: `toolResultMsg` stamps the message
|
|
9
|
+
* it mints with `Date.now()` (as it did inside the driver — the transcript's timestamp is minted where the
|
|
10
|
+
* message is). Nothing here reads host state through a callback, awaits, or keeps state between calls.
|
|
11
|
+
* Layer-1 machinery, not a driven lane; the prefix rule it follows is written once, on initial-run-state.ts.
|
|
12
|
+
*/
|
|
13
|
+
import { type AgentMessage } from "../../internal/harness.js";
|
|
14
|
+
import type { GateOutcome } from "../gate-outcome.js";
|
|
15
|
+
import { type McpDelivered } from "../mcp-failure.js";
|
|
16
|
+
import type { RecoveredOrphan } from "../session-reconcile.js";
|
|
17
|
+
import type { WriteFamily } from "./turn-attachments.js";
|
|
18
|
+
/** design/134 §3.1b — derive the thin `response` digest from a harness tool result: model-facing text
|
|
19
|
+
* content only (never `details` — the H4 thin-projection discipline), capped with a truncation note. */
|
|
20
|
+
export declare function batchResponseDigest(result: unknown): string | undefined;
|
|
21
|
+
/** The `tool_end` body fields projected from a harness tool result — output/truncated/totalChars via
|
|
22
|
+
* {@link toolOutputFrom} and the CC card via {@link structuredFrom}. Single construction point for BOTH
|
|
23
|
+
* the live loop's frames and the resumed batch's frames (`resolvePendingCall` + the deferred-sibling
|
|
24
|
+
* close): the resumed frames used to carry only `isError`, so a client rendering tool output from frames
|
|
25
|
+
* showed an empty body for every durable-approved call. Same projection = same source as the transcript.
|
|
26
|
+
* Also the one place the gate outcome reaches a frame — see the parameter. */
|
|
27
|
+
export declare function toolEndBodyFrom(result: unknown, isError: boolean,
|
|
28
|
+
/** The gate's record of the pass that admitted or refused this call, supplied by the CALLER of this
|
|
29
|
+
* projection — the live loop reads it off the gate's per-call sideband, the resumed leg off the decide's
|
|
30
|
+
* minted record. Deliberately a parameter and never derived from `result`: a tool's own `details`
|
|
31
|
+
* (which post-tool hooks may also replace) is writable by layers that adjudicate nothing, so reading
|
|
32
|
+
* provenance out of it would let a failing tool claim a person approved it. Omitted ⇒ the call never
|
|
33
|
+
* went through the gate (see the `tool_end.gate` doc, the one home). */
|
|
34
|
+
gate?: GateOutcome,
|
|
35
|
+
/** WHICH call this run's committed durable park is holding ({@link import("./park-commit.js").gatedCallIdOf}), for the frames the
|
|
36
|
+
* abort short-circuits. Same never-derived-from-`result` posture as the outcome above, and for the
|
|
37
|
+
* sharpest version of that reason: this one is an assertion about a DIFFERENT call, so a tool able to
|
|
38
|
+
* author it could point an approval UI at a call nobody is waiting on. Omitted ⇒ no park is holding a
|
|
39
|
+
* call (nothing parked, or the park that did holds none), and the frame then carries no id at all. */
|
|
40
|
+
gatedCallIdOfRun?: string): {
|
|
41
|
+
output?: unknown;
|
|
42
|
+
truncated?: boolean;
|
|
43
|
+
totalChars?: number;
|
|
44
|
+
structured?: unknown;
|
|
45
|
+
errorCode?: string;
|
|
46
|
+
delivered?: McpDelivered;
|
|
47
|
+
gatedCallId?: string;
|
|
48
|
+
gate?: GateOutcome;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* scan-1/A1 — the BODY of the synthetic `tool_end` that closes a reconcile-recovered orphan. ONE
|
|
52
|
+
* construction point for BOTH minting legs (the live-abort loop at the end of a run, and the wake/crash
|
|
53
|
+
* leg's replay at run open), so the two can never disagree about the shape of the same event.
|
|
54
|
+
*
|
|
55
|
+
* Both frames used to carry `isError:true` and NOTHING else: a consumer rendering tool output from the
|
|
56
|
+
* event stream showed an EMPTY body for every interrupted call, even though the persisted transcript
|
|
57
|
+
* (which the model reads) carried the full `[INTERRUPTED]` explanation — the two faces of one call
|
|
58
|
+
* disagreed. The projection goes through the same {@link toolEndBodyFrom} every live tool result uses, so
|
|
59
|
+
* `output` = the persisted model-facing text and `errorCode` = the persisted `details.errorKind`
|
|
60
|
+
* (`interrupted_never_started` / `interrupted_outcome_unknown`) — a consumer discriminates on the code
|
|
61
|
+
* instead of prose-matching. No `structured`: the reconcile mints no CC card (no `details.type`), which
|
|
62
|
+
* `structuredFrom`'s allowlist already enforces.
|
|
63
|
+
*/
|
|
64
|
+
export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">): ReturnType<typeof toolEndBodyFrom>;
|
|
65
|
+
/** task #51 F2/F3 — the CANONICAL write-tool → reminder-window mapping (CC qFm :480253-480254
|
|
66
|
+
* `a.name === bD || a.name === hF` = TaskCreate/TaskUpdate; BFm :480217-480219 = TodoWrite). Live
|
|
67
|
+
* deployment-alias resolution happens at the call sites (run-local `writeFamilyOf` over the mounted
|
|
68
|
+
* roster); retired-name normalization is gone (RB-476-A, 5.0.0). */
|
|
69
|
+
export declare function writeFamilyOfCanonical(name: string): WriteFamily | undefined;
|
|
70
|
+
/** A `toolResult` transcript message (text content). Used by the resume engine to close pending calls. */
|
|
71
|
+
export declare function toolResultMsg(toolCallId: string, toolName: string, text: string, isError: boolean): AgentMessage;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {} from "../../internal/harness.js";
|
|
2
|
+
import { MCP_DELIVERY_VERDICTS } from "../mcp-failure.js";
|
|
3
|
+
import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
|
|
4
|
+
import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
|
|
5
|
+
import { TASK_CREATE_TOOL_NAME, TASK_UPDATE_TOOL_NAME, TODO_WRITE_TOOL_NAME } from "../tool-catalog-entries.js";
|
|
6
|
+
const BATCH_RESPONSE_MAX_CHARS = 500;
|
|
7
|
+
export function batchResponseDigest(result) {
|
|
8
|
+
const content = result !== null && typeof result === "object" ? result.content : result;
|
|
9
|
+
if (content === undefined || content === null)
|
|
10
|
+
return undefined;
|
|
11
|
+
let text;
|
|
12
|
+
if (typeof content === "string") {
|
|
13
|
+
text = content;
|
|
14
|
+
}
|
|
15
|
+
else if (Array.isArray(content)) {
|
|
16
|
+
text = content
|
|
17
|
+
.map((c) => (c !== null && typeof c === "object" && typeof c.text === "string" ? c.text : ""))
|
|
18
|
+
.filter((t) => t.length > 0)
|
|
19
|
+
.join("\n");
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
try {
|
|
23
|
+
text = JSON.stringify(content) ?? "";
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (text.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
return text.length > BATCH_RESPONSE_MAX_CHARS
|
|
32
|
+
? `${text.slice(0, BATCH_RESPONSE_MAX_CHARS)}…[+${text.length - BATCH_RESPONSE_MAX_CHARS} chars truncated]`
|
|
33
|
+
: text;
|
|
34
|
+
}
|
|
35
|
+
export function toolEndBodyFrom(result, isError, gate, gatedCallIdOfRun) {
|
|
36
|
+
const o = toolOutputFrom(result);
|
|
37
|
+
const st = structuredFrom(result);
|
|
38
|
+
const det = isError ? result?.details : undefined;
|
|
39
|
+
const codeRaw = det?.code;
|
|
40
|
+
const kindRaw = det?.errorKind;
|
|
41
|
+
const code = typeof codeRaw === "string" ? codeRaw : typeof kindRaw === "string" ? kindRaw : undefined;
|
|
42
|
+
const deliveredRaw = det?.delivered;
|
|
43
|
+
const delivered = typeof deliveredRaw === "string" && MCP_DELIVERY_VERDICTS.includes(deliveredRaw) ? deliveredRaw : undefined;
|
|
44
|
+
const gatedCallId = code === "gate.parked" ? gatedCallIdOfRun : undefined;
|
|
45
|
+
return {
|
|
46
|
+
...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
|
|
47
|
+
...(st !== undefined ? { structured: st } : {}),
|
|
48
|
+
...(typeof code === "string" ? { errorCode: code } : {}),
|
|
49
|
+
...(delivered !== undefined ? { delivered } : {}),
|
|
50
|
+
...(gatedCallId !== undefined ? { gatedCallId } : {}),
|
|
51
|
+
...(gate !== undefined ? { gate } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function reconciledToolEndBody(orphan) {
|
|
55
|
+
return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true);
|
|
56
|
+
}
|
|
57
|
+
export function writeFamilyOfCanonical(name) {
|
|
58
|
+
if (name === TASK_CREATE_TOOL_NAME || name === TASK_UPDATE_TOOL_NAME)
|
|
59
|
+
return "task";
|
|
60
|
+
if (name === TODO_WRITE_TOOL_NAME)
|
|
61
|
+
return "todo";
|
|
62
|
+
if (name === TOOL_SEARCH_NAME)
|
|
63
|
+
return "tool_search";
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
export function toolResultMsg(toolCallId, toolName, text, isError) {
|
|
67
|
+
return {
|
|
68
|
+
role: "toolResult",
|
|
69
|
+
toolCallId,
|
|
70
|
+
toolName,
|
|
71
|
+
content: [{ type: "text", text }],
|
|
72
|
+
isError,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/388 §1.1 / §2.7 — the MCP `toolFaces` OVERLAY: a deployment's per-tool face declaration for a
|
|
3
|
+
* protocol tool that has no `ToolSpec` of its own (`McpServerSpec.toolFaces`, keyed by the server's RAW
|
|
4
|
+
* tool name like `allowTools` / `toolAxes`). Validated against the MATERIALIZED schema at every listing
|
|
5
|
+
* (the first mount and every RefreshMcpTools): `pathTarget.param` must be a top-level property; an
|
|
6
|
+
* `approvalCard` must find the keys the card needs; a `ruleFace` may only name scalar keys; `family`
|
|
7
|
+
* must be a member of the closed set. A failing overlay does NOT mount — the tool mounts BARE (no
|
|
8
|
+
* face) and the leg announces `config.tool_face_invalid` once per tool; a `toolFaces` key naming a
|
|
9
|
+
* tool the listing does not carry announces `config.tool_face_undeclared` once per name. Never a
|
|
10
|
+
* silent partial application: an overlay is one declaration, applied whole or not at all.
|
|
11
|
+
*
|
|
12
|
+
* Prepare-path machinery (layer 1): pure over the tool objects and the server specs; the notice sink
|
|
13
|
+
* is a parameter.
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentTool } from "../../internal/harness-types.js";
|
|
16
|
+
import type { McpServerSpec } from "../mcp-server-spec.js";
|
|
17
|
+
import { type ToolApprovalCard } from "../tool-face.js";
|
|
18
|
+
import { type EngineNotice } from "../engine-notice.js";
|
|
19
|
+
/** The keys each specialised approval card needs on the call (design/388 §2.5 boundary ①: the SHELL owns the
|
|
20
|
+
* card's full schema; this is the pre-filter the overlay is validated against). */
|
|
21
|
+
export declare const APPROVAL_CARD_REQUIRED_KEYS: Readonly<Record<ToolApprovalCard, readonly string[]>>;
|
|
22
|
+
export type McpToolFace = NonNullable<McpServerSpec["toolFaces"]>[string];
|
|
23
|
+
/** Why an overlay is refused — a closed reason list, each a distinct declaration mistake. */
|
|
24
|
+
export declare function toolFaceProblem(face: McpToolFace, schema: AgentTool["parameters"]): string | undefined;
|
|
25
|
+
export interface ApplyToolFacesInput {
|
|
26
|
+
/** The materialized tools of ONE server (namespaced names). */
|
|
27
|
+
tools: readonly AgentTool[];
|
|
28
|
+
server: Pick<McpServerSpec, "name" | "toolFaces">;
|
|
29
|
+
/** The leg's notice sink and its once-per-leg ledger (keys: `invalid:<name>` / `undeclared:<server>/<raw>`). */
|
|
30
|
+
onNotice: ((notice: EngineNotice) => void) | undefined;
|
|
31
|
+
announced: Set<string>;
|
|
32
|
+
sessionId: string;
|
|
33
|
+
runId: string;
|
|
34
|
+
}
|
|
35
|
+
/** Apply the server's overlay to its tools: a validated face is stamped on a COPY of the tool object (the same
|
|
36
|
+
* untyped carry `defineTool` uses, so the roster reads it off the instance). A face that does NOT fit the materialized
|
|
37
|
+
* schema is a bad declaration: the tool is NOT mounted on this leg (announced once, `config.tool_face_invalid`) —
|
|
38
|
+
* mounting it bare would silently drop the write protection the operator declared for it (an MCP name has no catalog
|
|
39
|
+
* floor), and a schema refresh that invalidates only a display hint must fail loud, never open. */
|
|
40
|
+
export declare function applyMcpToolFaces(input: ApplyToolFacesInput): AgentTool[];
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { MCP_NAMESPACE } from "../protocol-table.js";
|
|
2
|
+
import { mintNamespacePrefix } from "../protocol-naming.js";
|
|
3
|
+
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS } from "../tool-face.js";
|
|
4
|
+
import { inputKeysOf } from "../tool-roster.js";
|
|
5
|
+
import { deliverEngineNotice } from "../engine-notice.js";
|
|
6
|
+
export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({
|
|
7
|
+
"file-edit": ["file_path"],
|
|
8
|
+
"file-write": ["file_path"],
|
|
9
|
+
"notebook-edit": ["notebook_path"],
|
|
10
|
+
shell: ["command"],
|
|
11
|
+
fetch: ["url"],
|
|
12
|
+
plan: [],
|
|
13
|
+
question: [],
|
|
14
|
+
generic: [],
|
|
15
|
+
});
|
|
16
|
+
export function toolFaceProblem(face, schema) {
|
|
17
|
+
const { keys, scalar } = inputKeysOf(schema);
|
|
18
|
+
const top = new Set(keys.map((k) => k.name));
|
|
19
|
+
if (face.family !== undefined && !TOOL_FAMILIES.includes(face.family))
|
|
20
|
+
return `family ${JSON.stringify(face.family)} is outside the closed set`;
|
|
21
|
+
const overlong = (label, values) => {
|
|
22
|
+
const k = values.find((v) => v.length > TOOL_KEY_MAX_CHARS);
|
|
23
|
+
return k === undefined ? undefined : `${label} ${JSON.stringify(k.slice(0, 40) + "…")} is ${k.length} characters; a roster key is at most ${TOOL_KEY_MAX_CHARS}`;
|
|
24
|
+
};
|
|
25
|
+
const vocabulary = new Set(scalar);
|
|
26
|
+
if (face.pathTarget !== undefined) {
|
|
27
|
+
if (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param))
|
|
28
|
+
return `pathTarget.param ${JSON.stringify(face.pathTarget.param)} is not a top-level property of the tool's schema`;
|
|
29
|
+
if (!["read", "create", "edit"].includes(face.pathTarget.access))
|
|
30
|
+
return `pathTarget.access ${JSON.stringify(face.pathTarget.access)} is outside the closed set`;
|
|
31
|
+
const long = overlong("pathTarget.param", [face.pathTarget.param]) ?? overlong("pathTarget.aliases", face.pathTarget.aliases ?? []);
|
|
32
|
+
if (long !== undefined)
|
|
33
|
+
return long;
|
|
34
|
+
vocabulary.add(face.pathTarget.param);
|
|
35
|
+
for (const a of face.pathTarget.aliases ?? [])
|
|
36
|
+
vocabulary.add(a);
|
|
37
|
+
}
|
|
38
|
+
const hints = face.renderHints;
|
|
39
|
+
if (hints !== undefined) {
|
|
40
|
+
const card = hints.approvalCard;
|
|
41
|
+
if (card !== undefined) {
|
|
42
|
+
if (!TOOL_APPROVAL_CARDS.includes(card))
|
|
43
|
+
return `renderHints.approvalCard ${JSON.stringify(card)} is outside the closed set`;
|
|
44
|
+
const missing = APPROVAL_CARD_REQUIRED_KEYS[card].filter((k) => !top.has(k));
|
|
45
|
+
if (missing.length > 0)
|
|
46
|
+
return `renderHints.approvalCard ${JSON.stringify(card)} needs ${missing.join(", ")} on the tool's schema`;
|
|
47
|
+
}
|
|
48
|
+
if (hints.userFacingName !== undefined && hints.userFacingName.length > RENDER_HINT_MAX_CHARS)
|
|
49
|
+
return `renderHints.userFacingName is ${hints.userFacingName.length} characters; the bound is ${RENDER_HINT_MAX_CHARS}`;
|
|
50
|
+
if (hints.activity !== undefined && hints.activity.length > RENDER_HINT_ACTIVITY_MAX_CHARS)
|
|
51
|
+
return `renderHints.activity is ${hints.activity.length} characters; the bound is ${RENDER_HINT_ACTIVITY_MAX_CHARS}`;
|
|
52
|
+
if ((hints.summaryParams?.length ?? 0) > RENDER_HINT_MAX_LIST)
|
|
53
|
+
return `renderHints.summaryParams lists ${hints.summaryParams.length} entries; the bound is ${RENDER_HINT_MAX_LIST}`;
|
|
54
|
+
if ((hints.resultCards?.length ?? 0) > RENDER_HINT_MAX_LIST)
|
|
55
|
+
return `renderHints.resultCards lists ${hints.resultCards.length} entries; the bound is ${RENDER_HINT_MAX_LIST}`;
|
|
56
|
+
const long = overlong("renderHints.summaryParams", hints.summaryParams ?? []) ?? overlong("renderHints.resultCards", hints.resultCards ?? []);
|
|
57
|
+
if (long !== undefined)
|
|
58
|
+
return long;
|
|
59
|
+
const stray = (hints.summaryParams ?? []).find((p) => !top.has(p));
|
|
60
|
+
if (stray !== undefined)
|
|
61
|
+
return `renderHints.summaryParams names ${JSON.stringify(stray)}, which is not a top-level property`;
|
|
62
|
+
}
|
|
63
|
+
for (const p of face.ruleFace?.params ?? [])
|
|
64
|
+
if (!vocabulary.has(p))
|
|
65
|
+
return `ruleFace.params names ${JSON.stringify(p)}, which is neither a scalar top-level property nor the declared path target`;
|
|
66
|
+
for (const p of face.ruleFace?.primaryParams ?? [])
|
|
67
|
+
if (!vocabulary.has(p))
|
|
68
|
+
return `ruleFace.primaryParams names ${JSON.stringify(p)}, which is neither a top-level property nor the declared path target`;
|
|
69
|
+
const long = overlong("ruleFace.params", face.ruleFace?.params ?? []) ?? overlong("ruleFace.primaryParams", face.ruleFace?.primaryParams ?? []);
|
|
70
|
+
if (long !== undefined)
|
|
71
|
+
return long;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
export function applyMcpToolFaces(input) {
|
|
75
|
+
const faces = input.server.toolFaces;
|
|
76
|
+
if (faces === undefined)
|
|
77
|
+
return [...input.tools];
|
|
78
|
+
const prefix = mintNamespacePrefix(MCP_NAMESPACE, input.server.name);
|
|
79
|
+
const accepted = new Set(input.tools.filter((t) => t.name.startsWith(prefix)).map((t) => t.mcpRemoteName).filter((r) => typeof r === "string"));
|
|
80
|
+
for (const raw of Object.keys(faces)) {
|
|
81
|
+
if (accepted.has(raw))
|
|
82
|
+
continue;
|
|
83
|
+
const key = `undeclared:${input.server.name}/${raw}`;
|
|
84
|
+
if (input.announced.has(key))
|
|
85
|
+
continue;
|
|
86
|
+
input.announced.add(key);
|
|
87
|
+
deliverEngineNotice(input.onNotice, {
|
|
88
|
+
code: "config.tool_face_undeclared",
|
|
89
|
+
message: `MCP server "${input.server.name}": toolFaces declares a face for "${raw}", which the server's tool listing does not carry — the declaration is inert`,
|
|
90
|
+
detail: { server: input.server.name, tool: raw, sessionId: input.sessionId, runId: input.runId },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return input.tools.flatMap((t) => {
|
|
94
|
+
const stamped = stampOne(t);
|
|
95
|
+
return stamped === undefined ? [] : [stamped];
|
|
96
|
+
});
|
|
97
|
+
function stampOne(t) {
|
|
98
|
+
const raw = t.mcpRemoteName;
|
|
99
|
+
const face = raw !== undefined && t.name.startsWith(prefix) ? faces?.[raw] : undefined;
|
|
100
|
+
if (raw === undefined || face === undefined)
|
|
101
|
+
return t;
|
|
102
|
+
const hit = { raw, face };
|
|
103
|
+
const problem = toolFaceProblem(hit.face, t.parameters);
|
|
104
|
+
if (problem !== undefined) {
|
|
105
|
+
const key = `invalid:${t.name}`;
|
|
106
|
+
if (!input.announced.has(key)) {
|
|
107
|
+
input.announced.add(key);
|
|
108
|
+
deliverEngineNotice(input.onNotice, {
|
|
109
|
+
code: "config.tool_face_invalid",
|
|
110
|
+
message: `MCP server "${input.server.name}": the toolFaces overlay for "${hit.raw}" does not fit the tool's materialized schema (${problem}) — the tool is NOT mounted until the overlay fits (mounting it bare would drop the protection the overlay declares)`,
|
|
111
|
+
detail: { server: input.server.name, tool: hit.raw, problem, sessionId: input.sessionId, runId: input.runId },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const f = hit.face;
|
|
117
|
+
return {
|
|
118
|
+
...t,
|
|
119
|
+
...(f.family !== undefined ? { family: f.family } : {}),
|
|
120
|
+
...(f.pathTarget !== undefined ? { pathTarget: f.pathTarget } : {}),
|
|
121
|
+
...(f.renderHints !== undefined ? { renderHints: f.renderHints } : {}),
|
|
122
|
+
...(f.ruleFace !== undefined ? { ruleFace: f.ruleFace } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -9,4 +9,14 @@ export declare const toolOutputFrom: (result: unknown) => {
|
|
|
9
9
|
truncated: boolean;
|
|
10
10
|
totalChars?: number;
|
|
11
11
|
} | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* L4's run-time half (design/388 §2.4): a per-leg projector — the engine card set plus the leg's declared
|
|
14
|
+
* caller cards — that DROPS an undeclared `details.type` exactly as the module-level gate does and announces
|
|
15
|
+
* it ONCE per leg (`config.tool_card_undeclared`, audience operator). The module-level {@link structuredFrom}
|
|
16
|
+
* is this projector over the engine set alone, with no announce seat.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createStructuredProjector(opts: {
|
|
19
|
+
declaredCards?: Iterable<string>;
|
|
20
|
+
onUndeclared?: (type: string) => void;
|
|
21
|
+
}): (result: unknown) => unknown;
|
|
12
22
|
export declare const structuredFrom: (result: unknown) => unknown;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { engineCardTypes } from "../tool-registry.js";
|
|
1
2
|
const MAX_TOOL_END_OUTPUT_CHARS = 16384;
|
|
2
3
|
const safeJsonLength = (v) => {
|
|
3
4
|
try {
|
|
@@ -56,16 +57,38 @@ export const toolOutputFrom = (result) => {
|
|
|
56
57
|
}
|
|
57
58
|
return { output: raw, truncated: true, totalChars };
|
|
58
59
|
};
|
|
59
|
-
const CC_DETAIL_TYPES =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
const CC_DETAIL_TYPES = engineCardTypes();
|
|
61
|
+
export function createStructuredProjector(opts) {
|
|
62
|
+
const allowed = new Set([...CC_DETAIL_TYPES, ...(opts.declaredCards ?? [])]);
|
|
63
|
+
const announced = new Set();
|
|
64
|
+
return (result) => {
|
|
65
|
+
const details = result !== null && typeof result === "object" ? result.details : undefined;
|
|
66
|
+
if (details === null || typeof details !== "object")
|
|
67
|
+
return undefined;
|
|
68
|
+
const t = details.type;
|
|
69
|
+
if (typeof t !== "string")
|
|
70
|
+
return undefined;
|
|
71
|
+
if (!allowed.has(t)) {
|
|
72
|
+
if (opts.onUndeclared !== undefined && !announced.has(t)) {
|
|
73
|
+
announced.add(t);
|
|
74
|
+
opts.onUndeclared(t);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return boundedCard(details);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function boundedCard(details) {
|
|
82
|
+
try {
|
|
83
|
+
const serialized = JSON.stringify(details) ?? "";
|
|
84
|
+
if (serialized.length > MAX_TOOL_END_OUTPUT_CHARS * 4)
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return details;
|
|
91
|
+
}
|
|
69
92
|
export const structuredFrom = (result) => {
|
|
70
93
|
const details = result !== null && typeof result === "object" ? result.details : undefined;
|
|
71
94
|
if (details === null || typeof details !== "object")
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../internal/harness-types.js";
|
|
2
2
|
import type { ToolPolicy } from "./tool-policy.js";
|
|
3
|
+
/** fs hand tools that take a `path` and write — the reliably-guardable surface. NotebookEdit joined
|
|
4
|
+
* in [886]①a (mirror of the fs-write-gate's covered set): its target is `notebook_path`, and a
|
|
5
|
+
* guard that ignored notebook writes to the same paths by default was the same hole one tool away. */
|
|
3
6
|
/**
|
|
4
7
|
* RECOMMENDED default pattern set for {@link createSensitivePathPolicy} (additive export).
|
|
5
8
|
* NO upstream verbatim anchor exists (cc-anchors/2.1.209 carries no sensitive/dangerous file-pattern
|
|
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { canonicalizeTarget, expandHomeTilde, isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
4
4
|
import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
|
|
5
|
-
|
|
5
|
+
import { isProtectedWrite } from "./tool-registry.js";
|
|
6
6
|
export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
7
7
|
".env",
|
|
8
8
|
".env.*",
|
|
@@ -90,13 +90,14 @@ export function createSensitivePathPolicy(opts) {
|
|
|
90
90
|
}
|
|
91
91
|
return keys;
|
|
92
92
|
};
|
|
93
|
-
const
|
|
93
|
+
const guardedByName = opts.tools !== undefined ? new Set(opts.tools) : undefined;
|
|
94
94
|
return {
|
|
95
95
|
async check(req, signal) {
|
|
96
|
-
|
|
97
|
-
if (compiled.length === 0 || !guarded.has(canonical))
|
|
96
|
+
if (compiled.length === 0)
|
|
98
97
|
return { action: "allow" };
|
|
99
|
-
|
|
98
|
+
if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
|
|
99
|
+
return { action: "allow" };
|
|
100
|
+
const path = writeTargetPath(req);
|
|
100
101
|
if (typeof path !== "string" || path.length === 0)
|
|
101
102
|
return { action: "allow" };
|
|
102
103
|
const canon = await canonicalizeTarget(opts.env, path, signal, req.cwd ?? opts.rootPath);
|