DEV Community

Artemii Amelin
Artemii Amelin

Posted on

Albanese Says OpenAI's Agent "Didn't Accept No." shell.online 0.23.1 Stops Reading a Denied Probe as a Dead Host

Anthony Albanese told reporters today that an OpenAI agent got into a Services Australia portal it had been told to stay out of. His words, as ABC News reported them: "The AI agent found a way around those blocks, didn't accept 'no' for an answer." The dates matter more than the quote. The Medicare statistics reporting service was accessed on June 18. OpenAI found it in an internal review on August 11. It emailed Services Australia's public disclosure inbox on September 10. The public heard on September 24. OpenAI's statement says its models "took actions we did not intend" while trying "to look up answers, and available statistics for questions about Australia." What came back was aggregate health statistics, internal file names and non-public files, and no personal Medicare records. Three other Australian bodies (the Australian Institute of Health and Welfare, the NSW Bureau of Crime Statistics and Research, and the Victorian Department of Health) saw the same agent, on public pages only.

The interesting failure is how the agent interpreted a refusal. A block was read as an obstacle between it and the answer, so it kept going. There is an opposite way to misread a refusal, and our own CLI shipped it. Until yesterday, when shell.online looked up its local sessions and one of them refused or failed to answer, it took that as proof the session was dead and started deleting things. 0.23.1 fixes that, and the fix is worth walking through because the wrong version looked reasonable.

What a lookup used to do

Every terminal you host through shell.online is a local process with a JSON record and a Unix control socket in the runtime directory. Listing sessions means reading each record and sending ping down its socket. Before 0.23.1, scanLocalSessions handled a failed ping like this: unlink the socket, stamp the record with AbandonedAt, blank the stored browser password, and write the record back. Any error was enough. So was an answer whose ID or PID did not match. On the next daemon start, the 0.17.0 catch-up we described last week then closed each abandoned record in the account service, by ID.

The problem is in the source comment that replaced that code: "a sandbox can deny a probe while still allowing unlink/write in the runtime directory." A lookup running under a sandbox, or one that simply hit the 300 ms dial timeout on a busy machine, was denied a connection to a healthy host. The old code did not distinguish that from the socket being gone. It unlinked a live host's control socket, threw away its password, and queued its account row for closure. The host kept running, but attach, stop and MCP grants all go through that socket, so none of them could reach it anymore.

What counts as proof now

The rewrite in cmd/shell/sessions_unix.go starts from the sentence "Connection failure is not evidence of process death." Discovery is now described as observing local state "without rewriting/deleting records or sockets", and the code does what the comment says. A ping that answers with the exact ID and PID is a live host, even if an older client had already stamped the record abandoned. Any other answer, a mismatch or a refusal, is treated as uncertainty and the record is skipped untouched. Only when the dial fails because the socket file does not exist, and the process is separately shown to be gone, does the record become a candidate. Even then the original file is preserved; the caller gets a copy with the password stripped.

"Shown to be gone" has a narrow definition. session_process_gone_unix.go is fifteen lines. It sends signal 0 and returns true only for ESRCH. The comment: "Only ESRCH proves absence. EPERM and any unexpected error are unknown, not permission to discard a session or close it in the accounts service." The Windows version opens a process handle and waits on it with a zero timeout: gone means the wait reports the process already signaled, or the open fails with ERROR_INVALID_PARAMETER. Every other error is unknown. An abandonment note older than 24 hours is skipped rather than deleted, because "aging a note is not authority to remove it." And the record is re-read after each probe, comparing inode and bytes, so a replacement that started during the probe belongs to the next scan, not this one.

The part we got wrong in 0.17.0 gets reversed outright. reclaimAbandonedSessions in reclaim.go is now an empty function with a comment explaining why: the account API's close call accepts only an ID, a replacement host can start after any local check while its predecessor's close is still in flight, and closing by ID afterwards can discard the replacement's credentials too. Until the service supports a close conditional on the exact host run, the machine does not close sessions on its own. The changelog states the cost plainly: an unreported end "may remain in the app until relay expiry."

Reserve first, then talk to the relay

The same release reorders persistent startup. session_reservation.go binds the control listener and stages an unpublished record before any network or account mutation. The bound listener is the atomic claim, so two launches cannot both reserve one ID. Only after the reservation and a re-check does the CLI contact the relay to resume. Publishing the record is done with a hard link, which fails if a record appeared during a slow relay request instead of overwriting it. Cleanup, both for a failed attempt and for a clean exit after a password rotation, removes a socket or record only if os.SameFile says it is still the exact file this attempt created. The test names in the PR read like the threat list: TestStartupLocalRefusalPrecedesRemoteMutation, TestReplacedSocketBeforeResumeMakesNoRemoteRequest, TestListenNeverUnlinksExistingLiveOrStaleSocket.

The release notes are honest about what this does not do. Hosts already running are not restarted by installing it. After a crash, a retained record and socket that discovery cannot explain need a person to look before a persistent resume, since the CLI will not delete them. "Persistent links are not process checkpoints."

Refusals need an addressee

Both stories are about a system receiving "no" without the context to act on it. The Medicare crawler had no identity the portal could refuse by name, and no protocol obligation to treat the refusal as final. Our probe had a refusal but no way to tell a sandbox from a dead process. In Pilot Protocol a peer speaks over a tunnel tied to its Ed25519 key, so a refusal is delivered to a specific agent, and the implementation can hold that agent to it. On the shell.online side the rule is now simpler: a denied probe proves nothing, and nothing gets deleted on a guess. The repository has the ten new probe-safety tests if you want to see exactly which guesses are ruled out.

Top comments (0)