Promote the user-switch from a hardcoded `node` to a keyword arg
so callers can opt into root (or any other user) when needed.
Default stays `node` — matches the docker image's USER and the
smolmachines runuser default.
Lifts the change through the base ABC, docker, and smolmachines
backends:
- Base: `def exec(self, script, *, user="node")`.
- Docker: adds `-u <user>` to `docker exec` (no-op when user is
node, the image's default).
- Smolmachines: `runuser -l <user> -c <script>` — `runuser -l
root` is the trivial no-op form when the caller asked for root.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PRD 0021 chunk 1. The tmux split-pane helpers (chunk 2+) need
the same docker-exec argv that `exec_claude` builds — including
the `--append-system-prompt-file <path>` flag the bottle's
provisioner copies into place. Extract the argv construction
into a pure `claude_docker_argv(argv, *, tty)` method so both
foreground (`subprocess.run`) and tmux paths
(`tmux respawn-pane …`) build from the same source.
`exec_claude` becomes a one-liner that runs subprocess.run on
the argv. No behavior change; 472 unit tests pass (7 new for
the pure builder).
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>
Silences pylint W1510 / ruff PLW1510 across the codebase. The choice
at each site reflects existing intent:
- check=True where the caller implicitly trusts success (docker ps /
network ls returning stdout, docker build, exec chown/chmod inside
provisioners).
- check=False where the caller inspects .returncode (race-retry on
docker run, pipelock sidecar lifecycle, network plumbing, exec_claude
propagating the session's exit code, best-effort cleanup paths).
No behavior change; check= defaults to False so the False sites are
semantically identical.
Adds pyrightconfig.json (strict, Python 3.11) covering cli.py,
claude_bottle/, and tests/. Fixes the 49 strict-mode errors:
- Type DockerBottle.teardown as Callable[[], None].
- ResolvedEnv default_factory uses parameterized list[str] / dict[str, str].
- Erase BottleBackend generics at the registry boundary
(BottleBackend[Any, Any]) since selection is runtime-driven and
callers use the unparameterized interface.
- DockerBottleBackend.launch returns Generator[DockerBottle, None, None];
@contextmanager now flags Iterator returns as deprecated.
- Sidestep cli.list submodule shadowing builtins.list in main()'s argv
annotation via an aliased re-import in cli/__init__.py.
- Cast cfg[...] results in test_pipelock_yaml at the dict[str, object]
boundary.
- Annotate write_fixture's fn parameter and _manifest_with_runtime's
return type.
Across the package:
- claude_bottle/platform/ -> claude_bottle/backend/
- platform/docker/platform.py -> backend/docker/backend.py
- class BottlePlatform -> BottleBackend
- class DockerBottlePlatform -> DockerBottleBackend
- get_bottle_platform() -> get_bottle_backend()
- env var CLAUDE_BOTTLE_PLATFORM -> CLAUDE_BOTTLE_BACKEND
- dict _PLATFORMS -> _BACKENDS
"Backend" is shorter and more established as the term for a
pluggable strategy-pattern implementation. "Platform" was vague
(could mean OS, hardware, cloud) and mildly redundant — Docker is
itself a platform.
The previous PRD section claiming "the Backend protocol was
rejected" referred to a low-level run/exec/cp/network_connect
protocol; the name was never the reason. The PRD is updated to
describe that rejected design by shape rather than by name.
The bottle/agent concepts and the manifest schema are unchanged.