Commit Graph

10 Commits

Author SHA1 Message Date
didericis-claude 847baa84be refactor(manifest): raise ManifestError instead of die()
test / unit (pull_request) Successful in 36s
test / integration (pull_request) Successful in 59s
Review feedback on #102: a manifest that can't be read should raise an
exception, not call die() (a SystemExit). That SystemExit was the whole
reason the dashboard had to special-case Die.

manifest.py now raises ManifestError (a plain Exception) for every
validation failure. The CLI dispatcher catches it and prints+exits 1
(same UX as before); the dashboard catches it with a normal
`except ManifestError` and degrades to a status-line warning. Manifest
tests assert on ManifestError + its message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 00:15:15 -04:00
didericis-codex c4449001d1 fix(dashboard): tolerate missing manifest
test / unit (pull_request) Successful in 25s
test / integration (pull_request) Successful in 44s
2026-05-28 18:44:42 -04:00
didericis-codex c08b09dc9f refactor!: rename project to bot-bottle
Assisted-by: Codex
2026-05-28 17:56:14 -04:00
didericis 1e5b0dcfca refactor: rename egress-proxy → egress everywhere
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m10s
The manifest key is `egress:` now; finish the rename so the rest of
the codebase matches. Files (Dockerfile.egress, claude_bottle/egress.py
etc.), classes (Egress, EgressConfig, EgressRoute, EgressPlan,
DockerEgress), constants (EGRESS_HOSTNAME, EGRESS_ROUTES, ...),
container name prefix (claude-bottle-egress-*), docker network alias
(egress), the introspection host (_egress.local), the MCP tool IDs
(egress-block, list-egress-routes), and the preflight label all drop
the `-proxy` suffix.
2026-05-25 21:59:47 -04:00
didericis 14c8a51c16 refactor(manifest): rename egress_proxy key to egress
test / unit (pull_request) Successful in 16s
test / integration (pull_request) Successful in 1m4s
Now that `bottle.egress` (the old allowlist/dlp_action block) is
gone, the longer `egress_proxy:` disambiguator isn't needed. The
manifest field reads more naturally as just `egress:` with the
same nested `routes: [...]` shape.

Renamed:
  - Manifest YAML key:    `egress_proxy:` → `egress:`
  - Bottle dataclass attr: `bottle.egress_proxy` → `bottle.egress`
  - `_BOTTLE_KEYS` entry, schema docstring, and all
    user-facing error message labels (`egress.routes[N]`,
    `egress has unknown key …`, etc.).

Kept (these refer to the egress-proxy SIDECAR, not the manifest
field):
  - File names: `egress_proxy.py`, `egress_proxy_apply.py`,
    `egress_proxy_addon.py`, `egress_proxy_addon_core.py`.
  - Class names: `EgressProxyConfig`, `EgressProxyRoute`,
    `EgressProxyPlan`, `EgressProxy`, `DockerEgressProxy`.
  - Helper names: `egress_proxy_manifest_routes`,
    `egress_proxy_routes_for_bottle`,
    `egress_proxy_token_env_map`, etc.
  - Constants: `EGRESS_PROXY_HOSTNAME`, `EGRESS_PROXY_ROLES`,
    `EGRESS_PROXY_AUTH_SCHEMES`, `EGRESS_PROXY_FORWARD_PROXY`,
    `EGRESS_PROXY_INTROSPECT_URL`, `EGRESS_PROXY_PORT`, etc.
  - Container name prefix `claude-bottle-egress-proxy-*`, the
    `egress-proxy` docker network alias, the
    `egress-proxy-block` + `list-egress-proxy-routes` MCP tool
    IDs, the `egress-proxy` audit-log component label.

Local bottle migrated (`~/.claude-bottle/bottles/dev.md` already
updated). The legacy `egress_proxy` key isn't surfaced anywhere
anymore; the generic unknown-key validator catches typos with a
"did you mean: egress, env, git, supervise" hint.

409 unit + integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 21:25:51 -04:00
didericis 6456904763 refactor(manifest): drop bottle.egress field, egress_proxy is the only allowlist
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m4s
Goal: one allowlist surface (egress_proxy.routes), no second
free-form `egress:` knob. Anything that used to live there now
goes in `egress_proxy.routes` as a bare-pass entry
(`- host: <name>`).

Removed:
  - `BottleEgress` dataclass + DLP_ACTIONS constant + bottle.egress
    field on `Bottle`.
  - `pipelock_bottle_allowlist` helper.
  - `pipelock_allowlist_summary` helper (the compact preflight
    summary stopped using it after PR #31).
  - `allowlist_summary` field on `DockerBottlePlan`.
  - `bottle.egress.allowlist` folding in
    `egress_proxy_routes_for_bottle` — only DEFAULT_ALLOWLIST
    auto-folds now.
  - The two-branch logic in `pipelock_effective_allowlist`
    (egress-proxy-present vs not) — pipelock now just mirrors
    `egress_proxy_routes_for_bottle` unconditionally.

Hard-coded:
  - `request_body_scanning.action = "block"` in
    `pipelock_build_config` (was driven by
    `bottle.egress.dlp_action`). The previous default was already
    "block" — the knob to switch to "warn" was a foot-gun in a
    sandboxed agent context, so it's gone.

Tests:
  - `test_pipelock_allowlist.py` rewritten to assert the
    mirrored-from-egress-proxy semantics directly.
  - `test_manifest_md_load.py`, `test_pipelock_yaml.py`,
    `test_egress_proxy.py` fixtures migrated to put hosts in
    `egress_proxy.routes` instead of `egress.allowlist`.

Local bottle migrated too: `~/.claude-bottle/bottles/dev.md`
loses the `egress: { allowlist: [example.com] }` block, picks up
a bare-pass `- host: example.com` route.

409 unit + integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 21:12:56 -04:00
didericis 4abea282e0 revert(egress-proxy): drop Role + agent provisioner (keep git-push block)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m3s
Partial revert of fa06a3a. The role + agent-side provisioner felt
overengineered: anthropic-base-url + npm-registry's only realistic
host values match the tool defaults, so the role tags drove no-op
dotfile writes most of the time. If non-default npm registry / tea
config is needed in a future bottle, we can ship it through a more
direct mechanism then.

What stays from fa06a3a:
  - Universal HTTPS git-push block in the egress-proxy addon
    (`is_git_push_request` in egress_proxy_addon_core, called from
    the request hook before route matching; 403s git-receive-pack
    regardless of route). This is the security backstop so git-gate
    remains the only outbound write path; PR #29 keeps it.

What gets reverted:
  - `Role` field on EgressProxyRoute (manifest + runtime).
  - `EGRESS_PROXY_ROLES` + `EGRESS_PROXY_SINGLETON_ROLES` constants
    and singleton-role validation.
  - `backend/docker/provision/egress_proxy.py` (npmrc + tea config).
  - `provision_egress_proxy` slot in `BottleBackend.provision`.
  - `prepare.py`'s role-based ANTHROPIC_BASE_URL detection (back to
    the token_ref="CLAUDE_CODE_OAUTH_TOKEN" auto-detect).
  - Manifest + provisioner tests for the above.

355 unit + 24 integration tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 15:02:15 -04:00
didericis fa06a3a0ab feat(egress-proxy): block HTTPS git push + restore role provisioner
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m1s
Two related fixes on top of PR #29's chunk-2 cutover:

1. Universal HTTPS git-push block in the egress-proxy addon
   (`is_git_push_request` in egress_proxy_addon_core, called from the
   mitmproxy request hook before route matching). 403s any
   `/git-receive-pack` or `info/refs?service=git-receive-pack` —
   defense in depth so git-gate (PRD 0008) remains the only outbound
   path for writes, gitleaks-scanned by its pre-receive. Replicates
   cred-proxy's `is_git_push_request` behavior.

2. Restored agent-side role provisioner. Brings back `Role` on
   EgressProxyRoute (manifest + runtime) with three roles —
   `anthropic-base-url`, `npm-registry`, `tea-login`. Singleton
   constraint on the first two carries over from cred-proxy.
   `git-insteadof` is intentionally absent (option 1 above handles
   the push-bypass concern, and the canonical-URL rewrite has no
   function when egress-proxy is on HTTPS_PROXY).

   The provisioner (`backend/docker/provision/egress_proxy.py`):
     - `~/.npmrc` registry= the canonical upstream URL.
     - `~/.config/tea/config.yml` logins[] entry per tea-login route.
     - `ANTHROPIC_BASE_URL` env set in prepare.py based on the
       anthropic-base-url role (was a token_ref="CLAUDE_CODE_OAUTH_TOKEN"
       check in this PR's earlier draft — the role marker is cleaner
       and matches the cred-proxy precedent the user wants kept).

   All three dotfile values point at canonical upstream URLs; the
   agent's HTTPS_PROXY=egress-proxy routes them through the proxy
   automatically.

Tests: 11 new role-validation tests, 11 new provisioner-render tests,
the chunk-1 manifest fixture exercise role=anthropic-base-url. 400
tests pass (was 376).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:48:13 -04:00
didericis 70f773ac61 feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2)
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m3s
Hard cutover. cred-proxy is deleted; egress-proxy is now the agent's
HTTP_PROXY (when routes are declared) with pipelock on its outbound
leg. Two per-bottle CAs are minted: egress-proxy's (agent trust
store) and pipelock's (egress-proxy's outbound trust store).

Manifest:
  - `bottle.cred_proxy` → hard error with a migration recipe.
  - `bottle.egress_proxy` is the new shape (PRD 0017 chunk 1).
  - CredProxy* types + role validators removed.

Wiring:
  - launch.py: `egress_proxy_tls_init` mints the egress-proxy CA
    (cert+key concat for mitmproxy + cert-only for agent trust);
    `DockerEgressProxy.start` docker-cps both CAs in, sets
    `HTTPS_PROXY=pipelock` + `EGRESS_PROXY_UPSTREAM_CA` so mitmdump
    trusts pipelock's MITM. Agent's HTTP_PROXY points at
    egress-proxy when routes exist, else falls back to pipelock
    (no-routes bottles unchanged).
  - prepare.py / backend.py: `cred_proxy` arg → `egress_proxy`;
    sidecar-orphan probe + plan field + dashboard view all
    renamed.
  - provision_ca: selects the egress-proxy CA when present, else
    pipelock's (filename renamed to claude-bottle-mitm-ca.crt).
  - bottle.provision: cred-proxy dotfile rewrites (~/.npmrc,
    ~/.gitconfig insteadOf, tea config) are gone — HTTP_PROXY
    catches everything respecting it.

Pipelock helpers:
  - `pipelock_token_hosts` → `pipelock_route_hosts` (now reading
    egress_proxy.routes).
  - cred-proxy hostname auto-allow → egress-proxy hostname
    auto-allow.
  - Anthropic seed-phrase workaround now triggers when an
    egress_proxy route targets api.anthropic.com (was based on the
    cred-proxy `anthropic-base-url` role).

Dockerfile.egress-proxy:
  - Entrypoint conditionally passes
    `--set ssl_verify_upstream_trusted_ca=$EGRESS_PROXY_UPSTREAM_CA`
    (via the `${VAR:+...}` shell expansion) so standalone runs without
    a mounted pipelock CA still boot.
  - mkdirs `/home/mitmproxy/.mitmproxy` ahead of `docker cp`.

Deleted: claude_bottle/{cred_proxy,cred_proxy_server}.py,
backend/docker/{cred_proxy,provision/cred_proxy}.py,
Dockerfile.cred-proxy, plus the corresponding unit + integration
tests. backend/docker/cred_proxy_apply.py stays as a stub for
chunk 3 to rewrite (its container-name + routes-path constants
are inlined so it survives without the deleted module).

Test changes:
  - test_pipelock_allowlist rewritten against egress-proxy routes
    + the new `pipelock_route_hosts`.
  - test_manifest_md_load + test_pipelock_yaml + test_yaml_subset
    fixtures migrated to the `egress_proxy: { routes: [...] }`
    shape.
  - test_supervise_sidecar's round-trip test switched from
    `dashboard.approve` to `dashboard.reject`: the approval-apply
    path on cred-proxy-block proposals hits a deleted sidecar in
    chunk 2's transitional state. Chunk 3 restores the approval
    test once the remediation flow is retargeted at egress-proxy.

376 tests pass (was 427; net delta is removed cred-proxy tests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:30:39 -04:00
didericis 6ba5f9a9d3 feat(manifest): per-file MD directory loader (PRD 0011)
test / unit (pull_request) Successful in 13s
test / integration (pull_request) Successful in 22s
Manifest.resolve walks $HOME/.claude-bottle/{bottles,agents}/ and
$CWD/.claude-bottle/agents/ instead of reading claude-bottle.json.
A bottles/ subdir under $CWD is logged as a warn and ignored —
the filesystem layout IS the trust boundary, no resolver check
needed.

If claude-bottle.json exists alongside no .claude-bottle/ dir at
either location, dies with a clear pointer at the README — the
manifest format changed and we don't silently fall back.

Manifest.from_md_dirs(home, cwd) is the programmatic entry point
tests use to build a Manifest from fixture directories without
touching os.environ. Manifest.from_json_obj is preserved for
tests that still want to build manifests in-memory.

Bottle / agent frontmatter goes through Bottle.from_dict /
Agent.from_dict — same validators as today's JSON path. Unknown
top-level frontmatter keys die with a "did you mean" pointer
listing accepted keys. Filenames that don't match [a-z][a-z0-9-]*
are skipped with a warn.

Agent files accept the Claude Code subagent passthrough fields
(name, description, model, color, memory) so the same file can
drop into ~/.claude/agents/ — claude-bottle ignores them at
launch but doesn't reject.

The dry-run integration test ships a real MD fixture tree now;
all 200 unit + 17 integration tests stay green.
2026-05-24 22:15:02 -04:00