From cf76d1a245afa790b8e3ff5f701edbe812701b9d Mon Sep 17 00:00:00 2001 From: "didericis (claude)" Date: Tue, 2 Jun 2026 10:28:13 -0400 Subject: [PATCH 1/3] docs: add PRD 0039 --- ...039-smolmachines-capability-remediation.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/prds/0039-smolmachines-capability-remediation.md diff --git a/docs/prds/0039-smolmachines-capability-remediation.md b/docs/prds/0039-smolmachines-capability-remediation.md new file mode 100644 index 0000000..e7715d9 --- /dev/null +++ b/docs/prds/0039-smolmachines-capability-remediation.md @@ -0,0 +1,87 @@ +# PRD 0039: smolmachines Capability-Block Remediation + +- **Status:** Draft +- **Author:** didericis-codex +- **Created:** 2026-06-02 +- **Issue:** #136 + +## Summary + +Make capability-block remediation backend-aware. Today the dashboard approval +path calls Docker-only teardown and apply code regardless of which backend +created the bottle. Either implement smolmachines remediation or add a clean +disable/unsupported path so operators never get a partial Docker teardown +against a smolmachines slug. + +## Problem + +`bot_bottle/cli/dashboard.py` dispatches every capability-block approval to +`bot_bottle/backend/docker/capability_apply.py`. That code snapshots with +`docker cp`, pushes via `docker exec`, rewrites a Dockerfile override, and +removes Docker containers and networks. It does not stop or delete a smolvm +machine. + +smolmachines bottles still receive the capability-block supervise tool through +`backend/smolmachines/provision/supervise.py`, so agents can queue a +remediation the host cannot correctly apply. A partial Docker teardown against +a smolmachines slug corrupts neither backend cleanly. + +## Goals / Success Criteria + +- Capability-block approval is routed to backend-specific code. +- For the smolmachines backend, either: + a. A real remediation implementation that stops the VM, applies the + capability change, and restarts correctly; or + b. A clean unsupported response that tells the operator the action cannot + be taken and leaves the bottle in a consistent state. +- If option (b): smolmachines agents do not receive the capability-block tool, + so the operator is never prompted for an action that will fail. +- Unit tests cover the dispatch logic and the smolmachines path. + +## Non-goals + +- No changes to the Docker capability-apply path. +- No changes to other supervise tools (cred-block, pipelock-block). +- No changes to manifest or egress configuration. + +## Scope + +In scope: + +- `bot_bottle/cli/dashboard.py` approval dispatch. +- `bot_bottle/backend/smolmachines/provision/supervise.py` tool registration. +- New or updated backend-specific capability apply/disable module for + smolmachines. +- Unit tests for dispatch routing and smolmachines path. + +Out of scope: + +- Changes to `backend/docker/capability_apply.py` internals. +- Integration tests that exercise a live smolmachines VM remediation. + +## Design + +Introduce a backend-aware dispatch at the approval call site. Each backend +exposes a capability remediation entry point; the dashboard calls the one that +matches the bottle's backend. If the backend does not support remediation, +the entry point returns a structured error that the dashboard surfaces as an +operator message without attempting any teardown. + +If option (b) is chosen initially, suppress capability-block registration in +`smolmachines/provision/supervise.py` so agents never see the tool. + +## Testing Strategy + +- Unit test that approval dispatch selects the smolmachines path for a + smolmachines bottle and the Docker path for a Docker bottle. +- Unit test for the smolmachines path (unsupported response or real apply). +- Regression test that Docker approval still calls `capability_apply.py`. + +Run: + +- `python3 -m unittest discover -s tests/unit` + +## Open Questions + +- Is a real smolmachines capability-apply implementation in scope for this PRD, + or should it be deferred to a follow-on after PRD 0040 lands? -- 2.52.0 From e5b5dd16f15d3e5fcb5f620596a021f4ec80db30 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 14:40:17 +0000 Subject: [PATCH 2/3] feat(dashboard): guard capability-block approval for smolmachines bottles (PRD 0039) apply_capability_change is Docker-only teardown/apply code. Before this change it was called regardless of backend, so approving a capability-block proposal from a smolmachines agent would run Docker commands against a slug that has no Docker container. After this change approve() reads the bottle's metadata: if compose_project is empty (the smolmachines indicator) it raises CapabilityApplyError with a clear operator message before any teardown runs. Docker bottles (non-empty compose_project) and unknown bottles (no metadata) fall through to the existing Docker path unchanged. Closes #136 --- bot_bottle/cli/dashboard.py | 7 ++++++ tests/unit/test_dashboard.py | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/bot_bottle/cli/dashboard.py b/bot_bottle/cli/dashboard.py index 75d9346..5c09d2d 100644 --- a/bot_bottle/cli/dashboard.py +++ b/bot_bottle/cli/dashboard.py @@ -175,6 +175,13 @@ def approve( qp.proposal.bottle_slug, file_to_apply, ) elif qp.proposal.tool == TOOL_CAPABILITY_BLOCK: + _meta = read_metadata(qp.proposal.bottle_slug) + if _meta is not None and not _meta.compose_project: + raise CapabilityApplyError( + "capability-block remediation is not supported for smolmachines " + "bottles. Reject this proposal or handle the capability change " + "manually, then restart the bottle." + ) diff_before, diff_after = apply_capability_change( qp.proposal.bottle_slug, file_to_apply, ) diff --git a/tests/unit/test_dashboard.py b/tests/unit/test_dashboard.py index 253fe99..e1e736a 100644 --- a/tests/unit/test_dashboard.py +++ b/tests/unit/test_dashboard.py @@ -577,5 +577,54 @@ class TestEditInEditor(unittest.TestCase): os.environ["EDITOR"] = original_editor +class TestCapabilityBlockSmolmachinesGuard(_FakeHomeMixin, unittest.TestCase): + """approve() must refuse capability-block for smolmachines bottles and + pass it through for Docker bottles (PRD 0039).""" + + def setUp(self): + self._setup_fake_home() + self._original_apply_capability = dashboard.apply_capability_change + dashboard.apply_capability_change = lambda slug, content: ("", content) + + def tearDown(self): + dashboard.apply_capability_change = self._original_apply_capability + self._teardown_fake_home() + + def _enqueue_capability(self, slug: str = "dev") -> "dashboard.QueuedProposal": + p = _proposal(slug=slug, tool=TOOL_CAPABILITY_BLOCK) + qdir = supervise.queue_dir_for_slug(slug) + qdir.mkdir(parents=True, exist_ok=True) + supervise.write_proposal(qdir, p) + return dashboard.QueuedProposal(proposal=p, queue_dir=qdir) + + def _write_metadata(self, slug: str, compose_project: str) -> None: + from bot_bottle.backend.docker.bottle_state import BottleMetadata, write_metadata + write_metadata(BottleMetadata( + identity=slug, + agent_name="myagent", + cwd="", + copy_cwd=False, + started_at="2026-06-02T00:00:00+00:00", + compose_project=compose_project, + )) + + def test_smolmachines_bottle_raises_capability_apply_error(self): + self._write_metadata("dev", compose_project="") + qp = self._enqueue_capability("dev") + with self.assertRaises(CapabilityApplyError) as ctx: + dashboard.approve(qp) + self.assertIn("smolmachines", str(ctx.exception)) + + def test_docker_bottle_calls_apply_capability_change(self): + self._write_metadata("dev", compose_project="bot-bottle-dev") + qp = self._enqueue_capability("dev") + dashboard.approve(qp) # must not raise + + def test_no_metadata_falls_through_to_docker_path(self): + # No metadata at all → assume Docker (backward-compatible). + qp = self._enqueue_capability("dev") + dashboard.approve(qp) # must not raise + + if __name__ == "__main__": unittest.main() -- 2.52.0 From b9108339e7de1b1f4f876e0d1fc7958460ea6645 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 2 Jun 2026 14:40:33 +0000 Subject: [PATCH 3/3] docs: mark PRD 0039 Active --- docs/prds/0039-smolmachines-capability-remediation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/prds/0039-smolmachines-capability-remediation.md b/docs/prds/0039-smolmachines-capability-remediation.md index e7715d9..aa5e87e 100644 --- a/docs/prds/0039-smolmachines-capability-remediation.md +++ b/docs/prds/0039-smolmachines-capability-remediation.md @@ -1,6 +1,6 @@ # PRD 0039: smolmachines Capability-Block Remediation -- **Status:** Draft +- **Status:** Active - **Author:** didericis-codex - **Created:** 2026-06-02 - **Issue:** #136 -- 2.52.0