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:
@@ -11,7 +11,7 @@ from __future__ import annotations
|
||||
import subprocess
|
||||
from typing import Callable
|
||||
|
||||
from .. import Bottle
|
||||
from .. import Bottle, ExecResult
|
||||
|
||||
|
||||
class DockerBottle(Bottle):
|
||||
@@ -38,6 +38,23 @@ class DockerBottle(Bottle):
|
||||
cmd.extend([self.name, "claude", *full_argv])
|
||||
return subprocess.run(cmd, check=False).returncode
|
||||
|
||||
def exec(self, script: str) -> ExecResult:
|
||||
# Pipe via stdin to `sh -s` so the caller never has to worry
|
||||
# about quoting; the script source lands inside the container
|
||||
# without crossing argv.
|
||||
result = subprocess.run(
|
||||
["docker", "exec", "-i", self.name, "sh", "-s"],
|
||||
input=script,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return ExecResult(
|
||||
returncode=result.returncode,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
)
|
||||
|
||||
def cp_in(self, host_path: str, container_path: str) -> None:
|
||||
subprocess.run(
|
||||
["docker", "cp", host_path, f"{self.name}:{container_path}"],
|
||||
|
||||
Reference in New Issue
Block a user