Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -53,21 +53,26 @@ export default async () => {
'/pages/',
]
const assertAllowedPath = (path) => {
const p = path.trim()
if (!p.startsWith('/')) {
if (path !== path.trim()) {
throw new Error('path must not have leading or trailing whitespace')
}
if (!path.startsWith('/')) {
throw new Error('path must start with / and must not include a host')
}
if (!PREFIXES.some((prefix) => p.startsWith(prefix))) {
if (path.length > 2048) throw new Error('path exceeds maximum length')
if (/[\s#]/.test(path))
throw new Error('path contains disallowed characters')
const url = new URL(path, ORIGIN)
const pathname = url.pathname
if (!PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
throw new Error(`path must start with one of: ${PREFIXES.join(', ')}`)
}
if (p.includes('..')) throw new Error('path must not contain ..')
if (/[\s#]/.test(p)) throw new Error('path contains disallowed characters')
if (p.length > 2048) throw new Error('path exceeds maximum length')
if (pathname.includes('..')) throw new Error('path must not contain ..')
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
return url
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
}

const path = '/api/resources/accounts/'
assertAllowedPath(path)
const url = new URL(path, ORIGIN).toString()
const url = assertAllowedPath(path).toString()
const res = await fetch(url, { headers: { Accept: MARKDOWN_ACCEPT } })
const body = await res.text()
return {
Expand Down
4 changes: 2 additions & 2 deletions docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,9 @@ declaring package's own bucket no matter where the code runs:
```ts
import { packageStorage } from 'kody:runtime'

export default async function listSkills() {
export default async function listItems() {
const bucket = packageStorage()
const result = await bucket.sql('select name from skills order by name asc')
const result = await bucket.sql('select name from items order by name asc')
return result.rows.map((row) => row.name)
}
```
Expand Down
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"@sentry/cloudflare": "^10.45.0",
"@simplewebauthn/browser": "^13.3.0",
"@simplewebauthn/server": "^13.3.2",
"acorn": "^8.16.0",
"agents": ">=0.11.5 <0.11.8",
"croner": "^10.0.1",
"diff": "^9.0.0",
Expand Down
6 changes: 1 addition & 5 deletions packages/worker/src/jobs/service.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,7 @@ function mockRepoPersistence() {
id: sourceId,
user_id: String(existing['user_id']),
entity_kind:
(existing['entity_kind'] as
| 'job'
| 'package'
| 'skill'
| 'app') ?? 'job',
(existing['entity_kind'] as 'job' | 'package') ?? 'job',
entity_id: String(existing['entity_id'] ?? sourceId),
repo_id: String(existing['repo_id'] ?? sourceId),
published_commit: 'published-commit-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
import { resolveOwnedPackageSource } from '#mcp/capabilities/packages/resolve-package-source.ts'
import { getEntitySourceById } from '#worker/repo/entity-sources.ts'
import {
legacyKodyPublishGitNoteSchema,
kodyPublishGitNoteSchema,
readPublishGitNoteFromArtifactsRepo,
} from '#worker/repo/publish-git-notes.ts'

Expand Down Expand Up @@ -47,7 +47,7 @@ const outputSchema = z.object({
source_id: z.string(),
repo_id: z.string(),
raw_note: z.string().nullable(),
note: legacyKodyPublishGitNoteSchema.nullable(),
note: kodyPublishGitNoteSchema.nullable(),
})

export const repoShowPublishNoteCapability = defineDomainCapability(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Minimal Cloudflare API v4 HTTP client (Bearer token) for internal Worker-side
* Cloudflare API calls and tests. User-facing Cloudflare API access is via
* saved package exports and secret-aware `fetch`; see
* `docs/contributing/skill-patterns/cloudflare-api-v4.md`.
* `docs/contributing/execute-patterns/cloudflare-api-v4.md`.
*
* @see https://developers.cloudflare.com/fundamentals/api/how-to/make-api-calls/
*/
Expand Down
187 changes: 7 additions & 180 deletions packages/worker/src/mcp/run-kody-registry.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
buildKodyFns,
buildKodyProvider,
createWorkflowTools,
runKodyWithRegistry,
runBundledModuleWithRegistry,
runModuleWithRegistry,
} from './run-kody-registry.ts'
Expand Down Expand Up @@ -1242,7 +1241,7 @@ test('buildKodyFns rejects storage kody tools that collide with capabilities', a
}
})

test('runKodyWithRegistry redacts secret keys and survives cyclic results', async () => {
test('runModuleWithRegistry redacts secret keys and survives cyclic results', async () => {
silenceIncidentalRuntimeWarnings()
const env = {} as Env
const callerContext = createMcpCallerContext({
Expand Down Expand Up @@ -1413,125 +1412,7 @@ export default async function run() {
}
})

test('runKodyWithRegistry routes module and snippet inputs through the expected execution path', async () => {
silenceIncidentalRuntimeWarnings()
const env = {} as Env
const callerContext = createMcpCallerContext({
baseUrl: 'https://heykody.dev',
user: { userId: 'user-123' },
})
const buildBundleMock = vi.mocked(buildKodyModuleBundle)
buildBundleMock.mockClear()
const getRegistrySpy = vi
.spyOn(
await import('#mcp/capabilities/registry.ts'),
'getCapabilityRegistryForContext',
)
.mockResolvedValue({
capabilityDomains: [],
capabilityDomainDescriptionsByName: {} as Record<string, string>,
capabilityHandlers: {},
capabilityList: [],
capabilityMap: {},
capabilitySpecs: {},
capabilityToolDescriptors: {},
} as Awaited<ReturnType<typeof getCapabilityRegistryForContext>>)
const createExecuteExecutorSpy = vi
.spyOn(await import('#mcp/executor.ts'), 'createExecuteExecutor')
.mockReturnValue({
async execute() {
return {
result: 'ok',
logs: [],
}
},
} as never)

try {
const moduleCode = `import { kody } from 'kody:runtime'

export default async function run() {
return await kody.meta_list_capabilities({})
}`
const fencedModuleCode = `\`\`\`ts
import { kody } from 'kody:runtime'

export default async function run() {
return await kody.meta_list_capabilities({})
}
\`\`\``
const typescriptModuleCode = `import type { ExecuteResult } from '@cloudflare/codemode'

type ModuleOutput = ExecuteResult | null

export default async function run(): Promise<ModuleOutput> {
return null
}`
const moduleResult = await runKodyWithRegistry(
env,
callerContext,
moduleCode,
)
expect(moduleResult.result).toBe('ok')
expect(buildBundleMock).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
sourceFiles: {
'entry.ts': moduleCode,
},
entryPoint: 'entry.ts',
}),
)

const fencedModuleResult = await runKodyWithRegistry(
env,
callerContext,
fencedModuleCode,
)
expect(fencedModuleResult.result).toBe('ok')
expect(buildBundleMock).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
sourceFiles: {
'entry.ts': `import { kody } from 'kody:runtime'

export default async function run() {
return await kody.meta_list_capabilities({})
}
`,
},
}),
)

const typescriptModuleResult = await runKodyWithRegistry(
env,
callerContext,
typescriptModuleCode,
)
expect(typescriptModuleResult.result).toBe('ok')
expect(buildBundleMock).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
sourceFiles: {
'entry.ts': typescriptModuleCode,
},
}),
)

const snippetResult = await runKodyWithRegistry(
env,
callerContext,
'return "ok"',
)
expect(snippetResult.result).toBe('ok')
expect(buildBundleMock).toHaveBeenCalledTimes(3)
} finally {
createExecuteExecutorSpy.mockRestore()
getRegistrySpy.mockRestore()
}
})

test('runKodyWithRegistry forwards package context for module syntax', async () => {
test('runModuleWithRegistry forwards package context', async () => {
silenceIncidentalRuntimeWarnings()
const env = {} as Env
const callerContext = createMcpCallerContext({
Expand Down Expand Up @@ -1600,7 +1481,7 @@ test('runKodyWithRegistry forwards package context for module syntax', async ()
export default async function run() {
return packageContext?.packageId ?? null
}`
const result = await runKodyWithRegistry(
const result = await runModuleWithRegistry(
env,
callerContext,
code,
Expand Down Expand Up @@ -2368,7 +2249,7 @@ test('runBundledModuleWithRegistry rewrites guard-less unbound runtime helper er
'entry.js': `import { storage } from './.__kody_virtual__/runtime.js'

export default async function main() {
const result = await storage.sql('select count(*) as count from skills')
const result = await storage.sql('select count(*) as count from items')
return result.rows
}`,
},
Expand Down Expand Up @@ -2441,10 +2322,10 @@ export default async function main() {
.mockResolvedValue({
modules: {
'entry.js': `export default async function main() {
const mod = await import('kody:@scope/skills/skill-list')
const mod = await import('kody:@scope/notes/note-list')
return await mod.default({})
}`,
'.__kody_dynamic__/scope/skills/skill-list.js': `import { storage } from '../../.__kody_virtual__/runtime.js'
'.__kody_dynamic__/scope/notes/note-list.js': `import { storage } from '../../.__kody_virtual__/runtime.js'
export default async () => (await storage.sql('select 1')).rows`,
},
dynamicDependencyPackageIds: [],
Expand All @@ -2457,7 +2338,7 @@ export default async () => (await storage.sql('select 1')).rows`,
mainModule: 'entry.js',
modules: {
'entry.js': `export default async function main() {
const mod = await import('kody:@scope/skills/skill-list')
const mod = await import('kody:@scope/notes/note-list')
return await mod.default({})
}`,
},
Expand All @@ -2477,57 +2358,3 @@ export default async () => (await storage.sql('select 1')).rows`,
createExecuteExecutorSpy.mockRestore()
}
})

test('runKodyWithRegistry expression path omits OAuth helper prelude without execute helper capabilities', async () => {
silenceIncidentalRuntimeWarnings()
const env = {} as Env
const callerContext = createMcpCallerContext({
baseUrl: 'https://heykody.dev',
user: { userId: 'user-123' },
})
const emptyRegistry = {
capabilityDomains: [],
capabilityDomainDescriptionsByName: {} as Record<string, string>,
capabilityHandlers: {},
capabilityList: [],
capabilityMap: {},
capabilitySpecs: {},
capabilityToolDescriptors: {},
} as Awaited<ReturnType<typeof getCapabilityRegistryForContext>>
const getRegistrySpy = vi
.spyOn(
await import('#mcp/capabilities/registry.ts'),
'getCapabilityRegistryForContext',
)
.mockResolvedValue(emptyRegistry)
let wrappedSource = ''
const createExecuteExecutorSpy = vi
.spyOn(await import('#mcp/executor.ts'), 'createExecuteExecutor')
.mockReturnValue({
async execute(wrapped) {
wrappedSource = String(wrapped)
return {
result: 42,
logs: [],
}
},
} as never)

try {
const result = await runKodyWithRegistry(
env,
callerContext,
'async () => 42',
undefined,
{
capabilityRegistry: emptyRegistry,
},
)
expect(result.result).toBe(42)
expect(wrappedSource).not.toContain('IntegrationHostNotAllowedError')
expect(wrappedSource).not.toContain('__kodyRefreshAccessToken')
} finally {
createExecuteExecutorSpy.mockRestore()
getRegistrySpy.mockRestore()
}
})
Loading
Loading