feat!: remove capability apply
This commit is contained in:
@@ -115,8 +115,8 @@ class TestBottleIdentity(unittest.TestCase):
|
||||
|
||||
|
||||
class TestPreserveMarker(_FakeHomeMixin, unittest.TestCase):
|
||||
"""The .preserve marker is how capability_apply tells cli.py's
|
||||
session-end cleanup to keep the state dir instead of removing it."""
|
||||
"""The .preserve marker tells cli.py's session-end cleanup to keep
|
||||
the state dir instead of removing it."""
|
||||
|
||||
def setUp(self):
|
||||
self._setup_fake_home()
|
||||
|
||||
@@ -29,8 +29,8 @@ class _FakeHomeMixin:
|
||||
|
||||
|
||||
class TestCaptureSessionState(_FakeHomeMixin, unittest.TestCase):
|
||||
# snapshot_transcript is commented out (capability_apply is disabled);
|
||||
# capture_claude_session_state now only handles the preserve marker.
|
||||
# capture_claude_session_state handles the preserve marker for
|
||||
# non-zero agent exits.
|
||||
def setUp(self):
|
||||
self._setup_fake_home()
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ def _supervise_plan() -> SupervisePlan:
|
||||
return SupervisePlan(
|
||||
slug=SLUG,
|
||||
queue_dir=STATE / "supervise" / "queue",
|
||||
current_config_dir=STATE / "supervise" / "current-config",
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
)
|
||||
|
||||
@@ -271,18 +270,11 @@ class TestAgentAlwaysPresent(unittest.TestCase):
|
||||
s = bottle_plan_to_compose(_plan(**kwargs))["services"]["agent"]
|
||||
self.assertEqual(["sidecars"], s["depends_on"])
|
||||
|
||||
def test_agent_current_config_mount_only_with_supervise(self):
|
||||
def test_agent_has_no_current_config_mount_with_supervise(self):
|
||||
with_sv = bottle_plan_to_compose(_plan(supervise=True))["services"]["agent"]
|
||||
self.assertTrue(any(
|
||||
v["target"] == "/etc/bot-bottle/current-config"
|
||||
for v in with_sv.get("volumes", [])
|
||||
))
|
||||
self.assertNotIn("volumes", with_sv)
|
||||
without_sv = bottle_plan_to_compose(_plan(supervise=False))["services"]["agent"]
|
||||
# Either no volumes key at all, or no current-config target.
|
||||
self.assertFalse(any(
|
||||
v["target"] == "/etc/bot-bottle/current-config"
|
||||
for v in without_sv.get("volumes", [])
|
||||
))
|
||||
self.assertNotIn("volumes", without_sv)
|
||||
|
||||
|
||||
class TestSidecarBundleShape(unittest.TestCase):
|
||||
|
||||
@@ -75,7 +75,6 @@ def _plan(
|
||||
supervise_plan = SupervisePlan(
|
||||
slug="demo-abc12",
|
||||
queue_dir=Path("/tmp/queue"),
|
||||
current_config_dir=Path("/tmp/current-config"),
|
||||
)
|
||||
return DockerBottlePlan(
|
||||
spec=spec,
|
||||
|
||||
@@ -78,7 +78,6 @@ def _plan(
|
||||
supervise_plan = SupervisePlan(
|
||||
slug="demo-abc12",
|
||||
queue_dir=Path("/tmp/queue"),
|
||||
current_config_dir=Path("/tmp/current-config"),
|
||||
)
|
||||
return DockerBottlePlan(
|
||||
spec=spec,
|
||||
|
||||
@@ -65,8 +65,8 @@ class TestOrphanStateDirs(_FakeHomeMixin, unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_preserve_marker_skips_dir(self):
|
||||
# Preserve marker = capability-block or crash auto-preserve;
|
||||
# the user explicitly wanted this dir kept for `resume`.
|
||||
# Preserve marker means the user explicitly wanted this dir
|
||||
# kept for `resume`.
|
||||
bottle_state.write_per_bottle_dockerfile("kept-ccc", "FROM x\n")
|
||||
bottle_state.mark_preserved("kept-ccc")
|
||||
self.assertEqual(
|
||||
|
||||
@@ -130,7 +130,6 @@ def _plan(
|
||||
supervise_plan = SupervisePlan(
|
||||
slug="demo-abc12",
|
||||
queue_dir=Path("/tmp/queue"),
|
||||
current_config_dir=Path("/tmp/current-config"),
|
||||
)
|
||||
return SmolmachinesBottlePlan(
|
||||
spec=spec,
|
||||
|
||||
@@ -16,7 +16,7 @@ from bot_bottle.supervise import (
|
||||
STATUS_APPROVED,
|
||||
STATUS_MODIFIED,
|
||||
STATUS_REJECTED,
|
||||
TOOL_CAPABILITY_BLOCK,
|
||||
TOOL_EGRESS_ALLOW,
|
||||
TOOL_GITLEAKS_ALLOW,
|
||||
archive_proposal,
|
||||
audit_log_path,
|
||||
@@ -37,9 +37,9 @@ FIXED_TS = datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _proposal(
|
||||
tool: str = TOOL_CAPABILITY_BLOCK,
|
||||
proposed: str = "FROM python:3.13\n",
|
||||
justification: str = "need a capability",
|
||||
tool: str = TOOL_EGRESS_ALLOW,
|
||||
proposed: str = "routes:\n - host: example.com\n",
|
||||
justification: str = "need egress",
|
||||
) -> Proposal:
|
||||
return Proposal.new(
|
||||
bottle_slug="dev",
|
||||
@@ -57,7 +57,7 @@ class TestProposalRoundtrip(unittest.TestCase):
|
||||
self.assertTrue(p.id)
|
||||
self.assertEqual("2026-05-25T12:00:00+00:00", p.arrival_timestamp)
|
||||
self.assertEqual("dev", p.bottle_slug)
|
||||
self.assertEqual(TOOL_CAPABILITY_BLOCK, p.tool)
|
||||
self.assertEqual(TOOL_EGRESS_ALLOW, p.tool)
|
||||
|
||||
def test_to_from_dict_roundtrip(self):
|
||||
p = _proposal()
|
||||
@@ -142,14 +142,14 @@ class TestQueueIO(unittest.TestCase):
|
||||
def test_list_pending_sorted_by_arrival(self):
|
||||
# Fabricate two with explicit timestamps.
|
||||
a = Proposal.new(
|
||||
bottle_slug="dev", tool=TOOL_CAPABILITY_BLOCK,
|
||||
proposed_file="FROM python:3.13\n", justification="early",
|
||||
bottle_slug="dev", tool=TOOL_EGRESS_ALLOW,
|
||||
proposed_file="routes:\n - host: early.example.com\n", justification="early",
|
||||
current_file_hash="x",
|
||||
now=datetime(2026, 5, 25, 10, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
b = Proposal.new(
|
||||
bottle_slug="dev", tool=TOOL_CAPABILITY_BLOCK,
|
||||
proposed_file="FROM python:3.13\n", justification="late",
|
||||
bottle_slug="dev", tool=TOOL_EGRESS_ALLOW,
|
||||
proposed_file="routes:\n - host: late.example.com\n", justification="late",
|
||||
current_file_hash="x",
|
||||
now=datetime(2026, 5, 25, 14, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
@@ -319,7 +319,6 @@ class TestToolConstants(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
(
|
||||
supervise.TOOL_EGRESS_ALLOW,
|
||||
TOOL_CAPABILITY_BLOCK,
|
||||
supervise.TOOL_EGRESS_BLOCK,
|
||||
TOOL_GITLEAKS_ALLOW,
|
||||
supervise.TOOL_EGRESS_TOKEN_ALLOW,
|
||||
@@ -378,20 +377,16 @@ class TestSupervisePrepare(unittest.TestCase):
|
||||
supervise.bot_bottle_root = fake_root # type: ignore[assignment]
|
||||
return lambda: setattr(supervise, "bot_bottle_root", original)
|
||||
|
||||
def test_prepare_creates_queue_and_current_config(self):
|
||||
def test_prepare_creates_queue(self):
|
||||
plan = _StubSupervise().prepare("dev", self.stage_dir)
|
||||
self.assertTrue(plan.queue_dir.is_dir())
|
||||
self.assertTrue(plan.current_config_dir.is_dir())
|
||||
self.assertEqual("dev", plan.slug)
|
||||
self.assertEqual("", plan.internal_network)
|
||||
|
||||
def test_prepare_writes_no_files_to_current_config(self):
|
||||
# dockerfile_content is no longer accepted by prepare.
|
||||
# routes.yaml + allowlist live behind the
|
||||
# `list-egress-routes` MCP tool (PRD 0017 chunk 3).
|
||||
def test_prepare_does_not_create_current_config_dir(self):
|
||||
plan = _StubSupervise().prepare("dev", self.stage_dir)
|
||||
files = sorted(p.name for p in plan.current_config_dir.iterdir())
|
||||
self.assertEqual([], files)
|
||||
self.assertFalse((self.stage_dir / "current-config").exists())
|
||||
self.assertFalse(hasattr(plan, "current_config_dir"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -18,7 +18,7 @@ from bot_bottle.supervise import (
|
||||
STATUS_APPROVED,
|
||||
STATUS_MODIFIED,
|
||||
STATUS_REJECTED,
|
||||
TOOL_CAPABILITY_BLOCK,
|
||||
TOOL_EGRESS_ALLOW,
|
||||
TOOL_GITLEAKS_ALLOW,
|
||||
TOOL_EGRESS_TOKEN_ALLOW,
|
||||
read_audit_entries,
|
||||
@@ -30,9 +30,8 @@ from bot_bottle.supervise import (
|
||||
FIXED = datetime(2026, 5, 25, 12, 0, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _proposal(slug: str = "dev", tool: str = TOOL_CAPABILITY_BLOCK) -> Proposal:
|
||||
def _proposal(slug: str = "dev", tool: str = TOOL_EGRESS_ALLOW) -> Proposal:
|
||||
payloads = {
|
||||
TOOL_CAPABILITY_BLOCK: "FROM python:3.13\n",
|
||||
supervise.TOOL_EGRESS_ALLOW: "routes:\n - host: example.com\n",
|
||||
supervise.TOOL_EGRESS_BLOCK: "routes:\n - host: example.com\n",
|
||||
TOOL_GITLEAKS_ALLOW: "file: tests/test_fixture.py\nline: 3\n",
|
||||
@@ -86,14 +85,14 @@ class TestDiscoverPending(_FakeHomeMixin, unittest.TestCase):
|
||||
|
||||
def test_sorted_by_arrival_across_bottles(self):
|
||||
early = Proposal.new(
|
||||
bottle_slug="api", tool=TOOL_CAPABILITY_BLOCK,
|
||||
proposed_file="FROM python:3.13\n", justification="early",
|
||||
bottle_slug="api", tool=TOOL_EGRESS_ALLOW,
|
||||
proposed_file="routes:\n - host: early.example.com\n", justification="early",
|
||||
current_file_hash="h",
|
||||
now=datetime(2026, 5, 25, 10, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
late = Proposal.new(
|
||||
bottle_slug="dev", tool=TOOL_CAPABILITY_BLOCK,
|
||||
proposed_file="FROM python:3.13\n", justification="late",
|
||||
bottle_slug="dev", tool=TOOL_EGRESS_ALLOW,
|
||||
proposed_file="routes:\n - host: late.example.com\n", justification="late",
|
||||
current_file_hash="h",
|
||||
now=datetime(2026, 5, 25, 14, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
@@ -122,7 +121,7 @@ class TestApproveReject(_FakeHomeMixin, unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self._teardown_fake_home()
|
||||
|
||||
def _enqueue(self, tool: str = TOOL_CAPABILITY_BLOCK):
|
||||
def _enqueue(self, tool: str = TOOL_EGRESS_ALLOW):
|
||||
p = _proposal(tool=tool)
|
||||
qdir = supervise.queue_dir_for_slug("dev")
|
||||
qdir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -131,19 +130,29 @@ class TestApproveReject(_FakeHomeMixin, unittest.TestCase):
|
||||
|
||||
def test_approve_writes_response(self):
|
||||
qp = self._enqueue()
|
||||
supervise_cli.approve(qp)
|
||||
# capability-block is archived on approve, so the response file
|
||||
# moves to processed/ before the caller can read it.
|
||||
resp = read_response(qp.queue_dir / "processed", qp.proposal.id)
|
||||
with patch(
|
||||
"bot_bottle.cli.supervise.apply_routes_change",
|
||||
return_value=("routes: []\n", "routes:\n - host: example.com\n"),
|
||||
):
|
||||
supervise_cli.approve(qp)
|
||||
resp = read_response(qp.queue_dir, qp.proposal.id)
|
||||
self.assertEqual(STATUS_APPROVED, resp.status)
|
||||
self.assertIsNone(resp.final_file)
|
||||
|
||||
def test_approve_with_final_file_marks_modified(self):
|
||||
qp = self._enqueue()
|
||||
supervise_cli.approve(qp, final_file="FROM bookworm\n", notes="tweaked")
|
||||
resp = read_response(qp.queue_dir / "processed", qp.proposal.id)
|
||||
with patch(
|
||||
"bot_bottle.cli.supervise.apply_routes_change",
|
||||
return_value=("routes: []\n", "routes:\n - host: edited.example.com\n"),
|
||||
):
|
||||
supervise_cli.approve(
|
||||
qp,
|
||||
final_file="routes:\n - host: edited.example.com\n",
|
||||
notes="tweaked",
|
||||
)
|
||||
resp = read_response(qp.queue_dir, qp.proposal.id)
|
||||
self.assertEqual(STATUS_MODIFIED, resp.status)
|
||||
self.assertEqual("FROM bookworm\n", resp.final_file)
|
||||
self.assertEqual("routes:\n - host: edited.example.com\n", resp.final_file)
|
||||
self.assertEqual("tweaked", resp.notes)
|
||||
|
||||
def test_reject_writes_rejection(self):
|
||||
@@ -153,11 +162,6 @@ class TestApproveReject(_FakeHomeMixin, unittest.TestCase):
|
||||
self.assertEqual(STATUS_REJECTED, resp.status)
|
||||
self.assertEqual("nope", resp.notes)
|
||||
|
||||
def test_no_audit_log_for_capability_block(self):
|
||||
qp = self._enqueue(tool=TOOL_CAPABILITY_BLOCK)
|
||||
supervise_cli.approve(qp)
|
||||
self.assertEqual([], read_audit_entries("egress", "dev"))
|
||||
|
||||
def test_approve_egress_block_writes_audit_log(self):
|
||||
qp = self._enqueue(tool=supervise.TOOL_EGRESS_BLOCK)
|
||||
with patch(
|
||||
@@ -232,11 +236,6 @@ class TestApproveReject(_FakeHomeMixin, unittest.TestCase):
|
||||
self.assertEqual(".txt", supervise_cli._suffix_for_tool(TOOL_EGRESS_TOKEN_ALLOW))
|
||||
|
||||
|
||||
# class TestCapabilityApplyWiring(_FakeHomeMixin, unittest.TestCase):
|
||||
# # DISABLED — capability_apply functionality is currently commented out.
|
||||
# pass
|
||||
|
||||
|
||||
class TestEditInEditor(unittest.TestCase):
|
||||
def test_runs_editor_returns_edited_content(self):
|
||||
original_editor = os.environ.get("EDITOR")
|
||||
@@ -281,10 +280,5 @@ class TestEditInEditor(unittest.TestCase):
|
||||
os.environ["EDITOR"] = original_editor
|
||||
|
||||
|
||||
# class TestCapabilityBlockSmolmachinesGuard(_FakeHomeMixin, unittest.TestCase):
|
||||
# # DISABLED — capability_apply functionality is currently commented out.
|
||||
# pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -50,15 +50,15 @@ from bot_bottle.supervise_server import (
|
||||
|
||||
|
||||
class TestValidation(unittest.TestCase):
|
||||
def test_capability_block_accepts_anything_nonempty(self):
|
||||
validate_proposed_file(
|
||||
_sv.TOOL_CAPABILITY_BLOCK,
|
||||
"FROM python:3.13\nRUN apk add git\n",
|
||||
)
|
||||
|
||||
def test_empty_proposed_file_rejected_for_tools_with_file_field(self):
|
||||
with self.assertRaises(_RpcError):
|
||||
validate_proposed_file(_sv.TOOL_CAPABILITY_BLOCK, " \n\t")
|
||||
validate_proposed_file(_sv.TOOL_EGRESS_ALLOW, " \n\t")
|
||||
|
||||
def test_capability_block_rejected_as_unknown_tool(self):
|
||||
with self.assertRaises(_RpcError) as cm:
|
||||
validate_proposed_file("capability-block", "FROM python:3.13\n")
|
||||
self.assertEqual(ERR_INVALID_PARAMS, cm.exception.code)
|
||||
self.assertIn("unknown tool", cm.exception.message)
|
||||
|
||||
def test_egress_routes_yaml_is_validated(self):
|
||||
validate_proposed_file(
|
||||
@@ -127,9 +127,9 @@ class TestRpcInternalErrorOnIoFailure(unittest.TestCase):
|
||||
with self.assertRaises(_RpcInternalError) as cm:
|
||||
handle_tools_call(
|
||||
{
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_ALLOW,
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"routes_yaml": "routes:\n - host: example.com\n",
|
||||
"justification": "x",
|
||||
},
|
||||
},
|
||||
@@ -219,7 +219,6 @@ class TestHandleToolsList(unittest.TestCase):
|
||||
self.assertEqual(
|
||||
sorted([
|
||||
_sv.TOOL_EGRESS_ALLOW,
|
||||
_sv.TOOL_CAPABILITY_BLOCK,
|
||||
_sv.TOOL_EGRESS_BLOCK,
|
||||
_sv.TOOL_LIST_EGRESS_ROUTES,
|
||||
]),
|
||||
@@ -295,10 +294,10 @@ class TestHandleToolsCall(unittest.TestCase):
|
||||
try:
|
||||
result = handle_tools_call(
|
||||
{
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_BLOCK,
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"justification": "need git",
|
||||
"routes_yaml": "routes:\n - host: example.com\n",
|
||||
"justification": "need example.com",
|
||||
},
|
||||
},
|
||||
self.config,
|
||||
@@ -335,9 +334,9 @@ class TestHandleToolsCall(unittest.TestCase):
|
||||
try:
|
||||
result = handle_tools_call(
|
||||
{
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_ALLOW,
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"routes_yaml": "routes:\n - host: example.com\n",
|
||||
"justification": "needed for tests",
|
||||
},
|
||||
},
|
||||
@@ -359,20 +358,35 @@ class TestHandleToolsCall(unittest.TestCase):
|
||||
with self.assertRaises(_RpcError):
|
||||
handle_tools_call(
|
||||
{
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"arguments": {"dockerfile": "FROM python:3.13\n"},
|
||||
"name": _sv.TOOL_EGRESS_ALLOW,
|
||||
"arguments": {"routes_yaml": "routes:\n - host: example.com\n"},
|
||||
},
|
||||
self.config,
|
||||
)
|
||||
|
||||
def test_capability_block_call_raises_unknown_tool(self):
|
||||
with self.assertRaises(_RpcError) as cm:
|
||||
handle_tools_call(
|
||||
{
|
||||
"name": "capability-block",
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"justification": "need git",
|
||||
},
|
||||
},
|
||||
self.config,
|
||||
)
|
||||
self.assertEqual(ERR_INVALID_PARAMS, cm.exception.code)
|
||||
self.assertIn("unknown tool", cm.exception.message)
|
||||
|
||||
def test_archives_proposal_after_response(self):
|
||||
responder = self._respond_when_proposal_appears(_sv.STATUS_APPROVED)
|
||||
try:
|
||||
handle_tools_call(
|
||||
{
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_ALLOW,
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"routes_yaml": "routes:\n - host: example.com\n",
|
||||
"justification": "x",
|
||||
},
|
||||
},
|
||||
@@ -394,10 +408,10 @@ class TestHandleToolsCall(unittest.TestCase):
|
||||
)
|
||||
result = handle_tools_call(
|
||||
{
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_ALLOW,
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"justification": "need a capability",
|
||||
"routes_yaml": "routes:\n - host: example.com\n",
|
||||
"justification": "need egress",
|
||||
},
|
||||
},
|
||||
config,
|
||||
@@ -521,7 +535,7 @@ class TestHttpEndToEnd(unittest.TestCase):
|
||||
self.assertEqual("2.0", result["jsonrpc"])
|
||||
self.assertEqual(1, result["id"])
|
||||
names = [t["name"] for t in result["result"]["tools"]] # type: ignore[index]
|
||||
self.assertIn(_sv.TOOL_CAPABILITY_BLOCK, names)
|
||||
self.assertNotIn("capability-block", names)
|
||||
self.assertIn(_sv.TOOL_EGRESS_ALLOW, names)
|
||||
self.assertIn(_sv.TOOL_EGRESS_BLOCK, names)
|
||||
|
||||
@@ -541,9 +555,9 @@ class TestHttpEndToEnd(unittest.TestCase):
|
||||
"id": 99,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": _sv.TOOL_CAPABILITY_BLOCK,
|
||||
"name": _sv.TOOL_EGRESS_ALLOW,
|
||||
"arguments": {
|
||||
"dockerfile": "FROM python:3.13\n",
|
||||
"routes_yaml": "routes:\n - host: example.com\n",
|
||||
"justification": "x",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user