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>
This commit is contained in:
@@ -196,7 +196,7 @@ class TestSuperviseSidecar(unittest.TestCase):
|
||||
names = {t["name"] for t in result["result"]["tools"]}
|
||||
self.assertEqual(
|
||||
{
|
||||
_sv.TOOL_CRED_PROXY_BLOCK,
|
||||
_sv.TOOL_EGRESS_PROXY_BLOCK,
|
||||
_sv.TOOL_PIPELOCK_BLOCK,
|
||||
_sv.TOOL_CAPABILITY_BLOCK,
|
||||
},
|
||||
@@ -204,28 +204,38 @@ class TestSuperviseSidecar(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_tools_call_round_trips_through_queue(self):
|
||||
"""End-to-end: agent in the bottle calls cred-proxy-block;
|
||||
the call blocks on the queue; the host rejects via the
|
||||
dashboard helpers; the agent receives the rejection.
|
||||
"""End-to-end: agent in the bottle calls egress-proxy-block;
|
||||
the call blocks on the queue; the host approves via the
|
||||
dashboard helpers; the agent receives the approval.
|
||||
|
||||
PRD 0017 chunk 2 deleted the cred-proxy sidecar, so the
|
||||
approval-apply path on cred-proxy-block is broken in this
|
||||
intermediate state (chunk 3 retargets it at egress-proxy and
|
||||
restores the round-trip approval test). For now this verifies
|
||||
only the queue + response leg by exercising the reject path
|
||||
— no docker-exec into a sidecar needed."""
|
||||
This test focuses on the supervise sidecar's queue + response
|
||||
plumbing, not the egress-proxy apply path itself. The apply
|
||||
function is stubbed so we don't need to bring up a real
|
||||
egress-proxy sidecar (its docker lifecycle has its own
|
||||
integration coverage)."""
|
||||
self._require_bind_mount_sharing()
|
||||
self._bring_up_sidecar()
|
||||
|
||||
# Stub the apply step. The dashboard's approve() calls
|
||||
# apply_routes_change to docker-exec into the egress-proxy
|
||||
# sidecar; this test isn't exercising the real sidecar, so
|
||||
# patch it to a no-op that returns plausible before/after
|
||||
# strings the audit-log writer can render.
|
||||
from claude_bottle.cli import dashboard as _dash
|
||||
original_apply = _dash.apply_routes_change
|
||||
_dash.apply_routes_change = (
|
||||
lambda slug, new: ("(stubbed before)", new)
|
||||
)
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def caller() -> None:
|
||||
captured["response"] = self._curl_jsonrpc({
|
||||
"jsonrpc": "2.0", "id": 7, "method": "tools/call",
|
||||
"params": {
|
||||
"name": _sv.TOOL_CRED_PROXY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_PROXY_BLOCK,
|
||||
"arguments": {
|
||||
"routes": '{"routes": [{"path": "/x/"}]}',
|
||||
"routes": '{"routes": [{"host": "api.example.com"}]}',
|
||||
"justification": "integration test",
|
||||
},
|
||||
},
|
||||
@@ -249,16 +259,17 @@ class TestSuperviseSidecar(unittest.TestCase):
|
||||
self.assertIsNotNone(qp, "proposal never appeared in queue")
|
||||
assert qp is not None # type-narrowing
|
||||
self.assertEqual(
|
||||
_sv.TOOL_CRED_PROXY_BLOCK, qp.proposal.tool,
|
||||
_sv.TOOL_EGRESS_PROXY_BLOCK, qp.proposal.tool,
|
||||
)
|
||||
self.assertEqual("integration test", qp.proposal.justification)
|
||||
|
||||
# Reject via the dashboard helper. The reject path skips
|
||||
# the sidecar-apply step, so it works without a real
|
||||
# cred-proxy sidecar (which doesn't exist in chunk 2's
|
||||
# transitional state).
|
||||
dashboard.reject(qp, reason="no real cred-proxy in chunk 2")
|
||||
# Approve via the dashboard helper. The apply step (now
|
||||
# stubbed) would docker-exec into the egress-proxy sidecar
|
||||
# and SIGHUP it. The supervise sidecar sees the response
|
||||
# file and returns to the curl caller.
|
||||
dashboard.approve(qp, notes="lgtm from integration test")
|
||||
finally:
|
||||
_dash.apply_routes_change = original_apply
|
||||
t.join(timeout=20)
|
||||
|
||||
response = captured.get("response")
|
||||
@@ -267,12 +278,10 @@ class TestSuperviseSidecar(unittest.TestCase):
|
||||
self.assertEqual(7, response["id"])
|
||||
result = response["result"]
|
||||
assert isinstance(result, dict)
|
||||
# Rejected tool calls surface as MCP errors so the agent
|
||||
# treats them as failures (not silent successes).
|
||||
self.assertTrue(result.get("isError"))
|
||||
self.assertFalse(result.get("isError"))
|
||||
text = result["content"][0]["text"]
|
||||
self.assertIn("rejected", text)
|
||||
self.assertIn("no real cred-proxy", text)
|
||||
self.assertIn("status: approved", text)
|
||||
self.assertIn("notes: lgtm from integration test", text)
|
||||
|
||||
def test_orphan_sidecar_name_collision_recovered(self):
|
||||
"""An orphan supervise sidecar from a previous run blocks
|
||||
|
||||
Reference in New Issue
Block a user