@tanstack/ai-code-mode 0.3.2 → 0.3.4
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/dist/esm/create-code-mode-tool.js +14 -0
- package/dist/esm/create-code-mode-tool.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/types.d.ts +14 -0
- package/dist/esm/validate-bindings.d.ts +25 -0
- package/dist/esm/validate-bindings.js +105 -0
- package/dist/esm/validate-bindings.js.map +1 -0
- package/package.json +3 -3
- package/src/create-code-mode-tool.ts +22 -0
- package/src/index.ts +6 -0
- package/src/types.ts +15 -0
- package/src/validate-bindings.ts +198 -0
|
@@ -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":";;;;;AAmBA,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,gBAAgB,EACb,OAAA,EACA;AAAA,IACC;AAAA,EAAA;AAIN,CAAC;AAKD,MAAM,gCAAgC,EAAE,OAAO;AAAA,EAC7C,SAAS,EAAE,UAAU,SAAS,4CAA4C;AAAA,EAC1E,QAAQ,EACL,QAAA,EACA,SAAA,EACA,SAAS,qCAAqC;AAAA,EACjD,MAAM,EACH,MAAM,EAAE,OAAA,CAAQ,EAChB,SAAA,EACA,SAAS,0CAA0C;AAAA,EACtD,OAAO,EACJ,OAAO;AAAA,IACN,SAAS,EAAE,OAAA;AAAA,IACX,MAAM,EAAE,OAAA,EAAS,SAAA;AAAA,IACjB,MAAM,EAAE,OAAA,EAAS,SAAA;AAAA,EAAS,CAC3B,EACA,WACA,SAAS,mCAAmC;AACjD,CAAC;AAiCM,SAAS,mBACd,QAKA;AACA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV,cAAc;AAAA,IACd;AAAA,IACA;AAAA,IACA,YAAY;AAAA,EAAA,IACV;AAGJ,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,MAAM,0DAA0D;AAAA,EAC5E;AAGA,QAAM,iBAAiB,gBAAgB,OAAO,WAAW;AAIzD,QAAM,uCAAuB,IAAA;AAE7B,8BAA4B,OAAO,OAAO,cAAc,GAAG;AAAA,IACzD,SAAS;AAAA,IACT,YAAY;AAAA,EAAA,CACb;AAGD,QAAM,aAAa,eAAe;AAAA,IAChC,MAAM;AAAA,IACN,aAAa,qBAAqB,KAAK;AAAA,IACvC,aAAa;AAAA,IACb,cAAc;AAAA,EAAA,CACf;AAGD,SAAO,WAAW;AAAA,IAChB,OACE,OACA,gBACgC;AAChC,YAAM,EAAE,mBAAmB;AAG3B,YAAM,kBAAkB,aAAa,oBAAoB,MAAM;AAAA,MAAC;AAEhE,UAAI,CAAC,kBAAkB,OAAO,mBAAmB,UAAU;AACzD,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO;AAAA,YACL,SAAS;AAAA,YACT,MAAM;AAAA,UAAA;AAAA,QACR;AAAA,MAEJ;AAGA,UAAI,iBAAwC;AAG5C,sBAAgB,+BAA+B;AAAA,QAC7C,WAAW,KAAK,IAAA;AAAA,QAChB,YAAY,eAAe;AAAA,MAAA,CAC5B;AAED,UAAI;AAGF,YAAI;AACJ,YAAI;AACF,yBAAe,MAAM,UAAU,cAAc;AAAA,QAC/C,SAAS,OAAO;AAEd,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,OAAO;AAAA,cACL,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,cAC9D,MAAM;AAAA,YAAA;AAAA,UACR;AAAA,QAEJ;AAGA,cAAM,gBAAgB,mBAAmB,MAAM,iBAAA,IAAqB,CAAA;AAKpE,cAAM,qBAAqB,OAAO,OAAO,aAAa;AACtD,YAAI,mBAAmB,SAAS,GAAG;AACjC,sCAA4B,oBAAoB;AAAA,YAC9C,SAAS;AAAA,YACT,YAAY;AAAA,UAAA,CACb;AAAA,QACH;AAGA,cAAM,cAAc,EAAE,GAAG,gBAAgB,GAAG,cAAA;AAC5C,cAAM,qBAAqB;AAAA,UACzB;AAAA,UACA;AAAA,QAAA;AAIF,yBAAiB,MAAM,OAAO,cAAc;AAAA,UAC1C,UAAU;AAAA,UACV;AAAA,UACA;AAAA,QAAA,CACD;AAGD,cAAM,kBAAkB,MAAM,eAAe,QAAQ,YAAY;AAGjE,YAAI,gBAAgB,QAAQ,gBAAgB,KAAK,SAAS,GAAG;AAC3D,qBAAW,OAAO,gBAAgB,MAAM;AAEtC,gBAAI,QAA2C;AAC/C,gBAAI,UAAU;AAEd,gBAAI,IAAI,WAAW,SAAS,GAAG;AAC7B,sBAAQ;AACR,wBAAU,IAAI,MAAM,CAAC;AAAA,YACvB,WAAW,IAAI,WAAW,QAAQ,GAAG;AACnC,sBAAQ;AACR,wBAAU,IAAI,MAAM,CAAC;AAAA,YACvB,WAAW,IAAI,WAAW,QAAQ,GAAG;AACnC,sBAAQ;AACR,wBAAU,IAAI,MAAM,CAAC;AAAA,YACvB;AAEA,4BAAgB,qBAAqB;AAAA,cACnC;AAAA,cACA;AAAA,cACA,WAAW,KAAK,IAAA;AAAA,YAAI,CACrB;AAAA,UACH;AAAA,QACF;AAEA,YAAI,gBAAgB,SAAS;AAC3B,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,QAAQ,gBAAgB;AAAA,YACxB,MAAM,gBAAgB;AAAA,UAAA;AAAA,QAE1B,OAAO;AACL,iBAAO;AAAA,YACL,SAAS;AAAA,YACT,OAAO,gBAAgB,QACnB;AAAA,cACE,SAAS,gBAAgB,MAAM;AAAA,cAC/B,MAAM,gBAAgB,MAAM;AAAA,YAAA,IAE9B,EAAE,SAAS,0BAAA;AAAA,YACf,MAAM,gBAAgB;AAAA,UAAA;AAAA,QAE1B;AAAA,MACF,SAAS,OAAO;AACd,eAAO;AAAA,UACL,SAAS;AAAA,UACT,OAAO;AAAA,YACL,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,YAC9D,MAAM,iBAAiB,QAAQ,MAAM,OAAO;AAAA,UAAA;AAAA,QAC9C;AAAA,MAEJ,UAAA;AAEE,YAAI,gBAAgB;AAClB,gBAAM,eAAe,QAAA;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AAAA,EAAA;AAEJ;AAKA,SAAS,qBAAqB,OAAoC;AAChE,QAAM,QAAQ,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI;AACzC,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,IAAI;AACxC,QAAM,oBAAoB,MAAM,IAAI,CAAC,MAAM,YAAY,EAAE,IAAI,EAAE,EAAE,KAAK,IAAI;AAE1E,QAAM,eAAe,UACjB,yEACA;AAEJ,SACE,2GACkD,iBAAiB,IAAI,YAAY;AAIvF;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -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
|
+
"version": "0.3.4",
|
|
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.
|
|
57
|
+
"@tanstack/ai": "0.39.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@vitest/coverage-v8": "4.0.14",
|
|
61
61
|
"zod": "^4.2.0",
|
|
62
|
-
"@tanstack/ai": "0.
|
|
62
|
+
"@tanstack/ai": "0.39.0"
|
|
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
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
|
+
}
|