Skip to content

isb 3.4.0: exec -it runs a command interactively, on a terminal - #73

Merged
knowsuchagency merged 4 commits into
mainfrom
stack-exec-tty
Oct 28, 2026
Merged

knowsuchagency merged 4 commits into
mainfrom
stack-exec-tty

Conversation

@knowsuchagency

Copy link
Copy Markdown
Contributor

isb stack exec -it NAME SERVICE -- CMD (and isb app exec -it, isb instance exec -it) run the command on a pseudo-terminal in the replica, wired to your terminal, as docker exec -it / kubectl exec -it do:

  • keys go out raw (Ctrl-C reaches the command, not isb), output comes back live
  • the initial window size and every SIGWINCH resize reach the guest
  • isb exits with the command's status, and the local terminal is restored (also on SIGTERM/SIGHUP, which end the session and kill the command)
  • -i is implied; -u, -w, -e, --replica work as without -t; $TERM is passed along
  • refused with a clear error when stdin is not a terminal; conflicts with -n and --timeout

Motivating use: isb --org ocai stack exec -it email stalwart -- stalwart-migrate prepare ..., or -it ... -- sh for debugging.

How

It rides the daemon's existing terminal websocket (/orgs/<org>/api/v1/terminal, the web UI's terminals), which now also takes:

  • stack=NAME&service=SERVICE as a target (beside app= and instance=), replica picked as stack_exec picks it
  • a command instead of the login shell: argv= (repeated, percent-encoded), user=, cwd=, env=KEY=VALUE

A command session is admitted as the exec tool that runs it without a terminal (stack_exec, app_exec, instance_exec), so --deny-tools and token scopes apply exactly as for plain exec; a shell is still sandbox_exec's. The terminal websocket now also accepts the daemon's unix socket without an Origin (as the SSH websocket already did), so -it works on the host as well as over $ISB_URL + $ISB_TOKEN. The client loop (server::terminal::attach) is generic and tested against the server's own bridge.

MCP: deliberately left out. The *_exec tools run a command to completion and return its output; a terminal needs a person at a keyboard. Docs say so.

Verification

  • scripts/check.sh green in an isb sandbox (ratchet, fmt, clippy -D warnings, docs links, all unit tests). New tests: query round-trip of stack targets and commands (spaces, &, =, UTF-8), bad requests refused, admission per tool (--deny-tools stack_exec refuses -it on a stack), unix-socket Origin rule, and the client end against the real bridge (keys, resize, exit code, stop, refusal).
  • End to end in an isolated preview VM (its own incusd and isb, nothing on titan touched), driving isb ... exec -it on a real pty with a Python harness, against an alpine stack:
    • over the unix socket, stack exec -it and instance exec -it; over ISB_URL + an org token with the socket hidden, stack exec -it
    • each: keys out / output back, initial size 40x120 seen by stty size, resize to 50x150 seen by the guest, Ctrl-C interrupts the guest's sleep while isb keeps running, exit 7 returns 7, local termios identical before and after
    • -u nobody -w /tmp -e 'FOO=bar baz' reach the command; exit 4 relayed; a bogus token is refused cleanly; -it </dev/null is refused; plain stack exec unchanged
    • this caught a real bug (SIGWINCH's EINTR ended the session), fixed in dbb833e
  • Not separately exercised end to end: isb app exec -it (same daemon path as stacks, an app being the service of its own stack).

https://claude.ai/code/session_016AFG7KfrwCTBETjGfyW75K

`isb stack exec -it NAME SERVICE -- CMD` (and app exec, instance exec) run
the command on a pseudo-terminal in the replica, wired to the local
terminal: raw keys out, output back, SIGWINCH resizes, the command's exit
code returned, the terminal restored. It rides the daemon's terminal
websocket, which now takes stack=/service= targets and a command (argv=,
user=, cwd=, env=), admitted as the exec tool that runs it without a
terminal. Works over the daemon's unix socket and over $ISB_URL with
$ISB_TOKEN; refused when stdin is not a terminal. Not on MCP.

Claude-Session: https://claude.ai/code/session_016AFG7KfrwCTBETjGfyW75K
Found end to end: resizing the window interrupted the websocket read
(EINTR) and ended the session.

Claude-Session: https://claude.ai/code/session_016AFG7KfrwCTBETjGfyW75K
@knowsuchagency
knowsuchagency merged commit d6f97fd into main Oct 28, 2026
20 checks passed
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