Files
agentic-dev/apps/runner/AGENTS.md
Danijel Martinek 750ab44379 feat(runner): clone stage with ephemeral credential helper
Clone stage per tech spec §6, verbatim mechanics: every git invocation
carries a BLANK credential.helper first (suppresses OS keychain
helpers) then the inline veect helper; the PAT reaches git via child
env only — never argv, URLs, logs, or .git/config. GIT_TERMINAL_PROMPT
and GIT_ASKPASS are pinned so a headless clone can never hang on a TTY
or ambient IDE askpass. Staged status events (start/heartbeat/final) +
ready on success; named failures: invalid-git-url, auth-failed,
clone-failed (daemon's 'repository not exported' maps to bad-URL, not
auth). Integration suite clones the daemon-served vite-kitchen and an
authenticated dumb-HTTP remote that asserts the exact Basic credential
git presented, plus leak assertions over logs/argv/.git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 22:50:41 +02:00

4.1 KiB

apps/runner — workspace runner (cloud-runner process)

The runner process of ADR-027: executes a workspace's repo code (clone, install, scan, preview adapter) on behalf of the control plane, speaking @repo/core-runner-protocol over plain WS/JSON. App-tier (tags: ["app"]): imperative code is expected here; there is no feature manifest, no use-case layer, no DI container.

Process contract (what the provisioner — story 06 — relies on)

  • Config is env-only (see src/config.ts): RUNNER_TOKEN (required, secret — never argv), RUNNER_WORKSPACE_DIR (required), RUNNER_PORT (default 0 = ephemeral), RUNNER_HOST (default 127.0.0.1), RUNNER_HEARTBEAT_MS (default 1000).
  • Port discovery: on boot the runner writes a JSON line to stdout: {"level":"info","event":"listening","host":...,"port":...}.
  • Logs are JSON lines on stdout. Log event names + safe fields only — never protocol payloads, never credentials. Integration tests grep the entire child output for the PAT and the workspace token.
  • SIGTERM/SIGINT shut down gracefully.

Protocol session semantics (documented in src/server.ts)

  • Every inbound/outbound frame is envelope-wrapped and zod-parsed (envelopeSchema); rejections emit named errors (unsupported-protocol-version / invalid-message / unauthorized). Pre-handshake rejections and token mismatches close the socket (1008).
  • First message must be hello; the runner answers ready.
  • Commands run one at a time in arrival order. A running stage streams status (stage + elapsedMs: start, heartbeats, final). Success ends with ready (the idle marker the story-07 orchestrator drives on); failure ends with a named error and the session stays open.
  • Zod issues are summarized as path + code only — payload values (which may include a PAT) are never echoed into errors or logs.

Clone stage — credential mechanics (tech spec §6, verbatim)

  • Every git invocation carries -c credential.helper= (BLANK first — resets the helper list, suppressing OS keychain helpers) followed by -c credential.helper=<veect-helper> (an inline shell function that answers get from env vars and ignores store/erase).
  • The PAT reaches git via the child's env (VEECT_GIT_PAT) — never argv (the helper string only names env vars), never in the URL, never written to .git/config or anywhere else on disk.
  • GIT_TERMINAL_PROMPT=0 and GIT_ASKPASS=echo ride every invocation: a clone must never hang on a TTY prompt or an ambient IDE askpass.
  • Failures map to named causes (src/stages/clone.ts): unusable URL / unreachable repo → invalid-git-url, credential rejection → auth-failed, anything else → clone-failed.

How the auth path is honestly tested

git daemon (story 01's transport) has no authentication, so it cannot prove credential delivery. The integration suite therefore also serves the same bare fixture over authenticated dumb HTTP (tests/http-git-server.ts): a real git clone probes, receives 401, consults the ephemeral helper, and retries with Basic auth — the test asserts the server received exactly x-access-token:<PAT>, that git's first probe was unauthenticated, and that the PAT appears nowhere in runner logs (which include every spawned git argv), .git/config, or any other file under .git/. Wrong/missing PAT → named auth-failed. What is NOT covered: a real smart-HTTP provider (GitHub et al.) — the dumb-HTTP fallback exercises the same credential machinery in git, but the smart-protocol surface itself first meets reality in later PRDs.

Testing

  • Unit suites live next to sources in src/; protocol/WS suites live in tests/ (protocol-server.test.ts runs the real server in-process so v8 coverage sees it; runner-process.integration.test.ts spawns the real child process and discovers the port from stdout).
  • tests/protocol-client.ts envelope-parses every inbound frame, so every test doubles as an outbound-conformance assertion.
  • Coverage: app-tier vitest thresholds inherited from vitest.base.node; only src/main.ts (bootstrap glue, exercised by the spawn suite in a child process) is excluded.