Skip to content

feat(runner): isolate structured workflows in one-attempt Kubernetes Pods - #287

Closed
chrisleekr wants to merge 1 commit into
mainfrom
feat/isolated-workflow-runner
Closed

feat(runner): isolate structured workflows in one-attempt Kubernetes Pods#287
chrisleekr wants to merge 1 commit into
mainfrom
feat/isolated-workflow-runner

Conversation

@chrisleekr

@chrisleekr chrisleekr commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Stack 3 of 3 · base feat/repo-config-surface (#286) · after #285

Largest of the three. GitHub shows only this PR's own commit; review after #286 merges, or diff against its branch.

What this does

Structured workflows previously ran on the shared daemon fleet: a long-lived daemon claimed a job over the fleet WebSocket and executed it in-process via src/daemon/workflow-executor.ts, alongside other tenants' work, holding fleet-wide credentials. This PR removes that path entirely and gives every structured workflow-run its own Kubernetes Pod and its own capability Secret, for exactly one attempt.

flowchart TD
    Disp["dispatcher<br/>workflow-run claimed by controller"]:::ctl
    Payload["workflow-runner-payload.ts<br/>resolves Gate-2 policy, bounds input"]:::ctl
    Cap["workflow-runner-capability.ts<br/>mints per-attempt capability Secret"]:::sec
    Spawn["workflow-runner-spawner.ts<br/>creates ONE bare Pod + Secret"]:::sec
    Pod["src/runner/main.ts<br/>one attempt, one Pod"]:::pod
    Guard["process-boundary.ts<br/>+ native/daemon-process-guard.c<br/>worker cannot outlive its attempt"]:::pod
    Scan["output-sanitizer.ts<br/>STRICT: disable, fail or timeout<br/>rejects the command"]:::stop
    Result["workflow-runner-result.ts<br/>terminal payload stored<br/>BEFORE projections and ACK"]:::ctl
    Recon["workflow-runner-reconciler.ts<br/>+ liveness reaper, expiry notifier"]:::ctl
    Clean["workflow-runner-resources.ts<br/>Pod + Secret torn down"]:::sec
    Fence["fenced by run id, attempt id,<br/>owner id, lease, command receipt"]:::gate

    Disp --> Payload --> Cap --> Spawn --> Pod
    Pod --> Guard
    Pod -->|"every RPC command"| Scan
    Scan --> Result --> Recon --> Clean
    Fence -.->|"gates every mutation"| Result
classDef ctl fill:#2c3e50,color:#ffffff
classDef sec fill:#8e44ad,color:#ffffff
classDef pod fill:#1e8449,color:#ffffff
classDef stop fill:#c0392b,color:#ffffff
classDef gate fill:#ecf0f1,color:#2c3e50
Loading

The isolation contract

A workflow-run must never enter the shared-daemon job protocol. src/shared/workflow-runner-messages.ts is a deliberately separate schema from ws-messages.ts, not a superset of it, so the two protocols cannot drift into each other by accident. Legacy and scoped jobs stay on the shared fleet unchanged.

The runner's deny set rejects App credentials, PAT, database URL, Valkey URL, Kubernetes config, Context7 key, global GitHub token, and daemon-auth tokens. What the Pod gets instead is a short-lived App installation token scoped to its one target repository. This is also why structured dispatch fails closed in PAT mode: a PAT cannot be narrowed to one repo.

Stricter output scanning than the general path

The general GitHub-output scanner in safePostToGitHub is fail-open: a provider outage falls back to the regex pass rather than blocking a comment. The runner RPC scanner inverts that. Scanner disablement, failure, or timeout rejects the command and converts a result into a fixed safe failure, logging workflow_runner_output_scan_unavailable at error. The runner handles attacker-influenced repository content with a repo-scoped write token, so silently degrading to regex-only there is not an acceptable trade.

Ordering that matters

The terminal payload is stored before projections and before the ACK. A crash between store and ACK replays into an idempotent projection; the reverse order would lose the result outright.

Also in this PR

Area Change
Orchestrator resilience Liveness reaper, queue worker, ws-server split into ws-connection.ts, dispatch outbox, completion reconciler, expiry notifier
Migration 017 workflow_run_leases — adds attempt_id, offer_id, lease/deadline columns and the workflow_attempt_commands table
src/config.ts The remaining surface, including capability-secret and namespace validation that only applies once the runner exists
Workflows Handler and ship-rail updates that consume the Gate-2 policy from #286
Test layout Moves the last colocated src/**/*.test.ts files under test/ and tightens check:test-globs to reject tests in production source

Verification

Gate Result
typecheck · lint · format pass, 0 errors
all 11 check:* guards pass
test 185 files pass, 0 assertion failures — main baseline is 137

New CI job admission-policy validates the runner admission spec against Kubernetes 1.30 via bun run test:admission.

Review notes

  • src/orchestrator/history.ts and its two tests moved here from feat(core): extract HTTP router and add output-safety primitives #285 during review: they query attempt_id / offer_id, so they must land in the same PR as migration 017. Without that they would abort at runtime, not merely under test.
  • bunfig.toml drops the global 30s test timeout, which the runner tests exceed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KUPpJPtxAaHWrBsjytRGyM

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 214 files, which is 114 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: b2953114-d50b-4776-881f-21cc1e5f8c62

📥 Commits

Reviewing files that changed from the base of the PR and between 938aa20 and 44de644.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (214)
  • .env.example
  • .github/skills/research.md
  • .github/workflows/ci.yml
  • .github/workflows/docker-build.yml
  • .github/workflows/research.yml
  • .gitlab-ci.yml
  • CLAUDE.md
  • CONTRIBUTING.md
  • Dockerfile.daemon
  • bunfig.toml
  • docs/build/architecture.md
  • docs/operate/configuration.md
  • docs/operate/deployment.md
  • docs/operate/observability.md
  • docs/operate/runbooks/daemon-fleet.md
  • docs/operate/runbooks/scheduled-actions.md
  • docs/use/invoking.md
  • docs/use/repo-config.md
  • docs/use/scheduled-actions.md
  • docs/use/workflows/implement.md
  • docs/use/workflows/index.md
  • docs/use/workflows/plan.md
  • docs/use/workflows/resolve.md
  • docs/use/workflows/review.md
  • docs/use/workflows/triage.md
  • env-contract.json
  • examples/scheduled-actions/research.md
  • examples/workflow-runner-admission.yaml
  • mkdocs.yml
  • native/daemon-process-guard.c
  • package.json
  • scripts/build.ts
  • scripts/check-docs-sync.ts
  • scripts/check-test-globs.ts
  • scripts/env-contract.ts
  • scripts/test-isolated.sh
  • scripts/test-oauth.ts
  • scripts/test-workflow-runner-admission.sh
  • scripts/test-workflow-runner-admission.ts
  • src/app.ts
  • src/config-secret-env.ts
  • src/config.ts
  • src/core/pipeline.ts
  • src/core/tracking-comment.ts
  • src/daemon/daemon-id.ts
  • src/daemon/job-executor.ts
  • src/daemon/main.ts
  • src/daemon/process-boundary-smoke.ts
  • src/daemon/process-boundary.ts
  • src/daemon/scoped-rebase-executor.ts
  • src/daemon/workflow-executor.ts
  • src/daemon/ws-client.ts
  • src/db/migrations/017_workflow_run_leases.sql
  • src/k8s/ephemeral-daemon-spawner.ts
  • src/k8s/workflow-runner-spawner.ts
  • src/mcp/servers/inline-comment-dedup.ts
  • src/mcp/servers/inline-comment.ts
  • src/orchestrator/connection-handler.ts
  • src/orchestrator/history.ts
  • src/orchestrator/installation-token.ts
  • src/orchestrator/job-dispatcher.ts
  • src/orchestrator/job-queue.ts
  • src/orchestrator/liveness-reaper.ts
  • src/orchestrator/log-fields.ts
  • src/orchestrator/queue-worker.ts
  • src/orchestrator/repo-knowledge-persistence.ts
  • src/orchestrator/repo-knowledge.ts
  • src/orchestrator/review-learnings.ts
  • src/orchestrator/workflow-expiry-notifier.ts
  • src/orchestrator/workflow-runner-capability.ts
  • src/orchestrator/workflow-runner-controller.ts
  • src/orchestrator/workflow-runner-dispatch.ts
  • src/orchestrator/workflow-runner-output.ts
  • src/orchestrator/workflow-runner-payload.ts
  • src/orchestrator/workflow-runner-reconciler.ts
  • src/orchestrator/workflow-runner-resources.ts
  • src/orchestrator/workflow-runner-result.ts
  • src/orchestrator/workflow-runner-store.ts
  • src/orchestrator/ws-connection.ts
  • src/orchestrator/ws-server.ts
  • src/runner/main.ts
  • src/runner/output-sanitizer.ts
  • src/runner/process-boundary.ts
  • src/runner/token-deadline.ts
  • src/runner/workflow-executor.ts
  • src/runner/ws-client.ts
  • src/shared/daemon-types.ts
  • src/shared/dispatch-types.ts
  • src/shared/workflow-runner-messages.ts
  • src/shared/workflow-runner-provider.ts
  • src/shared/workflow-types.ts
  • src/shared/ws-messages.ts
  • src/utils/bot-identity.ts
  • src/webhook/auto-review-guard.ts
  • src/webhook/dispatch-failure.ts
  • src/webhook/events/issue-comment.ts
  • src/webhook/events/issues.ts
  • src/webhook/events/pull-request.ts
  • src/webhook/events/review-comment.ts
  • src/workflows/completion-reconciler.ts
  • src/workflows/discussion-digest.ts
  • src/workflows/dispatch-outbox.ts
  • src/workflows/dispatcher.ts
  • src/workflows/execution-row.ts
  • src/workflows/handlers/implement.ts
  • src/workflows/handlers/plan.ts
  • src/workflows/handlers/remember.ts
  • src/workflows/handlers/resolve.ts
  • src/workflows/handlers/review-learnings-footer.ts
  • src/workflows/handlers/review.ts
  • src/workflows/handlers/ship.ts
  • src/workflows/handlers/triage.ts
  • src/workflows/intent-classifier.ts
  • src/workflows/log-fields.ts
  • src/workflows/orchestrator.ts
  • src/workflows/registry.ts
  • src/workflows/runs-store.ts
  • src/workflows/ship/command-dispatch.ts
  • src/workflows/ship/intent.ts
  • src/workflows/ship/iteration.ts
  • src/workflows/ship/scoped/chat-thread.ts
  • src/workflows/ship/scoped/dispatch-scoped.ts
  • src/workflows/ship/session-runner.ts
  • src/workflows/tracking-mirror.ts
  • test/config.test.ts
  • test/core/hooks/forbidden-bash.test.ts
  • test/core/pipeline.test.ts
  • test/core/tracking-comment.test.ts
  • test/core/workspace-events.test.ts
  • test/daemon/daemon-id.test.ts
  • test/daemon/job-executor.test.ts
  • test/daemon/process-boundary.test.ts
  • test/daemon/scoped-offer-evaluator.test.ts
  • test/daemon/workflow-executor.test.ts
  • test/daemon/ws-client.test.ts
  • test/db/migrate.test.ts
  • test/db/migrations/008.test.ts
  • test/fixtures/workflow-runner-kind.yaml
  • test/integration/repo-knowledge.test.ts
  • test/integration/review-learnings.test.ts
  • test/integration/scoped-rebase-roundtrip.test.ts
  • test/integration/ship-iteration-loop.test.ts
  • test/integration/ship-tickle-resume.test.ts
  • test/integration/telemetry-aggregates.test.ts
  • test/integration/workflow-dispatch-wakeup.test.ts
  • test/k8s/ephemeral-daemon-spawner.test.ts
  • test/k8s/workflow-runner-spawner.test.ts
  • test/mcp/servers/inline-comment-dedup.test.ts
  • test/orchestrator/connection-handler.test.ts
  • test/orchestrator/daemon-disconnect-lifecycle.test.ts
  • test/orchestrator/history.test.ts
  • test/orchestrator/installation-token.test.ts
  • test/orchestrator/job-dispatcher.test.ts
  • test/orchestrator/job-queue.test.ts
  • test/orchestrator/liveness-reaper-resilience.test.ts
  • test/orchestrator/liveness-reaper.test.ts
  • test/orchestrator/log-fields.test.ts
  • test/orchestrator/queue-worker-resilience.test.ts
  • test/orchestrator/repo-knowledge-persistence.test.ts
  • test/orchestrator/workflow-expiry-notifier.test.ts
  • test/orchestrator/workflow-runner-capability.test.ts
  • test/orchestrator/workflow-runner-controller.test.ts
  • test/orchestrator/workflow-runner-dispatch.test.ts
  • test/orchestrator/workflow-runner-output.test.ts
  • test/orchestrator/workflow-runner-payload.test.ts
  • test/orchestrator/workflow-runner-reconciler.test.ts
  • test/orchestrator/workflow-runner-resources.test.ts
  • test/orchestrator/workflow-runner-result.test.ts
  • test/orchestrator/workflow-runner-store.test.ts
  • test/orchestrator/ws-server.test.ts
  • test/preload.ts
  • test/runner/main.test.ts
  • test/runner/output-sanitizer.test.ts
  • test/runner/process-boundary.test.ts
  • test/runner/token-deadline.test.ts
  • test/runner/workflow-executor.test.ts
  • test/runner/ws-client.test.ts
  • test/scripts/check-test-globs.test.ts
  • test/shared/dispatch-types.test.ts
  • test/shared/scoped-ws-messages.test.ts
  • test/shared/workflow-runner-messages.test.ts
  • test/shared/workflow-runner-provider.test.ts
  • test/shared/ws-messages.test.ts
  • test/utils/bot-identity.test.ts
  • test/webhook/auto-review-guard.test.ts
  • test/webhook/events/dispatch-failure.test.ts
  • test/webhook/events/issue-comment-cache.test.ts
  • test/webhook/events/issue-comment.test.ts
  • test/webhook/events/issues-cache.test.ts
  • test/webhook/events/pull-request-auto-review.test.ts
  • test/webhook/events/pull-request-cache.test.ts
  • test/webhook/events/pull-request-config-check.test.ts
  • test/workflows/dispatch-outbox.test.ts
  • test/workflows/dispatcher.test.ts
  • test/workflows/handlers/implement.test.ts
  • test/workflows/handlers/plan.test.ts
  • test/workflows/handlers/remember.test.ts
  • test/workflows/handlers/resolve.test.ts
  • test/workflows/handlers/review.test.ts
  • test/workflows/handlers/ship.test.ts
  • test/workflows/handlers/triage.test.ts
  • test/workflows/orchestrator.test.ts
  • test/workflows/runs-store.test.ts
  • test/workflows/ship/cancellation.test.ts
  • test/workflows/ship/command-dispatch.test.ts
  • test/workflows/ship/fix-attempts.test.ts
  • test/workflows/ship/intent.test.ts
  • test/workflows/ship/iteration-cap.test.ts
  • test/workflows/ship/iteration.test.ts
  • test/workflows/ship/lifecycle-commands.test.ts
  • test/workflows/ship/session-runner.resume.test.ts
  • test/workflows/ship/session-runner.test.ts
  • test/workflows/ship/tickle-scheduler.test.ts
  • test/workflows/tracking-mirror.test.ts

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


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.

@chrisleekr
chrisleekr force-pushed the feat/isolated-workflow-runner branch from 83743b4 to 8807bd5 Compare September 1, 2026 10:12
@chrisleekr
chrisleekr force-pushed the feat/isolated-workflow-runner branch from 8807bd5 to f75dafa Compare September 1, 2026 10:27
@chrisleekr
chrisleekr force-pushed the feat/isolated-workflow-runner branch from f75dafa to a67b143 Compare September 1, 2026 10:46
@chrisleekr
chrisleekr force-pushed the feat/isolated-workflow-runner branch from a67b143 to c701344 Compare September 1, 2026 10:50
Base automatically changed from feat/repo-config-surface to main September 1, 2026 12:15
…Pods

A `workflow-run` no longer enters the shared-daemon job protocol. One exact
attempt owns one Pod and one capability Secret, and every state mutation is
fenced by run ID, attempt ID, owner ID, lease, and command receipt.

Runner side:
- Add `src/runner/` (entry, WebSocket client, workflow executor, output
  sanitizer, token deadline, process boundary), replacing the deleted
  `src/daemon/workflow-executor.ts`.
- Add `native/daemon-process-guard.c` and `src/daemon/process-boundary.ts` so
  a worker cannot outlive its attempt.
- The runner deny set rejects App, PAT, database, Valkey, Kubernetes,
  Context7, global GitHub, and daemon-auth credentials.

Controller side:
- Add `src/orchestrator/workflow-runner-*.ts` (controller, dispatch, payload,
  store, result, reconciler, resources, capability, output) and
  `src/k8s/workflow-runner-spawner.ts`, which owns the per-attempt Secret.
- Add `src/shared/workflow-runner-messages.ts`: a separate protocol from the
  shared-daemon schema, deliberately not shared with it.
- Add migration `017_workflow_run_leases.sql`, which introduces the
  `attempt_id` / `offer_id` columns that `src/orchestrator/history.ts` and the
  daemon-disconnect fencing path query.
- The RPC output scanner is stricter than the general one: disablement,
  failure, or timeout rejects a command and converts a result to a fixed safe
  failure rather than failing open.

Also in this change:
- Orchestrator resilience: liveness reaper, queue worker, ws-server split into
  `ws-connection.ts`, dispatch outbox, completion reconciler, expiry notifier.
- The remaining `src/config.ts` surface, including the runner capability
  secret and namespace validation that only applies once the runner exists.
- Workflow handler and ship-rail updates that consume the Gate-2 policy.
- Move the last colocated `src/**/*.test.ts` files under `test/` and tighten
  `check:test-globs` to reject tests in production source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUPpJPtxAaHWrBsjytRGyM
@chrisleekr

Copy link
Copy Markdown
Owner Author

Superseded by a three-PR stack. Same change, same final tree, split so each PR is reviewable and under CodeRabbit's 100-file cap.

The stack's tip tree is byte-identical to this PR's head (44de644). Closing here; review continues on the three.

@chrisleekr chrisleekr closed this Sep 1, 2026
chrisleekr added a commit that referenced this pull request Sep 2, 2026
Slice-boundary artifacts introduced by the #287 split:

- .gitlab-ci.yml: drop the admission-policy job and its .docker_release
  `needs:` entry. `bun run test:admission` and the kind harness land with
  the isolated workflow runner, so on this branch the job could not run
  and its `needs:` edge would reject pipeline creation on any ref where
  the rule excluded it.
- Revert the `scoped-job:offer` / `scoped-job:completion` colon renames
  in test/daemon/scoped-offer-evaluator.test.ts,
  src/daemon/scoped-rebase-executor.ts and the scheduled-actions runbook.
  The wire schema on this branch still declares the hyphen form, so the
  test fixture was a type error.
- docs/operate/configuration.md: merge the two `## Per-repo config file`
  sections into one, keeping the deprecated-alias and trim-at-load notes.
- CLAUDE.md: tests live under test/ only. scripts/test-isolated.sh globs
  test/**/*.test.ts, and check:test-globs derives its glob set from that
  script, so a colocated src/ test now trips the guard.

Regressions against main:

- src/mcp/servers/inline-comment.ts: claim `pendingLocations` before the
  commit-SHA fetch and re-read the set after the dedup await. The claim
  had moved below two suspension points, reopening the duplicate-comment
  race the set exists to close.
- src/utils/bot-identity.ts: restore the `fetchSelfLogin` async helper in
  place of the inlined `.then(onFulfilled, onRejected)` chain.
- test/mcp/servers/inline-comment-dedup.test.ts: restore `SELF` in the
  three negative cases, which had been weakened to `null` and stopped
  exercising the author-matching branch.

- src/daemon/process-boundary.ts: the docstring claimed to fail daemon
  startup, but only the image smoke entrypoint calls it on this branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUPpJPtxAaHWrBsjytRGyM
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant