Skip to content

Extract package runtime lane into kody-runtime worker (ADR 0016) - #1384

Merged
kentcdodds merged 12 commits into
mainfrom
devin/1786432523-runtime-worker-extraction
Aug 12, 2026
Merged

kentcdodds merged 12 commits into
mainfrom
devin/1786432523-runtime-worker-extraction

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Intent

Implement track (b) of ADR 0016: move the package runtime lane — the fastest-churning, riskiest untrusted-code execution subsystem — out of the mono kody Worker into an independently deployed kody-runtime Worker, so a bad runtime deploy can no longer take down login/MCP and rollback becomes per-subsystem.

Summary

Runtime worker (packages/runtime-worker/)

  • New script kody-runtime with entry packages/worker/src/runtime-worker.ts (kept in the shared worker source tree so #worker/#app import maps and pre-bundled src/generated/ modules resolve identically). It owns: the package-app origin (PACKAGE_APP_BASE_URL / kodyapps.dev custom domain), inline package-app serving, the package invocation API, DynamicCallableWorkflow, and exports StorageRunner, RunLog, PackageRealtimeSession, PackageServiceInstance plus the loopback entrypoints (PackageAppRuntimeBridge, KodyFetchGateway).
  • Worker Loader bindings (LOADER, APP_LOADER) and direct APP_DB/AUDIT_DB/KV/R2/queue/Vectorize/AI bindings move with it — no relational access over RPC.
  • DO storage preserved via a transferred_classes script migration (from_script: "kody"), applied on the first production deploy:
    { "tag": "v1", "transferred_classes": [{ "from": "StorageRunner", "from_script": "kody", "to": "StorageRunner" }, ...RunLog/PackageRealtimeSession/PackageServiceInstance] }
  • UserMeter stays in main; the runtime worker reaches it (and MCP/connector/job/mailbox/repo DOs used by capability execution) via cross-script DO bindings (script_name: "kody").

Main worker

  • Forwards runtime-owned requests wholesale over a RUNTIME_WORKER service binding (runtime-worker-routing.ts); without the binding (tests, --env test) it keeps serving in-process, so the test suite and single-worker flows are unchanged.
  • Binds the four runtime DOs and the workflow cross-script (script_name: "kody-runtime") for dashboard/account-export reads. Coarse-grained shared contract (healthcheck) in packages/shared/src/runtime-worker.ts.

CI/CD

  • tools/ci/runtime-worker-config.ts derives deployable runtime configs from the provisioned main config (shared data-plane ids, per-PR worker names, package-app route ownership) and, for preview, a bootstrap main config that breaks the fresh-pair circular binding reference. Deploy order: preview = main(bootstrap) → runtime → main(full); production = runtime (applies the transfer) → main. Both workers get secrets sync and /health + /__runtime/health healthchecks.
  • tools/check-deploy-guardrails.ts now protects transferred_classes migrations: any transfer must exactly match a reviewed baseline entry.

Local dev

  • npm run dev / npm run preview run both scripts in one Miniflare via a secondary --config (wrangler multi-config), so the service binding and cross-script DOs resolve locally.

Runbook: docs/contributing/architecture/runtime-worker-migration-runbook.md — migration mechanics, coordinated deploy order, workflow-cutover caveat, rollback. Production migrations are NOT executed by this PR's session; the runbook is executed by the parent session after preview verification.

Testing

  • npm run validate passes (598 test files / 2028 tests, e2e, mcp, typecheck, lint, format, guardrails, migrations, docs, runtime dry-run build).
  • New unit tests: tools/ci/runtime-worker-config.node.test.ts (name rewriting, resource-id copying, bootstrap generation, production route/migration) and transfer-migration guardrail cases in tools/check-deploy-guardrails.node.test.ts.
  • npm run runtime:build (wrangler deploy --dry-run of the runtime config) verifies the runtime bundle and production bindings.
  • Not rehearsed: the actual production transferred_classes migration (preview pairs are created fresh with new_sqlite_classes); see runbook.

System changes

  • New deployable worker kody-runtime; kodyapps.dev custom domain moves from kody to kody-runtime.
  • One-time DO storage transfer for StorageRunner/RunLog/PackageRealtimeSession/PackageServiceInstance on first production deploy (guardrail-protected).
  • DynamicCallableWorkflow becomes a new workflow on the runtime script; in-flight instances at cutover are orphaned (see runbook caveat).

Link to Devin session: https://app.devin.ai/sessions/341d72934bdf4541a3877f07b6c8be93
Requested by: @kentcdodds

Summary by CodeRabbit

  • New Features

    • Introduced a dedicated runtime Worker for package apps, workflows, and related services.
    • Added runtime Worker health checks and deployment validation.
    • Migrated runtime Durable Objects with coordinated preview and production deployments.
    • Added automatic configuration, secret synchronization, and runtime routing.
  • Bug Fixes

    • Improved preview cleanup to remove runtime Workers before dependent resources.
    • Added safeguards preventing unsafe Durable Object migrations.
  • Documentation

    • Added runtime Worker deployment, migration, verification, and rollback guidance.

devin-ai-integration Bot and others added 2 commits August 11, 2026 07:41
Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…fixes

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
@kentcdodds kentcdodds self-assigned this Aug 11, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

github-actions Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1384.kody-a99.workers.dev

Worker: kody-pr-1384
Runtime worker: kody-pr-1384-runtime (https://kody-pr-1384-runtime.kody-a99.workers.dev)
D1: kody-pr-1384-db
KV: kody-pr-1384-oauth-kv

Mocks:

@kentcdodds

Copy link
Copy Markdown
Owner

bugbot review

@kentcdodds

Copy link
Copy Markdown
Owner

@coderabbitai review

@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run — GitHub account mismatch

The GitHub account linked to your Cursor account does not match the PR author.

Please ensure you're using the correct GitHub account, or run Bugbot from a team that covers this repository.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0c9d0ec-9020-4d3a-ab5f-6791377d4f4a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR extracts package runtime execution into the kody-runtime Worker. It adds cross-worker routing, Durable Object transfer configuration and guardrails, generated deployment configurations, coordinated preview and production deployments, health checks, local tooling, and migration documentation.

Changes

Runtime Worker extraction

Layer / File(s) Summary
Runtime contract and request routing
packages/shared/src/runtime-worker.ts, packages/worker/src/*runtime-worker*, packages/worker/src/index.ts, packages/worker/src/env-schema.ts, packages/worker/worker-configuration.d.ts
The main Worker classifies runtime-owned requests and forwards them through RUNTIME_WORKER. The runtime Worker serves health checks and package runtime routes.
Worker configuration and storage migration
packages/runtime-worker/wrangler.jsonc, packages/worker/wrangler.jsonc, tools/ci/durable-object-baseline.json, tools/check-deploy-guardrails.*
Worker bindings and Durable Object ownership now target kody-runtime. Protected transferred_classes migrations are validated against the baseline.
Runtime configuration generation and tooling
tools/ci/runtime-worker-config.*, tools/ci/resource-utils.*, wrangler-env.ts, package.json, packages/runtime-worker/project.json, packages/runtime-worker/readme.md, tools/ci/preview-resources.ts
The new generator creates runtime and bootstrap configurations, synchronizes provisioned resources, moves the package-app route, supports local development, and removes runtime Workers during preview cleanup.
Preview and production deployment workflows
.github/workflows/deploy.yml, .github/workflows/preview.yml
Workflows generate configurations, synchronize secrets, deploy the runtime Worker, validate its health and commit SHA, and report runtime Worker details.
Migration runbook
docs/contributing/architecture/index.md, docs/contributing/architecture/runtime-worker-migration-runbook.md
Architecture documentation links and describes the runtime Worker migration, verification, deployment order, and rollback procedures.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeployWorkflow
  participant ConfigGenerator
  participant Cloudflare
  participant RuntimeWorker
  DeployWorkflow->>ConfigGenerator: generate runtime and bootstrap configurations
  ConfigGenerator->>DeployWorkflow: return generated paths and bindings
  DeployWorkflow->>Cloudflare: synchronize shared secrets
  DeployWorkflow->>Cloudflare: deploy kody-runtime
  DeployWorkflow->>Cloudflare: deploy kody
  DeployWorkflow->>RuntimeWorker: GET /__runtime/health
  RuntimeWorker->>DeployWorkflow: return status and commit SHA
Loading

Possibly related PRs

  • kentcdodds/kody#969: Related to transferring runtime-owned Durable Objects to kody-runtime.
  • kentcdodds/kody#1013: Related to moving package-app custom-domain routing to the runtime Worker.
  • kentcdodds/kody#1224: Related to extending Durable Object deployment guardrails for transfer migrations.

Suggested reviewers: kentcdodds

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: extracting the package runtime lane into the kody-runtime Worker.
Description check ✅ Passed The description includes the required Intent, Summary, Testing, and System changes sections with detailed scope, validation, and known migration limitations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch devin/1786432523-runtime-worker-extraction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (7)
tools/ci/runtime-worker-config.ts (3)

337-344: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Guard against multiple workflow entries receiving the same name.

The loop assigns ${runtimeWorkerName}-dynamic-callable-workflows to every entry in runtimeEnv.workflows. Today the runtime config declares one workflow, so the result is correct. If a second workflow is added later, both entries get the same name and the deploy binds the wrong workflow. Derive the name from the entry binding or class_name, or fail when more than one entry exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/ci/runtime-worker-config.ts` around lines 337 - 344, Update the
workflow-name assignment in the runtimeEnv.workflows loop so multiple entries
cannot receive the same name. Derive each entry’s name from its binding or
class_name while preserving the existing dynamic-callable naming for the current
single-workflow configuration, or explicitly fail when more than one workflow
entry is present.

186-207: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm that overwriting runtimeEnv.routes is intended.

addPackageAppRoute replaces the whole routes array with the single package-app entry. If the committed base config at packages/runtime-worker/wrangler.jsonc ever declares extra routes for an env, generation silently drops them. Appending, with a duplicate-pattern filter, matches the behavior of addPackageAppCustomDomainRoute in tools/ci/resource-utils.ts.

♻️ Proposed change
-	runtimeEnv.routes = [{ pattern: hostname, custom_domain: true }]
+	const existingRoutes = Array.isArray(runtimeEnv.routes)
+		? (runtimeEnv.routes as Array<unknown>)
+		: []
+	const alreadyRouted = existingRoutes.some(
+		(route) =>
+			route &&
+			typeof route === 'object' &&
+			(route as JsonRecord).pattern === hostname,
+	)
+	runtimeEnv.routes = alreadyRouted
+		? existingRoutes
+		: [...existingRoutes, { pattern: hostname, custom_domain: true }]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/ci/runtime-worker-config.ts` around lines 186 - 207, Update
addPackageAppRoute to preserve existing runtimeEnv.routes entries instead of
replacing the entire array. Append the package-app route while filtering any
existing route with the same hostname/pattern, matching the duplicate-handling
behavior of addPackageAppCustomDomainRoute; retain workers_dev handling
unchanged.

268-303: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

parseArgs accepts a value that looks like a flag.

The pair loop reads argv[index + 1] without checking for a leading --. A missing value silently consumes the next flag as the value, for example --env --out-config x. The command then fails later with a confusing message, or generates a config for env --out-config. Add a check that the value does not start with --.

🛡️ Proposed fix
-		if (!flag?.startsWith('--') || value === undefined) {
+		if (!flag?.startsWith('--') || value === undefined || value.startsWith('--')) {
 			fail(`Invalid argument pair: ${flag ?? ''} ${value ?? ''}`)
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/ci/runtime-worker-config.ts` around lines 268 - 303, Update parseArgs
so each option value is rejected when it starts with “--”, alongside the
existing invalid-pair validation. Preserve the current fail behavior and ensure
missing values cannot be consumed as the next flag.
wrangler-env.ts (2)

43-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse isDevCommand, and confirm the path passed to wrangler resolves the same way as the existence check.

Two points:

  1. Line 52 repeats args[0] === 'dev'. isDevCommand at line 25 already holds this value. Use it.
  2. The existence check resolves runtimeWorkerConfigPath through resolveWranglerConfigPath(..., process.cwd()), but line 58 pushes the raw relative path. If resolveWranglerConfigPath anchors to the repository root rather than the cwd, wrangler receives a path that does not exist when the script runs from a subdirectory. Push the resolved path.
♻️ Proposed change
-	if (
-		args[0] === 'dev' &&
-		envName !== 'test' &&
-		existsSync(
-			resolveWranglerConfigPath(runtimeWorkerConfigPath, process.cwd()),
-		)
-	) {
-		commandArgs.push('--config', runtimeWorkerConfigPath)
-	}
+	const resolvedRuntimeWorkerConfigPath = resolveWranglerConfigPath(
+		runtimeWorkerConfigPath,
+		process.cwd(),
+	)
+	if (
+		isDevCommand &&
+		envName !== 'test' &&
+		existsSync(resolvedRuntimeWorkerConfigPath)
+	) {
+		commandArgs.push('--config', resolvedRuntimeWorkerConfigPath)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wrangler-env.ts` around lines 43 - 59, Update the runtime-worker config
condition to reuse the existing isDevCommand symbol instead of checking args[0]
directly, while preserving the envName !== 'test' guard. Resolve
runtimeWorkerConfigPath once with resolveWranglerConfigPath and use that same
resolved value for both existsSync and the --config argument.

71-75: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use a path-aware runtime-worker check. getArgValue supports both --config <path> and --config=<path>. Replace the substring check with a normalized path check for the packages/runtime-worker/ directory, including generated configs such as wrangler-production.generated.json; do not compare only with runtimeWorkerConfigPath.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wrangler-env.ts` around lines 71 - 75, Update the isRuntimeWorkerConfig
calculation near getArgValue to normalize the resolved --config path and detect
whether it belongs to the packages/runtime-worker/ directory, supporting both
--config argument forms and generated filenames such as
wrangler-production.generated.json. Do not rely on substring matching or compare
only against runtimeWorkerConfigPath; preserve the existing isWorkerBuildCommand
condition.
tools/ci/runtime-worker-config.node.test.ts (1)

252-256: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The migration assertion couples to the first committed migration tag.

runtimeConfig.migrations?.[0]?.tag must be 'v1'. If a migration is later inserted before the transfer migration, or the tag is renamed, this test fails for a reason unrelated to generation. Assert that some migration entry carries transferred_classes instead of indexing position 0.

♻️ Proposed change
-		expect(runtimeConfig.migrations?.[0]?.tag).toBe('v1')
-		expect(
-			Array.isArray(runtimeConfig.migrations?.[0]?.transferred_classes),
-		).toBe(true)
+		const transferMigration = runtimeConfig.migrations?.find((migration) =>
+			Array.isArray(migration.transferred_classes),
+		)
+		expect(transferMigration).toBeDefined()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/ci/runtime-worker-config.node.test.ts` around lines 252 - 256, Update
the migration assertions in the runtime configuration test to search
runtimeConfig.migrations for an entry containing transferred_classes, rather
than assuming the transfer migration is at index 0 or asserting its tag.
Preserve the requirement that the matching entry exposes transferred_classes as
an array.
tools/check-deploy-guardrails.node.test.ts (1)

139-204: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a case for a malformed transferred_classes entry.

The test covers matching, retargeted, and unrecorded migrations. It does not cover the normalizeTransferredClasses null path. An entry with a missing or non-string from_script must still produce the "was removed, renamed, or changed" error. Add that case to lock the validation behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/check-deploy-guardrails.node.test.ts` around lines 139 - 204, Extend
the test case around checkDurableObjectConfig with a malformed
transferred_classes entry whose from_script is missing or non-string, and assert
it produces the existing "protected transferred_classes migration \"v1\" was
removed, renamed, or changed" error. Keep the matching, retargeted, and
unrecorded migration assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy.yml:
- Around line 202-234: Define and apply one shared allowlist of runtime-lane
secrets across both sync paths. In .github/workflows/deploy.yml:202-234, update
the runtime sync env block and command to remove STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET, KIT_API_KEY, and all OAuth client ID/secret pairs,
retaining only secrets consumed by the runtime lane. In
.github/workflows/preview.yml:344-354, replace the dotenv-based runtime sync
with explicit --set or --set-from-env-optional entries for exactly the same
allowlisted secrets.
- Around line 380-416: In both runtime healthcheck blocks, add the existing
fail-fast guard for EXPECTED_COMMIT_SHA before polling begins: validate that the
environment value is non-empty, print an error to stderr, and exit 1 when
missing. Apply this to .github/workflows/deploy.yml lines 380-416 and
.github/workflows/preview.yml lines 423-458; leave the status and commitSha
response checks unchanged.
- Around line 243-263: Update the runtime deploy step identified by
deploy_runtime to reuse the shared is_retryable_deploy_failure helper and
deployment-attempt loop used by the main deploy, including retries for
workflows.api.error.internal_server and Cloudflare code 10001. Before extracting
the URL, remove ANSI escape sequences from deploy-runtime.log, then match the
URL specifically on RUNTIME_WORKER_NAME rather than selecting the last
workers.dev URL; preserve the existing GitHub output behavior.
- Around line 243-257: Add a handover step before the “Deploy runtime worker to
Cloudflare Workers” step that explicitly removes the kodyapps.dev custom domain
from the kody Worker using the existing Cloudflare credentials and API
conventions. Ensure the handover completes before the runtime deployment runs,
while preserving the current deployment command and configuration.

In `@docs/contributing/architecture/runtime-worker-migration-runbook.md`:
- Around line 108-122: Complete the manual execution procedure by adding the
equivalent secret synchronization for both Workers and the D1 migration step
before deployment, reusing the commands or symbols from the automated procedure.
Alternatively, state explicit verified preconditions requiring both operations
to finish before either deploy command runs.
- Around line 90-98: Update the runtime-worker migration runbook around the
consecutive kody-runtime and kody deployment steps to require quiescing or
gating runtime traffic before the transferred_classes migration. Document that
the gate must remain active through both deployments and be released only after
the main kody deployment and its healthchecks succeed, covering both the
workflow and manual procedure.

In `@packages/runtime-worker/wrangler.jsonc`:
- Around line 52-78: Update packages/runtime-worker/wrangler.jsonc lines 52-78
to ensure the transferred Durable Object classes match the exports from
packages/worker/src/runtime-worker.ts, and document the required deployment
order in the runbook. In packages/worker/wrangler.jsonc lines 255-292, verify
that packages/worker/src/index.ts still exports StorageRunner, RunLog,
PackageRealtimeSession, and PackageServiceInstance; if not, add migration
entries that retire those classes from the kody script so every declared
migration class is exported by its uploaded entry module.
- Around line 94-246: Add an `EMAIL_BLOBS` R2 bucket binding to the runtime
Worker's R2 configuration for both runtime environments, using the existing
email-blob bucket name and binding conventions. Ensure package-invocation code
can access the bucket through `env.EMAIL_BLOBS` in each environment.

In `@packages/worker/worker-configuration.d.ts`:
- Around line 58-67: Add the explicit RPC contracts currently represented by
local as unknown as casts for STORAGE_RUNNER and RUN_LOG to a shared runtime
interface, then type both DurableObjectNamespace declarations with that
interface. Remove the local casts and ensure the shared interface exposes all
RPC methods and payload types so signature changes remain type-checked.

In `@tools/check-deploy-guardrails.ts`:
- Around line 265-302: Update checkDurableObjectConfig and its migration
collection to include migrations from every config.env.<name> section, while
keeping duplicate-tag validation scoped per section so environment-specific tags
such as v1 may be reused. Add the preview v1 new_sqlite_classes migration for
packages/runtime-worker/wrangler.jsonc to tools/ci/durable-object-baseline.json,
ensuring it is protected by the baseline.

---

Nitpick comments:
In `@tools/check-deploy-guardrails.node.test.ts`:
- Around line 139-204: Extend the test case around checkDurableObjectConfig with
a malformed transferred_classes entry whose from_script is missing or
non-string, and assert it produces the existing "protected transferred_classes
migration \"v1\" was removed, renamed, or changed" error. Keep the matching,
retargeted, and unrecorded migration assertions unchanged.

In `@tools/ci/runtime-worker-config.node.test.ts`:
- Around line 252-256: Update the migration assertions in the runtime
configuration test to search runtimeConfig.migrations for an entry containing
transferred_classes, rather than assuming the transfer migration is at index 0
or asserting its tag. Preserve the requirement that the matching entry exposes
transferred_classes as an array.

In `@tools/ci/runtime-worker-config.ts`:
- Around line 337-344: Update the workflow-name assignment in the
runtimeEnv.workflows loop so multiple entries cannot receive the same name.
Derive each entry’s name from its binding or class_name while preserving the
existing dynamic-callable naming for the current single-workflow configuration,
or explicitly fail when more than one workflow entry is present.
- Around line 186-207: Update addPackageAppRoute to preserve existing
runtimeEnv.routes entries instead of replacing the entire array. Append the
package-app route while filtering any existing route with the same
hostname/pattern, matching the duplicate-handling behavior of
addPackageAppCustomDomainRoute; retain workers_dev handling unchanged.
- Around line 268-303: Update parseArgs so each option value is rejected when it
starts with “--”, alongside the existing invalid-pair validation. Preserve the
current fail behavior and ensure missing values cannot be consumed as the next
flag.

In `@wrangler-env.ts`:
- Around line 43-59: Update the runtime-worker config condition to reuse the
existing isDevCommand symbol instead of checking args[0] directly, while
preserving the envName !== 'test' guard. Resolve runtimeWorkerConfigPath once
with resolveWranglerConfigPath and use that same resolved value for both
existsSync and the --config argument.
- Around line 71-75: Update the isRuntimeWorkerConfig calculation near
getArgValue to normalize the resolved --config path and detect whether it
belongs to the packages/runtime-worker/ directory, supporting both --config
argument forms and generated filenames such as
wrangler-production.generated.json. Do not rely on substring matching or compare
only against runtimeWorkerConfigPath; preserve the existing isWorkerBuildCommand
condition.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e1a4186f-8b55-4578-9cf3-71469dbd338f

📥 Commits

Reviewing files that changed from the base of the PR and between 07e245e and 990b7ad.

📒 Files selected for processing (24)
  • .github/workflows/deploy.yml
  • .github/workflows/preview.yml
  • docs/contributing/architecture/index.md
  • docs/contributing/architecture/runtime-worker-migration-runbook.md
  • package.json
  • packages/runtime-worker/project.json
  • packages/runtime-worker/readme.md
  • packages/runtime-worker/wrangler.jsonc
  • packages/shared/src/runtime-worker.ts
  • packages/worker/src/env-schema.ts
  • packages/worker/src/index.ts
  • packages/worker/src/runtime-worker-routing.ts
  • packages/worker/src/runtime-worker.ts
  • packages/worker/worker-configuration.d.ts
  • packages/worker/wrangler.jsonc
  • tools/check-deploy-guardrails.node.test.ts
  • tools/check-deploy-guardrails.ts
  • tools/ci/durable-object-baseline.json
  • tools/ci/preview-resources.ts
  • tools/ci/resource-utils.node.test.ts
  • tools/ci/resource-utils.ts
  • tools/ci/runtime-worker-config.node.test.ts
  • tools/ci/runtime-worker-config.ts
  • wrangler-env.ts

Comment on lines +202 to +234
- name: 🔐 Sync Cloudflare Secrets to runtime worker (bulk)
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
WRANGLER_CONFIG:
${{ steps.runtime_config.outputs.runtime_wrangler_config }}
COOKIE_SECRET: ${{ secrets.COOKIE_SECRET }}
SECRET_STORE_KEY: ${{ secrets.SECRET_STORE_KEY }}
AI_GATEWAY_ID: ${{ secrets.AI_GATEWAY_ID }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
CAPABILITY_REINDEX_SECRET: ${{ secrets.CAPABILITY_REINDEX_SECRET }}
GITHUB_CLIENT_ID: ${{ secrets.OAUTH_GITHUB_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.OAUTH_GITHUB_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.OAUTH_GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.OAUTH_GOOGLE_CLIENT_SECRET }}
X_CLIENT_ID: ${{ secrets.OAUTH_X_CLIENT_ID }}
X_CLIENT_SECRET: ${{ secrets.OAUTH_X_CLIENT_SECRET }}
KIT_API_KEY: ${{ secrets.KIT_API_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
run: >
node tools/ci/sync-worker-secrets.ts --env production --config
"$WRANGLER_CONFIG" --set-from-env COOKIE_SECRET
--set-from-env-optional SECRET_STORE_KEY --set-from-env-optional
AI_GATEWAY_ID --set-from-env-optional SENTRY_DSN
--set-from-env-optional CLOUDFLARE_API_TOKEN --set-from-env-optional
CAPABILITY_REINDEX_SECRET --set-from-env-optional GITHUB_CLIENT_ID
--set-from-env-optional GITHUB_CLIENT_SECRET --set-from-env-optional
GOOGLE_CLIENT_ID --set-from-env-optional GOOGLE_CLIENT_SECRET
--set-from-env-optional X_CLIENT_ID --set-from-env-optional
X_CLIENT_SECRET --set-from-env-optional KIT_API_KEY
--set-from-env-optional STRIPE_SECRET_KEY --set-from-env-optional
STRIPE_WEBHOOK_SECRET

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

The runtime Worker receives the main Worker's full secret set in both workflows. Both secret-sync paths copy every application secret to kody-runtime and to ${APP_WORKER_NAME}-runtime, although the runtime handler in packages/worker/src/runtime-worker.ts only serves package apps, package invocation, and the health path. Billing, waiting-list, and OAuth flows stay on the main Worker, so the extra secrets widen the blast radius of a runtime-Worker compromise without a functional need. Define one shared allowlist of runtime-lane secrets and use it in both places.

  • .github/workflows/deploy.yml#L202-L234: drop STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, KIT_API_KEY, and the OAuth client id/secret pairs from the runtime sync command and its env block; keep only the secrets that the runtime lane reads.
  • .github/workflows/preview.yml#L344-L354: replace the second --from-dotenv "$ENV_FILE" --from-dotenv "$OVERRIDES_FILE" sync for $RUNTIME_WORKER_NAME with an explicit --set/--set-from-env-optional list limited to the same runtime-lane secrets.
📍 Affects 2 files
  • .github/workflows/deploy.yml#L202-L234 (this comment)
  • .github/workflows/preview.yml#L344-L354
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 202 - 234, Define and apply one
shared allowlist of runtime-lane secrets across both sync paths. In
.github/workflows/deploy.yml:202-234, update the runtime sync env block and
command to remove STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, KIT_API_KEY, and all
OAuth client ID/secret pairs, retaining only secrets consumed by the runtime
lane. In .github/workflows/preview.yml:344-354, replace the dotenv-based runtime
sync with explicit --set or --set-from-env-optional entries for exactly the same
allowlisted secrets.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 290f2d0. The production runtime sync now uses a strict runtime-lane allowlist: COOKIE_SECRET, SECRET_STORE_KEY, AI_GATEWAY_ID, SENTRY_DSN, CLOUDFLARE_API_TOKEN — Stripe, Kit, OAuth client pairs, and CAPABILITY_REINDEX_SECRET no longer reach kody-runtime (removed from both the env block and the command). Preview intentionally keeps the dotenv-based sync (see the new comment in preview.yml): those files contain only .env.example placeholders plus per-PR mock-endpoint overrides that runtime capability execution needs, and no production secret is present in either file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment thread .github/workflows/deploy.yml Outdated
Comment on lines +243 to +257
# Deploys before the main worker: the main worker's cross-script
# Durable Object / service bindings require the kody-runtime script to
# exist. On the first production deploy this also runs the
# transferred_classes migration; see
# docs/contributing/architecture/runtime-worker-migration-runbook.md.
- name: ☁️ Deploy runtime worker to Cloudflare Workers
id: deploy_runtime
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
DEPLOY_COMMIT_SHA: ${{ needs.sha-guard.outputs.deploy_sha }}
WRANGLER_CONFIG:
${{ steps.runtime_config.outputs.runtime_wrangler_config }}
run: |
set -euo pipefail
npm run deploy -- --config "$WRANGLER_CONFIG" --var "APP_COMMIT_SHA:${DEPLOY_COMMIT_SHA}" 2>&1 | tee deploy-runtime.log

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Look for custom-domain handover steps in the runtime worker migration runbook.
fd -t f 'runtime-worker-migration-runbook.md' --exec rg -n -C 4 -i 'custom domain|kodyapps|detach|route'

Repository: kentcdodds/kody

Length of output: 2483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- workflow deployment context ---'
sed -n '200,290p' .github/workflows/deploy.yml

echo '--- migration runbook ---'
fd -t f 'runtime-worker-migration-runbook.md' --exec sed -n '1,180p' {}

echo '--- custom-domain configuration and migration references ---'
rg -n -C 4 -i 'kodyapps\.dev|custom[_ -]?domain|customDomain|detach|transfer|runtime-worker' \
  .github tools docs wrangler.toml '*.json' '*.yml' '*.yaml' 2>/dev/null || true

Repository: kentcdodds/kody

Length of output: 50374


🌐 Web query:

Cloudflare Workers custom domain already assigned to another Worker deploy transfer ownership existing custom domain Wrangler

💡 Result:

When you encounter the error stating that a custom domain is already assigned to another Cloudflare Worker, you cannot automatically transfer ownership via a single Wrangler command [1][2]. Cloudflare prevents multiple workers from being assigned to the same custom domain or route to ensure routing stability [1][2]. To resolve this and move the domain to your desired Worker, you must manually unassign the custom domain from the existing Worker first [1][2]. Steps to transfer the domain: 1. Identify the worker currently assigned to the domain: The Wrangler error message typically identifies the worker name and provides a link to the Cloudflare dashboard [1][3]. 2. Unassign the domain: Log in to the Cloudflare dashboard, navigate to the Workers overview page or the specific worker currently using the domain, and remove the custom domain or route association [1][2]. 3. Deploy your new worker: Once the domain is unassigned, run your wrangler deploy command again [1][2]. The new worker will now be able to claim the custom domain successfully [1]. There is currently no native "override" or "force transfer" flag in Wrangler to automate this process because the Cloudflare API requires explicit unassignment to prevent configuration conflicts [4][2][5]. If you are managing many workers, you may need to design your workflow to explicitly unassign routes as part of your CI/CD process before deploying the new version [2]. For advanced scenarios where you need to route many domains, consider Cloudflare for Platforms (formerly Cloudflare for SaaS), which allows for more flexible programmatic hostname management [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

workflow = Path(".github/workflows/deploy.yml").read_text()
runbook = Path("docs/contributing/architecture/runtime-worker-migration-runbook.md").read_text()
runtime_config = Path("tools/ci/runtime-worker-config.ts").read_text()
resource_utils = Path("tools/ci/resource-utils.ts").read_text()

checks = {
    "runtime deploy precedes main deploy": workflow.index(
        "Deploy runtime worker to Cloudflare Workers"
    ) < workflow.index("Deploy to Cloudflare Workers"),
    "runbook has no custom-domain handover command": not any(
        term in runbook.lower()
        for term in ("detach", "unassign", "custom domain handover", "remove custom domain")
    ),
    "runtime config attaches package-app host": "runtimeEnv.routes = [{ pattern: hostname, custom_domain: true }]" in runtime_config,
    "main config excludes package-app host": "package-app host itself is attached to the runtime Worker" in resource_utils,
    "workflow has no explicit custom-domain removal": not any(
        term in workflow.lower()
        for term in ("delete custom domain", "remove custom domain", "unassign", "workers/domains")
    ),
}

for name, result in checks.items():
    print(f"{name}: {'yes' if result else 'no'}")
PY

Repository: kentcdodds/kody

Length of output: 386


Add an explicit kodyapps.dev handover before deploying kody-runtime.

The runbook and workflow do not remove the domain from kody. Cloudflare rejects assigning a custom domain that another Worker owns, so the first production deploy fails unless the workflow unassigns kodyapps.dev before the runtime deploy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 243 - 257, Add a handover step
before the “Deploy runtime worker to Cloudflare Workers” step that explicitly
removes the kodyapps.dev custom domain from the kody Worker using the existing
Cloudflare credentials and API conventions. Ensure the handover completes before
the runtime deployment runs, while preserving the current deployment command and
configuration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid — fixed in the runbook (290f2d0). Cloudflare refuses to attach a custom domain that another Worker still owns, so the first production kody-runtime deploy would fail while kody holds kodyapps.dev. The runbook now has a dedicated step: detach kodyapps.dev from kody in the dashboard immediately before merging, with the explicit note that package-app traffic on that domain is unserved between the detach and the runtime deploy's attach (one-time only; later deploys find the domain already on kody-runtime).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +243 to +263
# Deploys before the main worker: the main worker's cross-script
# Durable Object / service bindings require the kody-runtime script to
# exist. On the first production deploy this also runs the
# transferred_classes migration; see
# docs/contributing/architecture/runtime-worker-migration-runbook.md.
- name: ☁️ Deploy runtime worker to Cloudflare Workers
id: deploy_runtime
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
DEPLOY_COMMIT_SHA: ${{ needs.sha-guard.outputs.deploy_sha }}
WRANGLER_CONFIG:
${{ steps.runtime_config.outputs.runtime_wrangler_config }}
run: |
set -euo pipefail
npm run deploy -- --config "$WRANGLER_CONFIG" --var "APP_COMMIT_SHA:${DEPLOY_COMMIT_SHA}" 2>&1 | tee deploy-runtime.log

RUNTIME_URL="$(node -e "const fs = require('node:fs'); const t = fs.readFileSync('deploy-runtime.log','utf8'); const m = t.match(/https:\\/\\/[a-zA-Z0-9._-]+\\.workers\\.dev/g); process.stdout.write(m?.at(-1) ?? '')")"
if [ -n "$RUNTIME_URL" ]; then
echo "url=$RUNTIME_URL" >> "$GITHUB_OUTPUT"
fi

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The runtime deploy lacks the retry and ANSI handling that the main deploy has.

Three defects in this step:

  1. No retry. The main deploy at lines 275-313 retries workflows.api.error.internal_server and Cloudflare code: 10001. The runtime Worker also creates a Workflow and Durable Object namespaces, so it faces the same transient failures. A single transient error fails the production deploy before the main Worker is deployed.
  2. The URL extraction at line 259 does not strip ANSI escape codes. .github/workflows/preview.yml line 284 strips them before matching. If wrangler colorizes the URL, the character class [a-zA-Z0-9._-]+ stops at the escape sequence and the match is empty or truncated. RUNTIME_URL is then unset, and the runtime healthcheck at lines 388-391 exits 1 with Missing runtime worker deploy URL output.
  3. The extraction takes the last *.workers.dev match in the log without anchoring to the runtime Worker name. The preview step anchors on $RUNTIME_WORKER_NAME. An unrelated URL printed by wrangler produces a healthcheck against the wrong host.
🐛 Proposed fix for the URL extraction
-          RUNTIME_URL="$(node -e "const fs = require('node:fs'); const t = fs.readFileSync('deploy-runtime.log','utf8'); const m = t.match(/https:\\/\\/[a-zA-Z0-9._-]+\\.workers\\.dev/g); process.stdout.write(m?.at(-1) ?? '')")"
+          RUNTIME_URL="$(node -e 'const fs = require("node:fs"); const text = fs.readFileSync("deploy-runtime.log", "utf8").replace(/\u001b\[[0-9;]*m/g, ""); const worker = process.argv[1]; const escaped = worker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const workerUrlRegex = new RegExp(`https://(?:[a-zA-Z0-9-]+\\.)?${escaped}\\.[a-zA-Z0-9._-]+\\.workers\\.dev`, "g"); const matches = text.match(workerUrlRegex); process.stdout.write(matches?.at(-1) ?? "")' kody-runtime)"

For the retry, extract the is_retryable_deploy_failure helper and the attempt loop into a shared script and call it from both deploy steps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 243 - 263, Update the runtime
deploy step identified by deploy_runtime to reuse the shared
is_retryable_deploy_failure helper and deployment-attempt loop used by the main
deploy, including retries for workflows.api.error.internal_server and Cloudflare
code 10001. Before extracting the URL, remove ANSI escape sequences from
deploy-runtime.log, then match the URL specifically on RUNTIME_WORKER_NAME
rather than selecting the last workers.dev URL; preserve the existing GitHub
output behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

All three fixed in 290f2d0: (1) the runtime deploy now retries up to 3 attempts with exponential backoff on the same retryable signatures as the main deploy (workflows.api.error.internal_server, Cloudflare code: 10001), checked against an ANSI-stripped log; (2) URL extraction strips ANSI escapes before matching; (3) the workers.dev URL regex is anchored on the kody-runtime worker name (passed as an argument), matching the preview step's approach.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +380 to +416
- name: 🩺 Healthcheck runtime worker (production)
shell: bash
env:
RUNTIME_URL: ${{ steps.deploy_runtime.outputs.url }}
EXPECTED_COMMIT_SHA: ${{ needs.sha-guard.outputs.deploy_sha }}
run: |
set -euo pipefail

if [ -z "${RUNTIME_URL:-}" ]; then
echo "Missing runtime worker deploy URL output; cannot run healthcheck." >&2
exit 1
fi

HEALTHCHECK_URL="${RUNTIME_URL%/}/__runtime/health"
echo "Healthcheck URL: $HEALTHCHECK_URL"

attempts=20
delay_seconds=3

for i in $(seq 1 "$attempts"); do
echo "Attempt $i/$attempts"
if curl --fail --silent --show-error --location --max-time 10 \
--header "Accept: application/json" \
"$HEALTHCHECK_URL" > runtime-health.json && \
node -e "const fs = require('node:fs'); const json = JSON.parse(fs.readFileSync('runtime-health.json','utf8')); const expected = process.env.EXPECTED_COMMIT_SHA; if (json?.status !== 'ok') { console.error('runtime-healthcheck-unexpected-response', json); process.exit(1); } if (json?.commitSha !== expected) { console.error('runtime-healthcheck-unexpected-commit-sha', { expected, actual: json?.commitSha }); process.exit(1); } console.log('runtime-healthcheck-ok', json);"; then
exit 0
fi
sleep "$delay_seconds"
done

echo "Runtime worker healthcheck failed after ${attempts} attempts: $HEALTHCHECK_URL" >&2
if [ -f runtime-health.json ]; then
echo "Last response body:" >&2
cat runtime-health.json >&2
fi
exit 1

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect the shared runtime worker health payload contract.
fd -t f 'runtime-worker.ts' -p packages/shared --exec rg -n -C 8 'buildRuntimeWorkerHealth|runtimeWorkerHealthPath|status|commitSha'

Repository: kentcdodds/kody

Length of output: 986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for file in .github/workflows/deploy.yml .github/workflows/preview.yml; do
  echo "===== $file: healthcheck context ====="
  if [ "$file" = ".github/workflows/deploy.yml" ]; then
    sed -n '330,420p' "$file"
    echo "===== $file: SHA and runtime deployment references ====="
    rg -n -C 5 'deploy_sha|EXPECTED_COMMIT_SHA|deploy_runtime|commitSha|sha-guard' "$file"
  else
    sed -n '400,465p' "$file"
    echo "===== $file: SHA and runtime deployment references ====="
    rg -n -C 5 'EXPECTED_COMMIT_SHA|deploy_runtime|commitSha|github\.sha|sha-guard' "$file"
  fi
done

Repository: kentcdodds/kody

Length of output: 25344


Add fail-fast guards for EXPECTED_COMMIT_SHA. buildRuntimeWorkerHealth emits status and commitSha, so both field checks are correct. Add the guard used by the main healthchecks before polling in both runtime healthchecks. An empty expected SHA currently consumes the full retry budget before failing.

📍 Affects 2 files
  • .github/workflows/deploy.yml#L380-L416 (this comment)
  • .github/workflows/preview.yml#L423-L458
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 380 - 416, In both runtime
healthcheck blocks, add the existing fail-fast guard for EXPECTED_COMMIT_SHA
before polling begins: validate that the environment value is non-empty, print
an error to stderr, and exit 1 when missing. Apply this to
.github/workflows/deploy.yml lines 380-416 and .github/workflows/preview.yml
lines 423-458; leave the status and commitSha response checks unchanged.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 290f2d0: both runtime healthchecks (deploy.yml production and preview.yml) now fail fast with Missing expected commit SHA; cannot verify runtime deployment version. when EXPECTED_COMMIT_SHA is empty, before entering the polling loop — same guard the main healthchecks use.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +90 to +98
4. **deploys `kody-runtime` first** — this applies the `v1`
`transferred_classes` migration, moving the four classes' storage out of
`kody`. From this moment the still-running old `kody` deployment serves
runtime traffic against namespaces that have moved; the window until step
5 completes must be short and is why the two deploys are consecutive steps
in one job;
5. **deploys `kody` second** with the config that binds the four classes
cross-script (`script_name: "kody-runtime"`) plus the `RUNTIME_WORKER`
service binding, and stops routing runtime requests in-process;

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify that the production workflow prevents runtime traffic from reaching
# the old main Worker during the transferred_classes migration window.
rg -n -i -C 5 \
  'maintenance|drain|quiesce|traffic|kody-runtime|transferred_classes|deploy' \
  .github/workflows/deploy.yml

Repository: kentcdodds/kody

Length of output: 20622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runbook ---'
sed -n '35,115p' docs/contributing/architecture/runtime-worker-migration-runbook.md

printf '%s\n' '--- workflow deploy sequence and relevant controls ---'
sed -n '140,330p' .github/workflows/deploy.yml

printf '%s\n' '--- runtime migration references ---'
rg -n -i -C 4 \
  'transferred_classes|runtime worker|cross-script|script_name|RUNTIME_WORKER|namespace|quiet|maintenance|drain|traffic' \
  docs packages tools .github/workflows -g '*.md' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.json' -g '*.jsonc' -g '*.yml' -g '*.yaml'

Repository: kentcdodds/kody

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runbook excerpt ---'
sed -n '1,135p' docs/contributing/architecture/runtime-worker-migration-runbook.md

printf '%s\n' '--- routing implementation ---'
cat -n packages/worker/src/runtime-worker-routing.ts
sed -n '510,570p' packages/worker/src/index.ts

printf '%s\n' '--- exact migration and transfer references ---'
rg -l -i 'transferred_classes|transfer.*durable|durable.*transfer' docs packages tools .github/workflows \
  -g '*.md' -g '*.ts' -g '*.tsx' -g '*.js' -g '*.json' -g '*.jsonc' -g '*.yml' -g '*.yaml' |
  while IFS= read -r file; do
    echo "--- $file"
    rg -n -i -C 5 'transferred_classes|transfer.*durable|durable.*transfer' "$file"
  done

Repository: kentcdodds/kody

Length of output: 22498


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

workflow = Path(".github/workflows/deploy.yml").read_text()
runtime_step = workflow.index("Deploy runtime worker to Cloudflare Workers")
main_step = workflow.index("Deploy to Cloudflare Workers", runtime_step + 1)
before_runtime = workflow[:runtime_step]

gate_terms = re.findall(
    r"(?i)\b(?:maintenance|drain|quiesc|traffic[-_ ]?gate|pause|read[-_ ]?only)\b",
    before_runtime,
)
print("runtime_step_before_main_step:", runtime_step < main_step)
print("gate_terms_before_runtime_step:", gate_terms)

runbook = Path("docs/contributing/architecture/runtime-worker-migration-runbook.md").read_text()
print("runbook_explicitly_describes_old_worker_window:",
      "still-running old `kody` deployment serves runtime traffic" in runbook)
print("runbook_explicitly_describes_old_namespace_failure:",
      "DO requests routed through the old script's namespaces fail" in runbook)
PY

printf '%s\n' '--- runtime-owned route call sites ---'
rg -n -C 3 \
  'STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS|RUNTIME_WORKER|isRuntimeWorkerOwnedRequest' \
  packages/worker/src packages/runtime-worker/src packages/worker/wrangler.jsonc \
  packages/runtime-worker/wrangler.jsonc

Repository: kentcdodds/kody

Length of output: 50372


Quiesce runtime traffic before the transferred_classes migration.

The workflow and manual procedure deploy kody-runtime before kody, without an enforceable traffic gate. Requests from the old kody deployment can fail after the migration removes its Durable Object namespaces. Add a gate before the runtime deployment and release it only after the main Worker deploy and healthchecks complete.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/architecture/runtime-worker-migration-runbook.md` around
lines 90 - 98, Update the runtime-worker migration runbook around the
consecutive kody-runtime and kody deployment steps to require quiescing or
gating runtime traffic before the transferred_classes migration. Document that
the gate must remain active through both deployments and be released only after
the main kody deployment and its healthchecks succeed, covering both the
workflow and manual procedure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in the runbook (290f2d0) by documenting the accepted risk rather than adding a traffic gate: per ADR 0016 there is deliberately no maintenance-mode quiescing — runtime requests hitting the still-running old kody deployment during the seconds-long window between the two consecutive deploy steps error and surface as failed runs/invocations, which is why the deploy runs at a quiet time. Building a maintenance-mode gate would be a larger change than the extraction itself and would reverse the ADR's agreed cutover design.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +108 to +122
## Manual execution (only if the workflow cannot run)

From a checkout of the merged commit, with `CLOUDFLARE_API_TOKEN` set:

```sh
node tools/ci/production-resources.ts ensure --out-config packages/worker/wrangler-production.generated.json
node tools/ci/runtime-worker-config.ts generate \
--env production \
--main-config packages/worker/wrangler-production.generated.json \
--worker-name kody-runtime \
--main-worker-name kody \
--out-config packages/runtime-worker/wrangler-production.generated.json
npm run deploy -- --config packages/runtime-worker/wrangler-production.generated.json
npm run deploy -- --config packages/worker/wrangler-production.generated.json
```

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the manual deployment procedure complete.

The automated procedure syncs secrets to both Workers and applies D1 migrations in Lines 85-90. The manual procedure in Lines 113-121 performs neither operation. If an operator uses this fallback, kody-runtime can deploy without its required secrets and shared database changes can remain unapplied.

Add the equivalent secret-sync and D1-migration steps, or state verified preconditions that require both operations to have completed before either deploy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/contributing/architecture/runtime-worker-migration-runbook.md` around
lines 108 - 122, Complete the manual execution procedure by adding the
equivalent secret synchronization for both Workers and the D1 migration step
before deployment, reusing the commands or symbols from the automated procedure.
Alternatively, state explicit verified preconditions requiring both operations
to finish before either deploy command runs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 290f2d0: the manual-execution section now mirrors the workflow order — generate configs, sync secrets to both workers (tools/ci/sync-worker-secrets.ts, with the runtime-lane allowlist spelled out and deploy.yml named as the source of truth for the exact flag lists), apply the shared D1 migrations (wrangler-env.ts d1 migrations apply APP_DB/AUDIT_DB --remote), then deploy runtime → main.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +52 to +78
"migrations": [
{
"tag": "v1",
"transferred_classes": [
{
"from": "StorageRunner",
"from_script": "kody",
"to": "StorageRunner",
},
{
"from": "RunLog",
"from_script": "kody",
"to": "RunLog",
},
{
"from": "PackageRealtimeSession",
"from_script": "kody",
"to": "PackageRealtimeSession",
},
{
"from": "PackageServiceInstance",
"from_script": "kody",
"to": "PackageServiceInstance",
},
],
},
],

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The four Durable Object classes are transferred out of kody while kody still declares them. The runtime configuration transfers StorageRunner, RunLog, PackageRealtimeSession, and PackageServiceInstance from the kody script, but the main configuration still names those classes in its top-level new_sqlite_classes migrations, which env.test and env.preview inherit. Wrangler validates that every class named in a migration is exported by the uploaded entry module.

  • packages/runtime-worker/wrangler.jsonc#L52-L78: confirm the transfer targets match the classes that packages/worker/src/runtime-worker.ts exports, and record the required deploy order in the runbook.
  • packages/worker/wrangler.jsonc#L255-L292: confirm that packages/worker/src/index.ts still exports the four classes after the split, or plan the migration entries that retire them from the kody script.
📍 Affects 2 files
  • packages/runtime-worker/wrangler.jsonc#L52-L78 (this comment)
  • packages/worker/wrangler.jsonc#L255-L292
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/runtime-worker/wrangler.jsonc` around lines 52 - 78, Update
packages/runtime-worker/wrangler.jsonc lines 52-78 to ensure the transferred
Durable Object classes match the exports from
packages/worker/src/runtime-worker.ts, and document the required deployment
order in the runbook. In packages/worker/wrangler.jsonc lines 255-292, verify
that packages/worker/src/index.ts still exports StorageRunner, RunLog,
PackageRealtimeSession, and PackageServiceInstance; if not, add migration
entries that retire those classes from the kody script so every declared
migration class is exported by its uploaded entry module.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified — no change needed. packages/worker/src/index.ts deliberately still exports StorageRunner, RunLog, PackageRealtimeSession, and PackageServiceInstance (needed for the test environment's in-process DO fallback), so wrangler's "every migration class must be exported by the entry module" validation holds for kody in all environments including env.test/env.preview, which inherit the top-level new_sqlite_classes history. That history must stay on kody: a transferred_classes migration requires the source script to retain the migrations that created the classes. The transfer targets match the classes runtime-worker.ts exports, and the deploy order (runtime first, main second) is in the runbook. Retiring the classes from kody (deleted-class migration + dropping the exports) is a deliberate post-cutover follow-up, noted in the runbook's rollback constraints.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +94 to +246
"worker_loaders": [
{
"binding": "LOADER",
},
{
"binding": "APP_LOADER",
},
],
"workflows": [
{
"binding": "DYNAMIC_CALLABLE_WORKFLOWS",
"name": "kody-runtime-dynamic-callable-workflows",
"class_name": "DynamicCallableWorkflow",
},
],
"durable_objects": {
"bindings": [
// Runtime-owned classes, local to this script.
{
"class_name": "StorageRunner",
"name": "STORAGE_RUNNER",
},
{
"class_name": "RunLog",
"name": "RUN_LOG",
},
{
"class_name": "PackageRealtimeSession",
"name": "PACKAGE_REALTIME_SESSION",
},
{
"class_name": "PackageServiceInstance",
"name": "PACKAGE_SERVICE_INSTANCE",
},
// Main-worker classes reached cross-script. UserMeter stays
// in the main Worker per ADR 0016; the rest back capability
// execution paths reachable from package code.
{
"class_name": "UserMeter",
"name": "USER_METER",
"script_name": "kody",
},
{
"class_name": "MCP",
"name": "MCP_OBJECT",
"script_name": "kody",
},
{
"class_name": "RemoteConnectorSession",
"name": "REMOTE_CONNECTOR_SESSION",
"script_name": "kody",
},
{
"class_name": "McpClientHub",
"name": "MCP_CLIENT_HUB",
"script_name": "kody",
},
{
"class_name": "JobManager",
"name": "JOB_MANAGER",
"script_name": "kody",
},
{
"class_name": "Mailbox",
"name": "MAILBOX",
"script_name": "kody",
},
{
"class_name": "RepoSession",
"name": "REPO_SESSION",
"script_name": "kody",
},
],
},
"d1_databases": [
{
"binding": "APP_DB",
"database_name": "kody",
"migrations_dir": "./../worker/migrations",
},
{
"binding": "AUDIT_DB",
"database_name": "kody-audit",
"migrations_dir": "./../worker/audit-migrations",
},
],
"kv_namespaces": [
{
"binding": "OAUTH_KV",
},
{
"binding": "BUNDLE_ARTIFACTS_KV",
},
],
"r2_buckets": [
{
"binding": "COMMUNITY_ASSETS",
"bucket_name": "kody-community-assets",
},
],
"queues": {
"producers": [
{
"binding": "PLATFORM_FEEDBACK_DISPATCH_QUEUE",
"queue": "kody-platform-feedback-dispatch",
},
{
"binding": "COMMUNITY_ACTIVITY_DISPATCH_QUEUE",
"queue": "kody-community-activity-dispatch",
},
{
"binding": "SCHEDULED_DISPATCH_QUEUE",
"queue": "kody-scheduled-dispatch",
},
{
"binding": "PACKAGE_EVENTS_DISPATCH_QUEUE",
"queue": "kody-package-events-dispatch",
},
{
"binding": "WEBHOOK_DISPATCH_QUEUE",
"queue": "kody-webhook-dispatch",
},
],
},
"vectorize": [
{
"binding": "CAPABILITY_VECTOR_INDEX",
"index_name": "kody-capabilities-prod",
},
],
"analytics_engine_datasets": [
{
"binding": "USAGE_EVENTS",
"dataset": "kody_usage_events",
},
{
"binding": "FLAG_EXPOSURES",
"dataset": "kody_flag_exposures",
},
{
"binding": "MCP_PROTOCOL_EVENTS",
"dataset": "kody_mcp_protocol_events",
},
],
"ai": {
"binding": "AI",
},
"vars": {
"SENTRY_ENVIRONMENT": "production",
"SENTRY_TRACES_SAMPLE_RATE": 0,
"ARTIFACTS_NAMESPACE": "production",
"PACKAGE_APP_BASE_URL": "https://kodyapps.dev",
},

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare required env-schema keys against the runtime worker bindings.
set -euo pipefail

echo "== env schema =="
cat -n packages/worker/src/env-schema.ts

echo "== omitted binding usage in the runtime entry graph =="
rg -nP '\b(ASSETS|EMAIL|EMAIL_BLOBS|EMAIL_EVENTS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH)\b' \
  --glob 'packages/worker/src/**/*.ts' --glob '!**/*.test.ts' -C2

Repository: kentcdodds/kody

Length of output: 13740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== schema and validation references =="
rg -n -C3 'EnvSchema|safeParse|parse\(|env-schema|EMAIL_BLOBS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH|ASSETS' \
  packages/worker packages/runtime-worker --glob '!**/*.test.*'

echo "== all repository references to omitted bindings =="
rg -n -C2 '\b(ASSETS|EMAIL|EMAIL_BLOBS|EMAIL_EVENTS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH)\b' \
  --glob '!**/*.test.*'

echo "== runtime configuration keys =="
cat -n packages/runtime-worker/wrangler.jsonc | sed -n '1,280p'

Repository: kentcdodds/kody

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== environment validation implementation =="
cat -n packages/worker/src/app/env.ts

echo "== runtime entrypoints and imports =="
rg -n -C4 'from .*(app/env|`#worker/env`|worker/src)|validateEnv|getEnv|fetch\s*\(' \
  packages/runtime-worker packages/worker/src/index.ts packages/worker/src --glob '*.ts' \
  --glob '!**/*.test.ts' | head -n 500

echo "== runtime production and preview vars =="
rg -n -C8 '"(production|preview)"|SENTRY_ENVIRONMENT|AUTH_RATE_LIMITER|service' \
  packages/runtime-worker/wrangler.jsonc

echo "== focused omitted-binding references in source =="
rg -n -C3 'EMAIL_BLOBS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH|ASSETS' \
  packages/worker/src packages/runtime-worker --glob '*.ts' --glob '*.tsx' \
  --glob '!**/*.test.*' | head -n 500

Repository: kentcdodds/kody

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime-worker files =="
git ls-files packages/runtime-worker

echo "== runtime-worker entry references =="
rg -n -C5 'getEnv|EnvSchema|validateEnv|from .*(worker|`#app`|`#worker`)|export default|WorkerEntrypoint|fetch' \
  packages/runtime-worker --glob '!**/*.test.*'

echo "== runtime Worker source files and imports of shared handlers =="
fd -t f . packages/runtime-worker | while read -r file; do
  case "$file" in
    *.ts|*.tsx|*.js|*.mjs) echo "---- $file"; sed -n '1,220p' "$file" ;;
  esac
done

Repository: kentcdodds/kody

Length of output: 1356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime worker source candidates =="
fd -t f 'runtime-worker|index' packages/worker/src packages/runtime-worker

echo "== runtime worker source references =="
rg -n -C5 'runtime-worker|RuntimeWorker|getEnv|EnvSchema|SENTRY_ENVIRONMENT|AUTH_RATE_LIMITER' \
  packages/worker/src --glob '*.ts' --glob '*.tsx' --glob '!**/*.test.*'

echo "== runtime worker source file contents =="
for file in $(fd -t f 'runtime-worker' packages/worker/src); do
  echo "---- $file"
  cat -n "$file"
done

Repository: kentcdodds/kody

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime-worker.ts =="
cat -n packages/worker/src/runtime-worker.ts

echo "== runtime handlers imported by runtime-worker.ts =="
for file in packages/worker/src/app/handlers/package-app.ts packages/worker/src/app/package-app-origin.ts packages/worker/src/package-invocations/http.ts packages/worker/src/package-invocations/*.ts; do
  if [ -f "$file" ]; then
    echo "---- $file"
    rg -n -C4 'getEnv|handlePackage|EnvSchema|EMAIL_BLOBS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH' "$file"
  fi
done

Repository: kentcdodds/kody

Length of output: 5048


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== direct environment-validation calls in runtime request handlers =="
rg -n -C6 'getEnv|createAppRouter|handlePackageApp|handlePackageInvocation|packageAppOrigin' \
  packages/worker/src/app/handlers/package-app.ts \
  packages/worker/src/app/package-app-origin.ts \
  packages/worker/src/package-invocations/http.ts

echo "== imports of getEnv and runtime handler dependency names =="
rg -n '^import .*getEnv|from .*app/env|from .*app/handler|from .*ssr-render|from .*oauth-handlers' \
  packages/worker/src/app/handlers/package-app.ts \
  packages/worker/src/app/package-app-origin.ts \
  packages/worker/src/package-invocations/http.ts \
  packages/worker/src/package-invocations --glob '*.ts'

echo "== all runtime config binding declarations, vars, and secrets-related comments =="
cat -n packages/runtime-worker/wrangler.jsonc | sed -n '1,430p'

Repository: kentcdodds/kody

Length of output: 19278


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== runtime package invocation attachment path =="
cat -n packages/worker/src/package-invocations/module-execution.ts | sed -n '240,295p'
rg -n -C8 'getEmailAttachmentById|EMAIL_BLOBS' \
  packages/worker/src/email packages/worker/src/package-invocations

echo "== package invocation validation and environment access =="
cat -n packages/worker/src/package-invocations/http.ts | sed -n '200,380p'
rg -n -C5 'getEnv|EMAIL_BLOBS|AUTH_RATE_LIMITER|checkAuthRateLimit' \
  packages/worker/src/package-invocations --glob '*.ts'

echo "== all runtime-worker reachable references to omitted bindings =="
rg -n -C4 'EMAIL_BLOBS|EMAIL|EMAIL_EVENTS|AUTH_RATE_LIMITER|OAUTH_PURGE_COORDINATOR|STRIPE_PLAN_REFRESH|ASSETS' \
  packages/worker/src/runtime-worker.ts \
  packages/worker/src/app/handlers/package-app.ts \
  packages/worker/src/app/package-app-origin.ts \
  packages/worker/src/package-invocations \
  packages/worker/src/package-runtime \
  packages/worker/src/storage-runner.ts \
  packages/worker/src/run-records --glob '*.ts'

Repository: kentcdodds/kody

Length of output: 50372


Bind EMAIL_BLOBS in both runtime environments. The package-invocation path reads email attachments through env.EMAIL_BLOBS, but the runtime Worker does not define this R2 binding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/runtime-worker/wrangler.jsonc` around lines 94 - 246, Add an
`EMAIL_BLOBS` R2 bucket binding to the runtime Worker's R2 configuration for
both runtime environments, using the existing email-blob bucket name and binding
conventions. Ensure package-invocation code can access the bucket through
`env.EMAIL_BLOBS` in each environment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid — fixed in 290f2d0. The runtime worker's env-schema requires EMAIL_BLOBS (package capability paths can read email blobs), so packages/runtime-worker/wrangler.jsonc now binds it in both environments: kody-email-blobs (production) and kody-preview-email-blobs (preview), and the config generator test fixture covers the copy. The other omitted bindings (ASSETS, AUTH_RATE_LIMITER, OAUTH_PURGE_COORDINATOR, STRIPE_PLAN_REFRESH, etc.) are main-lane only and stay off the runtime worker.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +58 to +67
STORAGE_RUNNER: DurableObjectNamespace /* StorageRunner from kody-runtime */;
RUN_LOG: DurableObjectNamespace /* RunLog from kody-runtime */;
USER_METER: DurableObjectNamespace<import("./src/index").UserMeter>;
MAILBOX: DurableObjectNamespace<import("./src/index").Mailbox>;
STRIPE_PLAN_REFRESH: DurableObjectNamespace<import("./src/index").StripePlanRefresh>;
REPO_SESSION: DurableObjectNamespace<import("./src/index").RepoSession>;
PACKAGE_REALTIME_SESSION: DurableObjectNamespace<import("./src/index").PackageRealtimeSession>;
PACKAGE_SERVICE_INSTANCE: DurableObjectNamespace<import("./src/index").PackageServiceInstance>;
DYNAMIC_CALLABLE_WORKFLOWS: Workflow<Parameters<import("./src/index").DynamicCallableWorkflow['run']>[0]['payload']>;
PACKAGE_REALTIME_SESSION: DurableObjectNamespace /* PackageRealtimeSession from kody-runtime */;
PACKAGE_SERVICE_INSTANCE: DurableObjectNamespace /* PackageServiceInstance from kody-runtime */;
RUNTIME_WORKER: Fetcher /* kody-runtime */;
DYNAMIC_CALLABLE_WORKFLOWS: Workflow /* DynamicCallableWorkflow from kody-runtime */;

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find call sites of the runtime-owned Durable Object and Workflow bindings.
set -euo pipefail

rg -nP -C4 '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' \
  --glob '*.ts' --glob '!**/*.d.ts'

Repository: kentcdodds/kody

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- binding references ---'
rg -n -C3 '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' . \
  --glob '!**/node_modules/**' \
  --glob '!**/.git/**'

printf '%s\n' '--- worker source files ---'
git ls-files 'packages/worker/*' 'packages/worker/**/*' | sed -n '1,160p'

printf '%s\n' '--- DurableObjectNamespace and Workflow declarations/usages ---'
rg -n -C2 'DurableObjectNamespace|DYNAMIC_CALLABLE_WORKFLOWS|Workflow<' packages/worker packages \
  --glob '!**/node_modules/**' \
  --glob '!**/*.d.ts' | sed -n '1,240p'

Repository: kentcdodds/kody

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- production call sites in packages/worker/src ---'
rg -n -C2 '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' \
  packages/worker/src \
  --glob '*.ts' \
  --glob '!**/*.test.ts' \
  --glob '!**/*.spec.ts' \
  --glob '!**/test/**' \
  | sed -n '1,360p'

printf '%s\n' '--- type assertions and shared interfaces around these bindings ---'
rg -n -C3 'as unknown as|Rpc|DurableObjectNamespace|Workflow' packages/worker/src \
  --glob '*.ts' \
  --glob '!**/*.test.ts' \
  --glob '!**/*.spec.ts' \
  | rg -n -C2 'STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS|Rpc|DurableObjectNamespace|Workflow' \
  | sed -n '1,360p'

Repository: kentcdodds/kody

Length of output: 44367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- storage runner client ---'
sed -n '1,45p;560,620p' packages/worker/src/storage-runner.ts

printf '%s\n' '--- run log client and RPC type ---'
sed -n '190,255p' packages/worker/src/run-records/service.ts
rg -n 'export (type|interface) RunLogRpc|type RunLogRpc|interface RunLogRpc|runLogRpc\(' packages/worker/src/run-records/service.ts

printf '%s\n' '--- package service client ---'
sed -n '170,245p' packages/worker/src/package-runtime/package-service.ts

printf '%s\n' '--- realtime session client ---'
sed -n '845,925p' packages/worker/src/package-runtime/realtime-session.ts

printf '%s\n' '--- workflow binding resolver and workflow calls ---'
sed -n '135,180p;900,970p;1080,1135p;1260,1325p' packages/worker/src/package-runtime/package-workflows.ts

Repository: kentcdodds/kody

Length of output: 19119


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- RunLogRpc declaration and imports ---'
sed -n '1,35p' packages/worker/src/run-records/service.ts
rg -n -C3 'getPackageServiceStub|\.fetch\(|PackageService' packages/worker/src/package-runtime/package-service.ts \
  | sed -n '1,240p'

printf '%s\n' '--- all production calls on package-service and realtime stubs ---'
python3 - <<'PY'
from pathlib import Path
for name in [
    "packages/worker/src/package-runtime/package-service.ts",
    "packages/worker/src/package-runtime/realtime-session.ts",
]:
    text = Path(name).read_text().splitlines()
    print(f"--- {name} ---")
    for i, line in enumerate(text, 1):
        if "getPackageServiceStub" in line or "getPackageRealtimeStub" in line:
            lo, hi = max(1, i - 8), min(len(text), i + 35)
            for n in range(lo, hi + 1):
                print(f"{n}:{text[n-1]}")
PY

printf '%s\n' '--- direct methods on runtime-owned bindings in production source ---'
rg -n '\b(STORAGE_RUNNER|RUN_LOG|PACKAGE_REALTIME_SESSION|PACKAGE_SERVICE_INSTANCE|DYNAMIC_CALLABLE_WORKFLOWS)\b' packages/worker/src \
  --glob '*.ts' \
  --glob '!**/*.test.ts' \
  --glob '!**/*.spec.ts' \
  | rg '\.(get|fetch|idFromName|create)\b|\.get\(' \
  | sed -n '1,240p'

Repository: kentcdodds/kody

Length of output: 14082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- local RunLog RPC contract and implementation ---'
rg -n -C4 'RunLogRpc|class RunLog|RunLogBase|export .*RunLog' \
  packages/worker/src/run-records/run-log-do.ts \
  packages/worker/src/run-records \
  | sed -n '1,260p'

printf '%s\n' '--- runtime package definitions for corresponding RPC contracts ---'
rg -n -C3 'StorageRunner|RunLog|PackageRealtimeSession|PackageServiceInstance|DynamicCallableWorkflow|Rpc' \
  packages/runtime-worker packages \
  --glob '!**/*.test.ts' \
  --glob '!**/*.spec.ts' \
  --glob '!**/worker-configuration.d.ts' \
  | sed -n '1,320p'

printf '%s\n' '--- workflow instance helper declarations and calls ---'
rg -n -C3 'getExistingWorkflowInstance|readWorkflowInstanceSummary|workflowBinding\.(get|create)|WorkflowInstance' \
  packages/worker/src/package-runtime/package-workflows.ts \
  | sed -n '1,280p'

Repository: kentcdodds/kody

Length of output: 38831


Preserve explicit RPC contracts for runtime-owned stubs. PACKAGE_REALTIME_SESSION and PACKAGE_SERVICE_INSTANCE use fetch(), and DYNAMIC_CALLABLE_WORKFLOWS restores its payload type. However, STORAGE_RUNNER and RUN_LOG call RPC methods through local as unknown as contracts. Move these contracts to a shared runtime interface so RPC signature changes remain type-checked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/worker-configuration.d.ts` around lines 58 - 67, Add the
explicit RPC contracts currently represented by local as unknown as casts for
STORAGE_RUNNER and RUN_LOG to a shared runtime interface, then type both
DurableObjectNamespace declarations with that interface. Remove the local casts
and ensure the shared interface exposes all RPC methods and payload types so
signature changes remain type-checked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No change needed here. worker-configuration.d.ts is generated by wrangler types — for cross-script DO/Workflow bindings it cannot emit the class generic, so the untyped DurableObjectNamespace /* ... from kody-runtime */ shape is wrangler's output, not hand-written. The call sites are unchanged by this PR: both workers compile from the same source tree, and the existing local RPC contracts (RunLogRpc in run-records/service.ts, the typed shape in storage-runner.ts) still describe exactly what the classes implement. Promoting those to shared interfaces in packages/shared is a reasonable follow-up, but this PR keeps the cross-worker surface limited to the coarse contracts ADR 0016 calls for (packages/shared/src/runtime-worker.ts), so I'm leaving the pre-existing internal typing as is.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

Comment on lines +265 to +302
// Transfer migrations move Durable Object storage between scripts;
// changing or removing one after it ships would orphan or duplicate
// storage, so every transferred_classes migration must exactly match a
// reviewed baseline entry.
const protectedTransferMigrations =
baseline.protected_transfer_migrations ?? []
for (const protectedTransfer of protectedTransferMigrations) {
const current = migrationsByTag.get(protectedTransfer.tag)
const currentTransfers = normalizeTransferredClasses(
current?.transferred_classes,
)
if (
!currentTransfers ||
!sameStrings(
sortedTransferKeys(currentTransfers),
sortedTransferKeys(protectedTransfer.transferred_classes),
)
) {
errors.push(
`${configPath}: protected transferred_classes migration "${protectedTransfer.tag}" was removed, renamed, or changed (expected ${protectedTransfer.transferred_classes.map((transfer) => `${transfer.from_script}/${transfer.from} -> ${transfer.to}`).join(', ')}).`,
)
}
}
const protectedTransferTags = new Set(
protectedTransferMigrations.map(({ tag }) => tag),
)
for (const migration of migrations) {
if (
migration.transferred_classes !== undefined &&
typeof migration.tag === 'string' &&
!protectedTransferTags.has(migration.tag)
) {
errors.push(
`${configPath}: transferred_classes migration "${migration.tag}" is not recorded in ${defaultDurableObjectBaselinePath}. Update the reviewed baseline so this migration remains protected after it lands.`,
)
}
}

@coderabbitai coderabbitai Bot Aug 11, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

The guardrail reads only top-level migrations, so the runtime Worker's per-environment migration is unprotected. packages/runtime-worker/wrangler.jsonc declares env.preview.migrations with new_sqlite_classes for the four runtime classes. checkDurableObjectConfig collects migrations only from config.migrations, so that list is neither matched against the baseline nor reported as unrecorded.

  • tools/check-deploy-guardrails.ts#L265-L302: collect migrations from each env.<name> section in addition to the top level, and scope the duplicate-tag check per section so an environment may reuse the tag v1.
  • tools/ci/durable-object-baseline.json#L165-L241: after the collector change, record the preview v1 new_sqlite_classes migration for packages/runtime-worker/wrangler.jsonc so it stays protected.
📍 Affects 2 files
  • tools/check-deploy-guardrails.ts#L265-L302 (this comment)
  • tools/ci/durable-object-baseline.json#L165-L241
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/check-deploy-guardrails.ts` around lines 265 - 302, Update
checkDurableObjectConfig and its migration collection to include migrations from
every config.env.<name> section, while keeping duplicate-tag validation scoped
per section so environment-specific tags such as v1 may be reused. Add the
preview v1 new_sqlite_classes migration for
packages/runtime-worker/wrangler.jsonc to tools/ci/durable-object-baseline.json,
ensuring it is protected by the baseline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 290f2d0: checkDurableObjectConfig now collects migrations from every env.<name>.migrations section in addition to the top level, with the duplicate-tag check scoped per section (so preview may reuse v1). Error messages include the section location, baseline entries carry an optional location field, and the runtime worker's preview v1 new_sqlite_classes migration is now recorded in tools/ci/durable-object-baseline.json ("location": "env.preview.migrations"). Covered by a new test in tools/check-deploy-guardrails.node.test.ts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped: comment is from another GitHub bot.

devin-ai-integration Bot and others added 8 commits August 11, 2026 16:33
…althcheck guards, env-scoped migration guardrails, runbook completeness

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…inned names, no ai binding, injected vars)

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…tion

Resolves overlap between the runtime lane (ADR 0016 track b) and the jobs
lane extraction that landed on main:
- runtime worker reaches jobs via the JOBS service binding (JobManager
  moved to kody-jobs) instead of a cross-script binding
- runtime config generator copies the resolved JOBS service name from the
  main generated config
- production runtime deploy pins --name kody-runtime and rewrites
  cross-script refs to the deployed kody-production script name
- baseline records main env.preview/env.test migration sections and the
  jobs worker migrations for the extended guardrails

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
… classes

A preview app script deployed before a Durable Object class moved to
another worker (e.g. JobManager -> kody-jobs) still has live DO
namespaces for that class, so the API rejects new versions with
'does not export class' [code: 10064]. Preview scripts are disposable
(data lives in per-preview D1/KV/R2), so the bootstrap deploy now
deletes the stale script and deploys fresh when it hits that error.

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…er extraction

The wildcard zone route for per-user package-app subdomains
(*.kodyapps.dev/*, decision 0017) is published by the runtime worker
alongside the apex custom domain, since the package-app host belongs to
the runtime lane (ADR 0016). The main worker's generated config keeps
publishing only the app origin and legacy origins.

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
Local dev (and any fresh script) replays the whole top-level migrations
chain, which still created JobManager even though the class moved to the
jobs worker (ADR 0016) and is no longer exported by the main script, so
'npm run dev' failed to boot with 'Class extends value undefined'. The
preview and test envs already got chains without the v6/v9 pair; the
top-level (inherited by production) chain now matches. The deployed
production script is past these tags, so its migration diff is empty.

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
…t pitfall

Co-Authored-By: Kent C. Dodds <me@kentcdodds.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🧪 Runtime testing: multi-worker local dev

Verdict: npm run dev multi-worker (main + runtime + jobs) works on HEAD — after fixing a boot failure inherited from the jobs-worker extraction on main.

Boot failure found on HEAD (also reproduces on origin/main) — fixed in 0ff96f47

✘ [ERROR] service core:user:kody-production: Uncaught TypeError: Class extends value undefined is not a constructor or null
✘ [ERROR] The Workers runtime failed to start.

Root cause: local dev (--env production) inherits the top-level migrations chain in packages/worker/wrangler.jsonc, which still netted out JobManager as a live DO class (v6 adds JobManager+JobRunner, v9 deletes only JobRunner) even though the class moved to packages/jobs-worker and is no longer exported by the main script. Fresh Miniflare replays the full chain and can't wrap the missing class. preview/test already had chains without the v6/v9 pair; the top-level chain now matches (the deployed production script is past those tags, so its migration diff is unaffected).

After the fix, on HEAD: main worker Ready on http://localhost:3742, /health → {"ok":true}, RUNTIME_WORKER (kody-runtime) and JOBS (kody-jobs#JobsService) bindings resolve, /__runtime/health on the main port → 404 (forwarding is scoped, not wholesale), unauthenticated GET /@kody/packages/… → 302 → /login through the runtime lane.

This PR's three local-dev fixes confirmed working (tested at 9ed0ea40, clean boot, no workarounds)

  • No AI remote-proxy attempt (generated local config drops the ai binding)
  • env.RUNTIME_WORKER (kody-runtime) … [connected] (worker-name pinning works)
  • Runtime lane serves with injected secrets: GET /@kody/packages/nonexistent → 302→/login; POST /@kody/api/package-invocations/foo/bar → 401 {"ok":false,"error":{"code":"unauthorized"}}; main /health → {"ok":true}

Logged-in Account page — golden path works with multi-worker dev

Golden-path screenshots
🟢 Home page 🟢 Login page
home login

Untested: real package load/invocation (no seeded packages); preview deploy still blocked on the one-time workflow_dispatch cleanup of the stale kody-pr-1384 script (pre-jobs-extraction JobManager DOs).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant