From 042eb5d849aef6232c6b4997588a4d2a06b7e8ff Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Jul 2026 00:05:42 +0000 Subject: [PATCH] Fix execute codegen __name regression from bundled toString() PR #628 inlined createKodyRemoteProxy via Function.prototype.toString() into every generated execute module. Production wrangler/esbuild rewrites named functions to use a bundle-scoped __name helper, so dynamically loaded executor modules fail at init with '__name is not defined'. Replace the serialized factory with a static source template, add regression coverage, and gate production deploys with an authenticated execute smoke check via POST /__maintenance/execute-smoke. --- .github/workflows/deploy.yml | 43 ++++++++++ .../src/execute-maintenance.node.test.ts | 80 ++++++++++++++++++ packages/worker/src/execute-maintenance.ts | 60 ++++++++++++++ packages/worker/src/index.ts | 5 ++ packages/worker/src/mcp/executor.node.test.ts | 31 +++++++ packages/worker/src/mcp/executor.ts | 17 +++- .../src/mcp/kody-remote-proxy-source.ts | 81 +++++++++++++++++++ 7 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 packages/worker/src/execute-maintenance.node.test.ts create mode 100644 packages/worker/src/execute-maintenance.ts create mode 100644 packages/worker/src/mcp/kody-remote-proxy-source.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 02cb2c0134..364a1a9b68 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -251,6 +251,49 @@ jobs: fi exit 1 + - name: 🧪 Execute smoke check (production) + shell: bash + env: + DEPLOY_URL: ${{ steps.deploy.outputs.url }} + CAPABILITY_REINDEX_SECRET: ${{ secrets.CAPABILITY_REINDEX_SECRET }} + run: | + set -euo pipefail + if [ -z "${CAPABILITY_REINDEX_SECRET:-}" ]; then + echo "Skipping execute smoke check (CAPABILITY_REINDEX_SECRET not set)." + exit 0 + fi + if [ -z "${DEPLOY_URL:-}" ]; then + echo "Missing deploy URL output; cannot run execute smoke check." >&2 + exit 1 + fi + + base="${DEPLOY_URL%/}" + smoke_url="${base}/__maintenance/execute-smoke" + echo "POST ${smoke_url}" + + attempts=10 + delay_seconds=3 + for i in $(seq 1 "$attempts"); do + echo "Attempt $i/$attempts" + status="$(curl --silent --show-error --location --max-time 30 \ + -X POST \ + -H "Authorization: Bearer ${CAPABILITY_REINDEX_SECRET}" \ + -H "Accept: application/json" \ + --output execute-smoke.json \ + --write-out "%{http_code}" \ + "${smoke_url}")" + cat execute-smoke.json + echo + if [ "$status" -ge 200 ] && [ "$status" -lt 300 ] && \ + node -e "const fs = require('node:fs'); const json = JSON.parse(fs.readFileSync('execute-smoke.json','utf8')); if (json?.ok !== true || json?.result !== 42) { console.error('execute-smoke-unexpected-response', json); process.exit(1); } console.log('execute-smoke-ok', json);"; then + exit 0 + fi + sleep "$delay_seconds" + done + + echo "Execute smoke check failed after ${attempts} attempts: ${smoke_url}" >&2 + exit 1 + - name: 🧭 Reindex capability vectors (Vectorize) shell: bash env: diff --git a/packages/worker/src/execute-maintenance.node.test.ts b/packages/worker/src/execute-maintenance.node.test.ts new file mode 100644 index 0000000000..c5df79b6ab --- /dev/null +++ b/packages/worker/src/execute-maintenance.node.test.ts @@ -0,0 +1,80 @@ +import { expect, test, vi } from 'vitest' + +const createExecuteExecutorMock = vi.hoisted(() => vi.fn()) + +vi.mock('cloudflare:workers', () => ({ + exports: { + KodyFetchGateway: () => ({}), + }, +})) + +vi.mock('#mcp/executor.ts', () => ({ + createExecuteExecutor: (...args: Array) => + createExecuteExecutorMock(...args), +})) + +const { handleExecuteSmokeRequest, runExecuteSmokeCheck } = + await import('./execute-maintenance.ts') + +test('runExecuteSmokeCheck executes trivial code through the dynamic worker path', async () => { + const execute = vi.fn(async () => ({ result: 42, logs: [] })) + createExecuteExecutorMock.mockReturnValue({ execute }) + + const env = { + APP_BASE_URL: 'https://example.com', + LOADER: {}, + } as Env + + await expect(runExecuteSmokeCheck(env)).resolves.toEqual({ result: 42 }) + expect(createExecuteExecutorMock).toHaveBeenCalledWith( + expect.objectContaining({ + timeoutMs: 10_000, + gatewayProps: { + baseUrl: 'https://example.com', + userId: null, + storageContext: null, + }, + }), + ) + expect(execute).toHaveBeenCalledWith('async () => 42', [ + { + name: 'kody', + fns: {}, + kodyRemoteConnectors: [], + }, + ]) +}) + +test('handleExecuteSmokeRequest enforces auth and reports smoke results', async () => { + const execute = vi.fn(async () => ({ result: 42, logs: [] })) + createExecuteExecutorMock.mockReturnValue({ execute }) + + const env = { + CAPABILITY_REINDEX_SECRET: 'secret', + APP_BASE_URL: 'https://example.com', + LOADER: {}, + } as Env + + const unauthorized = await handleExecuteSmokeRequest( + new Request('https://example.com/__maintenance/execute-smoke', { + method: 'POST', + }), + env, + ) + expect(unauthorized.status).toBe(401) + + const success = await handleExecuteSmokeRequest( + new Request('https://example.com/__maintenance/execute-smoke', { + method: 'POST', + headers: { + Authorization: 'Bearer secret', + }, + }), + env, + ) + expect(success.status).toBe(200) + await expect(success.json()).resolves.toEqual({ + ok: true, + result: 42, + }) +}) diff --git a/packages/worker/src/execute-maintenance.ts b/packages/worker/src/execute-maintenance.ts new file mode 100644 index 0000000000..96673ed4bd --- /dev/null +++ b/packages/worker/src/execute-maintenance.ts @@ -0,0 +1,60 @@ +import { exports as workerExports } from 'cloudflare:workers' +import { createExecuteExecutor } from '#mcp/executor.ts' +import { handleSecretMaintenanceRequest } from './maintenance-handler.ts' +import { getErrorMessage } from '#mcp/capabilities/error-message.ts' + +const executeSmokeCode = 'async () => 42' +const executeSmokeProviders = [ + { + name: 'kody', + fns: {}, + kodyRemoteConnectors: [], + }, +] as const + +export async function runExecuteSmokeCheck(env: Env) { + if (!env.LOADER) { + throw new Error('Execute smoke check requires the LOADER binding.') + } + if (!workerExports?.KodyFetchGateway) { + throw new Error( + 'Execute smoke check requires the KodyFetchGateway worker export.', + ) + } + + const origin = env.APP_BASE_URL?.trim() || 'https://heykody.dev' + const executor = createExecuteExecutor({ + env, + exports: workerExports, + gatewayProps: { + baseUrl: origin, + userId: null, + storageContext: null, + }, + timeoutMs: 10_000, + }) + const result = await executor.execute(executeSmokeCode, [ + ...executeSmokeProviders, + ]) + if (result.error) { + throw new Error(`Execute smoke check failed: ${result.error}`) + } + if (result.result !== 42) { + throw new Error( + `Execute smoke check returned unexpected result: ${getErrorMessage(result.result)}`, + ) + } + return { result: result.result } +} + +export async function handleExecuteSmokeRequest( + request: Request, + env: Env, +): Promise { + return handleSecretMaintenanceRequest({ + request, + secret: env.CAPABILITY_REINDEX_SECRET, + notConfiguredMessage: 'Execute smoke check is not configured', + run: async () => await runExecuteSmokeCheck(env), + }) +} diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index f465a8336d..0c1246d7bb 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -38,6 +38,7 @@ import { isNonProductionRuntime } from '#app/deployment-env.ts' import { checkRateLimit, authRateLimitConfig } from '#app/rate-limit.ts' import { getRequestIp } from '#app/audit-log.ts' import { handleCapabilityReindexRequest } from './capability-maintenance.ts' +import { handleExecuteSmokeRequest } from './execute-maintenance.ts' import { handleJobReindexRequest } from './job-maintenance.ts' import { handleMemoryReindexRequest } from './memory-maintenance.ts' import { reconcileArtifactsPushes } from './jobs/reconcile-artifacts-pushes.ts' @@ -211,6 +212,10 @@ const appHandler = withCors({ return handleCapabilityReindexRequest(request, env) } + if (url.pathname === '/__maintenance/execute-smoke') { + return handleExecuteSmokeRequest(request, env) + } + if (url.pathname === '/__maintenance/reindex-memories') { return handleMemoryReindexRequest(request, env) } diff --git a/packages/worker/src/mcp/executor.node.test.ts b/packages/worker/src/mcp/executor.node.test.ts index 616574412b..e6cfb86357 100644 --- a/packages/worker/src/mcp/executor.node.test.ts +++ b/packages/worker/src/mcp/executor.node.test.ts @@ -11,12 +11,14 @@ import { createKodyRemoteProxy, createKodyProviderProxySource, createExecuteExecutor, + createExecutorModuleSource, extractRawContent, formatExecutionOutput, formatLimitedExecutionOutput, getExecutionErrorDetails, limitExecutionResultValue, } from './executor.ts' +import { assertGeneratedExecutorSourceIsBundleSafe } from './kody-remote-proxy-source.ts' type FakeWorkerOptions = Record @@ -152,6 +154,35 @@ test('kody remote proxy dispatches and reports connector/capability errors clear ) }) +test('generated kody provider source avoids bundle-scoped __name helpers', () => { + const source = createKodyProviderProxySource({ + providerName: 'kody', + remoteConnectors: [], + }) + expect(source).not.toMatch(/\b__name\b/u) + assertGeneratedExecutorSourceIsBundleSafe(source) + + const bundledFactorySource = `const __kodyCreateRemoteProxy = __name(function createKodyRemoteProxy() { return {}; }, "createKodyRemoteProxy");` + expect(bundledFactorySource).toMatch(/\b__name\b/u) + expect(() => new Function('__dispatchers', bundledFactorySource)({})).toThrow( + /__name is not defined/u, + ) + + const moduleSource = createExecutorModuleSource({ + code: 'async () => "ok"', + providers: [ + { + name: 'kody', + fns: {}, + kodyRemoteConnectors: [], + }, + ], + shadowGlobalThis: false, + timeoutMs: 1_000, + }) + assertGeneratedExecutorSourceIsBundleSafe(moduleSource) +}) + test('generated kody provider source wires remote proxy dispatch', async () => { const calls: Array<{ name: string; argsJson: string }> = [] const source = createKodyProviderProxySource({ diff --git a/packages/worker/src/mcp/executor.ts b/packages/worker/src/mcp/executor.ts index 03fa9fdcd5..b0fd345034 100644 --- a/packages/worker/src/mcp/executor.ts +++ b/packages/worker/src/mcp/executor.ts @@ -29,6 +29,10 @@ import { type KodyRemoteConnectorMetadata, type KodyResolvedProvider, } from '#mcp/kody-remote-types.ts' +import { + assertGeneratedExecutorSourceIsBundleSafe, + kodyRemoteProxyFactorySource, +} from '#mcp/kody-remote-proxy-source.ts' type WorkerLoopbackExports = Exclude @@ -383,6 +387,15 @@ function createExecutorModule(input: { ].join('\n') } +export function createExecutorModuleSource(input: { + code: string + providers: Array + shadowGlobalThis: boolean + timeoutMs: number +}) { + return createExecutorModule(input) +} + function createProviderProxySource(provider: ResolvedProvider) { const kodyProvider = provider as KodyResolvedProvider if (provider.name === 'kody' && kodyProvider.kodyRemoteConnectors) { @@ -401,7 +414,7 @@ export function createKodyProviderProxySource(input: { remoteConnectors: Array }) { const metadataJson = JSON.stringify(input.remoteConnectors) - return ` const __kodyCreateRemoteProxy = ${createKodyRemoteProxy.toString()}; + const source = ` const __kodyCreateRemoteProxy = ${kodyRemoteProxyFactorySource}; const __kodyRemote = __kodyCreateRemoteProxy({ remoteConnectors: ${metadataJson}, callTool: async (dispatchName, args) => { @@ -427,6 +440,8 @@ export function createKodyProviderProxySource(input: { }; } });` + assertGeneratedExecutorSourceIsBundleSafe(source) + return source } function createToolDispatchers( diff --git a/packages/worker/src/mcp/kody-remote-proxy-source.ts b/packages/worker/src/mcp/kody-remote-proxy-source.ts new file mode 100644 index 0000000000..cffb5c9c10 --- /dev/null +++ b/packages/worker/src/mcp/kody-remote-proxy-source.ts @@ -0,0 +1,81 @@ +// Static factory source for dynamic execute modules. Do not serialize bundled +// functions with Function.prototype.toString() here — production wrangler/esbuild +// can rewrite named functions to use a bundle-scoped __name helper that does not +// exist inside dynamically loaded worker modules. +export const kodyRemoteProxyFactorySource = [ + 'function createKodyRemoteProxy(input) {', + ' const connectors = Object.fromEntries(', + ' input.remoteConnectors.map((connector) => [', + ' connector.name,', + ' {', + ' ...connector,', + ' capabilitiesByName: Object.fromEntries(', + ' connector.capabilities.map((capability) => [', + ' capability.name,', + ' capability,', + ' ]),', + ' ),', + ' },', + ' ]),', + ' );', + ' const formatNames = (names) =>', + ' names.length > 0', + ' ? names.map((name) => JSON.stringify(name)).join(", ")', + ' : "none";', + '', + ' return new Proxy(', + ' {},', + ' {', + ' get(_target, connectorName) {', + ' if (typeof connectorName === "symbol" || connectorName === "then") {', + ' return undefined;', + ' }', + ' const normalizedConnectorName = String(connectorName);', + ' const connector = connectors[normalizedConnectorName];', + ' if (!connector) {', + ' throw new Error(', + ' "Unknown remote connector \\"" + normalizedConnectorName + "\\". Available remote connectors: " + formatNames(Object.keys(connectors)) + ".",', + ' );', + ' }', + ' return new Proxy(', + ' {},', + ' {', + ' get(_connectorTarget, capabilityName) {', + ' if (', + ' typeof capabilityName === "symbol" ||', + ' capabilityName === "then"', + ' ) {', + ' return undefined;', + ' }', + ' const normalizedCapabilityName = String(capabilityName);', + ' if (', + ' !connector.status.connected ||', + ' connector.status.toolCount === 0', + ' ) {', + ' throw new Error(connector.status.unavailableMessage);', + ' }', + ' const capability =', + ' connector.capabilitiesByName[normalizedCapabilityName];', + ' if (!capability) {', + ' throw new Error(', + ' "Unknown remote capability \\"" + normalizedCapabilityName + "\\" for connector \\"" + normalizedConnectorName + "\\". Available capabilities: " + formatNames(connector.capabilities.map((entry) => entry.name)) + ".",', + ' );', + ' }', + ' return async (args) =>', + ' await input.callTool(capability.dispatchName, args);', + ' },', + ' },', + ' );', + ' },', + ' },', + ' );', + '}', +].join('\n') + +export function assertGeneratedExecutorSourceIsBundleSafe(source: string) { + if (/\b__name\b/u.test(source)) { + throw new Error( + 'Generated executor source must not reference bundle-scoped __name helpers.', + ) + } +}