fix: restore backend prepare wiring
This commit is contained in:
@@ -290,19 +290,19 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
|
||||
manifest = spec.manifest
|
||||
manifest_bottle = manifest.bottle_for(spec.agent_name)
|
||||
manfiest_agent_provider = manifest_bottle.agent_provider
|
||||
agent_provider = get_provider(manfiest_agent_provider.template)
|
||||
manifest_agent_provider = manifest_bottle.agent_provider
|
||||
agent_provider = get_provider(manifest_agent_provider.template)
|
||||
resolved_env = resolve_env(manifest, spec.agent_name)
|
||||
|
||||
slug = mint_slug(spec)
|
||||
write_launch_metadata(slug, spec, compose_project="", backend="smolmachines")
|
||||
write_launch_metadata(slug, spec, compose_project="", backend=self.name)
|
||||
|
||||
# Manifest may override the Dockerfile per-bottle; otherwise fall
|
||||
# back to the provider plugin's bundled Dockerfile (next to its
|
||||
# agent_provider.py module).
|
||||
if manfiest_agent_provider.dockerfile:
|
||||
if manifest_agent_provider.dockerfile:
|
||||
agent_dockerfile_path = resolve_manifest_dockerfile(
|
||||
manfiest_agent_provider.dockerfile, spec,
|
||||
manifest_agent_provider.dockerfile, spec,
|
||||
)
|
||||
else:
|
||||
agent_dockerfile_path = str(agent_provider.dockerfile)
|
||||
@@ -310,14 +310,14 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
||||
agent_dir, prompt_file = prepare_agent_state_dir(slug, spec)
|
||||
|
||||
agent_provision_plan = build_agent_provision_plan(
|
||||
template=manfiest_agent_provider.template,
|
||||
template=manifest_agent_provider.template,
|
||||
dockerfile=agent_dockerfile_path,
|
||||
state_dir=agent_dir,
|
||||
instance_name=f"bot-bottle-{slug}",
|
||||
prompt_file=prompt_file,
|
||||
guest_env=self._build_guest_env(resolved_env),
|
||||
forward_host_credentials=manfiest_agent_provider.forward_host_credentials,
|
||||
auth_token=manfiest_agent_provider.auth_token,
|
||||
forward_host_credentials=manifest_agent_provider.forward_host_credentials,
|
||||
auth_token=manifest_agent_provider.auth_token,
|
||||
host_env=dict(os.environ),
|
||||
# trusted_project_path=workspace_plan.workdir,
|
||||
label=spec.label,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
This module is a thin façade. The real work lives in four siblings:
|
||||
|
||||
- prepare.py — host-side resolution into a DockerBottlePlan
|
||||
- launch.py — bring-up + teardown context manager
|
||||
- cleanup.py — orphan enumeration + removal
|
||||
- enumerate.py — active-agent listing
|
||||
- resolve_plan.py — Docker-specific resolution into a DockerBottlePlan
|
||||
- launch.py — bring-up + teardown context manager
|
||||
- cleanup.py — orphan enumeration + removal
|
||||
- enumerate.py — active-agent listing
|
||||
|
||||
The base class's `prepare` template runs cross-backend host-side
|
||||
validation before calling `_resolve_plan` here.
|
||||
@@ -53,6 +53,12 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
|
||||
launch."""
|
||||
return shutil.which("docker") is not None
|
||||
|
||||
def _preflight(self) -> None:
|
||||
_resolve_plan.preflight()
|
||||
|
||||
def _build_guest_env(self, resolved_env: ResolvedEnv) -> dict[str, str]:
|
||||
return _resolve_plan.build_guest_env(resolved_env)
|
||||
|
||||
def _resolve_plan(
|
||||
self,
|
||||
spec: BottleSpec,
|
||||
|
||||
@@ -21,12 +21,11 @@ from ...egress import EgressPlan
|
||||
from ...supervise import SupervisePlan
|
||||
from ...git_gate import GitGatePlan
|
||||
|
||||
def preflight():
|
||||
def preflight() -> None:
|
||||
docker_mod.require_docker()
|
||||
|
||||
def build_guest_env(resolved_env: ResolvedEnv):
|
||||
# resolved = resolve_env(spec.manifest, spec.agent_name)
|
||||
# forwarded_env: dict[str, str] = dict(resolved.forwarded)
|
||||
|
||||
def build_guest_env(resolved_env: ResolvedEnv) -> dict[str, str]:
|
||||
return dict(resolved_env.literals)
|
||||
|
||||
|
||||
@@ -59,4 +58,3 @@ def resolve_plan(
|
||||
agent_provision=agent_provision_plan,
|
||||
# workspace_plan=workspace_plan,
|
||||
)
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ class SmolmachinesBottleBackend(
|
||||
runtime check happens at `prepare`."""
|
||||
return _smolvm.is_available()
|
||||
|
||||
def _preflight(self) -> None:
|
||||
_resolve_plan.preflight()
|
||||
|
||||
def _build_guest_env(self, resolved_env: ResolvedEnv) -> dict[str, str]:
|
||||
return _resolve_plan.build_guest_env(resolved_env)
|
||||
|
||||
def _resolve_plan(
|
||||
self,
|
||||
spec: BottleSpec,
|
||||
|
||||
@@ -23,10 +23,11 @@ from ...git_gate import GitGatePlan
|
||||
from .bottle_plan import SmolmachinesBottlePlan
|
||||
from .util import smolmachines_bundle_subnet, smolmachines_preflight
|
||||
|
||||
def preflight():
|
||||
def preflight() -> None:
|
||||
smolmachines_preflight()
|
||||
|
||||
def build_guest_env(resolved_env: ResolvedEnv):
|
||||
|
||||
def build_guest_env(resolved_env: ResolvedEnv) -> dict[str, str]:
|
||||
# Agent's env: resolve through resolve_env() so ?prompt entries
|
||||
# are prompted and ${HOST_VAR} entries are interpolated — matching
|
||||
# the Docker backend's contract. Forwarded (secret/interpolated)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Unit: shared backend prepare wiring.
|
||||
|
||||
These tests keep the base `BottleBackend.prepare` template honest:
|
||||
backend-specific preflight/env hooks must be wired through, and launch
|
||||
metadata must record the backend that actually prepared the plan.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from bot_bottle import bottle_state
|
||||
from bot_bottle import supervise
|
||||
from bot_bottle.backend import BottleSpec
|
||||
from bot_bottle.backend.docker import DockerBottleBackend
|
||||
from bot_bottle.backend.smolmachines import SmolmachinesBottleBackend
|
||||
from bot_bottle.manifest import Manifest
|
||||
|
||||
|
||||
def _manifest() -> Manifest:
|
||||
return Manifest.from_json_obj({
|
||||
"bottles": {
|
||||
"dev": {
|
||||
"env": {
|
||||
"LITERAL_ENV": "literal-value",
|
||||
"FORWARDED_ENV": "${HOST_SECRET_ENV}",
|
||||
},
|
||||
},
|
||||
},
|
||||
"agents": {
|
||||
"demo": {
|
||||
"bottle": "dev",
|
||||
"skills": [],
|
||||
"prompt": "hello",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
def _spec(tmp: Path, *, identity: str) -> BottleSpec:
|
||||
return BottleSpec(
|
||||
manifest=_manifest(),
|
||||
agent_name="demo",
|
||||
copy_cwd=False,
|
||||
user_cwd=str(tmp),
|
||||
identity=identity,
|
||||
)
|
||||
|
||||
|
||||
class _FakeStateMixin:
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.TemporaryDirectory(prefix="backend-prepare.")
|
||||
self.root = Path(self.tmp.name) / ".bot-bottle"
|
||||
self.original_root = supervise.bot_bottle_root
|
||||
supervise.bot_bottle_root = lambda: self.root # type: ignore[assignment]
|
||||
|
||||
def tearDown(self) -> None:
|
||||
supervise.bot_bottle_root = self.original_root # type: ignore[assignment]
|
||||
self.tmp.cleanup()
|
||||
|
||||
|
||||
class TestDockerPrepare(_FakeStateMixin, unittest.TestCase):
|
||||
def test_records_backend_and_preserves_env_split(self) -> None:
|
||||
backend = DockerBottleBackend()
|
||||
spec = _spec(Path(self.tmp.name), identity="demo-docker")
|
||||
|
||||
with (
|
||||
patch.dict("os.environ", {"HOST_SECRET_ENV": "secret-value"}),
|
||||
patch(
|
||||
"bot_bottle.backend.docker.resolve_plan.docker_mod.require_docker",
|
||||
) as require_docker,
|
||||
patch(
|
||||
"bot_bottle.backend.docker.resolve_plan.docker_mod.runsc_available",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
plan = backend.prepare(spec, Path(self.tmp.name) / "stage")
|
||||
|
||||
require_docker.assert_called_once_with()
|
||||
metadata = bottle_state.read_metadata("demo-docker")
|
||||
self.assertIsNotNone(metadata)
|
||||
self.assertEqual("docker", metadata.backend)
|
||||
self.assertEqual({"FORWARDED_ENV": "secret-value"}, plan.forwarded_env)
|
||||
self.assertEqual("literal-value", plan.agent_provision.guest_env["LITERAL_ENV"])
|
||||
self.assertNotIn("FORWARDED_ENV", plan.agent_provision.guest_env)
|
||||
|
||||
|
||||
class TestSmolmachinesPrepare(_FakeStateMixin, unittest.TestCase):
|
||||
def test_records_backend_and_builds_guest_env(self) -> None:
|
||||
backend = SmolmachinesBottleBackend()
|
||||
spec = _spec(Path(self.tmp.name), identity="demo-smol")
|
||||
|
||||
with (
|
||||
patch.dict("os.environ", {"HOST_SECRET_ENV": "secret-value"}),
|
||||
patch(
|
||||
"bot_bottle.backend.smolmachines.resolve_plan.smolmachines_preflight",
|
||||
) as preflight,
|
||||
):
|
||||
plan = backend.prepare(spec, Path(self.tmp.name) / "stage")
|
||||
|
||||
preflight.assert_called_once_with()
|
||||
metadata = bottle_state.read_metadata("demo-smol")
|
||||
self.assertIsNotNone(metadata)
|
||||
self.assertEqual("smolmachines", metadata.backend)
|
||||
self.assertEqual("literal-value", plan.guest_env["LITERAL_ENV"])
|
||||
self.assertEqual("secret-value", plan.guest_env["FORWARDED_ENV"])
|
||||
self.assertEqual(
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
plan.guest_env["SSL_CERT_FILE"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user