Skip to content

feat: structured, durable evidence for every check a pipeline gate runs - #422

Merged
saucam merged 3 commits into
mainfrom
feat/loops-evidence
Oct 25, 2026
Merged

saucam merged 3 commits into
mainfrom
feat/loops-evidence

Conversation

@saucam

@saucam saucam commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

What this adds

The first piece of loops (docs/loop-engineering-design.md §7, milestone A): every check a pipeline gate executes now leaves durable evidence: what ran, on which tree, its result, and its full output. Until now a gate's command output was thrown away and its verdict was just an exit code.

  • Results that don't lie. A check is pass, fail, inconclusive, error or cancelled. A test probe that exits 0 after running no tests is inconclusive, not a pass. A missing command is error. A check that runs past its time limit is stopped, along with everything it started, and recorded as a timed-out fail.
  • Diagnostics reach the next attempt. A failing gate's reason now carries the end of the failing output (stderr first), so the agent's revise turn sees why it failed.
  • Safe to show a model. Checks run with the same stripped environment as an agent's own commands, so the daemon's credentials are never in reach. Stored excerpts are redacted with log-aware rules that keep test names, file paths and exit codes readable. Artifacts are readable only by the daemon's user and are pruned after 30 days or once they pass 2 GiB.
  • Owned processes. Each check runs in its own process group. Aborting a pipeline or stopping the daemon stops its checks. A check stopped that way is dropped rather than recorded as a failure, so the gate re-runs when the pipeline resumes.
  • Readable. codeoid evidence <id> prints a record. codeoid pipeline show lists each phase's evidence. A halted phase in the web cockpit has "Show check output".

Behaviour change: an auto-derived test/verify probe on a repo where the test command runs zero tests now fails as inconclusive. Pack command gates are still judged on exit status alone.

Review

Two independent audit rounds, each covering security and correctness.

  • Round 1 fixed: environment leakage into output shown to models, unredacted output, world-readable artifacts, no retention, zero-test detection that only read the excerpt, and checks orphaned on shutdown or abort.
  • Round 2 fixed: cancelled checks being recorded as gate failures, redaction across chunk boundaries and redaction that destroyed test diagnostics, a crash on artifact write errors, and fragile orphan matching. It also kept common toolchain variables for checks.

Full suite: 3822 pass / 0 fail. Web typecheck and tests pass.

For agents: implementation
  • src/daemon/evidence/:
    • types.ts: EvidenceRecord, CheckSpec.
    • runner.ts: runCheck. The check runs with detached (its own group) and checkEnv() = buildSubprocessEnv plus a toolchain allowlist and CODEOID_CHECK_ENV_ALLOW. Output goes through a line-scanned ZeroTestScan, head/tail excerpts are UTF-8 safe and cut at line boundaries, and artifact files are 0600 with an error listener.
    • parsers.ts, redact.ts: log-specific rules.
    • store.ts: the evidence table, with artifact_dir relative to the root.
    • service.ts: EvidenceService with per-owner abort flags, abortAll, sweep (age, size, orphans; skips live dirs), and ephemeralCheck via mkdtemp.
    • wire.ts: evidenceToWire. Paths are not exposed.
  • Pipeline:
    • gate-probes.ts and the pack.ts command gate run through ctx.runCheck, return evidenceIds plus diagnosticTail, and return cancelled when the check was stopped.
    • engine.ts: GateCancelled drops the step in run(). Evidence ids go on the active phase, capped at 50.
    • interface.ts: GateCtx.runCheck, GateVerdict.evidenceIds/cancelled, PipelinePhase.evidence.
  • session-manager.ts:
    • Builds EvidenceService. The sweep runs 60 s after boot and then daily, and its timers are cleared in drain.
    • abortAll runs in drain; abortOwner runs on pipeline.abort.
    • #evidenceGet checks pipeline:read and the pipeline before looking anything up, and returns not_found for anything the caller can't see.
    • The pipeline wire includes evidence for every status.
  • Protocol: packages/protocol/src/evidence.ts (evidence.get / evidence.record), plus its schema.
  • CLI: codeoid evidence, src/terminal/evidence-format.ts, and a pipeline-format line.
  • Web: EvidenceView/EvidenceLink and state/evidence.ts (cached per connection, bounded).

🤖 Generated with Claude Code

saucam and others added 3 commits October 25, 2026 08:44
The first slice of loops (docs/loop-engineering-design.md §7, milestone A).
A check's verdict is what a command did, captured by the daemon: result
(pass / fail / inconclusive / error / cancelled), exit status or signal,
bounded excerpts, the full output as artifact files, the verifier's digest,
and the environment — persisted in the daemon DB.

- evidence/runner.ts runs each check as its own process group, so a
  timeout, cancel, or the check's own exit kills everything it spawned; a
  test run that selects no tests is inconclusive, never a pass.
- Pipeline command gates and execution probes record evidence, carry the
  failing output into the gate reason, and list their evidence on the phase.
- evidence.get reads one record (pipeline:read, tenant- and owner-scoped;
  an id the caller cannot see is not_found); codeoid evidence <id> prints it;
  the halted phase in the web cockpit shows the latest check's output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…te files, retention, cancellation

- Checks run with the agent subprocess environment (no CODEOID_/ZEROID_/
  TELEGRAM_), since their output now flows back to the worker.
- Stored excerpts and gate diagnostics are redacted; artifacts are 0600 in
  0700 directories; unrecorded checks use a private mkdtemp directory.
- Zero-test detection scans every output line (go/cargo counts across the
  whole run), not the excerpt; excerpt cuts keep whole UTF-8 characters.
- EvidenceService tracks live checks: pipeline.abort and daemon drain stop
  them. A retention sweep (30 days / 2 GiB, orphan dirs) runs off the boot
  path and daily; the per-stream artifact cap is 8 MiB.
- evidence.get authorizes before any lookup; pipeline wire carries a phase's
  evidence in every status; stderr gets priority in gate diagnostics; the
  web view refetches when a newer check lands; the delayed SIGKILL is
  cleared once the group exits; a missing workdir is named as such.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, log-safe redaction, resilient artifacts

- A gate check stopped by an abort or shutdown no longer fails the gate:
  the engine drops the step unsaved, so the gate re-runs on resume instead
  of halting, burning a retry, or failing the pipeline.
- An owner's abort also cancels checks it starts afterwards; shutdown
  refuses new checks.
- Redaction uses log-specific rules over the whole excerpt (PEM blocks,
  JWTs, URL userinfo, auth headers, known token formats, secret-named
  keys) and keeps test names, paths, hashes and exit codes readable;
  excerpt edges are cut at line boundaries.
- An artifact write error (disk full, EMFILE) no longer crashes the
  daemon; the artifact is marked truncated.
- Artifact directories are stored relative to the evidence root and the
  sweep never removes a running check's directory.
- Checks keep common toolchain variables (LC_*, XDG_*, CI, GOPATH, …) plus
  CODEOID_CHECK_ENV_ALLOW, never the daemon's credentials.
- The web evidence cache is per connection and bounded; a git-diff probe
  that cannot run says so.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@saucam
saucam merged commit 3b19ddd into main Oct 25, 2026
4 checks passed
@saucam
saucam deleted the feat/loops-evidence branch October 25, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants