Skip to content

agent-runner

The in-pod coding-agent runtime: one stage per Job — investigate or remediate — via the harness CLI its runner image bundles (claude -p in the claude-agent-runner image, codex exec in the codex-agent-runner image, copilot -p in the copilot-agent-runner image). It never talks to GitHub or the Kubernetes API, and it has no flags — configuration is exclusively PATCHY_* environment variables, injected into the Job pod by the job controllers. A claude pod holds no credential of any kind: its model traffic goes through the egress broker, authenticated by a projected ServiceAccount token; a codex or copilot pod holds the one model key of its harness. Results leave the pod as a PATCHY-EVENT: JSONL stream on stdout (which is why all patchy logging goes to stderr).

You normally never configure the agent-runner directly: the investigation-controller and remediation-controller stage flags become this environment. The contract below matters when debugging a Job spec or running the runtime standalone.

Identity and phase

Env Default Purpose
PATCHY_REPO — (required) owner/name of the repository under analysis
PATCHY_FINDING — (required) Name of the owning Finding resource — echoed in every event, and the branch is patchy/<finding>
PATCHY_BASE_SHA The remote commit the workspace tree corresponds to (the changeset's push base)
PATCHY_PHASE investigate investigate or remediate
PATCHY_WORKSPACE /workspace Pod workspace root (repo/, input/, reports/)

Stage configuration

Mirrors of the controllers' stage flags: PATCHY_INVESTIGATE_TIMEOUT (15m), PATCHY_INVESTIGATE_MAX_TURNS (25), PATCHY_INVESTIGATE_TOKEN_BUDGET (150000), PATCHY_REMEDIATE_TIMEOUT (45m), PATCHY_REMEDIATE_AUTO_MAX_TURNS (80), PATCHY_REMEDIATE_AUTO_TOKEN_BUDGET (400000), PATCHY_REMEDIATE_MANUAL_MAX_TURNS (240), PATCHY_REMEDIATE_MANUAL_TOKEN_BUDGET (1200000), and PATCHY_MODEL_ALLOWLIST (canonical model ids, rendered into the analysis prompt). The per-Job PATCHY_<STAGE>_HARNESS and PATCHY_<STAGE>_MODEL (a canonical, provider-qualified id) are set by the controller from the harness and model it resolved for this Job — so the pod runs the harness its runner image was built for on the model the controller chose, and translates that canonical id to the CLI's own model id. The investigate limits are absolute. The remediate values are a floor and a backstop, not a clamp: every remediation runs on at least the ceiling whatever the investigation estimated, the per-Job PATCHY_GRANTED_MAX_TURNS / PATCHY_GRANTED_TOKEN_BUDGET raise that when a human approved a larger estimate, and the _HARD values bound the result. A hard cap below its ceiling is a configuration error and the runner refuses to start.

PATCHY_CALIBRATION is the last per-Job variable: a JSON summary of how earlier estimates in this repository compared to reality, rendered into the analysis prompt so the next estimate can correct for the observed skew. It is advisory — absent on a cold start, and the prompt then omits the section entirely.

Brokered (claude) Jobs add two more:

Env Purpose
PATCHY_BROKER_TOKEN_FILE Path of the projected ServiceAccount token (/var/run/patchy/broker/token); read fresh each stage — the kubelet rotates it — and sent to the broker as the X-Patchy-Broker-Token header
PATCHY_MODEL_MAP Comma-joined canonical=provider-id pairs; consulted before the registry when translating the stage model to the CLI's --model id (Bedrock inference profiles, Foundry deployment names)

The controllers also set the claude CLI's gateway environment on brokered Jobs — ANTHROPIC_BASE_URL or the CLAUDE_CODE_USE_* / CLAUDE_CODE_SKIP_*_AUTH / ANTHROPIC_*_BASE_URL switches, plus CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 — pointing every model request at the broker's provider route. All of these names are reserved in internal/jobs, so controller-global configuration can never shadow them.

Two knobs exist only here:

Env Default Purpose
PATCHY_CHANGESET_MAX_BYTES 5242880 (5 MiB) Size cap on the changeset's file contents carried out of the pod
PATCHY_TRANSCRIPT_MAX_TURN_BYTES 2048 Per-turn text cap in the captured conversation
PATCHY_TRANSCRIPT_MAX_TURNS 500 Turn cap for one run's conversation
PATCHY_TRANSCRIPT_MAX_TOTAL_BYTES 524288 (512 KiB) Total cap on one run's conversation, before compression
PATCHY_FAKE_FIXTURE Stream-JSON fixture the fake harness replays (tests, dev overlay)

Malformed values fail fast with an error naming the exact PATCHY_<KEY>.

The workspace, and how it got there

The pod's init container — not the runtime — fetches the repository: PATCHY_ARTIFACT_URL points at source-controller's in-cluster artifact server (an unguessable URL), PATCHY_ARTIFACT_DIGEST pins the sha256, and the init script verifies the digest before extracting to /workspace/repo and synthesizing a local git base commit. No forge credential is involved at any point — internal/jobs even lists GITHUB_TOKEN as a reserved name so no configuration can smuggle one in. The per-Job Secret carries only the handoff markdown (input/issue.md, plus input/investigation.md for the remediate phase).

Credentials in the pod

Harness In the pod
claude None. The broker caller token (an identity document, not a capability) is the pod's only secret material; the model credential lives with the egress broker
codex OPENAI_API_KEY via secretKeyRef (--codex-secret; CODEX_API_KEY / CODEX_ACCESS_TOKEN when --codex-secret-env names one)
copilot COPILOT_GITHUB_TOKEN via secretKeyRef (--copilot-secret) — a GitHub token, not a model API key; GH_TOKEN / GITHUB_TOKEN when --copilot-secret-env names one
fake None — the fixture replay authenticates nothing

At most one credential reaches a given pod: the Job wires the secretKeyRef of the harness it runs, so a codex Job carries only the OpenAI credential. The agent container's environment passes through to the harness CLI child process, so an injected key — or the brokered gateway environment — is inherited by claude (or codex, or copilot) automatically. The broker caller token is registered with the transcript scrubber the same way credential values are, so a tool result that dumps the environment cannot leak it into a persisted transcript.

The copilot rows are the exception to "no forge credential ever reaches the pod": the Copilot CLI authenticates with a GitHub token, so a copilot Job does carry one. It is a model credential by role, not a forge one — the runner passes --disable-builtin-mcps, so no tool in the session speaks the GitHub API, and the harness's egress policy admits only api.github.com (the token exchange) and the *.githubcopilot.com inference endpoints. patchy's own forge traffic is still controller-side only. Scope the token to Copilot with no repository permissions, and note the copilot runner ships disabled for exactly this reason.

The event stream

Progress and results are emitted as one JSON object per line, prefixed PATCHY-EVENT:, on stdout; the owning controller tails the pod log and applies them. Stage outcomes are ok, runtime_error, timeout, budget_exceeded, report_missing, report_invalid, commit_failed, and changeset_too_large — only ok carries a trusted report. A fatal error also exits 2 so the Job is marked failed for the controller's orphan handling.