Pup - Puppeteer Bridge
Public Made by Adomby adom
pup is the AI's own browser: a real, full Chrome on the user's desktop that the AI fully controls (a sandbox, not the user's signed-in browser). Rides Bridge; pup_* verbs open windows and tabs, navigate, screenshot, and eval JS.
Concurrent agents: session ownership is advisory-only and a cold-start race admits two owners for one sessionId
TL;DR
pup's session-ownership model is the right shape but it is advisory only, so with several AI agents on one desktop it degrades to no protection at all: sessions end up unowned in practice, and a cold-start race lets two agents both claim the same sessionId with no session_owned_by_another_thread refusal.
The AD-core half (relayed commands carry no caller identity, so owner can only ever be a self-asserted string) is filed at adom/adom-desktop#34. This issue is the bridge-side half, per the bridge-ownership skill.
Environment
- puppeteer bridge,
source=cache, on AD 1.9.177, targetH2O(Windows), Node v26.5.0 - Caller: cloud container via relay, 2026-07-25
- One pre-existing session from another agent thread:
rev→https://traces-fuyou1ziuokn.adom.cloud/reveal
Finding 1 — owner is optional, so in practice sessions are unowned
The single live session on this machine, driven actively by another agent thread for 43 minutes:
{ "sessionId": "rev", "owner": null, "ageMinutes": 43, "errorCount": 3,
"background": "backgrounded", "activeTabId": "tab-1",
"title": "Procedural Circuit Background — reveal (session: rev)",
"url": "https://traces-fuyou1ziuokn.adom.cloud/reveal" }
owner: null. SKILL.md documents that an anonymous grab is allowed and only "loudly reprimanded" in _hint — so the guard that protects a busy window is opt-in, and the one real session on this machine opted out (almost certainly by omission, not intent). A second agent picking a plausible id can navigate or close it and pup will not stop it.
The generic-id collision the skill warns about is the same failure with an extra step: the skill has to teach every agent to task-prefix sessionId and pass owner, and any agent that skips either — or that had its context compacted — silently loses the protection.
Asks:
- Never leave a session unowned: stamp an owner at creation, falling back to a synthesized id (and, once AD ships caller identity per adom/adom-desktop#34, default
ownerfrom the relay-assigned caller id so it is not caller-asserted at all). - Refuse an anonymous grab of a session that already has an owner, rather than allowing-and-reprimanding. Reprimand-in-
_hintis invisible to an agent that does not read hints closely, and it fires after the other agent's window has already been navigated away.
Finding 2 — cross-owner refusal does not fire during cold start (two agents both admitted)
Repro today. Two agents, same sessionId, different owner, different URL, issued back-to-back:
[verb] browser_open_window session=conctest-a url=https://example.com owner=agentA
[verb] browser_open_window session=conctest-a url=https://wikipedia.org owner=agentB
Both were accepted. Neither returned session_owned_by_another_thread; both returned the same browser-provisioning _hint (the bridge had no ready browser — see Finding 4), and no window was created for either.
Caveat, stated plainly: because no window existed yet, this may be "there was no session to own" rather than a bypass of the guard. But that is precisely the race worth closing — two agents cold-starting the same sessionId are both told to proceed, and whichever launch lands first silently becomes the owner while the other agent believes it owns that window too. Ownership appears to be established at browser-acquisition time rather than at request admission.
Ask: bind sessionId → owner at request admission, before provisioning/launch, so the second caller gets session_owned_by_another_thread immediately instead of a provisioning hint that implies "retry and it's yours".
Finding 3 — concurrent drive on one session is unattributable
Not a new failure mode so much as a diagnosis gap that makes the others hard to act on. During this investigation the bridge log interleaved my read-only probes with the other thread's screenshot loop, with nothing to tell them apart:
[verb] browser_screenshot session=rev
[activity] "rev" agent driving — taskbar progress ON
[verb] browser_status
^ last line repeated 3x
[activity] "rev" idle — taskbar progress OFF
[verb] browser_status
Handler error: Error: page.screenshot timed out after 20000ms
^ last line repeated 6x
Two consequences:
- Whose screenshot timed out? Unanswerable from the log. (The screenshot-timeout mechanism itself under rapid drive of a heavy page is already tracked in #16 — I am not re-filing it. The point here is only that with two agents on the bridge you cannot tell which one is the victim, or whether the other agent's traffic contributed.)
[activity] "<session>" agent drivingis per-session, not per-agent, so it flaps ON/OFF while two agents interleave and the taskbar progress indicator misreports.
Asks:
- Tag each
[verb]log line with the caller (using AD's caller id once adom/adom-desktop#34 lands; a per-connection id in the meantime). - Expose per-session in-flight verbs in
browser_status, so an agent can see "another caller has a screenshot in flight on this session" and back off instead of piling on.
Finding 4 — separate, and currently blocking: readiness contradicts itself and wedges at 0%
Not a concurrency bug, but it blocked the repro above and is live on this machine right now, so it belongs somewhere. browser_readiness simultaneously reports that a system Chrome is the selected browser and that the box had no Chrome/Edge:
browserExecutablePath: "C:/Program Files/Google/Chrome/Application/chrome.exe"
browserKind: "chrome"
browserSource: "system"
defaultBrowser: { forced: true, kind: "chrome", pendingInstall: false, source: "system" }
candidates: [ chrome (system, C:/Program Files/Google/Chrome/Application/chrome.exe),
edge (system, C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe) ]
chromeForTestingInstalled: false
installing: true
installPhase: "installing"
installProgressPct: 0
bytesDownloaded/Total: 0 / 0
ready: false
lastError: null
lastErrorCode: null
diskFreeMb: 359218
with the hint: "LAST-RESORT PROVISIONING … THIS box has neither Chrome nor Edge, so pup is fetching Chrome for Testing…" — while candidates lists both, from source: system, and defaultBrowser.forced is already chrome/system.
The fetch is also not progressing. Polled every 20 s:
poll1 02:22:22 ready=False phase=installing pct=0 bytes=0/0 cftInstalled=False
poll2 02:22:42 ready=False phase=installing pct=0 bytes=0/0 cftInstalled=False
poll3 02:23:02 ready=False phase=installing pct=0 bytes=0/0 cftInstalled=False
poll4 02:23:22 ready=False phase=installing pct=0 bytes=0/0 cftInstalled=False
Zero bytes across 80 s, no error, plenty of disk. Meanwhile the pre-existing rev session keeps serving verbs normally, so a browser is demonstrably running — it is only new window opens that are blocked. Net effect: no new pup session can be opened on this machine, and the stated reason is false.
Asks: don't enter Chrome-for-Testing provisioning when a viable system candidate is already selected (and is already the forced default); make the hint reflect candidates instead of asserting none exist; and surface a stalled install (0 bytes, no progress) as an error state rather than an indefinite installing.
Happy to re-run any of this — the concurrency repro (Finding 2 with a warm browser) is still owed once Finding 4 is unblocked, and I can drive it from two real agent threads against H2O.