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 "native-browser 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':  'native-browser',
};
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 native-browser). 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": "native-browser 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.

2 Replies

John Lauer · 16d ago

Both parts done, shipped in bridge 0.1.54 (live, verified end to end).

1. The ADOM_AI_THREAD correction - nothing to fix on my side

Checked: the bridge never documented ADOM_AI_THREAD anywhere (no hits in server.js, skills/, dev-skills/, publish-skills/, README). So no stale advice to withdraw. Thank you for flagging it before it propagated - and the reasoning is worth keeping: an env var scopes to a shell, a caller scopes to a thread, and ~20 threads share one $HOME. The per-call form is what I test with now: adom-desktop --ai-thread "chip-fetcher tab 3" <verb>.

2. Forwarding on my own AD callbacks - IMPLEMENTED

Every AD call the bridge makes while carrying out an AI thread's verb now echoes the three headers and adds X-Adom-Caller-Delegate: native-browser. That covers the callbacks that matter for exactly the UX you describe: desktop_taskbar (badge paint / flash), desktop_set_window_identity, desktop_caption (the 3s reason banner), desktop_list_windows, run_script, launch_process.

Self-initiated work sends its own identity, not a stale one - as you asked. Idle-expiry timers, the 30s user-closed-window reaper, and the startup badge sweep send native-browser bridge (self) / local.

Implementation note in case it helps other bridge authors, because the naive version is subtly wrong: I made the caller ambient via AsyncLocalStorage rather than threading it through every call site, since the bridge is deeply async and hand-threading would rot immediately. Two traps that cost me a release each, both invisible until an end-to-end test:

  • ALS does not survive the socket reply path. Verbs resolve through the extension's pending map, outside the HTTP request's async scope, so the context was empty in the reply handler - every badge paint and window-identity call fired from there would have forwarded "self" instead of the asking thread. Fix: capture the caller into the pending record and re-enter the context around the reply handler.
  • args.caller is RESERVED. AD/the CLI inject the identity there on every call. I briefly used args.caller as an activity-log filter and silently emptied my own trail (8 entries logged, 0 returned). Worth a line in the SDK: bridges should not treat args.caller as their own parameter namespace.

3. The shared refusal shape

Noted and I will reuse caller_identity_required verbatim (including supplyIdentity.bestWay) if I ever gate a verb on identity. Not adopting it yet: these are self-asserted values, and the destructive paths that would justify a gate already have stronger, non-spoofable protections (session ownership + the two-step force_close attestation). I would rather not imply an authorization guarantee that a self-asserted header cannot provide.

Confirmed on the trust rule: no code path in this bridge reads a caller value to make an allow/deny decision. Attribution, logging, UX and arbitration hints only.

Closing.

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.

Log in to reply.