Compare commits

..

1 Commits

Author SHA1 Message Date
didericis 4dbe44e7cc chore: SAVEPOINT
lint / lint (push) Failing after 1m35s
test / unit (pull_request) Failing after 32s
test / integration (pull_request) Failing after 18s
2026-06-08 12:28:08 -04:00
12 changed files with 194 additions and 181 deletions
+13 -14
View File
@@ -220,19 +220,18 @@ class AgentProvider(ABC):
Override for images that run as a different user or use a Override for images that run as a different user or use a
non-standard home directory.""" non-standard home directory."""
from .log import info from .log import info
# FIXME: re-enable workspace planning workspace = plan.workspace_plan
# workspace = plan.workspace_plan if workspace.enabled and workspace.copy_git and workspace.has_host_git_dir:
# if workspace.enabled and workspace.copy_git and workspace.has_host_git_dir: guest_workspace_git = f"{workspace.guest_path}/.git"
# guest_workspace_git = f"{workspace.guest_path}/.git" host_git = str(workspace.host_path / ".git")
# host_git = str(workspace.host_path / ".git") info(f"copying {host_git} -> {bottle.name}:{guest_workspace_git}")
# info(f"copying {host_git} -> {bottle.name}:{guest_workspace_git}") bottle.exec(f"mkdir -p {shlex.quote(workspace.guest_path)}", user="root")
# bottle.exec(f"mkdir -p {shlex.quote(workspace.guest_path)}", user="root") bottle.cp_in(host_git, guest_workspace_git)
# bottle.cp_in(host_git, guest_workspace_git) bottle.exec(
# bottle.exec( f"chown -R {shlex.quote(workspace.owner)} "
# f"chown -R {shlex.quote(workspace.owner)} " f"{shlex.quote(guest_workspace_git)}",
# f"{shlex.quote(guest_workspace_git)}", user="root",
# user="root", )
# )
manifest_bottle = plan.spec.manifest.bottle_for(plan.spec.agent_name) manifest_bottle = plan.spec.manifest.bottle_for(plan.spec.agent_name)
if manifest_bottle.git: if manifest_bottle.git:
@@ -328,7 +327,7 @@ def runtime_for(template: str) -> AgentProviderRuntime:
return get_provider(template).runtime return get_provider(template).runtime
def build_agent_provision_plan( def agent_provision_plan(
*, *,
template: str, template: str,
dockerfile: str, dockerfile: str,
+4 -81
View File
@@ -39,27 +39,16 @@ from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Generic, Sequence, TypeVar from typing import Any, Generic, Sequence, TypeVar
from ..agent_provider import AgentProvisionPlan, get_provider, build_agent_provision_plan from ..agent_provider import AgentProvisionPlan, get_provider
from ..egress import EgressPlan from ..egress import EgressPlan
from ..git_gate import GitGatePlan from ..git_gate import GitGatePlan
from ..log import die, info from ..log import die, info
from ..manifest import ManifestGitEntry, Manifest from ..manifest import ManifestGitEntry, Manifest
from ..supervise import SupervisePlan from ..supervise import SupervisePlan
from ..util import expand_tilde from ..util import expand_tilde
from ..env import resolve_env, ResolvedEnv
# from ..workspace import WorkspacePlan # from ..workspace import WorkspacePlan
from .print_util import print_multi, visible_agent_env_names from .print_util import print_multi, visible_agent_env_names
from .util import host_skill_dir from .util import host_skill_dir
from .resolve_common import (
merge_provision_env_vars,
mint_slug,
prepare_agent_state_dir,
prepare_egress,
prepare_git_gate,
prepare_supervise,
resolve_manifest_dockerfile,
write_launch_metadata,
)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -277,70 +266,14 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
name: str name: str
def prepare(self, spec: BottleSpec, stage_dir: Path) -> PlanT: def prepare(self, spec: BottleSpec, *, stage_dir: Path) -> PlanT:
"""Template method: run cross-backend host-side validation, then """Template method: run cross-backend host-side validation, then
delegate to the subclass's `_resolve_plan` for the delegate to the subclass's `_resolve_plan` for the
backend-specific resolution (names, scratch files, etc.). The backend-specific resolution (names, scratch files, etc.). The
validation step is enforced here so a future backend cannot validation step is enforced here so a future backend cannot
accidentally skip it. No remote/runtime resources are created.""" accidentally skip it. No remote/runtime resources are created."""
self._validate(spec) self._validate(spec)
return self._resolve_plan(spec, stage_dir=stage_dir)
self._preflight()
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)
agent_image = agent_provider.runtime.image
resolved_env = resolve_env(manifest, spec.agent_name)
slug = mint_slug(spec)
write_launch_metadata(slug, spec, compose_project="", backend="smolmachines")
agent_dockerfile_path = resolve_manifest_dockerfile(manfiest_agent_provider.dockerfile, spec)
instance_name = f"bot-bottle-{slug}"
agent_dir, prompt_file = prepare_agent_state_dir(slug, spec)
agent_provision_plan = build_agent_provision_plan(
template=manfiest_agent_provider.template,
dockerfile=agent_dockerfile_path,
state_dir=agent_dir,
guest_home="/home/node", # FIXME: should be coming from the agent plan
guest_env=self._build_guest_env(resolved_env),
forward_host_credentials=manfiest_agent_provider.forward_host_credentials,
auth_token=manfiest_agent_provider.auth_token,
host_env=dict(os.environ),
# trusted_project_path=workspace_plan.workdir,
label=spec.label,
color=spec.color,
)
agent_provision_plan = merge_provision_env_vars(agent_provision_plan)
egress_plan = prepare_egress(manifest_bottle, slug, agent_provision_plan)
supervise_plan = prepare_supervise(manifest_bottle, slug)
git_gate_plan = prepare_git_gate(manifest_bottle, slug)
return self._resolve_plan(
spec,
instance_name=instance_name, # FIXME: move to agent provision plan
agent_image=agent_image, # FIXME: move to agent provision plan
prompt_file=prompt_file, # FIXME: move to agent provision plan
agent_dockerfile_path=agent_dockerfile_path, # FIXME: move to agent provision plan
agent_provision_plan=agent_provision_plan,
egress_plan=egress_plan,
supervise_plan=supervise_plan,
git_gate_plan=git_gate_plan,
stage_dir=stage_dir
)
def _build_guest_env(self, resolved_env: ResolvedEnv) -> dict[str, str]:
return {}
def _preflight(self) -> None:
"""
tasks to do before resolving a plan
"""
pass
def _validate(self, spec: BottleSpec) -> None: def _validate(self, spec: BottleSpec) -> None:
"""Cross-backend pre-launch checks. Confirms the agent exists, """Cross-backend pre-launch checks. Confirms the agent exists,
@@ -392,17 +325,7 @@ class BottleBackend(ABC, Generic[PlanT, CleanupT]):
) )
@abstractmethod @abstractmethod
def _resolve_plan(self, def _resolve_plan(self, spec: BottleSpec, *, stage_dir: Path) -> PlanT:
spec: BottleSpec,
instance_name: str,
agent_image: str,
prompt_file: Path,
agent_provision_plan: AgentProvisionPlan,
agent_dockerfile_path: str,
egress_plan: EgressPlan,
git_gate_plan: GitGatePlan,
supervise_plan: SupervisePlan | None,
stage_dir: Path) -> PlanT:
"""Backend-specific plan resolution: image/container names, """Backend-specific plan resolution: image/container names,
env-file, prompt-file, proxy plan, runtime detection. Called by env-file, prompt-file, proxy plan, runtime detection. Called by
`prepare` after `_validate` succeeds.""" `prepare` after `_validate` succeeds."""
+1
View File
@@ -29,6 +29,7 @@ class DockerBottlePlan(BottlePlan):
# state file (~/.bot-bottle/state/<slug>/Dockerfile) after a # state file (~/.bot-bottle/state/<slug>/Dockerfile) after a
# capability-block remediation (PRD 0016). # capability-block remediation (PRD 0016).
dockerfile_path: str dockerfile_path: str
env_file: Path # docker --env-file: NAME=VALUE literals
# name -> value for vars forwarded into the docker-run child process # name -> value for vars forwarded into the docker-run child process
# via subprocess env (so values never land on argv or in a file). # via subprocess env (so values never land on argv or in a file).
# repr=False keeps secret/interpolated/OAuth values out of any # repr=False keeps secret/interpolated/OAuth values out of any
+2
View File
@@ -230,6 +230,8 @@ def _agent_service(plan: DockerBottlePlan) -> dict[str, Any]:
} }
if plan.use_runsc: if plan.use_runsc:
service["runtime"] = "runsc" service["runtime"] = "runsc"
if plan.env_file and plan.env_file.exists() and plan.env_file.stat().st_size > 0:
service["env_file"] = [str(plan.env_file)]
volumes: list[dict[str, Any]] = [] volumes: list[dict[str, Any]] = []
if plan.supervise_plan is not None: if plan.supervise_plan is not None:
+87 -27
View File
@@ -1,52 +1,94 @@
"""Prepare step for the Docker bottle backend. """Prepare step for the Docker bottle backend.
`resolve_plan` does all host-side resolution (image and container `resolve_plan` does all host-side resolution (image and container
names, prompt-file, proxy plan, runtime detection) and returns a names, env-file, prompt-file, proxy plan, runtime detection) and
frozen DockerBottlePlan. No Docker resources are created; the only returns a frozen DockerBottlePlan. No Docker resources are created;
side effects are scratch files under `stage_dir` and a probe of the only side effects are scratch files under `stage_dir` and a probe
`docker info`. Cross-backend host-side validation has already run of `docker info`. Cross-backend host-side validation has already run
via the base class's `prepare` template before this is called. via the base class's `prepare` template before this is called.
""" """
from __future__ import annotations from __future__ import annotations
import os
from pathlib import Path from pathlib import Path
from ...agent_provider import agent_provision_plan, get_provider
from ...env import ResolvedEnv, resolve_env
from ...log import die
# from ...workspace import workspace_plan as resolve_workspace_plan
from .. import BottleSpec
from ..resolve_common import (
merge_provision_env_vars,
mint_slug,
prepare_agent_state_dir,
prepare_egress,
prepare_git_gate,
prepare_supervise,
resolve_manifest_dockerfile,
write_launch_metadata,
)
from . import util as docker_mod from . import util as docker_mod
from .bottle_plan import DockerBottlePlan from .bottle_plan import DockerBottlePlan
from .. import BottleSpec # from ...bottle_state import (
from ...env import ResolvedEnv # # clear_preserve_marker,
from ...agent_provider import AgentProvisionPlan # per_bottle_dockerfile,
from ...egress import EgressPlan # per_bottle_dockerfile_path,
from ...supervise import SupervisePlan # per_bottle_image_tag,
from ...git_gate import GitGatePlan # )
from .sidecar_bundle import sidecar_bundle_container_name
def preflight(): def preflight():
docker_mod.require_docker() 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)
return dict(resolved_env.literals)
def resolve_plan( def resolve_plan(
spec: BottleSpec, spec: BottleSpec,
slug: str, *,
resolved_env: ResolvedEnv,
instance_name: str,
agent_image: str,
agent_dockerfile_path: str,
prompt_file: Path,
agent_provision_plan: AgentProvisionPlan,
egress_plan: EgressPlan,
supervise_plan: SupervisePlan,
git_gate_plan: GitGatePlan,
stage_dir: Path, stage_dir: Path,
) -> DockerBottlePlan: ) -> DockerBottlePlan:
"""Resolve Docker-specific names and write scratch files. Trusts """Resolve Docker-specific names and write scratch files. Trusts
that the agent and its skills/git-gate keys are present that the agent and its skills/git-gate keys are present
validation already ran in the base class.""" validation already ran in the base class."""
preflight()
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)
slug = mint_slug(spec)
# FIXME: don't thin the compose project should be directly written to metadata like this,
# should probably be a backend specific metadata field for details like this
write_launch_metadata(slug, spec, compose_project=f"bot-bottle-{slug}", backend="docker")
agent_image = agent_provider.runtime.image
agent_dockerfile_path = resolve_manifest_dockerfile(manfiest_agent_provider.dockerfile, spec)
instance_name = f"bot-bottle-{slug}"
agent_dir, prompt_file = prepare_agent_state_dir(slug, spec)
env_file = agent_dir / "agent.env"
agent_provision = agent_provision_plan(
template=manfiest_agent_provider.template,
dockerfile=agent_dockerfile_path,
state_dir=agent_dir,
guest_home="/home/node", # FIXME: should be coming from the agent plan
forward_host_credentials=manfiest_agent_provider.forward_host_credentials,
auth_token=manfiest_agent_provider.auth_token,
host_env=dict(os.environ),
# trusted_project_path=workspace_plan.workdir,
label=spec.label,
color=spec.color,
)
agent_provision = merge_provision_env_vars(agent_provision)
egress_plan = prepare_egress(manifest_bottle, slug, agent_provision)
supervise_plan = prepare_supervise(manifest_bottle, slug)
git_gate_plan = prepare_git_gate(manifest_bottle, slug)
resolved = resolve_env(manifest, spec.agent_name)
forwarded_env: dict[str, str] = dict(resolved.forwarded)
_write_env_file(resolved, env_file)
# ==== docker specific setup ==== # ==== docker specific setup ====
use_runsc = docker_mod.runsc_available() use_runsc = docker_mod.runsc_available()
@@ -59,14 +101,32 @@ def resolve_plan(
# container_name_pinned=container_name_pinned, # container_name_pinned=container_name_pinned,
image=agent_image, image=agent_image,
dockerfile_path=agent_dockerfile_path, dockerfile_path=agent_dockerfile_path,
forwarded_env=dict(resolved_env.forwarded), env_file=env_file,
forwarded_env=forwarded_env,
prompt_file=prompt_file, prompt_file=prompt_file,
git_gate_plan=git_gate_plan, git_gate_plan=git_gate_plan,
egress_plan=egress_plan, egress_plan=egress_plan,
supervise_plan=supervise_plan, supervise_plan=supervise_plan,
use_runsc=use_runsc, use_runsc=use_runsc,
agent_provision=agent_provision_plan, agent_provision=agent_provision,
# workspace_plan=workspace_plan, # workspace_plan=workspace_plan,
) )
def _write_env_file(resolved: ResolvedEnv, env_file: Path) -> None:
"""Serialize the literal portion of a ResolvedEnv into docker's
`--env-file` syntax (NAME=VALUE per line, mode 600 since the file
may carry verbatim values from the manifest). Forwarded names ride
on the plan as a structured tuple instead."""
env_lines: list[str] = []
for name, value in resolved.literals.items():
if "\n" in value:
die(
f"env entry {name} (literal) contains a newline; "
f"docker --env-file cannot represent multi-line values."
)
env_lines.append(f"{name}={value}")
env_file.write_text("\n".join(env_lines) + ("\n" if env_lines else ""))
env_file.chmod(0o600)
+4 -2
View File
@@ -83,14 +83,16 @@ def prepare_egress(
return Egress().prepare(bottle, slug, egress_dir, provision.egress_routes) return Egress().prepare(bottle, slug, egress_dir, provision.egress_routes)
def prepare_supervise(bottle: ManifestBottle, slug: str) -> SupervisePlan | None: def prepare_supervise(
bottle: ManifestBottle, slug: str, *, dockerfile_content: str = "",
) -> SupervisePlan | None:
"""Prepare the supervise sidecar state dir. Returns None when """Prepare the supervise sidecar state dir. Returns None when
bottle.supervise is falsy.""" bottle.supervise is falsy."""
if not bottle.supervise: if not bottle.supervise:
return None return None
supervise_dir = supervise_state_dir(slug) supervise_dir = supervise_state_dir(slug)
supervise_dir.mkdir(parents=True, exist_ok=True) supervise_dir.mkdir(parents=True, exist_ok=True)
return Supervise().prepare(slug, supervise_dir) return Supervise().prepare(slug, supervise_dir, dockerfile_content=dockerfile_content)
def merge_provision_env_vars(provision: AgentProvisionPlan) -> AgentProvisionPlan: def merge_provision_env_vars(provision: AgentProvisionPlan) -> AgentProvisionPlan:
@@ -49,7 +49,7 @@ class SmolmachinesBottlePlan(BottlePlan):
# `machine_create --from`. The pipeline runs at launch time # `machine_create --from`. The pipeline runs at launch time
# (not prepare time) so the docker build output doesn't garble # (not prepare time) so the docker build output doesn't garble
# the dashboard's preflight modal. # the dashboard's preflight modal.
agent_image: str agent_image_ref: str
# In-guest env vars (HTTPS_PROXY etc) — IP-literal URLs since # In-guest env vars (HTTPS_PROXY etc) — IP-literal URLs since
# the guest has no DNS resolver inside the TSI allowlist. # the guest has no DNS resolver inside the TSI allowlist.
# Passed to `smolvm machine create` as `-e K=V` flags. # Passed to `smolvm machine create` as `-e K=V` flags.
+1 -1
View File
@@ -90,7 +90,7 @@ def launch(
# here, not in prepare, so the docker-build output doesn't # here, not in prepare, so the docker-build output doesn't
# garble the dashboard's preflight modal. # garble the dashboard's preflight modal.
agent_from_path = _ensure_smolmachine( agent_from_path = _ensure_smolmachine(
plan.agent_image, plan.agent_image_ref,
dockerfile=plan.agent_dockerfile_path, dockerfile=plan.agent_dockerfile_path,
) )
+68 -42
View File
@@ -10,56 +10,31 @@ No VM bringup — that's `launch.launch`'s job."""
from __future__ import annotations from __future__ import annotations
import os
from pathlib import Path from pathlib import Path
from .. import BottleSpec from ...agent_provider import PROVIDER_TEMPLATES, agent_provision_plan, get_provider
from ...env import ResolvedEnv
from ...agent_provider import AgentProvisionPlan
from ...egress import EgressPlan
from ...supervise import SupervisePlan
from ...git_gate import GitGatePlan
from ...backend import BottleSpec from ...backend import BottleSpec
from ...env import ResolvedEnv from ...env import resolve_env
# from ...workspace import workspace_plan as resolve_workspace_plan # from ...workspace import workspace_plan as resolve_workspace_plan
from ..resolve_common import (
merge_provision_env_vars,
mint_slug,
prepare_agent_state_dir,
prepare_egress,
prepare_git_gate,
prepare_supervise,
resolve_manifest_dockerfile,
write_launch_metadata,
)
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():
smolmachines_preflight() smolmachines_preflight()
def build_guest_env(resolved_env: ResolvedEnv):
# 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)
# values still reach the guest as -e K=V smolvm flags because
# smolvm 0.8.0 has no env-file or stdin injection path; this is
# the known argv-exposure gap documented in PRD 0038.
# HTTPS_PROXY / GIT_GATE_URL / MCP_SUPERVISE_URL are populated
# in launch.py after bundle bringup.
return {
**resolved_env.literals,
**resolved_env.forwarded,
"NO_PROXY": "localhost,127.0.0.1",
"NODE_EXTRA_CA_CERTS": "/etc/ssl/certs/ca-certificates.crt",
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
"REQUESTS_CA_BUNDLE": "/etc/ssl/certs/ca-certificates.crt",
}
def resolve_plan( def resolve_plan(
spec: BottleSpec, spec: BottleSpec, *, stage_dir: Path
slug: str,
resolved_env: ResolvedEnv,
instance_name: str,
agent_image: str,
agent_dockerfile_path: str,
prompt_file: Path,
agent_provision_plan: AgentProvisionPlan,
egress_plan: EgressPlan,
supervise_plan: SupervisePlan,
git_gate_plan: GitGatePlan,
stage_dir: Path,
) -> SmolmachinesBottlePlan: ) -> SmolmachinesBottlePlan:
"""Materialize the smolmachines plan. The bundle's docker """Materialize the smolmachines plan. The bundle's docker
subnet + pinned IP are derived from the slug; the agent's subnet + pinned IP are derived from the slug; the agent's
@@ -68,9 +43,60 @@ def resolve_plan(
pull. Per-bottle guest env + the TSI allow_cidrs land on the pull. Per-bottle guest env + the TSI allow_cidrs land on the
plan for launch to pass straight through to plan for launch to pass straight through to
`machine create` flags.""" `machine create` flags."""
preflight()
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)
slug = mint_slug(spec)
write_launch_metadata(slug, spec, compose_project="", backend="smolmachines")
# ==== smolmachines specific setup ==== # ==== smolmachines specific setup ====
subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug) subnet, gateway, bundle_ip = smolmachines_bundle_subnet(slug)
# 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)
# values still reach the guest as -e K=V smolvm flags because
# smolvm 0.8.0 has no env-file or stdin injection path; this is
# the known argv-exposure gap documented in PRD 0038.
# HTTPS_PROXY / GIT_GATE_URL / MCP_SUPERVISE_URL are populated
# in launch.py after bundle bringup.
resolved = resolve_env(manifest, spec.agent_name)
guest_env: dict[str, str] = {
**resolved.literals,
**resolved.forwarded,
"NO_PROXY": "localhost,127.0.0.1",
"NODE_EXTRA_CA_CERTS": "/etc/ssl/certs/ca-certificates.crt",
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt",
"REQUESTS_CA_BUNDLE": "/etc/ssl/certs/ca-certificates.crt",
}
# ==============
agent_dockerfile_path = resolve_manifest_dockerfile(manfiest_agent_provider.dockerfile, spec)
instance_name = f"bot-bottle-{slug}"
agent_dir, prompt_file = prepare_agent_state_dir(slug, spec)
agent_provision = agent_provision_plan(
template=manfiest_agent_provider.template,
dockerfile=agent_dockerfile_path,
state_dir=agent_dir,
guest_home="/home/node", # FIXME: should be coming from the agent plan
guest_env=guest_env,
forward_host_credentials=manfiest_agent_provider.forward_host_credentials,
auth_token=manfiest_agent_provider.auth_token,
host_env=dict(os.environ),
# trusted_project_path=workspace_plan.workdir,
label=spec.label,
color=spec.color,
)
agent_provision = merge_provision_env_vars(agent_provision)
egress_plan = prepare_egress(manifest_bottle, slug, agent_provision)
supervise_plan = prepare_supervise(manifest_bottle, slug)
git_gate_plan = prepare_git_gate(manifest_bottle, slug)
return SmolmachinesBottlePlan( return SmolmachinesBottlePlan(
spec=spec, spec=spec,
@@ -80,12 +106,12 @@ def resolve_plan(
bundle_gateway=gateway, bundle_gateway=gateway,
bundle_ip=bundle_ip, bundle_ip=bundle_ip,
machine_name=instance_name, machine_name=instance_name,
agent_image=agent_image, agent_image_ref=agent_image_ref,
guest_env=agent_provision_plan.guest_env, guest_env=agent_provision.guest_env,
prompt_file=prompt_file, prompt_file=prompt_file,
git_gate_plan=git_gate_plan, git_gate_plan=git_gate_plan,
egress_plan=egress_plan, egress_plan=egress_plan,
supervise_plan=supervise_plan, supervise_plan=supervise_plan,
agent_provision=agent_provision_plan, agent_provision=agent_provision,
# workspace_plan=workspace_plan, # workspace_plan=workspace_plan,
) )
+11 -11
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from bot_bottle.agent_provider import ( from bot_bottle.agent_provider import (
CODEX_HOST_CREDENTIAL_HOSTS, CODEX_HOST_CREDENTIAL_HOSTS,
build_agent_provision_plan, agent_provision_plan,
) )
from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF from bot_bottle.egress import CODEX_HOST_CREDENTIAL_TOKEN_REF
@@ -25,7 +25,7 @@ def _jwt(exp: int) -> str:
class TestAgentProviderRuntime(unittest.TestCase): class TestAgentProviderRuntime(unittest.TestCase):
def test_codex_plan_declares_home_state(self): def test_codex_plan_declares_home_state(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="/tmp/Dockerfile.codex", dockerfile="/tmp/Dockerfile.codex",
@@ -50,7 +50,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
def test_codex_trusts_requested_project_path(self): def test_codex_trusts_requested_project_path(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
build_agent_provision_plan( agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="", dockerfile="",
@@ -68,7 +68,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
"auth_mode": "chatgpt", "auth_mode": "chatgpt",
"tokens": {"access_token": _jwt(2000000000)}, "tokens": {"access_token": _jwt(2000000000)},
})) }))
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="", dockerfile="",
@@ -88,7 +88,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
def test_claude_with_auth_token_injects_provider_route_and_placeholder(self): def test_claude_with_auth_token_injects_provider_route_and_placeholder(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="claude", template="claude",
dockerfile="/tmp/Dockerfile.claude", dockerfile="/tmp/Dockerfile.claude",
@@ -110,7 +110,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
def test_claude_trusts_requested_project_path(self): def test_claude_trusts_requested_project_path(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
build_agent_provision_plan( agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="claude", template="claude",
dockerfile="", dockerfile="",
@@ -129,7 +129,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
"auth_mode": "chatgpt", "auth_mode": "chatgpt",
"tokens": {"access_token": _jwt(2000000000)}, "tokens": {"access_token": _jwt(2000000000)},
})) }))
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="", dockerfile="",
@@ -145,7 +145,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
def test_codex_without_forward_host_credentials_has_passthrough_egress_routes(self): def test_codex_without_forward_host_credentials_has_passthrough_egress_routes(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="", dockerfile="",
@@ -162,7 +162,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
def test_claude_without_auth_token_has_passthrough_egress_route(self): def test_claude_without_auth_token_has_passthrough_egress_route(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="claude", template="claude",
dockerfile="", dockerfile="",
@@ -185,7 +185,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
"auth_mode": "chatgpt", "auth_mode": "chatgpt",
"tokens": {"access_token": access}, "tokens": {"access_token": access},
})) }))
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="", dockerfile="",
@@ -200,7 +200,7 @@ class TestAgentProviderRuntime(unittest.TestCase):
def test_codex_without_forward_host_credentials_has_empty_provisioned_env(self): def test_codex_without_forward_host_credentials_has_empty_provisioned_env(self):
with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp: with tempfile.TemporaryDirectory(prefix="bb-provider.") as tmp:
plan = build_agent_provision_plan( plan = agent_provision_plan(
guest_home="/home/node", guest_home="/home/node",
template="codex", template="codex",
dockerfile="", dockerfile="",
+1 -1
View File
@@ -130,7 +130,7 @@ def _smolmachines_plan(spec: BottleSpec, tmp: str) -> SmolmachinesBottlePlan:
bundle_gateway="10.99.0.1", bundle_gateway="10.99.0.1",
bundle_ip="10.99.0.2", bundle_ip="10.99.0.2",
machine_name="bot-bottle-test-00001", machine_name="bot-bottle-test-00001",
agent_image="bot-bottle-claude:latest", agent_image_ref="bot-bottle-claude:latest",
guest_env={"HTTPS_PROXY": "http://127.0.0.1:9999"}, guest_env={"HTTPS_PROXY": "http://127.0.0.1:9999"},
prompt_file=stage / "prompt.txt", prompt_file=stage / "prompt.txt",
) )
+1 -1
View File
@@ -147,7 +147,7 @@ def _plan(
bundle_gateway="192.168.50.1", bundle_gateway="192.168.50.1",
bundle_ip=bundle_ip, bundle_ip=bundle_ip,
machine_name="bot-bottle-demo-abc12", machine_name="bot-bottle-demo-abc12",
agent_image="bot-bottle-claude:latest", agent_image_ref="bot-bottle-claude:latest",
guest_env=dict(guest_env or {}), guest_env=dict(guest_env or {}),
prompt_file=Path("/tmp/state/demo-abc12/agent/prompt.txt"), prompt_file=Path("/tmp/state/demo-abc12/agent/prompt.txt"),
git_gate_plan=GitGatePlan( git_gate_plan=GitGatePlan(