@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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -47
  3. package/dist/code-mode/eligibility.d.ts +35 -0
  4. package/dist/code-mode/eligibility.js +64 -0
  5. package/dist/code-mode/executor.d.ts +53 -0
  6. package/dist/code-mode/executor.js +185 -0
  7. package/dist/code-mode/index.d.ts +107 -0
  8. package/dist/code-mode/index.js +267 -0
  9. package/dist/code-mode/isolated-vm.d.ts +65 -0
  10. package/dist/code-mode/isolated-vm.js +116 -0
  11. package/dist/code-mode/tools.d.ts +48 -0
  12. package/dist/code-mode/tools.js +73 -0
  13. package/dist/code-mode/type-hints.d.ts +20 -0
  14. package/dist/code-mode/type-hints.js +205 -0
  15. package/dist/docker-e2e.d.ts +54 -0
  16. package/dist/docker-e2e.js +234 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +227 -0
  19. package/dist/mcp/call-tool-result.d.ts +10 -0
  20. package/dist/mcp/call-tool-result.js +70 -0
  21. package/dist/mcp/client-factory.d.ts +59 -0
  22. package/dist/mcp/client-factory.js +76 -0
  23. package/dist/mcp/client-manager.d.ts +88 -0
  24. package/dist/mcp/client-manager.js +307 -0
  25. package/dist/mcp/config-loader.d.ts +7 -0
  26. package/dist/mcp/config-loader.js +42 -0
  27. package/dist/mcp/config.d.ts +56 -0
  28. package/dist/mcp/config.js +42 -0
  29. package/dist/mcp/gateway-defaults.d.ts +14 -0
  30. package/dist/mcp/gateway-defaults.js +21 -0
  31. package/dist/mcp/host-approval.d.ts +16 -0
  32. package/dist/mcp/host-approval.js +19 -0
  33. package/dist/mcp/host-elicitation.d.ts +19 -0
  34. package/dist/mcp/host-elicitation.js +94 -0
  35. package/dist/mcp/index.d.ts +8 -0
  36. package/dist/mcp/index.js +7 -0
  37. package/dist/mcp/policy.d.ts +314 -0
  38. package/dist/mcp/policy.js +848 -0
  39. package/dist/routing/facilities.d.ts +109 -0
  40. package/dist/routing/facilities.js +175 -0
  41. package/dist/routing/format.d.ts +24 -0
  42. package/dist/routing/format.js +64 -0
  43. package/dist/routing/index.d.ts +4 -0
  44. package/dist/routing/index.js +4 -0
  45. package/dist/routing/seam.d.ts +35 -0
  46. package/dist/routing/seam.js +28 -0
  47. package/dist/routing/tripwire.d.ts +42 -0
  48. package/dist/routing/tripwire.js +80 -0
  49. package/dist/skills/discover.d.ts +12 -0
  50. package/dist/skills/discover.js +85 -0
  51. package/dist/skills/format.d.ts +13 -0
  52. package/dist/skills/format.js +42 -0
  53. package/dist/skills/index.d.ts +6 -0
  54. package/dist/skills/index.js +6 -0
  55. package/dist/skills/load-skill-tool.d.ts +56 -0
  56. package/dist/skills/load-skill-tool.js +183 -0
  57. package/dist/skills/mcp-tool-proxy.d.ts +7 -0
  58. package/dist/skills/mcp-tool-proxy.js +41 -0
  59. package/dist/skills/sep2640/client.d.ts +91 -0
  60. package/dist/skills/sep2640/client.js +160 -0
  61. package/dist/skills/sep2640/discover.d.ts +33 -0
  62. package/dist/skills/sep2640/discover.js +82 -0
  63. package/dist/skills/sep2640/index.d.ts +13 -0
  64. package/dist/skills/sep2640/index.js +13 -0
  65. package/dist/skills/sep2640/integrity.d.ts +58 -0
  66. package/dist/skills/sep2640/integrity.js +185 -0
  67. package/dist/skills/sep2640/load.d.ts +61 -0
  68. package/dist/skills/sep2640/load.js +90 -0
  69. package/dist/skills/sep2640/protocol.d.ts +146 -0
  70. package/dist/skills/sep2640/protocol.js +214 -0
  71. package/dist/skills/sep2640/spec.d.ts +76 -0
  72. package/dist/skills/sep2640/spec.js +88 -0
  73. package/dist/skills/skill-registry.d.ts +85 -0
  74. package/dist/skills/skill-registry.js +85 -0
  75. package/dist/tool-cli/bridge.d.ts +14 -0
  76. package/dist/tool-cli/bridge.js +196 -0
  77. package/dist/tool-cli/format.d.ts +20 -0
  78. package/dist/tool-cli/format.js +73 -0
  79. package/dist/tool-cli/index.d.ts +7 -0
  80. package/dist/tool-cli/index.js +5 -0
  81. package/dist/tool-cli/provider.d.ts +21 -0
  82. package/dist/tool-cli/provider.js +114 -0
  83. package/dist/tool-cli/startup.d.ts +22 -0
  84. package/dist/tool-cli/startup.js +47 -0
  85. package/package.json +78 -26
  86. package/.dockerignore +0 -6
  87. package/.github/dependabot.yml +0 -25
  88. package/.github/workflows/ci.yml +0 -50
  89. package/.hadolint.yaml +0 -6
  90. package/.prettierignore +0 -4
  91. package/.prettierrc +0 -7
  92. package/AGENTS.md +0 -118
  93. package/DECISIONS.md +0 -99
  94. package/Dockerfile +0 -39
  95. package/aube-lock.yaml +0 -4146
  96. package/docs/code-mode.md +0 -38
  97. package/docs/skills.md +0 -57
  98. package/docs/tool-cli.md +0 -54
  99. package/eslint.config.js +0 -27
  100. package/images/banner.webp +0 -0
  101. package/images/code-c-maude.webp +0 -0
  102. package/images/code-mode-histogram.png +0 -0
  103. package/images/nuclear-mcp-football.webp +0 -0
  104. package/images/skills-enabling-mcp-tools.png +0 -0
  105. package/images/the-skill-dealer.webp +0 -0
  106. package/images/tool-cli-grep.png +0 -0
  107. package/mise.toml +0 -53
  108. package/scripts/docker-entrypoint.sh +0 -29
  109. package/scripts/test-echo-server.mjs +0 -28
  110. package/scripts/test-mcp-integration.mjs +0 -59
  111. package/scripts/test-tool-cli-smoke.mjs +0 -93
  112. package/src/code-mode/code-mode-integration.test.ts +0 -135
  113. package/src/code-mode/eligibility.test.ts +0 -60
  114. package/src/code-mode/eligibility.ts +0 -16
  115. package/src/code-mode/executor.test.ts +0 -168
  116. package/src/code-mode/executor.ts +0 -163
  117. package/src/code-mode/index.ts +0 -208
  118. package/src/code-mode/tools.ts +0 -110
  119. package/src/code-mode/type-hints.test.ts +0 -187
  120. package/src/code-mode/type-hints.ts +0 -249
  121. package/src/docker-e2e.test.ts +0 -195
  122. package/src/docker-e2e.ts +0 -277
  123. package/src/index.test.ts +0 -8
  124. package/src/index.ts +0 -179
  125. package/src/mcp/client-manager.test.ts +0 -250
  126. package/src/mcp/client-manager.ts +0 -199
  127. package/src/mcp/config-loader.test.ts +0 -115
  128. package/src/mcp/config-loader.ts +0 -46
  129. package/src/mcp/config.ts +0 -40
  130. package/src/mcp/index.ts +0 -4
  131. package/src/skills/discover.ts +0 -81
  132. package/src/skills/format.test.ts +0 -70
  133. package/src/skills/format.ts +0 -42
  134. package/src/skills/index.ts +0 -9
  135. package/src/skills/load-skill-tool.ts +0 -136
  136. package/src/skills/mcp-tool-proxy.ts +0 -143
  137. package/src/skills/skill-integration.test.ts +0 -139
  138. package/src/skills/skill-registry.test.ts +0 -83
  139. package/src/skills/skill-registry.ts +0 -69
  140. package/src/test-servers/weather-server.ts +0 -171
  141. package/src/test-servers/weather-stdio.ts +0 -11
  142. package/src/tool-cli/format.test.ts +0 -33
  143. package/src/tool-cli/format.ts +0 -56
  144. package/src/tool-cli/index.ts +0 -3
  145. 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;