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>
This commit is contained in:
@@ -61,15 +61,27 @@ class TestValidation(unittest.TestCase):
|
||||
'{"routes": [{"path": "/x/", "upstream": "https://example.com"}]}',
|
||||
)
|
||||
|
||||
def test_pipelock_block_accepts_clean_hostnames(self):
|
||||
def test_pipelock_block_accepts_https_url(self):
|
||||
validate_proposed_file(
|
||||
_sv.TOOL_PIPELOCK_BLOCK,
|
||||
"api.example.com\n# comment\nfoo.bar.baz\n",
|
||||
"https://api.github.com/repos/foo/bar",
|
||||
)
|
||||
|
||||
def test_pipelock_block_rejects_invalid_char(self):
|
||||
with self.assertRaises(_RpcError):
|
||||
validate_proposed_file(_sv.TOOL_PIPELOCK_BLOCK, "host with space.com\n")
|
||||
def test_pipelock_block_accepts_http_url(self):
|
||||
validate_proposed_file(
|
||||
_sv.TOOL_PIPELOCK_BLOCK,
|
||||
"http://internal.example/path/to/thing",
|
||||
)
|
||||
|
||||
def test_pipelock_block_rejects_missing_scheme(self):
|
||||
with self.assertRaises(_RpcError) as cm:
|
||||
validate_proposed_file(_sv.TOOL_PIPELOCK_BLOCK, "api.github.com/foo")
|
||||
self.assertIn("http://", str(cm.exception.message))
|
||||
|
||||
def test_pipelock_block_rejects_missing_host(self):
|
||||
with self.assertRaises(_RpcError) as cm:
|
||||
validate_proposed_file(_sv.TOOL_PIPELOCK_BLOCK, "https:///just-a-path")
|
||||
self.assertIn("hostname", str(cm.exception.message))
|
||||
|
||||
def test_capability_block_accepts_anything_nonempty(self):
|
||||
validate_proposed_file(
|
||||
@@ -235,7 +247,7 @@ class TestHandleToolsCall(unittest.TestCase):
|
||||
{
|
||||
"name": _sv.TOOL_PIPELOCK_BLOCK,
|
||||
"arguments": {
|
||||
"allowlist": "example.com\n",
|
||||
"failed_url": "https://example.com/path",
|
||||
"justification": "needed for tests",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user