Skip to content

Assay-Runner CI Lane Contract

Internal Phase 2A reference. This page classifies when ordinary continuous CI is enough and when the delegated Linux/eBPF runner lane is required.

Assay-Runner has two proof classes:

  • Continuous CI: hosted or ordinary repository checks that must stay fast, broadly available, and safe for pull requests.
  • Delegated CI: the manual Runner Spike Delegated workflow on [self-hosted, linux, assay-bpf-runner], used when a change can only be proven on the dedicated Linux/eBPF host.

The delegated lane is intentionally workflow_dispatch only. Do not add a pull-request, push, or schedule trigger to make the lane automatic. The host is dedicated and destructive cleanup is part of the contract.

Decision Table

Change type Continuous CI Delegated CI Required before merge?
Docs-only change outside runner acceptance, runbook, artifact contracts, or CI lane docs yes no no
Docs-only change to runner acceptance, runbook, artifact contracts, fixture contract, or CI lane docs yes no no, unless it changes acceptance criteria
Non-runner crate change with no monitor, policy, SDK fixture, cgroup, workflow, or evidence artifact impact yes no no
Runner artifact schema, field set, note format, or artifact determinism semantics yes yes yes
Runner telemetry-versus-evidence filter behavior yes yes yes
assay-monitor ring-buffer reader, BPF event decode, drop accounting, or capture stats yes yes yes
crates/assay-monitor/** or crates/assay-ebpf/** yes yes yes
eBPF build image, build command, or loader/attach path yes yes yes
Runner cgroup placement, domain-root resolution, or process spawn discipline yes yes yes
Policy correlation, policy event normalization, or policy-to-kernel coherence rule yes yes yes
OpenAI Agents fixture dependency, SDK event normalization, SDK version assertion, or tool_call_id binding yes yes yes
Gemini Python google-genai fixture dependency, SDK event normalization, SDK version assertion, cassette content, or tool_call_id binding yes yes yes
Acceptance fixture behavior or control paths yes yes case-by-case; required when observed evidence can change
Delegated workflow, runner labels, cleanup, checkout, sudo environment, preflight, or scripts/ci/runner-spike-*.sh yes yes yes
@openai/agents, zod, or fixture package-lock.json bump yes yes yes
aya, aya-ebpf, aya-log-ebpf, or BPF/runtime dependency bump yes yes yes
Workspace dependency bump that can affect assay-runner-spike, assay-monitor, assay-ebpf, assay-cli, policy correlation, or runner fixtures yes yes yes
Delegated runbook wording that only clarifies existing behavior yes no no
Follow-up issue text, planning notes, or extraction-roadmap prose yes no no

Required Delegated Gate

When delegated CI is required, choose the narrowest gate that proves the changed layer:

Touched surface Minimum delegated gate
Kernel fixture control path or kernel-only acceptance assertion kernel-only
Policy capture or policy-to-kernel coherence kernel-policy
OpenAI Agents SDK fixture, SDK event schema, SDK version assertion, or tool_call_id binding openai-agents-kernel-policy
Gemini Python google-genai fixture, SDK event schema, SDK version assertion, cassette content, or tool_call_id binding all (no narrower Gemini-specific gate exists in v0; a narrower gate is a separate coordinated decision per #1307)
Delegated hidden-write semantic-gap expansion, proof-pack collector changes, crates/assay-monitor/**, crates/assay-ebpf/**, crates/assay-xtask/**, eBPF build/attach path, cgroup placement, telemetry filter, cross-layer archive, artifact schema, correlation report, workflow/security model, runner scripts, BPF/runtime dependency bump, or final release/acceptance proof all

If a change touches multiple surfaces, run the highest required gate. If the right gate is ambiguous, default to all.

Use build_ebpf=true for delegated proof unless the workflow has been explicitly extended to restore a deterministic prebuilt eBPF object after cleanup and checkout.

Merge Discipline

  1. Run ordinary continuous CI for every PR.
  2. For runner-impacting changes, dispatch the minimum delegated gate after the PR branch contains the final candidate commit.
  3. Prefer the attested delegated proof pack emitted by the delegated workflow: assay-runner-delegated-proof-pack-<run_id> contains manifest.json, subject-checksums.txt, and attestation-bundle.json. Lane-check verifies the GitHub artifact attestation, the proof-pack subjects, the selected gate, and the gated-path tree OIDs. During the transition, recording the workflow run URL, commit SHA, selected gate, and result in the PR body or a PR comment remains a compatibility fallback.
  4. Do not treat a delegated skip as success. In the delegated lane, exit 40 means the runner contract has drifted.
  5. Do not bypass required repository checks for runner-impacting changes.

If a runner-impacting PR cannot get a delegated run because the host is unavailable, leave the PR open. Merge only docs-only or clearly non-runner-impacting changes without delegated proof.

The Assay-Runner Lane Check / lane-check workflow provides the machine-visible path classification and delegated proof check for this contract. To make the contract hard-blocking, repository branch protection must mark that check as required.

When lane-check resolves a PR from pull_request, workflow_dispatch, or a completed delegated workflow_run, the workflow also writes a PR-head commit status named lane-check/proof. That status is intentionally distinct from the Actions job named lane-check so checks-API and statuses-API results do not collide under the same display name. If maintainers want the refresh path itself to satisfy branch protection without manually rerunning the pull-request job, require the lane-check/proof status.

Attested proof is content-addressed over the configured gated paths. If a delegated proof was produced on an earlier commit but the current PR head has identical tree OIDs for every content-provenance path in scripts/ci/assay_runner_gated_paths.json, lane-check may accept the proof without another delegated run. This does not claim anything about unrelated repository state; it only preserves the proof for the gated content the delegated host actually exercised.

The workflow keeps pull-requests: write while the transition comment channel exists. Empirically, issues: write plus pull-requests: read is not enough to post or update PR comments on this repository.

The executable path mapping lives in scripts/ci/assay_runner_lane_check.py and must mirror the decision table on this page. Changes to this contract and changes to the classifier must land in the same PR; the helper's --self-test is the drift canary for the known runner-impacting surfaces.

Dependabot PRs follow the same proof rules, but need maintainer action because Dependabot cannot dispatch the delegated lane or record proof itself. See the Runner Dependabot lane flow for the required maintainer steps. Do not auto-dispatch the self-hosted delegated workflow from Dependabot PRs without a separate security design review.

Ring-buffer drop diagnostics remain a separate follow-up tracked in https://github.com/Rul1an/assay/issues/1271; that issue must not weaken the ringbuf_drops=0 delegated acceptance rule.

Required Lane Context (issue #1869, migration complete)

Branch protection on main requires the commit status lane-check/proof, pinned to the GitHub Actions app. The status is posted by the lane helper on the exact PR head, so a delegated same-head proof refreshes the required context without a manual rerun. The lane-check Actions job still runs and reports on every PR but is informational: a workflow_run execution cannot refresh a check-run on the PR head, which is why the check-run was removed from the required set (2026-07-27; empirical evidence on the tracking issue, including the app-pin match and automatic stale-proof rejection within ~90 s of a head move).

Operational consequence of strict: true: the proof binds to one SHA, so every branch update after a main advance produces a new head that needs a fresh delegated proof before the lane context reads clean again — measured at one delegated cycle per advance (≈4m31s warm, ~13 min cold, the dispatch being the only manual action). Batch main merges around open gated PRs accordingly.

Boundary Rule

The delegated lane proves the Linux/eBPF runner boundary, not macOS, Windows, live LLM calls, production load, or a distributed runner fleet. Platform expansion requires a separate platform spike with its own kill criteria and CI lane contract.