fix(supervise): provision MCP via claude mcp add #25

Merged
didericis merged 9 commits from supervise-mcp-add-via-cli into main 2026-05-25 08:31:17 -04:00
Owner

Summary

The previous provisioner wrote ~/.claude/settings.json with an mcpServers entry — but claude-code doesn't read its MCP servers from that path. Inside a bottle, /mcp showed "No MCP servers configured" even though the sidecar was running.

Switch to the official claude mcp add command run via docker exec:

docker exec -u node <agent> \
  claude mcp add --scope user --transport http supervise <url>

claude-code owns its config file format (~/.claude.json shape, key names, scope semantics) and has changed it between versions. The official command writes to the right place in the right shape for whatever version is installed.

Failure is logged but not fatal — the bottle still works; the warning surfaces the manual docker exec command to retry.

## Summary The previous provisioner wrote `~/.claude/settings.json` with an `mcpServers` entry — but claude-code doesn't read its MCP servers from that path. Inside a bottle, `/mcp` showed "No MCP servers configured" even though the sidecar was running. Switch to the official `claude mcp add` command run via docker exec: docker exec -u node <agent> \ claude mcp add --scope user --transport http supervise <url> claude-code owns its config file format (`~/.claude.json` shape, key names, scope semantics) and has changed it between versions. The official command writes to the right place in the right shape for whatever version is installed. Failure is logged but not fatal — the bottle still works; the warning surfaces the manual `docker exec` command to retry.
didericis added 1 commit 2026-05-25 07:20:20 -04:00
fix(supervise): provision MCP via claude mcp add, not raw settings.json
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m34s
0e2fc97aa8
The previous provisioner wrote ~/.claude/settings.json with an
mcpServers entry — but claude-code doesn't read its mcpServers from
that path. Inside a bottle, /mcp showed "No MCP servers configured"
even though the sidecar was running.

Switch to the official `claude mcp add` command run via docker exec:

  docker exec -u node <agent> \
    claude mcp add --scope user --transport http supervise <url>

claude-code owns its config file format (~/.claude.json shape, key
names, scope semantics) and has changed it between versions. The
official command writes to the right place in the right shape for
whatever version is installed.

Failure is logged but not fatal — the bottle still works; you just
have to register the server manually with the command surfaced in
the warning. Worst case is a bad agent claude-code version, not a
bad bottle.

To fix an already-running bottle without restarting, the user can
run the same `docker exec` command directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis force-pushed supervise-mcp-add-via-cli from d5fa0a145e to 0e2fc97aa8 2026-05-25 07:20:20 -04:00 Compare
didericis added 1 commit 2026-05-25 07:27:33 -04:00
fix(pipelock): auto-allow supervise hostname like cred-proxy
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m35s
d2e047fa66
When PR #19 added the supervise sidecar (PRD 0013), I forgot to
mirror the cred-proxy auto-allow in pipelock_effective_allowlist.
The agent's HTTP_PROXY points at pipelock, so a request for
http://supervise:9100/ (the MCP endpoint claude-code dials) arrives
at pipelock as hostname `supervise` — and pipelock 403s it because
the host isn't in api_allowlist.

End-user symptom: even after `claude mcp add` registers the
supervise server, `/mcp` shows it as ✘ failed and the supervise
sidecar's docker logs are silent (request never gets through).

Mirror what cred-proxy already does: when bottle.supervise is True,
add SUPERVISE_HOSTNAME to the rendered pipelock allowlist. New tests
cover both the auto-add and the no-add-when-disabled invariants.

Existing bottles: the dashboard `pipelock edit <bottle>` verb (or
backend.docker.pipelock_apply.apply_allowlist_change) can apply
this fix to a running bottle without a relaunch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis added 1 commit 2026-05-25 07:36:31 -04:00
fix(supervise): bypass pipelock for agent → supervise MCP traffic
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m36s
307400f08a
`/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>
didericis added 1 commit 2026-05-25 07:48:26 -04:00
fix(dashboard): auto-refresh the TUI every 1s
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m34s
4e4051f420
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>
didericis added 1 commit 2026-05-25 07:54:37 -04:00
feat(dashboard): highlight newly-arrived proposals in green for 5s
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m34s
a9bb34cb77
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>
didericis added 1 commit 2026-05-25 08:02:55 -04:00
feat(pipelock-block): tool sends failed URL, supervisor merges host
test / unit (pull_request) Successful in 16s
test / integration (pull_request) Successful in 1m32s
f3f2e3e9ab
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>
didericis added 1 commit 2026-05-25 08:15:41 -04:00
docs(pipelock-block): flag follow-up for path-aware filtering
test / unit (pull_request) Successful in 16s
test / integration (pull_request) Successful in 1m33s
82d6534e6b
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>
didericis added 1 commit 2026-05-25 08:25:28 -04:00
feat(dashboard): highlight new hostname in green on pipelock-block detail
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m32s
97ff506783
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>
didericis added 1 commit 2026-05-25 08:28:31 -04:00
fix(dashboard): show the literal new allowlist line in green, no prefix
test / unit (pull_request) Successful in 17s
test / integration (pull_request) Successful in 1m37s
6066bb4d4c
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>
didericis merged commit 0668c7bb45 into main 2026-05-25 08:31:17 -04:00
Sign in to join this conversation.