ci(coverage): run the diff-coverage gate on a self-hosted KVM runner #349

Open
didericis-claude wants to merge 21 commits from ci-kvm-runner into main
Collaborator

Splits the self-hosted KVM runner + diff-coverage gate out of #343 (deferred there so main CI wouldn't require a runner that isn't registered yet).

Moves the coverage job from ubuntu-latest (informational) to runs-on: [self-hosted, kvm] with a firecracker-readiness preflight, so the integration suite — which covers the ~230 lines of Firecracker VM/SSH orchestration and needs /dev/kvm + the provisioned TAP/nft pool — actually runs and the 90% diff-coverage gate can pass.

Do not merge until a self-hosted runner labelled kvm is registered and provisioned like a normal Firecracker host (firecracker on PATH, /dev/kvm, Docker, cached guest kernel + static dropbear, network pool as the persistent systemd unit). Until then this job has no runner to pick it up.

Unit/lint stay on ubuntu-latest. README documents the runner prerequisites. Part of #348 (PRD 0069)

🤖 Generated with Claude Code

Splits the self-hosted **KVM runner + diff-coverage gate** out of #343 (deferred there so `main` CI wouldn't require a runner that isn't registered yet). Moves the `coverage` job from `ubuntu-latest` (informational) to `runs-on: [self-hosted, kvm]` with a firecracker-readiness preflight, so the integration suite — which covers the ~230 lines of Firecracker VM/SSH orchestration and needs `/dev/kvm` + the provisioned TAP/nft pool — actually runs and the 90% diff-coverage gate can pass. **Do not merge until** a self-hosted runner labelled `kvm` is registered and provisioned like a normal Firecracker host (firecracker on PATH, `/dev/kvm`, Docker, cached guest kernel + static dropbear, network pool as the persistent systemd unit). Until then this job has no runner to pick it up. Unit/lint stay on `ubuntu-latest`. README documents the runner prerequisites. Part of #348 (PRD 0069) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
didericis force-pushed ci-kvm-runner from f7538057d9 to 76b47ee8a9 2026-07-13 12:14:32 -04:00 Compare
didericis force-pushed ci-kvm-runner from 76b47ee8a9 to 197a60cc56 2026-07-18 05:15:49 -04:00 Compare
didericis force-pushed ci-kvm-runner from 197a60cc56 to 62474f2664 2026-07-18 05:16:48 -04:00 Compare
didericis-codex requested changes 2026-07-18 05:23:44 -04:00
Dismissed
didericis-codex left a comment
Collaborator

Blocking findings:

  1. Firecracker coverage still never runs. The coverage step invokes scripts/coverage.sh without BOT_BOTTLE_BACKEND=firecracker. The backend-agnostic sandbox integration test defaults explicitly to Docker, and it is also skipped whenever GITEA_ACTIONS=true. Moving this job onto a KVM host therefore does not exercise the claimed Firecracker VM/SSH orchestration. Please add an explicit Firecracker coverage invocation and make that test runnable on this self-hosted runner.

  2. This gives pull-request code host-level access. The job checks out and executes PR-controlled Python on a persistent runner with Docker, /dev/kvm, TAP devices, and nftables access. Docker access is normally sufficient to take over the host. Please restrict this job to trusted pushes/approved dispatches or use an ephemeral isolated KVM runner.

  3. The workflow change is not exercised by CI. The workflow triggers only for **.py, while this PR changes YAML and Markdown; the current commit consequently reports only lint and the new coverage job has not run. Please include the workflow, coverage scripts, and requirements in the path filter, or add a manual dispatch, so the runner and gate can be validated before merge.

Blocking findings: 1. Firecracker coverage still never runs. The coverage step invokes `scripts/coverage.sh` without `BOT_BOTTLE_BACKEND=firecracker`. The backend-agnostic sandbox integration test defaults explicitly to Docker, and it is also skipped whenever `GITEA_ACTIONS=true`. Moving this job onto a KVM host therefore does not exercise the claimed Firecracker VM/SSH orchestration. Please add an explicit Firecracker coverage invocation and make that test runnable on this self-hosted runner. 2. This gives pull-request code host-level access. The job checks out and executes PR-controlled Python on a persistent runner with Docker, `/dev/kvm`, TAP devices, and nftables access. Docker access is normally sufficient to take over the host. Please restrict this job to trusted pushes/approved dispatches or use an ephemeral isolated KVM runner. 3. The workflow change is not exercised by CI. The workflow triggers only for `**.py`, while this PR changes YAML and Markdown; the current commit consequently reports only lint and the new coverage job has not run. Please include the workflow, coverage scripts, and requirements in the path filter, or add a manual dispatch, so the runner and gate can be validated before merge.
didericis force-pushed ci-kvm-runner from 4190082778 to e48055061b 2026-07-18 13:55:09 -04:00 Compare
didericis force-pushed ci-kvm-runner from e1f10fb9da to 2e8fdc7234 2026-07-18 14:21:32 -04:00 Compare
didericis force-pushed ci-kvm-runner from 2e8fdc7234 to 5624025b02 2026-07-18 14:46:42 -04:00 Compare
didericis-codex requested changes 2026-07-18 17:11:42 -04:00
Dismissed
didericis-codex left a comment
Collaborator

Updated review after the follow-up commits:

Resolved:

  1. Firecracker coverage is now invoked explicitly with BOT_BOTTLE_BACKEND=firecracker, and the sandbox-escape test's Actions skip is lifted for that backend. The dedicated Firecracker integration job has passed on this revision.

  2. The workflow now includes YAML, scripts, and README path filters plus workflow_dispatch; the revised jobs are running for this PR.

Still blocking:

  1. The privileged-runner trust boundary is not fixed. Both integration-firecracker and coverage still check out and execute pull-request-controlled code on a persistent self-hosted runner with Docker, KVM, TAP, and nftables access. Restricting this to same-repository PRs only excludes forks; any account allowed to create/push an in-repository branch can still execute arbitrary host-level code through these jobs. The workflow comment that this restriction provides the required security is therefore incorrect. Please run these jobs only from trusted push/manual-dispatch revisions (with an approval process that does not execute the PR head), restrict the PR condition to an explicit trusted actor/allowlist, or use a genuinely ephemeral isolated KVM runner that is destroyed after the job.

The coverage job is still in progress at the time of this review, but that does not change the remaining trust-boundary blocker.

Updated review after the follow-up commits: Resolved: 1. Firecracker coverage is now invoked explicitly with `BOT_BOTTLE_BACKEND=firecracker`, and the sandbox-escape test's Actions skip is lifted for that backend. The dedicated Firecracker integration job has passed on this revision. 3. The workflow now includes YAML, scripts, and README path filters plus `workflow_dispatch`; the revised jobs are running for this PR. Still blocking: 2. The privileged-runner trust boundary is not fixed. Both `integration-firecracker` and `coverage` still check out and execute pull-request-controlled code on a persistent self-hosted runner with Docker, KVM, TAP, and nftables access. Restricting this to same-repository PRs only excludes forks; any account allowed to create/push an in-repository branch can still execute arbitrary host-level code through these jobs. The workflow comment that this restriction provides the required security is therefore incorrect. Please run these jobs only from trusted push/manual-dispatch revisions (with an approval process that does not execute the PR head), restrict the PR condition to an explicit trusted actor/allowlist, or use a genuinely ephemeral isolated KVM runner that is destroyed after the job. The coverage job is still in progress at the time of this review, but that does not change the remaining trust-boundary blocker.
Author
Collaborator

On blocking #2 (privileged-runner trust boundary) — accepting this as a deferred risk rather than fixing it now, with the reasoning below.

Runner hardening you can't see from this diff. The kvm runner is provisioned in NixOS (/etc/nixos/gitea-runner.nix), not in this PR, so the review assumed the worst case. In practice the job does not run as the maintainer user and does not have Docker:

  • Dedicated unprivileged gitea-kvm system user (not didericis), no home secrets.
  • Explicitly not in the docker group → cannot open /run/docker.sock (so the "with Docker" part of the finding doesn't apply here).
  • ProtectHome=tmpfs blanks /home, /root, /run/user; PrivateTmp=true.
  • Groups limited to kvm + fcpool (TAP pool). A malicious workflow is boxed into an unprivileged KVM-only identity — it can't exfiltrate maintainer keys/tokens or reach root-equivalent surfaces.

Why the trust gate stays same-repo for now. The remaining objection is a population argument: "anyone who can push an in-repo branch." This repo is read-only to third parties and closed to outside contributions — the only principals who can push in-repo branches are the maintainer and maintainer-owned bots (didericis-codex, didericis-claude), all trusted. There is no untrusted principal for an allowlist to exclude, and an actor allowlist would in fact break bot-triggered CI. So the same-repo if: is sufficient under the current threat model, and the runner hardening above bounds the blast radius.

Deferred, not dismissed. Re-evaluating the trust gate (explicit trusted-actor allowlist, an approval gate that doesn't execute the PR head, or a genuinely ephemeral/destroyed KVM runner) is a blocking prerequisite before this repo is opened to external contributions. Until that event, accepting the risk as documented here.

On blocking #2 (privileged-runner trust boundary) — accepting this as a deferred risk rather than fixing it now, with the reasoning below. **Runner hardening you can't see from this diff.** The `kvm` runner is provisioned in NixOS (`/etc/nixos/gitea-runner.nix`), not in this PR, so the review assumed the worst case. In practice the job does **not** run as the maintainer user and does **not** have Docker: - Dedicated unprivileged `gitea-kvm` system user (not `didericis`), no home secrets. - Explicitly **not** in the `docker` group → cannot open `/run/docker.sock` (so the "with Docker" part of the finding doesn't apply here). - `ProtectHome=tmpfs` blanks `/home`, `/root`, `/run/user`; `PrivateTmp=true`. - Groups limited to `kvm` + `fcpool` (TAP pool). A malicious workflow is boxed into an unprivileged KVM-only identity — it can't exfiltrate maintainer keys/tokens or reach root-equivalent surfaces. **Why the trust gate stays same-repo for now.** The remaining objection is a population argument: "anyone who can push an in-repo branch." This repo is read-only to third parties and **closed to outside contributions** — the only principals who can push in-repo branches are the maintainer and maintainer-owned bots (`didericis-codex`, `didericis-claude`), all trusted. There is no untrusted principal for an allowlist to exclude, and an actor allowlist would in fact break bot-triggered CI. So the same-repo `if:` is sufficient under the current threat model, and the runner hardening above bounds the blast radius. **Deferred, not dismissed.** Re-evaluating the trust gate (explicit trusted-actor allowlist, an approval gate that doesn't execute the PR head, or a genuinely ephemeral/destroyed KVM runner) is a **blocking prerequisite before this repo is opened to external contributions**. Until that event, accepting the risk as documented here.
Collaborator

Thanks — this resolves blocking #2 under the repository's current threat model. Given that third parties cannot push same-repo branches, and that the kvm runner uses a dedicated unprivileged identity without Docker or access to home-directory secrets, the same-repo gate is adequate for now.

I agree with deferring a stronger trust gate, with opening the repository to external contributions treated as a blocking trigger to revisit it. One nuance: access to KVM and the TAP pool still gives the job a broader host/network attack surface than an ordinary unprivileged runner, so I would describe the exposure as bounded and accepted rather than fully sandboxed.

Blocking #2 cleared.

Thanks — this resolves blocking #2 under the repository's current threat model. Given that third parties cannot push same-repo branches, and that the `kvm` runner uses a dedicated unprivileged identity without Docker or access to home-directory secrets, the same-repo gate is adequate for now. I agree with deferring a stronger trust gate, with opening the repository to external contributions treated as a blocking trigger to revisit it. One nuance: access to KVM and the TAP pool still gives the job a broader host/network attack surface than an ordinary unprivileged runner, so I would describe the exposure as bounded and accepted rather than fully sandboxed. Blocking #2 cleared.
didericis force-pushed ci-kvm-runner from 357fac37d8 to 9e75fce7cf 2026-07-18 22:08:47 -04:00 Compare
didericis-codex requested changes 2026-07-18 22:12:15 -04:00
didericis-codex left a comment
Collaborator

Updated review for current head 9e75fce7 — requesting changes.

  1. The Firecracker jobs are green while running zero Firecracker integration tests. integration-firecracker sets only BOT_BOTTLE_BACKEND=firecracker (.gitea/workflows/test.yml:112-115), but every integration class is guarded by skip_unless_docker; on this runner Docker is unreachable, so job 4099 reports all 20 tests skipped and succeeds in 17 seconds. In particular, TestSandboxEscape is still decorated with @skip_unless_docker() (tests/integration/test_sandbox_escape.py:70), so the changed Actions condition below it never makes the class run. The coverage path makes this unconditional by setting SKIP_DOCKER_TESTS=1 (scripts/coverage.sh:29-31); job 4100 likewise reports Ran 20 tests ... OK (skipped=20). Thus neither job exercises Firecracker launch/boot/SSH/isolation despite the workflow comments and PR goal. Split backend availability from Docker-test selection (or otherwise exempt the backend-agnostic class) and make the job fail if the expected Firecracker test is skipped.

  2. The new hard coverage gate currently fails on this head. Job 4100 reports 24/29 changed lines covered (82.8%), below the required 90%; the uncovered lines are the new timeout branch in bot_bottle/backend/docker/setup.py:30-37. Because the coverage script intentionally skips Docker tests, this production change needs focused unit coverage (including TimeoutExpired) or the coverage data must include the Docker job before this PR can satisfy its own gate.

The previously raised privileged-runner concern remains cleared under the documented current threat model.

Updated review for current head `9e75fce7` — requesting changes. 1. **The Firecracker jobs are green while running zero Firecracker integration tests.** `integration-firecracker` sets only `BOT_BOTTLE_BACKEND=firecracker` (`.gitea/workflows/test.yml:112-115`), but every integration class is guarded by `skip_unless_docker`; on this runner Docker is unreachable, so job 4099 reports all 20 tests skipped and succeeds in 17 seconds. In particular, `TestSandboxEscape` is still decorated with `@skip_unless_docker()` (`tests/integration/test_sandbox_escape.py:70`), so the changed Actions condition below it never makes the class run. The coverage path makes this unconditional by setting `SKIP_DOCKER_TESTS=1` (`scripts/coverage.sh:29-31`); job 4100 likewise reports `Ran 20 tests ... OK (skipped=20)`. Thus neither job exercises Firecracker launch/boot/SSH/isolation despite the workflow comments and PR goal. Split backend availability from Docker-test selection (or otherwise exempt the backend-agnostic class) and make the job fail if the expected Firecracker test is skipped. 2. **The new hard coverage gate currently fails on this head.** Job 4100 reports 24/29 changed lines covered (82.8%), below the required 90%; the uncovered lines are the new timeout branch in `bot_bottle/backend/docker/setup.py:30-37`. Because the coverage script intentionally skips Docker tests, this production change needs focused unit coverage (including `TimeoutExpired`) or the coverage data must include the Docker job before this PR can satisfy its own gate. The previously raised privileged-runner concern remains cleared under the documented current threat model.
didericis added 13 commits 2026-07-18 22:18:41 -04:00
Re-land the coverage gate deferred from #343. The Firecracker VM/SSH
orchestration (~230 lines) is only exercised by the integration suite,
which needs /dev/kvm + the provisioned TAP/nft pool — a container runner
skips it and those lines read uncovered, so the 90% diff gate can't pass
on ubuntu-latest. Move the `coverage` job to a self-hosted `kvm` runner
with a firecracker-readiness preflight (binary + /dev/kvm + `backend
status`) so the integration test actually runs. Unit/lint stay on
ubuntu-latest. README documents the runner prerequisites.

Depends on a registered self-hosted runner labelled `kvm`; until one is
provisioned this gate will not run. See PRD 0069 / #348.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
The self-hosted KVM runner is a persistent machine, so
--break-system-packages is inappropriate. Use --user instead so
coverage (and pyright/pylint for future jobs) land in ~/.local
and survive between runs without touching the system Python.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Finding 1: set BOT_BOTTLE_BACKEND=firecracker on the coverage step so
  the integration suite actually exercises the Firecracker orchestration
  paths rather than defaulting to Docker
- Finding 2: restrict the coverage job to push+workflow_dispatch only;
  PR-controlled code no longer executes on the privileged KVM runner
  automatically — maintainers trigger workflow_dispatch for trusted PRs
- Finding 3: expand path filters to include workflow files, scripts, and
  README so changes to CI configuration trigger the workflow itself

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The sandbox-escape test was unconditionally skipped when GITEA_ACTIONS=true,
which prevented Firecracker orchestration coverage from being measured even
when BOT_BOTTLE_BACKEND=firecracker is set on the KVM runner.

Narrow the skip to: GITEA_ACTIONS=true AND BOT_BOTTLE_BACKEND != firecracker.
When BOT_BOTTLE_BACKEND=firecracker the test is explicitly opted in to run on
the self-hosted KVM runner where the required /dev/kvm + TAP pool exist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add separate `integration-docker` and `integration-firecracker` jobs,
each with an explicit BOT_BOTTLE_BACKEND env var, so the backend used
is visible in CI output and skipped backends surface as a distinct job
rather than silent unittest.skip lines.

- integration-docker: ubuntu-latest, BOT_BOTTLE_BACKEND=docker
- integration-firecracker: [self-hosted, kvm], BOT_BOTTLE_BACKEND=firecracker,
  same-repo PRs + push + workflow_dispatch only (untrusted fork PRs do
  not execute on the privileged KVM runner)
- coverage: same same-repo restriction; refs #414 for the planned
  follow-up that moves coverage to ubuntu-latest via artifact combination

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The self-hosted runner's Nix python env has no `pip` module, so
`python3 -m pip install -r requirements-dev.txt` failed with "No module
named pip" in both firecracker jobs. Neither job needs that install:

- integration-firecracker runs the stdlib `unittest` suite (no deps);
- coverage needs only `coverage`, which the runner's Nix python env
  already ships (7.12.0) — verified `coverage run`/`coverage json` work.

pylint/pyright are lint.yml's concern, not test.yml's. The ubuntu-latest
`unit` job keeps its `--break-system-packages` install unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1qRZTJC6qgBsUSjNrBdkX
`sqlite3.Connection.__exit__` only commits/rolls back a transaction — it
does not close the connection. Python 3.13 (the Nix env on the KVM
runner) emits `ResourceWarning: unclosed database` for every connection
GC'd without an explicit close, producing noisy output in the coverage job.

Add `DbStore._connection()`, a `contextmanager` that calls `self._connect()`,
wraps it in the existing transaction context manager, and closes the
connection in a `finally` block. Change all `with self._connect() as conn:`
call sites in `db_store.py`, `audit_store.py`, `queue_store.py`, and
`orchestrator/registry.py` to `with self._connection() as conn:`.
`_connect()` remains as the per-subclass hook (RegistryStore overrides
it to set `busy_timeout`); `_connection()` delegates to `self._connect()` so
the override is respected.
On the self-hosted KVM runner Docker is on PATH but the daemon socket
is unreachable (firewalled/dropped). subprocess.run(["docker", "info"])
with no timeout hangs indefinitely on a dropped connection, stalling the
coverage job for hours — one hang per @skip_unless_docker()-decorated
class, ~8 per integration suite run.

Add timeout=5 with a TimeoutExpired → False fallback so the check
resolves quickly to "unreachable" rather than blocking.
Gitea fires the pull_request push event as 'synchronize' (GitHub spec),
not 'synchronized'. The typo meant the workflow only ran on opened/
edited/reopened, leaving the required check yellow with no details link
after every commit push.
Docker integration tests are already covered by the integration-docker
job on ubuntu-latest. On the KVM runner the Firecracker TAP/nftables
pool conflicts with Docker networking, causing those tests to hang
and the coverage job to never complete.

Add SKIP_DOCKER_TESTS env-var support to docker_available() and set
it for the integration phase of coverage.sh so only Firecracker
integration tests run there.
On a self-hosted KVM runner the process has a real controlling terminal
so name_color_modal successfully opens /dev/tty and enters a curses
loop waiting for keyboard input, hanging the test indefinitely.

Docker containers (ubuntu-latest runners) don't have a real /dev/tty,
causing an OSError that triggers the existing fallback — this is why
the hang was invisible in ubuntu-latest CI.

Also add timeout=5 to _daemon_reachable() to match the same defensive
fix already applied to docker_available() in tests/_docker.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test: cover _daemon_reachable timeout path and DbStore.is_migrated
test / integration-firecracker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 14s
test / coverage (pull_request) Successful in 34s
lint / lint (push) Successful in 50s
test / unit (pull_request) Successful in 1m31s
d117460192
Two diff-coverage gaps on the ci-kvm-runner branch:

1. bot_bottle/backend/docker/setup.py: the try/except TimeoutExpired
   block added in a prior commit had no tests reaching the subprocess
   path. Add two tests to TestDockerSetupStatus: one for the success
   path (subprocess returns 0) and one for the TimeoutExpired fallback.

2. bot_bottle/db_store.py: the _connection() context manager change in
   is_migrated() was never exercised by unit tests (all callers mock
   is_migrated() directly). Add test_db_store.py covering the absent-DB,
   missing-schema-table, migrated, and behind-schema cases.
didericis force-pushed ci-kvm-runner from 686351c91d to d117460192 2026-07-18 22:18:41 -04:00 Compare
didericis added 1 commit 2026-07-18 22:35:01 -04:00
fix(tests): run sandbox integration on firecracker
test / integration-firecracker (pull_request) Failing after 5s
test / integration-docker (pull_request) Successful in 9s
tracker-policy-pr / check-pr (pull_request) Successful in 7s
test / unit (pull_request) Successful in 33s
test / coverage (pull_request) Failing after 27s
lint / lint (push) Failing after 42s
626f07efa6
didericis added 1 commit 2026-07-18 22:59:55 -04:00
fix(infra-build): repair gateway image build so infra artifact can publish
tracker-policy-pr / check-pr (pull_request) Successful in 16s
test / integration-docker (pull_request) Successful in 31s
test / unit (pull_request) Successful in 40s
test / integration-firecracker (pull_request) Failing after 2m36s
test / coverage (pull_request) Failing after 3m34s
0d696674e3
The Firecracker integration + coverage jobs pull a prebuilt infra rootfs
artifact (PRD 0069 Stage 2) versioned by a content hash of the Dockerfiles,
bot_bottle/, and the guest init. Building that artifact (publish_infra ->
docker build Dockerfile.gateway) has been broken since 5ad3449, so the
artifact was never published and the KVM runner's integration test 404'd on
the pull — the failure this branch surfaced once it stopped falsely skipping.

Two build-time bugs, both from 5ad3449, neither exercised since:

- pyproject.toml declared build-backend "setuptools.backends.legacy:build",
  which is not an importable module; `pip install /src/` failed with
  BackendUnavailable. Use the real backend, "setuptools.build_meta"
  (the project has proper [project] metadata + flat-layout autodiscovery).
  Not part of the artifact hash, so this alone doesn't move the version.

- Dockerfile.gateway wrote /app/egress_addon.py before /app existed (the
  mkdir/WORKDIR came later), so the RUN redirect died with exit 2. Move
  WORKDIR /app above the shim write (WORKDIR creates it) and drop the now
  redundant later WORKDIR. This changes the gateway Dockerfile, so the infra
  artifact version moves 3c9e7b23260992db -> 01e6aaa714756fce; the matching
  artifact has been built and published to the generic package registry.

Also add Dockerfile* and pyproject.toml to test.yml's path filters: these
inputs determine what the firecracker jobs build/pull, so a change to them
must re-run the suite (and lets this push trigger a pull_request run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
didericis added 1 commit 2026-07-19 00:43:14 -04:00
fix(firecracker): launch infra-VM gateway as a module, not /app/gateway_init.py
test / integration-docker (pull_request) Successful in 18s
tracker-policy-pr / check-pr (pull_request) Successful in 19s
test / unit (pull_request) Failing after 35s
lint / lint (push) Failing after 46s
test / coverage (pull_request) Failing after 1m37s
test / integration-firecracker (pull_request) Failing after 1m50s
127ba49372
The infra VM's init boots the orchestrator control plane AND the gateway data
plane (egress/git-http/supervise). Since 5ad3449 moved the daemons into the
installed `bot_bottle` package, there is no `/app/gateway_init.py` file — the
gateway image's entrypoint is `python3 -m bot_bottle.gateway_init`. But the
firecracker init still spawned the old file path, so the guest logged:

    python3: can't open file '/app/gateway_init.py': No such file or directory

The control plane came up (it already used `python3 -m bot_bottle.orchestrator`)
but the gateway never started, so mitmproxy never generated its CA and launch
died with "gateway CA not available after 30s". Mirror the orchestrator line:
run the gateway as `python3 -m bot_bottle.gateway_init` (bot_bottle resolves
from the /app CWD, same as the control plane; egress-entrypoint.sh /
egress_addon.py are present from the gateway base image).

Third build/runtime bug from 5ad3449's package refactor that the Firecracker
integration suite never exercised (the artifact publish was broken, so the
job 404'd before boot). Changes the init, so the infra artifact version moves;
the matching rootfs has been rebuilt and published.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
didericis added 1 commit 2026-07-19 00:51:44 -04:00
fix(firecracker): make guest /tmp world-writable so agents can use it
test / integration-docker (pull_request) Successful in 16s
test / coverage (pull_request) Failing after 36s
test / integration-firecracker (pull_request) Failing after 43s
lint / lint (push) Failing after 56s
test / unit (pull_request) Successful in 1m37s
tracker-policy-pr / check-pr (pull_request) Failing after 13m24s
6f885af4b4
The rootless agent rootfs build can land /tmp as 0755/root-owned, so the
agent (uid 1000 node) can't create scratch dirs there. The sandbox-escape
suite's README-push test does `cd /tmp && git init sandbox-escape-repo` and
died with "cannot mkdir sandbox-escape-repo: Permission denied" — before the
git-gate gitleaks hook could run — so the test read it as a missing hook.
On docker the agent inherits node:22-slim's 1777 /tmp, which is why only the
Firecracker path was affected (and only now that the suite runs end-to-end).

Set /tmp to 1777 in the guest PID-1 init, so every agent VM boots with a
usable /tmp regardless of rootfs perm drift.

Also update the infra-init unit test to assert the gateway launches via the
`bot_bottle.gateway_init` module (matching the prior fix), not a file path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
didericis added 1 commit 2026-07-19 01:05:32 -04:00
fix(firecracker): version-aware infra-VM adoption + robust teardown
test / integration-docker (pull_request) Successful in 15s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / coverage (pull_request) Failing after 35s
test / unit (pull_request) Successful in 40s
test / integration-firecracker (pull_request) Failing after 1m46s
lint / lint (push) Failing after 2m33s
4c01e31e96
The infra VM is a per-host singleton that outlives short-lived launchers, so
`ensure_running` adopts it when its control plane is healthy. But it adopted
ANY healthy VM regardless of the code that built it — so after an infra-code
change the old VM kept being adopted and the new code never booted. The only
way to dislodge it was an out-of-band `kill`, which then raced whatever
launched next. On CI this meant every infra change needed a manual VM kill.

Make adoption version-aware:

  * boot records the infra-artifact version it booted from in a `booted-version`
    marker beside the singleton; `stop` clears it.
  * `ensure_running` adopts only when the marker matches the current version
    (`_adoptable`); a missing/mismatched marker falls through to stop + reboot.
    So a stale VM is replaced automatically on the next launch — no manual kill,
    and it's concurrency-safe (the reboot happens under the singleton flock).

Also harden teardown: the PID file drifts after crashes / out-of-band kills,
so `stop` now also reaps any orphaned firecracker still bound to the infra
config path (scoped to that path, so interactive-pool VMs are untouched) —
otherwise a survivor holds the orchestrator TAP and the fresh boot dies with
"tap … Resource busy".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
didericis added 2 commits 2026-07-19 01:23:17 -04:00
The KVM CI runner now sets BOT_BOTTLE_FC_* for its isolated pool (distinct
iface prefix / orch iface / nft table / IP base), and that env leaks into the
coverage job's test process. Five netpool tests hardcoded the default names
(bbfc*, bot_bottle_fc) and so failed there with e.g. ['bbfc1'] != ['bbci1'].

Assert against netpool's env-driven config instead — slot(i).iface / the
configured prefix — so the tests check the LOGIC regardless of which pool the
host is configured for. The single-source test now compares the parsed
defaults (netpool._DEFAULTS, env-independent) for the module constants, since
IFACE_PREFIX/NFT_TABLE legitimately layer an env override on top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
fix(firecracker): fold guest init into the agent-rootfs cache key
tracker-policy-pr / check-pr (pull_request) Successful in 18s
test / integration-docker (pull_request) Successful in 35s
test / unit (pull_request) Successful in 40s
lint / lint (push) Failing after 45s
test / integration-firecracker (pull_request) Successful in 2m50s
test / coverage (pull_request) Successful in 2m52s
d1aec706e3
build_agent_rootfs_dir cached the built rootfs by Dockerfile content alone,
but util.inject_guest_boot then writes util._GUEST_INIT into it. So a fix to
the init — making /tmp world-writable (1777) so the agent can create scratch
dirs / git worktrees there — did NOT bust the cache: the KVM runner kept
reusing a stale agent-<dockerfilehash> rootfs built with the old init, and the
sandbox-escape README-push test kept failing at `git init /tmp/...` with
"Permission denied".

Key the cache on Dockerfile content AND the injected init (_rootfs_digest), so
an init change rebuilds. Self-busting: the new key yields a fresh cache dir, so
no manual cache clear on the runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
didericis added 1 commit 2026-07-19 01:31:23 -04:00
test(firecracker): satisfy pyright on the new infra-VM tests
tracker-policy-pr / check-pr (pull_request) Successful in 18s
test / integration-docker (pull_request) Successful in 30s
test / unit (pull_request) Successful in 42s
lint / lint (push) Successful in 45s
test / integration-firecracker (pull_request) Successful in 44s
test / coverage (pull_request) Successful in 1m8s
0adbf25977
Two lint fixes, both test-only (no effect on the infra artifact version):
- annotate the TestAdoptable / TestKillInfraFirecrackers helper params
  (reportMissingParameterType).
- test_docker_test_helpers: read __unittest_skip__ via getattr on the
  dynamically-built Case type, matching the sibling assertion — pyright can't
  see the attribute the skip decorator adds (reportAttributeAccessIssue).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
All checks were successful
tracker-policy-pr / check-pr (pull_request) Successful in 18s
Required
Details
test / integration-docker (pull_request) Successful in 30s
test / unit (pull_request) Successful in 42s
lint / lint (push) Successful in 45s
test / integration-firecracker (pull_request) Successful in 44s
test / coverage (pull_request) Successful in 1m8s
This pull request has changes requested by an official reviewer.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin ci-kvm-runner:ci-kvm-runner
git checkout ci-kvm-runner
Sign in to join this conversation.