fix: restore backend prepare wiring
This commit is contained in:
@@ -290,19 +290,19 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
|
|||||||
|
|
||||||
manifest = spec.manifest
|
manifest = spec.manifest
|
||||||
manifest_bottle = manifest.bottle_for(spec.agent_name)
|
manifest_bottle = manifest.bottle_for(spec.agent_name)
|
||||||
manfiest_agent_provider = manifest_bottle.agent_provider
|
manifest_agent_provider = manifest_bottle.agent_provider
|
||||||
agent_provider = get_provider(manfiest_agent_provider.template)
|
agent_provider = get_provider(manifest_agent_provider.template)
|
||||||
resolved_env = resolve_env(manifest, spec.agent_name)
|
resolved_env = resolve_env(manifest, spec.agent_name)
|
||||||
|
|
||||||
slug = mint_slug(spec)
|
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
|
# Manifest may override the Dockerfile per-bottle; otherwise fall
|
||||||
# back to the provider plugin's bundled Dockerfile (next to its
|
# back to the provider plugin's bundled Dockerfile (next to its
|
||||||
# agent_provider.py module).
|
# agent_provider.py module).
|
||||||
if manfiest_agent_provider.dockerfile:
|
if manifest_agent_provider.dockerfile:
|
||||||
agent_dockerfile_path = resolve_manifest_dockerfile(
|
agent_dockerfile_path = resolve_manifest_dockerfile(
|
||||||
manfiest_agent_provider.dockerfile, spec,
|
manifest_agent_provider.dockerfile, spec,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
agent_dockerfile_path = str(agent_provider.dockerfile)
|
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_dir, prompt_file = prepare_agent_state_dir(slug, spec)
|
||||||
|
|
||||||
agent_provision_plan = build_agent_provision_plan(
|
agent_provision_plan = build_agent_provision_plan(
|
||||||
template=manfiest_agent_provider.template,
|
template=manifest_agent_provider.template,
|
||||||
dockerfile=agent_dockerfile_path,
|
dockerfile=agent_dockerfile_path,
|
||||||
state_dir=agent_dir,
|
state_dir=agent_dir,
|
||||||
instance_name=f"bot-bottle-{slug}",
|
instance_name=f"bot-bottle-{slug}",
|
||||||
prompt_file=prompt_file,
|
prompt_file=prompt_file,
|
||||||
guest_env=self._build_guest_env(resolved_env),
|
guest_env=self._build_guest_env(resolved_env),
|
||||||
forward_host_credentials=manfiest_agent_provider.forward_host_credentials,
|
forward_host_credentials=manifest_agent_provider.forward_host_credentials,
|
||||||
auth_token=manfiest_agent_provider.auth_token,
|
auth_token=manifest_agent_provider.auth_token,
|
||||||
host_env=dict(os.environ),
|
host_env=dict(os.environ),
|
||||||
# trusted_project_path=workspace_plan.workdir,
|
# trusted_project_path=workspace_plan.workdir,
|
||||||
label=spec.label,
|
label=spec.label,
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
This module is a thin façade. The real work lives in four siblings:
|
This module is a thin façade. The real work lives in four siblings:
|
||||||
|
|
||||||
- prepare.py — host-side resolution into a DockerBottlePlan
|
- resolve_plan.py — Docker-specific resolution into a DockerBottlePlan
|
||||||
- launch.py — bring-up + teardown context manager
|
- launch.py — bring-up + teardown context manager
|
||||||
- cleanup.py — orphan enumeration + removal
|
- cleanup.py — orphan enumeration + removal
|
||||||
- enumerate.py — active-agent listing
|
- enumerate.py — active-agent listing
|
||||||
|
|
||||||
The base class's `prepare` template runs cross-backend host-side
|
The base class's `prepare` template runs cross-backend host-side
|
||||||
validation before calling `_resolve_plan` here.
|
validation before calling `_resolve_plan` here.
|
||||||
@@ -53,6 +53,12 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup
|
|||||||
launch."""
|
launch."""
|
||||||
return shutil.which("docker") is not None
|
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(
|
def _resolve_plan(
|
||||||
self,
|
self,
|
||||||
spec: BottleSpec,
|
spec: BottleSpec,
|
||||||
|
|||||||
@@ -21,12 +21,11 @@ from ...egress import EgressPlan
|
|||||||
from ...supervise import SupervisePlan
|
from ...supervise import SupervisePlan
|
||||||
from ...git_gate import GitGatePlan
|
from ...git_gate import GitGatePlan
|
||||||
|
|
||||||
def preflight():
|
def preflight() -> None:
|
||||||
docker_mod.require_docker()
|
docker_mod.require_docker()
|
||||||
|
|
||||||
def build_guest_env(resolved_env: ResolvedEnv):
|
|
||||||
# resolved = resolve_env(spec.manifest, spec.agent_name)
|
def build_guest_env(resolved_env: ResolvedEnv) -> dict[str, str]:
|
||||||
# forwarded_env: dict[str, str] = dict(resolved.forwarded)
|
|
||||||
return dict(resolved_env.literals)
|
return dict(resolved_env.literals)
|
||||||
|
|
||||||
|
|
||||||
@@ -59,4 +58,3 @@ def resolve_plan(
|
|||||||
agent_provision=agent_provision_plan,
|
agent_provision=agent_provision_plan,
|
||||||
# workspace_plan=workspace_plan,
|
# workspace_plan=workspace_plan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ class SmolmachinesBottleBackend(
|
|||||||
runtime check happens at `prepare`."""
|
runtime check happens at `prepare`."""
|
||||||
return _smolvm.is_available()
|
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(
|
def _resolve_plan(
|
||||||
self,
|
self,
|
||||||
spec: BottleSpec,
|
spec: BottleSpec,
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ from ...git_gate import GitGatePlan
|
|||||||
from .bottle_plan import SmolmachinesBottlePlan
|
from .bottle_plan import SmolmachinesBottlePlan
|
||||||
from .util import smolmachines_bundle_subnet, smolmachines_preflight
|
from .util import smolmachines_bundle_subnet, smolmachines_preflight
|
||||||
|
|
||||||
def preflight():
|
def preflight() -> None:
|
||||||
smolmachines_preflight()
|
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
|
# Agent's env: resolve through resolve_env() so ?prompt entries
|
||||||
# are prompted and ${HOST_VAR} entries are interpolated — matching
|
# are prompted and ${HOST_VAR} entries are interpolated — matching
|
||||||
# the Docker backend's contract. Forwarded (secret/interpolated)
|
# 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