Commit Graph

811 Commits

Author SHA1 Message Date
didericis c89847b626 fix(macos-container): tag the pinned base from its ref, not its ID
lint / lint (push) Successful in 59s
Update Quality Badges / update-badges (push) Successful in 1m6s
test / coverage (push) Successful in 22s
test / image-input-builds (push) Successful in 58s
test / integration-docker (push) Successful in 58s
test / unit (push) Successful in 2m51s
`container image tag` only accepts image-name[:tag] as its source and
rejects a bare 64-hex image ID ("cannot specify 64 byte hex string as
reference"), so pinning the agent image for the nested-containers layer
died before the build could start. Tag from the ref instead; the
existing post-tag inspection is what keeps the handoff fail-closed if
the ref moves between the two commands.

The unit test mocked subprocess and asserted the ID-as-source call
shape, so it never saw the CLI's rejection. It now pins the ref as the
source, and covers the bare-hex ID that `container image inspect`
actually reports plus the mid-flight tag move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 10:56:59 -04:00
didericis cd9f023f3d docs: name bot-bottle, not ./cli.py, in every instruction
prd-number-check / require-numbered-prds (pull_request) Successful in 11s
tracker-policy-pr / check-pr (pull_request) Successful in 14s
test / unit (pull_request) Successful in 59s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (push) Successful in 21s
test / image-input-builds (push) Successful in 44s
test / image-input-builds (pull_request) Successful in 1m15s
test / unit (push) Successful in 57s
test / coverage (pull_request) Successful in 20s
lint / lint (push) Successful in 1m3s
Update Quality Badges / update-badges (push) Successful in 1m12s
test / integration-docker (push) Successful in 1m0s
`doctor` told users to run `./cli.py backend setup`. cli.py is a four-line
wrapper at the repo root that calls bot_bottle.cli:main, and it does not ship —
[tool.setuptools.packages.find] includes only bot_bottle*, so anyone who
installed rather than cloned has no such file. Confirmed against a real
install: the user's tree contains exactly one executable, `bot-bottle`, and no
cli.py anywhere, while doctor recommended `./cli.py` three times. Invisible in
development, where ./cli.py works fine from a checkout, which is why only a
clean-install test surfaced it.

The two entry points are the same code, so the fix is to name the one that
always exists. 141 replacements across 45 files: the runtime messages that
caused this, plus README, docs, PRDs, research notes and test prose, so nothing
teaches the invocation a user cannot run.

scripts/demo.sh is deliberately untouched — it *executes* ./cli.py from a
checkout, where that is the correct and available path.

Verified end to end: a sandbox install now reports
"Run: bot-bottle backend setup --backend=firecracker", and bot-bottle is on
that user's PATH. Unit suite unchanged against the pre-existing baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 10:20:17 -04:00
didericis 5c12c6bf4e fix(doctor): survive a PATH entry the user can't execute
The install itself now works end to end on a fresh macOS account — venv built,
package installed from git, entry point linked — and `doctor` then died with an
unhandled traceback:

    PermissionError: [Errno 13] Permission denied: 'ip'

netpool's probe helpers caught only FileNotFoundError. That is not the only way
a probe binary can be unavailable: when a name on PATH exists but this user
cannot execute it, exec fails with EACCES, and CPython reports that in
preference to the ENOENT from the other PATH entries. So `except
FileNotFoundError` misses it and the crash propagates all the way out of
`doctor`. Reproduced directly: a mode-000 file named `ip` on PATH yields
exactly the error above.

_run_ok's docstring already stated the intent — treat an unavailable binary as
failure rather than crashing — so this widens the catch to OSError to match
what it says. Any OSError means the probe could not run, which for a
fail-closed check is indistinguishable from "not present". The same narrow
catch is fixed in overlapping_routes and in the two docker probes
(compose ls, docker ps), which are the same shape and equally reachable.

Deliberately not touched: the FileNotFoundError catches around file I/O in
bottle_state and orchestrator/service, where the narrow exception is correct.

The harness also required `bot-bottle` on PATH before running doctor, which
could never be true: install.sh prints the PATH line rather than editing a
shell profile, by design, so on a fresh account the entry point is installed
and working but not on PATH. It now looks where the installer actually puts
it (~/.local/bin, then the venv) and notes when it's running by absolute path.
That was the harness failing a run for a reason the installer intends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 09:38:39 -04:00
didericis 8d0782d1c7 fix(install): install into a private venv when pipx is absent
The harness's second run hit the wall the first one predicted: a fresh account
has no pipx, so install.sh fell to `pip install --user`, and every Python a Mac
offers — Homebrew and python.org alike — is externally managed, so PEP 668
blocked it. That fallback was never a fallback on macOS; it was a dead end that
printed instructions.

Replace it with a venv at ~/.bot-bottle/venv (BOT_BOTTLE_VENV to move it),
with the console script symlinked into ~/.local/bin. PEP 668 does not apply
inside a venv, and venv is stdlib, so unlike pipx there is nothing to bootstrap
first. pipx stays the preferred path when present, so anyone already managing
their Python apps that way is unaffected — and the post-install PATH check now
asks pipx for PIPX_BIN_DIR instead of assuming ~/.local/bin.

Keeping the venv under ~/.bot-bottle rather than ~/.local/share means the whole
footprint stays in one directory, which is what lets the throwaway-account
teardown remain a complete reset.

This removes the PEP 668 pre-flight and the sysconfig user-scheme lookup, both
of which existed only to serve the --user path. Their tests go with them:

* `detects_externally_managed_python` asserted the check that is now moot;
  replaced by one asserting pipx is still preferred when present.
* `checks_pip_usable_before_fallback` pinned a pip probe that no longer runs;
  replaced by one asserting the venv's own pip does the install, since using
  the base interpreter's would install outside the venv.
* `resolves_user_scripts_dir_not_hardcoded` and
  `macos_user_scheme_is_not_dot_local_bin` guarded the ~/Library/Python
  scripts-dir lookup. Nothing installs there now. The surviving "don't
  hardcode" concern is pipx's bin dir, which has its own test.

Five tests are added for the new path: the venv fallback exists, no --user path
survives, the venv is under the config dir, venv creation failure names
python3-venv (Debian ships it separately), and the entry point is exposed
outside the venv.

Verified end to end in a sandbox HOME with a fresh-account PATH and no pipx:
venv built, package installed, symlink created, `doctor` reached and green
(python 3.14.5, macos-container ready), exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 09:38:20 -04:00
didericis af293d7036 fix(install): find a usable python instead of dead-ending on PATH's
The macOS install harness caught this on its first real run: a throwaway
account gets `bot-bottle install: error: python3 3.11 or newer is required`
and stops. A fresh account's PATH is just /etc/paths, which excludes
/opt/homebrew/bin, so `python3` resolves to the Command Line Tools stub —
still 3.9.6 on macOS 26. Homebrew's shellenv line lives in the *installing*
user's ~/.zprofile and is inherited by nobody. The documented `curl … | sh`
path therefore dead-ends for anyone whose profile isn't already set up, which
is every new user, launchd job, and CI runner.

So look past PATH before giving up: try `python3`, then python3.11-3.14, then
/opt/homebrew/bin, /usr/local/bin, ~/.local/bin, and python.org framework
builds, and say which one was picked when it isn't the obvious one. On this
host that turns the failure into a successful install.

The chosen interpreter is now threaded through everything downstream — the pip
probe, the PEP 668 check, the pip --user fallback, and the user-scripts-dir
lookup — which were all still hardcoding `python3` and would otherwise have
run against the 3.9 stub we just rejected. `pipx install` gains `--python`,
since pipx otherwise builds the venv with whichever interpreter pipx itself
was installed with, not the one that passed the version check.

When nothing usable is found the error is now actionable: what was found and
why it's insufficient, where else it looked, a platform-appropriate install
command, and BOT_BOTTLE_PYTHON to point at an interpreter directly. An
explicit BOT_BOTTLE_PYTHON that is too old or unusable is an error rather than
a silent fallback to a different interpreter than the caller asked for.

Three existing tests asserted on incidental literals rather than the behaviour
they describe, and are narrowed to their actual intent:

* `never_uses_sudo` matched the word anywhere, including the new "sudo apt
  install python3.12" remediation *advice*. It now strips string literals and
  comments first, so it still catches sudo as a bare command, in a pipeline,
  and in a command substitution — verified by mutation — while allowing the
  script to print the word.
* `checks_pip_usable_before_fallback` pinned the literal `python3 -m pip`.
* `resolves_user_scripts_dir_not_hardcoded` banned ".local/bin" script-wide;
  it's now scoped to the USER_SCRIPTS assignment it exists to guard, since
  ~/.local/bin is a legitimate place to *find an interpreter*.

README grows the install command and a Requirements section it never had,
leading with the Python floor and why a working `python3` in your own shell
says nothing about what a fresh account sees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WEfZZhakx13bxTfXcZCoS5
2026-07-27 09:38:20 -04:00
didericis-codex 420184b874 fix: close consolidated quality review findings
test / image-input-builds (push) Successful in 44s
lint / lint (push) Successful in 1m3s
test / unit (push) Successful in 1m4s
test / integration-docker (push) Successful in 1m6s
test / coverage (push) Successful in 20s
Update Quality Badges / update-badges (push) Successful in 2m57s
2026-07-27 09:26:25 -04:00
didericis-codex 7938b90d19 fix: enforce shared storage permissions 2026-07-27 09:26:25 -04:00
didericis-codex 78d3b43061 fix: enforce cleanup and secret integrity 2026-07-27 09:26:25 -04:00
didericis-codex cdc5c8203d fix(cleanup): use authoritative resource identities 2026-07-27 09:26:25 -04:00
didericis-codex d04bbf1454 fix(gateway): contain output pump shutdown races 2026-07-27 09:26:25 -04:00
didericis-codex c00097d998 fix(gateway): bound stdlib HTTP request work 2026-07-27 09:26:25 -04:00
didericis-codex 6630a1f701 fix(cleanup): revalidate destructive backend plans 2026-07-27 09:26:25 -04:00
didericis-codex a7ad82f03a refactor(supervisor): separate MCP dispatch from transport 2026-07-27 09:26:25 -04:00
didericis-codex 9b8f126b8f refactor(egress): extract outbound DLP request stage 2026-07-27 09:26:25 -04:00
didericis-codex d167c03215 refactor(egress): extract request policy stages 2026-07-27 09:26:25 -04:00
didericis-codex 479b93bd0b fix(orchestrator): bound streamed request bodies 2026-07-27 09:26:25 -04:00
didericis-codex 7ebd067d2c build(orchestrator): pin FastAPI runtime dependencies 2026-07-27 09:26:25 -04:00
didericis-codex 38ff4a5e88 fix(orchestrator): satisfy adapter type contracts 2026-07-27 09:26:25 -04:00
didericis-codex b63c41db9c feat(firecracker): enumerate running bottles 2026-07-27 09:26:25 -04:00
didericis-codex 6c52686069 fix(orchestrator): bound unauthenticated HTTP requests 2026-07-27 09:26:25 -04:00
didericis-codex a52245af95 fix(security): authenticate persisted egress secrets 2026-07-27 09:26:25 -04:00
didericis-codex d4e48a4169 fix(docker): fail closed on network address scan errors 2026-07-27 09:26:25 -04:00
didericis-codex 53cc3ca492 fix(firecracker): abort cleanup on scan errors 2026-07-27 09:26:25 -04:00
didericis-codex bafb73fdb9 refactor(backend): type enumeration failures 2026-07-27 09:26:25 -04:00
didericis-codex b0f317c499 fix(docker): surface enumeration failures 2026-07-27 09:26:25 -04:00
didericis-codex 57eb4394dc fix(security): prohibit unauthenticated orchestrator 2026-07-27 09:26:25 -04:00
didericis-claude ed9fc76f97 fix(docker): only heal on the ParseAddr poison signature, not any inspect error
prd-number-check / require-numbered-prds (pull_request) Successful in 5s
test / unit (pull_request) Successful in 50s
tracker-policy-pr / check-pr (pull_request) Successful in 6s
test / image-input-builds (pull_request) Successful in 59s
test / integration-docker (pull_request) Successful in 1m6s
test / coverage (pull_request) Successful in 42s
Update Quality Badges / update-badges (push) Successful in 49s
lint / lint (push) Successful in 1m2s
test / coverage (push) Successful in 24s
test / integration-docker (push) Successful in 1m3s
test / unit (push) Successful in 48s
test / image-input-builds (push) Successful in 2m44s
Address review: the self-heal classified every non-absence `network inspect`
failure as a poisoned network and force-removed the shared gateway. But
inspect also fails on transient daemon/API errors, permission failures,
timeouts, or a bad context — destroying a healthy gateway on that guess would
tear the network out from under every live bottle.

Now the destructive path runs only for the known poison signature (docker's
`ParseAddr` error from the malformed `::1/64` IPv6 gateway). The absent case
(`No such network`) still just creates; any other inspect failure raises a
clear GatewayError without mutating shared state.

Adds a regression test asserting a generic inspect error issues neither
`docker rm --force` nor `docker network rm` and surfaces the error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 01:41:09 +00:00
didericis-claude bd8a146a46 fix(docker): heal a poisoned IPv6 gateway network, not just avoid creating one
test / integration-docker (pull_request) Successful in 1m15s
test / image-input-builds (pull_request) Successful in 1m14s
lint / lint (push) Successful in 3m28s
tracker-policy-pr / check-pr (pull_request) Failing after 10m28s
test / unit (pull_request) Failing after 10m39s
prd-number-check / require-numbered-prds (pull_request) Failing after 10m45s
test / coverage (pull_request) Has been skipped
PR #515 stopped bot-bottle from *creating* a gateway network with a
malformed IPv6 subnet (--ipv6=false), but it can't recover a network that
is *already* poisoned. On a daemon that default-enables IPv6, the fixed-name
`bot-bottle-gateway` network gets an `fdd0::/64` subnet whose `::1/64`
gateway trips docker's own netip.ParseAddr, so `docker network inspect`/`ls`
exit non-zero — poisoning every command that reads networks.

Such a network can survive on a shared runner from a pre-fix or concurrent
launch. `_ensure_network` never healed it: its migrate/recreate branch only
ran when `network inspect` *succeeded*, but a poisoned network makes inspect
*fail*, so the code fell through to `network create`, which no-ops on
"already exists" — leaving the poison in place. The next subnet read then
failed with ConsolidatedLaunchError (test_multitenant_isolation), and a bare
`docker network ls` failed too (test_orphan_cleanup).

Fix, two parts:
- gateway.py: when `network inspect` fails for a reason other than "no such
  network", treat the network as poisoned and force-remove + recreate it
  IPv4-only. Absent-vs-poisoned is distinguished by the inspect stderr.
- test.yml: add an integration-docker preflight that drops the leftover
  gateway network (and its container) before the suite, so direct `network
  ls`/`inspect` calls in tests are clean even on a daemon where the in-code
  heal can't run because inspect itself is what's broken.

Adds unit coverage for the poisoned-heal and the absent-create split.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 01:23:25 +00:00
didericis-claude 85fb6b0c98 fix(docker): disable IPv6 on the gateway network
test / coverage (push) Blocked by required conditions
test / unit (push) Successful in 55s
lint / lint (push) Successful in 1m5s
test / image-input-builds (push) Successful in 1m8s
test / integration-docker (push) Has been cancelled
Update Quality Badges / update-badges (push) Failing after 10m53s
On a docker daemon that default-enables IPv6 (default-address-pools),
creating the gateway network with only `--subnet` lets the daemon also
attach an fdd0::/64 IPv6 subnet. Its gateway is stored as `::1/64`,
which trips docker's own netip.ParseAddr in `network inspect`/`ls`:

  ParseAddr("fdd0:0:0:6::1/64"): unexpected character, want colon

That poisons every `_network_cidr`/`network ls` read and fails the
docker integration suite intermittently (whichever run the runner's
IPv6 pool index lands on a broken network). bot-bottle attribution
pins IPv4 source IPs and has no IPv6 support, so pass `--ipv6=false`
explicitly at network create to keep the gateway network IPv4-only
regardless of the daemon default.

Note: an already-poisoned runner still needs a one-time
`docker network rm bot-bottle-gateway` (and possibly a daemon
restart) to clear the malformed network.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:33:48 -04:00
didericis-codex 902286dbc0 fix(build): key nested images by base content
refresh-image-locks / refresh (push) Successful in 36s
lint / lint (push) Successful in 1m2s
test / image-input-builds (push) Successful in 1m6s
test / integration-docker (push) Successful in 1m12s
Update Quality Badges / update-badges (push) Successful in 56s
test / unit (push) Successful in 2m19s
test / coverage (push) Successful in 15s
2026-07-26 17:30:36 -04:00
didericis-codex 73c566f3ff fix(build): close remaining mutable image inputs 2026-07-26 17:30:36 -04:00
didericis-codex 33b7bcd082 build: centralize pinned base image arguments 2026-07-26 17:30:36 -04:00
didericis-codex 9e83ff1992 test: cover image input policy failure paths 2026-07-26 17:30:36 -04:00
didericis-codex 1d10595e5c build: make pinned image inputs portable 2026-07-26 17:30:36 -04:00
didericis-codex ff355f81de test: narrow pinned base match for type checking 2026-07-26 17:30:36 -04:00
didericis-codex e607af73ab test: assert reproducible provider image inputs 2026-07-26 17:30:36 -04:00
didericis-codex 5083b45f42 ci: reject mutable image build inputs 2026-07-26 17:30:36 -04:00
didericis-codex 24aeba9676 build: freeze operating system package inputs 2026-07-26 17:30:36 -04:00
didericis-codex 10d295eaf6 build: pin and verify image inputs 2026-07-26 17:30:36 -04:00
didericis-claude 72165b5db5 fix(git-gate): reject AGit review refs in pre-receive
tracker-policy-pr / check-pr (pull_request) Successful in 12s
test / unit (pull_request) Successful in 54s
test / integration-docker (pull_request) Successful in 1m9s
test / coverage (pull_request) Successful in 17s
prd-number-check / require-numbered-prds (pull_request) Successful in 8s
test / unit (push) Successful in 54s
Update Quality Badges / update-badges (push) Successful in 54s
lint / lint (push) Successful in 1m4s
test / integration-docker (push) Successful in 1m9s
test / coverage (push) Successful in 38s
Gitea AGit accepts pushes to refs/for/*, refs/draft/*, and
refs/for-review/* and opens pull requests backed by server-managed
refs/pull/<n>/head refs rather than ordinary refs/heads/* branches.
That breaks the git-gate branch workflow: follow-up commits can't be
pushed back through the branch, and Gitea rejects later direct updates
to the generated review ref, so recovery means recreating the PR.

Add a Phase 0 guard to the shared pre-receive hook that rejects
creation or update of those AGit review refs before any gitleaks scan
or upstream forward, with a message pointing callers at the
branch-backed PR workflow. Deletions (new == zero) stay allowed so
legacy AGit refs can still be cleaned up; normal branches and tags are
untouched.

Closes #506

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 17:59:21 +00:00
didericis-codex 1827593b89 test(docker): authenticate multitenant egress probes
prd-number-check / require-numbered-prds (pull_request) Successful in 9s
test / unit (pull_request) Successful in 53s
test / integration-docker (pull_request) Successful in 1m7s
test / coverage (pull_request) Successful in 17s
tracker-policy-pr / check-pr (pull_request) Successful in 32s
test / unit (push) Successful in 57s
lint / lint (push) Successful in 1m4s
test / integration-docker (push) Successful in 1m11s
test / coverage (push) Successful in 21s
Update Quality Badges / update-badges (push) Successful in 56s
2026-07-26 08:41:00 +00:00
didericis-codex 73e70e326c test(docker): wait for multitenant probe readiness
prd-number-check / require-numbered-prds (pull_request) Successful in 6s
tracker-policy-pr / check-pr (pull_request) Successful in 26s
test / unit (pull_request) Successful in 49s
lint / lint (push) Successful in 59s
test / integration-docker (pull_request) Failing after 1m1s
test / coverage (pull_request) Has been skipped
2026-07-26 08:38:16 +00:00
didericis-codex d744bec7b1 fix(docker): configure the attributed gateway subnet
tracker-policy-pr / check-pr (pull_request) Successful in 12s
prd-number-check / require-numbered-prds (pull_request) Successful in 42s
lint / lint (push) Successful in 1m4s
test / unit (pull_request) Successful in 57s
test / integration-docker (pull_request) Failing after 1m5s
test / coverage (pull_request) Has been skipped
2026-07-26 08:35:37 +00:00
didericis-codex f3fbfb3cc3 fix(ci): join control planes to the runner network
prd-number-check / require-numbered-prds (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 8s
test / integration-docker (pull_request) Failing after 35s
test / unit (pull_request) Successful in 52s
test / coverage (pull_request) Has been skipped
lint / lint (push) Successful in 1m7s
2026-07-26 08:30:57 +00:00
didericis-codex b2245ae1f3 test(orchestrator): make wrong-key coverage deterministic 2026-07-26 08:30:57 +00:00
didericis-codex 1022247ce5 test(ci): cover assurance gate entry points
prd-number-check / require-numbered-prds (pull_request) Successful in 7s
tracker-policy-pr / check-pr (pull_request) Successful in 8s
lint / lint (push) Successful in 58s
test / unit (pull_request) Successful in 48s
test / integration-docker (pull_request) Failing after 19m28s
test / coverage (pull_request) Has been skipped
2026-07-26 08:14:00 +00:00
didericis-codex 671d91070e docs(ci): document the assurance topology 2026-07-26 08:02:12 +00:00
didericis-codex 1c6d30ffd8 test(canary): verify the pinned gitleaks release 2026-07-26 08:01:07 +00:00
didericis-codex 583ff98b27 ci(docker): require the full integration suite 2026-07-26 08:00:15 +00:00
didericis-codex fafb828bb7 ci(coverage): enforce the critical core contract 2026-07-26 07:50:44 +00:00