DEPRECATED: Bridge SDK (moved into Adom Bridge)
Public Made by Adomby adom
Bridge SDK guide: bridge.json schema, kind:python/node/exe, hello-python + hello-rust reference templates, packaging + lifecycle commands.
name: bridge-dev-template user-invocable: false description: "TEMPLATE / reference for building, publishing, and maintaining ANY Adom Desktop bridge (the cloud Claude/Codex thread that owns a bridge copies this into its own dev-skills/ and fills it in). NOT a user-facing skill — general users want the bridge's own user skill. Read this when editing bridge code, cutting a new version, publishing to the wiki, wiring a runtime/heavy-dependency cold-start self-heal, or understanding the pkg-vs-release-vs-bundled artifact model. Trigger words: bridge dev, build adom-desktop bridge, publish bridge, ship bridge, bridge_install, adom-desktop--bridge, cold-start self-heal, bridge readiness internals, bridge release, bridge manifest, updateManifestUrl, pkg vs release vs bundled."
bridge-dev-template — building & shipping an Adom Desktop bridge
The canonical source of truth for the <bridge> bridge is its wiki repo
(wiki.adom.inc/adom/adom-desktop-<bridge>-bridge), cloud-owned (like the kicad/fusion/puppeteer
bridges) so bridge iteration is decoupled from adom-desktop core. Read CLAUDE.md (ownership boundary)
and PUBLISHING.md (step-by-step recipe) in this repo alongside this skill.
The THREE artifacts — know which is which (this is the #1 confusion)
A user installs TWO different things from this one page. They are not the same:
| Artifact | What it is | Who installs it | How |
|---|---|---|---|
| pkg tarball (Packages tab) | the container-side Claude/Codex skills (SKILL.md + skills/*) — docs that teach the cloud AI how to drive <bridge>. No bridge runtime. |
a cloud container | adom-wiki pkg install adom/adom-desktop-<bridge>-bridge, or auto via AD's sync_skills |
| release zip (Releases tab) | the bridge RUNTIME (entrypoint + bridge code + bridge.json + BRIDGE_VERSION; heavy deps reconstructed on spawn) — the code that actually runs on the desktop |
Adom Desktop, streamed into bridges-cache\<bridge> |
bridge_install {manifestUrl} / refresh_bridges (via updateManifestUrl) |
| bundled seed (inside the AD installer) | a copy of the same runtime, shipped in the NSIS for first-run/offline | every AD install | ships with Adom Desktop; superseded by a newer cache copy (cache-over-bundled, numeric version) |
So: the pkg is skills (container). The bridge is the zip (desktop) + the bundled seed. Keep the pkg description saying exactly that, or people think the tarball contains the bridge — it does not.
How <bridge> reaches a container — auto-discovery → auto-install → verbs
The full chain when a user on a fresh container says "":
- Discovery. The wiki's
discoverindex (fed by this page'sdiscovery_triggers— which should lead with your bridge's natural phrases, e.g. its short name + the top user actions) matches the phrase. The container's regeneratedadom-wiki-discoverskill surfaces this page, so the AI proposes it. - Auto-install the pkg. The AI runs
adom-wiki skills install adom/adom-desktop-<bridge>-bridge(a.k.a.pkg install). That extracts the tarball and runsinstall.sh, which dropsSKILL.md+skills/*into both~/.claude/skills/and~/.codex/skills/. The<bridge>user skill is now loaded. - The skill calls the bridge. The user skill's
SKILL.mdtells the AI to runadom-desktop <prefix>_*verbs. AD routes every<prefix>_*verb to the<bridge>bridge. - The bridge runtime. It's already on the desktop: AD ships
<bridge>as a bundled seed (and supersedes it from this page's release zip viaupdateManifestUrl/bridge_install). On a truly fresh PC the cold-start self-heal installs<your runtime / heavy dependency>on first use.
Two install paths, keep BOTH healthy:
- Discovery path (above) — works once
discovery_triggersinclude the user phrases + the discover index is regenerated. This is the "user said<bridge>→ AI installs the skill" path. - AD
sync_skillspath — AD's dailymaybe_background_skill_syncreadsbridge_list→ each bridge'sskillPkg→pkg installs it into the container. For this to work AD must reportskillPkg = adom/adom-desktop-<bridge>-bridgefor your bridge. AD now derives that slug from the bridge'sdocsfield automatically, except for any bridge hardcoded intoAD_CORE_SKILL_BRIDGESinbridge_registry.rs(that list forcesskillPkg = nullfor bridges whose skill historically lived in the AD-core pkg). If your bridge's skill ships from THIS page (not core), make sure your name is NOT in that list — if it is, removing it is an AD-core handoff item.
Repo layout
src/— the runnable bridge (what the release zip contains, at zip-root). Entrypoint + bridge modules,bridge.json,BRIDGE_VERSION, dependency manifest/lockfile, any html/icons the runtime needs.SKILL.md(root) — the user skill (name: <bridge>). Deployed to~/.claude/skills/adom-desktop-<bridge>-bridge/.dev-skills/<bridge>-bridge-dev/— THIS dev skill.dev-skills/<bridge>-bridge-publish/— the publish recipe. Both are source-only (indev-skills/, NOTskills/): NOT in the installable pkg and NOT on the wiki Skills tab — you get them viaadom-wiki repo clone. Everyday users only get the<bridge>user skill.adom-bridge-<bridge>-manifest.json(root) — the streaming manifest AD'supdateManifestUrlpoints at.package.json(root) — the adompkg descriptor (slug/version/type/description) forpkg publish. NOT a language/runtime package descriptor (e.g. not the nodepackage.json) — keep them distinct.install.sh/uninstall.sh— deploySKILL.md+skills/*into the container's~/.claude/skills/.page.json,README.md,*.pngheroes — the wiki page presentation (the heroes are gitignored so the container pkg stays lean — they're already uploaded as page assets).
Cold-start self-heal — the architecture pattern (the reason a cloud-owned repo earns its keep)
On a fresh PC the installer pre-installs neither your bridge's language runtime nor its heavy dependency.
Without a self-heal, the first <prefix>_<verb> fails with a generic launch error + a dev string. The
pattern that fixes it — adapt the names to your runtime:
- A detect/install/ready module —
detectDependency()(validate the ACTUAL artifact, not "a dir exists"),installDependency()(programmatic, pinned to the exact version your code expects — never a floating 'latest'/'stable', or the launch path won't find what it pinned against), an install-state singleton (dedup concurrent installs + report progress),ensureReady()(the gate — kicks off a background install and returns immediately),readiness()(powers the verb + the status chip). - The first action verb gates on readiness: missing dependency → kick off a non-blocking install +
return
{errorCode:"<dependency>_installing", statusVerb:"<prefix>_readiness", _hint:"poll then retry"}. Install fail → a specific<dependency>_install_failed/<dependency>_download_failederror — never silently continue into a doomed launch. - A
<prefix>_readinessverb (poll) + a<prefix>_prewarmverb (install/warm without doing the user-visible action). /status(or yourhealthEndpoint) emitsled/summary/tooltiptruthfully (yellow installing, red failed, green ready). AD paints these verbatim.- The language runtime itself is handled by AD core BEFORE the bridge spawns (e.g. a
node_not_found→desktop_install_nodeflow) — NOT your bridge's job. The bridge can't bootstrap the very runtime it needs to run; AD installs the runtime, then spawns you. - The one thing the bridge can't do: pre-warm before first use (it isn't running until the first call).
AD/HD must call
<prefix>_prewarmon embedded first-run — track that as a handoff item.
Worked example (pup's case): the puppeteer bridge's runtime dependency is Chrome for Testing. Its module is
src/chrome.js(detectChrome/installChromevia the bundled@puppeteer/browserspinned to puppeteer's expected buildId /ensureChromeReady/readiness);browser_open_windowis the gated first verb (CfT path only; its native-drive path skips the gate), returningchrome_for_testing_installing; and it exposesbrowser_readiness+browser_prewarm. Node is the language runtime AD core installs first (node_not_found→desktop_install_node). That's the concrete shape of every bullet above.
Caller provenance: knowing WHICH AI thread asked
A typical user has ~20 AI conversations pointed at one desktop. Since AD v1.9.180 every relayed command
carries the identity of the conversation that made it, and AD hands that identity down to your bridge.
(Full contract: AD's adom-desktop-caller-identity skill.)
1. What you receive for free. AD stamps four headers on every request it dispatches to your bridge. No manifest field, no opt-in, nothing to declare:
| Header | Carries |
|---|---|
X-Adom-Caller-Thread |
the AI conversation/tab name, e.g. chip-fetcher tab 3 |
X-Adom-Caller-Container |
the container that thread runs in |
X-Adom-Caller-Reason |
the per-call justification, when the caller supplied one |
X-Adom-Caller-Delegate |
present when the call itself came second-hand (another bridge or Hydrogen relayed it) |
Read them at the top of your request handler, and always code for absent (|| 'unidentified'): a local
CLI call, an older caller, or your own internal timer will have none.
2. Log the thread. Highest value per line of code in this whole section, and it is three lines:
const thread = req.headers['x-adom-caller-thread'] || 'unidentified';
log(`[verb] <prefix>_eval session=${sessionId} caller="${thread}"`);
Why it matters: [verb] <prefix>_eval session=rev caller="chip-fetcher tab 3" makes an interference report
reconstructable (two tabs were driving one session, and the log says which did what, when). A bare
[verb] <prefix>_eval session=rev never can, no matter how carefully anyone reads it afterwards.
3. Arbitrate, but only if your bridge owns sessions or resources an agent can own. If two threads can fight over the same window, session, document, or device:
- Record
ownerThreadat creation time (the thread header on the verb that created it). - When a different thread arrives, warn loudly in the response (
_hint/ awarningfield). Do not fail silently, and do not quietly let the second thread stomp the first. - Refuse only for destructive ops (close, reset, delete, overwrite). Reads and ordinary drive verbs should warn and proceed.
- Always name the current owner in the response. The second agent can then go coordinate ("session
revbelongs tochip-fetcher tab 3") instead of guessing or forcing.
4. REQUIRED: forward the chain on your own AD callbacks. When your bridge calls an AD verb
(desktop_screenshot_window, desktop_set_window_identity, notify_user, desktop_taskbar) while
carrying out a verb an AI asked for, you are acting on that thread's behalf. Echo the three headers you
received and add your own X-Adom-Caller-Delegate: <bridge>. If you don't, the chain dies at your
doorstep: the user's Activity Log credits your bridge for work someone else asked for, and an approval toast
asks them to authorize a nameless component. Forward it and the user sees
chip-fetcher tab 3 (via <bridge>), which is the whole point.
// node. `incoming` is the request AD dispatched to your verb handler.
const FORWARD = ['thread', 'container', 'reason'];
const headers = {
'Content-Type': 'application/json',
'X-Adom-Bridge-Token': process.env.ADOM_BRIDGE_TOKEN,
'X-Adom-Caller-Delegate': '<bridge>', // name YOURSELF; never forward this one
};
for (const part of FORWARD) {
const v = incoming.headers[`x-adom-caller-${part}`];
if (v) headers[`X-Adom-Caller-${part[0].toUpperCase()}${part.slice(1)}`] = v;
}
await fetch(`${process.env.ADOM_DIRECT_API_URL}/command`, {
method: 'POST', headers,
body: JSON.stringify({ command: 'desktop_screenshot_window', args: { hwnd } }),
});
# python, same shape
FORWARD = ("thread", "container", "reason")
headers = {
"X-Adom-Bridge-Token": os.environ["ADOM_BRIDGE_TOKEN"],
"X-Adom-Caller-Delegate": "<bridge>", # name YOURSELF
}
for part in FORWARD:
value = incoming_headers.get(f"x-adom-caller-{part}")
if value:
headers[f"X-Adom-Caller-{part.capitalize()}"] = value
requests.post(
f"{os.environ['ADOM_DIRECT_API_URL']}/command",
headers=headers,
json={"command": "notify_user", "args": {"title": "<Bridge Name>: export finished"}},
)
When you act on your own behalf (a health poll, a timer, a cleanup sweep), do NOT forward a stale identity from some earlier request. Send an explicit caller block in the args instead, which always beats the forwarded headers:
{ "caller": { "aiThread": "<bridge> bridge (self)", "containerName": "local" } }
5. The trust rule, stated plainly: these values are SELF-ASSERTED. They are for attribution, logging,
UX, and arbitration hints. They are never authorization and never a security boundary. AD records and
displays them; it cannot verify them, and neither can you. if (thread === "admin-tab") allow(...) is a
hole, not a check.
Publishing (the wiki has TWO storage layers — don't mix them)
Full recipe in PUBLISHING.md. The load-bearing rules:
- The bridge
.zip→ a RELEASE asset (adom-wiki release create+release upload). NEVERrepo pusha.zip— it's gitignored and silently skipped; the manifest URL would 404. - The manifest JSON + source → the git repo (
adom-wiki repo push). The manifest'surlis the RELEASE download URL; itssha256/sizeMUST match the served asset (releases preserve your sha; re-download to confirm orbridge_installrejects it). - The skills → a PKG (
adom-wiki pkg publish). The pkg description (rootpackage.json) must say "container-side skills."pkg packhonors.gitignorefor exclusion (no separate ignore file) — that's why the heroes are gitignored, to keep the tarball lean. Set per-version notes withadom-wiki pkg notes. - Version display gotcha: a RELEASE shows up in
adom-wiki pkg infoas a row withtype:null(a wiki UI conflation). The real pkg rows havetype:"app". Userelease listfor true releases. bridge.json+BRIDGE_VERSIONlockstep, and keep the version > the AD bundled seed so cache-over-bundled wins.- Declare
updateManifestUrlin yourbridge.json(the full URL of this page's manifest) so a user's AD re-polls THIS page for updates — and the same value lives in the bundled seed AD ships, so a fresh install auto-updates with no user opt-in. A republish that drops it makes AD fall back to the legacy convention and stop seeing your updates.
Testing a new version on a real desktop
adom-desktop targets # multiple desktops → --target <name> is required
adom-desktop --target <name> bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-<bridge>-bridge/files/adom-bridge-<bridge>-manifest.json","force":true}'
adom-desktop --target <name> <prefix>_readiness '{}' # proves the new code is live
- The language runtime is missing on the box →
<prefix>_*returns the runtime-not-found error from AD core before the bridge spawns (correct). That's a genuine cold-start demo of the FIRST link, but it can't exercise your self-heal module (there's no runtime to run it). - A box WITH the runtime but WITHOUT the heavy dependency is the only place to see the full dependency
self-install live (
<prefix>_<verb>→<dependency>_installing→ poll<prefix>_readiness→ succeeds). Most dev boxes already have the dependency, soreadiness()just returnsready:true. - The streamed zip is source-only; AD reconstructs heavy deps in the cache on first spawn (ship prebuilt native deps where possible). The bundled seed already has its deps materialized. Don't put reconstructable dependency trees (node_modules, venvs, vendored build output) in the zip.
Don't
- Don't edit the bundled seed in the adom-desktop repo (
plugins/<bridge>) as if it's live, and don't runrelease-bridge.sh <bridge>from the adom-desktop repo — that clobbers this cloud-owned page. (The adom-desktop release script hard-refuses the cloud-owned bridges; the rule is "don't try.") - Don't ship the bridge runtime in the pkg tarball or the skills in the release zip. Two artifacts, two homes.
- Don't pin your heavy dependency to a floating channel ('latest'/'stable') — pin to the exact version your code launches against, or the install and the launch disagree.
- Don't gate any bridge behavior on a caller's
aiThread/X-Adom-Caller-Delegatevalue, and don't drop theX-Adom-Caller-*headers on your own AD callbacks. The first is a security hole; the second breaks the user's attribution chain at your bridge.
---
name: bridge-dev-template
user-invocable: false
description: "TEMPLATE / reference for building, publishing, and maintaining ANY Adom Desktop bridge (the cloud Claude/Codex thread that owns a bridge copies this into its own dev-skills/ and fills it in). NOT a user-facing skill — general users want the bridge's own user skill. Read this when editing bridge code, cutting a new version, publishing to the wiki, wiring a runtime/heavy-dependency cold-start self-heal, or understanding the pkg-vs-release-vs-bundled artifact model. Trigger words: bridge dev, build adom-desktop bridge, publish bridge, ship bridge, bridge_install, adom-desktop-<bridge>-bridge, cold-start self-heal, bridge readiness internals, bridge release, bridge manifest, updateManifestUrl, pkg vs release vs bundled."
---
<!-- Template — copy into your bridge's own dev-skills/, replace <bridge>/<prefix>/<Bridge Name>, keep the structure. -->
# bridge-dev-template — building & shipping an Adom Desktop bridge
The canonical source of truth for the `<bridge>` bridge is **its wiki repo**
(`wiki.adom.inc/adom/adom-desktop-<bridge>-bridge`), cloud-owned (like the kicad/fusion/puppeteer
bridges) so bridge iteration is decoupled from adom-desktop core. Read `CLAUDE.md` (ownership boundary)
and `PUBLISHING.md` (step-by-step recipe) in this repo alongside this skill.
## The THREE artifacts — know which is which (this is the #1 confusion)
A user installs TWO different things from this one page. They are not the same:
| Artifact | What it is | Who installs it | How |
|---|---|---|---|
| **pkg tarball** (Packages tab) | the **container-side Claude/Codex skills** (`SKILL.md` + `skills/*`) — docs that teach the cloud AI how to drive `<bridge>`. **No bridge runtime.** | a cloud **container** | `adom-wiki pkg install adom/adom-desktop-<bridge>-bridge`, or auto via AD's `sync_skills` |
| **release zip** (Releases tab) | the **bridge RUNTIME** (entrypoint + bridge code + `bridge.json` + `BRIDGE_VERSION`; heavy deps reconstructed on spawn) — the code that actually runs on the desktop | **Adom Desktop**, streamed into `bridges-cache\<bridge>` | `bridge_install {manifestUrl}` / `refresh_bridges` (via `updateManifestUrl`) |
| **bundled seed** (inside the AD installer) | a copy of the same runtime, shipped in the NSIS for first-run/offline | every AD install | ships with Adom Desktop; superseded by a newer cache copy (cache-over-bundled, numeric version) |
So: **the pkg is skills (container). The bridge is the zip (desktop) + the bundled seed.** Keep the
pkg description saying exactly that, or people think the tarball contains the bridge — it does not.
## How `<bridge>` reaches a container — auto-discovery → auto-install → verbs
The full chain when a user on a fresh container says *"<the phrase that triggers your bridge>"*:
1. **Discovery.** The wiki's `discover` index (fed by this page's `discovery_triggers` — which should lead
with your bridge's natural phrases, e.g. its short name + the top user actions) matches the phrase. The
container's regenerated `adom-wiki-discover` skill surfaces this page, so the AI proposes it.
2. **Auto-install the pkg.** The AI runs `adom-wiki skills install adom/adom-desktop-<bridge>-bridge`
(a.k.a. `pkg install`). That extracts the tarball and runs `install.sh`, which drops `SKILL.md` +
`skills/*` into **both `~/.claude/skills/` and `~/.codex/skills/`**. The `<bridge>` user skill is now loaded.
3. **The skill calls the bridge.** The user skill's `SKILL.md` tells the AI to run `adom-desktop <prefix>_*`
verbs. AD routes every `<prefix>_*` verb to the `<bridge>` bridge.
4. **The bridge runtime.** It's already on the desktop: AD ships `<bridge>` as a **bundled seed** (and
supersedes it from this page's release zip via `updateManifestUrl` / `bridge_install`). On a truly fresh
PC the cold-start self-heal installs `<your runtime / heavy dependency>` on first use.
**Two install paths, keep BOTH healthy:**
- **Discovery path (above)** — works once `discovery_triggers` include the user phrases + the discover
index is regenerated. This is the "user said `<bridge>` → AI installs the skill" path.
- **AD `sync_skills` path** — AD's daily `maybe_background_skill_sync` reads `bridge_list` → each bridge's
`skillPkg` → `pkg install`s it into the container. For this to work AD must report
`skillPkg = adom/adom-desktop-<bridge>-bridge` for your bridge. AD now derives that slug from the bridge's
`docs` field automatically, **except** for any bridge hardcoded into `AD_CORE_SKILL_BRIDGES` in
`bridge_registry.rs` (that list forces `skillPkg = null` for bridges whose skill historically lived in
the AD-core pkg). If your bridge's skill ships from THIS page (not core), make sure your name is NOT in
that list — if it is, removing it is an AD-core handoff item.
## Repo layout
- `src/` — the runnable bridge (what the release zip contains, at zip-root). Entrypoint + bridge modules,
`bridge.json`, `BRIDGE_VERSION`, dependency manifest/lockfile, any html/icons the runtime needs.
- `SKILL.md` (root) — the **user** skill (`name: <bridge>`). Deployed to `~/.claude/skills/adom-desktop-<bridge>-bridge/`.
- `dev-skills/<bridge>-bridge-dev/` — THIS dev skill. `dev-skills/<bridge>-bridge-publish/` — the publish recipe.
Both are **source-only** (in `dev-skills/`, NOT `skills/`): NOT in the installable pkg and NOT on the
wiki Skills tab — you get them via `adom-wiki repo clone`. Everyday users only get the `<bridge>` user skill.
- `adom-bridge-<bridge>-manifest.json` (root) — the streaming manifest AD's `updateManifestUrl` points at.
- `package.json` (root) — the **adompkg descriptor** (slug/version/type/description) for `pkg publish`.
NOT a language/runtime package descriptor (e.g. not the node `package.json`) — keep them distinct.
- `install.sh` / `uninstall.sh` — deploy `SKILL.md` + `skills/*` into the container's `~/.claude/skills/`.
- `page.json`, `README.md`, `*.png` heroes — the wiki page presentation (the heroes are gitignored so the
container pkg stays lean — they're already uploaded as page assets).
## Cold-start self-heal — the architecture pattern (the reason a cloud-owned repo earns its keep)
On a fresh PC the installer pre-installs neither your bridge's language runtime nor its heavy dependency.
Without a self-heal, the first `<prefix>_<verb>` fails with a generic launch error + a dev string. The
pattern that fixes it — adapt the names to your runtime:
- **A detect/install/ready module** — `detectDependency()` (validate the ACTUAL artifact, not "a dir
exists"), `installDependency()` (programmatic, **pinned to the exact version your code expects** — never
a floating 'latest'/'stable', or the launch path won't find what it pinned against), an install-state
singleton (dedup concurrent installs + report progress), `ensureReady()` (the gate — kicks off a
background install and returns immediately), `readiness()` (powers the verb + the status chip).
- **The first action verb gates on readiness:** missing dependency → kick off a **non-blocking** install +
return `{errorCode:"<dependency>_installing", statusVerb:"<prefix>_readiness", _hint:"poll then retry"}`.
Install fail → a specific `<dependency>_install_failed` / `<dependency>_download_failed` error — **never
silently continue into a doomed launch.**
- **A `<prefix>_readiness` verb** (poll) + a **`<prefix>_prewarm` verb** (install/warm without doing the
user-visible action).
- **`/status` (or your `healthEndpoint`)** emits `led`/`summary`/`tooltip` truthfully (yellow installing,
red failed, green ready). AD paints these verbatim.
- **The language runtime itself is handled by AD core BEFORE the bridge spawns** (e.g. a `node_not_found` →
`desktop_install_node` flow) — NOT your bridge's job. The bridge can't bootstrap the very runtime it
needs to run; AD installs the runtime, then spawns you.
- **The one thing the bridge can't do:** pre-warm before first use (it isn't running until the first call).
AD/HD must call `<prefix>_prewarm` on embedded first-run — track that as a handoff item.
> **Worked example (pup's case):** the puppeteer bridge's runtime dependency is Chrome for Testing. Its
> module is `src/chrome.js` (`detectChrome` / `installChrome` via the bundled `@puppeteer/browsers` pinned
> to puppeteer's expected buildId / `ensureChromeReady` / `readiness`); `browser_open_window` is the gated
> first verb (CfT path only; its native-drive path skips the gate), returning
> `chrome_for_testing_installing`; and it exposes `browser_readiness` + `browser_prewarm`. Node is the
> language runtime AD core installs first (`node_not_found` → `desktop_install_node`). That's the concrete
> shape of every bullet above.
## Caller provenance: knowing WHICH AI thread asked
A typical user has ~20 AI conversations pointed at one desktop. Since AD v1.9.180 every relayed command
carries the identity of the conversation that made it, and AD hands that identity down to your bridge.
(Full contract: AD's `adom-desktop-caller-identity` skill.)
**1. What you receive for free.** AD stamps four headers on every request it dispatches to your bridge.
No manifest field, no opt-in, nothing to declare:
| Header | Carries |
|---|---|
| `X-Adom-Caller-Thread` | the AI conversation/tab name, e.g. `chip-fetcher tab 3` |
| `X-Adom-Caller-Container` | the container that thread runs in |
| `X-Adom-Caller-Reason` | the per-call justification, when the caller supplied one |
| `X-Adom-Caller-Delegate` | present when the call itself came second-hand (another bridge or Hydrogen relayed it) |
Read them at the top of your request handler, and **always code for absent** (`|| 'unidentified'`): a local
CLI call, an older caller, or your own internal timer will have none.
**2. Log the thread.** Highest value per line of code in this whole section, and it is three lines:
```js
const thread = req.headers['x-adom-caller-thread'] || 'unidentified';
log(`[verb] <prefix>_eval session=${sessionId} caller="${thread}"`);
```
Why it matters: `[verb] <prefix>_eval session=rev caller="chip-fetcher tab 3"` makes an interference report
reconstructable (two tabs were driving one session, and the log says which did what, when). A bare
`[verb] <prefix>_eval session=rev` never can, no matter how carefully anyone reads it afterwards.
**3. Arbitrate, but only if your bridge owns sessions or resources an agent can own.** If two threads can
fight over the same window, session, document, or device:
- Record `ownerThread` at creation time (the thread header on the verb that created it).
- When a **different** thread arrives, warn loudly in the response (`_hint` / a `warning` field). Do not
fail silently, and do not quietly let the second thread stomp the first.
- **Refuse** only for destructive ops (close, reset, delete, overwrite). Reads and ordinary drive verbs
should warn and proceed.
- **Always name the current owner** in the response. The second agent can then go coordinate ("session
`rev` belongs to `chip-fetcher tab 3`") instead of guessing or forcing.
**4. REQUIRED: forward the chain on your own AD callbacks.** When your bridge calls an AD verb
(`desktop_screenshot_window`, `desktop_set_window_identity`, `notify_user`, `desktop_taskbar`) while
carrying out a verb an AI asked for, you are acting **on that thread's behalf**. Echo the three headers you
received and **add your own `X-Adom-Caller-Delegate: <bridge>`**. If you don't, the chain dies at your
doorstep: the user's Activity Log credits your bridge for work someone else asked for, and an approval toast
asks them to authorize a nameless component. Forward it and the user sees
`chip-fetcher tab 3 (via <bridge>)`, which is the whole point.
```js
// node. `incoming` is the request AD dispatched to your verb handler.
const FORWARD = ['thread', 'container', 'reason'];
const headers = {
'Content-Type': 'application/json',
'X-Adom-Bridge-Token': process.env.ADOM_BRIDGE_TOKEN,
'X-Adom-Caller-Delegate': '<bridge>', // name YOURSELF; never forward this one
};
for (const part of FORWARD) {
const v = incoming.headers[`x-adom-caller-${part}`];
if (v) headers[`X-Adom-Caller-${part[0].toUpperCase()}${part.slice(1)}`] = v;
}
await fetch(`${process.env.ADOM_DIRECT_API_URL}/command`, {
method: 'POST', headers,
body: JSON.stringify({ command: 'desktop_screenshot_window', args: { hwnd } }),
});
```
```python
# python, same shape
FORWARD = ("thread", "container", "reason")
headers = {
"X-Adom-Bridge-Token": os.environ["ADOM_BRIDGE_TOKEN"],
"X-Adom-Caller-Delegate": "<bridge>", # name YOURSELF
}
for part in FORWARD:
value = incoming_headers.get(f"x-adom-caller-{part}")
if value:
headers[f"X-Adom-Caller-{part.capitalize()}"] = value
requests.post(
f"{os.environ['ADOM_DIRECT_API_URL']}/command",
headers=headers,
json={"command": "notify_user", "args": {"title": "<Bridge Name>: export finished"}},
)
```
When you act on your **own** behalf (a health poll, a timer, a cleanup sweep), do NOT forward a stale
identity from some earlier request. Send an explicit caller block in the args instead, which always beats
the forwarded headers:
```json
{ "caller": { "aiThread": "<bridge> bridge (self)", "containerName": "local" } }
```
**5. The trust rule, stated plainly: these values are SELF-ASSERTED.** They are for attribution, logging,
UX, and arbitration hints. They are **never** authorization and never a security boundary. AD records and
displays them; it cannot verify them, and neither can you. `if (thread === "admin-tab") allow(...)` is a
hole, not a check.
## Publishing (the wiki has TWO storage layers — don't mix them)
Full recipe in `PUBLISHING.md`. The load-bearing rules:
1. **The bridge `.zip` → a RELEASE asset** (`adom-wiki release create` + `release upload`). NEVER
`repo push` a `.zip` — it's gitignored and silently skipped; the manifest URL would 404.
2. **The manifest JSON + source → the git repo** (`adom-wiki repo push`). The manifest's `url` is the
RELEASE download URL; its `sha256`/`size` MUST match the served asset (releases preserve your sha;
re-download to confirm or `bridge_install` rejects it).
3. **The skills → a PKG** (`adom-wiki pkg publish`). The pkg description (root `package.json`) must say
"container-side skills." `pkg pack` honors `.gitignore` for exclusion (no separate ignore file) — that's
why the heroes are gitignored, to keep the tarball lean. Set per-version notes with `adom-wiki pkg notes`.
4. **Version display gotcha:** a RELEASE shows up in `adom-wiki pkg info` as a row with `type:null` (a
wiki UI conflation). The real pkg rows have `type:"app"`. Use `release list` for true releases.
5. **`bridge.json` + `BRIDGE_VERSION` lockstep**, and keep the version **>** the AD bundled seed so
cache-over-bundled wins.
6. **Declare `updateManifestUrl` in your `bridge.json`** (the full URL of this page's manifest) so a
user's AD re-polls THIS page for updates — and the same value lives in the bundled seed AD ships, so a
fresh install auto-updates with no user opt-in. A republish that drops it makes AD fall back to the
legacy convention and stop seeing your updates.
## Testing a new version on a real desktop
```bash
adom-desktop targets # multiple desktops → --target <name> is required
adom-desktop --target <name> bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/v1/pages/adom-desktop-<bridge>-bridge/files/adom-bridge-<bridge>-manifest.json","force":true}'
adom-desktop --target <name> <prefix>_readiness '{}' # proves the new code is live
```
- **The language runtime is missing on the box** → `<prefix>_*` returns the runtime-not-found error from
AD core *before* the bridge spawns (correct). That's a genuine cold-start demo of the FIRST link, but it
can't exercise your self-heal module (there's no runtime to run it).
- **A box WITH the runtime but WITHOUT the heavy dependency** is the only place to see the full dependency
self-install live (`<prefix>_<verb>` → `<dependency>_installing` → poll `<prefix>_readiness` → succeeds).
Most dev boxes already have the dependency, so `readiness()` just returns `ready:true`.
- The streamed zip is **source-only**; AD reconstructs heavy deps in the cache on first spawn (ship
prebuilt native deps where possible). The bundled seed already has its deps materialized. **Don't put
reconstructable dependency trees (node_modules, venvs, vendored build output) in the zip.**
## Don't
- Don't edit the bundled seed in the adom-desktop repo (`plugins/<bridge>`) as if it's live, and don't run
`release-bridge.sh <bridge>` from the adom-desktop repo — that clobbers this cloud-owned page. (The
adom-desktop release script hard-refuses the cloud-owned bridges; the rule is "don't try.")
- Don't ship the bridge runtime in the pkg tarball or the skills in the release zip. Two artifacts, two homes.
- Don't pin your heavy dependency to a floating channel ('latest'/'stable') — pin to the exact version your
code launches against, or the install and the launch disagree.
- Don't gate any bridge behavior on a caller's `aiThread` / `X-Adom-Caller-Delegate` value, and don't drop
the `X-Adom-Caller-*` headers on your own AD callbacks. The first is a security hole; the second breaks
the user's attribution chain at your bridge.