--headless fails with an opaque ENODEV when stdio is not a TTY #460

Closed
opened 2026-07-21 22:20:32 -04:00 by didericis-claude · 1 comment
Collaborator

./cli.py start <agent> --headless requires a PTY on stdio — that part is by design, and _start_headless says so:

Otherwise runs the same launch core as the interactive path, so the agent still execs on the inherited stdio/PTY — an orchestrator allocates that PTY and relays it to its desktop/mobile clients.

The problem is what happens when the caller does not supply one. Rather than a clear error up front, the launch does all of its expensive work first and then dies deep inside Apple Container with an opaque errno:

bot-bottle: starting guest-local container engine
bot-bottle: guest-local container engine is ready
bot-bottle: attaching interactive claude session (Ctrl-D or 'exit' to leave; container will be removed)
Error: failed to exec process Error Domain=NSPOSIXErrorDomain Code=19 "Operation not supported by device"
bot-bottle: session ended (exit 1); container bot-bottle-... will be removed

Code 19 is ENODEV, from bottle.exec_agent(agent_args, tty=True) in attach_agent (bot_bottle/cli/start.py:323) being handed a stdio that is not a terminal. Nothing in the message mentions a TTY.

Why it matters

--headless is documented as the path "for orchestrators, CI, and webhooks" — precisely the contexts where stdin is a pipe or socket rather than a terminal. Anyone wiring up automation hits this, and the error gives no hint what to fix.

By the time it fails the run has already:

  • built or resolved the agent image (minutes on a cold cache),
  • brought up the shared gateway,
  • provisioned per-repo deploy keys on the forge,
  • started the bottle and, if enabled, its nested-container engine.

Repro

# from any process without a controlling terminal, e.g. a background job
./cli.py start <agent> --headless --prompt 'echo hi' < /dev/null

Workaround: allocate a PTY, e.g. script -q /dev/null ./cli.py start ... --headless ..., or a small pty.fork() wrapper. Both work.

Suggested fix

Preflight os.isatty() on the relevant fd alongside the other host-side checks in prepare, and die() with a message naming the requirement and the workaround, before any resource is created. Something like:

--headless still execs the agent on the inherited stdio, which must be a TTY. Run it under a PTY (e.g. script -q /dev/null ./cli.py start …) or allocate one in the orchestrator.

An alternative, if headless should be usable with no terminal at all, is for the headless path to allocate its own PTY and relay it — a larger change, and a product decision about what --headless promises.

Related

A SIGKILL during a headless run also skips teardown, leaving the container running and its deploy keys un-revoked on the forge. Arguably its own issue; noting it here since both were hit while automating --headless runs for #392.

Found while running the #392 nested-container verification from a background process.

`./cli.py start <agent> --headless` requires a PTY on stdio — that part is by design, and `_start_headless` says so: > Otherwise runs the same launch core as the interactive path, so the agent still execs on the inherited stdio/PTY — an orchestrator allocates that PTY and relays it to its desktop/mobile clients. The problem is what happens when the caller does *not* supply one. Rather than a clear error up front, the launch does all of its expensive work first and then dies deep inside Apple Container with an opaque errno: ``` bot-bottle: starting guest-local container engine bot-bottle: guest-local container engine is ready bot-bottle: attaching interactive claude session (Ctrl-D or 'exit' to leave; container will be removed) Error: failed to exec process Error Domain=NSPOSIXErrorDomain Code=19 "Operation not supported by device" bot-bottle: session ended (exit 1); container bot-bottle-... will be removed ``` Code 19 is `ENODEV`, from `bottle.exec_agent(agent_args, tty=True)` in `attach_agent` (`bot_bottle/cli/start.py:323`) being handed a stdio that is not a terminal. Nothing in the message mentions a TTY. ## Why it matters `--headless` is documented as the path "for orchestrators, CI, and webhooks" — precisely the contexts where stdin is a pipe or socket rather than a terminal. Anyone wiring up automation hits this, and the error gives no hint what to fix. By the time it fails the run has already: - built or resolved the agent image (minutes on a cold cache), - brought up the shared gateway, - provisioned per-repo deploy keys on the forge, - started the bottle and, if enabled, its nested-container engine. ## Repro ```bash # from any process without a controlling terminal, e.g. a background job ./cli.py start <agent> --headless --prompt 'echo hi' < /dev/null ``` Workaround: allocate a PTY, e.g. `script -q /dev/null ./cli.py start ... --headless ...`, or a small `pty.fork()` wrapper. Both work. ## Suggested fix Preflight `os.isatty()` on the relevant fd alongside the other host-side checks in `prepare`, and `die()` with a message naming the requirement and the workaround, before any resource is created. Something like: > `--headless` still execs the agent on the inherited stdio, which must be a TTY. Run it under a PTY (e.g. `script -q /dev/null ./cli.py start …`) or allocate one in the orchestrator. An alternative, if headless should be usable with no terminal at all, is for the headless path to allocate its own PTY and relay it — a larger change, and a product decision about what `--headless` promises. ## Related A SIGKILL during a headless run also skips teardown, leaving the container running and its deploy keys un-revoked on the forge. Arguably its own issue; noting it here since both were hit while automating `--headless` runs for #392. Found while running the #392 nested-container verification from a background process.
didericis-claude added the Kind/Enhancement
Priority
Medium
3
labels 2026-07-21 22:20:32 -04:00
Author
Collaborator

Fixed in e719022 — pushed directly to main.

(I was chastised for this: should have opened a PR.)

Fixed in e719022 — pushed directly to `main`. (I was chastised for this: should have opened a PR.)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: didericis/bot-bottle#460