#!/usr/bin/env node import ts from "typescript"; import { runCallsiteGuard } from "./lib/callsite-guard.mjs"; import { runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs"; const sourceRoots = ["src", "extensions"]; const allowedManagedProxyRuntimeMutationScopes = new Set([ // Canonical managed proxy lifecycle owns process proxy env/global-agent mutation. "src/infra/net/proxy/proxy-lifecycle.ts#applyProxyEnv", "src/infra/net/proxy/proxy-lifecycle.ts#restoreProxyEnv", "src/infra/net/proxy/proxy-lifecycle.ts#restoreGlobalAgentRuntime", "src/infra/net/proxy/proxy-lifecycle.ts#restoreNodeHttpStack", "src/infra/net/proxy/proxy-lifecycle.ts#bootstrapNodeHttpStack", "src/infra/net/proxy/proxy-lifecycle.ts#writeGlobalAgentNoProxy", "src/infra/net/proxy/proxy-lifecycle.ts#disableGlobalAgentProxyForIpv6GatewayLoopback", // Browser CDP loopback helper leases NO_PROXY only for localhost/loopback CDP URLs. "extensions/browser/src/browser/cdp-proxy-bypass.ts#NoProxyLeaseManager.acquire", "extensions/browser/src/browser/cdp-proxy-bypass.ts#NoProxyLeaseManager.release", ]); const forbiddenEnvKeys = new Set([ "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "NO_PROXY", "no_proxy", "GLOBAL_AGENT_HTTP_PROXY", "GLOBAL_AGENT_HTTPS_PROXY", "GLOBAL_AGENT_NO_PROXY", "GLOBAL_AGENT_FORCE_GLOBAL_AGENT", "OPENCLAW_PROXY_ACTIVE", "OPENCLAW_PROXY_LOOPBACK_MODE", ]); const forbiddenGlobalAgentKeys = new Set(["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"]); function stringLiteralText(node) { return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : null; } function propertyNameText(name) { if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) { return name.text; } return null; } function qualifiedScopeName(name, classScopes) { const classScope = classScopes.at(-1); return classScope ? `${classScope}.${name}` : name; } function functionExpressionScopeName(node, classScopes) { const parent = node.parent; if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) { return parent.name.text; } if (ts.isPropertyAssignment(parent)) { const name = propertyNameText(parent.name); return name ? qualifiedScopeName(name, classScopes) : null; } if (ts.isPropertyDeclaration(parent) && parent.name) { const name = propertyNameText(parent.name); return name ? qualifiedScopeName(name, classScopes) : null; } return null; } function scopeNameForNode(node, classScopes) { if (ts.isFunctionDeclaration(node) && node.name) { return node.name.text; } if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) { return functionExpressionScopeName(node, classScopes); } if ( ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) ) { const name = propertyNameText(node.name); return name ? qualifiedScopeName(name, classScopes) : null; } if (ts.isConstructorDeclaration(node)) { return qualifiedScopeName("constructor", classScopes); } return null; } function isGlobalIdentifier(node, context = { globalAliases: new Set() }) { const unwrapped = unwrapExpression(node); return ( ts.isIdentifier(unwrapped) && (unwrapped.text === "global" || unwrapped.text === "globalThis" || context.globalAliases.has(unwrapped.text)) ); } function processEnvExpression(expression, context = { envAliases: new Set() }) { const unwrapped = unwrapExpression(expression); if (ts.isIdentifier(unwrapped) && context.envAliases.has(unwrapped.text)) { return unwrapped; } if (ts.isPropertyAccessExpression(unwrapped) && unwrapped.name.text === "env") { const base = unwrapExpression(unwrapped.expression); return ts.isIdentifier(base) && base.text === "process" ? unwrapped : null; } if (ts.isElementAccessExpression(unwrapped)) { const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression)); if (key !== "env") { return null; } const base = unwrapExpression(unwrapped.expression); return ts.isIdentifier(base) && base.text === "process" ? unwrapped : null; } return null; } function collectStringConstants(sourceFile) { const constants = new Map(); const visit = (node) => { if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { const literal = stringLiteralText(unwrapExpression(node.initializer)); if (literal) { constants.set(node.name.text, literal); } } ts.forEachChild(node, visit); }; visit(sourceFile); return constants; } function collectStringArrays(sourceFile) { const arrays = new Map(); for (let pass = 0; pass < 4; pass += 1) { const visit = (node) => { if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { const initializer = unwrapExpression(node.initializer); if (ts.isArrayLiteralExpression(initializer)) { const values = []; let complete = true; for (const element of initializer.elements) { const unwrapped = unwrapExpression(element); if ( ts.isSpreadElement(unwrapped) && ts.isIdentifier(unwrapExpression(unwrapped.expression)) ) { const nested = arrays.get(unwrapExpression(unwrapped.expression).text); if (nested) { values.push(...nested); } else { complete = false; } continue; } const literal = stringLiteralText(unwrapped); if (literal) { values.push(literal); } else { complete = false; } } if (complete) { const previous = arrays.get(node.name.text); const sameValues = previous && previous.join("\0") === values.join("\0"); if (!sameValues) { arrays.set(node.name.text, values); } } } } ts.forEachChild(node, visit); }; visit(sourceFile); } return arrays; } function collectForbiddenKeyArrays(sourceFile) { const stringArrays = collectStringArrays(sourceFile); const arrays = new Set(); for (const [name, values] of stringArrays) { if (values.some((key) => forbiddenEnvKeys.has(key))) { arrays.add(name); } } return arrays; } function collectForbiddenKeyVariables(sourceFile, forbiddenKeyArrays) { const variables = new Set(); const visit = (node) => { if (ts.isForOfStatement(node)) { const expression = unwrapExpression(node.expression); if (ts.isIdentifier(expression) && forbiddenKeyArrays.has(expression.text)) { const initializer = node.initializer; if (ts.isVariableDeclarationList(initializer)) { for (const declaration of initializer.declarations) { if (ts.isIdentifier(declaration.name)) { variables.add(declaration.name.text); } } } else if (ts.isIdentifier(initializer)) { variables.add(initializer.text); } } } ts.forEachChild(node, visit); }; visit(sourceFile); return variables; } function envKeyExpressionIsForbidden(argumentExpression, context) { const keyExpression = unwrapExpression(argumentExpression); const literalKey = stringLiteralText(keyExpression); if (literalKey) { return forbiddenEnvKeys.has(literalKey); } if (!ts.isIdentifier(keyExpression)) { return false; } if (context.forbiddenKeyVariables.has(keyExpression.text)) { return true; } const constant = context.stringConstants.get(keyExpression.text); return constant ? forbiddenEnvKeys.has(constant) : false; } function envMutationTarget(expression, context) { const unwrapped = unwrapExpression(expression); if ( ts.isPropertyAccessExpression(unwrapped) && processEnvExpression(unwrapped.expression, context) ) { return forbiddenEnvKeys.has(unwrapped.name.text) ? unwrapped : null; } if ( ts.isElementAccessExpression(unwrapped) && processEnvExpression(unwrapped.expression, context) ) { return envKeyExpressionIsForbidden(unwrapped.argumentExpression, context) ? unwrapped : null; } return null; } function globalAgentExpression( expression, context = { globalAgentAliases: new Set(), globalAliases: new Set() }, ) { const unwrapped = unwrapExpression(expression); if (ts.isIdentifier(unwrapped) && context.globalAgentAliases.has(unwrapped.text)) { return unwrapped; } if (ts.isPropertyAccessExpression(unwrapped)) { const receiver = unwrapExpression(unwrapped.expression); if (unwrapped.name.text === "GLOBAL_AGENT" && isGlobalIdentifier(receiver, context)) { return unwrapped; } } if (ts.isElementAccessExpression(unwrapped)) { const receiver = unwrapExpression(unwrapped.expression); const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression)); if (key === "GLOBAL_AGENT" && isGlobalIdentifier(receiver, context)) { return unwrapped; } } return null; } function collectEnvAliases(sourceFile) { const aliases = new Set(); const emptyContext = { envAliases: new Set() }; const visit = (node) => { if (ts.isVariableDeclaration(node) && node.initializer) { if (ts.isIdentifier(node.name) && processEnvExpression(node.initializer, emptyContext)) { aliases.add(node.name.text); } if (ts.isObjectBindingPattern(node.name)) { const initializer = unwrapExpression(node.initializer); if (ts.isIdentifier(initializer) && initializer.text === "process") { for (const element of node.name.elements) { if (!ts.isIdentifier(element.name)) { continue; } const importedName = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : element.name.text; if (importedName === "env") { aliases.add(element.name.text); } } } } } ts.forEachChild(node, visit); }; visit(sourceFile); return aliases; } function collectGlobalAliases(sourceFile) { const aliases = new Set(); const visit = (node) => { if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { if (isGlobalIdentifier(node.initializer, { globalAliases: new Set() })) { aliases.add(node.name.text); } } ts.forEachChild(node, visit); }; visit(sourceFile); return aliases; } function collectGlobalAgentAliases(sourceFile, globalAliases = collectGlobalAliases(sourceFile)) { const aliases = new Set(); const emptyContext = { globalAgentAliases: new Set(), globalAliases }; const visit = (node) => { if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) { if (globalAgentExpression(node.initializer, emptyContext)) { aliases.add(node.name.text); } } ts.forEachChild(node, visit); }; visit(sourceFile); return aliases; } function globalAgentMutationTarget(expression, context) { const unwrapped = unwrapExpression(expression); if (globalAgentExpression(unwrapped, context)) { return unwrapped; } if ( ts.isPropertyAccessExpression(unwrapped) && globalAgentExpression(unwrapped.expression, context) ) { return forbiddenGlobalAgentKeys.has(unwrapped.name.text) ? unwrapped : null; } if ( ts.isElementAccessExpression(unwrapped) && globalAgentExpression(unwrapped.expression, context) ) { const key = stringLiteralText(unwrapExpression(unwrapped.argumentExpression)); return key && forbiddenGlobalAgentKeys.has(key) ? unwrapped : null; } return null; } function mutationTarget(expression, context) { return envMutationTarget(expression, context) ?? globalAgentMutationTarget(expression, context); } function deleteTarget(expression, context) { const unwrapped = unwrapExpression(expression); return ts.isDeleteExpression(unwrapped) ? mutationTarget(unwrapped.expression, context) : null; } function assignmentTarget(expression, context) { const unwrapped = unwrapExpression(expression); if (ts.isBinaryExpression(unwrapped) && ts.isAssignmentOperator(unwrapped.operatorToken.kind)) { return mutationTarget(unwrapped.left, context); } return null; } function mutatingCallTarget(expression, context) { const unwrapped = unwrapExpression(expression); if (!ts.isCallExpression(unwrapped)) { return null; } const callee = unwrapExpression(unwrapped.expression); if (!ts.isPropertyAccessExpression(callee)) { return null; } const method = callee.name.text; if (method !== "defineProperty" && method !== "assign") { return null; } const receiver = unwrapExpression(callee.expression); if (!ts.isIdentifier(receiver) || receiver.text !== "Object") { return null; } const first = unwrapped.arguments[0] ? unwrapExpression(unwrapped.arguments[0]) : null; if (!first) { return null; } if (method === "assign") { return globalAgentExpression(first, context) ?? processEnvExpression(first, context); } const rawKeyArg = unwrapped.arguments[1] ? unwrapExpression(unwrapped.arguments[1]) : null; const literalKeyArg = rawKeyArg ? stringLiteralText(rawKeyArg) : null; const keyArg = literalKeyArg ?? (rawKeyArg && ts.isIdentifier(rawKeyArg) ? context.stringConstants.get(rawKeyArg.text) : null); if (keyArg && processEnvExpression(first, context)) { return forbiddenEnvKeys.has(keyArg) ? first : null; } if (keyArg && globalAgentExpression(first, context)) { return forbiddenGlobalAgentKeys.has(keyArg) ? first : null; } return null; } export function findManagedProxyRuntimeMutations(content, fileName = "source.ts") { const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.Latest, true); const globalAliases = collectGlobalAliases(sourceFile); const context = { forbiddenKeyVariables: collectForbiddenKeyVariables( sourceFile, collectForbiddenKeyArrays(sourceFile), ), globalAgentAliases: collectGlobalAgentAliases(sourceFile, globalAliases), globalAliases, envAliases: collectEnvAliases(sourceFile), stringConstants: collectStringConstants(sourceFile), }; const mutations = []; const classScopes = []; const scopeStack = []; const visit = (node) => { let pushedClass = false; let pushedScope = false; if (ts.isClassDeclaration(node) && node.name) { classScopes.push(node.name.text); pushedClass = true; } const scopeName = scopeNameForNode(node, classScopes); if (scopeName) { scopeStack.push(scopeName); pushedScope = true; } const match = assignmentTarget(node, context) ?? deleteTarget(node, context) ?? mutatingCallTarget(node, context); if (match) { mutations.push({ line: toLine(sourceFile, match), scope: scopeStack.at(-1) ?? null, }); } ts.forEachChild(node, visit); if (pushedScope) { scopeStack.pop(); } if (pushedClass) { classScopes.pop(); } }; visit(sourceFile); return mutations; } export function findManagedProxyRuntimeMutationLines(content, fileName = "source.ts") { return findManagedProxyRuntimeMutations(content, fileName).map((mutation) => mutation.line); } export function isAllowedManagedProxyRuntimeMutation(violation) { if (!violation.scope) { return false; } return allowedManagedProxyRuntimeMutationScopes.has( `${violation.relativePath}#${violation.scope}`, ); } function formatManagedProxyRuntimeMutationCallsite(violation) { const scope = violation.scope ? ` (${violation.scope})` : ""; return `${violation.relativePath}:${violation.line}${scope}`; } export async function main() { await runCallsiteGuard({ importMetaUrl: import.meta.url, sourceRoots, extraTestSuffixes: [ ".browser.test.ts", ".node.test.ts", ".live.test.ts", ".e2e.test.ts", ".integration.test.ts", ], findCallViolations: findManagedProxyRuntimeMutations, allowViolation: isAllowedManagedProxyRuntimeMutation, formatViolation: formatManagedProxyRuntimeMutationCallsite, header: "Found unmanaged managed-proxy runtime mutation:", footer: "Only proxy lifecycle code may mutate GLOBAL_AGENT or proxy-related process.env runtime state.", }); } runAsScript(import.meta.url, main);