feat(smolmachines): thread inner Plans + bundle daemons run (PRD 0023 chunk 4b) #70

Merged
didericis-claude merged 5 commits from prd-0023-chunk-4b-inner-plans into main 2026-05-27 13:21:42 -04:00
10 changed files with 99 additions and 124 deletions
Showing only changes of commit 73dc0d4a40 - Show all commits
+7 -14
View File
@@ -1,11 +1,11 @@
"""DockerEgress — the Docker-specific lifecycle for the """Docker-side egress helpers: port pin, in-container CA paths,
per-bottle egress sidecar (PRD 0017). Inherits the platform- container naming, and the host-side mitmproxy CA mint. The
agnostic prepare step (route lift + routes.yaml render + token-env prepare-time routes-yaml rendering itself lives on the
map derivation) from `Egress`. platform-neutral `Egress` ABC — backends instantiate it directly.
Chunks 1+2 of the PRD: the lifecycle is implemented and wired into The per-container `.start()` / `.stop()` lifecycle was removed in
launch.py — cred-proxy is gone. Chunk 3 retargets the cred-proxy- PRD 0024 chunk 3; the sidecar bundle (PRD 0024) runs egress
block remediation flow (PRD 0014).""" under its python init supervisor."""
from __future__ import annotations from __future__ import annotations
@@ -13,7 +13,6 @@ import os
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from ...egress import Egress
from ...log import die from ...log import die
@@ -130,9 +129,3 @@ def egress_tls_init(stage_dir: Path) -> tuple[Path, Path]:
mitm.write_bytes(cert_path.read_bytes() + key_path.read_bytes()) mitm.write_bytes(cert_path.read_bytes() + key_path.read_bytes())
mitm.chmod(0o644) mitm.chmod(0o644)
return (mitm, cert_path) return (mitm, cert_path)
class DockerEgress(Egress):
"""Docker-flavored Egress: inherits `.prepare()` from the base.
Container lifecycle is owned by compose; per-container
`.start()` / `.stop()` were removed in PRD 0024 chunk 3."""
+6 -13
View File
@@ -1,13 +1,12 @@
"""DockerGitGate — Docker-flavored git-gate config (PRD 0008). """Docker-side git-gate helpers: in-container paths the renderer's
Inherits the platform-agnostic prepare step (upstream lift + bind-mounts target, port pin, and container naming. The
entrypoint/hook render) from `GitGate`. The git-gate daemon runs prepare-time entrypoint/hook render lives on the platform-neutral
inside the sidecar bundle (PRD 0024); this module just holds the `GitGate` ABC — backends instantiate it directly. The git-gate
in-container paths the renderer's bind-mounts target.""" daemon's container lifecycle is owned by the sidecar bundle
(PRD 0024)."""
from __future__ import annotations from __future__ import annotations
from ...git_gate import GitGate
GIT_GATE_ENTRYPOINT_IN_CONTAINER = "/git-gate-entrypoint.sh" GIT_GATE_ENTRYPOINT_IN_CONTAINER = "/git-gate-entrypoint.sh"
GIT_GATE_HOOK_IN_CONTAINER = "/etc/git-gate/pre-receive" GIT_GATE_HOOK_IN_CONTAINER = "/etc/git-gate/pre-receive"
@@ -31,9 +30,3 @@ def git_gate_host(slug: str) -> str:
the bundle's network alias to the bundle container, where the the bundle's network alias to the bundle container, where the
git-gate daemon listens on GIT_GATE_PORT.""" git-gate daemon listens on GIT_GATE_PORT."""
return git_gate_container_name(slug) return git_gate_container_name(slug)
class DockerGitGate(GitGate):
"""Docker-flavored GitGate: inherits `.prepare()` from the base.
The git-gate daemon's container lifecycle is owned by the
sidecar bundle (PRD 0024)."""
+13 -25
View File
@@ -1,14 +1,12 @@
"""DockerPipelockProxy — the Docker-specific implementation of the """Docker-side pipelock helpers: image pin, container naming, and
sidecar's `.prepare()` step + in-container CA path constants. the one-shot `pipelock tls init` host-side CA mint. The
Inherits the platform-agnostic YAML-config generation from prepare-time YAML rendering itself lives on the platform-neutral
PipelockProxy. `PipelockProxy` ABC — backends instantiate it directly.
The per-container `.start()` / `.stop()` lifecycle was deleted in The per-container `.start()` / `.stop()` lifecycle was deleted in
PRD 0024 chunk 3 compose-up owns the container lifecycle (PRD PRD 0024 chunk 3; compose-up owns the container lifecycle (PRD
0018) and the bundle path (PRD 0024) collapses pipelock + egress 0018) and the bundle path (PRD 0024) collapses pipelock + egress
+ git-gate + supervise into one container. What remains here is + git-gate + supervise into one container."""
the prepare-time YAML rendering + the CA path constants the
compose renderer reads."""
from __future__ import annotations from __future__ import annotations
@@ -17,7 +15,13 @@ import subprocess
from pathlib import Path from pathlib import Path
from ...log import die from ...log import die
from ...pipelock import PipelockProxy # Re-exported for the compose renderer + smolmachines launch step
# (they used to import these from this module before they moved to
# the platform-neutral pipelock module).
from ...pipelock import ( # noqa: F401
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
)
# Pipelock image, pinned by digest. The digest is the multi-arch image # Pipelock image, pinned by digest. The digest is the multi-arch image
@@ -30,12 +34,6 @@ PIPELOCK_IMAGE = os.environ.get(
# Listening port for pipelock's forward proxy. # Listening port for pipelock's forward proxy.
PIPELOCK_PORT = os.environ.get("CLAUDE_BOTTLE_PIPELOCK_PORT", "8888") PIPELOCK_PORT = os.environ.get("CLAUDE_BOTTLE_PIPELOCK_PORT", "8888")
# In-container paths where the per-bottle CA cert + key land via
# the compose renderer's bind-mounts. Pipelock's rendered YAML
# references these paths under `tls_interception`.
PIPELOCK_CA_CERT_IN_CONTAINER = "/etc/pipelock-ca.pem"
PIPELOCK_CA_KEY_IN_CONTAINER = "/etc/pipelock-ca-key.pem"
def pipelock_container_name(slug: str) -> str: def pipelock_container_name(slug: str) -> str:
return f"claude-bottle-pipelock-{slug}" return f"claude-bottle-pipelock-{slug}"
@@ -82,13 +80,3 @@ def pipelock_tls_init(stage_dir: Path) -> tuple[Path, Path]:
key.chmod(0o600) key.chmod(0o600)
cert.chmod(0o644) cert.chmod(0o644)
return (cert, key) return (cert, key)
class DockerPipelockProxy(PipelockProxy):
"""Docker-flavored PipelockProxy: inherits `.prepare()` from the
base, exposes the in-container CA paths the renderer reads.
Container lifecycle is owned by compose."""
CA_CERT_IN_CONTAINER = PIPELOCK_CA_CERT_IN_CONTAINER
CA_KEY_IN_CONTAINER = PIPELOCK_CA_KEY_IN_CONTAINER
+12 -8
View File
@@ -14,13 +14,17 @@ import os
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from ...egress import Egress
from ...env import ResolvedEnv, resolve_env from ...env import ResolvedEnv, resolve_env
from ...git_gate import GitGate
from ...log import die from ...log import die
from ...pipelock import PipelockProxy
from ...supervise import Supervise
from .. import BottleSpec from .. import BottleSpec
from . import util as docker_mod from . import util as docker_mod
from .bottle_plan import DockerBottlePlan from .bottle_plan import DockerBottlePlan
from .egress import DockerEgress, egress_container_name from .egress import egress_container_name
from .git_gate import DockerGitGate, git_gate_container_name from .git_gate import git_gate_container_name
from .bottle_state import ( from .bottle_state import (
BottleMetadata, BottleMetadata,
agent_state_dir, agent_state_dir,
@@ -35,8 +39,8 @@ from .bottle_state import (
supervise_state_dir, supervise_state_dir,
write_metadata, write_metadata,
) )
from .pipelock import DockerPipelockProxy, pipelock_container_name from .pipelock import pipelock_container_name
from .supervise import DockerSupervise, supervise_container_name from .supervise import supervise_container_name
def resolve_plan( def resolve_plan(
@@ -49,10 +53,10 @@ def resolve_plan(
validation already ran in the base class.""" validation already ran in the base class."""
docker_mod.require_docker() docker_mod.require_docker()
proxy = DockerPipelockProxy() proxy = PipelockProxy()
git_gate = DockerGitGate() git_gate = GitGate()
egress = DockerEgress() egress = Egress()
supervise = DockerSupervise() supervise = Supervise()
manifest = spec.manifest manifest = spec.manifest
agent = manifest.agents[spec.agent_name] agent = manifest.agents[spec.agent_name]
+7 -13
View File
@@ -1,13 +1,13 @@
"""DockerSupervise — Docker-flavored supervise config (PRD 0013). """Docker-side supervise helpers: container naming for the legacy
Inherits the platform-agnostic prepare step (queue dir + per-sidecar topology (kept so the bundle's docker-network alias
current-config staging) from `Supervise`. The supervise daemon resolves the old name to the bundle IP). The prepare-time
Outdated
Review

We don't care about preserving the legacy topology

We don't care about preserving the legacy topology
Outdated
Review

Dropped in 8ecba2b — deleted claude_bottle/backend/docker/supervise.py entirely (only legacy-name function left after the refactor), removed the network alias in compose.py, removed the orphan probe in prepare.py, and stopped asserting the long-form alias in test_compose.py. The bundle keeps the short supervise alias since SUPERVISE_HOSTNAME is what consumers actually use.

Dropped in 8ecba2b — deleted `claude_bottle/backend/docker/supervise.py` entirely (only legacy-name function left after the refactor), removed the network alias in `compose.py`, removed the orphan probe in `prepare.py`, and stopped asserting the long-form alias in `test_compose.py`. The bundle keeps the short `supervise` alias since `SUPERVISE_HOSTNAME` is what consumers actually use.
Outdated
Review

The other container name functions can probably go too, right?

The other container name functions can probably go too, right?
Outdated
Review

Yep — done in 727f30d. Killed pipelock_container_name, egress_container_name, git_gate_container_name, plus pipelock_proxy_url and git_gate_host since they only existed to build URLs with the legacy long-form names. Added PIPELOCK_HOSTNAME = "pipelock" and GIT_GATE_HOSTNAME = "git-gate" (mirroring EGRESS_HOSTNAME = "egress"), so the agent's HTTPS_PROXY is now http://pipelock:8888 and the gitconfig insteadOf rewrites are git://git-gate/<repo>.git. The bundle-internal egress→pipelock hop drops the DNS alias entirely and uses loopback (BUNDLE_LOCAL_PIPELOCK_URL = http://127.0.0.1:8888) — same pattern smolmachines was already using.

The prepare-time orphan probe collapses to one check on the bundle container name (claude-bottle-sidecars-<slug>) instead of the four legacy per-sidecar names nothing creates anymore.

One thing I left alone: capability_apply._per_bottle_container_names still lists the four legacy names in its teardown helper. Harmless (missing containers are silently skipped) but technically also dead. Easy to wipe in a follow-up if you want.

Yep — done in 727f30d. Killed `pipelock_container_name`, `egress_container_name`, `git_gate_container_name`, plus `pipelock_proxy_url` and `git_gate_host` since they only existed to build URLs with the legacy long-form names. Added `PIPELOCK_HOSTNAME = "pipelock"` and `GIT_GATE_HOSTNAME = "git-gate"` (mirroring `EGRESS_HOSTNAME = "egress"`), so the agent's HTTPS_PROXY is now `http://pipelock:8888` and the gitconfig insteadOf rewrites are `git://git-gate/<repo>.git`. The bundle-internal egress→pipelock hop drops the DNS alias entirely and uses loopback (`BUNDLE_LOCAL_PIPELOCK_URL = http://127.0.0.1:8888`) — same pattern smolmachines was already using. The prepare-time orphan probe collapses to one check on the bundle container name (`claude-bottle-sidecars-<slug>`) instead of the four legacy per-sidecar names nothing creates anymore. One thing I left alone: `capability_apply._per_bottle_container_names` still lists the four legacy names in its teardown helper. Harmless (missing containers are silently skipped) but technically also dead. Easy to wipe in a follow-up if you want.
Outdated
Review

wipe that out too

wipe that out too
Review

Done in 519a71f. _per_bottle_container_names is now just [agent, sidecar-bundle]; the four legacy per-sidecar literals are gone. Integration test follows — the fake supervise sidecar (which existed to give teardown an extra container to nuke) switches to a fake sidecar bundle.

Done in 519a71f. `_per_bottle_container_names` is now just `[agent, sidecar-bundle]`; the four legacy per-sidecar literals are gone. Integration test follows — the fake supervise sidecar (which existed to give teardown an extra container to nuke) switches to a fake sidecar bundle.
runs inside the sidecar bundle (PRD 0024); this module just holds queue-dir + current-config staging lives on the platform-neutral
the container-name helper the renderer's network alias targets.""" `Supervise` ABC — backends instantiate it directly. The
supervise daemon's container lifecycle is owned by the sidecar
bundle (PRD 0024)."""
from __future__ import annotations from __future__ import annotations
from ...supervise import Supervise
def supervise_container_name(slug: str) -> str: def supervise_container_name(slug: str) -> str:
"""The legacy per-sidecar container name. Kept as a function so """The legacy per-sidecar container name. Kept as a function so
@@ -15,9 +15,3 @@ def supervise_container_name(slug: str) -> str:
bundle — any code still referring to bundle — any code still referring to
`claude-bottle-supervise-<slug>` resolves to the bundle's IP.""" `claude-bottle-supervise-<slug>` resolves to the bundle's IP."""
return f"claude-bottle-supervise-{slug}" return f"claude-bottle-supervise-{slug}"
class DockerSupervise(Supervise):
"""Docker-flavored Supervise: inherits `.prepare()` from the base.
The supervise daemon's container lifecycle is owned by the
sidecar bundle (PRD 0024)."""
6
+5 -5
View File
@@ -25,6 +25,10 @@ from contextlib import ExitStack, contextmanager
from typing import Callable, Generator from typing import Callable, Generator
from ...egress import EGRESS_ROUTES_IN_CONTAINER, egress_resolve_token_values from ...egress import EGRESS_ROUTES_IN_CONTAINER, egress_resolve_token_values
from ...pipelock import (
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
)
from ...supervise import QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT from ...supervise import QUEUE_DIR_IN_CONTAINER, SUPERVISE_PORT
from ...util import expand_tilde from ...util import expand_tilde
from ..docker.egress import ( from ..docker.egress import (
@@ -38,11 +42,7 @@ from ..docker.git_gate import (
GIT_GATE_ENTRYPOINT_IN_CONTAINER, GIT_GATE_ENTRYPOINT_IN_CONTAINER,
GIT_GATE_HOOK_IN_CONTAINER, GIT_GATE_HOOK_IN_CONTAINER,
) )
from ..docker.pipelock import ( from ..docker.pipelock import pipelock_tls_init
PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER,
pipelock_tls_init,
)
from . import sidecar_bundle as _bundle from . import sidecar_bundle as _bundle
from . import smolvm as _smolvm from . import smolvm as _smolvm
from .bottle import SmolmachinesBottle from .bottle import SmolmachinesBottle
+14 -16
View File
@@ -21,10 +21,10 @@ from ...backend.docker.bottle_state import (
supervise_state_dir, supervise_state_dir,
write_metadata, write_metadata,
) )
from ...backend.docker.egress import DockerEgress from ...egress import Egress
from ...backend.docker.git_gate import DockerGitGate from ...git_gate import GitGate
from ...backend.docker.pipelock import DockerPipelockProxy from ...pipelock import PipelockProxy
from ...backend.docker.supervise import DockerSupervise from ...supervise import Supervise
from . import smolvm as _smolvm from . import smolvm as _smolvm
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
@@ -94,31 +94,29 @@ def resolve_plan(
f"http://{bundle_ip}:{_BUNDLE_SUPERVISE_PORT}" f"http://{bundle_ip}:{_BUNDLE_SUPERVISE_PORT}"
) )
# Inner Plans for the four bundle daemons. Use the docker # Inner Plans for the four bundle daemons. The ABCs are
# backend's concrete subclasses — the `.prepare()` method # platform-neutral — `.prepare()` writes config files + returns
# they inherit is platform-neutral (writes config files + # a Plan dataclass with no backend-specific assumptions. State
# returns a Plan dataclass); the docker-specific subclasses # dirs are still keyed by slug under the docker backend's
# exist only to satisfy ABC instantiation. Future: factor # bottle_state layout (shared on-host convention; not a docker
didericis marked this conversation as resolved Outdated
Outdated
Review

Do this refactor now

Do this refactor now
Outdated
Review

Done in 73dc0d4 — moved CA in-container path constants up to claude_bottle/pipelock.py, made PipelockProxy a regular class, and deleted the four empty Docker* sidecar subclasses. Both backends now instantiate the platform-neutral ABCs directly.

Done in 73dc0d4 — moved CA in-container path constants up to `claude_bottle/pipelock.py`, made `PipelockProxy` a regular class, and deleted the four empty `Docker*` sidecar subclasses. Both backends now instantiate the platform-neutral ABCs directly.
# the prepare logic out of the docker subpackage so # dependency).
# smolmachines doesn't have to reach across the backend
# boundary.
pipelock_dir = pipelock_state_dir(slug) pipelock_dir = pipelock_state_dir(slug)
pipelock_dir.mkdir(parents=True, exist_ok=True) pipelock_dir.mkdir(parents=True, exist_ok=True)
proxy_plan = DockerPipelockProxy().prepare(bottle, slug, pipelock_dir) proxy_plan = PipelockProxy().prepare(bottle, slug, pipelock_dir)
git_gate_dir = git_gate_state_dir(slug) git_gate_dir = git_gate_state_dir(slug)
git_gate_dir.mkdir(parents=True, exist_ok=True) git_gate_dir.mkdir(parents=True, exist_ok=True)
git_gate_plan = DockerGitGate().prepare(bottle, slug, git_gate_dir) git_gate_plan = GitGate().prepare(bottle, slug, git_gate_dir)
egress_dir = egress_state_dir(slug) egress_dir = egress_state_dir(slug)
egress_dir.mkdir(parents=True, exist_ok=True) egress_dir.mkdir(parents=True, exist_ok=True)
egress_plan = DockerEgress().prepare(bottle, slug, egress_dir) egress_plan = Egress().prepare(bottle, slug, egress_dir)
supervise_plan = None supervise_plan = None
if bottle.supervise: if bottle.supervise:
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)
supervise_plan = DockerSupervise().prepare(slug, supervise_dir) supervise_plan = Supervise().prepare(slug, supervise_dir)
# Prompt file is always written (mode 0o600) so the in-VM # Prompt file is always written (mode 0o600) so the in-VM
# path always exists. Content is the agent's `prompt` # path always exists. Content is the agent's `prompt`
+26 -21
View File
@@ -17,7 +17,6 @@ Image pin: ghcr.io/luckypipewrench/pipelock@sha256:<digest> for tag 2.3.0.
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import cast from typing import cast
@@ -47,6 +46,15 @@ DEFAULT_TLS_PASSTHROUGH: tuple[str, ...] = (
) )
# In-container paths the rendered pipelock YAML references under
# `tls_interception`. The pipelock binary expects the per-bottle CA
# cert + key at these exact paths inside its container — independent
# of how the daemon is wrapped (own container, sidecar bundle, etc.),
# which is why they live in the platform-neutral module.
PIPELOCK_CA_CERT_IN_CONTAINER = "/etc/pipelock-ca.pem"
PIPELOCK_CA_KEY_IN_CONTAINER = "/etc/pipelock-ca-key.pem"
# --- Allowlist resolution -------------------------------------------------- # --- Allowlist resolution --------------------------------------------------
@@ -301,25 +309,22 @@ class PipelockProxyPlan:
ca_key_host_path: Path = Path() ca_key_host_path: Path = Path()
class PipelockProxy(ABC): class PipelockProxy:
"""The pipelock egress proxy. Encapsulates the YAML-config """The pipelock egress proxy. Encapsulates the YAML-config
generation; the sidecar's start/stop lifecycle is backend-specific generation; the container lifecycle is owned by whatever
and lives on concrete subclasses. wraps the daemon (compose-managed pipelock container on docker,
sidecar-bundle PID 1 on smolmachines).
The class-level constants `CA_CERT_IN_CONTAINER` / Backends instantiate the class directly — there are no
`CA_KEY_IN_CONTAINER` are the in-container paths the YAML config platform-specific subclasses; the in-container CA paths are
references — they correspond to wherever the backend's `.start` universal module-level constants
places the CA cert and key inside the sidecar. Subclasses (`PIPELOCK_CA_CERT_IN_CONTAINER` / `PIPELOCK_CA_KEY_IN_CONTAINER`)."""
override the constants."""
CA_CERT_IN_CONTAINER: str = ""
CA_KEY_IN_CONTAINER: str = ""
def prepare( def prepare(
self, bottle: Bottle, slug: str, stage_dir: Path self, bottle: Bottle, slug: str, stage_dir: Path
) -> PipelockProxyPlan: ) -> PipelockProxyPlan:
"""Write the pipelock yaml config (mode 600) under `stage_dir` """Write the pipelock yaml config (mode 600) under `stage_dir`
and return the plan for `.start`. Pure host-side, no docker and return the plan for launch. Pure host-side, no docker
subprocess. subprocess.
`slug` is the agent-derived identifier (lowercased, `slug` is the agent-derived identifier (lowercased,
@@ -327,18 +332,18 @@ class PipelockProxy(ABC):
resource name — the agent container, the pipelock container resource name — the agent container, the pipelock container
(`claude-bottle-pipelock-<slug>`), the internal/egress (`claude-bottle-pipelock-<slug>`), the internal/egress
networks. It's stored on the returned plan so the backend's networks. It's stored on the returned plan so the backend's
start step can derive the sidecar's container name. launch step can derive the sidecar's container name.
The CA paths the YAML references are the in-container paths The CA paths the YAML references are the module-level
from the concrete subclass's class-level constants. The in-container constants. The host-side counterparts are
host-side counterparts are generated by the launch step generated by the launch step (not here, so prepare stays
(not here, so prepare stays side-effect-free on docker) and side-effect-free on docker) and added to the plan via
added to the plan via `dataclasses.replace` before `.start`.""" `dataclasses.replace` before the daemon starts."""
yaml_path = stage_dir / "pipelock.yaml" yaml_path = stage_dir / "pipelock.yaml"
cfg = pipelock_build_config( cfg = pipelock_build_config(
bottle, bottle,
ca_cert_path=self.CA_CERT_IN_CONTAINER, ca_cert_path=PIPELOCK_CA_CERT_IN_CONTAINER,
ca_key_path=self.CA_KEY_IN_CONTAINER, ca_key_path=PIPELOCK_CA_KEY_IN_CONTAINER,
) )
yaml_path.write_text(pipelock_render_yaml(cfg)) yaml_path.write_text(pipelock_render_yaml(cfg))
yaml_path.chmod(0o600) yaml_path.chmod(0o600)
+2 -2
View File
@@ -35,9 +35,9 @@ from claude_bottle.backend.docker.network import (
from claude_bottle.backend.docker.pipelock import ( from claude_bottle.backend.docker.pipelock import (
PIPELOCK_CA_CERT_IN_CONTAINER, PIPELOCK_CA_CERT_IN_CONTAINER,
PIPELOCK_CA_KEY_IN_CONTAINER, PIPELOCK_CA_KEY_IN_CONTAINER,
DockerPipelockProxy,
pipelock_tls_init, pipelock_tls_init,
) )
from claude_bottle.pipelock import PipelockProxy
from claude_bottle.backend.docker.pipelock_apply import ( from claude_bottle.backend.docker.pipelock_apply import (
PipelockApplyError, PipelockApplyError,
apply_allowlist_change, apply_allowlist_change,
@@ -99,7 +99,7 @@ class TestPipelockApply(unittest.TestCase):
the updated config.""" the updated config."""
state_dir = pipelock_state_dir(self.slug) state_dir = pipelock_state_dir(self.slug)
state_dir.mkdir(parents=True, exist_ok=True) state_dir.mkdir(parents=True, exist_ok=True)
prep = DockerPipelockProxy().prepare( prep = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], self.slug, state_dir, fixture_minimal().bottles["dev"], self.slug, state_dir,
) )
self.internal_net = network_create_internal(self.slug) self.internal_net = network_create_internal(self.slug)
+7 -7
View File
@@ -12,10 +12,10 @@ import unittest
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any, cast
from claude_bottle.backend.docker.pipelock import DockerPipelockProxy
from claude_bottle.manifest import Manifest from claude_bottle.manifest import Manifest
from claude_bottle.pipelock import ( from claude_bottle.pipelock import (
DEFAULT_TLS_PASSTHROUGH, DEFAULT_TLS_PASSTHROUGH,
PipelockProxy,
pipelock_build_config, pipelock_build_config,
pipelock_render_yaml, pipelock_render_yaml,
) )
@@ -54,7 +54,7 @@ class TestBuildConfig(unittest.TestCase):
self.assertNotIn("tls_interception", cfg) self.assertNotIn("tls_interception", cfg)
def test_tls_interception_block_emitted_when_paths_supplied(self): def test_tls_interception_block_emitted_when_paths_supplied(self):
# PRD 0006: paths flow in via DockerPipelockProxy's in-container # PRD 0006: paths flow in via the platform-neutral in-container
# constants; this directly pins the dict shape. passthrough_domains # constants; this directly pins the dict shape. passthrough_domains
# is baked in so LLM provider endpoints (api.anthropic.com) skip # is baked in so LLM provider endpoints (api.anthropic.com) skip
# MITM — pipelock's docs explicitly recommend this for LLM hosts, # MITM — pipelock's docs explicitly recommend this for LLM hosts,
@@ -152,7 +152,7 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertNotIn("ssrf:", text) self.assertNotIn("ssrf:", text)
def test_prepare_writes_file_at_mode_600(self): def test_prepare_writes_file_at_mode_600(self):
plan = DockerPipelockProxy().prepare( plan = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", self.out_dir fixture_minimal().bottles["dev"], "demo", self.out_dir
) )
self.assertEqual(0o600, os.stat(plan.yaml_path).st_mode & 0o777) self.assertEqual(0o600, os.stat(plan.yaml_path).st_mode & 0o777)
@@ -170,7 +170,7 @@ class TestRenderAndWrite(unittest.TestCase):
}, },
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}}, "agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
}) })
plan = DockerPipelockProxy().prepare( plan = PipelockProxy().prepare(
manifest.bottles["dev"], "demo", self.out_dir manifest.bottles["dev"], "demo", self.out_dir
) )
content = plan.yaml_path.read_text() content = plan.yaml_path.read_text()
@@ -179,13 +179,13 @@ class TestRenderAndWrite(unittest.TestCase):
self.assertNotIn("prompt-message", content) self.assertNotIn("prompt-message", content)
def test_render_emits_tls_interception_via_prepare(self): def test_render_emits_tls_interception_via_prepare(self):
"""`DockerPipelockProxy.prepare` plumbs its in-container CA """`PipelockProxy.prepare` plumbs the module-level in-container
constants through to the YAML. The block should land in the CA constants through to the YAML. The block should land in the
rendered output with `enabled: true`, the configured paths, rendered output with `enabled: true`, the configured paths,
and the baked LLM-provider passthrough list. The actual and the baked LLM-provider passthrough list. The actual
host-side CA generation happens in launch (not prepare), so host-side CA generation happens in launch (not prepare), so
this test exercises only the YAML rendering.""" this test exercises only the YAML rendering."""
plan = DockerPipelockProxy().prepare( plan = PipelockProxy().prepare(
fixture_minimal().bottles["dev"], "demo", self.out_dir fixture_minimal().bottles["dev"], "demo", self.out_dir
) )
content = plan.yaml_path.read_text() content = plan.yaml_path.read_text()