9e3b7e441e
First slice of chunk 4: implement the two provisioning methods
that don't depend on agent-image tooling beyond `cp` and
`mkdir`. provision_ca / provision_git / provision_supervise
land once the agent-image gap is solved (chunk 4b+) — they need
update-ca-certificates, git, and the claude binary respectively,
none of which the chunk-2d alpine placeholder provides.
What this PR ships:
- `claude_bottle/backend/smolmachines/provision/` subpackage
with `prompt.py` + `skills.py`. Each routes through
`smolvm.machine_cp` / `machine_exec`. provision_prompt mirrors
the docker contract (file always copied; return value drives
--append-system-prompt-file iff the agent has a non-empty
prompt). provision_skills mkdir + cp per skill, matching
the docker backend's loop.
- prepare.py now writes the prompt file under
agent_state_dir(slug) with the agent's `prompt` body, mode
0o600. The in-guest path is `/root/.claude-bottle-prompt.txt`
(alpine has no `node` user; will become `/home/node/...` once
the real claude-bottle image lands).
- launch.py calls `provision(plan, machine_name)` after
machine_start. The returned prompt path threads to
SmolmachinesBottle so exec_claude can add
--append-system-prompt-file when the agent has a prompt.
- backend.py: provision_prompt / provision_skills now real;
provision_git is a deliberate stub (waiting on the git-gate
inner Plan + git in the agent image). provision_supervise
stays the chunk-2d stub.
Tests:
- 7 new unit cases (test_smolmachines_provision.py): argv
shape (mocked smolvm.machine_cp / .machine_exec),
prompt return-value contract, no-op-with-no-skills,
CLAUDE_BOTTLE_GUEST_SKILLS_DIR override, fail-on-missing-skill.
- 1 new integration case in test_smolmachines_launch.py:
end-to-end verification that the prompt file lands in the
alpine guest at /root/.claude-bottle-prompt.txt with the
expected content (via `bottle.exec("cat ...")`). The smoke +
the two TSI probes stay green.
552 unit + 4 integration (Darwin+smolvm+docker gated) passing.
What's left in chunk 4:
- 4b: thread the inner Plans (PipelockProxyPlan / EgressPlan /
GitGatePlan / SupervisePlan) through prepare + launch so the
bundle daemons actually run (currently daemons_csv="").
- 4c: the agent-image-conversion gap — get claude-code + git +
curl + ca-certificates into the guest image (build a
.smolmachine via `pack create --from-vm` after manual setup,
or push the docker image to a registry smolvm can pull).
- 4d: provision_ca + provision_git + provision_supervise once
4b + 4c land.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
72 lines
2.3 KiB
Python
72 lines
2.3 KiB
Python
"""SmolmachinesBottleBackend — the smolmachines implementation of
|
|
BottleBackend (PRD 0023)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Generator
|
|
|
|
from .. import BottleBackend, BottleSpec
|
|
from . import launch as _launch
|
|
from . import prepare as _prepare
|
|
from .bottle import SmolmachinesBottle
|
|
from .bottle_cleanup_plan import SmolmachinesBottleCleanupPlan
|
|
from .bottle_plan import SmolmachinesBottlePlan
|
|
from .provision import prompt as _prompt
|
|
from .provision import skills as _skills
|
|
|
|
|
|
class SmolmachinesBottleBackend(
|
|
BottleBackend["SmolmachinesBottlePlan", "SmolmachinesBottleCleanupPlan"]
|
|
):
|
|
"""smolmachines backend. Selected by
|
|
`CLAUDE_BOTTLE_BACKEND=smolmachines`."""
|
|
|
|
name = "smolmachines"
|
|
|
|
def _resolve_plan(
|
|
self, spec: BottleSpec, *, stage_dir: Path
|
|
) -> SmolmachinesBottlePlan:
|
|
return _prepare.resolve_plan(spec, stage_dir=stage_dir)
|
|
|
|
@contextmanager
|
|
def launch(
|
|
self, plan: SmolmachinesBottlePlan
|
|
) -> Generator[SmolmachinesBottle, None, None]:
|
|
with _launch.launch(plan, provision=self.provision) as bottle:
|
|
yield bottle
|
|
|
|
def provision_prompt(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> str | None:
|
|
return _prompt.provision_prompt(plan, target)
|
|
|
|
def provision_skills(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
_skills.provision_skills(plan, target)
|
|
|
|
def provision_git(
|
|
self, plan: SmolmachinesBottlePlan, target: str
|
|
) -> None:
|
|
# Chunk 4 follow-on: needs the git-gate inner Plan (so the
|
|
# gitconfig insteadOf URL points at the gate's host) and
|
|
# the agent image must contain `git`. Stub for chunk 4a.
|
|
del plan, target
|
|
|
|
def prepare_cleanup(self) -> SmolmachinesBottleCleanupPlan:
|
|
return SmolmachinesBottleCleanupPlan()
|
|
|
|
def cleanup(self, plan: SmolmachinesBottleCleanupPlan) -> None:
|
|
del plan
|
|
# Nothing to clean in chunks 1-3 — see
|
|
# SmolmachinesBottleCleanupPlan docstring.
|
|
|
|
def list_active(self) -> None:
|
|
from ...log import info
|
|
info(
|
|
"smolmachines list_active: not implemented (chunk 4 wires "
|
|
"it to `smolvm machine ls --json`)"
|
|
)
|