@tanstack/ai-code-mode 0.3.3 → 0.3.5

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.
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { toolDefinition } from "@tanstack/ai";
3
3
  import { toolsToBindings, createEventAwareBindings } from "./bindings/tool-to-binding.js";
4
4
  import { stripTypeScript } from "./strip-typescript.js";
5
+ import { warnIfBindingsExposeSecrets } from "./validate-bindings.js";
5
6
  const executeTypescriptInputSchema = z.object({
6
7
  typescriptCode: z.string().describe(
7
8
  "TypeScript code to execute in the sandbox. Use external_* functions to call available APIs. Return a value to pass results back."
@@ -24,12 +25,18 @@ function createCodeModeTool(config) {
24
25
  timeout = 3e4,
25
26
  memoryLimit = 128,
26
27
  getSkillBindings,
28
+ onSecretParameter,
27
29
  transpile = stripTypeScript
28
30
  } = config;
29
31
  if (tools.length === 0) {
30
32
  throw new Error("At least one tool must be provided to createCodeModeTool");
31
33
  }
32
34
  const staticBindings = toolsToBindings(tools, "external_");
35
+ const secretDedupCache = /* @__PURE__ */ new Set();
36
+ warnIfBindingsExposeSecrets(Object.values(staticBindings), {
37
+ handler: onSecretParameter,
38
+ dedupCache: secretDedupCache
39
+ });
33
40
  const definition = toolDefinition({
34
41
  name: "execute_typescript",
35
42
  description: buildToolDescription(tools),
@@ -69,6 +76,13 @@ function createCodeModeTool(config) {
69
76
  };
70
77
  }
71
78
  const skillBindings = getSkillBindings ? await getSkillBindings() : {};
79
+ const skillBindingValues = Object.values(skillBindings);
80
+ if (skillBindingValues.length > 0) {
81
+ warnIfBindingsExposeSecrets(skillBindingValues, {
82
+ handler: onSecretParameter,
83
+ dedupCache: secretDedupCache
84
+ });
85
+ }
72
86
  const allBindings = { ...staticBindings, ...skillBindings };
73
87
  const eventAwareBindings = createEventAwareBindings(
74
88
  allBindings,
@@ -1 +1 @@
1
- {"version":3,"file":"create-code-mode-tool.js","sources":["../../src/create-code-mode-tool.ts"],"sourcesContent":["import { z } from 'zod'\nimport { toolDefinition } from '@tanstack/ai'\nimport {\n createEventAwareBindings,\n toolsToBindings,\n} from './bindings/tool-to-binding'\nimport { stripTypeScript } from './strip-typescript'\nimport type { ServerTool, ToolExecutionContext } from '@tanstack/ai'\nimport type {\n CodeModeTool,\n CodeModeToolConfig,\n CodeModeToolResult,\n IsolateContext,\n} from './types'\n\n/**\n * Schema for the execute_typescript tool input\n */\nconst executeTypescriptInputSchema = z.object({\n typescriptCode: z\n .string()\n .describe(\n 'TypeScript code to execute in the sandbox. ' +\n 'Use external_* functions to call available APIs. ' +\n 'Return a value to pass results back.',\n ),\n})\n\n/**\n * Schema for the execute_typescript tool output\n */\nconst executeTypescriptOutputSchema = z.object({\n success: z.boolean().describe('Whether execution completed without errors'),\n result: z\n .unknown()\n .optional()\n .describe('Return value from the executed code'),\n logs: z\n .array(z.string())\n .optional()\n .describe('Console output captured during execution'),\n error: z\n .object({\n message: z.string(),\n name: z.string().optional(),\n line: z.number().optional(),\n })\n .optional()\n .describe('Error details if execution failed'),\n})\n\nexport type ExecuteTypescriptInput = z.infer<\n typeof executeTypescriptInputSchema\n>\nexport type ExecuteTypescriptOutput = z.infer<\n typeof executeTypescriptOutputSchema\n>\n\n/**\n * Create an execute_typescript tool that can be used alongside other agent tools.\n *\n * This tool allows an LLM to execute TypeScript code in a secure sandbox.\n * Tools passed in the config become `external_*` functions available inside the sandbox.\n *\n * @example\n * ```typescript\n * import { createCodeMode } from '@tanstack/ai-code-mode'\n * import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node'\n *\n * const { tool, systemPrompt } = createCodeMode({\n * driver: createNodeIsolateDriver(),\n * tools: [weatherTool, dbTool], // Become external_fetchWeather, external_dbQuery\n * timeout: 30000,\n * })\n *\n * chat({\n * systemPrompts: [myPrompt, systemPrompt],\n * tools: [tool, searchTool, emailTool],\n * messages,\n * })\n * ```\n */\nexport function createCodeModeTool(\n config: CodeModeToolConfig,\n): ServerTool<\n typeof executeTypescriptInputSchema,\n typeof executeTypescriptOutputSchema,\n 'execute_typescript'\n> {\n const {\n driver,\n tools,\n timeout = 30000,\n memoryLimit = 128,\n getSkillBindings,\n transpile = stripTypeScript,\n } = config\n\n // Validate tools\n if (tools.length === 0) {\n throw new Error('At least one tool must be provided to createCodeModeTool')\n }\n\n // Transform tools to bindings with external_ prefix (static bindings)\n const staticBindings = toolsToBindings(tools, 'external_')\n\n // Create the tool definition\n const definition = toolDefinition({\n name: 'execute_typescript' as const,\n description: buildToolDescription(tools),\n inputSchema: executeTypescriptInputSchema,\n outputSchema: executeTypescriptOutputSchema,\n })\n\n // Return server tool with execute function that accepts context\n return definition.server(\n async (\n input,\n toolContext?: ToolExecutionContext,\n ): Promise<CodeModeToolResult> => {\n const { typescriptCode } = input\n\n // Get emitCustomEvent from context or use no-op\n const emitCustomEvent = toolContext?.emitCustomEvent || (() => {})\n\n if (!typescriptCode || typeof typescriptCode !== 'string') {\n return {\n success: false,\n error: {\n message: 'typescriptCode must be a non-empty string',\n name: 'ValidationError',\n },\n }\n }\n\n // Create a fresh sandbox context for this execution\n let isolateContext: IsolateContext | null = null\n\n // Emit execution started event immediately\n emitCustomEvent('code_mode:execution_started', {\n timestamp: Date.now(),\n codeLength: typescriptCode.length,\n })\n\n try {\n // Step 1: Strip TypeScript (also serves as syntax validation via the\n // transpiler — sucrase by default, or a user-supplied `transpile`)\n let strippedCode: string\n try {\n strippedCode = await transpile(typescriptCode)\n } catch (error) {\n // Type/syntax error from the transpiler\n return {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: 'TypeScriptError',\n },\n }\n }\n\n // Step 2: Get dynamic skill bindings if available\n const skillBindings = getSkillBindings ? await getSkillBindings() : {}\n\n // Step 3: Merge static and dynamic bindings, then wrap with event awareness\n const allBindings = { ...staticBindings, ...skillBindings }\n const eventAwareBindings = createEventAwareBindings(\n allBindings,\n emitCustomEvent,\n )\n\n // Step 4: Create sandbox context with event-aware bindings\n isolateContext = await driver.createContext({\n bindings: eventAwareBindings,\n timeout,\n memoryLimit,\n })\n\n // Step 5: Execute the code in the sandbox\n const executionResult = await isolateContext.execute(strippedCode)\n\n // Emit console logs as custom events\n if (executionResult.logs && executionResult.logs.length > 0) {\n for (const log of executionResult.logs) {\n // Parse log level from prefix (added by sandbox console implementation)\n let level: 'log' | 'warn' | 'error' | 'info' = 'log'\n let message = log\n\n if (log.startsWith('ERROR: ')) {\n level = 'error'\n message = log.slice(7)\n } else if (log.startsWith('WARN: ')) {\n level = 'warn'\n message = log.slice(6)\n } else if (log.startsWith('INFO: ')) {\n level = 'info'\n message = log.slice(6)\n }\n\n emitCustomEvent('code_mode:console', {\n level,\n message,\n timestamp: Date.now(),\n })\n }\n }\n\n if (executionResult.success) {\n return {\n success: true,\n result: executionResult.value,\n logs: executionResult.logs,\n }\n } else {\n return {\n success: false,\n error: executionResult.error\n ? {\n message: executionResult.error.message,\n name: executionResult.error.name,\n }\n : { message: 'Unknown execution error' },\n logs: executionResult.logs,\n }\n }\n } catch (error) {\n return {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : 'Error',\n },\n }\n } finally {\n // Always clean up the sandbox context\n if (isolateContext) {\n await isolateContext.dispose()\n }\n }\n },\n )\n}\n\n/**\n * Build the tool description including available external functions\n */\nfunction buildToolDescription(tools: Array<CodeModeTool>): string {\n const eager = tools.filter((t) => !t.lazy)\n const hasLazy = tools.some((t) => t.lazy)\n const externalFunctions = eager.map((t) => `external_${t.name}`).join(', ')\n\n const discoverable = hasLazy\n ? ` Additional functions can be discovered via the discover_tools tool.`\n : ''\n\n return (\n `Execute TypeScript code in a secure sandbox environment. ` +\n `The code can use these external API functions: ${externalFunctions}.${discoverable} ` +\n `All external_* calls are async and must be awaited. ` +\n `Return a value to pass results back. Use console.log() for debugging.`\n )\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"create-code-mode-tool.js","sources":["../../src/create-code-mode-tool.ts"],"sourcesContent":["import { z } from 'zod'\nimport { toolDefinition } from '@tanstack/ai'\nimport {\n createEventAwareBindings,\n toolsToBindings,\n} from './bindings/tool-to-binding'\nimport { stripTypeScript } from './strip-typescript'\nimport { warnIfBindingsExposeSecrets } from './validate-bindings'\nimport type { ServerTool, ToolExecutionContext } from '@tanstack/ai'\nimport type {\n CodeModeTool,\n CodeModeToolConfig,\n CodeModeToolResult,\n IsolateContext,\n} from './types'\n\n/**\n * Schema for the execute_typescript tool input\n */\nconst executeTypescriptInputSchema = z.object({\n typescriptCode: z\n .string()\n .describe(\n 'TypeScript code to execute in the sandbox. ' +\n 'Use external_* functions to call available APIs. ' +\n 'Return a value to pass results back.',\n ),\n})\n\n/**\n * Schema for the execute_typescript tool output\n */\nconst executeTypescriptOutputSchema = z.object({\n success: z.boolean().describe('Whether execution completed without errors'),\n result: z\n .unknown()\n .optional()\n .describe('Return value from the executed code'),\n logs: z\n .array(z.string())\n .optional()\n .describe('Console output captured during execution'),\n error: z\n .object({\n message: z.string(),\n name: z.string().optional(),\n line: z.number().optional(),\n })\n .optional()\n .describe('Error details if execution failed'),\n})\n\nexport type ExecuteTypescriptInput = z.infer<\n typeof executeTypescriptInputSchema\n>\nexport type ExecuteTypescriptOutput = z.infer<\n typeof executeTypescriptOutputSchema\n>\n\n/**\n * Create an execute_typescript tool that can be used alongside other agent tools.\n *\n * This tool allows an LLM to execute TypeScript code in a secure sandbox.\n * Tools passed in the config become `external_*` functions available inside the sandbox.\n *\n * @example\n * ```typescript\n * import { createCodeMode } from '@tanstack/ai-code-mode'\n * import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node'\n *\n * const { tool, systemPrompt } = createCodeMode({\n * driver: createNodeIsolateDriver(),\n * tools: [weatherTool, dbTool], // Become external_fetchWeather, external_dbQuery\n * timeout: 30000,\n * })\n *\n * chat({\n * systemPrompts: [myPrompt, systemPrompt],\n * tools: [tool, searchTool, emailTool],\n * messages,\n * })\n * ```\n */\nexport function createCodeModeTool(\n config: CodeModeToolConfig,\n): ServerTool<\n typeof executeTypescriptInputSchema,\n typeof executeTypescriptOutputSchema,\n 'execute_typescript'\n> {\n const {\n driver,\n tools,\n timeout = 30000,\n memoryLimit = 128,\n getSkillBindings,\n onSecretParameter,\n transpile = stripTypeScript,\n } = config\n\n // Validate tools\n if (tools.length === 0) {\n throw new Error('At least one tool must be provided to createCodeModeTool')\n }\n\n // Transform tools to bindings with external_ prefix (static bindings)\n const staticBindings = toolsToBindings(tools, 'external_')\n\n // Shared across static + dynamic (skill) binding scans so a given\n // (toolName, paramPath) pair surfaces at most once per code-mode instance.\n const secretDedupCache = new Set<string>()\n\n warnIfBindingsExposeSecrets(Object.values(staticBindings), {\n handler: onSecretParameter,\n dedupCache: secretDedupCache,\n })\n\n // Create the tool definition\n const definition = toolDefinition({\n name: 'execute_typescript' as const,\n description: buildToolDescription(tools),\n inputSchema: executeTypescriptInputSchema,\n outputSchema: executeTypescriptOutputSchema,\n })\n\n // Return server tool with execute function that accepts context\n return definition.server(\n async (\n input,\n toolContext?: ToolExecutionContext,\n ): Promise<CodeModeToolResult> => {\n const { typescriptCode } = input\n\n // Get emitCustomEvent from context or use no-op\n const emitCustomEvent = toolContext?.emitCustomEvent || (() => {})\n\n if (!typescriptCode || typeof typescriptCode !== 'string') {\n return {\n success: false,\n error: {\n message: 'typescriptCode must be a non-empty string',\n name: 'ValidationError',\n },\n }\n }\n\n // Create a fresh sandbox context for this execution\n let isolateContext: IsolateContext | null = null\n\n // Emit execution started event immediately\n emitCustomEvent('code_mode:execution_started', {\n timestamp: Date.now(),\n codeLength: typescriptCode.length,\n })\n\n try {\n // Step 1: Strip TypeScript (also serves as syntax validation via the\n // transpiler — sucrase by default, or a user-supplied `transpile`)\n let strippedCode: string\n try {\n strippedCode = await transpile(typescriptCode)\n } catch (error) {\n // Type/syntax error from the transpiler\n return {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: 'TypeScriptError',\n },\n }\n }\n\n // Step 2: Get dynamic skill bindings if available\n const skillBindings = getSkillBindings ? await getSkillBindings() : {}\n\n // Scan dynamic bindings too — their schemas are equally in-scope for\n // the same exfiltration threat. Dedup cache prevents repeat warnings\n // when the same binding reappears across executions.\n const skillBindingValues = Object.values(skillBindings)\n if (skillBindingValues.length > 0) {\n warnIfBindingsExposeSecrets(skillBindingValues, {\n handler: onSecretParameter,\n dedupCache: secretDedupCache,\n })\n }\n\n // Step 3: Merge static and dynamic bindings, then wrap with event awareness\n const allBindings = { ...staticBindings, ...skillBindings }\n const eventAwareBindings = createEventAwareBindings(\n allBindings,\n emitCustomEvent,\n )\n\n // Step 4: Create sandbox context with event-aware bindings\n isolateContext = await driver.createContext({\n bindings: eventAwareBindings,\n timeout,\n memoryLimit,\n })\n\n // Step 5: Execute the code in the sandbox\n const executionResult = await isolateContext.execute(strippedCode)\n\n // Emit console logs as custom events\n if (executionResult.logs && executionResult.logs.length > 0) {\n for (const log of executionResult.logs) {\n // Parse log level from prefix (added by sandbox console implementation)\n let level: 'log' | 'warn' | 'error' | 'info' = 'log'\n let message = log\n\n if (log.startsWith('ERROR: ')) {\n level = 'error'\n message = log.slice(7)\n } else if (log.startsWith('WARN: ')) {\n level = 'warn'\n message = log.slice(6)\n } else if (log.startsWith('INFO: ')) {\n level = 'info'\n message = log.slice(6)\n }\n\n emitCustomEvent('code_mode:console', {\n level,\n message,\n timestamp: Date.now(),\n })\n }\n }\n\n if (executionResult.success) {\n return {\n success: true,\n result: executionResult.value,\n logs: executionResult.logs,\n }\n } else {\n return {\n success: false,\n error: executionResult.error\n ? {\n message: executionResult.error.message,\n name: executionResult.error.name,\n }\n : { message: 'Unknown execution error' },\n logs: executionResult.logs,\n }\n }\n } catch (error) {\n return {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : 'Error',\n },\n }\n } finally {\n // Always clean up the sandbox context\n if (isolateContext) {\n await isolateContext.dispose()\n }\n }\n },\n )\n}\n\n/**\n * Build the tool description including available external functions\n */\nfunction buildToolDescription(tools: Array<CodeModeTool>): string {\n const eager = tools.filter((t) => !t.lazy)\n const hasLazy = tools.some((t) => t.lazy)\n const externalFunctions = eager.map((t) => `external_${t.name}`).join(', ')\n\n const discoverable = hasLazy\n ? ` Additional functions can be discovered via the discover_tools tool.`\n : ''\n\n return (\n `Execute TypeScript code in a secure sandbox environment. ` +\n `The code can use these external API functions: ${externalFunctions}.${discoverable} ` +\n `All external_* calls are async and must be awaited. ` +\n `Return a value to pass results back. Use console.log() for debugging.`\n )\n}\n"],"names":[],"mappings":"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"}
@@ -9,3 +9,4 @@ export { generateTypeStubs, jsonSchemaToTypeScript, type TypeGeneratorOptions, }
9
9
  export { stripTypeScript } from './strip-typescript.js';
10
10
  export { wrapCode } from './code-wrapper.js';
11
11
  export type { CodeModeToolConfig, CreateCodeModeResult, CodeModeToolResult, IsolateDriver, IsolateConfig, IsolateContext, ExecutionResult, NormalizedError, ToolBinding, CodeModeTool, ToolExecutionContext, } from './types.js';
12
+ export type { SecretParameterHandler, SecretParameterInfo, } from './validate-bindings.js';
@@ -1,4 +1,5 @@
1
1
  import { LazyToolsConfig, SchemaInput, ServerTool, ToolExecutionContext } from '@tanstack/ai';
2
+ import { SecretParameterHandler } from './validate-bindings.js';
2
3
  /**
3
4
  * Interface for isolate/sandbox drivers
4
5
  * Each runtime environment implements this to provide sandboxed code execution
@@ -152,6 +153,19 @@ export interface CodeModeToolConfig {
152
153
  * ```
153
154
  */
154
155
  getSkillBindings?: () => Promise<Record<string, ToolBinding>>;
156
+ /**
157
+ * How to surface tool parameters whose names look like secrets.
158
+ * Defaults to `'warn'` (logs via `console.warn`).
159
+ *
160
+ * - `'warn'`: log a warning for each match.
161
+ * - `'throw'`: throw an Error on the first match — useful in tests/CI.
162
+ * - `'ignore'`: suppress the check entirely.
163
+ * - `(info) => void`: receive each match and decide how to react.
164
+ *
165
+ * Matches are deduplicated per `(toolName, paramPath)` across the lifetime
166
+ * of a single `createCodeModeTool` instance.
167
+ */
168
+ onSecretParameter?: SecretParameterHandler;
155
169
  /**
156
170
  * Optional lazy-tool discovery config. Tools marked `lazy: true` are kept out
157
171
  * of the system prompt's full documentation and listed in a Discoverable APIs
@@ -0,0 +1,25 @@
1
+ export interface SecretParameterInfo {
2
+ toolName: string;
3
+ paramName: string;
4
+ paramPath: Array<string>;
5
+ }
6
+ export type SecretParameterHandler = 'warn' | 'throw' | 'ignore' | ((info: SecretParameterInfo) => void);
7
+ interface ToolLike {
8
+ name: string;
9
+ inputSchema?: Record<string, unknown>;
10
+ }
11
+ /**
12
+ * Scan tool input schemas for parameter names that look like secrets.
13
+ * Emits a warning (or invokes the configured handler) for each match.
14
+ *
15
+ * Recurses into nested object properties, array items, union branches
16
+ * (anyOf/oneOf/allOf), additionalProperties, and `$ref` targets that
17
+ * resolve within the same schema's `$defs`/`definitions`.
18
+ *
19
+ * Best-effort heuristic, not a security boundary.
20
+ */
21
+ export declare function warnIfBindingsExposeSecrets(tools: Array<ToolLike>, options?: {
22
+ handler?: SecretParameterHandler;
23
+ dedupCache?: Set<string>;
24
+ }): void;
25
+ export {};
@@ -0,0 +1,105 @@
1
+ const DANGEROUS_WORDS = /* @__PURE__ */ new Set([
2
+ "password",
3
+ "passwd",
4
+ "pwd",
5
+ "passcode",
6
+ "secret",
7
+ "token",
8
+ "credential",
9
+ "credentials",
10
+ "authorization",
11
+ "jwt",
12
+ "bearer"
13
+ ]);
14
+ const COMPOUND_PATTERNS = [
15
+ "apikey",
16
+ "accesskey",
17
+ "authkey",
18
+ "privatekey",
19
+ "clientsecret",
20
+ "webhooksecret"
21
+ ];
22
+ function splitIntoWords(name) {
23
+ return name.replace(/[_\-\s]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/\s+/).filter(Boolean);
24
+ }
25
+ function looksLikeSecret(name) {
26
+ const words = splitIntoWords(name);
27
+ if (words.some((w) => DANGEROUS_WORDS.has(w))) return true;
28
+ const normalized = name.replace(/[_\-\s]/g, "").toLowerCase();
29
+ return COMPOUND_PATTERNS.some((p) => normalized.includes(p));
30
+ }
31
+ function resolveRef(ref, root) {
32
+ const match = ref.match(/^#\/(\$defs|definitions)\/(.+)$/);
33
+ if (!match) return void 0;
34
+ const bucket = match[1];
35
+ const key = match[2];
36
+ return key === void 0 ? void 0 : root[bucket]?.[key];
37
+ }
38
+ function findSecretParams(schema, root, seen, path, found) {
39
+ if (!schema || typeof schema !== "object" || seen.has(schema)) return;
40
+ seen.add(schema);
41
+ if (schema.properties && typeof schema.properties === "object") {
42
+ for (const [paramName, sub] of Object.entries(schema.properties)) {
43
+ if (looksLikeSecret(paramName)) {
44
+ found.push({ path: [...path, paramName], name: paramName });
45
+ }
46
+ findSecretParams(sub, root, seen, [...path, paramName], found);
47
+ }
48
+ }
49
+ if (Array.isArray(schema.items)) {
50
+ schema.items.forEach(
51
+ (s, i) => findSecretParams(s, root, seen, [...path, `[${i}]`], found)
52
+ );
53
+ } else if (schema.items && typeof schema.items === "object") {
54
+ findSecretParams(schema.items, root, seen, [...path, "[]"], found);
55
+ }
56
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
57
+ findSecretParams(schema.additionalProperties, root, seen, path, found);
58
+ }
59
+ for (const key of ["anyOf", "oneOf", "allOf"]) {
60
+ const arr = schema[key];
61
+ if (Array.isArray(arr)) {
62
+ arr.forEach((s) => findSecretParams(s, root, seen, path, found));
63
+ }
64
+ }
65
+ if (typeof schema.$ref === "string") {
66
+ const target = resolveRef(schema.$ref, root);
67
+ if (target) findSecretParams(target, root, seen, path, found);
68
+ }
69
+ }
70
+ function buildMessage(toolName, paramPath) {
71
+ return `[TanStack AI Code Mode] Tool "${toolName}" has parameter "${paramPath.join(".")}" that looks like a secret. Code Mode executes LLM-generated code — any value passed through this parameter is accessible to generated code and could be exfiltrated. Keep secrets in your server-side tool implementation instead of passing them as tool parameters.`;
72
+ }
73
+ function warnIfBindingsExposeSecrets(tools, options = {}) {
74
+ const { handler = "warn", dedupCache } = options;
75
+ if (handler === "ignore") return;
76
+ for (const tool of tools) {
77
+ const schema = tool.inputSchema;
78
+ if (!schema) continue;
79
+ const found = [];
80
+ findSecretParams(schema, schema, /* @__PURE__ */ new Set(), [], found);
81
+ for (const entry of found) {
82
+ const dedupKey = `${tool.name}::${entry.path.join(".")}`;
83
+ if (dedupCache) {
84
+ if (dedupCache.has(dedupKey)) continue;
85
+ dedupCache.add(dedupKey);
86
+ }
87
+ const info = {
88
+ toolName: tool.name,
89
+ paramName: entry.name,
90
+ paramPath: entry.path
91
+ };
92
+ if (typeof handler === "function") {
93
+ handler(info);
94
+ } else if (handler === "throw") {
95
+ throw new Error(buildMessage(tool.name, entry.path));
96
+ } else {
97
+ console.warn(buildMessage(tool.name, entry.path));
98
+ }
99
+ }
100
+ }
101
+ }
102
+ export {
103
+ warnIfBindingsExposeSecrets
104
+ };
105
+ //# sourceMappingURL=validate-bindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-bindings.js","sources":["../../src/validate-bindings.ts"],"sourcesContent":["/**\n * Single words that on their own signal \"this is a credential\".\n * Matched after splitting a parameter name into camelCase/snake/kebab words.\n * So `accessToken` → `['access', 'token']` → matches `token`,\n * but `tokenizer` → `['tokenizer']` → does NOT match `token`.\n */\nconst DANGEROUS_WORDS = new Set<string>([\n 'password',\n 'passwd',\n 'pwd',\n 'passcode',\n 'secret',\n 'token',\n 'credential',\n 'credentials',\n 'authorization',\n 'jwt',\n 'bearer',\n])\n\n/**\n * Compound patterns matched as substrings of the normalized (lowercased,\n * separator-stripped) parameter name. Catches forms like `openai_api_key`,\n * `x-api-key`, and `webhookSecret`.\n */\nconst COMPOUND_PATTERNS = [\n 'apikey',\n 'accesskey',\n 'authkey',\n 'privatekey',\n 'clientsecret',\n 'webhooksecret',\n] as const\n\nexport interface SecretParameterInfo {\n toolName: string\n paramName: string\n paramPath: Array<string>\n}\n\nexport type SecretParameterHandler =\n | 'warn'\n | 'throw'\n | 'ignore'\n | ((info: SecretParameterInfo) => void)\n\ninterface ToolLike {\n name: string\n inputSchema?: Record<string, unknown>\n}\n\ninterface JsonSchemaLike {\n type?: string\n properties?: Record<string, JsonSchemaLike>\n items?: JsonSchemaLike | Array<JsonSchemaLike>\n anyOf?: Array<JsonSchemaLike>\n oneOf?: Array<JsonSchemaLike>\n allOf?: Array<JsonSchemaLike>\n additionalProperties?: boolean | JsonSchemaLike\n $ref?: string\n $defs?: Record<string, JsonSchemaLike>\n definitions?: Record<string, JsonSchemaLike>\n}\n\nfunction splitIntoWords(name: string): Array<string> {\n return name\n .replace(/[_\\-\\s]+/g, ' ')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .split(/\\s+/)\n .filter(Boolean)\n}\n\nfunction looksLikeSecret(name: string): boolean {\n const words = splitIntoWords(name)\n if (words.some((w) => DANGEROUS_WORDS.has(w))) return true\n const normalized = name.replace(/[_\\-\\s]/g, '').toLowerCase()\n return COMPOUND_PATTERNS.some((p) => normalized.includes(p))\n}\n\nfunction resolveRef(\n ref: string,\n root: JsonSchemaLike,\n): JsonSchemaLike | undefined {\n const match = ref.match(/^#\\/(\\$defs|definitions)\\/(.+)$/)\n if (!match) return undefined\n const bucket = match[1] as '$defs' | 'definitions'\n const key = match[2]\n return key === undefined ? undefined : root[bucket]?.[key]\n}\n\nfunction findSecretParams(\n schema: JsonSchemaLike | undefined,\n root: JsonSchemaLike,\n seen: Set<object>,\n path: Array<string>,\n found: Array<{ path: Array<string>; name: string }>,\n): void {\n if (!schema || typeof schema !== 'object' || seen.has(schema)) return\n seen.add(schema)\n\n if (schema.properties && typeof schema.properties === 'object') {\n for (const [paramName, sub] of Object.entries(schema.properties)) {\n if (looksLikeSecret(paramName)) {\n found.push({ path: [...path, paramName], name: paramName })\n }\n findSecretParams(sub, root, seen, [...path, paramName], found)\n }\n }\n\n if (Array.isArray(schema.items)) {\n schema.items.forEach((s, i) =>\n findSecretParams(s, root, seen, [...path, `[${i}]`], found),\n )\n } else if (schema.items && typeof schema.items === 'object') {\n findSecretParams(schema.items, root, seen, [...path, '[]'], found)\n }\n\n if (\n schema.additionalProperties &&\n typeof schema.additionalProperties === 'object'\n ) {\n findSecretParams(schema.additionalProperties, root, seen, path, found)\n }\n\n for (const key of ['anyOf', 'oneOf', 'allOf'] as const) {\n const arr = schema[key]\n if (Array.isArray(arr)) {\n arr.forEach((s) => findSecretParams(s, root, seen, path, found))\n }\n }\n\n if (typeof schema.$ref === 'string') {\n const target = resolveRef(schema.$ref, root)\n if (target) findSecretParams(target, root, seen, path, found)\n }\n}\n\nfunction buildMessage(toolName: string, paramPath: Array<string>): string {\n return (\n `[TanStack AI Code Mode] Tool \"${toolName}\" has parameter \"${paramPath.join('.')}\" ` +\n `that looks like a secret. Code Mode executes LLM-generated code — any ` +\n `value passed through this parameter is accessible to generated code and ` +\n `could be exfiltrated. Keep secrets in your server-side tool implementation ` +\n `instead of passing them as tool parameters.`\n )\n}\n\n/**\n * Scan tool input schemas for parameter names that look like secrets.\n * Emits a warning (or invokes the configured handler) for each match.\n *\n * Recurses into nested object properties, array items, union branches\n * (anyOf/oneOf/allOf), additionalProperties, and `$ref` targets that\n * resolve within the same schema's `$defs`/`definitions`.\n *\n * Best-effort heuristic, not a security boundary.\n */\nexport function warnIfBindingsExposeSecrets(\n tools: Array<ToolLike>,\n options: {\n handler?: SecretParameterHandler\n dedupCache?: Set<string>\n } = {},\n): void {\n const { handler = 'warn', dedupCache } = options\n if (handler === 'ignore') return\n\n for (const tool of tools) {\n const schema = tool.inputSchema as JsonSchemaLike | undefined\n if (!schema) continue\n\n const found: Array<{ path: Array<string>; name: string }> = []\n findSecretParams(schema, schema, new Set(), [], found)\n\n for (const entry of found) {\n const dedupKey = `${tool.name}::${entry.path.join('.')}`\n if (dedupCache) {\n if (dedupCache.has(dedupKey)) continue\n dedupCache.add(dedupKey)\n }\n\n const info: SecretParameterInfo = {\n toolName: tool.name,\n paramName: entry.name,\n paramPath: entry.path,\n }\n\n if (typeof handler === 'function') {\n handler(info)\n } else if (handler === 'throw') {\n throw new Error(buildMessage(tool.name, entry.path))\n } else {\n console.warn(buildMessage(tool.name, entry.path))\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/ai-code-mode",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Secure TypeScript Code Mode for TanStack AI agents to execute sandboxed tool orchestration programs.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -54,12 +54,12 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "zod": "^3.0.0 || ^4.0.0",
57
- "@tanstack/ai": "0.39.0"
57
+ "@tanstack/ai": "0.39.1"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@vitest/coverage-v8": "4.0.14",
61
61
  "zod": "^4.2.0",
62
- "@tanstack/ai": "0.39.0"
62
+ "@tanstack/ai": "0.39.1"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "vite build",
@@ -5,6 +5,7 @@ import {
5
5
  toolsToBindings,
6
6
  } from './bindings/tool-to-binding'
7
7
  import { stripTypeScript } from './strip-typescript'
8
+ import { warnIfBindingsExposeSecrets } from './validate-bindings'
8
9
  import type { ServerTool, ToolExecutionContext } from '@tanstack/ai'
9
10
  import type {
10
11
  CodeModeTool,
@@ -93,6 +94,7 @@ export function createCodeModeTool(
93
94
  timeout = 30000,
94
95
  memoryLimit = 128,
95
96
  getSkillBindings,
97
+ onSecretParameter,
96
98
  transpile = stripTypeScript,
97
99
  } = config
98
100
 
@@ -104,6 +106,15 @@ export function createCodeModeTool(
104
106
  // Transform tools to bindings with external_ prefix (static bindings)
105
107
  const staticBindings = toolsToBindings(tools, 'external_')
106
108
 
109
+ // Shared across static + dynamic (skill) binding scans so a given
110
+ // (toolName, paramPath) pair surfaces at most once per code-mode instance.
111
+ const secretDedupCache = new Set<string>()
112
+
113
+ warnIfBindingsExposeSecrets(Object.values(staticBindings), {
114
+ handler: onSecretParameter,
115
+ dedupCache: secretDedupCache,
116
+ })
117
+
107
118
  // Create the tool definition
108
119
  const definition = toolDefinition({
109
120
  name: 'execute_typescript' as const,
@@ -162,6 +173,17 @@ export function createCodeModeTool(
162
173
  // Step 2: Get dynamic skill bindings if available
163
174
  const skillBindings = getSkillBindings ? await getSkillBindings() : {}
164
175
 
176
+ // Scan dynamic bindings too — their schemas are equally in-scope for
177
+ // the same exfiltration threat. Dedup cache prevents repeat warnings
178
+ // when the same binding reappears across executions.
179
+ const skillBindingValues = Object.values(skillBindings)
180
+ if (skillBindingValues.length > 0) {
181
+ warnIfBindingsExposeSecrets(skillBindingValues, {
182
+ handler: onSecretParameter,
183
+ dedupCache: secretDedupCache,
184
+ })
185
+ }
186
+
165
187
  // Step 3: Merge static and dynamic bindings, then wrap with event awareness
166
188
  const allBindings = { ...staticBindings, ...skillBindings }
167
189
  const eventAwareBindings = createEventAwareBindings(
package/src/index.ts CHANGED
@@ -55,3 +55,9 @@ export type {
55
55
  // Re-exported from @tanstack/ai
56
56
  ToolExecutionContext,
57
57
  } from './types'
58
+
59
+ // Secret-parameter warning configuration
60
+ export type {
61
+ SecretParameterHandler,
62
+ SecretParameterInfo,
63
+ } from './validate-bindings'
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  ServerTool,
5
5
  ToolExecutionContext,
6
6
  } from '@tanstack/ai'
7
+ import type { SecretParameterHandler } from './validate-bindings'
7
8
 
8
9
  // ============================================================================
9
10
  // Isolate Driver Interfaces
@@ -197,6 +198,20 @@ export interface CodeModeToolConfig {
197
198
  */
198
199
  getSkillBindings?: () => Promise<Record<string, ToolBinding>>
199
200
 
201
+ /**
202
+ * How to surface tool parameters whose names look like secrets.
203
+ * Defaults to `'warn'` (logs via `console.warn`).
204
+ *
205
+ * - `'warn'`: log a warning for each match.
206
+ * - `'throw'`: throw an Error on the first match — useful in tests/CI.
207
+ * - `'ignore'`: suppress the check entirely.
208
+ * - `(info) => void`: receive each match and decide how to react.
209
+ *
210
+ * Matches are deduplicated per `(toolName, paramPath)` across the lifetime
211
+ * of a single `createCodeModeTool` instance.
212
+ */
213
+ onSecretParameter?: SecretParameterHandler
214
+
200
215
  /**
201
216
  * Optional lazy-tool discovery config. Tools marked `lazy: true` are kept out
202
217
  * of the system prompt's full documentation and listed in a Discoverable APIs
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Single words that on their own signal "this is a credential".
3
+ * Matched after splitting a parameter name into camelCase/snake/kebab words.
4
+ * So `accessToken` → `['access', 'token']` → matches `token`,
5
+ * but `tokenizer` → `['tokenizer']` → does NOT match `token`.
6
+ */
7
+ const DANGEROUS_WORDS = new Set<string>([
8
+ 'password',
9
+ 'passwd',
10
+ 'pwd',
11
+ 'passcode',
12
+ 'secret',
13
+ 'token',
14
+ 'credential',
15
+ 'credentials',
16
+ 'authorization',
17
+ 'jwt',
18
+ 'bearer',
19
+ ])
20
+
21
+ /**
22
+ * Compound patterns matched as substrings of the normalized (lowercased,
23
+ * separator-stripped) parameter name. Catches forms like `openai_api_key`,
24
+ * `x-api-key`, and `webhookSecret`.
25
+ */
26
+ const COMPOUND_PATTERNS = [
27
+ 'apikey',
28
+ 'accesskey',
29
+ 'authkey',
30
+ 'privatekey',
31
+ 'clientsecret',
32
+ 'webhooksecret',
33
+ ] as const
34
+
35
+ export interface SecretParameterInfo {
36
+ toolName: string
37
+ paramName: string
38
+ paramPath: Array<string>
39
+ }
40
+
41
+ export type SecretParameterHandler =
42
+ | 'warn'
43
+ | 'throw'
44
+ | 'ignore'
45
+ | ((info: SecretParameterInfo) => void)
46
+
47
+ interface ToolLike {
48
+ name: string
49
+ inputSchema?: Record<string, unknown>
50
+ }
51
+
52
+ interface JsonSchemaLike {
53
+ type?: string
54
+ properties?: Record<string, JsonSchemaLike>
55
+ items?: JsonSchemaLike | Array<JsonSchemaLike>
56
+ anyOf?: Array<JsonSchemaLike>
57
+ oneOf?: Array<JsonSchemaLike>
58
+ allOf?: Array<JsonSchemaLike>
59
+ additionalProperties?: boolean | JsonSchemaLike
60
+ $ref?: string
61
+ $defs?: Record<string, JsonSchemaLike>
62
+ definitions?: Record<string, JsonSchemaLike>
63
+ }
64
+
65
+ function splitIntoWords(name: string): Array<string> {
66
+ return name
67
+ .replace(/[_\-\s]+/g, ' ')
68
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
69
+ .toLowerCase()
70
+ .split(/\s+/)
71
+ .filter(Boolean)
72
+ }
73
+
74
+ function looksLikeSecret(name: string): boolean {
75
+ const words = splitIntoWords(name)
76
+ if (words.some((w) => DANGEROUS_WORDS.has(w))) return true
77
+ const normalized = name.replace(/[_\-\s]/g, '').toLowerCase()
78
+ return COMPOUND_PATTERNS.some((p) => normalized.includes(p))
79
+ }
80
+
81
+ function resolveRef(
82
+ ref: string,
83
+ root: JsonSchemaLike,
84
+ ): JsonSchemaLike | undefined {
85
+ const match = ref.match(/^#\/(\$defs|definitions)\/(.+)$/)
86
+ if (!match) return undefined
87
+ const bucket = match[1] as '$defs' | 'definitions'
88
+ const key = match[2]
89
+ return key === undefined ? undefined : root[bucket]?.[key]
90
+ }
91
+
92
+ function findSecretParams(
93
+ schema: JsonSchemaLike | undefined,
94
+ root: JsonSchemaLike,
95
+ seen: Set<object>,
96
+ path: Array<string>,
97
+ found: Array<{ path: Array<string>; name: string }>,
98
+ ): void {
99
+ if (!schema || typeof schema !== 'object' || seen.has(schema)) return
100
+ seen.add(schema)
101
+
102
+ if (schema.properties && typeof schema.properties === 'object') {
103
+ for (const [paramName, sub] of Object.entries(schema.properties)) {
104
+ if (looksLikeSecret(paramName)) {
105
+ found.push({ path: [...path, paramName], name: paramName })
106
+ }
107
+ findSecretParams(sub, root, seen, [...path, paramName], found)
108
+ }
109
+ }
110
+
111
+ if (Array.isArray(schema.items)) {
112
+ schema.items.forEach((s, i) =>
113
+ findSecretParams(s, root, seen, [...path, `[${i}]`], found),
114
+ )
115
+ } else if (schema.items && typeof schema.items === 'object') {
116
+ findSecretParams(schema.items, root, seen, [...path, '[]'], found)
117
+ }
118
+
119
+ if (
120
+ schema.additionalProperties &&
121
+ typeof schema.additionalProperties === 'object'
122
+ ) {
123
+ findSecretParams(schema.additionalProperties, root, seen, path, found)
124
+ }
125
+
126
+ for (const key of ['anyOf', 'oneOf', 'allOf'] as const) {
127
+ const arr = schema[key]
128
+ if (Array.isArray(arr)) {
129
+ arr.forEach((s) => findSecretParams(s, root, seen, path, found))
130
+ }
131
+ }
132
+
133
+ if (typeof schema.$ref === 'string') {
134
+ const target = resolveRef(schema.$ref, root)
135
+ if (target) findSecretParams(target, root, seen, path, found)
136
+ }
137
+ }
138
+
139
+ function buildMessage(toolName: string, paramPath: Array<string>): string {
140
+ return (
141
+ `[TanStack AI Code Mode] Tool "${toolName}" has parameter "${paramPath.join('.')}" ` +
142
+ `that looks like a secret. Code Mode executes LLM-generated code — any ` +
143
+ `value passed through this parameter is accessible to generated code and ` +
144
+ `could be exfiltrated. Keep secrets in your server-side tool implementation ` +
145
+ `instead of passing them as tool parameters.`
146
+ )
147
+ }
148
+
149
+ /**
150
+ * Scan tool input schemas for parameter names that look like secrets.
151
+ * Emits a warning (or invokes the configured handler) for each match.
152
+ *
153
+ * Recurses into nested object properties, array items, union branches
154
+ * (anyOf/oneOf/allOf), additionalProperties, and `$ref` targets that
155
+ * resolve within the same schema's `$defs`/`definitions`.
156
+ *
157
+ * Best-effort heuristic, not a security boundary.
158
+ */
159
+ export function warnIfBindingsExposeSecrets(
160
+ tools: Array<ToolLike>,
161
+ options: {
162
+ handler?: SecretParameterHandler
163
+ dedupCache?: Set<string>
164
+ } = {},
165
+ ): void {
166
+ const { handler = 'warn', dedupCache } = options
167
+ if (handler === 'ignore') return
168
+
169
+ for (const tool of tools) {
170
+ const schema = tool.inputSchema as JsonSchemaLike | undefined
171
+ if (!schema) continue
172
+
173
+ const found: Array<{ path: Array<string>; name: string }> = []
174
+ findSecretParams(schema, schema, new Set(), [], found)
175
+
176
+ for (const entry of found) {
177
+ const dedupKey = `${tool.name}::${entry.path.join('.')}`
178
+ if (dedupCache) {
179
+ if (dedupCache.has(dedupKey)) continue
180
+ dedupCache.add(dedupKey)
181
+ }
182
+
183
+ const info: SecretParameterInfo = {
184
+ toolName: tool.name,
185
+ paramName: entry.name,
186
+ paramPath: entry.path,
187
+ }
188
+
189
+ if (typeof handler === 'function') {
190
+ handler(info)
191
+ } else if (handler === 'throw') {
192
+ throw new Error(buildMessage(tool.name, entry.path))
193
+ } else {
194
+ console.warn(buildMessage(tool.name, entry.path))
195
+ }
196
+ }
197
+ }
198
+ }