Commit Graph

664 Commits

Author SHA1 Message Date
didericis a3ca17722a fix(macos): substitute the gateway address before the service starts
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / integration-docker (pull_request) Successful in 36s
test / unit (pull_request) Successful in 43s
lint / lint (push) Successful in 1m1s
test / integration-firecracker (pull_request) Successful in 3m31s
test / coverage (pull_request) Successful in 18s
test / publish-infra (pull_request) Has been skipped
Third attempt at the same symptom, and the first at the right layer.
podman's Docker-compatible API injects proxy settings from the *daemon*
environment after containers.conf is applied, so neither the env list
(892bfc16) nor http_proxy=false (21c144ae) could override it — the same
compat-path blind spot already seen with hosts_file. Nested containers
kept reporting `wget: bad address 'bot-bottle-gateway:9099'` through
both.

Substituting the resolved address into the service's own environment,
before `podman system service` starts, is the one place it sticks.
NO_PROXY keeps the name, which is matched against what a client asks
for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:50:12 -04:00
didericis 21c144aee9 fix(macos): stop podman re-injecting the gateway name into containers
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / integration-docker (pull_request) Successful in 19s
lint / lint (push) Successful in 54s
test / unit (pull_request) Successful in 1m39s
test / integration-firecracker (pull_request) Successful in 3m29s
test / coverage (pull_request) Successful in 24s
test / publish-infra (pull_request) Has been skipped
The address-bearing proxy URLs from 892bfc16 were written correctly but
never took effect: podman copies the host's proxy environment into every
container by default, and that copy overrides containers.conf `env`,
restoring the `bot-bottle-gateway` name a nested container cannot
resolve. Verified on the macOS host — containers still reported
`wget: bad address 'bot-bottle-gateway:9099'` with the addresses in
place.

Setting http_proxy=false disables only podman's own passthrough; the
proxy vars still reach containers, by address, from the env list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:46:47 -04:00
didericis 892bfc164c fix(macos): reach the egress proxy by address in nested containers
test / integration-docker (pull_request) Successful in 19s
tracker-policy-pr / check-pr (pull_request) Successful in 22s
test / unit (pull_request) Successful in 44s
lint / lint (push) Successful in 54s
test / integration-firecracker (pull_request) Successful in 3m19s
test / coverage (pull_request) Successful in 40s
test / publish-infra (pull_request) Has been skipped
podman's containers.conf `hosts_file` turns out to apply only to native
`podman run`; the Docker-compatible API ignores it, and the agent types
`docker`. (`base_hosts_file`, used before this, is a no-op in both
paths on podman 5.4.2.) So no config key can put the gateway name in a
nested container's /etc/hosts.

The name is therefore resolved in the bottle, where /etc/hosts does
carry it, and the address goes into the proxy URL handed to nested
containers. NO_PROXY keeps the name, since that is matched against what
a client asks for.

Verified on the macOS host: with the gateway reachable, a nested
container gets 200 from an allowlisted host and 403 from one that is
not — the egress boundary applies inside nested containers too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:26:56 -04:00
didericis 8f611b5687 fix(egress): emit preserve_auth into the generated routes
test / integration-docker (pull_request) Successful in 20s
test / unit (pull_request) Successful in 39s
lint / lint (push) Successful in 53s
test / integration-firecracker (pull_request) Successful in 3m31s
test / coverage (pull_request) Successful in 27s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 12m58s
The flag from #453 was parsed off the manifest and honored by the
addon, but `_route_to_yaml_fields` never wrote it, so it was dropped on
the way to the proxy and has never worked end to end. Every registry
route silently kept the unconditional Authorization strip.

That is the whole of the Docker Hub / GHCR "unauthorized" blocker in
#392: podman fetched a valid 2658-byte bearer from auth.docker.io, the
proxy stripped it, and registry-1.docker.io answered the resulting
anonymous request with a fresh WWW-Authenticate challenge — which reads
exactly like a credential problem rather than a serializer gap.

Found by diffing a bottle's rendered routes.yaml against its manifest:
preserve_auth was in the manifest and absent from the output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:17:04 -04:00
didericis f9f27788db feat(macos): give nested containers working egress
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 43s
lint / lint (push) Successful in 56s
test / integration-firecracker (pull_request) Successful in 3m32s
test / coverage (pull_request) Successful in 23s
test / publish-infra (pull_request) Has been skipped
Answers the last open question in #392: DNS from inside a nested
container was not a phantom, but it was also not a DNS problem. Public
resolution fails there by design — everything egresses through the
proxy — and the actual breakage was two missing pieces:

- The proxy URL names `bot-bottle-gateway`, which resolves only through
  the bottle's own /etc/hosts. Containers got their own hosts file, so
  they failed at "Could not resolve proxy", which reads like broken DNS.
  base_hosts_file propagates the bottle's entries.
- The gateway TLS-intercepts, so a container that does not trust the
  bottle's CA bundle fails with "unable to get local issuer
  certificate". The bundle is now mounted read-only and the usual env
  vars point at it, which covers curl, wget, python, and node without
  distro-specific trust commands.

Verified on the macOS host by reproducing each failure and confirming
that these three conditions applied by hand produced HTTP 200 from
inside a nested container. podman already forwards the proxy env, so
that needed nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:37:39 -04:00
didericis 41959fe2a2 fix(macos): install podman 5's full networking stack
test / integration-docker (pull_request) Successful in 43s
test / unit (pull_request) Successful in 52s
lint / lint (push) Successful in 1m0s
test / integration-firecracker (pull_request) Successful in 3m36s
test / coverage (pull_request) Successful in 16s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 11m9s
netavark shells out to `nft` for the bridge network every compose file
expects, and aardvark-dns serves name resolution inside nested
containers. Neither arrives with the base image's
`--no-install-recommends` podman, and each fails at a different and
misleading layer: without nft containers are created but never start;
without aardvark-dns DNS inside a container fails while the engine, the
pulls, and the bridge all look healthy. That last one is the likeliest
explanation for the "DNS inside nested containers fails entirely" note
in #392 — a missing package, not a design gap.

Verified on the macOS host: with passt alone, `docker run` reached
netavark and died on "unable to execute nft".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:28:42 -04:00
didericis e267ba24d9 fix(macos): install passt for podman 5 rootless networking
test / integration-docker (pull_request) Successful in 21s
lint / lint (push) Successful in 56s
test / unit (pull_request) Successful in 1m51s
test / integration-firecracker (pull_request) Successful in 3m30s
test / coverage (pull_request) Successful in 22s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 13m15s
podman 5 uses pasta as the default rootless network helper; podman 4
used slirp4netns. The spike was written against bookworm's podman 4.3.1,
so its package list never included passt — and the trixie bump (#451)
changed the default out from under it. Every nested container failed to
start with "could not find pasta", while image pulls and the service
itself worked, which disguised a missing dependency as a compat-API bug.

The bootstrap now checks for pasta up front so this fails with a clear
message instead of surfacing at the first `docker run`.

Note the sun_path fix in a12d2191 did NOT cause or cure this; that path
was over the 108-byte limit independently and would have bitten as soon
as attach was reached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:20:27 -04:00
didericis a12d219187 fix(macos): shorten the podman runtime dir to fit sun_path
podman derives conmon's attach socket as
`$XDG_RUNTIME_DIR/libpod/tmp/socket/<64-hex-id>/attach`. With the
descriptive `/tmp/bot-bottle-podman-run` that path is 116 bytes, past
the 108-byte sun_path limit, so every attached `docker run` failed with
"unable to upgrade to tcp, received 500" — while image pulls and the
service itself worked, which is what made it look like a compat-API bug
rather than a path-length one.

Found on the macOS host: quay.io pulled fine and then no container
could be started. Test pins the budget so a more readable name cannot
silently reintroduce it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:15:40 -04:00
didericis 9e9b418a92 fix(manifest): OR nested_containers across composed bottles
tracker-policy-pr / check-pr (pull_request) Successful in 13s
test / integration-docker (pull_request) Successful in 38s
test / unit (pull_request) Successful in 40s
lint / lint (push) Successful in 50s
test / integration-firecracker (pull_request) Successful in 3m22s
test / coverage (pull_request) Successful in 19s
test / publish-infra (pull_request) Has been skipped
merge_bottles_runtime sees resolved bottles, where a bottle that never
mentioned the key is indistinguishable from one that set it false. With
"later replaces" semantics, `--bottle with-containers --bottle claude-dev`
silently dropped the capability — the second bottle's default clobbered
it. OR instead: composing bottles adds capabilities, it does not remove
them. The file-based extends path still sees raw keys, so an explicit
`nested_containers: false` in a child continues to turn it back off.

Also surface the flag in the launch summary, where its absence was the
only reason the clobber went unnoticed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:43:31 -04:00
didericis ba25845886 feat(macos): run containers inside a bottle via guest-local podman
tracker-policy-pr / check-pr (pull_request) Successful in 12s
lint / lint (push) Successful in 51s
test / integration-docker (pull_request) Successful in 40s
test / unit (pull_request) Successful in 46s
test / integration-firecracker (pull_request) Successful in 3m35s
test / coverage (pull_request) Successful in 43s
test / publish-infra (pull_request) Has been skipped
Adds the `nested_containers` bottle flag. On the macOS backend it starts
a rootless podman service inside the bottle and exposes its
Docker-compatible API socket, so the agent still runs `docker` and
`docker compose`. No host daemon socket is mounted and the guest gains
no capabilities; backends that cannot run a guest-local engine reject
the flag in the shared prepare template rather than ignoring it.

Podman rather than rootless Docker because Apple Container's capability
bounding set omits CAP_SYS_ADMIN, which the kernel requires to write a
multi-range uid_map via newuidmap. With no subordinate UID range podman
falls back to a single-UID self-mapping an unprivileged process may
write itself, so the image build strips /etc/subuid and /etc/subgid
entries rather than adding them.

That mapping is also why nested containers are not an isolation layer:
root inside one is the agent user outside it. They are a build/test
convenience; the bottle remains the boundary.

Ports the spike branch onto main, renaming docker_access — it implied
Docker and granted access to nothing on the host — and drops podman
from the derived layer now that every built-in image ships it (#451).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:39:11 -04:00
didericis-claude ccd987a501 feat(egress): add preserve_auth flag to pass agent Authorization through
test / integration-docker (push) Successful in 11s
test / unit (push) Successful in 37s
lint / lint (push) Successful in 55s
Update Quality Badges / update-badges (push) Failing after 37s
test / integration-firecracker (push) Successful in 4m49s
test / coverage (push) Successful in 14s
test / publish-infra (push) Successful in 1m42s
Adds a per-route boolean field preserve_auth (default false) that skips
the gateway's Authorization header stripping for that host. Intended for
registry endpoints like Docker Hub (registry-1.docker.io) and GHCR
(ghcr.io) where the agent must supply its own per-scope bearer token.

Threaded through ManifestEgressRoute → EgressRoute → Route, serialized
in route_to_yaml_dict, and parsed in parse_routes. The strip at
egress_addon.py now checks route.preserve_auth before popping the header.

Closes #392
2026-07-21 15:01:59 -04:00
didericis-codex 2cd44cf79a fix(macos): persist gateway CA on host
test / integration-docker (push) Successful in 19s
Update Quality Badges / update-badges (push) Failing after 42s
lint / lint (push) Successful in 52s
test / unit (push) Successful in 1m43s
test / integration-firecracker (push) Successful in 4m56s
test / coverage (push) Successful in 17s
test / publish-infra (push) Successful in 1m49s
2026-07-21 15:01:38 -04:00
didericis-claude 8a1b833aaa fix(gateway): persist mitmproxy CA on the host, not a named volume (#450)
The shared gateway self-generates a mitmproxy CA that every bottle installs
to trust its TLS interception. It was persisted on a Docker named volume,
which survives `docker rm` but is silently wiped by `docker volume prune` /
`docker system prune --volumes` during routine host maintenance. When that
happens the gateway mints a fresh CA on restart, and every already-running
bottle fails the TLS handshake even after it re-resolves and reconnects to
the moved gateway — a re-attachment blocker distinct from #443/#445.

Move CA persistence to a host bind-mount under the app-data root
(`bot_bottle_root()/gateway-ca`, via `host_gateway_ca_dir()`), mirroring how
the shared DB and control-plane token already live on the host. Docker never
prunes a path under the root, and it stays inspectable + rotatable from the
host. mitmproxy already adopts an existing CA and generates one only on first
run, so the bind-mount gives adopt-existing/generate-on-first-run for free.

Add an explicit rollover path: `rotate_gateway_ca()` clears the persisted CA
so the next start remints it, and `python -m bot_bottle.orchestrator.rotate_ca`
wires that together with dropping the running gateway container (whose
mitmproxy still holds the old CA in memory). Rotation stays an operator action
— it doesn't auto-re-provision running bottles, which re-attach to pick up the
new anchor.

Scope: the Docker infra/gateway path (the "infra container" in the report).
The macOS (`container`-only volume) and Firecracker (VM-attached ext4) backends
persist the CA differently and are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:01:38 -04:00
didericis-codex 3b5c55bc8e test(git): cover provisioning failures
prd-number / assign-numbers (push) Failing after 17s
test / integration-docker (push) Successful in 19s
Update Quality Badges / update-badges (push) Failing after 44s
lint / lint (push) Successful in 52s
test / unit (push) Successful in 1m44s
test / integration-firecracker (push) Successful in 4m40s
test / coverage (push) Successful in 15s
test / publish-infra (push) Successful in 1m34s
2026-07-21 14:52:44 -04:00
didericis-codex 95220b4808 fix(firecracker): restore agent home ownership at boot 2026-07-21 14:52:44 -04:00
didericis-codex b032562d74 fix(firecracker): pin the provisioned Git config 2026-07-21 14:52:44 -04:00
didericis-codex 1d925172ec fix(firecracker): repair runtime Git config ownership 2026-07-21 14:52:44 -04:00
didericis-codex f6ae485b68 Revert "refactor(agent-images): use explicit Debian base"
This reverts commit 51b82f80d1.
2026-07-21 14:52:44 -04:00
didericis-codex e3258d0683 refactor(agent-images): use explicit Debian base 2026-07-21 14:52:44 -04:00
didericis-codex 0ff11d8ed7 fix(agent-images): own Git config directory 2026-07-21 14:52:44 -04:00
didericis-codex a970f974a2 fix(agent-images): retain SSH client 2026-07-21 14:52:44 -04:00
didericis-codex c845d3fed4 feat(agent-images): update Debian and add Podman 2026-07-21 14:52:44 -04:00
didericis 36fb019007 feat(cli): make bot_bottle.cli runnable with python -m
`python -m bot_bottle.cli` failed with "is a package and cannot be
directly executed" — the package had a `if __name__ == "__main__"` block
in `__init__.py`, which never fires for a package and made the invocation
look supported when it wasn't. Add a real `__main__.py` and drop the dead
block.

Matters for `bb login`, whose docstring documents a `bb` entry point that
nothing installs; `-m` is the closest thing to it until there's a
console-script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:25:38 -04:00
didericis-claude adc033a902 fix(login): narrow exception types and cover cleanup path
Replaces broad `except Exception` with specific types that reflect the
actual failure modes:
- _save_credentials: `except OSError` (IO-only path; re-raises for cleanup)
- _post error handler: `except (OSError, ValueError)` (network + bad JSON)
- poll-loop: `except (OSError, ValueError)` (network + bad JSON)

Also:
- Simplify _fake_get to use next(..., default) — removes uncovered
  StopIteration branch
- Add encoding="utf-8" to open() in test_approved_flow_returns_0
- Add test_cleanup_on_write_failure to cover the except OSError block
  in _save_credentials

All files score 10.00/10 on pylint (fail-under=10) and 0 errors on
pyright strict.
2026-07-21 14:25:38 -04:00
didericis-codex 21b253c7eb test(login): satisfy type and coverage gates 2026-07-21 14:25:38 -04:00
didericis-claude d4e2bc5f93 fix(login): atomic credential write and respect server poll_interval
Write credentials via a 0600 temp file + os.replace() so the token file
never appears at its final path with world-readable permissions, even if
the process is interrupted between write and chmod.

Parse poll_interval from the authorization response (clamped to 1–60 s,
falling back to _POLL_SLEEP) so aggressive polling can't trigger console
rate limits.

Tests: add atomicity spy asserting the temp file is 0600 before replace;
patch time.sleep instead of _POLL_SLEEP; add explicit interval-passthrough
assertion.
2026-07-21 14:25:38 -04:00
didericis-claude 4998a5ec6a feat: add bb login command for console host registration
Starts a device-authorization flow against a bot-bottle console, polls
until the operator approves, then writes access + refresh tokens to
$BOT_BOTTLE_ROOT/console.json. Console URL is read from --console-url
flag or BB_CONSOLE_URL env var.

Part of didericis/bot-bottle-platform#1
2026-07-21 14:25:38 -04:00
didericis-codex efd413c1ba test(macos): cover cached image selection
tracker-policy-pr / check-pr (pull_request) Successful in 6s
test / integration-docker (pull_request) Successful in 13s
test / unit (pull_request) Successful in 38s
test / integration-firecracker (pull_request) Successful in 3m19s
test / coverage (pull_request) Successful in 39s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (push) Successful in 13s
test / unit (push) Successful in 38s
lint / lint (push) Successful in 54s
Update Quality Badges / update-badges (push) Failing after 1m41s
test / integration-firecracker (push) Successful in 4m45s
test / coverage (push) Successful in 19s
test / publish-infra (push) Successful in 1m36s
2026-07-21 13:56:29 -04:00
didericis-claude d3d468532f fix(rebase): correct test_cached_lookup key and drop duplicate test
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 13:56:29 -04:00
didericis-claude ad2927b3b1 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 13:56:29 -04:00
didericis-claude 2582373490 fix(pyright): assert not None before accessing datetime attributes in tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-21 13:56:29 -04:00
didericis-claude 17ac1be93b 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 13:56:29 -04:00
didericis-codex c53254e9d5 fix(firecracker): honor cached image policy 2026-07-21 13:56:29 -04:00
didericis-claude 58ecd8cb90 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 13:56:29 -04:00
didericis-claude 310b36196d 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 13:56:29 -04:00
didericis-claude 137426d9ac 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 13:56:29 -04:00
didericis-claude 2a3a7dfb5c 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 13:56:29 -04:00
didericis-claude 16c12177d2 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 13:56:29 -04:00
didericis-claude 0f1734b823 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 13:56:29 -04:00
didericis-codex 2bf28e03f4 fix: make config store schema explicit 2026-07-21 13:56:29 -04:00
didericis-codex d3428b8c14 Add cached image quickstart 2026-07-21 13:56:29 -04:00
didericis-claude 4199de5e3e feat(claude): add forward_host_credentials support
tracker-policy-pr / check-pr (pull_request) Successful in 10s
test / integration-docker (pull_request) Successful in 17s
test / unit (pull_request) Successful in 1m41s
test / integration-firecracker (pull_request) Successful in 3m17s
test / coverage (pull_request) Successful in 21s
test / publish-infra (pull_request) Has been skipped
test / integration-docker (push) Successful in 19s
prd-number / assign-numbers (push) Failing after 23s
test / unit (push) Successful in 42s
Update Quality Badges / update-badges (push) Failing after 43s
lint / lint (push) Successful in 51s
test / integration-firecracker (push) Successful in 4m46s
test / coverage (push) Successful in 15s
test / publish-infra (push) Successful in 1m35s
Reads the host's Claude OAuth session key from ~/.claude.json at launch
and forwards it only to the egress sidecar (never to the agent), placing
a placeholder CLAUDE_CODE_OAUTH_TOKEN in the agent env so Claude Code
starts without seeing the real credential.

Mirrors the existing Codex forward_host_credentials flow (PRD 0029).
Adds claude_auth.py to extract and validate the sessionKey, a
CLAUDE_HOST_CREDENTIAL_TOKEN_REF constant in egress.py, and updates
manifest_agent.py to allow the flag for both 'codex' and 'claude'
templates. Also adds a mutual-exclusion check that rejects setting
both auth_token and forward_host_credentials together.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix(claude): read credentials from ~/.claude/.credentials.json

The actual OAuth token is in ~/.claude/.credentials.json under
claudeAiOauth.accessToken, not in ~/.claude.json.
~/.claude.json holds only UI state and profile metadata (oauthAccount
has no token fields). expiresAt in the credentials file is milliseconds,
not seconds.

Discovered after testing against Claude Code 2.1.198.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

fix(claude): fall back to macOS Keychain for credentials

On macOS, Claude Code stores credentials in the Keychain under
service "Claude Code-credentials" rather than in a file. When
~/.claude/.credentials.json is absent, shell out to:
  security find-generic-password -s "Claude Code-credentials" -w
and parse the result as the same JSON schema.

~/.claude.json holds only profile/UI metadata (oauthAccount has
no token fields). expiresAt in the credentials is milliseconds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

docs(prd): fix credential path references (~/.claude/.credentials.json)

fix(test): suppress gitleaks false positives on synthetic Claude tokens
2026-07-21 13:15:05 -04:00
didericis-codex 8348714e3e test(firecracker): cover published artifact reuse
tracker-policy-pr / check-pr (pull_request) Successful in 9s
test / integration-docker (pull_request) Successful in 16s
test / unit (pull_request) Successful in 1m37s
test / integration-firecracker (pull_request) Successful in 3m30s
test / coverage (pull_request) Successful in 15s
test / publish-infra (pull_request) Has been skipped
prd-number / assign-numbers (push) Failing after 23s
test / integration-docker (push) Successful in 15s
lint / lint (push) Successful in 58s
test / unit (push) Successful in 38s
Update Quality Badges / update-badges (push) Failing after 1m40s
test / integration-firecracker (push) Successful in 5m9s
test / coverage (push) Successful in 44s
test / publish-infra (push) Successful in 2m7s
2026-07-21 16:55:06 +00: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 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 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 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