Commit Graph

1263 Commits

Author SHA1 Message Date
didericis-claude 75d0c0741a fix(rebase): correct test_cached_lookup key and drop duplicate test
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / integration-docker (pull_request) Successful in 8s
tracker-policy-pr / check-pr (pull_request) Successful in 15s
test / unit (pull_request) Successful in 31s
lint / lint (push) Successful in 44s
test / build-infra (pull_request) Successful in 3m39s
test / integration-firecracker (pull_request) Successful in 1m52s
test / coverage (pull_request) Failing after 1m58s
test / publish-infra (pull_request) Has been skipped
Two issues introduced by the rebase conflict resolution:

- test_cached_lookup_requires_ready_marker used _dockerfile_hash as the
  cache-dir key, but cached_agent_rootfs_dir now uses _rootfs_digest (which
  also folds in the guest init). Updated the test to match.
- test_pyproject_toml_change_bumps_version appeared twice in
  test_infra_artifact.py (once from main, once from the PR commit that
  added pyproject.toml support). Removed the duplicate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 01:50:45 -04:00
didericis-claude 85b85f1c84 fix(infra-artifact): include pyproject.toml in rootfs version digest
Dockerfile.gateway COPYs pyproject.toml into /src and runs pip install
/src, so it is a real input to the baked rootfs. A dependency-only change
previously reused stale artifact versions, potentially booting a rootfs
whose installed packages differed from the current checkout.

Also adds _fake_repo fixture support and a regression test so this input
can't silently drop out of the hash again.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 01:50:45 -04:00
didericis-claude 0dce65cd45 fix(pyright): assert not None before accessing datetime attributes in tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 01:50:45 -04:00
didericis-claude 9991cc2740 fix: return None from image_created_at when timestamp is absent
FROM-scratch images (commit_container output) and registry images built
for reproducibility often omit the created field entirely. Both backends
were calling die() in that case, crashing the cached-image quickstart
before any container started.

image_created_at now returns datetime | None — None when the field is
absent or unparseable, die() only on real inspect failures (non-zero
exit, malformed JSON). stale_checks in both backends skips the staleness
check when None is returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 01:50:45 -04:00
didericis-codex 8e43ab86e4 fix(firecracker): honor cached image policy 2026-07-21 01:50:45 -04:00
didericis-claude 31a6e0f18a fix: correct host_db_path import and stale-check test expectations
- config_store.py imported host_db_path from supervise_types (wrong);
  it lives in paths.py, matching all other stores (audit, queue, etc.)
- test_stale_checks: two tests expected check_stale called twice
  (agent + sidecar) — consolidated arch has no sidecar, so once is
  correct; update assertions and remove unused Path import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 01:50:45 -04:00
didericis-claude 0dd6adf37a fix(lint): remove unused BottleImages imports from two test files
pyright strict reportUnusedImport flagged BottleImages in
test_docker_launch_committed_image.py and test_macos_container_launch.py;
neither file references the type by name (they only use the returned
value's attributes).
2026-07-21 01:50:45 -04:00
didericis-claude d3b37801a8 fix: remove redundant quoted annotations in BottleImages and abstract methods
`from __future__ import annotations` already defers all annotation
evaluation, so quoting `str | Path`, `BottleImages` inside the same
module was redundant and tripped pyright strict mode.
2026-07-21 01:50:45 -04:00
didericis-claude f0cab98c79 refactor: BottleImages dataclass, prelaunch_checks, build_or_load_images
Addresses review comments 3098, 3099, 3100 on PR #336:

- Add BottleImages(agent, sidecar) dataclass to backend/__init__.py.
  Docker/macOS backends use str image refs; smolmachines uses Path
  artifacts. Replaces the singular `image` variable from the canonical
  pattern in comment 3100.

- Replace _image_stale_checks/skip_stale with public prelaunch_checks().
  CLI now calls backend.prelaunch_checks(plan) before backend.launch(plan);
  if StaleImageError is raised and the operator confirms, launch proceeds
  without re-checking. Removes the while-True/skip_stale retry loop.

- Add abstract _build_or_load_images(plan) -> BottleImages to
  BottleBackend. launch() calls it then passes images to _launch_impl.
  Each backend implements both methods.

- Fix comment 3098 (macos-container): _build_images is removed.
  build_or_load_images() has separate fresh/cached code paths — the
  cached path never calls a build helper.

- Update _start_bundle (smolmachines) to accept sidecar_artifact: Path
  directly. Sidecar artifact resolution moves to _sidecar_from_path(),
  called by build_or_load_images alongside _agent_from_path().
2026-07-21 01:50:45 -04:00
didericis-claude ea16123fcf fix(tests): resolve pyright errors in stale-check test files
Remove unused imports, add missing type annotations, fix Die()
constructor calls (int code, not str), replace fake backend
subclass with patch.object approach to avoid reportMissingParameterType
and override-incompatibility errors in strict pyright mode.
2026-07-21 01:50:45 -04:00
didericis-claude 05ca730b6b test: add unit tests for stale-image checks to reach ≥90% diff-coverage
Covers StaleImageError / check_stale / check_stale_path, the
BottleBackend.launch template method (skip_stale flag), stale_checks
functions across docker / smolmachines / macos-container backends,
_build_images cached paths in the macOS backend, image_created_at edge
cases in both docker and container util modules, the CLI stale loop
(headless die, interactive decline, interactive confirm + retry), and
ConfigStore.cached_image_stale_warning_days fallback paths.

Diff-coverage: 488/507 changed lines covered (96.3%).
2026-07-21 01:50:45 -04:00
didericis-claude 2be3fb6531 refactor(image-cache): replace warn_if_stale with StaleImageError; add launch template
- image_cache: StaleImageError exception + check_stale/check_stale_path (raise instead of warn)
- BottleBackend.launch: template method (skip_stale flag) that calls _image_stale_checks then _launch_impl
- Each backend: _image_stale_checks delegates to a stale_checks() function in its launch module; _launch_impl replaces launch override
- macos_container: adds image_created_at to util, cached-image support in _build_images, stale_checks
- cli/start.py: catches StaleImageError, prompts interactively, retries with skip_stale=True; headless mode dies on it
2026-07-21 01:50:45 -04:00
didericis-codex 240b540e11 fix: make config store schema explicit 2026-07-21 01:50:45 -04:00
didericis-codex 2cc090c184 Add cached image quickstart 2026-07-21 01:50:45 -04:00
didericis-claude b25cd72fc3 test(backend): cover poll_ca_cert timeout paths for diff-coverage gate
test / integration-docker (pull_request) Successful in 12s
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / unit (pull_request) Successful in 35s
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / build-infra (pull_request) Successful in 3m37s
test / integration-firecracker (pull_request) Successful in 1m30s
test / coverage (pull_request) Successful in 1m32s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (push) Successful in 13s
prd-number / assign-numbers (push) Failing after 19s
lint / lint (push) Successful in 43s
Update Quality Badges / update-badges (push) Failing after 41s
test / unit (push) Successful in 1m42s
test / stage-firecracker-inputs (push) Successful in 2s
test / build-infra (push) Failing after 7s
test / integration-firecracker (push) Has been skipped
test / coverage (push) Has been skipped
test / publish-infra (push) Has been skipped
Add tests for the three uncovered paths introduced by the poll_ca_cert
extraction: the timeout + sleep branches in backend/util, and the
TimeoutError → GatewayError and TimeoutError → die() conversions in the
macOS and Firecracker callers.
2026-07-21 04:54:40 +00:00
didericis-claude 8e43c26ab4 fix(backend): extract poll_ca_cert helper and fix PRD port docs
Extract the shared CA cert polling loop into `backend/util.poll_ca_cert`
(firecracker and macos backends were duplicating deadline/sleep/raise logic).
Each caller now wraps a fetch lambda and converts TimeoutError to its own
error type. Also corrects the PRD port publication line from {port}:{port}
to {host_port}:8099.
2026-07-21 04:54:40 +00:00
didericis-claude 14ff4fe186 fix(lifecycle): build gateway before infra and fix orchestrator port mapping
- `_build_images()` now builds Dockerfile.gateway → Dockerfile.orchestrator
  → Dockerfile.infra in order; Dockerfile.infra starts FROM bot-bottle-gateway
  so the base must exist on clean hosts.
- Publish mapping corrected from `self.port:self.port` to `self.port:DEFAULT_PORT`
  (8099) — gateway_init hardcodes the orchestrator on port 8099 inside the
  container, so the host-side published port must map to that fixed internal port.
- BOT_BOTTLE_ORCHESTRATOR_URL inside the container now always points to
  127.0.0.1:8099, not self.port, since gateway daemons reach the orchestrator
  over loopback at the fixed internal port.
- Update test_ensure_running_builds_both_images → _all_images for the new
  three-step build sequence; add test_publish_maps_host_port_to_fixed_internal_port
  to lock in the port-mapping fix.

Addresses the P1 findings from the didericis-codex review on PR #432.
2026-07-21 04:54:40 +00:00
didericis-claude cae1215f63 test(lifecycle): cover edge paths to satisfy diff-coverage gate
Add three new tests:
- noop when healthy but docker inspect fails (returns True → don't churn)
- build failure raises GatewayError
- _ensure_network creates the network when it doesn't exist

Also update the integration test to use new OrchestratorService API
(infra_name/image instead of orchestrator_name/gateway_name/gateway_image).

Brings diff-coverage from 86% to 90.3% against origin/main.
2026-07-21 04:54:40 +00:00
didericis-claude 28766d7733 fix(pyright): resolve type errors introduced by lifecycle refactor
- Remove unused INFRA_IMAGE import from test_orchestrator_lifecycle
- Update integration test to use new single-container OrchestratorService
  API (infra_name/image replaces orchestrator_name/gateway_name/gateway_image)
- Move type: ignore to the lambda line in gateway_init SIGHUP handler
- Break two long lines in test_orchestrator_lifecycle
2026-07-21 04:54:40 +00:00
didericis-claude 819f967844 fix(lint): resolve pylint findings in gateway_init
- Extract _sigkill_all() to cut nesting depth below the 5-block limit
- Add pylint: disable=consider-using-with on Popen (process must outlive caller)
- Break long SIGHUP signal line to stay within 100 chars
2026-07-21 04:54:40 +00:00
didericis-claude 2f45f5afec feat(docker): consolidate to single infra container under gateway_init supervise tree
Collapses the two-container Docker model (gateway + orchestrator) into one
bot-bottle-infra container, matching the macOS and Firecracker backends.

- Dockerfile.infra: now a shared gateway+orchestrator base (COPY bot_bottle
  from orchestrator build, no CMD override)
- Dockerfile.infra.fc: new Firecracker-specific layer (buildah/crun/netavark)
- gateway_init: adds orchestrator daemon with _OPT_IN_DAEMONS gating so it
  only starts when BOT_BOTTLE_GATEWAY_DAEMONS explicitly includes it
- orchestrator/lifecycle: OrchestratorService manages one infra container;
  builds orchestrator (intermediate) then infra; live source bind-mounted at
  /bot-bottle-src with PYTHONPATH so the subprocess uses the checkout
- backend/consolidated_util: extracts provision_bottle + teardown_consolidated
  shared across all three backends; removes duplication in docker/fc/macos
  consolidated_launch modules
- firecracker/infra_vm: builds four images (orchestrator→gateway→infra→infra.fc)
- All unit tests updated and passing (1878 tests)
- PRD status: Draft → Active
2026-07-21 04:54:40 +00:00
didericis-claude 31a5ec2fc8 docs(prd): consolidate infra backend for docker
Adds PRD for collapsing the Docker backend from two containers
(gateway + orchestrator) to a single bot-bottle-infra container,
restructuring Dockerfile.infra as the shared base, and extracting
duplicated CA polling / teardown / provision helpers into a shared
backend utility module.

Closes #431
2026-07-21 04:49:18 +00:00
didericis-claude 1f192d785a refactor(config_store): replace generic key/value table with explicit schema
test / stage-firecracker-inputs (push) Successful in 2s
test / integration-docker (push) Successful in 17s
Update Quality Badges / update-badges (push) Failing after 42s
lint / lint (push) Successful in 47s
test / unit (push) Successful in 1m41s
test / build-infra (push) Successful in 3m43s
test / integration-firecracker (push) Successful in 1m28s
test / coverage (push) Successful in 1m29s
test / publish-infra (push) Successful in 1m49s
Switch orchestrator_config from a generic key/text store to a typed
single-row table with a dedicated teardown_timeout_seconds REAL column.
The CHECK (id = 1) constraint enforces at most one config row.

Remove TEARDOWN_TIMEOUT_KEY constant; update OrchestratorConfigStore to
expose get/set/delete_teardown_timeout_seconds() instead of the generic
get/set/delete(key). Resolving the DB value in resolve_teardown_timeout()
no longer needs a float() conversion since the column is already REAL.
2026-07-21 00:40:59 -04:00
didericis-codex 853b6d1678 test(orchestrator): satisfy config store type checking 2026-07-21 00:40:59 -04:00
didericis-claude cf9a53d582 fix: resolve teardown timeout before registration to prevent orphaned state
If resolve_teardown_timeout() raised after launch_consolidated()/register_agent()
returned, the teardown callback was never registered, leaving the bottle
registered with no cleanup path. Resolve the timeout into a local variable
before the registration call so any failure aborts before the bottle exists.

Add tests covering OrchestratorConfigStore, resolve_teardown_timeout priority
ordering (env > db > default), and the source-order invariant that the resolver
runs before registration in all three backends.
2026-07-21 00:40:59 -04:00
didericis-claude 0b36c3eb48 fix: make orchestrator teardown timeout configurable (closes #435)
Resolves via ENV VAR -> orchestrator DB config -> default (30 s, up from 5 s):
  BOT_BOTTLE_ORCHESTRATOR_TEARDOWN_TIMEOUT_SECONDS
  teardown_timeout_seconds key in new orchestrator_config table (bot-bottle.db)

New OrchestratorConfigStore (same DbStore/TableMigrations pattern as the
registry) stores the DB-level setting. resolve_teardown_timeout() implements
the priority chain and is called at stack.callback registration time in all
three backends (macos_container, docker, firecracker).
2026-07-21 00:40:59 -04:00
didericis-claude 28fcc3f2d2 test: cover inspect_container_network_ip parser directly
test / stage-firecracker-inputs (pull_request) Successful in 5s
test / unit (pull_request) Successful in 35s
test / integration-docker (pull_request) Successful in 9s
tracker-policy-pr / check-pr (pull_request) Successful in 8s
test / build-infra (pull_request) Successful in 3m48s
test / integration-firecracker (pull_request) Successful in 1m43s
test / coverage (pull_request) Successful in 1m53s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (push) Successful in 11s
test / unit (push) Successful in 35s
test / stage-firecracker-inputs (push) Successful in 2s
Update Quality Badges / update-badges (push) Failing after 40s
lint / lint (push) Successful in 2m43s
test / build-infra (push) Successful in 3m25s
test / integration-firecracker (push) Successful in 1m45s
test / coverage (push) Successful in 1m50s
test / publish-infra (push) Successful in 2m7s
The None/empty-string distinction is the invariant that prevents
live_source_ips from treating a failed inspect as a legitimately
no-address container. Add TestInspectContainerNetworkIp to util tests
covering: IP found, CIDR stripping, no address yet, absent network
list, non-zero exit, malformed JSON, and unexpected JSON shape.
2026-07-21 03:49:19 +00:00
didericis-claude 571030b8e8 fix: require authoritative container snapshot before reconciling
test / stage-firecracker-inputs (pull_request) Successful in 5s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Successful in 33s
test / unit (pull_request) Successful in 39s
lint / lint (push) Successful in 49s
test / build-infra (pull_request) Successful in 3m23s
test / integration-firecracker (pull_request) Successful in 1m42s
test / coverage (pull_request) Failing after 1m26s
test / publish-infra (pull_request) Has been skipped
A failed container list or a partial per-container inspect were
previously indistinguishable from a legitimately empty/partial live
set, so reconciliation could silently unregister healthy bottles.

- enumerate_active() now raises EnumerationError instead of returning []
  when `container list` fails
- Add inspect_container_network_ip() to util, which returns None on
  inspect failure (vs "" for "no DHCP address yet"), so live_source_ips
  can tell the two apart
- live_source_ips() raises EnumerationError on either failure mode;
  register_agent() catches it and skips reconciliation, same as
  OrchestratorClientError
- Update tests: rename test_empty_when_the_cli_fails to
  test_raises_when_the_cli_fails, update patching to the new function,
  add coverage for list failure, per-container inspect failure, and the
  launch-not-blocked path
2026-07-21 03:39:18 +00:00
didericis-codex 288b205a44 test(macos): isolate registration tests from container discovery
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / integration-docker (pull_request) Successful in 6s
tracker-policy-pr / check-pr (pull_request) Successful in 5s
test / unit (pull_request) Successful in 30s
lint / lint (push) Successful in 42s
test / build-infra (pull_request) Successful in 3m18s
test / integration-firecracker (pull_request) Successful in 1m27s
test / coverage (pull_request) Successful in 1m51s
test / publish-infra (pull_request) Has been skipped
2026-07-21 03:27:16 +00:00
didericis 0c1d27b605 fix(egress): the unattributed message must name the token-mismatch cause too
`/resolve` fail-closes on a missing/ambiguous registry row *and* on a
request whose identity token doesn't match. The message named only the
first, so a bottle that was registered correctly but sent no token read as
"not registered" and sent the reader looking for a deregistered bottle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:27:16 +00:00
didericis 69361114d1 fix(egress): name the real fault when a deny-all is not an allowlist miss
An unattributed bottle, an unreachable orchestrator, and an unparseable
policy all become a deny-all Config, and a deny-all is indistinguishable
from "policy loaded, host not allowed" at the decision point — both are just
"no matching route". So every one of them reported `host X is not in the
bottle's egress.routes allowlist`, which reads as a config problem and sends
the operator hunting for a route that was never missing. Diagnosing a
bricked registration cost hours for exactly this reason.

Carry the structural reason on Config and prefer it in decide(). A genuine
allowlist miss — a policy that loaded and simply lacks the host — keeps the
original wording, so the message now tells the two cases apart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:27:16 +00:00
didericis e4d53fd360 fix(orchestrator): reap registry rows whose bottle is no longer running
A registry row only ever left the registry two ways: an explicit
teardown_bottle (the launcher's cleanup callback) or the same-IP supersede
sweep in register(). Neither runs when the launching CLI dies hard, so the
row outlives its container.

That orphan is not inert. Source IPs are recycled by the backend's DHCP and
by_source_ip fail-closes on ambiguity, so a leftover row at a reused address
resolves no policy at all for the next bottle that lands there — and a
bottle with no policy denies every host, which surfaces to the agent as
"host X is not in the allowlist" for hosts that were never the problem.

Add reap_absent/reconcile and call it from the macOS launch path before
registering, so each launch self-heals the registry. Restores the invariant
the data plane needs: at most one active row per live address, and none for
a dead one. The second half matters as much as the first — when several rows
claim a *live* address the newest wins and the rest are swept, otherwise a
recycled address stays ambiguous, which is exactly the bricked state.

The host supplies the live set because the orchestrator runs inside the
infra container and cannot see the backend. A grace window exempts rows
younger than it, so reconciliation cannot race a bottle still coming up, and
a reconcile failure is logged rather than blocking an otherwise-fine launch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:27:16 +00:00
didericis-codex 5e01c28016 test(macos): cover root container exec helper
test / integration-docker (pull_request) Successful in 11s
tracker-policy-pr / check-pr (pull_request) Successful in 20s
test / unit (pull_request) Successful in 32s
test / stage-firecracker-inputs (pull_request) Successful in 3s
test / build-infra (pull_request) Successful in 8m31s
test / integration-firecracker (pull_request) Successful in 1m32s
test / coverage (pull_request) Successful in 1m27s
test / publish-infra (pull_request) Has been skipped
test / stage-firecracker-inputs (push) Successful in 1s
test / integration-docker (push) Successful in 7s
test / unit (push) Successful in 29s
Update Quality Badges / update-badges (push) Failing after 33s
lint / lint (push) Successful in 42s
test / build-infra (push) Successful in 8m37s
test / integration-firecracker (push) Successful in 1m27s
test / coverage (push) Successful in 1m27s
test / publish-infra (push) Successful in 1m48s
2026-07-21 03:12:17 +00:00
didericis 2f8539c2c7 fix(macos): name the gateway instead of addressing it, so bottles survive it moving
test / integration-docker (pull_request) Successful in 14s
test / unit (pull_request) Successful in 38s
tracker-policy-pr / check-pr (pull_request) Successful in 25s
lint / lint (push) Successful in 49s
test / stage-firecracker-inputs (pull_request) Successful in 5s
test / build-infra (pull_request) Successful in 3m31s
test / integration-firecracker (pull_request) Successful in 1m51s
test / coverage (pull_request) Failing after 1m33s
test / publish-infra (pull_request) Has been skipped
The shared gateway's address is DHCP-assigned and changes whenever the infra
container is recreated — a source-hash bump, an image upgrade, a crash. Every
agent-facing URL embedded that address, and the proxy URL reaches the agent
as process environment at `container exec` time. A running process's environ
cannot be rewritten from outside, so a moved gateway stranded every running
bottle permanently: not degraded, unreachable, until relaunched and its agent
session thrown away.

Give the agent a stable name instead. `GATEWAY_HOSTNAME` replaces the address
in the egress proxy URL, NO_PROXY, git-http, and supervise URLs, and resolves
through the bottle's own /etc/hosts. Unlike environ that is a file, so it can
be rewritten inside a container that is already running — which is the whole
point: a gateway that returns at a new address is picked up by live bottles.

Launch writes the entry before anything execs (every agent URL names the
gateway, so it must resolve for the first connection), and re-points every
running bottle once the gateway is up, so one stranded by an earlier restart
re-attaches instead of needing a relaunch.

The write needs root and the agent runs as `node`: the host can repoint a
bottle's gateway name, the agent cannot repoint its own. Keep that asymmetry.

Apple Container 1.0 has no container-name DNS on a user network and
`container run` has no --add-host, so the entry is written by exec after
start rather than declared at run.

Does not address the other half of #443: per-bottle egress auth tokens are
held in memory by the orchestrator and are still lost across a restart, so a
re-attached bottle resolves its policy but not its injected credentials.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 23:08:59 -04:00
didericis ad100b8a84 fix(macos): supply proxy env only at exec, so Codex keeps its identity token
test / stage-firecracker-inputs (push) Successful in 4s
test / integration-docker (push) Successful in 17s
test / unit (push) Successful in 34s
lint / lint (push) Successful in 46s
Update Quality Badges / update-badges (push) Failing after 35s
test / build-infra (push) Successful in 9m18s
test / integration-firecracker (push) Successful in 1m46s
test / coverage (push) Successful in 2m3s
test / publish-infra (push) Successful in 2m31s
`container exec --env` does not override the run-time environment on Apple
Container — it appends. The launch path baked a token-less `*_PROXY` into
`container run` and relied on the exec-time, token-bearing value superseding
it, so the agent's `environ` ended up with two `HTTPS_PROXY` entries,
token-less first.

Which entry a runtime reads is then luck. Node reads the last, so Claude
bottles picked up the token and worked. Rust's `std::env::var` reads the
first, so Codex proxied with no identity token at all; `/resolve` requires a
matching (source_ip, identity_token) pair and fail-closes, so every request
from a Codex bottle was denied — its model calls, its `wss://` reconnects,
and its MCP servers alike. The registry row was correct the whole time,
which is what made this read as a registration bug.

Drop the run-time proxy vars entirely. A token-less proxy URL has no
legitimate consumer: the init process is `sleep` and everything that
egresses arrives by exec. Its only benefit was a tidy 403 for unattributed
callers, which is not worth silently dropping attribution for — and a
process that egresses before the exec-time env still fails closed, since the
agent network is host-only and the gateway is the only route off it.

Also corrects the two comments that asserted the false "exec --env wins"
invariant, so the next reader doesn't rebuild the same assumption.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:54:52 -04:00
didericis-claude c7375051fd fix(lint): mark read_tty_line as an explicit re-export in _common.py
test / coverage (push) Successful in 2m6s
test / publish-infra (push) Successful in 2m38s
test / build-infra (push) Successful in 3m49s
test / integration-firecracker (push) Successful in 2m14s
test / stage-firecracker-inputs (pull_request) Successful in 2s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Successful in 27s
test / unit (pull_request) Successful in 34s
test / build-infra (pull_request) Successful in 3m39s
test / integration-firecracker (pull_request) Successful in 1m33s
test / coverage (pull_request) Successful in 2m2s
test / publish-infra (pull_request) Has been skipped
test / stage-firecracker-inputs (push) Successful in 3s
test / unit (push) Successful in 34s
test / integration-docker (push) Successful in 32s
lint / lint (push) Successful in 43s
Update Quality Badges / update-badges (push) Failing after 39s
Pyright reportUnusedImport fired because the bare import is not used
within _common.py itself. The X as X idiom signals an intentional
public re-export without requiring __all__.
2026-07-20 20:11:04 +00:00
didericis-claude d9e685e860 fix(backend): don't prompt in headless/non-interactive backend selection
lint / lint (push) Failing after 45s
test / unit (pull_request) Successful in 1m34s
test / integration-docker (pull_request) Successful in 12s
tracker-policy-pr / check-pr (pull_request) Successful in 20s
test / stage-firecracker-inputs (pull_request) Successful in 3s
test / build-infra (pull_request) Successful in 3m21s
test / integration-firecracker (pull_request) Successful in 1m30s
test / coverage (pull_request) Successful in 1m30s
test / publish-infra (pull_request) Has been skipped
_auto_select_backend gains a prompt parameter (default True). When
prompt=False the docker-fallback [i/d/q] menu is skipped and the call
dies immediately with an actionable message ("set
BOT_BOTTLE_BACKEND=docker or install a VM backend"), preventing hangs
in CI, webhook dispatch, and orchestrator launches.

prepare_with_preflight passes prompt=not spec.headless so the headless
start path can never block waiting for TTY input it cannot receive.
2026-07-20 19:58:14 +00:00
didericis-claude b4b73a8acc refactor: consolidate read_tty_line into bot_bottle/util.py
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / integration-docker (pull_request) Successful in 26s
test / unit (pull_request) Successful in 36s
lint / lint (push) Failing after 42s
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / build-infra (pull_request) Successful in 3m57s
test / integration-firecracker (pull_request) Successful in 1m55s
test / coverage (pull_request) Successful in 2m14s
test / publish-infra (pull_request) Has been skipped
Remove the private _read_tty_line duplicate from backend/__init__.py and
the local definition from cli/_common.py. Both now import from the
shared bot_bottle.util module.
2026-07-20 19:44:32 +00:00
didericis-claude b1ebc6f1b8 test: cover _read_tty_line, macos install branch, and reply==i path
test / stage-firecracker-inputs (pull_request) Successful in 3s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 14s
lint / lint (push) Successful in 51s
test / unit (pull_request) Successful in 1m29s
test / build-infra (pull_request) Successful in 3m30s
test / integration-firecracker (pull_request) Successful in 1m34s
test / coverage (pull_request) Successful in 1m30s
test / publish-infra (pull_request) Has been skipped
2026-07-20 19:26:00 +00:00
didericis-claude 8b5b5730ae fix: remove CLI backend assumptions and add docker fallback prompt
test / stage-firecracker-inputs (pull_request) Successful in 3s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
lint / lint (push) Successful in 42s
test / integration-docker (pull_request) Successful in 33s
test / unit (pull_request) Successful in 37s
test / build-infra (pull_request) Successful in 3m47s
test / integration-firecracker (pull_request) Successful in 1m39s
test / coverage (pull_request) Failing after 1m27s
test / publish-infra (pull_request) Has been skipped
- Remove hardcoded --backend=macos-container flag reference in
  firecracker/util.py require_firecracker() error message
- Remove --backend flag from cli.py start; backend selection now
  driven exclusively by BOT_BOTTLE_BACKEND env var or auto-selection
- Skip unavailable backends in cli.py cleanup (fixes crash on Linux
  when macos-container.prepare_cleanup calls require_container())
- Add two-tier auto-selection: VM backend first (macos-container on
  macOS, firecracker on Linux+KVM); fall back to docker with a
  security warning and interactive i/d/q prompt; exit if docker
  also unavailable and print VM install instructions

Closes #344
2026-07-20 19:13:08 +00:00
didericis 44479f328e fix(macos-container): launch gateway as a module, not /app/gateway_init.py
test / stage-firecracker-inputs (pull_request) Successful in 5s
test / integration-docker (pull_request) Successful in 8s
tracker-policy-pr / check-pr (pull_request) Successful in 7s
test / unit (pull_request) Successful in 30s
test / build-infra (pull_request) Successful in 3m56s
test / integration-firecracker (pull_request) Successful in 1m36s
test / coverage (pull_request) Successful in 2m33s
test / publish-infra (pull_request) Has been skipped
test / stage-firecracker-inputs (push) Successful in 2s
test / integration-docker (push) Successful in 33s
test / unit (push) Successful in 34s
lint / lint (push) Successful in 48s
Update Quality Badges / update-badges (push) Failing after 44s
test / build-infra (push) Successful in 3m48s
test / integration-firecracker (push) Successful in 1m44s
test / coverage (push) Successful in 1m36s
test / publish-infra (push) Successful in 2m33s
The macOS infra container's init script still spawned the supervisor as
`python3 /app/gateway_init.py`. 5ad3449 moved bot_bottle from flat files
under /app into a pip-installed package, so that file no longer exists:
the supervisor never started, mitmdump never generated its CA, and the
launch failed downstream with a misleading "gateway CA not available"
error pointing at TLS rather than at the supervisor.

This is the same defect 127ba49 fixed for the firecracker backend. It
survived on macOS because CI has no Apple Container runner — the KVM
runner added in c193b04 covers firecracker, and ubuntu-latest covers
docker, but nothing exercises macos_container.

test_macos_infra asserted the substring "gateway_init.py", which the
broken path satisfied; it now asserts the module form, matching the
assertion 127ba49 already made in test_firecracker_infra_vm.

Verified end to end on the macos-container backend: image builds, CA is
generated, agent registers, and a headless bottle runs a prompt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:37:09 -04:00
didericis 2de223a33b test(gateway-image): probe package imports, not pre-refactor flat modules
test / stage-firecracker-inputs (pull_request) Successful in 3s
test / integration-docker (pull_request) Successful in 10s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / unit (pull_request) Successful in 1m31s
test / build-infra (pull_request) Successful in 3m45s
test / integration-firecracker (pull_request) Successful in 1m34s
test / coverage (pull_request) Successful in 1m59s
test / publish-infra (pull_request) Has been skipped
test / stage-firecracker-inputs (push) Successful in 3s
test / integration-docker (push) Successful in 16s
Update Quality Badges / update-badges (push) Failing after 42s
lint / lint (push) Successful in 53s
test / unit (push) Successful in 1m35s
test / build-infra (push) Successful in 3m30s
test / integration-firecracker (push) Successful in 1m46s
test / coverage (push) Successful in 2m1s
test / publish-infra (push) Successful in 2m14s
test_python_imports_supervise_module probed the image with
`import supervise; import supervise_server`, the flat sibling modules that
5ad3449 replaced. They now live in the installed package, so the test fails
with ModuleNotFoundError when actually run.

It went unnoticed because the whole class carries a GITEA_ACTIONS skip — it
is interactive-only, so CI never executes it.

All 4 tests in the class now pass against a freshly built gateway image.

Closes #429.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:40:14 -04:00
didericis-codex af1690ab22 docs(research): consolidate agent sandbox landscape 2026-07-20 14:36:40 +00:00
didericis-claude 09debcf4f0 docs(research): add Agent Safehouse HN launch and Apple Container 1.0 stable
- agent-sandbox-landscape.md: update agent-safehouse star count (~1.4k →
  ~1.8k), add HN thread #47301085 link and key discussion notes (creator
  framing, Simon Willison observation, top community quote on sandboxing
  being THE major challenge), note Apple Container 1.0 stable (Jun 9 2026)
- hn-agent-safety-discourse-july-2026.md: add Agent Safehouse March 12
  launch to the sandboxing boom section as context that prefigures the
  June–July shift in community tone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-20 13:51:52 +00:00
didericis fa11ad9a4a fix(ci,infra): repair post-merge publish + harden candidate handling
test / unit (push) Successful in 32s
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / integration-docker (pull_request) Successful in 14s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 34s
test / build-infra (pull_request) Successful in 3m35s
test / integration-firecracker (pull_request) Successful in 1m33s
test / coverage (pull_request) Successful in 1m39s
test / publish-infra (pull_request) Has been skipped
test / stage-firecracker-inputs (push) Successful in 2s
test / integration-docker (push) Successful in 32s
lint / lint (push) Successful in 43s
test / build-infra (push) Successful in 3m37s
test / integration-firecracker (push) Successful in 1m34s
test / coverage (push) Successful in 1m33s
test / publish-infra (push) Failing after 39s
Update Quality Badges / update-badges (push) Failing after 1m33s
Review follow-ups for the infra candidate-artifact flow:

1. publish-infra would fail on the first merge to main. The rootfs version now
   hashes the dropbear bytes, and build-infra sets BOT_BOTTLE_FC_DROPBEAR to the
   staged dropbear — but publish-infra (ubuntu-latest) set none, so _publish_
   bundle re-derived the version with a "<missing>" dropbear and rejected the
   candidate as "does not match checkout". Download the same firecracker-inputs
   dropbear and export BOT_BOTTLE_FC_DROPBEAR in publish-infra (and add
   stage-firecracker-inputs to its needs, since it now consumes that artifact).

2. Guard stage-firecracker-inputs with the same fork-PR check as the other
   KVM-runner jobs, so a fork PR can't spin the privileged runner (it only
   copies a static binary, but keep the posture consistent; it gates the whole
   Firecracker chain via needs).

3. ensure_artifact_gz / _publish_bundle read version.txt before checking it
   exists — a bundle missing it raised a raw FileNotFoundError instead of the
   intended "bundle is incomplete" die. Guard the read (kept before the gz/sha
   completeness check so a wrong-version bundle still reports the version
   mismatch, not "incomplete").

4. test_infra_artifact.setUp didn't isolate BOT_BOTTLE_INFRA_ARTIFACT_DIR, so
   an ambient candidate dir (the coverage job exports one) would send the
   registry-pull tests down the local-bundle path. Pin it off in setUp; the
   candidate-path cases set it explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
2026-07-20 02:07:08 -04:00
didericis-codex ad6471af12 test(infra): cover candidate release failure paths
test / stage-firecracker-inputs (pull_request) Successful in 3s
test / integration-docker (pull_request) Successful in 12s
tracker-policy-pr / check-pr (pull_request) Successful in 25s
test / unit (pull_request) Successful in 32s
lint / lint (push) Successful in 45s
test / build-infra (pull_request) Successful in 3m59s
test / integration-firecracker (pull_request) Successful in 1m36s
test / coverage (pull_request) Successful in 1m45s
test / publish-infra (pull_request) Has been skipped
2026-07-19 22:42:54 +00:00
didericis-codex 137df6f853 fix(coverage): scope infra candidate to integration tests
test / stage-firecracker-inputs (pull_request) Successful in 4s
test / integration-docker (pull_request) Successful in 12s
test / unit (pull_request) Successful in 34s
test / build-infra (pull_request) Successful in 3m34s
test / integration-firecracker (pull_request) Successful in 1m46s
test / coverage (pull_request) Failing after 1m46s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 11m25s
2026-07-19 22:33:38 +00:00
didericis-codex 4252ca3562 fix(ci): stage complete reproducible infra inputs
test / stage-firecracker-inputs (pull_request) Successful in 14s
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / integration-docker (pull_request) Successful in 22s
test / unit (pull_request) Successful in 34s
lint / lint (push) Successful in 43s
test / build-infra (pull_request) Successful in 3m32s
test / integration-firecracker (pull_request) Successful in 1m41s
test / coverage (pull_request) Failing after 57s
test / publish-infra (pull_request) Has been skipped
2026-07-19 22:25:45 +00:00
didericis-codex 701f5bf5e3 ci(infra): test and publish one candidate artifact
tracker-policy-pr / check-pr (pull_request) Successful in 20s
test / integration-docker (pull_request) Successful in 22s
lint / lint (push) Failing after 46s
test / build-infra (pull_request) Failing after 49s
test / integration-firecracker (pull_request) Has been skipped
test / coverage (pull_request) Has been skipped
test / unit (pull_request) Successful in 1m28s
test / publish-infra (pull_request) Has been skipped
2026-07-19 22:22:42 +00:00
didericis d589c08d9d test(firecracker): pin INFRA_BUILD off in the default-pull test
test / integration-docker (pull_request) Successful in 17s
tracker-policy-pr / check-pr (pull_request) Successful in 15s
test / integration-firecracker (pull_request) Successful in 42s
lint / lint (push) Successful in 51s
test / coverage (pull_request) Successful in 1m7s
test / unit (pull_request) Successful in 1m31s
The coverage CI job exports BOT_BOTTLE_INFRA_BUILD=local so the integration
suite builds infra/agent images from source. But that job also runs the unit
suite, and the ambient =local leaked into test_default_pulls_artifact_without_
docker — it drove ensure_built() down the local Docker-build branch, so the
"build_image not called / artifact pulled" assertion failed (build_image
called 3 times).

Pin the var off in that test so it exercises the default pull path regardless
of the runner's ambient env (mirrors test_local_mode_builds_deps_before_infra,
which pins it on). Full unit suite verified green under BOT_BOTTLE_INFRA_BUILD
=local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9qa3xoavjQScufDfZaXKR
2026-07-19 18:03:11 -04:00