From ebfe2f211f3a45df95612f7719b3c8e3cd0470d2 Mon Sep 17 00:00:00 2001 From: Kody <72270156+kody-bot@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:21:53 -0600 Subject: [PATCH] fix(local-execute): always load gateway-fetch shim for secret placeholders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Always call package-graph (even with no kody:@ imports) and side-effect import the origin-supplied .__kody_virtual__/runtime.js shim before user code so ambient Bearer {{secret:…}} fetch hops through gatewayFetch instead of leaving workerd raw (kentcdodds/kody#3020). --- src/local-execute.ts | 34 +++++++++---- src/local-package-graph.ts | 7 +-- src/local-runtime-source.ts | 17 ++++++- test/local-execute.test.ts | 86 ++++++++++++++++++++++++++++++-- test/local-package-graph.test.ts | 46 +++++++++++++++++ 5 files changed, 171 insertions(+), 19 deletions(-) diff --git a/src/local-execute.ts b/src/local-execute.ts index 0d36393..80da5c4 100644 --- a/src/local-execute.ts +++ b/src/local-execute.ts @@ -23,6 +23,7 @@ import { apiTokenEnvVar } from './defaults.js' import { fetchLocalPackageGraph, hasSavedPackageImports, + localExecuteGatewayFetchShimModuleName, type LocalPackageGraph, } from './local-package-graph.js' import { @@ -67,21 +68,28 @@ const workerdExitGraceMs = 1_000 export const savedPackageImportLocalResolveStatus = 'Module imports saved packages (kody:@…). Fetching stamped package modules for local workerd bundling (no cloud kody.execute defer).' +export const localExecutePackageGraphResolveStatus = + 'Fetching local-execute package graph (gateway-fetch shim + any stamped kody:@ modules).' + export async function runLocalExecute(input: LocalExecuteInput): Promise { assertLocalExecuteNodeEngine() assertTokenSafeApiUrl(input.apiUrl) const client = { apiUrl: input.apiUrl, token: input.token, fetchFn: input.fetchFn } await openCapabilityProxySession(client) - let packageGraph: LocalPackageGraph = { modules: [], imports: [] } - if (hasSavedPackageImports(input.code)) { - input.onStatus?.(savedPackageImportLocalResolveStatus) - packageGraph = await fetchLocalPackageGraph({ - ...client, - code: input.code, - conversationId: input.conversationId, - }) - } + // Always fetch package-graph: origin returns the gateway-fetch shim even + // when there are no `kody:@` imports so ambient `{{secret:…}}` fetch hops + // (or fails closed) instead of sending a raw placeholder (kody#3020). + input.onStatus?.( + hasSavedPackageImports(input.code) + ? savedPackageImportLocalResolveStatus + : localExecutePackageGraphResolveStatus, + ) + const packageGraph = await fetchLocalPackageGraph({ + ...client, + code: input.code, + conversationId: input.conversationId, + }) const workerdPath = input.workerdPath ?? @@ -119,7 +127,13 @@ export async function runLocalExecute(input: LocalExecuteInput): Promise module.name) + .filter((name) => name === localExecuteGatewayFetchShimModuleName) + await writeFile( + join(workDir, files.entry), + createLocalEntrySource({ sideEffectModules: gatewayFetchShimModules }), + ) await writeFile(join(workDir, files.user), input.code) await writeFile(join(workDir, files.runtime), createLocalRuntimeModuleSource()) const packageModules = await writePackageModuleFiles(workDir, packageGraph) diff --git a/src/local-package-graph.ts b/src/local-package-graph.ts index 62d0682..41f18bb 100644 --- a/src/local-package-graph.ts +++ b/src/local-package-graph.ts @@ -78,14 +78,15 @@ export class LocalPackageGraphError extends Error { * Fetch published, stamped package modules for local workerd embedding. * Never falls back to CapabilityProxy → `kody.execute`. */ +/** Exact workerd module name for the origin-supplied gateway-fetch shim. */ +export const localExecuteGatewayFetchShimModuleName = + '.__kody_virtual__/runtime.js' + export async function fetchLocalPackageGraph( input: CapabilityProxyClientInput & { code: string; conversationId?: string }, ): Promise { assertTokenSafeApiUrl(input.apiUrl) const imports = listSavedPackageImports(input.code) - if (imports.length === 0) { - return { modules: [], imports: [] } - } if (hasLiteralDynamicSavedPackageImports(input.code)) { throw new LocalPackageGraphError( `Local execute cannot bind literal dynamic import("kody:@…") yet — use a static import, or wait for runtime package resolution (${localPackageGraphPlatformIssueUrl}).`, diff --git a/src/local-runtime-source.ts b/src/local-runtime-source.ts index d9f22cc..6c60ee1 100644 --- a/src/local-runtime-source.ts +++ b/src/local-runtime-source.ts @@ -244,9 +244,22 @@ export function __kodyRunInLocalRuntime(callback) { `.trimStart() } -export function createLocalEntrySource(): string { +/** + * Local workerd entry. Optional `sideEffectModules` are imported for their + * evaluation side effects before the user module runs — used to load the + * origin-supplied gateway-fetch shim so ambient `{{secret:…}}` fetch hops + * even when the entry has no `kody:@` imports (kentcdodds/kody#3020). + */ +export function createLocalEntrySource(input: { + sideEffectModules?: ReadonlyArray +} = {}): string { + const sideEffectImports = [...new Set(input.sideEffectModules ?? [])] + .filter((name) => typeof name === 'string' && name.trim().length > 0) + .map((name) => `import ${JSON.stringify(name)};`) + .join('\n') + const sideEffectBlock = sideEffectImports ? `${sideEffectImports}\n` : '' return ` -import { +${sideEffectBlock}import { __kodyInstallLocalBridge, __kodyRunInLocalRuntime, } from ${JSON.stringify(localWorkerModuleNames.runtime)}; diff --git a/test/local-execute.test.ts b/test/local-execute.test.ts index cdf2f71..40b5dfd 100644 --- a/test/local-execute.test.ts +++ b/test/local-execute.test.ts @@ -11,11 +11,24 @@ import { runCli } from '../src/cli.js' import { buildLocalExecuteResult, hasSavedPackageImports, + localExecutePackageGraphResolveStatus, runLocalExecute, savedPackageImportLocalResolveStatus, } from '../src/local-execute.js' -import { createWorkerdConfig } from '../src/local-runtime-source.js' -import { localPackageGraphPath, localPackageGraphPlatformIssueUrl } from '../src/local-package-graph.js' +import { + createLocalEntrySource, + createWorkerdConfig, +} from '../src/local-runtime-source.js' +import { + localExecuteGatewayFetchShimModuleName, + localPackageGraphPath, + localPackageGraphPlatformIssueUrl, +} from '../src/local-package-graph.js' + +const gatewayFetchShimModule = { + name: localExecuteGatewayFetchShimModuleName, + esModule: 'export {}\n', +} const goodToken = 'kody_tok_good' @@ -94,6 +107,7 @@ before(async () => { if (imports.includes('kody:@test/pkg/hello')) { send(200, { imports: ['kody:@test/pkg/hello'], + warnings: [], modules: [ { name: 'kody:@test/pkg/hello', @@ -103,10 +117,21 @@ before(async () => { export default greet `, }, + gatewayFetchShimModule, ], }) return } + // Origin always returns the gateway-fetch shim, including when the + // entry has no kody:@ imports (kentcdodds/kody#3020). + if (imports.length === 0) { + send(200, { + imports: [], + warnings: [], + modules: [gatewayFetchShimModule], + }) + return + } send(404, { error: { code: 'not_found', message: 'package-graph not deployed' } }) return } @@ -164,6 +189,19 @@ test('hasSavedPackageImports detects static and dynamic kody:@ imports', () => { assert.equal(hasSavedPackageImports(`import { kody } from 'kody:runtime'`), false) }) +test('createLocalEntrySource side-effect imports the gateway-fetch shim before user code', () => { + const source = createLocalEntrySource({ + sideEffectModules: [localExecuteGatewayFetchShimModuleName], + }) + assert.match( + source, + new RegExp( + `^import ${JSON.stringify(localExecuteGatewayFetchShimModuleName)};`, + ), + ) + assert.match(source, /import\(\s*"\.\/main\.js"\s*\)/) +}) + test( 'runLocalExecute with kody:@ imports bundles package modules into local workerd', { timeout: 180_000 }, @@ -343,11 +381,12 @@ export default async function main(params) { assert.deepEqual(structured.logs, ['hello kent', '[warn] careful']) assert.equal(structured.conversationId, 'conv-local') assert.equal(proxyRequests[0]?.url, '/v1/capability-proxy/session') - assert.equal(proxyRequests.length, 5) + assert.equal(proxyRequests[1]?.url, `/${localPackageGraphPath}`) + assert.equal(proxyRequests.length, 6) for (const request of proxyRequests) { assert.equal(request.authorization, `Bearer ${goodToken}`) } - assert.deepEqual(proxyRequests[1]?.body, { + assert.deepEqual(proxyRequests[2]?.body, { path: ['kody', 'emailSend'], args: [{ to: 'kent' }], conversationId: 'conv-local', @@ -355,6 +394,45 @@ export default async function main(params) { }, ) +test( + 'runLocalExecute always fetches package-graph for Bearer secret placeholders with no kody:@ imports', + { timeout: 180_000 }, + async () => { + reset() + const statuses: Array = [] + // Keep the Bearer {{secret:…}} shape in-source (kentcdodds/kody#3020) but + // do not fetch outbound — this asserts CLI wiring only (always call + // package-graph + side-effect import the returned shim). Origin expands + // or fails closed once the real shim wraps globalThis.fetch. + const code = `export default async function main() { + const authorization = 'Bearer {{secret:cloudflarePagesApiToken}}' + return { authorization } +}` + const result = await runLocalExecute({ + code, + token: goodToken, + apiUrl, + onStatus: (message) => statuses.push(message), + }) + assert.equal(result.isError, false) + assert.ok(statuses.includes(localExecutePackageGraphResolveStatus)) + assert.equal( + proxyRequests.some((request) => request.url === `/${localPackageGraphPath}`), + true, + ) + const graphRequest = proxyRequests.find( + (request) => request.url === `/${localPackageGraphPath}`, + ) + assert.deepEqual( + (graphRequest?.body as { imports?: Array } | null)?.imports, + [], + ) + assert.deepEqual((result.structuredContent as { result: unknown }).result, { + authorization: 'Bearer {{secret:cloudflarePagesApiToken}}', + }) + }, +) + test( 'runLocalExecute reports module errors like cloud execute', { timeout: 180_000 }, diff --git a/test/local-package-graph.test.ts b/test/local-package-graph.test.ts index 5ea9b48..bf1f1ca 100644 --- a/test/local-package-graph.test.ts +++ b/test/local-package-graph.test.ts @@ -6,6 +6,7 @@ import { hasSavedPackageImports, listSavedPackageImports, LocalPackageGraphError, + localExecuteGatewayFetchShimModuleName, localPackageGraphPath, localPackageGraphPlatformIssueUrl, } from '../src/local-package-graph.js' @@ -61,6 +62,51 @@ export default async () => m` ) }) +test('fetchLocalPackageGraph posts even when there are no kody:@ imports to load the gateway-fetch shim', async () => { + const code = `export default async function main() { + return fetch('https://api.example.com', { + headers: { authorization: 'Bearer {{secret:cloudflarePagesApiToken}}' }, + }) +}` + const requests: Array<{ url: string; body: unknown }> = [] + const result = await fetchLocalPackageGraph({ + apiUrl: 'https://api.kody.codes', + token: goodToken, + code, + fetchFn: async (input, init) => { + requests.push({ + url: String(input), + body: init?.body ? JSON.parse(String(init.body)) : null, + }) + return new Response( + JSON.stringify({ + imports: [], + warnings: [], + modules: [ + { + name: localExecuteGatewayFetchShimModuleName, + esModule: 'export {}\n', + }, + ], + }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ) + }, + }) + assert.equal(requests.length, 1) + assert.match(requests[0]!.url, new RegExp(`${localPackageGraphPath}$`)) + assert.deepEqual(requests[0]!.body, { code, imports: [] }) + assert.deepEqual(result, { + imports: [], + modules: [ + { + name: localExecuteGatewayFetchShimModuleName, + esModule: 'export {}\n', + }, + ], + }) +}) + test('fetchLocalPackageGraph posts code + imports and returns embeddable modules', async () => { const code = `import { greet } from 'kody:@test/pkg/hello' export default async function main(params) { return greet(params.name) }`