Commit Graph

7 Commits

Author SHA1 Message Date
didericis-claude 3103266053 fix(dashboard): hoist claude_argv to Bottle ABC so smolmachines pane attach works
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 42s
test / unit (push) Successful in 26s
test / integration (push) Successful in 45s
Launching a smolmachines agent from the dashboard inside tmux
crashed with

  AttributeError: 'SmolmachinesBottle' object has no attribute
  'claude_docker_argv'

because the tmux pane-respawn path called
`bottle.claude_docker_argv(...)` directly — a method that only
existed on DockerBottle. The foreground-handoff path (curses
endwin → subprocess.run → restore) doesn't hit it; it goes
through `bottle.exec_claude` which is on the ABC.

- Move the argv builder onto the `Bottle` ABC as
  `claude_argv(argv, *, tty=True) -> list[str]`. Both backends
  implement it; both `exec_claude` impls collapse to
  `subprocess.run(self.claude_argv(argv, tty=tty), check=False)`.

- DockerBottle: rename `claude_docker_argv` → `claude_argv`,
  body unchanged.

- SmolmachinesBottle: extract the argv-building from
  `exec_claude` into `claude_argv`; the new method returns the
  full `smolvm machine exec --name … -- runuser -u node --
  claude …` argv. The `runuser` switch lives on the
  exec-framing prefix so the dashboard's
  `_build_resume_argv_with_fallback` split-at-"claude" trick
  keeps the UID switch when wrapping the claude tail in
  `sh -c "… --continue || …"`.

- Dashboard: drop the docker-specific wording — local + helper
  arg names `docker_argv` → `claude_argv`; docstrings on
  `_build_resume_argv_with_fallback`, `_build_split_pane_argv`,
  `_build_respawn_pane_argv` now say "backend-exec argv". The
  shell-fallback wrap is unchanged; the existing logic works
  for smolmachines because `claude` is still the marker token.

Tests:
- `tests/unit/test_smolmachines_bottle.py` (new): locks down
  the smolmachines argv shape — prompt-file flag injection,
  guest-env `-e K=V` forwarding, TTY toggle, runuser-precedes-
  claude invariant.
- `test_docker_bottle.py`: TestClaudeDockerArgv →
  TestClaudeArgv; method renames follow.
- `test_dashboard_active_agents.py`: docstring follow.

615 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:52:02 -04:00
didericis-claude af65c10361 refactor: Bottle.exec takes a user= kwarg, default node
test / unit (pull_request) Successful in 26s
test / integration (pull_request) Successful in 41s
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>
2026-05-27 15:00:13 -04:00
didericis 2303cbc0be refactor(bottle): extract claude_docker_argv from exec_claude
test / unit (pull_request) Successful in 18s
test / integration (pull_request) Successful in 1m10s
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).
2026-05-26 14:21:04 -04:00
didericis 4864516b33 feat(bottle): add exec method to the bottle abstraction
test / unit (push) Successful in 11s
test / integration (push) Failing after 12s
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>
2026-05-12 11:18:43 -04:00
didericis 95a14bb8d2 style: pass explicit check= to every subprocess.run call
test / unit (push) Successful in 11s
test / integration (push) Failing after 11s
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.
2026-05-12 10:13:56 -04:00
didericis 64a31a382b chore(types): add pyright strict config and fix resulting errors
test / unit (push) Successful in 11s
test / integration (push) Successful in 12s
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.
2026-05-12 10:03:48 -04:00
didericis 70a22fa210 refactor: rename platform abstraction to backend
test / run tests/run_tests.py (pull_request) Successful in 21s
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.
2026-05-10 23:59:38 -04:00