Skip to content

feat: let a profile name the runner, so Apple's container works - #29

Merged
tkc merged 1 commit into
mainfrom
runner-profiles
Sep 10, 2026
Merged

feat: let a profile name the runner, so Apple's container works#29
tkc merged 1 commit into
mainfrom
runner-profiles

Conversation

@tkc

@tkc tkc commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Profiles were hard-wired to docker run. The argument spelling termit emits (-v, -w, -e, --network, -it, --rm) is not Docker-specific, so a profile can now name the command instead.

[profile.vm]
image       = "termit-agent:latest"
runner      = "container"              # Apple's container: one lightweight VM each
mount       = ["{cwd}:/work"]
env         = ["ANTHROPIC_API_KEY", "TERM"]
runner_args = ["--memory", "2048MB"]   # inserted just before the image
  • runner defaults to docker, so existing configs are unchanged.
  • runner_args is the escape hatch for flags termit knows nothing about.
  • network is now passed only when set: the name differs per runner (docker bridge, container default) and both reach the network by default. network = "none" under Docker still closes it.
  • No runner name appears in the code except as that default. termit still only assembles an argument list.

Verified against Apple's container 1.4.1

An #[ignore]d test spawns a real container through termit's own pty path and asserts both that guest output reaches the grid and that stty size inside reports 40 120:

cargo test -- --ignored コンテナの中の出力が届く --nocapture

Measured while trying it:

First run (image pull included) 15.1 s
Warm start 0.74 s (a whole VM)
Interactive prompt through a pty 1.02 s
Idle cost, no containers apiserver 24 MB + core-images 22 MB
Per container ~21 MB host-side, guest defaults to 4 CPUs / 1024 MB

The caveat, documented rather than worked around

For about the first second the guest terminal size is 0 0; the real size arrives after that, and later resizes propagate correctly:

t1: 0 0        <- immediately after start
t2: 40 120     <- one second later
t6: 50 200     <- after a host-side resize + SIGWINCH

So a full-screen UI that reads its size once at startup can come up at 80 columns. Re-sending the same size raises no SIGWINCH, so a fix inside termit would have to jiggle the size and flicker the screen. Config avoids it instead:

[agent]
new = "sh -c 'sleep 1.5; exec claude --session-id {new_id}'"

Upstream has no issue for this (apple/container#1747 is about SIGWINCH forwarding errors, a different thing).

Docs

docs/references/sandbox.md is new: the six isolation levels from Anthropic's own comparison, how sbx (Docker Sandboxes) differs — microVM per session, credentials kept on the host and injected at a proxy, --branch auto worktrees — the container measurements above, and what termit deliberately leaves outside:

  • sbx-shaped argument listssbx run <agent> does not fit; supporting it means generalising to a command template and giving up the image/mount/network vocabulary. Decide when it is actually needed.
  • git worktree per fork — the terminal would start understanding git; [agent] fork can run git worktree add itself.
  • credential injectionenv is as strong as the host, and that is now said plainly rather than implied.

README and the design spec follow the same change.

Test

cargo test — 206 pass, 1 ignored (the container one). cargo clippy --all-targets -- -D warnings and cargo fmt --check — clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GvMacYD6T9EoWqc7C247Hr

Profiles were hard-wired to `docker run`. The argument spelling termit
emits (-v, -w, -e, --network, -it, --rm) is not Docker-specific, so a
profile now names the command:

    runner      = "container"            # defaults to docker
    runner_args = ["--memory", "2048MB"] # inserted just before the image

`runner_args` is the escape hatch for flags termit knows nothing about,
and `network` is now only passed when set, because the name differs per
runner (docker: bridge, container: default) and both reach the network by
default anyway. No runner name appears anywhere in the code except as
that default.

Verified end to end against Apple's container 1.4.1: an ignored test
spawns a real container through termit's own pty path and asserts the
guest output reaches the grid and that 40x120 arrives inside. Warm start
was 0.74s, an interactive prompt 1.02s.

One measured caveat is documented rather than worked around: for about
the first second the guest terminal is 0x0 before the real size arrives.
Re-sending the same size raises no SIGWINCH, so a workaround inside
termit would have to jiggle the size and flicker; starting the agent
through `sh -c 'sleep 1.5; exec claude'` avoids the window from config.

docs/references/sandbox.md records the survey behind this: the six
isolation levels, sbx (Docker Sandboxes, microVM per session with
credential injection at a host proxy), and what termit deliberately
leaves outside — sbx-shaped argument lists, git worktrees per fork, and
credential injection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GvMacYD6T9EoWqc7C247Hr
@tkc
tkc merged commit 237dc87 into main Sep 10, 2026
1 check passed
@tkc
tkc deleted the runner-profiles branch September 10, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant