@sammorrowdrums/mcpi-ext 0.2.0 → 1.0.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/LICENSE +21 -0
- package/README.md +137 -47
- package/dist/code-mode/eligibility.d.ts +35 -0
- package/dist/code-mode/eligibility.js +64 -0
- package/dist/code-mode/executor.d.ts +53 -0
- package/dist/code-mode/executor.js +185 -0
- package/dist/code-mode/index.d.ts +107 -0
- package/dist/code-mode/index.js +267 -0
- package/dist/code-mode/isolated-vm.d.ts +65 -0
- package/dist/code-mode/isolated-vm.js +116 -0
- package/dist/code-mode/tools.d.ts +48 -0
- package/dist/code-mode/tools.js +73 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.js +205 -0
- package/dist/docker-e2e.d.ts +54 -0
- package/dist/docker-e2e.js +234 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +227 -0
- package/dist/mcp/call-tool-result.d.ts +10 -0
- package/dist/mcp/call-tool-result.js +70 -0
- package/dist/mcp/client-factory.d.ts +59 -0
- package/dist/mcp/client-factory.js +76 -0
- package/dist/mcp/client-manager.d.ts +88 -0
- package/dist/mcp/client-manager.js +307 -0
- package/dist/mcp/config-loader.d.ts +7 -0
- package/dist/mcp/config-loader.js +42 -0
- package/dist/mcp/config.d.ts +56 -0
- package/dist/mcp/config.js +42 -0
- package/dist/mcp/gateway-defaults.d.ts +14 -0
- package/dist/mcp/gateway-defaults.js +21 -0
- package/dist/mcp/host-approval.d.ts +16 -0
- package/dist/mcp/host-approval.js +19 -0
- package/dist/mcp/host-elicitation.d.ts +19 -0
- package/dist/mcp/host-elicitation.js +94 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.js +7 -0
- package/dist/mcp/policy.d.ts +314 -0
- package/dist/mcp/policy.js +848 -0
- package/dist/routing/facilities.d.ts +109 -0
- package/dist/routing/facilities.js +175 -0
- package/dist/routing/format.d.ts +24 -0
- package/dist/routing/format.js +64 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/seam.d.ts +35 -0
- package/dist/routing/seam.js +28 -0
- package/dist/routing/tripwire.d.ts +42 -0
- package/dist/routing/tripwire.js +80 -0
- package/dist/skills/discover.d.ts +12 -0
- package/dist/skills/discover.js +85 -0
- package/dist/skills/format.d.ts +13 -0
- package/dist/skills/format.js +42 -0
- package/dist/skills/index.d.ts +6 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/load-skill-tool.d.ts +56 -0
- package/dist/skills/load-skill-tool.js +183 -0
- package/dist/skills/mcp-tool-proxy.d.ts +7 -0
- package/dist/skills/mcp-tool-proxy.js +41 -0
- package/dist/skills/sep2640/client.d.ts +91 -0
- package/dist/skills/sep2640/client.js +160 -0
- package/dist/skills/sep2640/discover.d.ts +33 -0
- package/dist/skills/sep2640/discover.js +82 -0
- package/dist/skills/sep2640/index.d.ts +13 -0
- package/dist/skills/sep2640/index.js +13 -0
- package/dist/skills/sep2640/integrity.d.ts +58 -0
- package/dist/skills/sep2640/integrity.js +185 -0
- package/dist/skills/sep2640/load.d.ts +61 -0
- package/dist/skills/sep2640/load.js +90 -0
- package/dist/skills/sep2640/protocol.d.ts +146 -0
- package/dist/skills/sep2640/protocol.js +214 -0
- package/dist/skills/sep2640/spec.d.ts +76 -0
- package/dist/skills/sep2640/spec.js +88 -0
- package/dist/skills/skill-registry.d.ts +85 -0
- package/dist/skills/skill-registry.js +85 -0
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +20 -0
- package/dist/tool-cli/format.js +73 -0
- package/dist/tool-cli/index.d.ts +7 -0
- package/dist/tool-cli/index.js +5 -0
- package/dist/tool-cli/provider.d.ts +21 -0
- package/dist/tool-cli/provider.js +114 -0
- package/dist/tool-cli/startup.d.ts +22 -0
- package/dist/tool-cli/startup.js +47 -0
- package/package.json +78 -26
- package/.dockerignore +0 -6
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/ci.yml +0 -50
- package/.hadolint.yaml +0 -6
- package/.prettierignore +0 -4
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -118
- package/DECISIONS.md +0 -99
- package/Dockerfile +0 -39
- package/aube-lock.yaml +0 -4146
- package/docs/code-mode.md +0 -38
- package/docs/skills.md +0 -57
- package/docs/tool-cli.md +0 -54
- package/eslint.config.js +0 -27
- package/images/banner.webp +0 -0
- package/images/code-c-maude.webp +0 -0
- package/images/code-mode-histogram.png +0 -0
- package/images/nuclear-mcp-football.webp +0 -0
- package/images/skills-enabling-mcp-tools.png +0 -0
- package/images/the-skill-dealer.webp +0 -0
- package/images/tool-cli-grep.png +0 -0
- package/mise.toml +0 -53
- package/scripts/docker-entrypoint.sh +0 -29
- package/scripts/test-echo-server.mjs +0 -28
- package/scripts/test-mcp-integration.mjs +0 -59
- package/scripts/test-tool-cli-smoke.mjs +0 -93
- package/src/code-mode/code-mode-integration.test.ts +0 -135
- package/src/code-mode/eligibility.test.ts +0 -60
- package/src/code-mode/eligibility.ts +0 -16
- package/src/code-mode/executor.test.ts +0 -168
- package/src/code-mode/executor.ts +0 -163
- package/src/code-mode/index.ts +0 -208
- package/src/code-mode/tools.ts +0 -110
- package/src/code-mode/type-hints.test.ts +0 -187
- package/src/code-mode/type-hints.ts +0 -249
- package/src/docker-e2e.test.ts +0 -195
- package/src/docker-e2e.ts +0 -277
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -179
- package/src/mcp/client-manager.test.ts +0 -250
- package/src/mcp/client-manager.ts +0 -199
- package/src/mcp/config-loader.test.ts +0 -115
- package/src/mcp/config-loader.ts +0 -46
- package/src/mcp/config.ts +0 -40
- package/src/mcp/index.ts +0 -4
- package/src/skills/discover.ts +0 -81
- package/src/skills/format.test.ts +0 -70
- package/src/skills/format.ts +0 -42
- package/src/skills/index.ts +0 -9
- package/src/skills/load-skill-tool.ts +0 -136
- package/src/skills/mcp-tool-proxy.ts +0 -143
- package/src/skills/skill-integration.test.ts +0 -139
- package/src/skills/skill-registry.test.ts +0 -83
- package/src/skills/skill-registry.ts +0 -69
- package/src/test-servers/weather-server.ts +0 -171
- package/src/test-servers/weather-stdio.ts +0 -11
- package/src/tool-cli/format.test.ts +0 -33
- package/src/tool-cli/format.ts +0 -56
- package/src/tool-cli/index.ts +0 -3
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { loadIsolatedVm } from "./isolated-vm.js";
|
|
2
|
+
import { sanitizeToolName } from "./type-hints.js";
|
|
3
|
+
export class CodeModeDispatchError extends Error {
|
|
4
|
+
details;
|
|
5
|
+
constructor(details) {
|
|
6
|
+
super(details.message);
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "CodeModeDispatchError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_MEMORY_LIMIT = 128;
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
|
+
const STRUCTURED_ERROR_PREFIX = "__CODE_MODE_ERROR__";
|
|
14
|
+
/** Error code surfaced when the V8 isolate backend is not installed. */
|
|
15
|
+
export const SANDBOX_UNAVAILABLE_ERROR = "sandbox_unavailable";
|
|
16
|
+
/**
|
|
17
|
+
* Execute model-generated JavaScript code in an isolated V8 sandbox.
|
|
18
|
+
*
|
|
19
|
+
* The code runs in a fresh `isolated-vm` isolate with:
|
|
20
|
+
* - Enforced memory limit (default 128MB)
|
|
21
|
+
* - Execution timeout (default 30s)
|
|
22
|
+
* - No access to Node.js APIs, filesystem, or network
|
|
23
|
+
* - Only access to provided tool dispatch functions via `codemode.*` proxy
|
|
24
|
+
*
|
|
25
|
+
* Tool calls are dispatched to the host via `Reference` callbacks —
|
|
26
|
+
* actual MCP tool execution happens outside the sandbox.
|
|
27
|
+
*
|
|
28
|
+
* The `isolated-vm` addon is optional and loaded lazily. If it is unavailable
|
|
29
|
+
* this returns a structured `sandbox_unavailable` error rather than falling
|
|
30
|
+
* back to Node's `vm` module: `node:vm` shares the host realm and heap, so
|
|
31
|
+
* using it here would silently void the isolation guarantee this API makes.
|
|
32
|
+
*/
|
|
33
|
+
export async function executeInSandbox(code, toolNames, dispatch, options = {}) {
|
|
34
|
+
const memoryLimit = options.memoryLimit ?? DEFAULT_MEMORY_LIMIT;
|
|
35
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
36
|
+
const load = await loadIsolatedVm();
|
|
37
|
+
if (!load.available) {
|
|
38
|
+
const message = `Code Mode is unavailable: ${load.reason}. Use tool-cli or the MCP tool proxies instead.`;
|
|
39
|
+
return {
|
|
40
|
+
result: undefined,
|
|
41
|
+
error: message,
|
|
42
|
+
errorDetails: {
|
|
43
|
+
error: SANDBOX_UNAVAILABLE_ERROR,
|
|
44
|
+
message,
|
|
45
|
+
reason: load.reason,
|
|
46
|
+
alternatives: ["tool-cli", "MCP tool proxies"],
|
|
47
|
+
},
|
|
48
|
+
logs: [],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const ivm = load.module;
|
|
52
|
+
const isolate = new ivm.Isolate({ memoryLimit });
|
|
53
|
+
try {
|
|
54
|
+
return await runInIsolate(ivm, isolate, code, toolNames, dispatch, timeoutMs);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
isolate.dispose();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function runInIsolate(ivm, isolate, code, toolNames, dispatch, timeoutMs) {
|
|
61
|
+
const ctx = await isolate.createContext();
|
|
62
|
+
const jail = ctx.global;
|
|
63
|
+
const logs = [];
|
|
64
|
+
// Inject console.log that captures to logs array
|
|
65
|
+
const logCallback = new ivm.Callback((...args) => {
|
|
66
|
+
logs.push(args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
67
|
+
});
|
|
68
|
+
await jail.set("__log", logCallback);
|
|
69
|
+
// Inject tool dispatcher Reference (async callback)
|
|
70
|
+
const dispatchRef = new ivm.Reference(async (toolName, argsJson) => {
|
|
71
|
+
const args = JSON.parse(argsJson);
|
|
72
|
+
try {
|
|
73
|
+
const result = await dispatch(toolName, args);
|
|
74
|
+
return JSON.stringify({ ok: true, value: result === undefined ? null : result });
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof CodeModeDispatchError) {
|
|
78
|
+
return JSON.stringify({ ok: false, error: error.details });
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
await jail.set("__dispatch", dispatchRef);
|
|
84
|
+
// Build the tool call helper and codemode proxy as setup code
|
|
85
|
+
const toolProxyEntries = toolNames
|
|
86
|
+
.map((name) => {
|
|
87
|
+
const safe = sanitizeToolName(name);
|
|
88
|
+
return ` ${safe}: async (args) => __callTool(${JSON.stringify(name)}, args)`;
|
|
89
|
+
})
|
|
90
|
+
.join(",\n");
|
|
91
|
+
const normalized = normalizeCode(code);
|
|
92
|
+
// Use ctx.eval with { promise: true } instead of compileModule,
|
|
93
|
+
// because module.evaluate() can resolve prematurely with multiple
|
|
94
|
+
// sequential async Reference.apply calls.
|
|
95
|
+
const wrappedCode = `
|
|
96
|
+
(async () => {
|
|
97
|
+
const console = { log: (...args) => __log(args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')) };
|
|
98
|
+
const __callTool = async (name, args) => {
|
|
99
|
+
const r = await __dispatch.apply(undefined, [name, JSON.stringify(args ?? {})], { arguments: { copy: true }, result: { promise: true, copy: true } });
|
|
100
|
+
const response = JSON.parse(r);
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(${JSON.stringify(STRUCTURED_ERROR_PREFIX)} + JSON.stringify(response.error));
|
|
103
|
+
}
|
|
104
|
+
return response.value;
|
|
105
|
+
};
|
|
106
|
+
const codemode = {
|
|
107
|
+
listTools: async () => ${JSON.stringify(toolNames)},
|
|
108
|
+
describeTools: async (names) => "Use the typed codemode.toolName(args) methods instead.",
|
|
109
|
+
${toolProxyEntries}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const __userFn = async () => {
|
|
113
|
+
${normalized}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return JSON.stringify({ value: await __userFn() });
|
|
117
|
+
})()
|
|
118
|
+
`;
|
|
119
|
+
try {
|
|
120
|
+
const rawResult = (await ctx.eval(wrappedCode, {
|
|
121
|
+
promise: true,
|
|
122
|
+
copy: true,
|
|
123
|
+
timeout: timeoutMs,
|
|
124
|
+
}));
|
|
125
|
+
const parsed = JSON.parse(rawResult);
|
|
126
|
+
return { result: parsed.value, logs };
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
const errorDetails = parseStructuredError(message);
|
|
131
|
+
return {
|
|
132
|
+
result: undefined,
|
|
133
|
+
error: errorDetails?.message ?? message,
|
|
134
|
+
errorDetails,
|
|
135
|
+
logs,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Normalize model-generated code:
|
|
141
|
+
* - Strip markdown code fences
|
|
142
|
+
* - Handle arrow functions, function declarations, export default
|
|
143
|
+
* - Otherwise leave code as a statement block; callers must explicitly `return` a value
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeCode(code) {
|
|
146
|
+
let normalized = code.trim();
|
|
147
|
+
// Strip markdown code fences
|
|
148
|
+
normalized = normalized.replace(/^```(?:js|javascript|typescript|ts)?\s*\n?/i, "");
|
|
149
|
+
normalized = normalized.replace(/\n?```\s*$/i, "");
|
|
150
|
+
normalized = normalized.trim();
|
|
151
|
+
// If it's an arrow function or function expression, invoke it
|
|
152
|
+
if (/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(normalized)) {
|
|
153
|
+
return `return (${normalized})();`;
|
|
154
|
+
}
|
|
155
|
+
// If it's a function declaration, invoke it
|
|
156
|
+
if (/^(?:async\s+)?function\s+/.test(normalized)) {
|
|
157
|
+
const match = normalized.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
158
|
+
if (match) {
|
|
159
|
+
return `${normalized}\nreturn ${match[1]}();`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// If it starts with export default, strip the export default
|
|
163
|
+
if (/^export\s+default\s+/.test(normalized)) {
|
|
164
|
+
normalized = normalized.replace(/^export\s+default\s+/, "");
|
|
165
|
+
return `return (${normalized})();`;
|
|
166
|
+
}
|
|
167
|
+
// Otherwise, treat as a code block — return the last expression
|
|
168
|
+
return normalized;
|
|
169
|
+
}
|
|
170
|
+
function parseStructuredError(message) {
|
|
171
|
+
const markerIndex = message.indexOf(STRUCTURED_ERROR_PREFIX);
|
|
172
|
+
if (markerIndex === -1)
|
|
173
|
+
return undefined;
|
|
174
|
+
const serialized = message.slice(markerIndex + STRUCTURED_ERROR_PREFIX.length);
|
|
175
|
+
try {
|
|
176
|
+
const details = JSON.parse(serialized);
|
|
177
|
+
if (typeof details.error !== "string" || typeof details.message !== "string") {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
return details;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { McpClientManager, McpTool } from "../mcp/index.js";
|
|
2
|
+
import { type McpPolicy } from "../mcp/policy.js";
|
|
3
|
+
import { type CodeModeDiagnostics, type CodeModeTool } from "./eligibility.js";
|
|
4
|
+
import type { ExecuteResult, ExecutorOptions } from "./executor.js";
|
|
5
|
+
import { executeInSandbox } from "./executor.js";
|
|
6
|
+
export { SYNTHESIZED_OUTPUT_SCHEMA, getCodeModeDiagnostics, getCodeModeTools, getEligibleTools, isEligibleForCodeMode, toCodeModeTool, } from "./eligibility.js";
|
|
7
|
+
export type { CodeModeDiagnostics, CodeModeRefusalReason, CodeModeTool, OutputSchemaProvenance, } from "./eligibility.js";
|
|
8
|
+
export type { CodeModeErrorDetails, ExecuteResult, ExecutorOptions } from "./executor.js";
|
|
9
|
+
export { SANDBOX_UNAVAILABLE_ERROR, executeInSandbox, normalizeCode } from "./executor.js";
|
|
10
|
+
export { loadIsolatedVm, peekIsolatedVm, resetIsolatedVmCacheForTests, setIsolatedVmForTests, type IsolatedVmLoad, type IsolatedVmModule, } from "./isolated-vm.js";
|
|
11
|
+
export { createCodeExecuteTool, createCodeSearchTool } from "./tools.js";
|
|
12
|
+
export { generateTypeHints, jsonSchemaToTypeString, sanitizeToolName } from "./type-hints.js";
|
|
13
|
+
export interface CodeModeManagerOptions extends ExecutorOptions {
|
|
14
|
+
/** Log function for status messages. */
|
|
15
|
+
log?: (msg: string) => void;
|
|
16
|
+
/** Test seam for proving pre-isolate refusals. */
|
|
17
|
+
sandboxExecutor?: typeof executeInSandbox;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether the sandbox backend can run code.
|
|
21
|
+
*
|
|
22
|
+
* `unknown` is a real state, not a synonym for unavailable: before the optional
|
|
23
|
+
* native addon has been probed we have not established anything, and reporting
|
|
24
|
+
* that honestly is better than guessing in either direction.
|
|
25
|
+
*/
|
|
26
|
+
export interface SandboxAvailability {
|
|
27
|
+
readonly state: "available" | "unavailable" | "unknown";
|
|
28
|
+
readonly reason: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Orchestrates code mode: catalogs tools, generates type hints,
|
|
32
|
+
* and executes model-generated code in a sandbox with tool dispatch.
|
|
33
|
+
*/
|
|
34
|
+
export declare class CodeModeManager {
|
|
35
|
+
private mcpManager;
|
|
36
|
+
private policy;
|
|
37
|
+
private codeModeTools;
|
|
38
|
+
private diagnostics;
|
|
39
|
+
private typeHints;
|
|
40
|
+
private readonly options;
|
|
41
|
+
private readonly sandboxExecutor;
|
|
42
|
+
private log;
|
|
43
|
+
private lastDiagnosticSummary;
|
|
44
|
+
private sandbox;
|
|
45
|
+
private sandboxProbe;
|
|
46
|
+
constructor(options?: CodeModeManagerOptions);
|
|
47
|
+
/**
|
|
48
|
+
* Whether code mode should be advertised to the model.
|
|
49
|
+
*
|
|
50
|
+
* Only a *proven* unavailable sandbox switches this off. An unprobed backend
|
|
51
|
+
* stays active because `code_execute` is registered synchronously at load and
|
|
52
|
+
* returns a structured `sandbox_unavailable` error if it turns out it cannot
|
|
53
|
+
* run — a truthful refusal at call time beats hiding a facility that works.
|
|
54
|
+
*/
|
|
55
|
+
get isActive(): boolean;
|
|
56
|
+
/** Current sandbox backend availability, without triggering a probe. */
|
|
57
|
+
getSandboxAvailability(): SandboxAvailability;
|
|
58
|
+
/**
|
|
59
|
+
* Load the optional native addon once and cache the verdict.
|
|
60
|
+
*
|
|
61
|
+
* Safe to call from any lifecycle hook; concurrent callers share one probe.
|
|
62
|
+
*/
|
|
63
|
+
probeSandbox(): Promise<SandboxAvailability>;
|
|
64
|
+
/** Initialize with MCP manager and policy, catalog tools, and generate type hints. */
|
|
65
|
+
initialize(mcpManager: McpClientManager, policy: McpPolicy, log?: (msg: string) => void): void;
|
|
66
|
+
/** Refresh the complete tool catalog and type hints (call on tools/list_changed). */
|
|
67
|
+
refresh(): void;
|
|
68
|
+
/** Get the type hints string for injection into system prompt. */
|
|
69
|
+
getTypeHints(): string;
|
|
70
|
+
/** Get eligible tools. */
|
|
71
|
+
getEligibleTools(): McpTool[];
|
|
72
|
+
/** Get the complete client-internal catalog, including permission and schema provenance. */
|
|
73
|
+
getCatalogTools(): readonly CodeModeTool[];
|
|
74
|
+
getDiagnostics(): CodeModeDiagnostics;
|
|
75
|
+
/** Execute code in search mode (tool catalog queries). */
|
|
76
|
+
searchTools(code: string): Promise<ExecuteResult>;
|
|
77
|
+
/** Execute code that chains MCP tool calls. */
|
|
78
|
+
executeCode(code: string): Promise<ExecuteResult>;
|
|
79
|
+
/** Create the Pi tool definitions for code_search and code_execute. */
|
|
80
|
+
createTools(): {
|
|
81
|
+
codeSearch: {
|
|
82
|
+
name: string;
|
|
83
|
+
label: string;
|
|
84
|
+
description: string;
|
|
85
|
+
parameters: import("typebox").TObject<{
|
|
86
|
+
code: import("typebox").TString;
|
|
87
|
+
}>;
|
|
88
|
+
execute(_toolCallId: string, params: {
|
|
89
|
+
code: string;
|
|
90
|
+
}, _signal: AbortSignal | undefined, _onUpdate: undefined, _ctx: import("@sammorrowdrums/mcpi").ExtensionContext): Promise<import("@sammorrowdrums/mcpi-agent").AgentToolResult<import("./tools.js").CodeModeToolDetails>>;
|
|
91
|
+
};
|
|
92
|
+
codeExecute: {
|
|
93
|
+
name: string;
|
|
94
|
+
label: string;
|
|
95
|
+
description: string;
|
|
96
|
+
parameters: import("typebox").TObject<{
|
|
97
|
+
code: import("typebox").TString;
|
|
98
|
+
}>;
|
|
99
|
+
execute(_toolCallId: string, params: {
|
|
100
|
+
code: string;
|
|
101
|
+
}, _signal: AbortSignal | undefined, _onUpdate: undefined, _ctx: import("@sammorrowdrums/mcpi").ExtensionContext): Promise<import("@sammorrowdrums/mcpi-agent").AgentToolResult<import("./tools.js").CodeModeToolDetails>>;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/** Format a system prompt section for code mode. */
|
|
105
|
+
formatSystemPromptSection(): string;
|
|
106
|
+
private execute;
|
|
107
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { McpPolicyError } from "../mcp/policy.js";
|
|
2
|
+
import { getCodeModeDiagnostics, getCodeModeTools, } from "./eligibility.js";
|
|
3
|
+
import { CodeModeDispatchError, executeInSandbox } from "./executor.js";
|
|
4
|
+
import { loadIsolatedVm } from "./isolated-vm.js";
|
|
5
|
+
import { createCodeExecuteTool, createCodeSearchTool } from "./tools.js";
|
|
6
|
+
import { generateTypeHints } from "./type-hints.js";
|
|
7
|
+
export { SYNTHESIZED_OUTPUT_SCHEMA, getCodeModeDiagnostics, getCodeModeTools, getEligibleTools, isEligibleForCodeMode, toCodeModeTool, } from "./eligibility.js";
|
|
8
|
+
export { SANDBOX_UNAVAILABLE_ERROR, executeInSandbox, normalizeCode } from "./executor.js";
|
|
9
|
+
export { loadIsolatedVm, peekIsolatedVm, resetIsolatedVmCacheForTests, setIsolatedVmForTests, } from "./isolated-vm.js";
|
|
10
|
+
export { createCodeExecuteTool, createCodeSearchTool } from "./tools.js";
|
|
11
|
+
export { generateTypeHints, jsonSchemaToTypeString, sanitizeToolName } from "./type-hints.js";
|
|
12
|
+
const NO_ELIGIBLE_TOOLS_ERROR = {
|
|
13
|
+
error: "no_eligible_tools",
|
|
14
|
+
message: "code_search has no callable read-only MCP tools to search.",
|
|
15
|
+
alternatives: ["code_execute", "tool-cli"],
|
|
16
|
+
};
|
|
17
|
+
const SANDBOX_UNPROBED = {
|
|
18
|
+
state: "unknown",
|
|
19
|
+
reason: "the isolated-vm native addon has not been probed yet",
|
|
20
|
+
};
|
|
21
|
+
const SANDBOX_INJECTED = {
|
|
22
|
+
state: "available",
|
|
23
|
+
reason: "a sandbox executor was supplied directly, bypassing the isolated-vm addon",
|
|
24
|
+
};
|
|
25
|
+
const SANDBOX_NATIVE = {
|
|
26
|
+
state: "available",
|
|
27
|
+
reason: "the isolated-vm native addon loaded",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Orchestrates code mode: catalogs tools, generates type hints,
|
|
31
|
+
* and executes model-generated code in a sandbox with tool dispatch.
|
|
32
|
+
*/
|
|
33
|
+
export class CodeModeManager {
|
|
34
|
+
mcpManager = null;
|
|
35
|
+
policy = null;
|
|
36
|
+
codeModeTools = [];
|
|
37
|
+
diagnostics = getCodeModeDiagnostics([]);
|
|
38
|
+
typeHints = generateTypeHints([]);
|
|
39
|
+
options;
|
|
40
|
+
sandboxExecutor;
|
|
41
|
+
log;
|
|
42
|
+
lastDiagnosticSummary = "";
|
|
43
|
+
sandbox;
|
|
44
|
+
sandboxProbe;
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.sandboxExecutor = options.sandboxExecutor ?? executeInSandbox;
|
|
48
|
+
this.log = options.log;
|
|
49
|
+
// An injected executor is the sandbox. Probing the native addon in that
|
|
50
|
+
// case would report on a backend this manager will never call.
|
|
51
|
+
this.sandbox = options.sandboxExecutor ? SANDBOX_INJECTED : SANDBOX_UNPROBED;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether code mode should be advertised to the model.
|
|
55
|
+
*
|
|
56
|
+
* Only a *proven* unavailable sandbox switches this off. An unprobed backend
|
|
57
|
+
* stays active because `code_execute` is registered synchronously at load and
|
|
58
|
+
* returns a structured `sandbox_unavailable` error if it turns out it cannot
|
|
59
|
+
* run — a truthful refusal at call time beats hiding a facility that works.
|
|
60
|
+
*/
|
|
61
|
+
get isActive() {
|
|
62
|
+
return this.sandbox.state !== "unavailable";
|
|
63
|
+
}
|
|
64
|
+
/** Current sandbox backend availability, without triggering a probe. */
|
|
65
|
+
getSandboxAvailability() {
|
|
66
|
+
return this.sandbox;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load the optional native addon once and cache the verdict.
|
|
70
|
+
*
|
|
71
|
+
* Safe to call from any lifecycle hook; concurrent callers share one probe.
|
|
72
|
+
*/
|
|
73
|
+
async probeSandbox() {
|
|
74
|
+
if (this.sandbox.state !== "unknown")
|
|
75
|
+
return this.sandbox;
|
|
76
|
+
this.sandboxProbe ??= loadIsolatedVm().then((load) => {
|
|
77
|
+
this.sandbox = load.available
|
|
78
|
+
? SANDBOX_NATIVE
|
|
79
|
+
: { state: "unavailable", reason: load.reason };
|
|
80
|
+
if (!load.available) {
|
|
81
|
+
this.log?.(`[code-mode] disabled: ${load.reason}. Skills, tool-cli, and routing are unaffected.`);
|
|
82
|
+
}
|
|
83
|
+
return this.sandbox;
|
|
84
|
+
});
|
|
85
|
+
return this.sandboxProbe;
|
|
86
|
+
}
|
|
87
|
+
/** Initialize with MCP manager and policy, catalog tools, and generate type hints. */
|
|
88
|
+
initialize(mcpManager, policy, log) {
|
|
89
|
+
this.mcpManager = mcpManager;
|
|
90
|
+
this.policy = policy;
|
|
91
|
+
this.log = log ?? this.log;
|
|
92
|
+
this.refresh();
|
|
93
|
+
}
|
|
94
|
+
/** Refresh the complete tool catalog and type hints (call on tools/list_changed). */
|
|
95
|
+
refresh() {
|
|
96
|
+
this.codeModeTools = this.mcpManager ? getCodeModeTools(this.mcpManager) : [];
|
|
97
|
+
this.diagnostics = getCodeModeDiagnostics(this.codeModeTools);
|
|
98
|
+
this.typeHints = generateTypeHints(this.codeModeTools);
|
|
99
|
+
const summary = `[code-mode] ${this.diagnostics.totalTools} tool(s): ` +
|
|
100
|
+
`${this.diagnostics.callableTools} callable, ${this.diagnostics.refusedTools} dispatch-refused; ` +
|
|
101
|
+
`output schemas: ${this.diagnostics.declaredOutputSchemas} declared, ` +
|
|
102
|
+
`${this.diagnostics.synthesizedOutputSchemas} synthesized, ` +
|
|
103
|
+
`${this.diagnostics.unavailableOutputSchemas} unavailable; ` +
|
|
104
|
+
`${this.typeHints.length} chars of type hints`;
|
|
105
|
+
if (summary !== this.lastDiagnosticSummary) {
|
|
106
|
+
this.log?.(summary);
|
|
107
|
+
this.lastDiagnosticSummary = summary;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Get the type hints string for injection into system prompt. */
|
|
111
|
+
getTypeHints() {
|
|
112
|
+
return this.typeHints;
|
|
113
|
+
}
|
|
114
|
+
/** Get eligible tools. */
|
|
115
|
+
getEligibleTools() {
|
|
116
|
+
return this.codeModeTools.filter((entry) => entry.callable).map((entry) => entry.tool);
|
|
117
|
+
}
|
|
118
|
+
/** Get the complete client-internal catalog, including permission and schema provenance. */
|
|
119
|
+
getCatalogTools() {
|
|
120
|
+
return this.codeModeTools;
|
|
121
|
+
}
|
|
122
|
+
getDiagnostics() {
|
|
123
|
+
return this.diagnostics;
|
|
124
|
+
}
|
|
125
|
+
/** Execute code in search mode (tool catalog queries). */
|
|
126
|
+
async searchTools(code) {
|
|
127
|
+
this.refresh();
|
|
128
|
+
if (this.diagnostics.callableTools === 0) {
|
|
129
|
+
return {
|
|
130
|
+
result: undefined,
|
|
131
|
+
error: NO_ELIGIBLE_TOOLS_ERROR.message,
|
|
132
|
+
errorDetails: {
|
|
133
|
+
...NO_ELIGIBLE_TOOLS_ERROR,
|
|
134
|
+
alternatives: [...(NO_ELIGIBLE_TOOLS_ERROR.alternatives ?? [])],
|
|
135
|
+
},
|
|
136
|
+
logs: [],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return this.execute(code);
|
|
140
|
+
}
|
|
141
|
+
/** Execute code that chains MCP tool calls. */
|
|
142
|
+
async executeCode(code) {
|
|
143
|
+
this.refresh();
|
|
144
|
+
return this.execute(code);
|
|
145
|
+
}
|
|
146
|
+
/** Create the Pi tool definitions for code_search and code_execute. */
|
|
147
|
+
createTools() {
|
|
148
|
+
return {
|
|
149
|
+
codeSearch: createCodeSearchTool(this),
|
|
150
|
+
codeExecute: createCodeExecuteTool(this),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/** Format a system prompt section for code mode. */
|
|
154
|
+
formatSystemPromptSection() {
|
|
155
|
+
return [
|
|
156
|
+
"",
|
|
157
|
+
"<code_mode>",
|
|
158
|
+
"## Code mode",
|
|
159
|
+
"",
|
|
160
|
+
"Use when a task needs exact computation or control flow: math, aggregation, looping over",
|
|
161
|
+
"results, data transformation, or chaining several MCP tool calls with logic in between.",
|
|
162
|
+
"",
|
|
163
|
+
"`code_execute` runs vanilla JavaScript in a sandboxed V8 isolate. Concretely, that covers:",
|
|
164
|
+
"",
|
|
165
|
+
"1. **Arbitrary computation** — math, string manipulation, date arithmetic, data transformation,",
|
|
166
|
+
" or any calculation the user asks for. No MCP tools needed; plain JS works.",
|
|
167
|
+
"2. **Multi-tool aggregation** — counting, filtering, trending, or transforming results across",
|
|
168
|
+
" many tool calls. Write a loop inside one `code_execute` instead of making many separate tool calls.",
|
|
169
|
+
"3. **Pagination** — fetch batches in a loop until exhausted, then compute over the full dataset.",
|
|
170
|
+
"",
|
|
171
|
+
"Use `code_search` first to discover which MCP tools are reachable from inside the sandbox.",
|
|
172
|
+
"",
|
|
173
|
+
"`code_search` and `code_execute` are always registered and never gated.",
|
|
174
|
+
"",
|
|
175
|
+
"After producing a result, verify it makes sense — run a quick sanity check or spot-check values.",
|
|
176
|
+
"",
|
|
177
|
+
"### How to write code",
|
|
178
|
+
"",
|
|
179
|
+
"Write vanilla JavaScript (not TypeScript, not Node.js). No `require`, `import`, `fetch`,",
|
|
180
|
+
"`fs`, `process`, or any Node.js/browser APIs. The only external API is the `codemode` namespace",
|
|
181
|
+
"for MCP tool calls (optional — pure computation works without it). Always `return` the final result.",
|
|
182
|
+
"",
|
|
183
|
+
"**Write ONE `code_execute` call that does the whole job.** Loops, comparisons, pagination,",
|
|
184
|
+
"and aggregation all happen inside a single execution.",
|
|
185
|
+
"",
|
|
186
|
+
"```javascript",
|
|
187
|
+
"// Pure computation — no tools needed",
|
|
188
|
+
"const factorial = (n) => n <= 1 ? 1 : n * factorial(n - 1);",
|
|
189
|
+
"return { result: factorial(20), formatted: factorial(20).toLocaleString() };",
|
|
190
|
+
"```",
|
|
191
|
+
"",
|
|
192
|
+
"```javascript",
|
|
193
|
+
"// Aggregate across paginated MCP tool results",
|
|
194
|
+
"const counts = {};",
|
|
195
|
+
"let page = 1;",
|
|
196
|
+
"while (true) {",
|
|
197
|
+
" const result = await codemode.list_items({ page, perPage: 100 });",
|
|
198
|
+
" for (const item of result.items) {",
|
|
199
|
+
" counts[item.category] = (counts[item.category] || 0) + 1;",
|
|
200
|
+
" }",
|
|
201
|
+
" if (result.items.length < 100) break;",
|
|
202
|
+
" page++;",
|
|
203
|
+
"}",
|
|
204
|
+
"return counts;",
|
|
205
|
+
"```",
|
|
206
|
+
"",
|
|
207
|
+
"### Available tools",
|
|
208
|
+
"",
|
|
209
|
+
"```typescript",
|
|
210
|
+
this.typeHints,
|
|
211
|
+
"```",
|
|
212
|
+
"</code_mode>",
|
|
213
|
+
].join("\n");
|
|
214
|
+
}
|
|
215
|
+
async execute(code) {
|
|
216
|
+
const policy = this.policy;
|
|
217
|
+
const toolNames = this.codeModeTools.map((entry) => entry.tool.name);
|
|
218
|
+
const dispatch = async (toolName, args) => {
|
|
219
|
+
const codeModeTool = this.codeModeTools.find((entry) => entry.tool.name === toolName);
|
|
220
|
+
if (!codeModeTool) {
|
|
221
|
+
throw new Error(`Tool "${toolName}" not found in code mode eligible tools`);
|
|
222
|
+
}
|
|
223
|
+
if (!policy) {
|
|
224
|
+
throw new Error("Code mode MCP policy is not initialized");
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const terminal = await policy.callTool({
|
|
228
|
+
source: "code-mode",
|
|
229
|
+
serverName: codeModeTool.tool.serverName,
|
|
230
|
+
toolName: codeModeTool.tool.name,
|
|
231
|
+
args,
|
|
232
|
+
});
|
|
233
|
+
return terminal.result;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw toCodeModeDispatchError(error, codeModeTool);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
return this.sandboxExecutor(code, toolNames, dispatch, {
|
|
240
|
+
memoryLimit: this.options.memoryLimit,
|
|
241
|
+
timeoutMs: this.options.timeoutMs,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Translate a policy denial into Code Mode's structured dispatch error, keeping
|
|
247
|
+
* the catalog's refusal detail so the model learns why a tool was refused.
|
|
248
|
+
*/
|
|
249
|
+
function toCodeModeDispatchError(error, codeModeTool) {
|
|
250
|
+
if (!(error instanceof McpPolicyError))
|
|
251
|
+
return error;
|
|
252
|
+
const isPermission = error.reason === "permission_denied";
|
|
253
|
+
return new CodeModeDispatchError({
|
|
254
|
+
error: isPermission ? "permission_denied" : error.reason,
|
|
255
|
+
message: error.message,
|
|
256
|
+
alternatives: [...error.alternatives],
|
|
257
|
+
toolName: codeModeTool.tool.name,
|
|
258
|
+
...(isPermission ? { reason: formatRefusalReasons(codeModeTool) } : {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function formatRefusalReasons(codeModeTool) {
|
|
262
|
+
return codeModeTool.refusalReasons
|
|
263
|
+
.map((reason) => reason === "destructive_hint"
|
|
264
|
+
? "annotations.destructiveHint is true"
|
|
265
|
+
: "annotations.readOnlyHint is not true")
|
|
266
|
+
.join("; ");
|
|
267
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy adapter for the optional `isolated-vm` native addon.
|
|
3
|
+
*
|
|
4
|
+
* Code Mode is the only feature that needs a real V8 isolate. `isolated-vm` is a
|
|
5
|
+
* native addon, so it can legitimately be absent: an unsupported platform, a
|
|
6
|
+
* missing prebuild with no toolchain to compile one, or a deliberate
|
|
7
|
+
* `--omit=optional` install. When that happens Code Mode must report itself
|
|
8
|
+
* unavailable and the rest of the extension — skills, tool-cli, routing — must
|
|
9
|
+
* keep working.
|
|
10
|
+
*
|
|
11
|
+
* Two rules govern this module:
|
|
12
|
+
*
|
|
13
|
+
* 1. **No top-level import.** The specifier is resolved through a dynamic
|
|
14
|
+
* `import()` behind a variable so neither Node nor TypeScript binds to the
|
|
15
|
+
* addon at module-evaluation time. Loading the extension never throws
|
|
16
|
+
* because a native addon is missing.
|
|
17
|
+
* 2. **Never fall back to Node's `vm`.** `node:vm` shares the host heap and
|
|
18
|
+
* realm; it is a code-organisation tool, not a security boundary. Silently
|
|
19
|
+
* downgrading to it would turn "sandboxed execution" into a false claim.
|
|
20
|
+
* If the addon is absent, Code Mode is unavailable — full stop.
|
|
21
|
+
*/
|
|
22
|
+
/** The subset of the `isolated-vm` surface Code Mode actually uses. */
|
|
23
|
+
export interface IsolatedVmContext {
|
|
24
|
+
readonly global: {
|
|
25
|
+
set(name: string, value: unknown): Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
eval(code: string, options: {
|
|
28
|
+
promise: true;
|
|
29
|
+
copy: true;
|
|
30
|
+
timeout: number;
|
|
31
|
+
}): Promise<unknown>;
|
|
32
|
+
}
|
|
33
|
+
export interface IsolatedVmIsolate {
|
|
34
|
+
createContext(): Promise<IsolatedVmContext>;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
export interface IsolatedVmModule {
|
|
38
|
+
Isolate: new (options: {
|
|
39
|
+
memoryLimit: number;
|
|
40
|
+
}) => IsolatedVmIsolate;
|
|
41
|
+
Callback: new (fn: (...args: never[]) => unknown) => unknown;
|
|
42
|
+
Reference: new (fn: (...args: never[]) => unknown) => unknown;
|
|
43
|
+
}
|
|
44
|
+
/** Outcome of attempting to load the native addon. */
|
|
45
|
+
export type IsolatedVmLoad = {
|
|
46
|
+
readonly available: true;
|
|
47
|
+
readonly module: IsolatedVmModule;
|
|
48
|
+
} | {
|
|
49
|
+
readonly available: false;
|
|
50
|
+
readonly reason: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Load the native addon at most once per process.
|
|
54
|
+
*
|
|
55
|
+
* The result — success *or* failure — is cached. A failed load is not retried:
|
|
56
|
+
* a missing or broken native addon does not become present later in the same
|
|
57
|
+
* process, and retrying would repeat the cost on every Code Mode call.
|
|
58
|
+
*/
|
|
59
|
+
export declare function loadIsolatedVm(): Promise<IsolatedVmLoad>;
|
|
60
|
+
/** Synchronously report a previously resolved load, if one has happened. */
|
|
61
|
+
export declare function peekIsolatedVm(): IsolatedVmLoad | undefined;
|
|
62
|
+
/** Reset the memoised state. Test-only. */
|
|
63
|
+
export declare function resetIsolatedVmCacheForTests(): void;
|
|
64
|
+
/** Seed the memoised state. Test-only — lets tests simulate an absent addon. */
|
|
65
|
+
export declare function setIsolatedVmForTests(load: IsolatedVmLoad): void;
|