Skip to content

Commit a67b143

Browse files
chrisleekrclaude
andauthored
feat(runner): isolate structured workflows in one-attempt Kubernetes 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
1 parent 7edc5cd commit a67b143

207 files changed

Lines changed: 25646 additions & 3272 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ ANTHROPIC_API_KEY=
2626
# CLAUDE_CODE_OAUTH_TOKEN=
2727

2828
# Model override. Required when CLAUDE_PROVIDER=bedrock (Bedrock uses a different model ID
29-
# format than the Anthropic API). Optional for anthropic: defaults to claude-opus-4-7
29+
# format than the Anthropic API). Optional for anthropic: defaults to claude-opus-5
3030
# when unset.
3131
# Bedrock example: us.anthropic.claude-sonnet-4-6
32-
# Anthropic example: claude-opus-4-7
33-
# CLAUDE_MODEL=claude-opus-4-7
32+
# Anthropic example: claude-opus-5
33+
# CLAUDE_MODEL=claude-opus-5
3434

3535
# ──────────────────────────────────────────────────────────────────────────────
3636
# Amazon Bedrock (when CLAUDE_PROVIDER=bedrock)
@@ -141,6 +141,15 @@ AGENT_JOB_MODE=inline
141141
# the primary or this previous token (constant-time). Drop after rolling daemons.
142142
# DAEMON_AUTH_TOKEN_PREVIOUS=
143143

144+
# Controller-only HMAC root for deadline-bound workflow-runner capabilities.
145+
# Never mount either value on shared daemons or isolated runners, and never
146+
# reuse either DAEMON_AUTH_TOKEN rotation value.
147+
# Generate at least 32 random bytes (e.g. openssl rand -hex 32).
148+
# WORKFLOW_RUNNER_CAPABILITY_SECRET=
149+
# Optional rotation-window predecessor. Remove after every capability minted
150+
# with the old root has reached its signed expiry.
151+
# WORKFLOW_RUNNER_CAPABILITY_SECRET_PREVIOUS=
152+
144153
# Per-job cost ceiling in USD
145154
# JOB_MAX_COST_USD=80
146155

@@ -154,6 +163,8 @@ AGENT_JOB_MODE=inline
154163
# STALE_EXECUTION_THRESHOLD_MS=600000
155164
# DAEMON_DRAIN_TIMEOUT_MS=300000
156165
# JOB_MAX_RETRIES=3
166+
# Maximum wall-clock age for a queued structured-workflow dispatch.
167+
# WORKFLOW_DISPATCH_TIMEOUT_MS=4200000
157168
# OFFER_TIMEOUT_MS=5000
158169

159170
# Daemon-side settings (used by scripts/run-daemon.sh)
@@ -203,11 +214,14 @@ AGENT_JOB_MODE=inline
203214
# Enable debug logging of LLM prompts.
204215
# DEBUG_LLM_PROMPTS=1
205216

217+
# Per-repo config file, read from each installed repo's DEFAULT BRANCH root only.
218+
# Deprecated alias: SCHEDULER_CONFIG_FILE.
219+
# REPO_CONFIG_FILE=.github-app.yaml
220+
206221
# Scheduled actions (.github-app.yaml). The scheduler also requires
207222
# DATABASE_URL and a non-empty ALLOWED_OWNERS to start.
208223
# SCHEDULER_ENABLED=false
209224
# SCHEDULER_SCAN_INTERVAL_MS=300000
210-
# SCHEDULER_CONFIG_FILE=.github-app.yaml
211225
# Hard kill-switch for unattended auto-merge; per-action auto_merge is AND-ed
212226
# with this. Leave false unless you accept LLM-judged merges.
213227
# SCHEDULER_ALLOW_AUTO_MERGE=false

.github/skills/research.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Read the key files for the focus area:
6767
| idempotency | src/webhook/router.ts, src/core/tracking-comment.ts |
6868
| security | src/utils/, src/config.ts |
6969
| observability | src/logger.ts (and grep for logger usage across src/) |
70-
| testing | src/\*\*/\*.test.ts (sample 3-5; do not read all) |
70+
| testing | test/\*\*/\*.test.ts (sample 3-5; do not read all) |
7171
| docs | CLAUDE.md, README.md, docs/ |
7272
| infrastructure | .github/workflows/, Dockerfile.\*, package.json |
7373
| agent-sdk | src/core/prompt-builder.ts, src/core/executor.ts |

.github/workflows/ci.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,8 @@ jobs:
112112
run: bun run check:runner-pins
113113

114114
- name: Test-glob drift guard
115-
# Fails if any `*.test.ts` file is not reachable by the runner glob in
116-
# scripts/test-isolated.sh, so a colocated test outside the globbed
117-
# roots cannot go dark while CI stays green. See issue #201.
115+
# Fails if any `*.test.ts` file is outside the canonical `test/` tree
116+
# consumed by scripts/test-isolated.sh.
118117
run: bun run check:test-globs
119118

120119
- name: Destructive-action guard (FR-009)
@@ -190,3 +189,22 @@ jobs:
190189
# build/resolve drift class that shipped in 001990d before the daemon
191190
# image hits production. Must run AFTER `bun run build`.
192191
run: bun run check:mcp-bundle
192+
193+
admission-policy:
194+
name: Workflow runner admission
195+
runs-on: ubuntu-24.04
196+
timeout-minutes: 10
197+
steps:
198+
- name: Checkout source code
199+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
200+
201+
- name: Setup Bun
202+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
203+
with:
204+
bun-version-file: .tool-versions
205+
206+
- name: Install dependencies
207+
run: bun install --frozen-lockfile
208+
209+
- name: Validate admission against Kubernetes 1.30
210+
run: bun run test:admission

.github/workflows/docker-build.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
# - chrisleekr/github-app:<tag>-daemon (rich, Claude worker)
77
# Orchestrator additionally publishes a bare `<tag>` alias so existing
88
# consumers that pull `chrisleekr/github-app:<version>` keep working.
9-
# `:latest` is published on prod releases only, on the orchestrator variant.
9+
# Each variant also gets a mutable `:latest-<variant>` alias, so `latest-daemon`
10+
# resolves on Docker Hub the same way it does on the GitLab registry. Both that
11+
# alias and the bare `:latest` (orchestrator only) are prod-release only: a beta
12+
# prerelease must not move a tag prod consumers follow.
1013
#
1114
# Multi-platform via the documented split-and-merge pattern:
1215
# https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
@@ -185,6 +188,22 @@ jobs:
185188
GIT_HASH=${{ steps.meta.outputs.git_hash }}
186189
NODE_ENV=production
187190
191+
- name: Smoke daemon process boundary
192+
if: matrix.variant == 'daemon'
193+
env:
194+
DIGEST: ${{ steps.build.outputs.digest }}
195+
run: |
196+
docker pull "${IMAGE_NAME}@${DIGEST}"
197+
docker run --rm \
198+
--user 1000:1000 \
199+
--read-only \
200+
--network none \
201+
--cap-drop ALL \
202+
--security-opt no-new-privileges=true \
203+
--entrypoint bun \
204+
"${IMAGE_NAME}@${DIGEST}" \
205+
run dist/daemon/process-boundary-smoke.js
206+
188207
- name: Export digest
189208
env:
190209
DIGEST: ${{ steps.build.outputs.digest }}
@@ -256,6 +275,7 @@ jobs:
256275
type=raw,value=${{ steps.tag.outputs.variant_tag }}
257276
type=raw,value=${{ steps.tag.outputs.version }},enable=${{ matrix.variant == 'orchestrator' }}
258277
type=raw,value=latest,enable=${{ matrix.variant == 'orchestrator' && inputs.is-dev-release == false }}
278+
type=raw,value=latest-${{ matrix.variant }},enable=${{ inputs.is-dev-release == false }}
259279
260280
- name: Create manifest list and push
261281
working-directory: ${{ runner.temp }}/digests

.github/workflows/research.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ jobs:
195195
| idempotency | src/webhook/router.ts, src/core/tracking-comment.ts |
196196
| security | src/utils/, src/config.ts |
197197
| observability | src/logger.ts (and grep for logger usage across src/) |
198-
| testing | src/**/*.test.ts (sample 3-5; do not read all) |
198+
| testing | test/**/*.test.ts (sample 3-5; do not read all) |
199199
| docs | CLAUDE.md, top-level README.md if present, docs/ |
200200
| infrastructure | .github/workflows/, Dockerfile, package.json |
201201
| agent-sdk | src/core/prompt-builder.ts, src/core/executor.ts |

0 commit comments

Comments
 (0)