DEV Community

Artemii Amelin
Artemii Amelin

Posted on

OpenAI's Self-Hosted Executor Only Dials Out. So Does Our Daemon, Which Is Why the Stop Button We Added Today Is a Queue

OpenAI opened the Agents API to public beta on September 10. The page worth reading is not the overview. It is the self-hosted sandbox guide, which describes what runs on your hardware when you decline OpenAI's hosted container. You run codex exec-server inside your own environment. It "runs shell commands, reads and writes files, and uses local MCP servers." It "registers with the API using an environment ID and a restricted API key" and then "connects over WebSocket to receive commands and return results." And then: "All connections are outbound. The executor reconnects if the connection drops."

That sentence settles a lot. Nothing on OpenAI's side opens a connection into your box. The executor is a client that keeps dialing home, and every action the harness wants taken on your machine rides that connection. Each session gets its own environment ID and needs its own executor. The key the executor holds "only permits connecting environments," which the docs justify plainly: "agent-generated code can read the executor key."

What the docs do not describe is stopping. The overview walks through four steps: create a session, give it a task, follow progress by streaming or webhooks, then continue with another task or give guidance on the current one. A stop call is not among them. A session is defined as "a durable instance of an agent that works on tasks and responds to input." How a durable thing on your hardware gets told to die is left to whatever supervises the executor.

shell.online has the same shape, a process on your machine that only dials out, so the stop question was ours to answer too. Today's merge to main, PR #88, put the answer on the board view and on the session's own page, where before it only existed in the sessions table.

Who is allowed to press it

A shell.online session is a process on somebody's machine, serving its own local socket and holding its own terminal key. The accounts service relays a sealed session password it cannot open and has no route into the machine. To make a signed-in machine reachable from the browser at all, the machine runs a daemon. The comment at the top of daemon.go explains why it exists: "the rest of shell.online has no long-lived process." The daemon refuses to start unless the machine was signed in with shell login --allow-remote-start, and exactly one runs per machine.

Before today, Stop lived in the table only. The board had no stop at all, and the session page had neither stop nor remove, which on a phone is the page you are actually on. PR #88 adds both, gated by the same two rules the table already used from session-view.ts:

  • canStop: you own the session, and the session has a device id. Nothing else qualifies. Being assigned to a session lets you type into it. Being an organization admin lets you tidy the list. Neither lets you kill a process on someone else's laptop. The server comment says it directly: "an organization role must never silently broaden into remote-process administration."
  • canRemove: the owner, or an organization owner or admin. The button's tooltip carries the rest of the rule: "Remove from the list. The machine is not touched."

What a stop actually does

Pressing Stop sends a POST to /api/commands with kind kill. The server in app.ts then does five things, in this order.

  1. Checks the caller owns the session. Anyone else gets a 403: "only the session owner can stop its process."
  2. Writes the "stopped" audit entry before the machine is asked. The comment: "The queue is the decision; whether the process was still alive to receive it is a fact about the machine."
  3. Resolves the target machine from the session's recorded device, not from the browser's copy. If that device row was replaced by a re-login, it follows the machine to whichever device carries it now.
  4. If the daemon has not polled in the last 15 seconds (AGENT_ONLINE_MS), returns a 409 saying the stop cannot be delivered. Nothing is queued for a machine that is not listening.
  5. Otherwise queues the command and returns 202.

Then the machine's side. The daemon asks GET /api/agent/commands every two seconds (agentPollInterval in agent_loop.go). When it claims a kill, it runs shell kill <id> as a subprocess. That command writes "stop" to the session's local control socket. The session process sends SIGTERM to its process group, waits two seconds, then sends SIGKILL. The daemon posts completion back, and the server closes the session row on that confirmation rather than waiting for the session to report its own exit, because a session started before a machine was unlinked has revoked credentials and can never report anything again.

Worst case, that is two seconds of poll wait plus two seconds between SIGTERM and SIGKILL. The session page reloads 1.5 seconds after the click, and the comment beside that timer admits the row "catches up shortly." The delay is the cost of an outbound-only design. What it buys is what OpenAI's executor buys: no inbound port, no service that can reach into a machine, and a stop refused up front when nobody is listening instead of sitting in a queue forever. The full path from button to signal is readable end to end in the shell.online repository, across app.ts, agent_loop.go and session_unix.go.

The same trade one layer down

Pilot Protocol answers the reachability half of this question differently, because it sits at a different layer. Our IETF draft, and the open-source implementation of it, give an agent a permanent virtual address and a bilateral trust model beneath application protocols such as A2A and MCP, with NAT traversal by STUN and hole-punching and a relay fallback for symmetric NATs. Whether this peer may reach that agent is decided by the trust relationship before a packet is delivered, not by a queue that a daemon polls. Reachability and the right to send a signal to a process are separate facts, and today's PR is about the second one.

The OpenAI executor and our daemon are both processes that dial out and wait to be told what to do. That makes a stop a message rather than a signal, and a message has an owner, a route, a queue and a delay. Today's change just makes sure the message can be sent from the page a phone user is standing on.

Top comments (0)