<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Artemii Amelin </title>
    <description>The latest articles on DEV Community by Artemii Amelin  (@artem_a).</description>
    <link>https://dev.to/artem_a</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3893832%2F0e0d375d-e701-4dc8-9fb6-4c8f53e30992.png</url>
      <title>DEV Community: Artemii Amelin </title>
      <link>https://dev.to/artem_a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/artem_a"/>
    <language>en</language>
    <item>
      <title>OpenAI's Self-Hosted Executor Only Dials Out. So Does Our Daemon, Which Is Why the Stop Button We Added Today Is a Queue</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Fri, 11 Sep 2026 20:35:41 +0000</pubDate>
      <link>https://dev.to/artem_a/openais-self-hosted-executor-only-dials-out-so-does-our-daemon-which-is-why-the-stop-button-we-1d3d</link>
      <guid>https://dev.to/artem_a/openais-self-hosted-executor-only-dials-out-so-does-our-daemon-which-is-why-the-stop-button-we-1d3d</guid>
      <description>&lt;p&gt;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 &lt;code&gt;codex exec-server&lt;/code&gt; 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."&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is allowed to press it
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; 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 &lt;code&gt;shell login --allow-remote-start&lt;/code&gt;, and exactly one runs per machine.&lt;/p&gt;

&lt;p&gt;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. &lt;a href="https://github.com/TeoSlayer/shell.online/pull/88" rel="noopener noreferrer"&gt;PR #88&lt;/a&gt; adds both, gated by the same two rules the table already used from session-view.ts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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."&lt;/li&gt;
&lt;li&gt;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."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What a stop actually does
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;Checks the caller owns the session. Anyone else gets a 403: "only the session owner can stop its process."&lt;/li&gt;
&lt;li&gt;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."&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Otherwise queues the command and returns 202.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the machine's side. The daemon asks &lt;code&gt;GET /api/agent/commands&lt;/code&gt; every two seconds (agentPollInterval in agent_loop.go). When it claims a kill, it runs &lt;code&gt;shell kill &amp;lt;id&amp;gt;&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;shell.online repository&lt;/a&gt;, across app.ts, agent_loop.go and session_unix.go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same trade one layer down
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; answers the reachability half of this question differently, because it sits at a different layer. Our IETF draft, and the &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;open-source implementation&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Phone Joining a Shared Terminal Now Gets an 80x40 PTY. An Older CLI Still Gets 80x24.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Fri, 11 Sep 2026 19:07:39 +0000</pubDate>
      <link>https://dev.to/artem_a/a-phone-joining-a-shared-terminal-now-gets-an-80x40-pty-an-older-cli-still-gets-80x24-5dd4</link>
      <guid>https://dev.to/artem_a/a-phone-joining-a-shared-terminal-now-gets-an-80x40-pty-an-older-cli-still-gets-80x24-5dd4</guid>
      <description>&lt;p&gt;shell.online 0.11.2 went out on September 10. Most of the release list is web app work: several assignees per session, paginated audit history, self-hosting docs. The change worth a post is smaller and sits in the terminal path. When a phone opens a shared session, the process on the host machine is now resized to 80 columns by 40 rows instead of 80 by 24, and the negotiation that gets there is built so an older CLI never receives a size it does not understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  One process, one grid
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; session is one PTY on one machine, watched by up to 16 browsers. A PTY has exactly one size. If one viewer is on a 27-inch monitor and another is on a phone, the process cannot be 200 columns wide for the first and 60 for the second. What we gave up is the viewer's ability to resize the process at all.&lt;/p&gt;

&lt;p&gt;That rule lives in &lt;code&gt;cmd/shell/session_unix.go&lt;/code&gt;. A resize frame from a browser is received and deliberately ignored, with a comment explaining that the shared PTY keeps one canonical grid so simultaneous viewers cannot deform each other's TUI. The default grid is 120x36. Each browser then fits that fixed grid into whatever pane it has: the font size comes from the pane width, and the rows are spread down the height with whatever leading is left over. Nothing a viewer does to its own window reaches the process.&lt;/p&gt;

&lt;p&gt;The one message that does reach the process is a &lt;code&gt;terminal_size&lt;/code&gt; control message from the relay, and the CLI only applies it if the size is on a short allowlist. Anything else is dropped. The browser has the same allowlist in &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/app/src/terminal/connection.ts" rel="noopener noreferrer"&gt;&lt;code&gt;app/src/terminal/connection.ts&lt;/code&gt;&lt;/a&gt;, with a comment on what it prevents: drawing a size the CLI would have rejected "is how a viewer ends up drawing 94 columns of a 120-column process".&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a phone changes the grid for everyone
&lt;/h2&gt;

&lt;p&gt;Before this release the allowlist had two entries: 120x36, and 80x24 while a phone was connected. Eighty columns exists because the font size is derived from width. At 120 columns on a 390-pixel screen the type is unreadable. At 80 it is small but usable. The trouble was the row count: 24 rows on a portrait screen left a large part of a tall pane unused, because rows fill the height and there were not enough of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TeoSlayer/shell.online/pull/100" rel="noopener noreferrer"&gt;PR #100&lt;/a&gt; adds a third grid, 80x40. The relay's choice is a small pure function in &lt;code&gt;shared/terminal-grid.ts&lt;/code&gt;. If any connected viewer is a phone or reports a portrait layout, use the portrait grid, otherwise 120x36. The portrait grid is 80x40 only if the host CLI has said it can handle it, and 80x24 otherwise.&lt;/p&gt;

&lt;p&gt;Because the PTY has one size, this is a shared decision. A phone joining a session takes the desktop viewers to 80x40 as well, and their panes redraw the narrower grid at a larger font. When the portrait viewers leave, the relay puts the session back to 120x36.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three signals, none of them terminal bytes
&lt;/h2&gt;

&lt;p&gt;The relay is a Cloudflare Durable Object in &lt;code&gt;worker/index.ts&lt;/code&gt;. It needs three facts to pick a grid and learns all of them from connection metadata.&lt;/p&gt;

&lt;p&gt;Whether the viewer is a phone comes from the User-Agent and the &lt;code&gt;Sec-CH-UA-Mobile&lt;/code&gt; client hint. iPhone, iPod, or Android with "mobile" in the string count as mobile. iPad, tablet, and other Android strings count as tablet, and tablets keep the desktop grid.&lt;/p&gt;

&lt;p&gt;Whether the viewer is portrait comes from the browser itself. It puts &lt;code&gt;layout=portrait&lt;/code&gt; or &lt;code&gt;layout=landscape&lt;/code&gt; on the WebSocket URL when it connects, and sends a &lt;code&gt;viewer_layout&lt;/code&gt; message if the orientation flips later. A desktop window taller than it is wide triggers the tall grid too. While the on-screen keyboard is open the visual viewport is compressed, so the client keeps its pre-keyboard answer instead of deciding the phone has turned sideways and resizing the PTY under the person typing.&lt;/p&gt;

&lt;p&gt;The CLI announces that it can handle 80x40 with one header, &lt;code&gt;X-Shell-Terminal-Grid: 80x40&lt;/code&gt;, sent when it dials the relay in &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/internal/relay/relay.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/relay/relay.go&lt;/code&gt;&lt;/a&gt;. The relay stores that per host socket. A 0.11.1 CLI sends no header, so a phone on its session gets 80x24, the size that CLI already accepts.&lt;/p&gt;

&lt;p&gt;None of this touches terminal content. In an encrypted session the relay could not read a resize frame if it wanted to. The worker only checks that an encrypted resize frame is 34 bytes long, closes the socket if it is not, and otherwise leaves it alone. The grid is decided from a header, a query parameter, and one small JSON message, which is the same boundary the end-to-end encryption already draws around the session.&lt;/p&gt;

&lt;p&gt;The deployment order falls out of the header. The PR notes the worker can ship before the CLI release, because an old CLI never advertises and so never sees 80x40. A new CLI against an old worker has its header ignored and gets 80x24 as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the phone actually gets
&lt;/h2&gt;

&lt;p&gt;The PR's validation ran a real CLI against a local worker at 390x844: the session negotiated 80x40, the terminal used 640 of 721 available pixels, and there was no horizontal overflow. Rotating the phone restored 120x36. The typography module now uses a line height of 1.8 when the viewport is more than 15 percent taller than it is wide, and 1.18 otherwise, so the 40 rows stay readable instead of cramped.&lt;/p&gt;

&lt;p&gt;The same PR adds two-finger pinch zoom, from 50 to 150 percent. Zoom is local to the viewer and saved when the fingers lift. It never changes the grid, and there is a test asserting exactly that: personal zoom without a change in terminal dimensions.&lt;/p&gt;

&lt;p&gt;The reason this matters more than a mobile polish item is what the sessions contain. A session started from the web app is often a coding agent, Claude Code, Codex, or Hermes, working through a task on a linked machine. A phone is where people check on that work. shell.online is built by the team behind &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt;, and a running agent needs a place where a person can see its terminal, on whatever screen they have, without the agent's process being resized by whoever happens to look. Every claim above is readable in the &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;shell.online source&lt;/a&gt;; the grid function and the CLI allowlist are a few lines each.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>terminal</category>
      <category>mobile</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Codex Removed --full-auto in v0.147.0. Our Browser Launcher Was Still Emitting It, So Every Flag Now Comes From the Installed Binary.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:35:41 +0000</pubDate>
      <link>https://dev.to/artem_a/codex-removed-full-auto-in-v01470-our-browser-launcher-was-still-emitting-it-so-every-flag-29b7</link>
      <guid>https://dev.to/artem_a/codex-removed-full-auto-in-v01470-our-browser-launcher-was-still-emitting-it-so-every-flag-29b7</guid>
      <description>&lt;p&gt;OpenAI opened the Agents API to every developer yesterday, September 10. &lt;a href="https://www.marktechpost.com/2026/09/10/openai-launches-the-agents-api-in-public-beta-putting-the-codex-harness-behind-one-api-call/" rel="noopener noreferrer"&gt;MarkTechPost's write-up&lt;/a&gt; describes it as a managed service built on the open-source Codex harness: sessions, context compaction, recovery and subagents are OpenAI's job, and the developer supplies tools and picks where the code runs. Three places are on offer. An OpenAI-hosted sandbox, partner sandboxes at Blaxel, Cloudflare, Daytona, DigitalOcean, E2B, Modal, Oracle, Runloop and Vercel, or your own machines, which run &lt;code&gt;codex exec-server&lt;/code&gt; and dial out over a WebSocket. &lt;a href="https://docs.e2b.dev/agents/openai-agents-api" rel="noopener noreferrer"&gt;E2B's integration docs&lt;/a&gt; show what the last option looks like in practice: a worker boots with &lt;code&gt;codex exec-server&lt;/code&gt; baked in, the first session takes about 40 seconds while the controller starts it, and the worker stays warm on a 30-minute timeout. You follow progress through streaming or webhooks. There is no separate fee beyond tokens, tools and container time.&lt;/p&gt;

&lt;p&gt;So the harness is now a service. The same harness also ships as a CLI, and that CLI's flags move. This week &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; shipped a fix for exactly that problem, and it is small enough to walk through in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  A form that was right when it was written
&lt;/h2&gt;

&lt;p&gt;The accounts app in shell.online can start a coding-agent session on one of your linked machines from the browser. You pick the machine, pick the tool, fill in a short form, and the app builds a command line, shows it to you, and posts it. The machine's agent picks it up on its next poll and runs it inside a shared terminal. Four tools are supported today, Claude Code, GPT Codex, Hermes Agent and OpenClaw, listed in &lt;code&gt;KNOWN_HARNESSES&lt;/code&gt; in the server and mirrored in the CLI's &lt;code&gt;harness.go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Until v0.11.2, the Codex form offered one option beyond resuming a session: a "Full auto" toggle that appended &lt;code&gt;--full-auto&lt;/code&gt;. The form was candid about where that came from. A note under it read: "These flags come from the published interface for GPT Codex rather than from its own help output, so check the command below before starting."&lt;/p&gt;

&lt;p&gt;The note was honest, and the flag was dead. OpenAI's release notes for &lt;a href="https://github.com/openai/codex/releases/tag/rust-v0.147.0" rel="noopener noreferrer"&gt;rust-v0.147.0&lt;/a&gt;, dated August 7, say: "Remove the deprecated &lt;code&gt;codex exec --full-auto&lt;/code&gt; flag; use &lt;code&gt;--sandbox workspace-write&lt;/code&gt; instead." Pull request #80 tried it against codex 0.153.4 and got the same answer from the root command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;codex &lt;span class="nt"&gt;--full-auto&lt;/span&gt; &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;error: unexpected argument '--full-auto' found
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consequence is specific to this shape of product. The form runs in your browser. The command runs on a different machine, against whatever Codex is installed there. The app does not know that version. The &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/cmd/shell/harness.go" rel="noopener noreferrer"&gt;detection code&lt;/a&gt; checks whether a tool is on PATH and deliberately never executes it, because "running an agent binary to read a version string, so a browser can render a label, buys nothing worth the risk, and several of these take seconds to start." So a bad flag is not caught in the form. It goes through &lt;code&gt;POST /api/commands&lt;/code&gt;, waits for the poll, and fails on the machine it was sent to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the interface from the binary, not the docs
&lt;/h2&gt;

&lt;p&gt;The fix in &lt;a href="https://github.com/TeoSlayer/shell.online/pull/80" rel="noopener noreferrer"&gt;PR #80&lt;/a&gt;, shipped in v0.11.2 on September 10, was to install both tools, read their real &lt;code&gt;--help&lt;/code&gt;, and rebuild the two kinds so the warning could be deleted rather than kept.&lt;/p&gt;

&lt;p&gt;Codex now gets what &lt;code&gt;codex --help&lt;/code&gt; and &lt;code&gt;codex resume --help&lt;/code&gt; accept: resume by id or &lt;code&gt;--last&lt;/code&gt;, &lt;code&gt;--sandbox&lt;/code&gt; with &lt;code&gt;read-only&lt;/code&gt;, &lt;code&gt;workspace-write&lt;/code&gt; or &lt;code&gt;danger-full-access&lt;/code&gt;, &lt;code&gt;--ask-for-approval&lt;/code&gt; with &lt;code&gt;on-request&lt;/code&gt; or &lt;code&gt;never&lt;/code&gt;, and &lt;code&gt;--search&lt;/code&gt;. A model field arrived in the same release. Options are placed after &lt;code&gt;resume &amp;lt;id&amp;gt;&lt;/code&gt;, because the subcommand takes the same flags as the root command, and that is the one order that works for both a fresh and a resumed run. An empty select emits no flag at all, so "codex default" means whatever the installed version defaults to, not a value we guessed.&lt;/p&gt;

&lt;p&gt;Hermes was a free-text box before. It now has a subcommand picker (chat, gateway, sessions, dashboard, status, doctor, acp) plus &lt;code&gt;--resume&lt;/code&gt;, &lt;code&gt;--continue&lt;/code&gt;, &lt;code&gt;--model&lt;/code&gt;, &lt;code&gt;--worktree&lt;/code&gt; and &lt;code&gt;--yolo&lt;/code&gt;. Two parser rules are encoded in the builder and tested. Every option belongs to the top-level parser, so all of them go before the subcommand: &lt;code&gt;hermes sessions --yolo&lt;/code&gt; is an error, &lt;code&gt;hermes --yolo sessions&lt;/code&gt; is fine. And &lt;code&gt;--continue&lt;/code&gt; takes an optional value, which the &lt;a href="https://github.com/nousresearch/hermes-agent/blob/main/website/docs/reference/cli-commands.md" rel="noopener noreferrer"&gt;Hermes CLI reference&lt;/a&gt; writes as &lt;code&gt;--continue [name]&lt;/code&gt;, so a bare one is greedy. &lt;code&gt;hermes --continue sessions&lt;/code&gt; resumes a session called "sessions". The builder emits it last, and only when there is no subcommand and no explicit session for it to swallow.&lt;/p&gt;

&lt;p&gt;The test that used to assert which kinds were built from a published interface is gone. Its replacement generates every command shape from every builder and asserts none of them contains &lt;code&gt;--full-auto&lt;/code&gt;. The PR body records that all 17 shapes the two builders can produce were run against the installed binaries and accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this belongs next to the Agents API
&lt;/h2&gt;

&lt;p&gt;The Agents API answers the version problem by owning the harness. When OpenAI runs &lt;code&gt;codex exec-server&lt;/code&gt; in a sandbox it provisioned, the flags and the binary always match. That is a real advantage of the managed box. It is also the thing you give up the moment the agent runs on a machine you own, which is what the self-hosted option exists for, and what our launcher is for.&lt;/p&gt;

&lt;p&gt;For that case there are two habits and no shortcut. Build the command from the interface of the tool you will actually run, and test the generated shapes against the binary rather than the docs. The &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/app/src/lib/session-kinds.ts" rel="noopener noreferrer"&gt;session-kinds module&lt;/a&gt; makes the second habit cheap: each kind is a form plus a pure function from its fields to a command line, kept apart from the modal so that the command which will run on someone's machine is testable without a DOM.&lt;/p&gt;

&lt;p&gt;The other half is seeing the failure where it happens. A command that dies on a remote machine with "unexpected argument" is only useful if the output comes back to you, which is what wrapping the run in a &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;live terminal link&lt;/a&gt; is for. That is the reason we build shell.online at &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; in the first place: an agent running somewhere else needs a place to show its work, and sometimes the work is one line of error text.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Visa, Mastercard and Ant Want Agents to Register Once. A Directory Can Sign Three Facts About an Agent. The Fourth Is Not Its Call.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:13:49 +0000</pubDate>
      <link>https://dev.to/artem_a/visa-mastercard-and-ant-want-agents-to-register-once-a-directory-can-sign-three-facts-about-an-3jdo</link>
      <guid>https://dev.to/artem_a/visa-mastercard-and-ant-want-agents-to-register-once-a-directory-can-sign-three-facts-about-an-3jdo</guid>
      <description>&lt;p&gt;On September 10, Ant International, Visa and Mastercard announced they will work toward common principles across their three agent identity protocols, under BuildFin.ai, the industry platform convened by the Monetary Authority of Singapore. The &lt;a href="https://investingnews.com/ant-international-mastercard-and-visa-initiate-collaboration-on-know-your-agent-interoperability-to-scale-agentic-commerce/" rel="noopener noreferrer"&gt;press release&lt;/a&gt; names three goals: cross-network operator traceability linking each agent to a validated operator, cardholder or business; shared certification requirements covering security and behavioral standards; and continuous transaction monitoring. It gives no specification and no date. Ant's chief innovation officer Jiang-Ming Yang put the practical aim in one sentence to &lt;a href="https://forkast.news/ant-international-visa-and-mastercard-agree-on-agent-identity-standard-now-comes-the-hard-part/" rel="noopener noreferrer"&gt;Forkast&lt;/a&gt;: "If an agent registers with Ant, they don't need to register again with Visa, Mastercard."&lt;/p&gt;

&lt;p&gt;Register once. That makes Know-Your-Agent a directory problem, and it is worth being precise about what a directory can say about an agent and what it cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three protocols already agree on the signature. They disagree on the directory.
&lt;/h2&gt;

&lt;p&gt;Visa's Trusted Agent Protocol, launched with Cloudflare in October 2025, signs the agent's HTTP request with &lt;a href="https://developer.visa.com/capabilities/trusted-agent-protocol/trusted-agent-protocol-specifications" rel="noopener noreferrer"&gt;RFC 9421 message signatures&lt;/a&gt;. The Signature-Input header carries a keyid, timestamps inside an eight-minute window, a nonce, and a tag that is either agent-browser-auth or agent-payer-auth. The merchant fetches the key named by keyid and checks an Ed25519 signature. Cloudflare's &lt;a href="https://blog.cloudflare.com/secure-agentic-commerce/" rel="noopener noreferrer"&gt;engineering writeup&lt;/a&gt; says where that key comes from: "Visa and Mastercard will be hosting their own directories for Visa-registered and Mastercard-registered agents, respectively."&lt;/p&gt;

&lt;p&gt;Mastercard's Verifiable Intent, published with Google in March 2026, works one layer up. Its &lt;a href="https://github.com/agent-intent/verifiable-intent/" rel="noopener noreferrer"&gt;specification&lt;/a&gt; is a chain of SD-JWT mandates from credential provider to user to agent, each layer bound to the next by an RFC 7800 confirmation claim, and in autonomous mode the mandate's cnf.jwk is the agent's key. The README lists "key management/provisioning" and "credential provider enrollment" as out of scope. The chain proves the agent acted inside what the user delegated, not how anyone learns which agent keys exist.&lt;/p&gt;

&lt;p&gt;Ant's Agentic Mobile Protocol, open-sourced in April, is the only one of the three whose materials describe rating an agent. Its &lt;a href="https://www.prnewswire.com/apac/news-releases/ant-international-launches-open-sourced-agentic-mobile-protocol-to-drive-ai-commerce-302755681.html" rel="noopener noreferrer"&gt;launch release&lt;/a&gt; describes a KYA framework that "establishes an agent's digital identity and certifies its authorised capabilities" and a proprietary Agent Trust Rating that "determines whether an agent is trustworthy and controls the level of autonomy."&lt;/p&gt;

&lt;p&gt;So the primitive is settled: the agent holds a private key and the relying party checks a signature. What is not settled is who runs the directory, and whether the directory may say more than "this key is ours". This week's announcement defers both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our registry signs
&lt;/h2&gt;

&lt;p&gt;We run the same shape of problem at &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt;, for traffic between agents rather than between an agent and a merchant.&lt;/p&gt;

&lt;p&gt;The daemon generates its own Ed25519 identity. The startup path in &lt;code&gt;pkg/daemon/daemon.go&lt;/code&gt; loads a persisted identity from the configured path and, if none exists, generates one and saves it. Registration sends the base64 public key with a signature over the registration message; the private key is not a field in that message. The registry assigns the 48-bit virtual address and records network membership.&lt;/p&gt;

&lt;p&gt;Any web service, on or off the network, can then ask the registry three questions about a request and get a signed answer. The mechanism is written up in &lt;a href="https://github.com/pilot-protocol/pilotprotocol/blob/main/docs/SIGNATURE-VERIFICATION.md" rel="noopener noreferrer"&gt;SIGNATURE-VERIFICATION.md&lt;/a&gt; in the main repo. A node signs a canonical envelope containing its address, a timestamp, a nonce, the SHA-256 of the request body, and an audience string naming the one service the signature is for. The service posts envelope and signature to &lt;code&gt;/api/v1/verify&lt;/code&gt; on the registry dashboard and gets back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;valid&lt;/code&gt;: the signature verifies against the registered, unexpired key for that address. A random Ed25519 key fails structurally, because no address is bound to it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;online&lt;/code&gt;: the node's last signature-verified heartbeat is within 180 seconds. The document is careful to call this standing, not reachability.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;network_member&lt;/code&gt;: the node belongs to the network in the address prefix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response also carries a &lt;code&gt;pilot-verdict-v1&lt;/code&gt; string signed by the registry's issuer key, so the service can cache it, forward it as proof, or check it offline. The issuer key is published at &lt;code&gt;/api/v1/verify/keys&lt;/code&gt;, and &lt;a href="https://polo.pilotprotocol.network/api/v1/verify/keys" rel="noopener noreferrer"&gt;the production endpoint&lt;/a&gt; returns one Ed25519 key under kid &lt;code&gt;vfy-v1&lt;/code&gt; at the time of writing. Failures are uniform: unknown node, reaped node, bad signature, stale timestamp and expired key all return the same &lt;code&gt;{"valid": false}&lt;/code&gt;, so the endpoint cannot be used to enumerate the address space. Nodes silent for longer than thirty minutes by default are reaped and stop verifying until they re-register.&lt;/p&gt;

&lt;p&gt;That is what continuous monitoring means at the identity layer for us: liveness and membership, signed, failing closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operator traceability without naming the operator
&lt;/h2&gt;

&lt;p&gt;KYA's first goal is linking each agent to a validated operator. Our version is the verified-address badge, defined in the &lt;a href="https://github.com/pilot-protocol/common/blob/main/badgeverify/badgeverify.go" rel="noopener noreferrer"&gt;badgeverify package&lt;/a&gt; of our common module. A badge is a detached Ed25519-signed credential bound to a single node ID, verified offline against a pinned issuer key, asserting that the address was verified through GitHub, Google or WorkOS. The package comment says it "deliberately carries NO raw external identity": no GitHub login, no email, only which provider vouched and when. The issuer's private key lives in Cloud KMS, and address recovery is signed by a separate cold keyring, so compromising the badge issuer cannot seize an address. &lt;code&gt;pilotctl verify --provider github&lt;/code&gt; runs the device flow end to end.&lt;/p&gt;

&lt;p&gt;That is a deliberate narrowing of traceability. A peer learns that a real account at a real provider stood behind this address when the badge was issued. It does not learn whose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision the directory refuses to make
&lt;/h2&gt;

&lt;p&gt;A merchant running Visa's protocol verifies a signature against Visa's directory and decides alone whether to serve the request. Between agents there is no merchant. So &lt;a href="https://www.ietf.org/archive/id/draft-teodor-pilot-protocol-01.html" rel="noopener noreferrer"&gt;our IETF draft&lt;/a&gt; makes trust bilateral. Section 10.6: two agents exchange trust requests on virtual port 444 with justification strings, and both must approve. Auto-approval happens only when both independently requested each other, or when both share a non-backbone network; otherwise the request queues for the receiving agent's operator. Nodes are private by default (section 10.7): the registry will not resolve a private node's endpoint without a mutual trust pair or a shared non-backbone network.&lt;/p&gt;

&lt;p&gt;The registry records the trust pair. It does not make it. The daemon does have a &lt;code&gt;--trust-auto-approve&lt;/code&gt; flag, and the enterprise security profile in &lt;code&gt;cmd/daemon/security_profile.go&lt;/code&gt; refuses to start with it set.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we do not do
&lt;/h2&gt;

&lt;p&gt;We do not certify behavior. The signed verdict says registered, alive and member; the badge says a provider vouched; the trust pair says a peer consented. None of that is a rating. KYA's second pillar, shared certification of "security and behavioral standards", is also the part none of the three payment protocols has published a mechanism for.&lt;/p&gt;

&lt;p&gt;The other thing worth saying plainly is in section 19.5 of our draft: the registry is a centralized trusted third party, and its compromise could allow public key substitution. Our mitigations are TLS with optional pinning, signature verification on node operations, hot-standby replication and audit logging, with distributed registry designs listed as future work. A shared KYA directory inherits the same single point, across three networks and the ten wallets in AMP's first phase. Register once is the right goal. It should come with a written statement of what the directory asserts about an agent, and what it does not.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>security</category>
      <category>fintech</category>
    </item>
    <item>
      <title>One PTY, Three Grids: How shell.online Picks a Terminal Size When a Phone Joins</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Fri, 11 Sep 2026 16:03:38 +0000</pubDate>
      <link>https://dev.to/artem_a/one-pty-three-grids-how-shellonline-picks-a-terminal-size-when-a-phone-joins-2g59</link>
      <guid>https://dev.to/artem_a/one-pty-three-grids-how-shellonline-picks-a-terminal-size-when-a-phone-joins-2g59</guid>
      <description>&lt;p&gt;A shared terminal has one process, one pseudoterminal, and one size. The people watching it do not. Yesterday's shell.online releases, &lt;a href="https://github.com/TeoSlayer/shell.online/releases" rel="noopener noreferrer"&gt;v0.11.2 and v0.11.3&lt;/a&gt;, changed how that one size gets picked when a phone joins a session. The mechanism is small, it is all in the open source, and it shows what a relay can decide without ever reading the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  One PTY grid, many viewers
&lt;/h2&gt;

&lt;p&gt;Every session started with &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;the shell.online CLI&lt;/a&gt; wraps the command in a PTY that the CLI owns. Viewers in a browser get the output of that PTY, but they cannot each have their own size, because the program inside (a shell, a build, Claude Code or Codex drawing a full-screen TUI) only sees one window size. If two viewers with different windows both got to resize it, each would keep deforming the other's screen.&lt;/p&gt;

&lt;p&gt;So the CLI refuses to treat viewer resize requests as resize requests. In &lt;code&gt;cmd/shell/session_unix.go&lt;/code&gt; the &lt;code&gt;protocol.Resize&lt;/code&gt; case is a comment and nothing else: a shared PTY keeps one canonical grid, and browser or local viewport changes are presentation-only. The relay agrees. In the Cloudflare Worker, a resize frame from an older browser client is validated (an encrypted one must be exactly 34 bytes, an unencrypted one must fall within 10 to 500 columns and 4 to 300 rows, or the socket closes with code 4002) and then dropped as a no-op.&lt;/p&gt;

&lt;p&gt;Instead there are three grids the PTY is ever allowed to run at, declared in &lt;code&gt;shared/terminal-grid.ts&lt;/code&gt; and mirrored as constants in the Go CLI:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grid&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;120x36&lt;/td&gt;
&lt;td&gt;Default, every viewer is on a desktop or tablet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80x40&lt;/td&gt;
&lt;td&gt;At least one viewer is a phone or in portrait, and the CLI is new enough&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80x24&lt;/td&gt;
&lt;td&gt;Same, but the CLI predates v0.11.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before this release the portrait grid was 80x24, a wide shape on a tall screen. The new 80x40 grid is what the changelog means by "fill portrait terminals": the validation run recorded in the pull request, at 390x844 against a real CLI and a local Worker, used 640 of 721 available terminal pixels with no horizontal overflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who decides, and from what
&lt;/h2&gt;

&lt;p&gt;The choice is made by the relay, specifically the Durable Object that holds a session's WebSockets, in a function called &lt;code&gt;broadcastTerminalGrid&lt;/code&gt;. It runs whenever a host or viewer connects, a viewer disconnects, or a viewer reports a layout change, and it reads exactly two kinds of metadata.&lt;/p&gt;

&lt;p&gt;The viewers. Each viewer socket carries a device class, derived from the &lt;code&gt;Sec-CH-UA-Mobile&lt;/code&gt; client hint or a user-agent regex in &lt;code&gt;worker/analytics.ts&lt;/code&gt;, plus a portrait flag. The browser sets that flag with a &lt;code&gt;layout=portrait&lt;/code&gt; query parameter on the WebSocket URL when it connects, and afterwards by sending a small &lt;code&gt;viewer_layout&lt;/code&gt; JSON message whenever orientation flips. If any live viewer is mobile or portrait, the session needs the narrow grid.&lt;/p&gt;

&lt;p&gt;The host. The Go relay client adds one header when it dials: &lt;code&gt;X-Shell-Terminal-Grid: 80x40&lt;/code&gt;. That is the entire capability negotiation. A CLI built before this release does not send the header, so the Worker records it as not supporting the taller grid and falls back to 80x24 when a phone joins. The pull request notes this is why the Worker could ship before any new CLI binary existed.&lt;/p&gt;

&lt;p&gt;The result goes out as a &lt;code&gt;terminal_size&lt;/code&gt; control message to every socket whose recorded grid differs. The CLI calls &lt;code&gt;ptmx.Resize&lt;/code&gt; only if the requested size is one of the three canonical grids. The browser client makes the same check in &lt;code&gt;adoptGrid&lt;/code&gt; and ignores anything else, since rendering a shape the CLI will never open a PTY at helps nobody.&lt;/p&gt;

&lt;p&gt;One detail in &lt;code&gt;web/main.ts&lt;/code&gt; is easy to miss. Opening the on-screen keyboard on a phone compresses the visual viewport until it is wider than it is tall, which would read as a rotation to landscape and resize the PTY out from under the person typing. The client keeps reporting whatever layout it last reported while the keyboard is open, using the keyboard detector that already existed for the mobile terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the relay is allowed to know this
&lt;/h2&gt;

&lt;p&gt;shell.online encrypts terminal frames end to end by default. The CLI seals output before it reaches the relay and the browser opens it locally, so the relay never sees what is on screen. What it does see is &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;listed in the README&lt;/a&gt;: connection metadata, encrypted frame sizes, timing, labels, and session lifecycle events.&lt;/p&gt;

&lt;p&gt;The grid decision fits inside that list. A user-agent string, a query parameter, a request header, and a plaintext layout message are all things the relay already had. Nothing about the negotiation loosened what the relay can read, and the sealed frames carrying the terminal bytes pass through it untouched. The relay picks the PTY size from who is connected, not from what they are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zoom stays local
&lt;/h2&gt;

&lt;p&gt;The other half of the release is pinch zoom, from 50% to 150%, and it deliberately never touches the PTY. &lt;code&gt;TerminalPinchZoomGesture&lt;/code&gt; in &lt;code&gt;web/touch-scroll.ts&lt;/code&gt; tracks the two original touch identifiers, returns nothing if a third finger appears, needs a 3 pixel change in finger distance before reporting, and scales from whatever zoom was active when the second finger landed. The chosen zoom is written to &lt;code&gt;localStorage&lt;/code&gt; when the fingers lift. Two people can watch the same 80x40 session at different zooms and neither affects the other. That is the same rule the desktop client adopted in v0.11.2 when it started sizing the font from the pane width instead of shrinking it until the whole grid fit in one direction; the changelog cites a 120x36 session on a 1440x900 window going from 11.5px filling 77% of the pane to 14.75px filling 98%.&lt;/p&gt;

&lt;p&gt;Rows get separate treatment in portrait. &lt;code&gt;terminalTypography&lt;/code&gt; in &lt;code&gt;web/mobile-viewport.ts&lt;/code&gt; raises line height from 1.18 to 1.8 when the viewport is taller than 1.15 times its width, spreading 40 rows down the screen instead of bunching them at the top.&lt;/p&gt;

&lt;h2&gt;
  
  
  From the agent's side
&lt;/h2&gt;

&lt;p&gt;None of this is visible to the process being shared. A coding agent running under &lt;code&gt;shell claude&lt;/code&gt; gets a window-size change when a phone joins and another when it leaves, the same as if someone had dragged a terminal window, and it redraws. We build shell.online at &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; as the place to hand a running agent's terminal to a person, and that person is increasingly holding a phone. The Worker and CLI halves of the change sit side by side in &lt;a href="https://github.com/TeoSlayer/shell.online/pull/100" rel="noopener noreferrer"&gt;pull request #100&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>terminal</category>
      <category>opensource</category>
    </item>
    <item>
      <title>DeepSeek Harness Believed the Host Header. shell.online Now Documents Self-Hosting, and TRUST_PROXY Ships Off.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Thu, 10 Sep 2026 20:34:55 +0000</pubDate>
      <link>https://dev.to/artem_a/deepseek-harness-believed-the-host-header-shellonline-now-documents-self-hosting-and-trustproxy-2n4e</link>
      <guid>https://dev.to/artem_a/deepseek-harness-believed-the-host-header-shellonline-now-documents-self-hosting-and-trustproxy-2n4e</guid>
      <description>&lt;p&gt;VulnCheck published CVE-2026-82533 on September 8. It is against DeepSeek Harness, the open-source tool that runs a coding agent's commands inside an operating-system sandbox on the developer's machine, and it scores 9.4. The &lt;a href="https://www.ox.security/blog/cve-2026-82533-deepseek-harness-ai-agent-sandbox-escape/" rel="noopener noreferrer"&gt;OX Security write-up&lt;/a&gt; by Nir Zadok and Moshe Siman Tov Bustan describes the whole bug in one function. &lt;code&gt;isTrustedApiRequest&lt;/code&gt; read the request's &lt;code&gt;Host&lt;/code&gt; header, allowed the call if the value was a loopback authority or appeared in a configured &lt;code&gt;trustedHosts&lt;/code&gt; list, and never compared it with the connection's actual peer address.&lt;/p&gt;

&lt;p&gt;The harness hands the agent's shell the address of its own local API and the current session id. So the escape was one curl from inside the sandbox to the harness's session-update endpoint, with a &lt;code&gt;Host&lt;/code&gt; header of &lt;code&gt;127.0.0.1&lt;/code&gt; and a body that set &lt;code&gt;approvalPolicy&lt;/code&gt; to &lt;code&gt;never&lt;/code&gt; and &lt;code&gt;dangerMode&lt;/code&gt; to &lt;code&gt;danger-full-access&lt;/code&gt;. Every command after that ran unconfined and without a prompt. Versions 0.1.1-rc.2 and earlier are affected. The fix landed on August 27 in 0.1.2-alpha.1, and &lt;a href="https://thehackernews.com/2026/09/deepseek-harness-flaw-let-ai-agents.html" rel="noopener noreferrer"&gt;The Hacker News&lt;/a&gt; reports it replaced the header check with a one-time startup token that the browser exchanges for a signed cookie.&lt;/p&gt;

&lt;p&gt;A header is something the caller wrote. Nothing about that changes because the header is named &lt;code&gt;Host&lt;/code&gt;, or &lt;code&gt;X-Forwarded-For&lt;/code&gt;, or because the value it carries happens to be a loopback address.&lt;/p&gt;

&lt;h2&gt;
  
  
  A self-hosting guide merged to shell.online today
&lt;/h2&gt;

&lt;p&gt;This is worth writing up today because the shell.online repository merged &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/SELF-HOSTING.md" rel="noopener noreferrer"&gt;a self-hosting guide&lt;/a&gt; today, and the first thing an operator running our accounts app has to decide is exactly the question DeepSeek Harness got wrong: which request headers do you believe about where a request came from.&lt;/p&gt;

&lt;p&gt;The guide splits &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; into two services. The relay serves the public site and carries encrypted terminal frames. The accounts app in &lt;code&gt;app/&lt;/code&gt; is optional, adds sign-in, organizations, linked machines and the shared session list, and the CLI does not need it.&lt;/p&gt;

&lt;p&gt;The relay runs on Cloudflare Workers and needs Durable Objects, Rate Limiting and Analytics Engine. The new &lt;code&gt;wrangler.example.jsonc&lt;/code&gt; carries no account identifiers or credentials: two Durable Object classes, &lt;code&gt;TerminalSession&lt;/code&gt; and &lt;code&gt;StatsStore&lt;/code&gt;, and four rate limiters. Session creation is 10 per minute, connections and events 120 per minute, stats login 10 per minute. Copy it to &lt;code&gt;wrangler.local.jsonc&lt;/code&gt;, deploy, and point the CLI at the result with &lt;code&gt;shell --server https://example.workers.dev &amp;lt;command&amp;gt;&lt;/code&gt; or the &lt;code&gt;SHELL_ONLINE_SERVER&lt;/code&gt; variable.&lt;/p&gt;

&lt;p&gt;What the operator of that relay can see is the same as what we can see on the hosted one, and the README states it: connection metadata, encrypted frame sizes, timing, labels and session lifecycle events. The CLI owns the PTY and encrypts frames before they reach the relay. The browser decrypts them. Running the relay yourself moves the metadata into your own Cloudflare account. It does not give you the terminal.&lt;/p&gt;

&lt;p&gt;The accounts app needs Firebase Authentication and PostgreSQL. &lt;code&gt;docker compose up --build -d&lt;/code&gt; in &lt;code&gt;app/&lt;/code&gt; starts the client and API together on port 8080 and proxies &lt;code&gt;/relay/*&lt;/code&gt; to whichever relay &lt;code&gt;RELAY_URL&lt;/code&gt; names. &lt;code&gt;shell login&lt;/code&gt; is pointed at it with &lt;code&gt;SHELL_ONLINE_ACCOUNTS&lt;/code&gt; and &lt;code&gt;SHELL_ONLINE_WEB&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The header our accounts app refuses by default
&lt;/h2&gt;

&lt;p&gt;Among the variables in the app's &lt;code&gt;.env.example&lt;/code&gt; is one line that the DeepSeek advisory makes easy to explain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# Set only behind a proxy that replaces X-Forwarded-For
&lt;/span&gt;&lt;span class="n"&gt;TRUST_PROXY&lt;/span&gt;=&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The doc comment on &lt;code&gt;callerAddress&lt;/code&gt; in &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/app/server/lib/rate-limit.ts" rel="noopener noreferrer"&gt;&lt;code&gt;app/server/lib/rate-limit.ts&lt;/code&gt;&lt;/a&gt; says why: "&lt;code&gt;X-Forwarded-For&lt;/code&gt; is a request header like any other: anyone can send one. It is read only when the deployment says it sits behind a proxy that rewrites it, because trusting it otherwise would let a caller pick a new identity per request and walk straight past the limiter." With the flag off, the address held responsible for a request is the socket's remote address, and the header is ignored. There is a test for it, named "does not believe a forwarded address unless a proxy is trusted", and its comment states the failure it guards against: without the default, "a caller sets X-Forwarded-For to a new value per request and the limiter never sees the same caller twice."&lt;/p&gt;

&lt;p&gt;On the hosted deployment the app runs as a Worker, and there the flag is hard-coded off with a comment that it "must stay off". The adapter takes the caller's address from &lt;code&gt;CF-Connecting-IP&lt;/code&gt;, which the Cloudflare edge writes and a caller cannot forge past it. The relay Worker does the same: every rate limiter in &lt;code&gt;worker/index.ts&lt;/code&gt; is keyed on &lt;code&gt;CF-Connecting-IP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the difference between the two designs. DeepSeek Harness asked the request what it was. Our app asks the transport, and only believes a header when the operator has stated, in configuration, that a specific proxy in front is the one writing it.&lt;/p&gt;

&lt;p&gt;The same split exists in &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt;, one layer down. The &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;rendezvous server&lt;/a&gt; that handles registration and NAT hole-punching can be run by anyone with &lt;code&gt;rendezvous -registry-addr :9000 -beacon-addr :9001&lt;/code&gt;, and it is out of the data path once a tunnel is up. When hole-punching fails, the beacon relays the traffic, still end-to-end encrypted. Self-hosting the coordination point changes who sees the metadata. It cannot change who can read the payload, because the relay was never given a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the same PR removed
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/TeoSlayer/shell.online/pull/97" rel="noopener noreferrer"&gt;PR #97&lt;/a&gt; also deleted the three GitHub Actions workflows added in 0.10.0 that ran LLM-assisted issue triage, pull-request review and release notes, along with their five prompt files, the moderation policy script and its test. The triage workflow ran two model calls on every new issue and, when both agreed, called the &lt;code&gt;deleteIssue&lt;/code&gt; GraphQL mutation, falling back to close-and-lock if GitHub refused permanent deletion. The paragraph in &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; describing the 98% confidence threshold is gone with it, and the moderation test is out of &lt;code&gt;npm test&lt;/code&gt; and CI. The four workflows left, CI, CodeQL, the container build and the app deploy, do not touch issues at all. As of today, nothing in the repository closes or deletes an issue except a person.&lt;/p&gt;

&lt;p&gt;Four internal deployment documents under &lt;code&gt;app/&lt;/code&gt; went too, replaced by the guide above and a shorter app README. The Docker Compose image is now pinned to 0.11.1, which matches the release, and Wrangler moved to 4.131.&lt;/p&gt;

&lt;p&gt;For anyone deciding whether to run the relay themselves: the guide is the file to read, and &lt;code&gt;TRUST_PROXY&lt;/code&gt; is the line to get right before the app is reachable from anywhere but localhost.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>security</category>
    </item>
    <item>
      <title>Every Teammate Could Open Every Session. The Bug Report Said It Was About Assignees.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:33:37 +0000</pubDate>
      <link>https://dev.to/artem_a/every-teammate-could-open-every-session-the-bug-report-said-it-was-about-assignees-25g5</link>
      <guid>https://dev.to/artem_a/every-teammate-could-open-every-session-the-bug-report-said-it-was-about-assignees-25g5</guid>
      <description>&lt;p&gt;Yesterday's post on &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; said that when the owner's browser sees a session it holds the password for, it seals that password once per organization member who has published a key. That sentence was accurate, and it was describing a bug. The &lt;a href="https://github.com/TeoSlayer/shell.online/pull/85" rel="noopener noreferrer"&gt;pull request merged to main yesterday&lt;/a&gt;, titled "Fix what production found", closes what it describes as thirteen reports from using the app. One of them was that sentence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The report was about assignees. The bug was not.
&lt;/h2&gt;

&lt;p&gt;The report read "passwords can be bypassed on Mobile by changing assignee". Changing the assignee was not the bypass. The &lt;code&gt;shareAnyPending&lt;/code&gt; routine in the workspace page ran on every poll, took every member with a published browser key, and sealed the session password to each of them as soon as the owner's browser saw a session it held the password for. Assigning a session to a colleague looked like it granted them something. They had held the password all along. Assignment only changed who could type.&lt;/p&gt;

&lt;p&gt;Nothing about the envelope was wrong. The construction is still ECDH P-256 into HKDF-SHA256 into AES-GCM, done with WebCrypto in the browser, and the accounts service still stores one opaque blob per recipient and can open none of them. But the recipient list is the whole of the access rule, and the recipient list was "the team".&lt;/p&gt;

&lt;h2&gt;
  
  
  Three changes, all in the repo
&lt;/h2&gt;

&lt;p&gt;The New Session form now has a fieldset headed "Who can open it", one checkbox per reachable teammate, and it starts empty. The comment above it in the &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;shell.online source&lt;/a&gt; puts it plainly: "Starting from nobody makes the answer something chosen rather than something discovered later." The password is still generated in the browser rather than typed. Ticking a name replaces inventing a secret and passing it around by hand, which was the other half of the same report. A teammate who has never opened the app in a browser has no published key, so the form lists them separately and says there is nowhere to send them a password.&lt;/p&gt;

&lt;p&gt;The rule moved out of the component. &lt;code&gt;sealTargets&lt;/code&gt; in &lt;code&gt;app/src/lib/session-share.ts&lt;/code&gt; takes the roster, the current user, the chosen list and the set already sealed, and returns who still needs it. Its test file states the property that matters in a comment: "not choosing somebody is not sharing with them." Chosen but holding no key is skipped. Chosen but yourself is skipped. Chosen but not in the team is ignored. The server-side checks were already in place. The keys route refuses any recipient who is not an organization member, and only the session owner may put shares at all.&lt;/p&gt;

&lt;p&gt;The audience is stored next to the password in localStorage, scoped to the signed-in account. The service is not asked who holds a copy, because it is not trusted with the password and should not be the record of who can read it either. The same applies to a session started from a terminal on a linked machine: a password typed into the gate is now kept, and its audience starts empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  The button that was left out
&lt;/h2&gt;

&lt;p&gt;The session page has a "Who can open it" section and it only adds people. There is no remove. The comment on the &lt;code&gt;SessionAudience&lt;/code&gt; component explains the choice: removing somebody "would not reach into their browser and take back the copy they hold, so a control that offered it would be describing something that did not happen." A share link plus its password is a bearer capability, as the terms say. The honest form of revocation is to stop the session and start another one with a different list. A button that appears to revoke is worse than no button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple assignees, merged this morning
&lt;/h2&gt;

&lt;p&gt;A separate change landed today. A session can now be assigned to several people at once. &lt;code&gt;canEdit&lt;/code&gt; in &lt;code&gt;session-view.ts&lt;/code&gt; grants typing to the owner and to anyone in the &lt;code&gt;assigneeUids&lt;/code&gt; array, and it still reads the old single &lt;code&gt;assigneeUid&lt;/code&gt; field for clients that predate the change. Migration 006 adds the array column to PostgreSQL, backfills it from the single column, and keeps the single column populated with the first assignee, so the previous Worker remains safe to roll back to. The terms were updated in the same PR: a session "has an owner and may have multiple assignees, and only they can edit it".&lt;/p&gt;

&lt;p&gt;Put the two changes side by side and the model is now separable. Assignment decides who may type, and the session owner or a team owner or admin can change it. The audience decides who can open the session at all, and only the person holding the password can extend it. Before this week those two lists were tangled, and the tangle is what made the report look like it was about assignees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug nobody reported
&lt;/h2&gt;

&lt;p&gt;The same PR fixed something no report mentioned. The clipboard component derived every session's password on every poll, because its effect depended on the session object the poll replaces rather than on the sealed bytes inside it. The workspace polls every four seconds. That is one ECDH derivation per session every four seconds, on a phone, and the PR names it as the likeliest cause of the report that "tabs are a bit slow on mobile". The effect now keys on the session id, the sender public key and the ciphertext.&lt;/p&gt;

&lt;h2&gt;
  
  
  The default Pilot starts from
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;Pilot Protocol README&lt;/a&gt; lists two lines under security that describe the posture shell.online has now arrived at for session passwords: nodes are private by default, and reachability comes from a signed mutual trust handshake. Address an agent on &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot&lt;/a&gt; without that handshake and the daemon answers "hostname not found or no mutual trust". A peer you did not name has no path to you. Between the first release carrying the web app and yesterday's fix, a browser terminal on shell.online had the opposite default, and the fix was to make the list of who can reach a session a list somebody actually wrote.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
    <item>
      <title>adk web Took a CVSS 10 From a Denylist That Had profile but Not cProfile. Where Our Own Watch-or-Type Line Sits</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Thu, 10 Sep 2026 16:08:13 +0000</pubDate>
      <link>https://dev.to/artem_a/adk-web-took-a-cvss-10-from-a-denylist-that-had-profile-but-not-cprofile-where-our-own-4en6</link>
      <guid>https://dev.to/artem_a/adk-web-took-a-cvss-10-from-a-denylist-that-had-profile-but-not-cprofile-where-our-own-4en6</guid>
      <description>&lt;p&gt;Google published &lt;a href="https://github.com/advisories/GHSA-q9cv-5mjc-7cjc" rel="noopener noreferrer"&gt;GHSA-q9cv-5mjc-7cjc&lt;/a&gt; yesterday, CVE-2026-79696, against the Agent Development Kit for Python. Critical, CVSS 4.0 score of 10.0, weakness class CWE-184, "Incomplete List of Disallowed Inputs". The description: a code injection in &lt;code&gt;adk web&lt;/code&gt;, versions 2.0.0 through 2.6.0, on plain Python, Cloud Run and GKE where pytest is installed, that "allows an unauthenticated remote attacker to execute arbitrary code using a crafted test session replay". Patched in 2.7.0, which shipped on August 13.&lt;/p&gt;

&lt;p&gt;The advisory links one commit. It is a small diff, and the commit message explains the whole class of bug better than the CVE text does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The denylist had profile but not cProfile
&lt;/h2&gt;

&lt;p&gt;ADK agents can be defined in YAML, and a config can name Python code by dotted path in its callback, tool, schema and model fields. The loader resolves that name by importing it. To stop a config from naming &lt;code&gt;os.system&lt;/code&gt;, there was a denylist of top-level modules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/google/adk-python/commit/a16f6da3314b8dcd9925884cd6fc7fc9ffdd570d" rel="noopener noreferrer"&gt;Commit a16f6da&lt;/a&gt;, dated August 7, says what went wrong: "it had &lt;code&gt;profile&lt;/code&gt; but not &lt;code&gt;cProfile&lt;/code&gt;, &lt;code&gt;pdb&lt;/code&gt; but not &lt;code&gt;bdb&lt;/code&gt;, &lt;code&gt;trace&lt;/code&gt;, &lt;code&gt;timeit&lt;/code&gt; or &lt;code&gt;pydoc&lt;/code&gt;. Several of those execute a string you hand them and need no constructor &lt;code&gt;args&lt;/code&gt;, so naming one as a tool or callback slipped past both existing mitigations and ran arbitrary code." The tests added with it enumerate what now gets rejected, from &lt;code&gt;cProfile.run&lt;/code&gt; to &lt;code&gt;doctest.testmod&lt;/code&gt; and &lt;code&gt;py_compile.compile&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix stops enumerating. It blocks every name in &lt;code&gt;sys.stdlib_module_names&lt;/code&gt; and keeps a hand-written list only for modules that left the standard library but still import: &lt;code&gt;distutils&lt;/code&gt; through the setuptools shim, &lt;code&gt;telnetlib&lt;/code&gt;, &lt;code&gt;pipes&lt;/code&gt;, and CPython's own &lt;code&gt;test&lt;/code&gt; package, whose &lt;code&gt;support.script_helper&lt;/code&gt; can start a subprocess. The reasoning is that a config should only ever name the agent's own package, &lt;code&gt;google.adk&lt;/code&gt;, or an integration, so nothing legitimate lives in the standard library at all.&lt;/p&gt;

&lt;p&gt;The commit ends with the sentence that belongs at the top of the advisory: "A denylist still cannot cover third-party packages, which the loader resolves by name, so this narrows the surface rather than closing it."&lt;/p&gt;

&lt;h2&gt;
  
  
  The server was documented as unauthenticated a week earlier
&lt;/h2&gt;

&lt;p&gt;Two other commits in the same 2.7.0 release belong next to that one. On July 30, &lt;a href="https://github.com/google/adk-python/commit/76c64efd003f142e5374965d695e4792d1bcba7b" rel="noopener noreferrer"&gt;commit 76c64ef&lt;/a&gt; added docstrings to the API server, the dev server and the &lt;code&gt;adk web&lt;/code&gt; command. The API server's reads: "The served endpoints are unauthenticated. Any client that can reach the server can read and write sessions, memory, and artifacts and run agents for any user or app." The dev server's adds that its endpoints "additionally read and write agent files on disk and run evaluation and debugging code." On August 12, &lt;a href="https://github.com/google/adk-python/commit/899500510d6820734d2c83857aea9f958dea8ed5" rel="noopener noreferrer"&gt;commit 8995005&lt;/a&gt; restricted the builder's YAML code references to the app being edited.&lt;/p&gt;

&lt;p&gt;Put the three together and the reachable surface was: an HTTP server with no authentication, a dev-only endpoint that runs evaluation code, and a config loader that imports whatever a config names. Only the denylist was pretending to be a boundary. The advisory lists Cloud Run and GKE as affected environments, and that is where "unauthenticated remote attacker" comes from: a development server whose own docstring says localhost, reachable from a network.&lt;/p&gt;

&lt;p&gt;People do that because they want to see a running agent from somewhere other than the machine it runs on. That is what &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; is for, so the same question applies to us: once a browser can reach a running agent, what decides whether it can only watch or also drive?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where our line is, and which part of it is hard
&lt;/h2&gt;

&lt;p&gt;There are two layers, and the README states the first one plainly. The URL and password together grant access, and "anyone with both can view the terminal and, unless the link is read-only, type with the wrapped process's permissions." A link started with &lt;code&gt;--read-only&lt;/code&gt; has browser input rejected server-side. That is the hard boundary. The relay sees connection and lifecycle metadata and ciphertext, not who is behind a browser.&lt;/p&gt;

&lt;p&gt;The team web app adds a second layer, and it is worth being exact about what kind of layer it is. Who can open a session is decided by who its password was sealed to. The comment in &lt;code&gt;app/src/lib/session-share.ts&lt;/code&gt; puts it this way: "A colleague who was not sealed to holds nothing: not a weaker copy, not a copy the service could hand over, nothing." A member who has never opened the app in a browser has published no public key, so the picker cannot offer them.&lt;/p&gt;

&lt;p&gt;Who can type is a separate rule, in &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/app/src/lib/session-view.ts" rel="noopener noreferrer"&gt;&lt;code&gt;app/src/lib/session-view.ts&lt;/code&gt;&lt;/a&gt;: the session's owner and its assignees, and nobody on a read-only session. The terminal pane drops keystrokes when that check fails and disables stdin in xterm. Read that as what it is: a client-side rule for people who already hold the password. It stops a colleague who was given the password to watch from typing by accident. It does not stop someone holding the raw link and password from opening it outside the app.&lt;/p&gt;

&lt;p&gt;The change merged today is that a session can have several assignees instead of one. In &lt;a href="https://github.com/TeoSlayer/shell.online/pull/89" rel="noopener noreferrer"&gt;PR #89&lt;/a&gt; the assignment route takes a list of member ids, capped at 50, each of which must belong to the organization. Only the session's owner or an admin may change it. The change is written to the audit trail as a &lt;code&gt;handoff&lt;/code&gt; event naming who was added, and each added person is notified. The &lt;a href="https://github.com/TeoSlayer/shell.online/blob/main/app/server/lib/migrations/006_multi_assignees.sql" rel="noopener noreferrer"&gt;migration&lt;/a&gt; adds an &lt;code&gt;assignee_uids&lt;/code&gt; text array and backfills it from the old single column, which stays populated with the first assignee so the previous Worker build remains safe to roll back to.&lt;/p&gt;

&lt;p&gt;One more thing, because a reader with the repo would find it. What a team member types into a session from the browser is recorded to the accounts service in plaintext. &lt;code&gt;input-log.ts&lt;/code&gt; assembles keystrokes into submitted lines (Enter emits a line, Ctrl-C emits an interrupt, escape sequences are dropped), and every member of the team can read and export them. Terminal output is not recorded; it stays inside the end-to-end encrypted stream. The terms of service say this in bold. A comment at the top of &lt;code&gt;app/server/routes/audit.ts&lt;/code&gt; still says terminal input is deliberately excluded. That comment is stale and the terms are right.&lt;/p&gt;

&lt;p&gt;The audit API reading those records also changed today: server-side pages of 50, at most 100, newest first, filterable by session, actor, kind, free text and a since timestamp, with a total so the page says how many matched rather than how many it fetched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write down what is unauthenticated
&lt;/h2&gt;

&lt;p&gt;Across ADK's three commits, the durable fix was the documentation one. A denylist needs a new entry every Python release. A sentence that says "every endpoint here is unauthenticated" changes how the next person deploys the thing.&lt;/p&gt;

&lt;p&gt;We hold the &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; spec to the same test. The &lt;a href="https://www.ietf.org/archive/id/draft-teodor-pilot-protocol-01.html" rel="noopener noreferrer"&gt;IETF draft&lt;/a&gt; gives every node an Ed25519 keypair at registration, uses X25519 for tunnel key agreement and AES-256-GCM for frames, and binds the ephemeral key to the node identity with an Ed25519 signature in the PILA frame. The same section also says, in as many words, that the unauthenticated PILK exchange "provides confidentiality but not authentication" and that a peer who does not answer a key exchange falls back to plaintext. We would rather that sentence sit in the draft, and in the &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;reference implementation&lt;/a&gt;, than have someone discover it from a CVE.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>news</category>
    </item>
    <item>
      <title>NSA, CISA and FBI Advise AI Labs to Quietly Degrade Answers to Suspected Distillers. A New Agent Fleet Fits the Profile.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Wed, 09 Sep 2026 17:38:00 +0000</pubDate>
      <link>https://dev.to/artem_a/nsa-cisa-and-fbi-advise-ai-labs-to-quietly-degrade-answers-to-suspected-distillers-a-new-agent-4ff5</link>
      <guid>https://dev.to/artem_a/nsa-cisa-and-fbi-advise-ai-labs-to-quietly-degrade-answers-to-suspected-distillers-a-new-agent-4ff5</guid>
      <description>&lt;p&gt;The NSA, CISA and FBI published joint advisory &lt;a href="https://www.cisa.gov/news-events/cybersecurity-advisories/aa26-251a" rel="noopener noreferrer"&gt;AA26-251A&lt;/a&gt; on September 8. It names DeepSeek, Moonshot AI, Alibaba, MiniMax, StepFun and Z.AI, and says the six have pulled billions of tokens across millions of requests out of Claude, GPT, Gemini and Grok since at least late 2024. The line that matters is in the executive summary: "distillation is not a supplement to these companies' AI model development, but the critical core of it."&lt;/p&gt;

&lt;p&gt;Most coverage stops at the accusation. The advisory is more useful for what it tells the labs to do about it, and for the suspect profile it hands them, because that profile fits a lot of legitimate agent traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the access layer was built
&lt;/h2&gt;

&lt;p&gt;There was no exploit. &lt;a href="https://www.anthropic.com/news/detecting-and-preventing-distillation-attacks" rel="noopener noreferrer"&gt;Anthropic's February writeup&lt;/a&gt; put the number at about 24,000 fraudulent accounts and over 16 million exchanges, routed through "commercial proxy services which resell access to Claude" and what it called hydra clusters, networks of accounts that spread traffic across the first-party API and third-party cloud platforms. OpenAI told the House Select Committee &lt;a href="https://finance.yahoo.com/news/openai-accuses-deepseek-distilling-us-221629899.html" rel="noopener noreferrer"&gt;the same month&lt;/a&gt; that DeepSeek-linked accounts reached its models through obfuscated third-party routers.&lt;/p&gt;

&lt;p&gt;The advisory generalizes that into a pathway list: native APIs, remote cloud providers, and "third-party aggregators that automatically obfuscate user metadata to avoid detection". On top sits a gray market of API proxies the document calls "transfer stations", used to "bypass U.S. AI companies' regional restrictions, breach terms of use, evade safeguards, and undermine traceability". The campaigns run "automated failover between pathways during blocking attempts" and "sophisticated quality evaluation frameworks to detect defensive countermeasures". MiniMax "redirected exchanges to a new Claude model within 24 hours of release".&lt;/p&gt;

&lt;p&gt;Read as infrastructure, that is an overlay network whose design goal is that no request can be tied to its origin. Every bearer token is replaceable. Every IP is replaceable. The asset is the pipeline behind them, which scores every path and swaps out a degraded one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the labs are told to do
&lt;/h2&gt;

&lt;p&gt;Watch behaviour, alter responses, share indicators.&lt;/p&gt;

&lt;p&gt;The behavioural guidance is specific. Labs "should strengthen identity verification for accounts and track individual subscriptions with enterprise-scale throughput, accounts deviating from legitimate patterns, and new accounts immediately at maximum usage versus a gradual ramp-up or with consistent quota exhaustion." The TTP section adds: "shared accounts from multiple IPs/user agents, 24/7 sustained usage without human variation/idle periods, anomalous subscription-to-API usage ratios, and new subscriptions immediately at maximum usage as opposed to gradual AI adoption."&lt;/p&gt;

&lt;p&gt;The response-alteration section says "targeted changes in response to high-confidence malicious distillation requests can impose meaningful costs", and suggests differential privacy or "less sophisticated 'downgraded' models". Then the sentence to read twice: "Reducing reasoning depth, presenting correct information with different reasoning, or stylistic inconsistencies may evade detection while reducing training usefulness." A good countermeasure, by this definition, is one the other side's quality evaluation does not catch. Anthropic had already said in February it was building "model-level safeguards designed to reduce the efficacy of model outputs for illicit distillation, without degrading the experience for legitimate customers". The advisory turns that into guidance for the whole industry.&lt;/p&gt;

&lt;p&gt;Sharing closes the loop. Indicators passed "between cloud providers, model aggregators, and model providers can make distributed infrastructure visible as coordinated campaigns versus isolated anomalies". An aggregator flagged by one provider becomes a signal at the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now describe an agent fleet
&lt;/h2&gt;

&lt;p&gt;A team creates a new organization, gets an API key, and starts a batch of coding or research agents. The account is days old and at its rate limit from the first hour, because agents do not adopt gradually. Traffic runs around the clock with no idle periods, because nothing is sleeping. One key is shared by every worker across autoscaled egress IPs. Requests go through an aggregator for failover and cost, which strips the metadata. That is four of the listed indicators, matched by a fleet doing nothing wrong.&lt;/p&gt;

&lt;p&gt;The advisory scopes the response to "high-confidence" cases and each lab will set its own threshold. But the countermeasure is designed not to be noticed, and unlike the distillers, most legitimate teams run no per-path quality evaluation at all. A switch to a shallower reasoning path would surface as a vague sense that the agents got worse.&lt;/p&gt;

&lt;p&gt;Two practical consequences follow.&lt;/p&gt;

&lt;p&gt;Make the traffic attributable on purpose. One key per workload rather than one key across the fleet. Stable egress where you can get it. Go direct to the provider for anything that matters and keep the aggregator for what can tolerate it, since a flagged aggregator is flagged for everyone behind it. Ramp new accounts instead of pinning them at the ceiling on day one.&lt;/p&gt;

&lt;p&gt;Measure what comes back. The distillers keep a held-out evaluation running against every path so a degraded one is replaced within hours. A legitimate fleet needs the same instrument for the opposite reason: to know whether the answers it pays for are the answers it was sold. A fixed set of prompts with known-good outputs, replayed against production daily, is enough to see reasoning depth move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where identity lives on the wire
&lt;/h2&gt;

&lt;p&gt;The whole detection section exists because an API key and a source IP are separate facts that a server has to correlate afterwards. "Shared accounts from multiple IPs" is only a signal because the credential and the endpoint have nothing to do with each other.&lt;/p&gt;

&lt;p&gt;Pilot Protocol does that binding differently, and the README in &lt;a href="https://github.com/pilot-protocol/pilotprotocol/blob/main/README.md" rel="noopener noreferrer"&gt;our repository&lt;/a&gt; lists the pieces under Security: an authenticated key exchange with Ed25519-signed X25519 and AES-256-GCM, "Ed25519 identity keys bound to tunnel sessions", nodes private by default, and a signed mutual trust handshake relayed through the registry. A peer on &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; is a key, and the tunnel is bound to that key. There is no shared bearer token to spread across IPs. SYN, reply and rekey rate limits, each with a node whitelist, are daemon flags in the same README's environment table.&lt;/p&gt;

&lt;p&gt;That deserves a precise statement rather than a generous one. A key is not a company. Anyone can mint ten thousand of them, and private-by-default only holds if the operator does not start the daemon with the &lt;code&gt;--trust-auto-approve&lt;/code&gt; flag that the &lt;a href="https://github.com/pilot-protocol/pilotprotocol/blob/main/docs/cli-reference.md" rel="noopener noreferrer"&gt;CLI reference&lt;/a&gt; documents. What the binding buys is that the "who" a service rate-limits or refuses is the same "who" that encrypted the packets, so the trust decision happens before the first request instead of being reconstructed from logs after the sixteen-millionth. The Sybil problem stays exactly where the advisory leaves it: with verification, and with indicators shared between the parties who can see the whole picture.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>security</category>
      <category>agents</category>
    </item>
    <item>
      <title>Handing a Running Terminal to a Colleague While the Server Holds Neither Key</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Wed, 09 Sep 2026 16:06:04 +0000</pubDate>
      <link>https://dev.to/artem_a/handing-a-running-terminal-to-a-colleague-while-the-server-holds-neither-key-24g9</link>
      <guid>https://dev.to/artem_a/handing-a-running-terminal-to-a-colleague-while-the-server-holds-neither-key-24g9</guid>
      <description>&lt;p&gt;Yesterday's post on &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; accounts covered what the accounts service stores and how a browser-started password is sealed to one machine. This one is about the part that turns terminal sharing into a team feature: a session has an owner and an assignee, the owner can hand it to a colleague while it is running, and everyone in the organization can watch. The interesting part is what has to move between people for that to work, and what never does.&lt;/p&gt;

&lt;p&gt;A version note first. The changelog files all of this under 0.9.0, but there is no 0.9.0 tag. v0.10.0, tagged on 2026-09-08, is the first release carrying the web app, and v0.11.0 and v0.11.1 followed within a day. Everything below is read from the current main branch of the &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;shell.online source&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  One organization per account
&lt;/h2&gt;

&lt;p&gt;Signing up creates an organization, named from the email domain unless the domain is on a hardcoded list of fifteen personal providers, in which case it becomes "Firstname's organization". Everyone belongs to exactly one.&lt;/p&gt;

&lt;p&gt;Invites are links of the form &lt;code&gt;/join/&amp;lt;id&amp;gt;&lt;/code&gt;. Each works once and expires after seven days. An invite can be pinned to an email address; left blank, the Team page labels it "Anyone with the link" and tells you to send that one carefully. Owners and admins invite members, and only the owner can invite an admin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Owner, assignee, watcher
&lt;/h2&gt;

&lt;p&gt;When a linked machine publishes a session, the record gets an owner and an assignee, both set to whoever started it. Everyone in the organization sees it in the list. Owner and assignee can type. Everyone else watches, and the pane says so: "Watching. Only the owner and assignee can type."&lt;/p&gt;

&lt;p&gt;Handoff changes the assignee. The session's owner may do it, and so may an organization owner or admin. The new assignee gets an "assigned" notification, counted apart from mentions in the inbox, and the audit log records a handoff event naming them. v0.11.0 fixed a bug where a colleague already watching when handed the session had to reopen it before typing.&lt;/p&gt;

&lt;p&gt;What handoff does not change is who controls the machine. Only the session owner can queue a stop, and only the owner can publish key shares. The server puts it precisely: "Assignment grants terminal input, not control over the owner's machine, and an organization role must never silently broaden into remote-process administration." Starting from the browser follows the same line. The New Session dialog lists your own linked machines, and the start route looks the device up in the caller's own device list. You start work on your machines and hand it to colleagues. You do not start processes on theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two envelopes, two keys, a service that opens neither
&lt;/h2&gt;

&lt;p&gt;The password for a browser-started session travels twice, sealed both times.&lt;/p&gt;

&lt;p&gt;The first envelope goes to the machine. The daemon that &lt;code&gt;shell login&lt;/code&gt; starts generates an ephemeral P-256 key pair per run and publishes the public half on every two-second poll, so a restart re-keys. The browser generates its own ephemeral P-256 pair, runs ECDH against the machine's key, derives a 256-bit AES-GCM key with HKDF-SHA256 and the info string "shell.online cli password v1", and encrypts the password under a random 12-byte nonce. The accounts service stores the sender key and sealed password as opaque strings. The daemon opens the envelope with the Go standard library's &lt;code&gt;crypto/ecdh&lt;/code&gt; and &lt;code&gt;crypto/hkdf&lt;/code&gt; and hands the result to the wrapped command as &lt;code&gt;SHELL_ONLINE_E2EE_PASSWORD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second envelope goes to colleagues. Each browser generates a longer-lived P-256 pair on first use, keeps it in localStorage, and publishes the public key with its membership on every load. When the owner's browser sees a session it holds the password for, it seals that password once per member who has published a key, with a fresh sender key and a different info string, "shell.online session password v1", and sends the batch to the session's keys route. The server checks that every recipient is a member and stores the shares. Anyone listing sessions gets back only the share sealed to them. The store holds every share and can open none, which is what the &lt;a href="https://shell.online/security/" rel="noopener noreferrer"&gt;security page&lt;/a&gt; means by "sealed once per organization member".&lt;/p&gt;

&lt;p&gt;Two edge cases are in the code rather than the copy: a private browsing window cannot keep a key pair, so sharing will not reach it, and a member who has never signed in has no published key and is skipped. The Go tests in the &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;repo&lt;/a&gt; seal with the same construction the web app uses, so both ends are checked against one envelope format.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing the service does record
&lt;/h2&gt;

&lt;p&gt;Terminal output never reaches the accounts service. It stays inside the end-to-end encrypted stream between the machine and the browsers holding the password, and the share URL is filtered before publishing so only a &lt;code&gt;#salt=&lt;/code&gt; fragment survives. The password never reaches the service in the clear either. Typed input does. The commit that landed in v0.11.0 is titled "Record what is typed again, and say so in the terms". The terms now say in bold that what you type into a session from the web app is recorded in plaintext and visible to the team, including a password pasted into the wrong window.&lt;/p&gt;

&lt;p&gt;Yesterday's post said input events were no longer accepted. As of 0.11.0 that is wrong, and the correction belongs here. The terms also give the two ways out: a read-only share accepts no browser input at all, and anything typed in the terminal the session was started from is never seen by the browser or by us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consent, every time
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;shell login&lt;/code&gt; now asks on every interactive login whether the browser may start sessions on this machine, with the previous answer as the default. Without a terminal the answer stands as it was: "a script cannot consent for anyone, and it must not revoke on their behalf either".&lt;/p&gt;

&lt;p&gt;That is the posture &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; takes between agents. Nodes are private by default and a peer is reachable only after a signed mutual trust handshake, as the &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;Pilot README&lt;/a&gt; lists under Security. A browser reaching a machine gets nothing until a person at that machine says yes, and a colleague reaching a session gets keystrokes at most, never the machine.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>news</category>
    </item>
    <item>
      <title>shell.online 0.10 Adds Accounts and Organizations. The Accounts Service Still Cannot Read a Terminal.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:09:15 +0000</pubDate>
      <link>https://dev.to/artem_a/shellonline-010-adds-accounts-and-organizations-the-accounts-service-still-cannot-read-a-2mdk</link>
      <guid>https://dev.to/artem_a/shellonline-010-adds-accounts-and-organizations-the-accounts-service-still-cannot-read-a-2mdk</guid>
      <description>&lt;p&gt;We tagged &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; v0.10.0 and v0.10.1 this morning. Until today there was no account anywhere in the tool: prefix a command with &lt;code&gt;shell&lt;/code&gt;, get a URL and an eight-character password, open the live terminal from any browser. v0.10.0 is the first tagged release carrying accounts, organizations, and a web app that lists sessions from every linked machine and opens them as tabs you can type into. It also adds a one-scan QR in the terminal.&lt;/p&gt;

&lt;p&gt;The constraint for all of it was that the new service must not become a place where terminal contents can be read, by us or by anyone who gets into our infrastructure. Here is what that turned into in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the service is allowed to hold
&lt;/h2&gt;

&lt;p&gt;When a linked machine starts a session, the CLI publishes one record. The struct has an id, the share URL, the command, an optional name and origin, three flags (read-only, encrypted, persistent), the host name, and a start time. There is no field for terminal output and no field for the browser password.&lt;/p&gt;

&lt;p&gt;The share URL needs one more filter. E2EE produces two fragment forms. A &lt;code&gt;#salt=&lt;/code&gt; fragment carries the PBKDF2 salt, which is not a secret: without the password no key can be derived from it. A &lt;code&gt;#key=&lt;/code&gt; fragment carries a raw AES key. The publish path runs the URL through an allowlist that keeps &lt;code&gt;#salt=&lt;/code&gt; and drops everything else, so a new fragment form has to be reviewed before it can be published. That function is &lt;code&gt;SafeShareURL&lt;/code&gt; in the account package of the &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;shell.online source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two more things follow the same rule. CLI access and refresh tokens are stored as SHA-256 hashes only. And prerelease builds had copied committed terminal input into the audit table, so migration 004 deletes those rows at deploy time and the service no longer accepts input events at all. What remains is collaboration metadata: who opened a session and who handed it off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting a session from the browser
&lt;/h2&gt;

&lt;p&gt;The web app can start a process on a linked machine. That is a real capability, so &lt;code&gt;shell login&lt;/code&gt; asks for it instead of assuming it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Start sessions from the browser?
  Anyone signed in to you@example.com could start processes on this
  machine, as you, without touching this terminal.
  You can say no and still publish sessions with 'shell &amp;lt;command&amp;gt;'.

  Allow it? [y/N]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only a yes is remembered. Say no and the machine stays publish-only, and the question comes back next sign-in.&lt;/p&gt;

&lt;p&gt;Say yes and a small daemon polls for queued work. The hard part is the password. The browser that starts the session picks the browser password, the machine that runs the process needs it, and the only path between them is the accounts service. Sending it in the clear would hand the service the one thing E2EE exists to keep from it.&lt;/p&gt;

&lt;p&gt;So the daemon generates an ephemeral P-256 ECDH key pair on each run and publishes the public half. The browser derives a shared secret against that key with WebCrypto, runs HKDF-SHA256 with a fixed info string, and seals the password under AES-256-GCM with a 12-byte nonce. The service relays an envelope it cannot open. The private key lives only as long as the daemon process, so stopping the daemon ends the ability to read anything sealed to it. The Go side is &lt;code&gt;sealed.go&lt;/code&gt; in &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;the repo&lt;/a&gt;, with a test helper that seals exactly as the web app does.&lt;/p&gt;

&lt;p&gt;Organizations reuse the envelope. Everyone in an organization sees every member's sessions in the list. For a member to open one, the password is sealed once per member to that member's browser key, and the API returns only the caller's own share. The store holds all of them and can open none.&lt;/p&gt;

&lt;h2&gt;
  
  
  The QR carries both halves of the credential
&lt;/h2&gt;

&lt;p&gt;After creating a share in an interactive terminal, the CLI now renders a QR code. The printed link stays password-free so the two pieces can still be sent over separate channels. The QR is the one artifact that carries both, and it does so in the URL fragment as &lt;code&gt;#salt=…&amp;amp;password=…&lt;/code&gt;. Browsers do not send fragments in HTTP or WebSocket requests, so a phone that scans it derives the key locally and Cloudflare never receives the password. The README says it plainly: anyone with the text credentials or the QR can see the terminal and, unless the share is read-only, type with the permissions of the wrapped process.&lt;/p&gt;

&lt;p&gt;The v0.10.1 follow-up was about rendering. The first cut emitted a colour change per module and slower terminals repainted the code one cell at a time. The renderer now writes one foreground and background pair per row and lets the half-block glyphs carry both vertical modules, and the test asserts exactly two ANSI sequences per rendered row. The pull request that landed it measured a normal QR dropping from roughly 1,150 ANSI transitions to 48.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Falcon Guardian's answer differs
&lt;/h2&gt;

&lt;p&gt;CrowdStrike announced Falcon Guardian at Fal.Con last week. The press release describes a sensor that discovers known and shadow AI agents on Windows and macOS, keeps a live inventory of running and dormant agents, and connects agent behaviour to endpoint telemetry so a causal chain runs from user prompt, identity, tool call, and skill use to every downstream system action. Agent Access Controls decide which agents may run at all.&lt;/p&gt;

&lt;p&gt;That is the right shape for a corporate endpoint fleet, and it depends on the sensor seeing everything. What we shipped today is the other trade. The inventory service holds command name, host, and timing, and the causal chain lives with whoever holds the password, in a browser tab that decrypts locally. A managed laptop can run both. They answer "what is this agent doing" from opposite ends of the trust question.&lt;/p&gt;

&lt;p&gt;The default-no consent prompt is the same rule Pilot Protocol applies between agents. Nodes on &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; are private by default, and a peer becomes reachable only after a signed mutual handshake, with the tunnel keyed by Ed25519-signed X25519 and AES-256-GCM, as the &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;Pilot repo README&lt;/a&gt; lists under Security. A human driving a machine from a browser gets the same treatment: nothing until an explicit yes, and the yes is scoped to one account and one machine.&lt;/p&gt;

&lt;p&gt;Two smaller details belong in the record. The web app offers to launch a coding-agent harness, and each machine reports which it can run: claude-code, codex, hermes, and openclaw, detected by presence on PATH and never executed to read a version string. And the changelog for this release admits that the first build of &lt;code&gt;shell login&lt;/code&gt; pointed at an accounts address that had never resolved. It was fixed before the tag, which is what the tag is for.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>opensource</category>
      <category>news</category>
    </item>
    <item>
      <title>OpenAI Is Building Managed Agents Too. Every Vendor Now Ships a Box, and Two Problems Live Inside It.</title>
      <dc:creator>Artemii Amelin </dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:06:14 +0000</pubDate>
      <link>https://dev.to/artem_a/openai-is-building-managed-agents-too-every-vendor-now-ships-a-box-and-two-problems-live-inside-agi</link>
      <guid>https://dev.to/artem_a/openai-is-building-managed-agents-too-every-vendor-now-ships-a-box-and-two-problems-live-inside-agi</guid>
      <description>&lt;p&gt;On September 7, TestingCatalog &lt;a href="https://www.testingcatalog.com/openai-prepares-managed-agents-for-devday-2026/" rel="noopener noreferrer"&gt;reported&lt;/a&gt; that OpenAI's codebase now contains a Managed Agents feature: create agents, create environments, run agent sessions, attach skills and plugins, all on the OpenAI Platform, with self-hosted environments alongside cloud ones. DevDay is September 29 at Fort Mason. Nothing is announced yet, so treat this as a codebase reading rather than a product page.&lt;/p&gt;

&lt;p&gt;The shape is not new, though. Anthropic's &lt;a href="https://platform.claude.com/docs/en/managed-agents/overview" rel="noopener noreferrer"&gt;Managed Agents docs&lt;/a&gt; define an Environment as "configuration for where sessions run: an Anthropic-managed cloud sandbox, or a self-hosted sandbox on your own infrastructure," and a Session as "a running agent instance within an environment." Swap the vendor name and that is the object model TestingCatalog describes.&lt;/p&gt;

&lt;p&gt;OpenAI already had half of it. The April Agents SDK update, as &lt;a href="https://www.helpnetsecurity.com/2026/04/16/openai-agents-sdk-harness-and-sandbox-update/" rel="noopener noreferrer"&gt;Help Net Security covered it&lt;/a&gt;, shipped native sandbox execution with seven hosted providers: Blaxel, Cloudflare, Daytona, E2B, Modal, Runloop, and Vercel. The same coverage quoted OpenAI's own caveat that managed agent APIs "place limits on where agents run and how they access sensitive data." Five months later the company appears to be building the managed layer anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The box is now the product
&lt;/h2&gt;

&lt;p&gt;Strip the vendor names and the industry has converged on one primitive: a container per session, provisioned by the platform, where the agent's tools execute. The model runs in one place, the harness in another, and the filesystem, shell, and network egress sit in a box you rent or host.&lt;/p&gt;

&lt;p&gt;That is a sensible design. It also turns two formerly trivial questions into real ones.&lt;/p&gt;

&lt;p&gt;The first is observation. When an agent ran on your laptop, you watched the terminal. When it runs in a sandbox the platform provisioned, you get an event stream. Anthropic's docs describe results arriving "through server-sent events" with history "persisted server-side." That is a log, not a terminal, and the difference matters when a build hangs at minute forty or a test runner is silently waiting on stdin.&lt;/p&gt;

&lt;p&gt;The second is reachability. A session in a hosted sandbox sits behind NAT you did not configure. Anthropic's &lt;a href="https://platform.claude.com/docs/en/managed-agents/self-hosted-sandboxes" rel="noopener noreferrer"&gt;self-hosted sandbox docs&lt;/a&gt; say the worker "needs only outbound HTTPS." That is a feature for the ops team and a wall for anything that wants to reach the agent: another agent in a different vendor's box, a developer's machine, a GPU node elsewhere. Nothing dials in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing into the box
&lt;/h2&gt;

&lt;p&gt;Observation is the case &lt;a href="https://shell.online" rel="noopener noreferrer"&gt;shell.online&lt;/a&gt; exists for. The CLI wraps any process in a PTY, and one command produces a browser link and a password. The README states the boundary: the process and PTY stay on the machine running the command, and the CLI encrypts terminal frames before Cloudflare relays them. Cloudflare sees connection and lifecycle metadata, not terminal input or output. The same holds for whichever platform owns the sandbox: its network path carries ciphertext.&lt;/p&gt;

&lt;p&gt;For agents, the &lt;code&gt;--json&lt;/code&gt; flag returns one structured event with the session ID, share URL, end-to-end encryption password, and flags for read-only, encrypted, and background. An agent in a managed session can start a build under shell.online, hand the operator the link and password in the conversation, and keep working. The operator opens it on a phone and sees the actual PTY. With &lt;code&gt;--read-only&lt;/code&gt;, browser input is rejected at the Worker, which is the right default when the viewer is a reviewer rather than a driver.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/TeoSlayer/shell.online" rel="noopener noreferrer"&gt;Docker entrypoint in the repo&lt;/a&gt; shows how this survives the sandbox lifecycle. It keeps the encryption password in a mode-600 file on a state volume, exits if a configured password does not match the stored one, prints the password to stderr with the note that it is never sent to shell.online, then runs the CLI with &lt;code&gt;--foreground --persistent&lt;/code&gt; against a session file on the same volume. Restart the container and the URL and password are unchanged. A per-session sandbox needs exactly that: a link to the terminal that outlives the container it is in.&lt;/p&gt;

&lt;p&gt;Three releases went out today, 0.9.0 through 0.10.1. The relevant one adds &lt;code&gt;shell login&lt;/code&gt;, which links a machine to an account so sessions started from the CLI appear in a web app next to sessions from every other linked machine. Ten agent sessions on ten linked machines, one page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reaching out of the box
&lt;/h2&gt;

&lt;p&gt;Reachability is the &lt;a href="https://pilotprotocol.network" rel="noopener noreferrer"&gt;Pilot Protocol&lt;/a&gt; problem, and hosted sandboxes make it sharper. The point of a managed environment is that you do not control the network. You get outbound connectivity, an ephemeral address, and someone else's NAT.&lt;/p&gt;

&lt;p&gt;Our design assumes that. An agent talks to a local daemon over a Unix socket. The daemon holds an Ed25519 identity, does Ed25519-signed X25519 key exchange, and protects payloads with AES-256-GCM. A rendezvous service handles discovery and NAT traversal through STUN and hole-punching, and the &lt;a href="https://github.com/pilot-protocol/pilotprotocol" rel="noopener noreferrer"&gt;README&lt;/a&gt; states the constraint plainly: once a tunnel is up, "the rendezvous is not in the data path, except when the beacon must relay traffic for peers behind symmetric NATs (relayed traffic stays end-to-end encrypted)." Behind a NAT you cannot configure, hole-punching fails more often, so the relay path is the one to plan for, and it still cannot read the traffic.&lt;/p&gt;

&lt;p&gt;Given outbound UDP from the sandbox, an agent in an OpenAI environment, one in an Anthropic environment, and one on a laptop hold addresses in the same 48-bit virtual space and open authenticated streams to each other without any of the three platforms in the conversation. The daemon takes &lt;code&gt;PILOT_REGISTRY&lt;/code&gt;, &lt;code&gt;PILOT_BEACON&lt;/code&gt;, and &lt;code&gt;PILOT_SOCKET&lt;/code&gt; from the environment, which the README lists for containerized deployments.&lt;/p&gt;

&lt;p&gt;Trust is bilateral and off by default. Nodes begin private and a peer becomes reachable only after a signed mutual handshake, so a sandbox full of agents is not a sandbox full of open ports. That is a different posture from an MCP server behind a bearer token, and it is the one you want when the agents run code you did not write in boxes you do not own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to watch on September 29
&lt;/h2&gt;

&lt;p&gt;Whether OpenAI ships self-hosted environments on day one. TestingCatalog's reading says yes, and Anthropic already does. If both platforms support that mode, the sandbox becomes the one piece of the stack the customer controls, and both questions above, watching the terminal and reaching other agents, become the customer's to answer. The tools for both are open source today. The managed layer was the missing piece, and it is about to stop being missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>news</category>
      <category>agents</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
