Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type GeneratedUiRuntimeBootstrap = {
appSession?: GeneratedUiAppSessionBootstrap | null
}

export const generatedUiRuntimeModuleSpecifier = '@kody/utils' as const
export const generatedUiRuntimeModuleSpecifier = '@kody/ui-utils' as const

export function buildGeneratedUiRuntimeImportMap(runtimeScriptHref: string) {
const importMapJson = escapeInlineScriptSource(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ export function getKodyWidget(): KodyWidgetPublicApi {
return widget
}
throw new Error(
'kodyWidget is not ready yet. Import `whenKodyWidgetReady()` from `@kody/utils` and await it before using the runtime helpers.',
'kodyWidget is not ready yet. Import `whenKodyWidgetReady()` from `@kody/ui-utils` and await it before using the runtime helpers.',
)
}

Expand Down
4 changes: 2 additions & 2 deletions packages/worker/src/app/saved-ui-hosted-html.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test('renderHostedSavedUiHtml emits shared runtime assets for html apps', () =>
'<script type="module" src="https://kody.example/mcp-apps/generated-ui-runtime.js"></script>',
)
expect(result).toContain(
'<script type="importmap">{"imports":{"@kody/utils":"https://kody.example/mcp-apps/generated-ui-runtime.js"}}</script>',
'<script type="importmap">{"imports":{"@kody/ui-utils":"https://kody.example/mcp-apps/generated-ui-runtime.js"}}</script>',
)
expect(result).toContain(
'window.__kodyGeneratedUiBootstrap = {"mode":"hosted","appSession":{"token":"token-123","endpoints":{"source":"https://kody.example/ui-api/session-123/source","execute":"https://kody.example/ui-api/session-123/execute","secrets":"https://kody.example/ui-api/session-123/secrets","deleteSecret":"https://kody.example/ui-api/session-123/secrets/delete"}}};',
Expand Down Expand Up @@ -69,7 +69,7 @@ test('renderHostedSavedUiHtml keeps user javascript separate from runtime bootst
'<script type="module" src="https://kody.example/mcp-apps/generated-ui-runtime.js"></script>',
)
expect(result).toContain(
'<script type="importmap">{"imports":{"@kody/utils":"https://kody.example/mcp-apps/generated-ui-runtime.js"}}</script>',
'<script type="importmap">{"imports":{"@kody/ui-utils":"https://kody.example/mcp-apps/generated-ui-runtime.js"}}</script>',
)
expect(result).toContain(
'<script type="module">\ndocument.querySelector("[data-generated-ui-root]")?.append("hello")',
Expand Down
2 changes: 1 addition & 1 deletion packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const inputSchema = z.object({
.array(uiArtifactParameterSchema)
.optional()
.describe(
'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` helper from `@kody/utils`.',
'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` helper from `@kody/ui-utils`.',
),
})

Expand Down
2 changes: 1 addition & 1 deletion packages/worker/src/mcp/capabilities/apps/ui-update-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const inputSchema = z
.array(uiArtifactParameterSchema)
.optional()
.describe(
'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` API from `@kody/utils`.',
'Optional parameter definitions for reusable saved apps. Resolved values are exposed at runtime on the imported `kodyWidget.params` API from `@kody/ui-utils`.',
),
})
.refine(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ test('generated_ui_oauth_guide distinguishes PKCE and server-side exchange helpe
)
expect(result.body).toContain('`exchangeOAuthCodeWithSecrets(...)`')
expect(result.body).toContain('run server-side')
expect(result.body).toContain("import { kodyWidget } from '@kody/ui-utils'")
expect(result.body).not.toContain('@kody/utils')
expect(result.body).not.toContain('exchangeOAuthCode({ tokenUrl')
})
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ of treating it like a secret.
4. Give the user the hosted saved-app URL and tell them to open it in their
browser instead of trying to complete the flow in the conversation iframe.
5. In the hosted generated UI, import \`kodyWidget\` from
\`@kody/utils\` and use it directly to read the callback, validate state,
\`@kody/ui-utils\` and use it directly to read the callback, validate state,
exchange the code in the browser, and save tokens.

## Generated UI helpers to use
Expand Down Expand Up @@ -105,7 +105,7 @@ before building the authorization URL or token request.
\`\`\`html
<p id="status"></p>
<script type="module">
import { kodyWidget } from '@kody/utils'
import { kodyWidget } from '@kody/ui-utils'

async function requireClientId() {
const clientIdRecord = await kodyWidget.getValue({
Expand Down Expand Up @@ -188,7 +188,7 @@ authorization flow.
<button type="submit">Save and continue</button>
</form>
<script type="module">
import { kodyWidget } from '@kody/utils'
import { kodyWidget } from '@kody/ui-utils'

document.querySelector('#oauth-client-form')?.addEventListener('submit', async (event) => {
event.preventDefault()
Expand Down Expand Up @@ -240,7 +240,7 @@ Create and persist the OAuth state before redirecting the browser.
\`\`\`html
<button id="connect-muffin-club">Connect Muffin Club</button>
<script type="module">
import { kodyWidget } from '@kody/utils'
import { kodyWidget } from '@kody/ui-utils'

document.querySelector('#connect-muffin-club')?.addEventListener('click', async () => {
const clientIdRecord = await kodyWidget.getValue({
Expand Down Expand Up @@ -340,7 +340,7 @@ succeeds.
root.innerHTML = '<p>' + message + '</p>'
}

import { kodyWidget } from '@kody/utils'
import { kodyWidget } from '@kody/ui-utils'

void (async () => {
const callback = kodyWidget.readOAuthCallback({
Expand Down Expand Up @@ -427,7 +427,7 @@ export const generatedUiOAuthGuideCapability = defineDomainCapability(
'provider registration',
'ui_save_app',
'open_generated_ui',
'@kody/utils',
'@kody/ui-utils',
],
readOnly: true,
idempotent: true,
Expand Down
245 changes: 237 additions & 8 deletions packages/worker/src/mcp/executor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { DynamicWorkerExecutor, type ExecuteResult } from '@cloudflare/codemode'
import {
type ExecuteResult,
type ResolvedProvider,
ToolDispatcher,
normalizeCode,
sanitizeToolName,
} from '@cloudflare/codemode'
import * as acorn from 'acorn'
import { exports as workerExports } from 'cloudflare:workers'
type WorkerLoopbackExports = Exclude<typeof workerExports, undefined>
import { type FetchGatewayProps } from '#mcp/fetch-gateway.ts'
Expand All @@ -10,6 +17,10 @@ import {
parseHostApprovalRequiredMessage,
parseMissingSecretMessage,
} from '#mcp/secrets/errors.ts'
import {
buildCodemodeUtilsModuleSource,
codemodeUtilsModuleSpecifier,
} from '#mcp/tools/codemode-utils.ts'

const charsPerToken = 4
const maxTokens = 6_000
Expand All @@ -26,13 +37,231 @@ export function createExecuteExecutor(input: {
'CodemodeFetchGateway export is required for execute-time fetch.',
)
}
return new DynamicWorkerExecutor({
loader: input.env.LOADER,
timeout: 90_000,
globalOutbound: loopbackExports.CodemodeFetchGateway({
props: input.gatewayProps,
}),
})
return {
execute(code: string, providersOrFns: Array<ResolvedProvider>) {
return executeWithWorkerLoader({
loader: input.env.LOADER,
timeoutMs: 90_000,
code,
providers: providersOrFns,
globalOutbound: loopbackExports.CodemodeFetchGateway({
props: input.gatewayProps,
}),
})
},
}
}

function buildProviderProxySource(provider: ResolvedProvider) {
if (provider.positionalArgs) {
return `new Proxy({}, {
get: (_, toolName) => async (...args) => {
const resJson = await __dispatchers.${provider.name}.call(String(toolName), JSON.stringify(args));
const data = JSON.parse(resJson);
if (data.error) throw new Error(data.error);
return data.result;
}
})`
}
return `new Proxy({}, {
get: (_, toolName) => async (args) => {
const resJson = await __dispatchers.${provider.name}.call(String(toolName), JSON.stringify(args ?? {}));
const data = JSON.parse(resJson);
if (data.error) throw new Error(data.error);
return data.result;
}
})`
}

function buildUserEntryModuleSource(code: string) {
const trimmed = code.trim()
const looksLikeModule =
/^\s*import\s/m.test(trimmed) || /^\s*export\s/m.test(trimmed)
if (looksLikeModule) {
return wrapImportedExecuteCode(trimmed)
}
return `export default ${normalizeCode(code)};`
}

function wrapImportedExecuteCode(source: string) {
try {
const parsed = acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
allowReturnOutsideFunction: true,
})
if (
parsed.body.some((node) => node.type === 'ExportDefaultDeclaration') ||
parsed.body.some(
(node) =>
node.type === 'ExportNamedDeclaration' ||
node.type === 'ExportAllDeclaration',
)
) {
return source
}
const importNodes = parsed.body.filter(
(node) => node.type === 'ImportDeclaration',
)
const bodyNodes = parsed.body.filter(
(node) => node.type !== 'ImportDeclaration',
)
const imports = importNodes
.map((node) => source.slice(node.start, node.end))
.join('\n')
const body = bodyNodes
.map((node, index) => {
if (
index === bodyNodes.length - 1 &&
node.type === 'ExpressionStatement' &&
!node.directive
) {
return `return ${source.slice(node.start, node.end)}`
}
return source.slice(node.start, node.end)
})
.join('\n')
return [
imports,
imports.length > 0 ? '' : undefined,
'export default async () => {',
body,
'}',
]
.filter((part) => typeof part === 'string')
.join('\n')
} catch {
return source
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

function buildUserCodeRunnerModuleSource() {
return [
'import * as __kodyUserModule from "user-entry.js";',
'',
'export async function __kodyRunUserCode() {',
' const __kodyExecuteFunction = __kodyUserModule.default;',
' if (typeof __kodyExecuteFunction !== "function") {',
' throw new Error("Execute code must evaluate to an async function.");',
' }',
' return await __kodyExecuteFunction();',
'}',
].join('\n')
}

async function executeWithWorkerLoader(input: {
loader: WorkerLoader
timeoutMs: number
code: string
providers: Array<ResolvedProvider>
globalOutbound: Fetcher
}): Promise<ExecuteResult> {
const reservedNames = new Set(['__dispatchers', '__logs', '__providers'])
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
const seenNames = new Set<string>()
for (const provider of input.providers) {
if (reservedNames.has(provider.name)) {
return {
result: undefined,
error: `Provider name "${provider.name}" is reserved`,
}
}
if (!validIdentifier.test(provider.name)) {
return {
result: undefined,
error: `Provider name "${provider.name}" is not a valid JavaScript identifier`,
}
}
if (seenNames.has(provider.name)) {
return {
result: undefined,
error: `Duplicate provider name "${provider.name}"`,
}
}
seenNames.add(provider.name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Current provider-name validation and dynamic map writes:"
sed -n '159,227p' packages/worker/src/mcp/executor.ts

echo
echo "2) Trace provider name usage/definition points:"
rg -n -C3 --type=ts '\bResolvedProvider\b|provider\.name|name:\s*string'

Repository: kentcdodds/kody

Length of output: 50372


🏁 Script executed:

# Verify the exact locations and check for other plain object usages
grep -n "Object.create\|const.*=.*{}" packages/worker/src/mcp/executor.ts | head -20

Repository: kentcdodds/kody

Length of output: 273


Harden provider-name handling against prototype-key collisions.

Provider names like __proto__, constructor, and prototype pass validation at lines 159-181 because the validIdentifier regex only checks for syntactic validity, not reserved property names. Subsequent dynamic writes at lines 194 and 215 using plain {} objects can mutate prototypes unexpectedly.

Proposed fix
-	const reservedNames = new Set(['__dispatchers', '__logs', '__providers'])
+	const reservedNames = new Set([
+		'__dispatchers',
+		'__logs',
+		'__providers',
+		'__proto__',
+		'prototype',
+		'constructor',
+	])
@@
-		'    const __providers = {};',
+		'    const __providers = Object.create(null);',
@@
-	const dispatchers = {} as Record<string, ToolDispatcher>
+	const dispatchers: Record<string, ToolDispatcher> = Object.create(null)

Also applies to: 194-199, 215-223

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/executor.ts` around lines 159 - 181, The
provider-name validation must also block prototype/property collision names and
avoid prototype-polluting plain objects: add "__proto__", "prototype", and
"constructor" to the reservedNames set (in addition to
'__dispatchers','__logs','__providers') and keep the syntactic check via
validIdentifier and duplicate check via seenNames; also replace uses of plain
object literals used as maps for providers/logs with objects created via
Object.create(null) (i.e., create maps with null-prototype) where dynamic writes
occur so assigning provider names cannot mutate prototypes.

}

const executorModuleSource = [
'import { WorkerEntrypoint } from "cloudflare:workers";',
'import { __kodyRunUserCode } from "user-code.js";',
'',
'export default class CodeExecutor extends WorkerEntrypoint {',
' async evaluate(__dispatchers = {}) {',
' const __logs = [];',
' console.log = (...a) => { __logs.push(a.map(String).join(" ")); };',
' console.warn = (...a) => { __logs.push("[warn] " + a.map(String).join(" ")); };',
' console.error = (...a) => { __logs.push("[error] " + a.map(String).join(" ")); };',
' const __providers = {};',
...input.providers.flatMap((provider) => [
` const ${provider.name} = ${buildProviderProxySource(provider)};`,
` __providers.${provider.name} = ${provider.name};`,
]),
' globalThis.__kodyProviders = __providers;',
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
' let __timeoutId;',
' const __timeoutPromise = new Promise((_, reject) => {',
` __timeoutId = setTimeout(() => reject(new Error("Execution timed out")), ${input.timeoutMs});`,
' });',
' try {',
' const result = await Promise.race([',
' __kodyRunUserCode(),',
' __timeoutPromise,',
' ]);',
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
' return { result, logs: __logs };',
' } catch (err) {',
' return { result: undefined, error: err instanceof Error ? err.message : String(err), logs: __logs };',
' } finally {',
' if (__timeoutId !== undefined) {',
' clearTimeout(__timeoutId);',
' }',
' delete globalThis.__kodyProviders;',
' }',
' }',
'}',
].join('\n')

const dispatchers = {} as Record<string, ToolDispatcher>
for (const provider of input.providers) {
const sanitizedFns = Object.fromEntries(
Object.entries(provider.fns).map(([name, fn]) => [
sanitizeToolName(name),
fn,
]),
)
dispatchers[provider.name] = new ToolDispatcher(
sanitizedFns,
provider.positionalArgs,
)
}

const entrypoint = input.loader
.get(`codemode-${crypto.randomUUID()}`, () => ({
compatibilityDate: '2025-06-01',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are other compatibility dates used in the codebase for reference
rg -n "compatibilityDate" --type ts -C1

Repository: kentcdodds/kody

Length of output: 552


🏁 Script executed:

#!/bin/bash
# Get full context around line 216 in executor.ts
sed -n '200,230p' packages/worker/src/mcp/executor.ts

Repository: kentcdodds/kody

Length of output: 944


🏁 Script executed:

#!/bin/bash
# Search for any comments or documentation about compatibility dates
rg -i "compatibility" packages/worker/src/ -A 2 -B 2 --type ts | head -50

Repository: kentcdodds/kody

Length of output: 1329


🏁 Script executed:

#!/bin/bash
# Look for any config files that might set or reference compatibility dates
fd -e json -e toml -e yaml -e yml packages/worker | xargs grep -l "2025" 2>/dev/null || echo "No matches"

Repository: kentcdodds/kody

Length of output: 452


🌐 Web query:

Cloudflare Workers compatibility date format valid dates 2025-06-01

💡 Result:

The compatibility date for Cloudflare Workers uses the YYYY-MM-DD date format (e.g., "2022-04-05"). The date "2025-06-01" follows this format exactly and is a valid past date as of today (2026-03-29). Official documentation consistently shows examples in this format, and Wrangler validates dates, rejecting invalid ones (e.g., future dates like "2024-11-05" in 2024 caused issues). Compatibility dates must be in the past or present; the runtime supports all historical dates from 2021-11-02 onward indefinitely. Flags enable at specific dates up to 2026-04-01, confirming support for 2025 dates. Use the latest date like "2026-03-29" for new projects to opt into all changes.

Citations:


Update compatibility date to current version.

The compatibility date 2025-06-01 is outdated. Since we are in March 2026, this date is approximately 9 months old. For optimal compatibility and access to recent runtime features, update this to the current date (e.g., 2026-03-29). Cloudflare Workers supports all historical dates from 2021-11-02 onward, so there is no risk in updating to a newer date.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/executor.ts` at line 216, Update the
compatibilityDate value to the current runtime date: locate the
compatibilityDate key (currently '2025-06-01') in the configuration object in
executor.ts and change its string value to the current date (e.g., '2026-03-29')
so the Workers runtime uses up-to-date feature availability.

compatibilityFlags: ['nodejs_compat'],
mainModule: 'executor.js',
modules: {
[`${codemodeUtilsModuleSpecifier}`]: {
js: buildCodemodeUtilsModuleSource(),
},
'user-entry.js': buildUserEntryModuleSource(input.code),
'user-code.js': buildUserCodeRunnerModuleSource(),
'executor.js': executorModuleSource,
},
globalOutbound: input.globalOutbound,
}))
.getEntrypoint()

const response = (await entrypoint.evaluate(dispatchers)) as ExecuteResult
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (response.error) {
return {
result: undefined,
error: response.error,
logs: response.logs,
}
}
return {
result: response.result,
logs: response.logs,
}
}

export type ExecutionErrorDetails =
Expand Down
2 changes: 1 addition & 1 deletion packages/worker/src/mcp/generated-ui-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ function createGeneratedUiExecuteHandler(env: Env) {
{
ok: false,
error:
'Generated UI executeCode may not return unresolved `{{secret:...}}` placeholders. Secret placeholders only resolve inside secret-aware fetch requests or capability inputs that explicitly opt into `x-kody-secret`. Use persisted values for non-secret configuration, and call `kodyWidget.sendMessage(...)` (for example after `import { kodyWidget } from "@kody/utils"`) when the user needs to intervene.',
'Generated UI executeCode may not return unresolved `{{secret:...}}` placeholders. Secret placeholders only resolve inside secret-aware fetch requests or capability inputs that explicitly opt into `x-kody-secret`. Use persisted values for non-secret configuration, and call `kodyWidget.sendMessage(...)` (for example after `import { kodyWidget } from "@kody/ui-utils"`) when the user needs to intervene.',
logs: result.logs ?? [],
},
400,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('canonical APP_BASE_URL drives runtime script href and MCP URL for widget d
const html = renderGeneratedUiRuntimeHtmlEntry(appBase)
expect(html).toContain('https://heykody.dev/mcp-apps/generated-ui-runtime.js')
expect(html).toContain(
'"@kody/utils":"https://heykody.dev/mcp-apps/generated-ui-runtime.js"',
'"@kody/ui-utils":"https://heykody.dev/mcp-apps/generated-ui-runtime.js"',
)

const mcpServerUrl = new URL(mcpResourcePath, appBase).toString()
Expand Down
Loading
Loading