Files
agentic-dev/apps/runner/AGENTS.md
Danijel Martinek ec7bf948af feat(runner): install stage with progress events
Install stage: package-manager detection (lockfile beats the
packageManager field, npm default — vite-kitchen's shape), install run
inside the clone with staged status heartbeats, and every failure —
including install-before-clone — mapped to the named install-failed
cause with a bounded output tail. The spawned-runner suite now runs the
full clone → install pipeline on the daemon-served vite-kitchen (real
npm registry install) and greps the child's entire stdout+stderr plus
the clone's .git/config for the PAT and workspace token. Shared test
doubles extracted (exec.mock.ts, tests/runner-session.ts) to keep the
suites duplication-free.

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

5.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.

Install stage

  • Package-manager detection (src/stages/install.ts): lockfile first (pnpm-lock.yaml / yarn.lock / bun.lockb / bun.lock / package-lock.json / npm-shrinkwrap.json — the lockfile pins install semantics, so it beats a contradicting packageManager field), then the packageManager field, then npm as the default (vite-kitchen's shape: no lockfile, no field).
  • The detected manager runs install in the clone (npm adds --no-audit --no-fund), streaming staged status heartbeats; every failure — including "install before clone" — is the named install-failed cause with a bounded output tail as the message.
  • The in-process integration suite covers success on a minimal no-dependency fixture (real npm, instant, offline) and the named failure via a fixture depending on a package that cannot exist; the spawned-runner suite runs the full clone → install pipeline on vite-kitchen (real registry install) with the leak grep over the child's entire output.

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.