80da66fd5dbb28ff0a80cdc0a7ebecffd01bc38b
508 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b9f6889d09 | Merge pull request 'refactor(state): write prepare-time scratch files under state/<slug>/' (#34) from chunk-2-state-bind-mount into main | ||
|
|
cd82a48399 |
refactor(state): write prepare-time scratch files under state/<slug>/
PRD 0018 chunk 2. Each sidecar's prepare-time output (pipelock yaml + CAs, egress routes.yaml + CAs, git-gate entrypoint + hooks, supervise current-config, agent env + prompt) now lands in ~/.claude-bottle/state/<slug>/<service>/ instead of an ephemeral mktemp dir. The state subdirs become the stable bind-mount sources that chunk 3's docker compose project will reference. The SDK launch path is unchanged — `docker cp` still copies from the plan-held paths into containers, just from new locations. start.py's session-end cleanup is now in `finally`, which also reaps state dirs left behind by dry-run / preflight-N / prepare-exception paths (previously only the post-launch path settled state). |
||
|
|
c8c302e50e | Merge pull request 'docs(prd-0018): one compose project per bottle instance' (#33) from compose-per-instance into main | ||
|
|
3386cabe62 | docs(prd-0018): resolve TTY open question — keep exec -it | ||
|
|
4760a09263 |
feat(compose): pure renderer for bottle plan -> compose dict
PRD 0018 chunk 1. New module `claude_bottle/backend/docker/compose.py` exposing `bottle_plan_to_compose(plan) -> dict` — a pure function that translates a fully-resolved DockerBottlePlan into a Compose v2 spec. Not wired in yet. Tests cover the conditional-service matrix (git on/off × egress on/off × supervise on/off) plus per-service shape (images vs builds, network aliases, bind mounts, env vars, depends_on). |
||
|
|
3251ee1394 |
docs(prd-0018): one compose project per bottle instance
Draft a PRD that replaces the chain of per-sidecar docker SDK calls in `claude-bottle start` with a single `docker compose` project per instance. Each `state/<slug>/` dir gets a self-describing set of artifacts: metadata.json, docker-compose.yml, compose.log, and the existing transcript/ + live-config/. |
||
|
|
a77545dc91 | Merge pull request 'refactor(manifest): drop bottle.egress field' (#32) from drop-egress-field into main | ||
|
|
1e5b0dcfca |
refactor: rename egress-proxy → egress everywhere
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. |
||
|
|
14c8a51c16 |
refactor(manifest): rename egress_proxy key to egress
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>
|
||
|
|
6456904763 |
refactor(manifest): drop bottle.egress field, egress_proxy is the only allowlist
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>
|
||
|
|
d79a976999 | Merge pull request 'refactor(preflight): compact y/N summary' (#31) from compact-preflight-summary into main | ||
|
|
572106d98f |
refactor(cli): drop --format=json end-to-end
Companion to the compact preflight in #31 — the JSON format was the structured alternative to the verbose text summary. With the new compact text already on screen, no consumer was using the JSON shape, and the abstract `BottlePlan.to_dict` was the biggest piece of API surface no one is implementing against. Removed: - `--format` CLI flag from `start` and `resume`. - `output_format` kwarg from `_launch_bottle`. - `BottlePlan.to_dict` abstract method. - `DockerBottlePlan.to_dict` (60-line dict builder). - The `_PlanView` dataclass — `print` was the only remaining caller, so the env-name computation is inlined. - `tests/integration/test_dry_run_plan.py` (JSON-shape integration test). - `tests/unit/test_cli_start_format.py` (flag-conflict unit). Plan-introspection is still possible by reading the `DockerBottlePlan` dataclass directly — fields like `image`, `container_name`, `stage_dir`, `use_runsc` are all there. Tooling that needs a stable wire shape can JSON-serialize the dataclass themselves. 411 unit + integration tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5d5f118fb4 |
refactor(preflight): compact summary — agent / env / skills / bottle / gates
Trim the y/N preflight to the parts the operator actually scans
before pressing y:
agent
env (one per line)
skills (one per line)
bottle
git gate (one upstream per line)
egress-proxy (one route per line, with [auth:scheme] when set)
Dropped from the display (still on the plan dataclass / json
output for tooling): image, dockerfile, derived-image (cwd) line,
container, stage dir, docker runtime, git remotes list, egress
allowlist summary, tls interception note, supervise note, prompt
metadata, remote-control flag.
`remote_control` kwarg kept on `.print()` for callsite stability
but unused in the compact format.
A `_multi(label, values)` helper does the "first value next to
the label, remainder continuation-indented" pattern that env /
skills / git gate / egress-proxy all share — keeps the columns
aligned to the label width.
Verified against my own dev bottle: output is byte-for-byte the
spec the operator asked for.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
5edff68d52 | Merge pull request 'feat(egress-proxy): retarget remediation flow (PRD 0017 chunk 3)' (#30) from egress-proxy-block-remediation into main | ||
|
|
6c886200d9 |
revert(egress-proxy): drop wildcard host support entirely
The apex-vs-subdomain question, the cert/SNI mismatch when pipelock-passthrough hosts have wildcard certs, and the mirror-divergence corner cases stacked up faster than the feature earned its keep. Going back to exact-host match only. Addon (`match_route`): single pass, case-insensitive exact match. `*.foo.com` in a route table is now a literal string that won't match anything — operators that want subdomains declare them individually. Pipelock mirror (`_pipelock_safe_hosts`): silently drops hosts that don't fit pipelock's `[A-Za-z0-9_.-]+` charset (wildcards, IPv6 literals, stray chars). Previously normalised wildcards to their suffix; now just drops them, which matches egress-proxy's behavior of not matching them either. 8 wildcard test cases removed; 2 lightweight "wildcards are not supported" assertions retained as documentation. 386 unit pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
6177c0518e |
fix(egress-proxy-addon): wildcard hosts also match the apex
`*.example.com` now matches `example.com` itself in addition to
every subdomain. RFC 6125 TLS-wildcard semantics excluded the
apex; an allowlist's natural reading of `*.example.com` is "all
of example.com" — and the pipelock mirror already strips
`*.example.com` to `example.com`, so without the apex match the
two layers disagreed (pipelock allowed the apex, egress-proxy
blocked it).
Behavior:
- `*.example.com` matches `example.com` (apex)
- `*.example.com` matches `foo.example.com` (subdomain)
- `*.example.com` matches `a.b.example.com` (nested)
- `*.example.com` does NOT match `barexample.com` (label
boundary required)
Test renamed: `test_wildcard_does_not_match_apex` →
`test_wildcard_matches_apex`. 395 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
811a6fbfe9 |
feat(egress-proxy-addon): wildcard host matching with exact-match precedence
PRD 0017 v1 deliberately punted wildcards ("Exact match in v1 —
globs / wildcards are a follow-up"). Now that the supervise mirror
strips `*.` to its suffix for pipelock, the addon needs to actually
match wildcard hosts on its side or the route is dead weight.
Addon `match_route` now does two passes:
1. Exact (case-insensitive) literal match on the hostname.
2. Wildcard suffix match: a route whose host starts with `*.`
matches any request host that ends with `.<suffix>`. So
`*.example.com` matches `foo.example.com` and
`a.b.example.com`, but NOT the apex `example.com` and not
`barexample.com` (the leading `.` of the suffix is
required).
Exact wins — operators can layer a specific route (e.g.
`api.github.com` with auth) on top of a broader wildcard (e.g.
`*.github.com` bare-pass).
8 new unit tests: direct subdomain match, nested subdomain match,
apex rejection, overlapping-suffix rejection, case-insensitive,
exact-wins-over-wildcard (both route orders), no-match
fall-through. 395 unit + integration pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
e26fe874e4 |
fix(egress-proxy-apply): wildcard hosts normalise to suffix in pipelock mirror
Previous fix stripped wildcard hosts entirely from the pipelock
mirror; the operator wanted the suffix kept so pipelock pins the
base hostname. Now `*.example.com` becomes `example.com` in the
mirror — egress-proxy keeps the wildcard for its own host match,
pipelock allows the suffix.
Behavior change:
- `*.example.com` → `example.com` (was: dropped)
- `*.foo.bar.com` → `foo.bar.com` (one `*.` strip, not
recursive)
- `*` → dropped (normalises to empty)
- `example.com` → `example.com` (unchanged)
- `[::1]`, etc. → dropped (still off pipelock's
charset after any prefix
strip)
Adds explicit de-dup so `*.example.com` + `example.com` collapse
to one entry. Existing wildcard-strip test reshaped + 3 new
edge-case tests.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
93f7d248f6 |
fix(egress-proxy-apply): strip pipelock-incompatible hosts from mirror
Pipelock's allowlist parser only accepts `[A-Za-z0-9_.-]+` literal hostnames. Wildcard routes (`*.example.com`) that egress-proxy's route table accepts trip pipelock's parser the moment the mirror tries to render them into the new yaml; the whole apply fails before pipelock is even touched. Symptom: operator approves an egress-proxy-block proposal, gets "pipelock allowlist mirror failed: allowlist line N: '<wildcard>' has disallowed characters." Fix: `_mirror_hosts_to_pipelock` filters through `_pipelock_safe_hosts` before merging — anything outside pipelock's allowed charset is silently skipped. Wildcard routes stay live on egress-proxy; pipelock just won't pin a hostname for the wildcard-matched traffic (caller's call to accept the hostname-only enforcement gap there). Adds 4 unit tests covering normal hostnames pass-through, wildcard stripping, IPv6-literal stripping, and order preservation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
db1b523881 |
fix(egress-proxy-apply): correct misleading "egress-proxy updated" wording
`_mirror_hosts_to_pipelock` runs BEFORE the egress-proxy write in
`apply_routes_change` — if it raises, egress-proxy is left intact.
The error message claimed the opposite ("egress-proxy routes
updated but pipelock allowlist mirror failed"), pointing the
operator at the wrong half-state.
Reword to make the actual state clear: pipelock failed,
egress-proxy NOT updated, fix pipelock manually with
`pipelock edit <bottle>` then retry.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1542ee0b93 |
feat(egress-proxy-block): single-route input + merge-on-apply
Instead of asking the agent to compose and submit a full routes
file, the tool now takes ONE proposed route — host + optional
path_allowlist + optional auth — and the supervisor merges it
into the live routes table at approval time. The agent no longer
needs to fetch / reproduce / extend the existing allowlist; it
just describes the host it wants reachable.
Tool input (new):
- `host` (required)
- `path_allowlist` (optional, array of absolute path prefixes)
- `auth` (optional, {scheme, token_ref})
- `justification` (required)
Merge semantics (in `egress_proxy_apply._merge_single_route`):
- Host NOT in current routes → append the proposed route as a
new entry. If `auth` is set, assign the next EGRESS_PROXY_TOKEN_N
slot.
- Host already present → union the proposed `path_allowlist`
with the existing one (proposed entries appended after
existing, deduped). Existing `auth_scheme` / `token_env`
preserved; proposed `auth` ignored (operator-controlled, not
agent-controlled).
- Hostname comparison is case-insensitive.
Dashboard wiring: `approve()` on an egress-proxy-block proposal
now calls `add_route(slug, proposed_route_json)` instead of
`apply_routes_change(slug, full_file)`. add_route fetches the
current routes from the running egress-proxy, merges, and calls
apply_routes_change with the merged content — so the
pipelock-mirror + SIGHUP plumbing from chunk 3 still runs
end-to-end. Audit diff still captures the full-file before/after.
Tool description rewritten to make the new shape obvious and to
stop pointing the agent at the routes file. The
`list-egress-proxy-routes` tool stays available for agents that
want to see what's currently allowed.
Tests: 9 new `_merge_single_route` cases (host absent/present,
path-allowlist union+dedup, auth-slot indexing, case-insensitive
match, existing-auth preservation, missing-host rejection,
malformed-current rejection). 407 unit + integration pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
3be70eb07a |
feat(supervise): list-egress-proxy-routes MCP tool, defaults on egress-proxy
Reshape the allowlist topology so the egress-proxy is the bottle's
single allowlist surface, and replace the agent-side
routes/allowlist file mounts with a live MCP tool.
Policy change (move defaults to egress-proxy):
- `egress_proxy_routes_for_bottle(bottle)` now folds in
DEFAULT_ALLOWLIST (the claude-code defaults) and
`bottle.egress.allowlist` (user adds) as bare-pass routes (no
auth, no path filter), on top of the bottle's
`egress_proxy.routes`. Manifest routes win on host collision.
- `pipelock_effective_allowlist(bottle)` mirrors egress-proxy's
effective host set when egress-proxy is in use. Pipelock is
no longer the bottle's primary allowlist authority; it
enforces a downstream copy as defense-in-depth + does DLP body
scanning.
- Split out `egress_proxy_manifest_routes(bottle)` for callers
that want just the manifest entries (tests, internal use).
- DEFAULT_ALLOWLIST moves from `pipelock.py` to `egress_proxy.py`
(pipelock re-imports for the no-egress-proxy fallback path).
- Dropped the `egress-proxy` auto-allow on pipelock's allowlist
— the agent never dials egress-proxy via the proxy mechanism;
pipelock only sees upstream hostnames from egress-proxy's
CONNECTs.
Introspection endpoint (existing mitmproxy feature):
- Egress-proxy addon recognises requests to the magic host
`_egress-proxy.local` and synthesizes responses via
`flow.response = http.Response.make(...)` — no upstream
connection, no allowlist enforcement on the magic host.
- `GET /allowlist` returns the in-memory route table as JSON
(host + path_allowlist + auth_scheme + token_env per route;
no token VALUES).
- Smoke-tested end-to-end against a real egress-proxy container.
MCP tool (existing supervise plumbing):
- New `list-egress-proxy-routes` tool (no inputs, no operator
approval). Handler fetches via egress-proxy's introspection
endpoint using urllib's ProxyHandler against
`EGRESS_PROXY_FORWARD_PROXY`. Returns the JSON payload as the
tool's text content; `isError: true` if the proxy is
unreachable.
- `egress-proxy-block` description now points the agent at
`list-egress-proxy-routes` instead of a staged file path.
- `pipelock-block` description acknowledges the mirror — agents
should prefer `egress-proxy-block` to add hosts; pipelock-block
stays for the rare divergence case.
Drop agent-side file mounts:
- Supervise's `current-config` dir staging no longer writes
routes.yaml / allowlist. Only `Dockerfile` remains
(capability-block still reads it from
`/etc/claude-bottle/current-config/Dockerfile`).
- `prepare.py` stops passing `routes_content` /
`allowlist_content` to `supervise.prepare`.
- `Supervise.prepare` signature simplified to one
`dockerfile_content` kwarg.
Tests: 400 unit + integration pass. Added coverage for
defaults-folding (`TestRoutesForBottleFoldsDefaults`), the new
tool definition + handler, and the updated supervise.prepare
shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1cec0d9aa6 |
feat(egress-proxy-apply): mirror new route hosts into pipelock allowlist
When the operator approves an egress-proxy-block proposal that
adds a host to egress-proxy's routes, the request would still 403
downstream at pipelock — pipelock's hostname allowlist is set at
bottle launch and doesn't learn about routes added later. The
agent saw "Approved" but the very next retry still failed.
Fix: `apply_routes_change` now mirrors every host in the proposed
routes onto pipelock's allowlist before flipping egress-proxy.
Order matters — pipelock first so a pipelock failure doesn't
leave egress-proxy in a half-state:
1. Validate the new routes content.
2. Extract the hosts.
3. Merge them onto pipelock's current allowlist
(`apply_allowlist_change` — restarts pipelock with the merged
yaml). No-op when every host is already present.
4. docker cp the new routes.yaml into egress-proxy + SIGHUP.
If pipelock's restart fails, egress-proxy is untouched and the
operator gets a clear error pointing at the pipelock half-state.
If egress-proxy's update fails after pipelock succeeded, pipelock
just has the host pre-allowlisted — harmless extra-permissive
until the operator retries.
Adds `_hosts_in_routes` helper using the addon's own parser
(so the mirrored host set matches exactly what the addon will
match on). 4 new unit tests; 368 total pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
d75d5f3e48 |
fix(egress-proxy-apply): chmod tmp file 0644 so mitmproxy can read post-cp
apply_routes_change wrote the proposed routes via
`tempfile.mkstemp` (default mode 0600) then `docker cp`'d into the
egress-proxy container. docker cp preserves mode + host uid, so
the file landed inside the container as 0600 owned by the host
user's uid — which is not the mitmproxy user (uid 1000) the
addon runs as. The SIGHUP-triggered reload then failed with
PermissionError on the re-read, the old routes table stayed in
memory, and the operator-approved route never took effect.
Symptoms reported:
- Operator approves egress-proxy-block proposal that adds
google.com to routes.
- Agent retries `curl https://google.com` and still gets 403
"egress-proxy: host 'google.com' is not in the bottle's
egress_proxy.routes allowlist."
- `docker exec <egress-proxy> cat /etc/egress-proxy/routes.yaml`
returns "Permission denied" (mitmproxy user can't read it,
so the reload couldn't either).
Fix: chmod 0644 on the host tmp file before docker cp. Mirrors
the same pattern in DockerEgressProxy.start which already chmods
the original routes.yaml + the CAs before cp. The proposed routes
content carries no secrets (tokens live in the egress-proxy
container's environ, not the routes file), so 0644 in /tmp for
the brief window between write and cp is safe.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
fad76d3364 |
fix(supervise): stage current-config routes file as routes.yaml
The supervise sidecar mounted a snapshot named routes.json into the agent at /etc/claude-bottle/current-config/routes.json, but the egress-proxy-block tool description (and the live proxy file the apply step writes) say routes.yaml. The agent couldn't find the file at the documented path, composed proposals against stale or empty current state, and reported "routes wasn't updated on disk" because it was looking at the wrong filename. Rename the staged file to routes.yaml so the tool description, the staged snapshot, and the live proxy file all agree on the name. Content stays JSON-in-a-yaml-extension (per PRD 0017 chunk 1's decision: every JSON document is valid YAML, stdlib parsers handle it on both ends). Note: the staged file is still a one-shot snapshot taken at bottle prep time. It does NOT auto-update when the operator approves an egress-proxy-block. Agents that want to verify their proposal took effect should retry the request that triggered the block — a successful upstream response is the real signal. Fixing the snapshot-staleness UX is a separate follow-up. Tests migrated from routes.json → routes.yaml. 364 pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
c4cf2453e2 |
fix(launch): also set lowercase {http,https,no}_proxy on the agent
CVE-2016-5388 ("httpoxy") mitigation: libcurl ignores uppercase
HTTP_PROXY for http:// URLs to prevent untrusted CGI HTTP_*
headers from hijacking the proxy. Only lowercase http_proxy is
honored for HTTP. Without the lowercase var, plain-HTTP requests
from the agent skip egress-proxy entirely — they go direct,
which is "network unreachable" on the agent's --internal bridge,
not the egress-proxy 403 we expect.
Confirmed against a live bottle: `curl http://1.1.1.1/` reported
"Immediate connect fail for 1.1.1.1: Network is unreachable"
instead of the addon's "host not in allowlist" 403. With both
cases set the agent's curl honors the proxy and our allowlist
enforcement kicks in.
Also set lowercase HTTPS_PROXY + NO_PROXY for symmetry. Some
tools check one case only; sending both means we don't have to
audit which convention each tool uses.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f807ed1149 |
fix(egress-proxy): force traffic through pipelock + block unallowlisted hosts
Two issues stopping the bottle's egress allowlist from being enforced: 1. mitmproxy was bypassing pipelock. We set HTTPS_PROXY=pipelock in the egress-proxy container's env, but mitmproxy is a proxy *server* — it does NOT honor HTTP(S)_PROXY env vars on its outbound side the way HTTP-client libraries do. All post-MITM traffic was going direct to the upstream, never touching pipelock's hostname allowlist or DLP scanner. Fix: use mitmproxy's `--mode upstream:URL` flag. The Dockerfile entrypoint now reads a new `EGRESS_PROXY_UPSTREAM_PROXY` env (set by `DockerEgressProxy.start` to the pipelock URL when pipelock is in the topology) and switches mitmdump to upstream-proxy mode. Standalone runs of the image without the env still get `--mode regular@9099` direct-to-upstream — useful for unit-test boots. Confirmed in the boot log: "HTTP(S) proxy (upstream mode) listening at *:9099." 2. egress-proxy was forwarding unrecognized hosts. The addon's `decide()` returned `Decision(action="forward")` whenever no route matched the request host, deferring to pipelock to gate. With #1 broken pipelock wasn't gating either; even with #1 fixed, defense-in-depth wants both layers enforcing. Fix: no-route-match → 403 with a "host not in allowlist" reason. The egress allowlist is now strictly the set of hosts declared in `bottle.egress_proxy.routes`; bare-pass routes (host with no auth, no path_allowlist) cover the passthrough case for hosts that just need reach. path_allowlist enforcement on matched routes is unchanged. Test updated: `test_no_matching_route_forwards` → `test_no_matching_route_blocks`. 364 unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5dc33f3acc |
fix(egress-proxy): mint CA via openssl req so leaf AKI matches CA SKI
Root cause of the persistent SSL handshake failure: pipelock's `tls init` stamps a non-standard `Subject Key Identifier` on the CAs it generates (random rather than SHA-1 of the pubkey). mitmproxy computes the `Authority Key Identifier` on each leaf cert it mints as SHA-1(issuer's pubkey). openssl's chain validator uses the leaf's AKI to find the issuer cert by SKI; with pipelock's SKI off by definition, the lookup fails and openssl returns "unable to get local issuer certificate" — even though the CA is right there in the trust store with the matching SHA-256 fingerprint. (Also, pipelock generates EC CAs; the cert+key concat fit in 834 bytes vs ~2.3KB for RSA, which was the first red flag.) Diagnostic from a live bottle confirmed: leaf cert AKI: A8:F0:D5:E3:B5:B9:C2:38:2B:9F:DD:4A:DF:26:8C:72:19:A2:5E:94 CA cert SKI: 81:CA:6D:4C:ED:5C:C2:B1:48:0C:3E:E8:8D:73:86:97:B9:89:B4:3D CA cert + leaf cert: same Pipelock-named subject, same public key bytes openssl verify -CAfile <our CA> <leaf>: error 20 Fix: switch `egress_proxy_tls_init` from `pipelock tls init` to host `openssl req` with an explicit `subjectKeyIdentifier=hash` extension. SHA-1(pubkey) for the SKI matches what mitmproxy puts in the AKI, so chain validation works. The generated CA is also RSA-2048 / sha256WithRSAEncryption — mitmproxy's most-tested configuration. The new generator is independent of pipelock entirely (no docker run on the pipelock image to mint the CA), so the egress-proxy CA generation now requires only `openssl` on the host. macOS + Linux dev images both have it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
b9c70f7daa |
fix(egress-proxy): build combined trust bundle (system + pipelock CA)
`--set ssl_verify_upstream_trusted_ca` REPLACES mitmproxy's default
trust store with the file we point it at. The earlier wiring
pointed it at just pipelock's CA, which broke for any host pipelock
passes through (api.anthropic.com is in DEFAULT_TLS_PASSTHROUGH):
pipelock CONNECT-tunnels the handshake to the real upstream,
egress-proxy sees the real public cert (signed by e.g. DigiCert),
and refuses to validate because pipelock's CA doesn't sign it.
Fix in Dockerfile entrypoint: when EGRESS_PROXY_UPSTREAM_CA is
set, concatenate /etc/ssl/certs/ca-certificates.crt + the pipelock
CA into /home/mitmproxy/.mitmproxy/combined-trust.pem, and pass
that as ssl_verify_upstream_trusted_ca. Covers both legs:
- pipelock-MITM'd hosts → leaf cert signed by pipelock CA →
validates against the pipelock half of the bundle.
- pipelock-passthrough hosts (api.anthropic.com et al.) → real
upstream cert → validates against the system half.
Standalone runs of the image (no EGRESS_PROXY_UPSTREAM_CA) skip
the concat and use mitmproxy's default trust store.
Reproduces against today's main: agent gets "Unable to connect to
API: SSL certificate verification failed" on api.anthropic.com,
egress-proxy logs "Server TLS handshake failed. Certificate verify
failed: unable to get local issuer certificate". After this patch
the trust bundle includes the real upstream root + pipelock's CA
and both validation paths succeed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
57a9707e1c |
fix(egress-proxy): chmod 644 host CAs so mitmproxy user can read after docker cp
mitmdump crashed at boot with PermissionError on
~/.mitmproxy/mitmproxy-ca.pem. Cause: `docker cp` preserves the
host file's mode AND uid. The CA files were 0600 owned by the host
user (uid 501 on macOS), so inside the container the mitmproxy
user (uid 1000, set by USER directive in Dockerfile) couldn't read
them.
Fix:
- `egress_proxy_tls_init`: chmod 644 the cert-only + the cert+key
concat on the host stage dir.
- `DockerEgressProxy.start`: chmod 644 routes.yaml and the
pipelock CA before `docker cp` into the egress-proxy container
(pipelock itself runs as root so its in-pipelock copy is
unaffected).
The host stage_dir is mode 700 — other host users still can't
traverse in, so the cert+key concat isn't actually exposed despite
the 644 mode. The container side gets world-readable, which is
fine inside the per-bottle container.
Reproduces against today's main: bottle's egress-proxy sidecar
crashes with PermissionError; after this patch mitmdump boots and
listens on :9099.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
f04fbb68a9 |
feat(egress-proxy): drive claude-code OAuth placeholder off a role marker
The chunk 2 detection keyed on `token_ref == "CLAUDE_CODE_OAUTH_TOKEN"`,
which broke any bottle whose host env var has a different name (e.g.
`CLAUDE_BOTTLE_OAUTH_TOKEN`). The token_ref is the user's choice —
the placeholder-env trigger shouldn't be locked to one specific
string.
Restoring a minimal `role` marker on `EgressProxyRoute`:
- `EGRESS_PROXY_ROLES = frozenset({"claude_code_oauth"})` — one
marker for now; the field is back so we can grow it.
- `EGRESS_PROXY_SINGLETON_ROLES` — claude_code_oauth is a
singleton (only one route per bottle can carry it).
- `Role: tuple[str, ...]` field on `EgressProxyRoute` (manifest +
runtime), parsed as string or list-of-strings; unknown roles
are rejected so typos can't become silent no-ops.
`prepare.py:has_anthropic_auth` now checks for `"claude_code_oauth"
in r.roles` instead of matching a literal token_ref string. Bottles
can name their host OAuth env var anything; the role marker is what
flips on `CLAUDE_CODE_OAUTH_TOKEN=<placeholder>` and the
telemetry-off env vars on the agent.
Test coverage: 7 new manifest tests (omitted / string / list /
unknown role rejected / non-string rejected / list-item non-string
rejected / singleton enforced).
364 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
9cd583fbbb |
feat(egress-proxy): retarget remediation at egress-proxy (PRD 0017 chunk 3)
Finishes PRD 0017. The `cred-proxy-block` MCP tool is renamed and
its remediation apply path is repointed at egress-proxy.
- `claude_bottle/supervise.py` — `TOOL_CRED_PROXY_BLOCK` →
`TOOL_EGRESS_PROXY_BLOCK`; `COMPONENT_FOR_TOOL` maps the new
tool ID to `egress-proxy` for audit-log routing.
- `claude_bottle/supervise_server.py` — tool definition renamed
+ description rewritten: "Call when egress-proxy refused your
HTTPS request ... Read the current routes.yaml from /etc/
claude-bottle/current-config/routes.yaml, compose a modified
version, pass the full new file plus a justification." The
syntactic validator dispatches on the new tool ID.
- `claude_bottle/backend/docker/egress_proxy_apply.py` — renamed
from `cred_proxy_apply.py`. Reads routes.yaml from
/etc/egress-proxy/routes.yaml via `docker exec cat`; validates
via `egress_proxy_addon_core.load_routes` (so both sides use
the same parser); writes via `docker cp`; SIGHUPs egress-proxy
with `docker kill --signal HUP`. `EgressProxyApplyError`
replaces `CredProxyApplyError`.
- `claude_bottle/cli/dashboard.py` — wires the new apply +
`discover_egress_proxy_slugs` helper; the operator-initiated
`routes edit <bottle>` verb now writes to egress-proxy with
`.yaml` suffix. Stale follow-up comment about path-aware
filtering removed — PRD 0017 settled that question.
- `tests/integration/test_supervise_sidecar.py` — restores the
approval round-trip test (chunk 2 had switched it to a reject
path because no cred-proxy existed). Approval stubs
`apply_routes_change` so the test focuses on the supervise
queue/response plumbing rather than docker-exec into a real
egress-proxy sidecar (that's covered separately).
- `tests/unit/test_egress_proxy_apply.py` — rewritten against
the new validator; covers JSON shape, missing routes key,
partial-auth-pair rejection (the addon-core parser catches
these before SIGHUP).
- PRDs 0010 + 0014 — status headers updated to
Superseded / Retargeted with a callout block pointing at PRD
0017's migration section. Historical text preserved.
384 unit + integration tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a135415dfe | Merge pull request 'feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2)' (#29) from egress-proxy-cutover into main | ||
|
|
4abea282e0 |
revert(egress-proxy): drop Role + agent provisioner (keep git-push block)
Partial revert of |
||
|
|
fa06a3a0ab |
feat(egress-proxy): block HTTPS git push + restore role provisioner
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> |
||
|
|
70f773ac61 |
feat(egress-proxy): cutover from cred-proxy (PRD 0017 chunk 2)
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>
|
||
|
|
9e41845a2b | Merge pull request 'feat(egress-proxy): mitmproxy sidecar core (PRD 0017 chunk 1)' (#28) from egress-proxy-sidecar-core into main | ||
|
|
3df54573d4 |
feat(egress-proxy): add mitmproxy-based sidecar core (PRD 0017 chunk 1)
Lands the new egress-proxy artifact alongside cred-proxy. Chunk 2
wires the agent's HTTP_PROXY to it and removes cred-proxy.
- `Dockerfile.egress-proxy` — mitmproxy 11.1.3 base, COPY addon
files flat to /app, mkdir routes dir at /etc/egress-proxy/.
Digest pin deferred to chunk 2.
- `egress_proxy_addon_core.py` — pure-logic parse + decide
(host-importable; 21 unit tests).
- `egress_proxy_addon.py` — mitmproxy hook wrapper, container-only
(boot + SIGHUP reload, strip-Authorization + decide + 403/inject).
- `egress_proxy.py` — host helpers: manifest lift, routes.yaml
render (JSON content), token-env-map, Plan + abstract class.
- `backend/docker/egress_proxy.py` — `DockerEgressProxy` start/stop
mirroring `DockerCredProxy`; not yet called from launch.py.
- `manifest.py` — new `EgressProxyRoute` + `EgressProxyConfig` types
with the nested `auth: { scheme, token_ref }` block per PRD;
`bottle.egress_proxy` added to the bottle key set alongside
`cred_proxy` (chunk 2 hard-fails on the latter).
All 427 unit tests pass. Image builds; `docker run` boots mitmdump
and the addon loads routes from a mounted routes.yaml.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a2a7396a14 | Merge pull request 'PRD 0017: Egress-proxy — universal MITM via mitmproxy (replaces cred-proxy)' (#27) from prd-0017-path-aware-egress into main | ||
|
|
a79b2b7be0 |
docs(prd-0017): nest auth.scheme + auth.token_ref under optional auth
Earlier draft had `auth_scheme: "none"` as the unauthenticated
signal — awkward sentinel. Nest the two credential-injection
fields under an optional `auth` key instead. Presence of the key
= authenticated; absence = unauthenticated. Empty `auth: {}` is
an error (omission is what means "no auth").
Touches: scope bullet, manifest example, mitmproxy addon
description's auth-handling step. Two trailing `auth_scheme:
"none"` references kept as historical context for what the new
shape replaces.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
b0d9802469 |
docs(prd-0017): pivot to mitmproxy-based egress-proxy
Significant rewrite of PRD 0017 based on PR #25 design discussion. Original draft proposed adding `path_allowlist` to the existing cred-proxy. That bought opt-in path filtering for tools that voluntarily routed through cred-proxy (Claude Code, git, npm) — but raw `curl https://github.com/foo` from the agent goes to HTTPS_PROXY=pipelock and bypasses cred-proxy entirely, so any universal enforcement claim was a lie. New design: replace cred-proxy with a mitmproxy-based egress-proxy that becomes the agent's HTTP_PROXY/HTTPS_PROXY. Every agent HTTP/HTTPS request flows through it before reaching pipelock. Path-level allow/deny enforcement is universal because the proxy is on every leg. The proxy also absorbs cred-proxy's credential injection role (mitmproxy addon hooks request → strip + inject Authorization). Net sidecar count: unchanged. cred-proxy is replaced 1:1 by egress-proxy. Pipelock stays as hostname allow + DLP downstream of egress-proxy. Decisions baked in per PR-#25 discussion: - Tool: mitmproxy (designed for this; Python addons; well-maintained). - CA custody: egress-proxy holds the per-bottle MITM CA key (concentration accepted; documented in trust-domain section). - Migration: hard cutover. Existing `bottle.cred_proxy.routes[]` manifests fail-fast at load time with a pointer at this PRD. Open questions retained for the implementation PRs: addon distribution (bake vs mount), prefix-vs-glob match, double-strip of Authorization between egress-proxy and pipelock, whether pipelock keeps TLS interception or stays hostname-only post-cutover, performance under two-MITM-hops. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5b925a6699 |
docs(prd-0017): path-aware egress filtering via cred-proxy
Extends cred-proxy to filter (not just route) paths, including for unauthenticated upstreams via a new `auth_scheme: "none"` mode and `path_allowlist` field per route. Pipelock keeps its hostname allowlist + DLP role; cred-proxy adds path-level enforcement for routes that opt in. Motivated by PR #25's follow-up note in _apply_pipelock_url: pipelock 2.3.0's api_allowlist is hostname-only, so approving pipelock-block opens the entire host. For shared platforms (github.com, gitlab.com, public registries) operators usually want narrower-than-host granularity. Draft status; open questions on match semantics, allow-route-with- empty-allowlist edge case, and the eventual MCP tool shape for agent-proposed path additions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0668c7bb45 | Merge pull request 'fix(supervise): provision MCP via claude mcp add' (#25) from supervise-mcp-add-via-cli into main | ||
|
|
6066bb4d4c |
fix(dashboard): show the literal new allowlist line in green, no prefix
The "→ would allow host: api.github.com" framing added narration where none was needed. Just render the host on its own line in green — that's literally the text that gets appended to pipelock's allowlist on approve, and the green color carries "what's about to change". The URL (with path) is still right above for context. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
97ff506783 |
feat(dashboard): highlight new hostname in green on pipelock-block detail
When the operator opens a pipelock-block proposal in the detail
view (Enter / 'v'), append a green-coloured line:
→ would allow host: api.github.com
so what's actually about to change is obvious at a glance. The
full failed URL stays above the new line (the path is operator
context — pipelock can't enforce it, just records intent).
- _detail_lines now returns (text, attr) tuples; pipelock-block
appends the host-extract line tagged with the green color pair.
- _detail_view threaded the green_attr through from the main loop
(matches the new-proposal highlight pattern from earlier in this
PR).
- Best-effort URL parsing; unparseable payloads skip the highlight
line rather than render a misleading blank host.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
82d6534e6b |
docs(pipelock-block): flag follow-up for path-aware filtering
PR #25's pipelock-block tool sends a full URL and the supervisor extracts just the hostname for pipelock's allowlist — pipelock 2.3.0's api_allowlist is hostname-only (verified by inspecting the binary's strict preset). The path component is operator context, not enforced. Document the follow-up shape inline at the apply site so a future reader looking at why we're throwing away the path lands on the plan: adding `auth_scheme: none` + `path_allowlist` to cred-proxy, and rewiring pipelock-block to propose cred-proxy routes instead of pipelock hostnames. Multi-touch change, its own PRD. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
f3f2e3e9ab |
feat(pipelock-block): tool sends failed URL, supervisor merges host
Reshape the pipelock-block MCP tool around what the agent actually knows at the moment of failure (the URL pipelock just refused), not what the operator needs (a full allowlist file). Before: agent had to read /etc/claude-bottle/current-config/allowlist, copy the whole file, append their host, send back. Lots of work, easy to get wrong, and the operator's diff was noisy because the proposal contained every host the agent saw — most of which weren't the change. After: agent calls pipelock-block(failed_url="https://api.github.com/repos/foo/bar", justification="...") supervisor extracts api.github.com, fetches the running allowlist, adds the host if not already present, applies the merged content. Path is captured as operator context (the detail view labels it "failed URL" instead of "proposed file") but isn't enforced — pipelock's api_allowlist is hostname-only, so the path can't become an allow rule. - supervise_server: pipelock-block input schema gains `failed_url` (replaces `allowlist`); validate_proposed_file checks for http/https + hostname. - PROPOSED_FILE_FIELD updated; tool description rewritten. - dashboard._apply_pipelock_url: extract host, fetch current, merge, apply. - _proposed_payload_label: detail view renders "failed URL" for pipelock-block, "proposed file" otherwise. - Tests updated end-to-end; new url-host-merge + idempotent-merge + invalid-url cases added. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a9bb34cb77 |
feat(dashboard): highlight newly-arrived proposals in green for 5s
When a new proposal lands in the dashboard's list, the operator shouldn't have to compare the list to a mental snapshot to spot what's new. Render newly-arrived proposals in green for the first five seconds after they show up. - _try_init_green: initialise a green color pair; returns 0 if the terminal lacks color so the highlight degrades to no-op. - _main_loop tracks first_seen[proposal_id] across refresh ticks, pruning entries when a proposal leaves the queue. - _render ORs green into the existing attr (composes with selection reverse-video — terminal handles the mix). Applies to all tool types (cred-proxy-block, pipelock-block, capability-block). If a tool-specific highlight is wanted later, filter on qp.proposal.tool in _is_recent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4e4051f420 |
fix(dashboard): auto-refresh the TUI every 1s
The main loop blocked on stdscr.getch() until the operator hit a key — a tool call landing in the queue while the operator was just watching wouldn't appear on the screen. The operator had to press any key to trigger a re-render and see the new proposal. Switch to stdscr.timeout(1000): getch returns -1 after 1s if no key was pressed, and the loop re-renders with the latest discover_pending() result. CPU cost is trivial; the loop body is ~one filesystem scan + curses draw per second. Also restructure status_line lifecycle: was cleared right after every render, which meant a timeout-driven re-render would wipe the message ~1s after the operator's keystroke set it. Now status_line is cleared only on actual key press, so messages like "approved cred-proxy-block for [dev-xyz]" persist until the operator does something else. Detail view + prompt view are unchanged — they're modal, the underlying proposal data doesn't move, and getstr can't tolerate a re-render mid-input. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
307400f08a |
fix(supervise): bypass pipelock for agent → supervise MCP traffic
`/mcp` showed the supervise server as ✔ connected (initialize is fast), but any actual tool call failed because the supervise MCP design is long-poll — the sidecar holds the HTTP request open until the operator approves in the dashboard (potentially minutes) and only then returns the response. Pipelock is a forward proxy with idle timeouts; it cut the long- polled HTTPS-style request well before the operator could act, and claude-code reported the tool as ✘ failed. Fix: add `supervise` to the agent's NO_PROXY when bottle.supervise is true. The supervise sidecar is on the bottle's internal network with the `supervise` network-alias, so the agent can dial it directly via docker DNS — no proxy, no idle timeout. Body-scanning supervise traffic isn't critical because the operator reviews every proposal in the TUI before approving. The earlier pipelock allowlist auto-add for `supervise` stays as belt-and- braces (handles any proxy-respecting client other than claude-code that might dial supervise). Existing bottles need a restart to pick up the new NO_PROXY value (env can't be changed on a running container). The dashboard's pipelock-edit workaround from PR #25 unblocks short-running tool calls in the meantime but won't survive the pipelock idle timeout on a long-polled call. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |