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
32 changes: 16 additions & 16 deletions docs/contributing/packages-and-manifests.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ A saved package is the only top-level persisted primitive. Five concepts:
publish-time artifact rebuilds, Kody records the imported saved package's
published commit in bundle dependency metadata. Republishing the imported
package does not rewrite already-published dependent bundles.
- Literal dynamic imports such as `await import("kody:@scope/pkg/export")` are
**deprecated agent guidance** (keep the mechanism working; stop recommending
it — the replacements are static imports when the name is known at write time
and `packages.invoke` otherwise). Mechanically they are runtime/current
package dependencies: bundle artifacts persist only a host-resolved
placeholder plus review metadata; just before execution, Kody resolves the
target package under the caller's `userId` and hydrates the current published
- Literal dynamic imports such as `await import("kody:@scope/pkg/export")` were
**removed** (the replacements are static imports when the name is known at
write time and `packages.invoke` otherwise). New bundles rewrite the call site
to a teaching error and publish checks fail on the pattern. For bundles
published before the removal, artifacts persisted a host-resolved placeholder
plus review metadata; just before execution, Kody resolves the target package
under the caller's `userId` and hydrates the current published
`importable-module` artifact into the dynamic worker module graph.
- Direct static `kody:@...` imports are a breaking manifest contract: they must
be listed in `package.json#kody.dependencies` by package name, for example
Expand Down Expand Up @@ -261,15 +261,15 @@ statically importing subscriber packages. Republish subscribers independently;
the dispatcher will observe the current published subscriber export on its next
dispatch without being republished.

**Deprecated (widen phase):** `packages.invokeChecked`, `packages.check`, and
literal dynamic `import("kody:@...")` keep working while callers migrate, but no
guidance surface (tool descriptions, search detail, error `nextStep` strings,
docs) may recommend them. `packages.invoke` subsumes both helpers because
checking is no longer optional or separate (`invokeChecked` is a plain alias of
`invoke`). The sandbox prelude warns once per run on `check` / `invokeChecked`,
the dynamic import helper warns once per specifier, and publish checks surface
non-fatal deprecation warnings in the passing lint message
(`deprecated-invocation-usage.ts`). See
**Removed (narrow phase):** `packages.invokeChecked`, `packages.check`, and
literal dynamic `import("kody:@...")` were removed. `packages.invoke` subsumes
both helpers because checking is not optional or separate. The sandbox prelude
throws teaching errors for `check` / `invokeChecked`, the bundler rewrites
literal dynamic kody imports to a teaching error, and publish checks fail on all
three with the replacement named (`deprecated-invocation-usage.ts`, shared with
the `0002-static-first-invocation` package codemod). Hydration still resolves
placeholder modules inside bundles published before the removal so pinned
snapshots keep working until dependents republish. See
[Invocation overhead guardrails](./architecture/invocation-overhead-guardrails.md)
for the performance budget that keeps the keyless path honest.

Expand Down
16 changes: 8 additions & 8 deletions docs/use/execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,11 @@ package artifacts do not contain a copy of the host runtime implementation, so
old package artifacts automatically observe current host runtime behavior.

Literal dynamic imports (`await import('kody:@scope/my-package/export-name')`)
are **deprecated**. They still resolve at runtime for the signed-in user (and
log a deprecation warning naming the replacement), but do not write new code
with them: use a static `kody:@...` import when the target package's name is
known when the code is written, or `packages.invoke` when it is not. Computed
dynamic Kody package imports, including variables and template strings, have
never been supported.
were **removed**. The call site throws a teaching error naming the replacement:
use a static `kody:@...` import when the target package's name is known when the
code is written, or `packages.invoke` when it is not. Computed dynamic Kody
package imports, including variables and template strings, have never been
supported.

**execute** also accepts optional **`params`**. Kody passes that JSON object to
the module's **default export** as the first function argument. Shared helpers
Expand Down Expand Up @@ -117,8 +116,9 @@ Execute responses include Server-Timing-style phase entries under

- `bundle` — module-graph preparation and bundling. This span contains the
typecheck phases below, so subtract `typecheck-total` for bundler-only time.
- `hydrate` — installing published sources for literal dynamic
`import("kody:@…")` targets (a deprecated pattern).
- `hydrate` — refreshing nested runtime modules (and resolving literal dynamic
`import("kody:@…")` placeholders in bundles published before that pattern was
removed).
- `provider-assembly` — capability registry, runtime helper, and provider wiring
ahead of sandbox startup.
- `sandbox` — the dynamic worker evaluation of the module itself.
Expand Down
27 changes: 13 additions & 14 deletions docs/use/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,16 @@ exhaustive.
Ad hoc execute code bundles per call, so static imports from execute always
see the current published version.
- Literal dynamic imports such as
`await import("kody:@scope/my-package/export")` are **deprecated**. They still
resolve the target package export at runtime for the signed-in user, but log a
deprecation warning (once per specifier) naming the replacement. Do not write
new code with them: use a static import when the name is known at write time,
`await import("kody:@scope/my-package/export")` were **removed**. The call
site throws a teaching error naming the replacement, and package publish
checks fail on them: use a static import when the name is known at write time,
or `packages.invoke` when it is not (see
[Dynamic package invocation](#dynamic-package-invocation)).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- Every direct static `kody:@...` import must be declared in
`package.json#kody.dependencies` using the imported package name, for example
`"dependencies": ["@scope/my-package"]` inside the `kody` object. Package
checks fail when static imports and declarations differ. Type-only imports do
not count, declaration files such as `.d.ts` are treated as type-only, and
current-version literal dynamic `import("kody:@...")` expressions do not need
`kody.dependencies` declarations.
not count, and declaration files such as `.d.ts` are treated as type-only.
- Computed dynamic Kody package imports, including template strings and
variables such as `import(packageSpecifier)`, are unsupported. When the target
package is not known until runtime, use `packages.invoke` instead.
Expand Down Expand Up @@ -262,13 +259,15 @@ Use keyless `packages.invoke` from execute when you need to enter a saved
package as that package so it receives `packageContext`, package-owned storage,
package-mounted secrets (`kody.secretMounts`), and its own `packages` helper.

**Deprecated:** `packages.invokeChecked`, `packages.check`, and literal dynamic
`import("kody:@...")` still work but should not appear in new code.
`packages.invoke` already performs the contract check that `invokeChecked` and
`check` provided (`invokeChecked` is now a plain alias of `invoke`), and the
static/dynamic rules above cover the literal dynamic import cases. Using any of
the three logs a runtime deprecation warning naming the replacement, and package
publish checks report them as non-fatal warnings.
**Removed:** `packages.invokeChecked`, `packages.check`, and literal dynamic
`import("kody:@...")` no longer exist. Calling any of them from new source or
newly built bundles throws a teaching error naming the replacement, and package
publish checks fail on them (bundles published before the removal keep working
through hydration until their packages republish). `packages.invoke` performs
the contract check that `invokeChecked` and `check` provided, and the
static/dynamic rules above cover the literal dynamic import cases. The
`0002-static-first-invocation` package codemod migrates `invokeChecked` call
sites mechanically.

## Package storage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const executeToolSandboxSurfaceDescription = `Sandbox surface:
- Execute has a hard timeout (~90s by default). For batch sweeps, migrations, polling loops, or work likely to run >~60s, submit one durable \`workflows.create({ code, params })\` from a single execute call instead of chaining many MCP round-trips.
- Optional \`params\` are passed as the first argument to the module default export. Prefer \`export default async function main(input = {}) { ... }\`; pass \`input\` to shared helpers explicitly.
- \`import { packageContext } from 'kody:runtime'\` in saved package code when you need package metadata; it is \`null\` for ad hoc execute calls.
- Package reuse, two rules. (1) Target package name known when the code is written → static import: \`import fn from 'kody:@scope/my-package/export-name'\` (npm-scoped package name). This is the default from execute and from other packages: typed, publish-verified, dependency-graph-visible, zero per-call platform cost. Ad hoc execute bundles per call, so static imports from execute always see the current published version. (2) Target name is data, the call needs the target package's own runtime (\`packageContext\`, \`kody.secretMounts\` package secrets, its own \`packageStorage()\` bucket), or you need exactly-once → \`import { packages } from 'kody:runtime'\` and \`packages.invoke({ kodyId: 'github', exportName, params })\`, the only dynamic call, always contract-checked before invoking. \`kodyId\` is the bare Kody id (for example \`github\`), not the npm-scoped name. Keyless invoke is lean/ephemeral (current published version, run records on failure only); add the optional \`idempotencyKey\` field only for exactly-once needs such as webhook event ids and retried dispatch. \`packages.invokeChecked\`, \`packages.check\`, and literal dynamic \`import("kody:@...")\` are deprecated — do not use them in new code.
- Package reuse, two rules. (1) Target package name known when the code is written → static import: \`import fn from 'kody:@scope/my-package/export-name'\` (npm-scoped package name). This is the default from execute and from other packages: typed, publish-verified, dependency-graph-visible, zero per-call platform cost. Ad hoc execute bundles per call, so static imports from execute always see the current published version. (2) Target name is data, the call needs the target package's own runtime (\`packageContext\`, \`kody.secretMounts\` package secrets, its own \`packageStorage()\` bucket), or you need exactly-once → \`import { packages } from 'kody:runtime'\` and \`packages.invoke({ kodyId: 'github', exportName, params })\`, the only dynamic call, always contract-checked before invoking. \`kodyId\` is the bare Kody id (for example \`github\`), not the npm-scoped name. Keyless invoke is lean/ephemeral (current published version, run records on failure only); add the optional \`idempotencyKey\` field only for exactly-once needs such as webhook event ids and retried dispatch. \`packages.invokeChecked\`, \`packages.check\`, and literal dynamic \`import("kody:@...")\` were removed and throw errors naming the replacement.
- \`fetch(...)\` is the host-provided network global; \`{{secret:name}}\` / \`{{secret:name|scope=user}}\` work in URL, headers, or body on approved hosts only. \`secretHeaders.basic(...)\` returns an opaque placeholder for fetch headers; the gateway resolves both referenced secrets, enforces host approval for both, and sends only the derived Basic header. For host approval failures, use the error’s approval path.
- Fields marked \`x-kody-secret: true\` accept the same placeholder form; respect per-secret allowed-capability lists.
- Placeholders are not general string interpolation (they do not resolve in arbitrary return values). Never place placeholder text into user-visible or third-party-visible content such as issue bodies, comments, prompts, logs, or returned strings; obfuscate the \`{{secret:...}}\` token if you must describe it literally.
Expand Down
46 changes: 4 additions & 42 deletions packages/worker/src/mcp/run-kody-registry.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1909,25 +1909,7 @@ test('runBundledModuleWithRegistry passes params and injects runtime helpers', a
undefined,
{
packageInvokeTools: {
check: async (input) => ({
ok: true,
invoke: input as {
kodyId: string
exportName: string
},
contract: {
packageId: 'pkg-discord-general-chat',
kodyId: 'discord-general-chat',
name: '@kentcdodds/discord-general-chat',
sourceId: 'source-discord-general-chat',
publishedCommit: 'commit-1',
exportName: './handle-discord-message-created',
runtimeTarget: 'src/handle-discord-message-created.ts',
warnings: [],
},
}),
invoke: async (input) => ({ ok: true, input }),
invokeChecked: async (input) => ({ ok: true, input }),
},
},
)
Expand All @@ -1937,18 +1919,10 @@ test('runBundledModuleWithRegistry passes params and injects runtime helpers', a
expect(providerFns?.package_invoke).toBeUndefined()
expect(providerFns?.package_invoke_checked).toBeUndefined()
expect(packageBridgeFns).not.toBeNull()
await expect(
packageBridgeFns?.check({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
}),
).resolves.toMatchObject({
ok: true,
invoke: {
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
},
})
// The removed check/invokeChecked APIs have no bridge tools; only
// invoke crosses the sandbox boundary.
expect(packageBridgeFns?.check).toBeUndefined()
expect(packageBridgeFns?.invokeChecked).toBeUndefined()
await expect(
packageBridgeFns?.invoke({
kodyId: 'discord-general-chat',
Expand All @@ -1961,18 +1935,6 @@ test('runBundledModuleWithRegistry passes params and injects runtime helpers', a
exportName: './handle-discord-message-created',
},
})
await expect(
packageBridgeFns?.invokeChecked({
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
}),
).resolves.toEqual({
ok: true,
input: {
kodyId: 'discord-general-chat',
exportName: './handle-discord-message-created',
},
})

createExecuteExecutorSpy.mockImplementation(
() =>
Expand Down
52 changes: 18 additions & 34 deletions packages/worker/src/mcp/runtime-helper-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ export type PackageInvokeCheckResult =

export type PackageInvokeTools = {
invoke: (input: PackageInvokeInput) => Promise<unknown>
check: (input: PackageInvokeInput) => Promise<PackageInvokeCheckResult>
invokeChecked: (input: PackageInvokeInput) => Promise<unknown>
}

export type PackageEventDispatchInput = {
Expand Down Expand Up @@ -248,33 +246,29 @@ const workflows = {
const packageInvokeRuntimeBridgeProviderName =
'__kodyPackageInvokeRuntimeBridge'
const packageEventRuntimeBridgeProviderName = '__kodyPackageEventRuntimeBridge'

export const removedPackagesCheckMessage =
'packages.check was removed: packages.invoke always contract-checks before invoking, so call packages.invoke({ kodyId, exportName, params }) directly. A failing contract rejects with "packages.invoke contract check failed: ..." before any execution.'

export const removedPackagesInvokeCheckedMessage =
'packages.invokeChecked was removed: call packages.invoke({ kodyId, exportName, params }) instead — it is always contract-checked (add idempotencyKey only when you need exactly-once) — or use a static import (import fn from "kody:@scope/pkg/export") when the target package is known at write time.'
const staticCallMeterRuntimeBridgeProviderName =
'__kodyStaticCallMeterRuntimeBridge'

function createPackagesHelperPrelude() {
// `check` and `invokeChecked` are deprecated widen-phase shims: they keep
// working but warn once per run naming the replacement, because agents
// learn the current contract from logs and error text.
// `check` and `invokeChecked` were removed with the static-first model.
// They throw teaching errors naming the exact replacement because agents
// learn the current contract from error text.
return `
const packages = (() => {
const warned = new Set();
const warnOnce = (name, message) => {
if (warned.has(name)) return;
warned.add(name);
console.warn(message);
};
return {
check: async (input) => {
warnOnce('check', '[deprecated] packages.check: packages.invoke always contract-checks before invoking, so call packages.invoke({ kodyId, exportName, params }) directly (add idempotencyKey only when you need exactly-once).');
return await ${packageInvokeRuntimeBridgeProviderName}.check(input ?? {});
},
invoke: async (input) => await ${packageInvokeRuntimeBridgeProviderName}.invoke(input ?? {}),
invokeChecked: async (input) => {
warnOnce('invokeChecked', '[deprecated] packages.invokeChecked: use a static import (import fn from "kody:@scope/pkg/export") when the target package is known at write time, or packages.invoke({ kodyId, exportName, params }) for dynamic targets (add idempotencyKey only when you need exactly-once).');
return await ${packageInvokeRuntimeBridgeProviderName}.invokeChecked(input ?? {});
},
};
})();
const packages = {
check: () => {
throw new Error(${JSON.stringify(removedPackagesCheckMessage)});
},
invoke: async (input) => await ${packageInvokeRuntimeBridgeProviderName}.invoke(input ?? {}),
invokeChecked: () => {
throw new Error(${JSON.stringify(removedPackagesInvokeCheckedMessage)});
},
};
`.trim()
}

Expand Down Expand Up @@ -428,20 +422,10 @@ function createPackageInvokeRuntimeBridgeProvider(
const provider: ToolProvider = {
name: packageInvokeRuntimeBridgeProviderName,
tools: {
check: {
execute: async (args: unknown) =>
await packageInvokeTools.check((args ?? {}) as PackageInvokeInput),
},
invoke: {
execute: async (args: unknown) =>
await packageInvokeTools.invoke((args ?? {}) as PackageInvokeInput),
},
invokeChecked: {
execute: async (args: unknown) =>
await packageInvokeTools.invokeChecked(
(args ?? {}) as PackageInvokeInput,
),
},
},
}
return resolveProvider(provider)
Expand Down
4 changes: 2 additions & 2 deletions packages/worker/src/package-invocations/http-invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export async function invokePackageExportForExecuteRuntime(input: {
conversationId?: string | null
toolFactories: PackageRuntimeToolFactories
waitUntil?: (promise: Promise<unknown>) => void
/** Check-phase loads from `packages.invokeChecked`; see invoke-check.ts. */
/** Check-phase loads from the `packages.invoke` contract check; see invoke-check.ts. */
preloads?: PackageInvokeCheckPreloads | null
}): Promise<PackageInvocationResponse> {
const packageIdOrKodyId = input.request.packageIdOrKodyId.trim()
Expand Down Expand Up @@ -164,7 +164,7 @@ export async function invokePackageExportForPackageRuntime(input: {
runtimeInvokeDepth?: number
toolFactories: PackageRuntimeToolFactories
waitUntil?: (promise: Promise<unknown>) => void
/** Check-phase loads from `packages.invokeChecked`; see invoke-check.ts. */
/** Check-phase loads from the `packages.invoke` contract check; see invoke-check.ts. */
preloads?: PackageInvokeCheckPreloads | null
}): Promise<PackageInvocationResponse> {
const packageIdOrKodyId = input.request.packageIdOrKodyId.trim()
Expand Down
Loading
Loading