Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions src/local-execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { apiTokenEnvVar } from './defaults.js'
import {
fetchLocalPackageGraph,
hasSavedPackageImports,
localExecuteGatewayFetchShimModuleName,
type LocalPackageGraph,
} from './local-package-graph.js'
import {
Expand Down Expand Up @@ -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<ToolCallResult> {
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 ??
Expand Down Expand Up @@ -119,7 +127,13 @@ export async function runLocalExecute(input: LocalExecuteInput): Promise<ToolCal
}),
})
const files = { entry: 'entry.js', user: 'main.js', runtime: 'runtime.js' }
await writeFile(join(workDir, files.entry), createLocalEntrySource())
const gatewayFetchShimModules = packageGraph.modules
.map((module) => 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)
Expand Down
7 changes: 4 additions & 3 deletions src/local-package-graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocalPackageGraph> {
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}).`,
Expand Down
17 changes: 15 additions & 2 deletions src/local-runtime-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
} = {}): 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)};
Expand Down
86 changes: 82 additions & 4 deletions test/local-execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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',
Expand All @@ -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
}
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -343,18 +381,58 @@ 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',
})
},
)

test(
'runLocalExecute always fetches package-graph for Bearer secret placeholders with no kody:@ imports',
{ timeout: 180_000 },
async () => {
reset()
const statuses: Array<string> = []
// 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<string> } | 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 },
Expand Down
46 changes: 46 additions & 0 deletions test/local-package-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
hasSavedPackageImports,
listSavedPackageImports,
LocalPackageGraphError,
localExecuteGatewayFetchShimModuleName,
localPackageGraphPath,
localPackageGraphPlatformIssueUrl,
} from '../src/local-package-graph.js'
Expand Down Expand Up @@ -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) }`
Expand Down
Loading