feat(bottle): add exec method to the bottle abstraction
Bottle.exec(script) -> ExecResult runs a POSIX shell script inside a running bottle and returns captured stdout/stderr/returncode. The Docker impl pipes the script via stdin to `docker exec -i ... sh -s` so the source never crosses argv. Two integration tests exercise it end-to-end through the pipelock sidecar: a Node request to a non-allowlisted host (example.com) returns 403 from pipelock; a Node CONNECT to an allowlisted host (raw.githubusercontent.com) is tunneled with 200 Connection Established. The 200/403 split on each verb is decided by pipelock itself, isolating the allowlist decision from whatever the remote might return. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -94,12 +94,26 @@ class BottleCleanupPlan(ABC):
|
||||
short-circuit before showing the y/N."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ExecResult:
|
||||
"""Captured result of `Bottle.exec`. Backend-neutral: the Docker
|
||||
impl populates it from a `subprocess.CompletedProcess`, but a
|
||||
future fly/smolmachines backend could populate it from any source
|
||||
that produces a returncode + captured streams."""
|
||||
|
||||
returncode: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
|
||||
|
||||
class Bottle(ABC):
|
||||
"""Handle to a running bottle. Yielded by a backend's launch step.
|
||||
|
||||
`exec_claude` runs `claude` inside the bottle and blocks until the
|
||||
session ends. `cp_in` copies a host path into the bottle. `close`
|
||||
is an idempotent alias for context-manager teardown.
|
||||
session ends. `exec` runs a POSIX shell script inside the bottle
|
||||
and returns the captured result. `cp_in` copies a host path into
|
||||
the bottle. `close` is an idempotent alias for context-manager
|
||||
teardown.
|
||||
"""
|
||||
|
||||
name: str
|
||||
@@ -107,6 +121,14 @@ class Bottle(ABC):
|
||||
@abstractmethod
|
||||
def exec_claude(self, argv: list[str], *, tty: bool = True) -> int: ...
|
||||
|
||||
@abstractmethod
|
||||
def exec(self, script: str) -> ExecResult:
|
||||
"""Run `script` as a POSIX shell script inside the bottle and
|
||||
return the captured stdout/stderr/returncode. The bottle's
|
||||
environment (including HTTPS_PROXY pointing at the pipelock
|
||||
sidecar) is inherited by the child. Non-zero exit does not
|
||||
raise — callers inspect `returncode` themselves."""
|
||||
|
||||
@abstractmethod
|
||||
def cp_in(self, host_path: str, container_path: str) -> None: ...
|
||||
|
||||
@@ -270,5 +292,6 @@ __all__ = [
|
||||
"BottleCleanupPlan",
|
||||
"BottlePlan",
|
||||
"BottleSpec",
|
||||
"ExecResult",
|
||||
"get_bottle_backend",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user