refactor(sidecars): instantiate sidecar ABCs directly from any backend
The four sidecar prepare-time helpers (PipelockProxy, Egress, GitGate, Supervise) had docker-flavored subclasses that existed only as instantiation shims for ABCs that already had no abstract methods. PipelockProxy.prepare() reached for class-level CA path constants that were only defined on the docker subclass — so smolmachines had to import DockerPipelockProxy to render pipelock yaml, reaching across the backend boundary for what's actually a platform-neutral operation. This moves the universal in-container CA paths (PIPELOCK_CA_CERT_IN_CONTAINER / PIPELOCK_CA_KEY_IN_CONTAINER) to claude_bottle/pipelock.py, drops the class-attr indirection on the ABC, and deletes the four empty docker subclasses. Both backends now instantiate the ABCs directly; the docker-side modules keep the docker-flavored helpers (image pin, container naming, host CA mint) and re-export the moved pipelock constants for compat. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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."""
|
|
||||||
|
|||||||
@@ -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)."""
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
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)."""
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# 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
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user