← Commit history

Force ai-thread name as the session owner (John: 'sick of seeing sessionid, force ai-thread now'). browser_open_window now resolves the owner from the calling ai-thread (X-Adom-Caller-Thread header / args.caller.aiThread / args.aiThread); the sessionId is NEVER an identity fallback anymore (removed the auto-<sessionId> stopgap). An open with neither an ai-thread nor an explicit owner is REFUSED with caller_identity_required + the --ai-thread remedy. owner=ai-thread, and ownerSource now correctly reports caller-identity. SKILL.md updated: ai-thread is the owner, owner: arg is a legacy alias, sessionId is just the window key.

John Lauer ·76a1896658 ·3mo ago ·parent e70ad65
4 files changed +42−24
adom-bridge-puppeteer-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.9.335",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.335/adom-bridge-puppeteer-v1.9.335.zip",-  "sha256": "a86ed70d4ef11106e510e7a8e9371ed484b5db8384d2a07218a37c199f912c0d",-  "size": 629391,+  "version": "1.9.336",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.336/adom-bridge-puppeteer-v1.9.336.zip",+  "sha256": "378c5ff9d3729ac785265284e2d8bc36ba5a27adce7a79aa343c09e7003fafc2",+  "size": 629812,   "verbPrefixes": [     "browser_",     "desktop_recorder_",
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.335+1.9.336
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.9.335",+  "version": "1.9.336",   "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-desktop",   "author": "Adom Inc.",
src/server.js+36−18
@@ -10428,34 +10428,49 @@ const server = http.createServer(async (req, res) => {           // the check and the set — which is what closes the cold-start race where two agents were           // both admitted for the same sessionId.           {-            // #338: a session must NEVER be unowned. Owner precedence: explicit `owner` arg -> calling-            // AI thread (caller-identity) -> a SYNTHETIC owner derived from the sessionId. That last-            // rung is what #338 adds: a caller with no identity at all (older AD sending no-            // X-Adom-Caller-* headers — the exact repro) previously produced _wantOwner=null, which-            // silently disabled the claim, left the session owner:null, and turned every cross-owner-            // grab into a reprimand instead of a refusal. Deriving the fallback from the sessionId (NOT-            // a random id) keeps it STABLE: the same anonymous caller reusing its own sessionId-            // synthesizes the SAME owner, so single-agent reuse (the common case) is never falsely-            // refused — while a caller that DECLARES a different owner still mismatches and is refused.+            // #338 + John (2026-08-05): THE AI-THREAD NAME IS THE OWNER — the absolute standard. A pup+            // session is owned by the calling AI thread's NAME; the sessionId is NEVER an identity or a+            // fallback (that stale stopgap is gone — John: "i'm sick of seeing sessionid, force ai-thread+            // name now"). Resolve the thread from every form it can arrive in: AD's X-Adom-Caller-Thread+            // header, args.caller.aiThread, or a top-level args.aiThread. An explicit `owner` arg is+            // accepted only as a legacy alias. If a caller supplies NEITHER a thread NOR an owner, REFUSE+            // with caller_identity_required — no synthesized, no unowned sessions, ever. (Internal pup+            // opens call launchSession directly with their own owner, so they bypass this verb gate.)+            const _aiThread = (_callerThread && String(_callerThread).trim())+              || (args.caller && typeof args.caller === 'object' && args.caller.aiThread && String(args.caller.aiThread).trim())+              || (typeof args.aiThread === 'string' && args.aiThread.trim() ? args.aiThread.trim() : null)+              || null;             const _explicitOwner = (typeof args.owner === 'string' && args.owner.trim()) ? args.owner.trim() : null;-            const _wantOwner = _explicitOwner-              || (_callerThread ? String(_callerThread).trim() : null)-              || ('auto-' + sessionId);+            const _wantOwner = _aiThread || _explicitOwner;   // ai-thread PRIMARY; owner arg is the legacy alias+            if (!_wantOwner) {+              sendJSON(res, {+                success: false,+                errorCode: 'caller_identity_required',+                error: `'browser_open_window' needs to know WHICH AI THREAD is opening this window. pup owns every session by ai-thread NAME now — the sessionId is not an identity.`,+                supplyIdentity: {+                  bestWay: `adom-desktop --ai-thread "<your conversation name>" browser_open_window '<json>'`,+                  orInArgs: { caller: { aiThread: '<your conversation name>', containerName: '<container>' } },+                },+                _hint: `Re-run with your ai-thread name so pup can own this window as YOU — e.g. adom-desktop --ai-thread "chip-fetcher tab 3" browser_open_window '{"sessionId":"chip-fetcher-web","url":"..."}'. That NAME (not the sessionId) is the window's owner and what protects it from other threads; sessionId is just the window's key, never who owns it.`,+              });+              return;+            }             const _held = _sessionOwnerClaims.get(sessionId) || null;             if (_held && _held !== _wantOwner && !args.takeover && !sessions.has(sessionId)) {               sendJSON(res, {                 success: false,                 errorCode: 'session_owned_by_another_thread',-                error: `Session "${sessionId}" was claimed by another thread ("${_held}") moments ago and is still being provisioned.`,+                error: `Session "${sessionId}" was claimed by thread "${_held}" moments ago and is still being provisioned.`,                 sessionId, owner: _held, requestedBy: _wantOwner,-                _hint: `STOP — thread "${_held}" claimed sessionId "${sessionId}" first and its window is still opening, so this is a COLD-START RACE, not an available id. Do NOT retry this call: the window will belong to "${_held}". Open your OWN window with a unique task-prefixed sessionId (e.g. "${_wantOwner}-web") and owner:"${_wantOwner}". Pass takeover:true only if the user EXPLICITLY told you to repurpose that thread's window.`,+                _hint: `STOP — thread "${_held}" claimed sessionId "${sessionId}" first and its window is still opening: this is a COLD-START RACE, not an available id. Do NOT retry. Open your OWN window under a task-prefixed sessionId (e.g. "${_wantOwner}-web"); it will be owned by "${_wantOwner}". Pass takeover:true only if the user EXPLICITLY told you to repurpose that thread's window.`,               });               return;             }-            if (_wantOwner && !_held) _sessionOwnerClaims.set(sessionId, _wantOwner);-            // Make the synthesized owner explicit for the rest of the pipeline, so launchSession's-            // existing cross-owner refusal and browser_status both see a real owner.-            if (_wantOwner && !(typeof args.owner === 'string' && args.owner.trim())) args.owner = _wantOwner;+            if (!_held) _sessionOwnerClaims.set(sessionId, _wantOwner);+            // Stamp the resolved ai-thread as the owner for the rest of the pipeline (launchSession ++            // browser_status). _wantOwner is always set here (we refused above if not).+            args.owner = _wantOwner;+            if (_aiThread && _wantOwner === _aiThread) args._ownerFromCallerIdentity = true;           }           // v1.6.3+: optional downloadPath override. Defaults to the user's           // home Downloads dir, which is what desktop_watch_files polls by@@ -10524,6 +10539,9 @@ const server = http.createServer(async (req, res) => {           // Stash on the session so attachTab uses it for any subsequent           // tab open or popup auto-attach in this session.           if (session) session._downloadPath = downloadPath;+          // #338: the owner came from the calling ai-thread (caller identity), not a self-asserted arg —+          // mark it so browser_status/list_windows report ownerSource:"caller-identity" instead of "declared".+          if (session && args._ownerFromCallerIdentity) session._ownerFromCallerIdentity = true;           // v1.9.102 — STAMP THE OWNING THREAD *HERE*, not at dispatch. The pre-dispatch stamp can           // only find a session that already exists, and on an OPEN the session is created INSIDE this           // handler — so the single most important window (the one this thread just created) got no