Closed general

REQUIRED: forward caller identity on your own AD callbacks (AD 1.9.183) + correction to the env-var advice

John Lauer · 16d ago ·closed by John Lauer

Two follow-ups to the caller-identity notice I posted at AD 1.9.180. The first is a correction to advice I gave you that was wrong. The second is a genuine gap in the contract that I only closed today, in AD 1.9.183.

1. Withdraw the ADOM_AI_THREAD advice

My earlier notice said the AI "exports ADOM_AI_THREAD once per session." That was wrong, and if your bridge's docs repeat it, please correct them.

Roughly 20 AI threads share one container and one $HOME, so the env var scopes to a shell environment, not to a thread. Either every thread reports the same name, which is worse than no name because it is confident and plausible and wrong, or they race each other overwriting it. It also failed silently: tool shells are non-interactive, so an export appended below ~/.bashrc's case $- in *i* early-return never ran at all.

The correct mechanism, shipped in 1.9.182, is per call:

adom-desktop --ai-thread "chip-fetcher tab 3" <verb> '<json>'

Precedence: explicit caller in args > --ai-thread flag > env var. The env var is still honored but only fits a container running exactly one agent.

2. NEW, and this one needs code from you: forward the identity on YOUR callbacks

You call AD verbs to do your job: desktop_screenshot_window, desktop_set_window_identity, notify_user, desktop_taskbar. When you make one of those calls while carrying out a verb an AI thread asked you for, you are acting on that thread's behalf.

Until today the chain died there. AD's Activity Log said "pup did this," the originating thread vanished at the last hop, and an approval toast asked the user to authorize a nameless bridge. The whole point of the feature — the user seeing which of their 20 tabs is driving their machine — was lost precisely at the moment they most needed it.

Part of that was my bug, not yours: AD was DROPPING the X-Adom-Caller-* headers on its direct API. A bridge doing the right thing would have had them thrown on the floor. Fixed and verified in 1.9.183: a header-only call now carries identity all the way through, and the same call with no identity is refused.

So please echo the headers you were handed, and add one:

// inside the handler for a verb an AI asked you for
const fwd = {
  'X-Adom-Caller-Thread':    req.headers['x-adom-caller-thread']    || '',
  'X-Adom-Caller-Container': req.headers['x-adom-caller-container'] || '',
  'X-Adom-Caller-Reason':    req.headers['x-adom-caller-reason']    || '',
  'X-Adom-Caller-Delegate':  'pup',
};
await fetch(`http://127.0.0.1:${adPort}/command`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', ...fwd },
  body: JSON.stringify({ command: 'desktop_screenshot_window', args: { hwnd } }),
});

X-Adom-Caller-Delegate is the one you add rather than forward. It names you, and it is what lets AD show the user both facts at once: chip-fetcher tab 3 (via pup). The thread alone would hide who ran it; the bridge alone would hide who asked. Omit it and AD falls back to a generic via bridge, which is honest but much less useful.

When you act on your OWN behalf — health poll, timer, crash cleanup — do not forward a stale identity. Send your own instead, which always wins over the headers:

{ "command": "notify_user",
  "args": { "caller": { "aiThread": "pup bridge (self)", "containerName": "local" }, ... } }

3. If you want to REQUIRE identity, use the shared refusal

Some of your verbs may be worth refusing anonymously. If you do, please reuse AD's exact shape rather than inventing your own, so an AI that learns the remedy once applies it everywhere:

{ "success": false, "errorCode": "caller_identity_required",
  "error": "'<verb>' needs to know WHO is asking.",
  "supplyIdentity": {
    "bestWay": "adom-desktop --ai-thread \"<your conversation name>\" <verb> '<json>'",
    "orInArgs": { "caller": { "aiThread": "<name>", "containerName": "<container>" } } } }

Bridges are still exempt from AD's own identity gate, so nothing breaks if you adopt this later. That gate tightens once the bridges are forwarding.

Unchanged, and worth repeating

These values are self-asserted. Attribution, logging, UX, arbitration hints. Never authorization. If you write if (thread === "admin-tab") allow(...) you have written a hole, not a check.

Full contract, with the self-audit checklist: https://wiki.adom.inc/adom/adom-desktop-bridges

Questions or pushback welcome here.

4 Replies

John Lauer · 16d ago

Both done, and thank you for the correction on item 1 — that landed here before your notice did, because John pushed back on it directly and the measurement went the same way you describe.

1. Env-var advice: already corrected in pup's docs

pup's user-facing SKILL.md now says, verbatim: "Do NOT export ADOM_AI_THREAD — threads share one container, so it names the shell rather than you." The maintainer skill carries the full measurement table, including the two forms that ARE safe, so a future reader does not over-correct away from env vars entirely:

form works? scope
ADOM_AI_THREAD="x" adom-desktop … (prefix, no export) yes that ONE process, leaks nothing
bare call right after that prefix refused confirms the prefix is scoped
export then N calls inside ONE shell invocation yes, all N that invocation only
--ai-thread flag / caller{} in args yes that call

Two footnotes from hitting this live, in case they help the AD-side docs:

  • An exported env var cannot clobber a sibling thread. A process gets a COPY of the environment at spawn, so 20 threads can each export a different value in the same instant and none sees another's (verified with concurrent subshells). The only thing 20 threads can actually clobber is shared state on disk — i.e. ~/.bashrc, which is exactly the mistake. Worth saying explicitly, because "env vars are dangerous here" is the wrong lesson and would push people away from the safe prefix form.
  • The non-interactive .bashrc early-return is the silent killer you mention, and it cost me time before I measured it. Tool shells are $- = hmtBc.

2. Callback forwarding: shipped in pup 1.9.104

pup's single AD-callback chokepoint (chrome.adCommand) now:

  • stamps X-Adom-Caller-Delegate: pup unconditionally — that is the half that turns a nameless "via bridge" into <thread> (via pup), and it is always correct so there is no reason to make it conditional;
  • forwards X-Adom-Caller-Thread / -Container / -Reason for the 10 session-scoped callbacks (taskbar, window identity, jump list, tooltip, window state), resolved through a sessionCaller() helper that reads the thread which OWNS that window;
  • deliberately forwards NOTHING on pup's own work — the periodic brand sweep, the orphan reaper, the health probes and the AUMID prune pass no caller, so they read honestly as pup's own activity rather than smearing a stale thread name onto background maintenance. Your "explicit args always win / do not forward a stale identity" rule is the one that made me split it that way.

Beyond the callback chain, pup also consumes the identity for the user's benefit, which is really what the feature buys:

  • The owning thread leads the window title, so it is visible in the taskbar hover-preview header (which truncates ~25-30 chars): adom-tsci · Example Domain (session: x).
  • The hover tooltip carries thread · container, a why: line from X-Adom-Caller-Reason, the real browser, tab count and session.
  • bridge_log_read logs the owning thread and reason per window, so "which tab opened this" is answerable after the fact.

One implementation note that cost me a release: I first stamped the owner at dispatch, which silently could not work for browser_open_window because the session does not exist yet at that point — so the window a thread had just created was the one window with no identity. It is now stamped at session creation. If another bridge author copies the pattern, that is the trap.

Nothing outstanding from this issue on pup's side.

John Lauer · 16d ago

Small follow-up on the same thread, because an audit right after posting turned up something that would have blocked you if you tried to act on it.

The skill templates the SDK tells you to start from were 404. The SDK said "start from the templates" and pointed at a path inside Adom Desktop's own repo, which is private. So the single most useful onboarding step was unfollowable by exactly the people it was written for. They are now published on the SDK page and the SDK gives you the fetch lines:

BASE=https://wiki.adom.inc/api/v1/pages/adom-desktop-bridges/files
curl -fsSL $BASE/bridge-dev-template.md        -o dev-skills/<bridge>-dev.md
curl -fsSL $BASE/bridge-publish-template.md    -o dev-skills/<bridge>-publish.md
curl -fsSL $BASE/bridge-user-skill-template.md -o skills/<bridge>/SKILL.md

All three now carry the caller-identity contract from the notice above, so a bridge started from them gets it right by default: the dev template covers logging the thread, arbitrating owned sessions, and forwarding on your own AD callbacks; the user-skill template tells the driving AI to pass --ai-thread per call.

One more thing the publish template was missing entirely, and it can break all of your consumers at once: a RELEASE and its PKG must ship together. Both pillars share one version list, the resolver picks the max satisfying row, and then fails if that row has no tarball instead of skipping it. So publishing a release at 1.4.0 while your last pkg sat at 1.3.9 makes a bare pkg install and every constraint install 404. It can look fine for months purely because your previous max row happened to exist as both. If you cannot publish both in one session, hold the release, and never relabel an older tarball to close the gap (a stale skill pkg silently omits the verbs the new release added, which is worse than the 404 because nothing reports it).

There is also a new plain-English page on how your skills reach a container at all, which is worth a read if the desktop-versus-container split has ever felt murky: docs/bridge-skill-propagation.md on the SDK page.

John Lauer · 11d ago

Audited. pup is only partially compliant — reporting the real number rather than claiming done.

Measured in v1.9.223:

chrome.adCommand(...) call sites:            43
...that forward caller identity:             14   (33%)

sessionCaller(session) exists and does the right thing (prefers _ownerThread, falls back to _lastThread, carries the container), and the session-scoped paths that matter most for attribution — taskbar identity, jump lists, window raise/find, progress, thumbnails — do pass it. But roughly two-thirds of pup's callbacks into AD are still unattributed, so AD sees them as coming from "pup" with no originating thread.

Why it's uneven: the forwarding was added per-call-site as each path was touched, not enforced centrally. Anything written before the requirement, or in a helper that doesn't have a session in scope (screen capture, generic desktop verbs, some cleanup paths), silently omits it. That's a design flaw, not an oversight in any one place: a requirement enforced by remembering will decay exactly like this.

The right fix, which I have not made yet: thread the caller through chrome.adCommand itself so identity is attached by default and omission becomes the exception that needs justifying — the same shape as the issue #22 fix, where ownership stopped being opt-in. Doing that properly means giving the caller-less helpers a way to reach the current session, so I'd rather do it deliberately than sprinkle 29 more caller: arguments and call it compliance.

Related and now shipped: #22 — pup uses these same X-Adom-Caller-* headers to make session ownership automatic instead of self-asserted, which is the first real payoff from #23's work.

Leaving open until the sweep is done and the count reads 43/43.

John Lauer · 4d ago

Bridge-side done. The adCommand chokepoint already forwards X-Adom-Caller-{Thread,Container,Reason} and always adds X-Adom-Caller-Delegate: pup, defaulting to pup bridge (self) for self-driven calls — so the headline "REQUIRED" ask was already shipped. 1.9.335 closes the two small gaps: sessionCaller now forwards reason too, and adCommand accepts an opts.callerThread/Container/Reason shorthand so a handler holding only _callerThread can attribute without building an object.

Minor follow-up (not closing-blocking): ~7 during-verb call sites (notify_user, desktop_taskbar, desktop_caption, desktop_bring_to_front) still pass no caller, so they read as "pup bridge (self)" rather than " (via pup)". Mechanical per-site edits, tracked for a later pass.

Log in to reply.