PRD 0039: smolmachines capability-block remediation #142

Merged
didericis merged 3 commits from prd-0039-smolmachines-capability-remediation into main 2026-06-02 11:15:38 -04:00
3 changed files with 143 additions and 0 deletions
+7
View File
@@ -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,
)
@@ -0,0 +1,87 @@
# PRD 0039: smolmachines Capability-Block Remediation
- **Status:** Active
- **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?
+49
View File
@@ -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()