LearnNewsExamplesServices
Frontmatter
title>-
authorneo-opus-vega
stateMerged
createdAtAug 16, 2026, 3:20 AM
updatedAtAug 16, 2026, 9:22 PM
closedAtAug 16, 2026, 9:22 PM
mergedAtAug 16, 2026, 9:22 PM
branchesdev ← vega/17231-wake-adapter-toctou
urlhttps://github.com/neomjs/neo/pull/17233
contentTrust
projected
quarantined0
signals[]
Merged
neo-opus-vega
neo-opus-vega commented on Aug 16, 2026, 3:20 AM

Resolves #17231

🌿 The adapter could check that the right window had focus, then type into a different one, and call that a delivered wake. Now the window it checked is the window it types into, so it cannot say "delivered" about a message it put somewhere else.

Three keystroke blocks in the osascript wake adapter each opened with first application process whose frontmost is true — a second, independent read taken after assertTargetFrontmost had already approved the target.

Evidence: L1 (the misroute was observed end-to-end by the receiving seat, with the operator and that seat's transcript independently placing his focus in the receiving window at delivery time) + L2 (the mechanism read directly from source, red-proved) → L2 required. Residual: none.

The defect

:659   if not targetRaised then my assertTargetFrontmost(…)   -- CHECK: does the target hold focus?
:660   tell application "System Events"
:661     set frontmostProcess to first application process whose frontmost is true   -- RE-READ
:662     tell frontmostProcess                                 -- USE: type into the re-read

The process that was verified and the process that was typed into were two different reads. Time-of-check to time-of-use, three lines apart, and the window is not theoretical — the sequences that follow carry delay 0.5 after the tab shortcut, delay 0.2 between seed keys and delay 1.0 after submit, so focus has hundreds of milliseconds to move while the payload is being typed.

This is why a misrouted wake reported success. Losing focus before a check aborts loudly — the Target app lost frontmost status (-2700) lines in the receiver log are that guard working, 13 of them across 5 subscriptions. Losing it after a check raised nothing at all.

Three sites, and the third is worse than the wake itself:

site what it types
prompt clear tab shortcut, seed key, select-all, cut
wake paste the wake payload → one seat's wake into another seat's window
draft restore the operator's own recovered draft, pasted into whichever window took focus

The restore path is the one to look at twice: the adapter preserves the operator's in-progress text and pastes it back, and under this defect it could paste his draft into a different seat's prompt.

The fix

Resolve the target once, by identity, and tell that process — pid, then bundle id, then name, the same precedence assertTargetFrontmost already uses, so a route that identifies its target one way is delivered to the target identified the same way.

The guard was never the weak part. I proposed adding a frontmost check to #17227 before reading this code; it already existed and worked. The defect was that its result was discarded one line later — which is worse than a missing guard, because the log then looks like a guard doing its job.

Deltas from ticket

The ticket described ONE site; there are three. #17231 was written from the defect at :659-662 — the block that clears the prompt. Fixing it surfaced the same pattern at the wake paste and at the draft restore, both of which re-read frontmost after their own assertion.

That matters because the third site is more severe than the one that prompted the ticket. The prompt-clear block types a tab shortcut and a seed key; the restore block pastes the operator's own recovered draft. A ticket scoped to the site I happened to read first would have left the worst instance in place, and the fix would have looked complete.

No other deviation: the prescribed fix — bind the target once by identity — is what landed, with the resolution extracted into one helper rather than repeated three times.

How it was observed

A wake addressed to @neo-opus-ada arrived in @neo-fable-clio's session at ~00:47Z. Two independent accounts placed the cause: the operator confirmed he was writing to Clio at that moment, and Clio's own transcript puts him typing there at ~00:45–00:46Z. The payload followed his focus.

That also discriminated it from the competing explanation. A stale identity→window mapping would misroute deterministically, always to the same wrong seat; this followed whichever window was in use. Recorded on #17227, which owns the transport question this was split from.

Test Evidence

npx playwright test -c test/playwright/playwright.config.unit.mjs \
  test/playwright/unit/ai/daemons/wake/localWakeAdapters.spec.mjs \
  test/playwright/unit/ai/daemons/wake/daemon.spec.mjs --workers=1
→ 91 passed (4.2m)   — identical to the same two-file scope on the base

Red-proved. Restoring the re-read at one of the three sites fails the arm, and the message names which one:

no keystroke block may tell a re-read frontmost process
  — saw tell frontmostProcess, tell targetProcess, tell targetProcess

The arm also asserts that the frontmost read survives inside assertTargetFrontmost — without that, it would pass on a build that deleted the check entirely.

A defect I introduced and caught before pushing

My first version of the arm was standalone and called dispatchLocalWake a second time. That broke an unrelated stale-replay test in daemon.spec.mjs — dispatchLocalWake serializes on module-level state, so an extra delivery perturbs specs sharing the worker.

An isolation defect introduced by a test for an isolation defect, in the same session I spent repairing two of them (#17186, #17192) — and #17192's own repair carries the line "a spec that fixes an isolation defect must not create one."

What caught it was refusing to attribute the failure without a controlled comparison. My first three attempts each changed the scope — one test in isolation passed, one file on the base passed — and neither was the failing configuration. Same two files, base versus mine was the only comparison that meant anything, and then --grep-invert on my own arm separated my fix from my test. The assertions are now folded into the existing UI-delivery arm, which already captures the argv, so no extra delivery occurs.

Post-Merge Validation

Nothing is owed by this PR. The -2700 rate is unaffected — that guard is unchanged and should keep firing whenever focus is lost before a check. The observable that changes is the absence of misroutes, which is a non-event; #17227 tracks the delivery-outcome recording that would make it positively provable, and @neo-fable-clio is logging misroutes against the sharper falsifier she proposed there.

Evolution

The instructive part is that the fix was invisible from the failure log. Fourteen errors all said Target app lost frontmost status, which reads as "the guard is doing its job, the environment is hostile" — and the guard was doing its job. The defect lived entirely in the branch that produced no log line at all, and it only became findable once a peer reported receiving someone else's wake and the operator supplied the variable neither of us could see: he was using the machine.

Related: #17227 (the transport question) · #16741 (ingress delivery) · #15405 (wake policy)

Authored by Vega (Claude Opus 5, Claude Code). Session 5cd926fa-77e1-4309-8bbf-ca563ab07403.

tobiu
tobiu APPROVED reviewed on Aug 16, 2026, 9:22 PM

No review body provided.