@tanstack/ai-code-mode
Version:
Secure TypeScript Code Mode for TanStack AI agents to execute sandboxed tool orchestration programs.
199 lines (177 loc) • 5.76 kB
text/typescript
/**
* Single words that on their own signal "this is a credential".
* Matched after splitting a parameter name into camelCase/snake/kebab words.
* So `accessToken` → `['access', 'token']` → matches `token`,
* but `tokenizer` → `['tokenizer']` → does NOT match `token`.
*/
const DANGEROUS_WORDS = new Set<string>([
'password',
'passwd',
'pwd',
'passcode',
'secret',
'token',
'credential',
'credentials',
'authorization',
'jwt',
'bearer',
])
/**
* Compound patterns matched as substrings of the normalized (lowercased,
* separator-stripped) parameter name. Catches forms like `openai_api_key`,
* `x-api-key`, and `webhookSecret`.
*/
const COMPOUND_PATTERNS = [
'apikey',
'accesskey',
'authkey',
'privatekey',
'clientsecret',
'webhooksecret',
] as const
export interface SecretParameterInfo {
toolName: string
paramName: string
paramPath: Array<string>
}
export type SecretParameterHandler =
| 'warn'
| 'throw'
| 'ignore'
| ((info: SecretParameterInfo) => void)
interface ToolLike {
name: string
inputSchema?: Record<string, unknown>
}
interface JsonSchemaLike {
type?: string
properties?: Record<string, JsonSchemaLike>
items?: JsonSchemaLike | Array<JsonSchemaLike>
anyOf?: Array<JsonSchemaLike>
oneOf?: Array<JsonSchemaLike>
allOf?: Array<JsonSchemaLike>
additionalProperties?: boolean | JsonSchemaLike
$ref?: string
$defs?: Record<string, JsonSchemaLike>
definitions?: Record<string, JsonSchemaLike>
}
function splitIntoWords(name: string): Array<string> {
return name
.replace(/[_\-\s]+/g, ' ')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.toLowerCase()
.split(/\s+/)
.filter(Boolean)
}
function looksLikeSecret(name: string): boolean {
const words = splitIntoWords(name)
if (words.some((w) => DANGEROUS_WORDS.has(w))) return true
const normalized = name.replace(/[_\-\s]/g, '').toLowerCase()
return COMPOUND_PATTERNS.some((p) => normalized.includes(p))
}
function resolveRef(
ref: string,
root: JsonSchemaLike,
): JsonSchemaLike | undefined {
const match = ref.match(/^#\/(\$defs|definitions)\/(.+)$/)
if (!match) return undefined
const bucket = match[1] as '$defs' | 'definitions'
const key = match[2]
return key === undefined ? undefined : root[bucket]?.[key]
}
function findSecretParams(
schema: JsonSchemaLike | undefined,
root: JsonSchemaLike,
seen: Set<object>,
path: Array<string>,
found: Array<{ path: Array<string>; name: string }>,
): void {
if (!schema || typeof schema !== 'object' || seen.has(schema)) return
seen.add(schema)
if (schema.properties && typeof schema.properties === 'object') {
for (const [paramName, sub] of Object.entries(schema.properties)) {
if (looksLikeSecret(paramName)) {
found.push({ path: [...path, paramName], name: paramName })
}
findSecretParams(sub, root, seen, [...path, paramName], found)
}
}
if (Array.isArray(schema.items)) {
schema.items.forEach((s, i) =>
findSecretParams(s, root, seen, [...path, `[${i}]`], found),
)
} else if (schema.items && typeof schema.items === 'object') {
findSecretParams(schema.items, root, seen, [...path, '[]'], found)
}
if (
schema.additionalProperties &&
typeof schema.additionalProperties === 'object'
) {
findSecretParams(schema.additionalProperties, root, seen, path, found)
}
for (const key of ['anyOf', 'oneOf', 'allOf'] as const) {
const arr = schema[key]
if (Array.isArray(arr)) {
arr.forEach((s) => findSecretParams(s, root, seen, path, found))
}
}
if (typeof schema.$ref === 'string') {
const target = resolveRef(schema.$ref, root)
if (target) findSecretParams(target, root, seen, path, found)
}
}
function buildMessage(toolName: string, paramPath: Array<string>): string {
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.`
)
}
/**
* Scan tool input schemas for parameter names that look like secrets.
* Emits a warning (or invokes the configured handler) for each match.
*
* Recurses into nested object properties, array items, union branches
* (anyOf/oneOf/allOf), additionalProperties, and `$ref` targets that
* resolve within the same schema's `$defs`/`definitions`.
*
* Best-effort heuristic, not a security boundary.
*/
export function warnIfBindingsExposeSecrets(
tools: Array<ToolLike>,
options: {
handler?: SecretParameterHandler
dedupCache?: Set<string>
} = {},
): void {
const { handler = 'warn', dedupCache } = options
if (handler === 'ignore') return
for (const tool of tools) {
const schema = tool.inputSchema as JsonSchemaLike | undefined
if (!schema) continue
const found: Array<{ path: Array<string>; name: string }> = []
findSecretParams(schema, schema, new Set(), [], found)
for (const entry of found) {
const dedupKey = `${tool.name}::${entry.path.join('.')}`
if (dedupCache) {
if (dedupCache.has(dedupKey)) continue
dedupCache.add(dedupKey)
}
const info: SecretParameterInfo = {
toolName: tool.name,
paramName: entry.name,
paramPath: entry.path,
}
if (typeof handler === 'function') {
handler(info)
} else if (handler === 'throw') {
throw new Error(buildMessage(tool.name, entry.path))
} else {
console.warn(buildMessage(tool.name, entry.path))
}
}
}
}