Compare commits

...

3 Commits

Author SHA1 Message Date
didericis 241df1f835 fix(tests): fix integration test failures from deprecated git key, missing wget, and wrong prompt path
lint / lint (push) Successful in 1m26s
test / unit (pull_request) Successful in 32s
test / integration (pull_request) Successful in 45s
- test_sandbox_escape: migrate manifest fixture from deprecated `git`
  key to `git-gate` (PRD 0047) — `remotes` → `repos`, field names
  `Name`/`Upstream`/`IdentityFile` → `url`/`identity`
- test_smolmachines_launch probes: replace `wget` (not in node:22-slim)
  with `curl -s --show-error --max-time 3` (installed in Dockerfile.claude)
- test_smolmachines_launch prompt test: correct path /root/ → /home/node/
  to match guest_home in smolmachines/prepare.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 12:23:31 -04:00
didericis-claude 63a3b9b50a docs: remove pipelock references from README, examples, and test docs
lint / lint (push) Successful in 1m27s
test / unit (push) Successful in 33s
test / integration (push) Successful in 46s
Update Quality Badges / update-badges (push) Successful in 1m8s
Pipelock was removed in PR #193. Update the five remaining places
where current documentation (README, examples/bottles/claude.md,
tests/README.md, docs/ci.md, sidecar_bundle.py comment) still
described the old pipelock + cred-proxy topology.
2026-06-06 05:08:59 +00:00
Quality Badge Bot 7e6e0b1f5a chore: update quality badges
- Pylint: 9.92/10
- Pyright: 0 errors

[skip ci]
2026-06-06 05:03:57 +00:00
7 changed files with 49 additions and 56 deletions
+16 -24
View File
@@ -6,7 +6,7 @@
[![test](https://gitea.dideric.is/didericis/bot-bottle/actions/workflows/test.yml/badge.svg?branch=main)](https://gitea.dideric.is/didericis/bot-bottle/actions?workflow=test.yml)
[![pylint](https://img.shields.io/badge/pylint-9.92%2F10-brightgreen)](https://github.com/PyCQA/pylint)
[![pyright](https://img.shields.io/badge/pyright-18%20errors-brightgreen)](https://github.com/microsoft/pyright)
[![pyright](https://img.shields.io/badge/pyright-0%20errors-brightgreen)](https://github.com/microsoft/pyright)
**Problem:** Developer wants to run a coding agent without supervision, but they don't want a prompt injected or misbehaving agent wrecking their environment or exfiltrating sensitive data.
@@ -27,7 +27,7 @@
## Architecture
A bottle is two containers per agent: an `agent` container, and a `sidecars` container that bundles pipelock + cred-proxy + git-gate + supervise behind a Python init supervisor. They share a per-agent Docker `--internal` network; the agent has no default route off-box.
A bottle is two containers per agent: an `agent` container, and a `sidecars` container that bundles egress + git-gate + supervise behind a Python init supervisor. They share a per-agent Docker `--internal` network; the agent has no default route off-box.
```
host ( ./cli.py )
@@ -36,31 +36,25 @@ A bottle is two containers per agent: an `agent` container, and a `sidecars` con
┌─────────────────────────── bottle ──────────────────────────────────┐
│ │
│ ┌──────────────────┐ ┌──────────────
│ │ agent image │ HTTP(S) proxy │ cred-proxy │
│ │ (claude-code, │ ─────────────────►│ (strips/inj │
│ │ codex, etc) │ │ Authoriz.) │ │
│ │ │ └──────┬───────┘
│ │ environ: URLs │
│ │ only, no real │
│ │ tokens ┌────────────────┐ HTTPS to
│ │ │ │ pipelock image │──────────┼──► allowlisted
│ │ │ │ (TLS bump, DLP │ │ hosts (incl.
│ │ │ │ body scan, │ │ cred-proxy
│ │ │ │ allowlist) │ │ upstreams)
│ │ │ └────────────────┘ │
│ │ │ │
│ ┌──────────────────┐ ┌──────────────────────┐
│ │ agent image │ HTTP(S) proxy │ egress image
│ │ (claude-code, │ ─────────────────►│ (mitmproxy; TLS bump │ HTTPS to
│ │ codex, etc) │ │ DLP scan, path │───┼──► allowlisted
│ │ │ │ matching, auth hosts
│ │ environ: proxy │injection)
│ │ URLs only, no └──────────────────────┘
│ │ real tokens │
│ │ │ git proxy ┌────────────────┐ │ SSH push/fetch
│ │ │ ────────────────►│ git-gate image │──────────┼──► to bottle.git
│ │ │ │ (gitleaks + │ │ upstreams
│ └──────────────────┘ │ git daemon) │ │ (direct — not
│ └────────────────┘ │ via pipelock)
│ └────────────────┘ │ via egress)
│ │
│ agent on internal network (no default route); pipelock,
cred-proxy, and git-gate straddle internal + egress networks. │
pipelock is the single HTTP/HTTPS chokepoint — cred-proxy's
outbound traverses it too. git-gate's SSH egress is direct │
│ because pipelock is HTTP-only. │
│ agent on internal network (no default route); egress and
│ git-gate straddle internal + egress networks.
egress is the single HTTP/HTTPS chokepoint — all agent HTTP/HTTPS
traffic flows through it. git-gate's SSH egress is direct
│ because egress is HTTP-only.
└─────────────────────────────────────────────────────────────────────┘
```
@@ -104,8 +98,6 @@ egress:
auth:
scheme: token
token_ref: BOT_BOTTLE_GITEA_TOKEN
pipelock:
ssrf_ip_allowlist: [100.78.141.42/32]
---
The `gitea-dev` bottle. Provider auth via the inherited Claude route;
+1 -2
View File
@@ -14,8 +14,7 @@ import os
# Bundle image. Defaults to a built-locally tag (built from the
# repo's Dockerfile.sidecars via compose `build:`). Operators
# pinning to a published digest can override via env, matching
# the existing `BOT_BOTTLE_PIPELOCK_IMAGE` shape.
# pinning to a published digest can override via env.
SIDECAR_BUNDLE_IMAGE = os.environ.get(
"BOT_BOTTLE_SIDECAR_IMAGE",
"bot-bottle-sidecars:latest",
+6 -4
View File
@@ -22,7 +22,9 @@ mounted in. That topology breaks two assumptions those tests make:
`http://127.0.0.1:<host_port>` from inside the job time out.
The affected tests (`test_orphan_cleanup.test_create_and_remove`,
`test_pipelock_sidecar_smoke.test_smoke`) still run locally where the
test process and Docker daemon share a host. Making them work in CI
is a follow-up: either re-write them to discover container IPs via
`docker inspect`, or reconfigure the runner with host networking.
`test_sidecar_bundle_image.TestSidecarBundleImage`,
`test_sidecar_bundle_compose.TestSidecarBundleCompose`) still run
locally where the test process and Docker daemon share a host.
Making them work in CI is a follow-up: either re-write them to
discover container IPs via `docker inspect`, or reconfigure the
runner with host networking.
-2
View File
@@ -9,8 +9,6 @@ egress:
auth:
scheme: Bearer
token_ref: BOT_BOTTLE_CLAUDE_OAUTH_TOKEN
pipelock:
tls_passthrough: true
---
Common Claude provider boundary. Drop this file into
+16 -13
View File
@@ -11,16 +11,19 @@ tests/
fixtures.py # JSON manifest builders (shared)
_docker.py # docker-availability skip helper (shared)
unit/
test_pipelock_classify.py
test_pipelock_allowlist.py
test_pipelock_yaml.py
test_egress.py
test_egress_addon_core.py
test_manifest_egress.py
test_dlp_detectors.py
test_manifest_runtime.py
... # many others; see unit/ directory
integration/
test_pipelock_sidecar_smoke.py
test_sidecar_bundle_image.py
test_sidecar_bundle_compose.py
test_dry_run_plan.py
test_orphan_cleanup.py
canaries/
test_pipelock_image.py # opt-in; see below
...
canaries/ # opt-in; see below (currently empty)
```
Classification falls out of the directory — no hand-maintained list to
@@ -32,7 +35,7 @@ keep in sync.
python -m unittest discover -t . -s tests/unit -v # unit only
python -m unittest discover -t . -s tests/integration -v # integration only
python -m unittest discover -t . -s tests -v # both (recursive)
python -m unittest tests.unit.test_pipelock_yaml # one file
python -m unittest tests.unit.test_manifest_egress # one file
```
Discovery is invoked with `-t .` (top-level dir = repo root) so the
@@ -46,18 +49,18 @@ Discovery is invoked with `-t .` (top-level dir = repo root) so the
- `test_orphan_cleanup.py``network_remove` is idempotent against
missing resources, so the EXIT trap can call it unconditionally.
- `test_sidecar_bundle_image.py` — builds Dockerfile.sidecars and
probes that pipelock / gitleaks / mitmdump / supervise are all
reachable inside the bundle.
probes that gitleaks / mitmdump / supervise are all reachable
inside the bundle.
- `test_sidecar_bundle_compose.py` — end-to-end compose-up of an
agent + bundle pair; verifies the agent reaches the bundle via
the legacy network aliases.
## Canaries
`tests/canaries/` holds upstream-regression checks (e.g. the pinned
pipelock digest's binary still runs). These are gated on
`tests/canaries/` holds upstream-regression checks gated on
`BOT_BOTTLE_RUN_CANARIES=1` and not part of the per-push suite.
They're invoked by the scheduled `canaries` workflow.
They're invoked by the scheduled `canaries` workflow. Currently
no canaries are defined.
```bash
BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v
@@ -67,7 +70,7 @@ BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v
- `bot_bottle/ssh.py` end-to-end (would need a fake SSH host inside
the container).
- A live SSH-through-pipelock tunnel against a real Tailscale-style IP.
- A live SSH-through-git-gate tunnel against a real Tailscale-style IP.
- DLP false-positive measurements.
- TLS handling / cert pinning behavior.
+4 -5
View File
@@ -120,11 +120,10 @@ class TestSandboxEscape(unittest.TestCase):
# is intentionally unreachable — the pre-receive
# gitleaks hook must reject BEFORE git-gate
# attempts the upstream push.
"git": {"remotes": {
"unreachable.invalid": {
"Name": "throwaway",
"Upstream": "ssh://git@unreachable.invalid:22/throwaway.git",
"IdentityFile": str(cls._key_path),
"git-gate": {"repos": {
"throwaway": {
"url": "ssh://git@unreachable.invalid:22/throwaway.git",
"identity": str(cls._key_path),
},
}},
},
@@ -110,10 +110,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
# (high-numbered) so we're confirming TSI refusal, not
# just "no service listening."
r = self.bottle.exec(
"wget -T 3 -t 1 -O - http://127.0.0.1:9 2>&1 || true"
"curl -s --show-error --max-time 3 http://127.0.0.1:9 2>&1 || true"
)
# `wget` to a denied destination produces a connect error.
# The exact phrasing varies (busybox vs gnu); we assert
# `curl` to a denied destination produces a connect error.
# The exact phrasing varies by curl version; we assert
# the response is NOT the body of any real service.
self.assertNotIn("hello-from-vm", r.stdout)
self.assertTrue(
@@ -126,10 +126,10 @@ class TestSmolmachinesLaunch(unittest.TestCase):
def test_prompt_file_lands_in_guest(self):
# provision_prompt copies the host-side prompt.txt into the
# guest at /root/.bot-bottle-prompt.txt. The content
# guest at /home/node/.bot-bottle-prompt.txt. The content
# must match what the manifest declared so claude-code's
# --append-system-prompt-file reads the right text.
r = self.bottle.exec("cat /root/.bot-bottle-prompt.txt")
r = self.bottle.exec("cat /home/node/.bot-bottle-prompt.txt")
self.assertEqual(0, r.returncode, msg=r.stderr)
self.assertEqual(_AGENT_PROMPT, r.stdout.rstrip("\n"))
@@ -143,7 +143,7 @@ class TestSmolmachinesLaunch(unittest.TestCase):
# connect fails, which is the property chunk 3 will
# preserve once egress is actually running.
r = self.bottle.exec(
f"wget -T 3 -t 1 -O - http://{self.plan.bundle_ip}:9099 "
f"curl -s --show-error --max-time 3 http://{self.plan.bundle_ip}:9099 "
"2>&1 || true"
)
self.assertTrue(