---
name: adom-desktop-embedded
description: Adom Bridge's EMBEDDED mode under Adom Hydrogen - what it means when Hydrogen owns Bridge, the lifecycle contract (Bridge stays up; exit when Hydrogen exits is opt-in flag-only), embedded permissions (Hydrogen's auto_approve on /embedded/enter, forwarding gated commands to Hydrogen's /permission/request, status.permissions.shell.embeddedPolicy), inspecting Hydrogen with hd_status and the hd_ verb family, and the read-only Hydrogen-managed relay entry. Use when reasoning about embedded vs standalone behavior, why Bridge survived (or should survive) a Hydrogen crash, why a gated command is waiting on Hydrogen instead of showing a Bridge dialog, or how to drive/inspect Hydrogen from a cloud caller.
---

# Adom Bridge embedded mode (under Adom Hydrogen)

## What embedded mode is

Adom Hydrogen bundles the signed Adom Bridge installer, installs it, and at
runtime spawns Bridge as its child:

```
adom-desktop --embedded --start-hidden --relay-url ws://127.0.0.1:8765 --relay-name adom-hydrogen [--session-token <hydrogen token>]
```

Install locations on macOS: a Hydrogen-managed Bridge lives under
`~/Library/Application Support/adom/bridge/` (Hydrogen 0.1.238+); a
standalone install is `Adom Bridge.app` in `/Applications`. Either way the
bundle id is `inc.adom.desktop` and the CLI binary names are unchanged
(`adom-desktop` / `adom-desktop-cli`).

While embedded:

- Bridge's window is hidden and Bridge HIDES its menu-bar item; Hydrogen is
  the app the human sees. With the window hidden, Bridge runs under the
  Accessory activation policy, so there is no Dock icon and no Cmd-Tab
  entry either. Bridge keeps serving everything (relay, direct API on
  127.0.0.1:47200, bridges) the whole time.
- Bridge writes an `embedded.json` marker (with `entered_via`) so a restart
  re-embeds if Hydrogen is still alive.
- Hydrogen injects a relay-back server entry tagged `added_by:"hd-spawn"`
  (see the read-only rule below).
- Sign-in is shared either way: one Adom sign-in serves both apps through a
  shared keychain item, so an embedded Bridge is signed in whenever Hydrogen
  is (and vice versa).

Entry paths: `POST /embedded/enter` on the direct API, or the `--embedded`
launch flag. Hydrogen polls `GET http://127.0.0.1:47200/health` after
spawning.

## The lifecycle contract (AD_LIFECYCLE.md, on one page)

1. **Bridge stays up.** It is the recovery channel for the machine: if
   Bridge exited whenever Hydrogen exited, one bad Hydrogen exit would leave
   the box unreachable to any remote agent (no log reads, no relaunch, no
   rollback).
2. **The ONLY thing that exits Bridge is an explicit shutdown**:
   `desktop_shutdown` / `POST /command {"command":"shutdown"}`, or the user
   quitting Bridge themselves.
3. **"Embedded" is a display state, not a lifecycle.** Hydrogen gone means
   Bridge drops the costume and reverts to a normal standalone menu-bar
   agent.

What happens when Hydrogen goes away:

| How Hydrogen goes away | What Bridge does |
|---|---|
| Graceful: Hydrogen sends `POST /embedded/release` before stopping | Revert to standalone (menu-bar item back, relay preserved), regardless of how Bridge entered embedded. |
| Ungraceful: Hydrogen's GUI crashes or is killed, no release sent | The `embedded_monitor` (10s probes, 3-miss debounce, ~30s) notices and reverts to standalone. Same end state. |
| A kill takes Hydrogen's whole process tree | Bridge dies WITH Hydrogen because it is Hydrogen's direct child process. This is the OS killing a child, NOT a lifecycle rule firing. Check for the tree-kill before blaming any Bridge logic. |

Reverting (`revert_to_standalone`) shows the menu-bar item again and keeps
the WS connection and the hd-spawn server entry; the supervisor reconnects
with backoff, so a remote caller sees `status=reconnecting`, never a
vanished row (Hydrogen's relay can outlive Hydrogen's GUI).

**Exit-with-Hydrogen is opt-in, flag-only (v1.9.165).** The only way to make
Bridge disposable is the spawner passing `--exit-when-hd-exits` on the
command line. Hydrogen does not pass it, so in practice Bridge always
survives. There is no config key (the old `config.exit_when_hd_exits` is
inert) and no Settings checkbox. The one flag-gated exit lives in
`embedded_monitor::transition_to_standalone`.

Forensics: both transitions write to `startup.log` (`EXIT:` / `REVERT:`
lines). No `crash.log` plus a `startup.log` that runs to `===` means it was
not a panic; an absent process with no crash.log is the signature of an
external (tree) kill.

## Embedded permissions: Hydrogen owns the prompt

Hydrogen sends `auto_approve` in `POST /embedded/enter`; it is authoritative:

- **absent**: Bridge keeps standalone behavior (older Hydrogen; the feature
  is inert until Hydrogen opts in).
- **`false`** (the new-contract default): Bridge does NOT auto-approve and
  shows no UI of its own. For each gated command from a remote relay caller,
  Bridge FORWARDS the request to Hydrogen's `POST /permission/request`
  (control port discovered fresh from Hydrogen's ports.json each call,
  blocking up to 180s) and applies Hydrogen's decision: `allow_once`,
  `allow_1h`, `allow_24h`, `bypass_while_running`, or `deny` (refused with
  `errorCode:denied_by_user`; do not auto-retry). `auto_approve:false`
  overrides any standing grant (the `--shell-approval` flag, Settings
  checkbox, or timer) for relay callers, so the forward always triggers; a
  forwarded allow_1h/24h/bypass sets a dedicated grant window that is
  honored without re-forwarding.
- **`true`**: auto-approve (the user explicitly opted in); behaves like a
  standing grant.

Fail-safe: if Hydrogen is unreachable, Bridge falls back to its OWN approval
dialog (the window summons itself from the menu bar; never hard-deny, never
silent-approve). Coverage runs through one chokepoint
(`consult_embedded_gate`): `shell_execute`, `run_script`, and the Tier-2
class (`launch_process`, `wiki_exec`, `write_file`, `delete_file`,
`process_kill`). Gated verbs accept a `reason` arg that is shown verbatim
(attributed as "The AI says:") on Hydrogen's modal and Bridge's dialog. Only
remote relay callers are gated; local callers (CLI, Hydrogen, bridges) are
trusted by transport.

**Discoverability:** `status.permissions.shell.embeddedPolicy` reports
`"standalone"` | `"forward-to-hd"` | `"auto-approve"`. If a gated call seems
to hang while embedded, a human is likely looking at Hydrogen's prompt; poll
rather than re-issue.

## Inspecting state: hd_status's three orthogonal signals

Hydrogen is killed and respawned many times a day (upgrades, restarts), often
onto a different control port. `hd_status` therefore never collapses its view
into one boolean; it reports THREE independent signals:

- `processPresent`: a process-list check (ps/pgrep on macOS), sampled with
  retry to ride the ~1s kill-then-respawn gap.
- `controlApiReachable`: TCP to Hydrogen's control port, discovered fresh per
  call (ports.json, never cached or hardcoded).
- `embedded`: Bridge's own state (its `AppState` / embedded marker).

`running = processPresent || controlApiReachable`. A mismatch between the
three is normal mid-churn; do not treat a single snapshot as Hydrogen being
dead (Bridge itself requires 3 consecutive misses over ~30s before leaving
embedded mode).

## The hd_* verb family

Bridge exposes `hd_*` verbs for driving and observing Hydrogen itself
(`hd_status`, `hd_stop` / `hd_restart`, `hd_send_files`). Architectural rule:
Bridge owns host/process facts (process present, install path); anything
about Hydrogen's OWN runtime state is fetched from Hydrogen's control API
endpoints (`GET /health`, `GET /buildinfo`, `GET /workspace/health`) and
relayed verbatim. The Hydrogen BUILD verbs (`hd_ship`, `hd_build_*`) are part
of the Windows dev pipeline and return a clean "not supported on macOS yet"
error on this fork.

Updates while embedded follow the same split: Bridge checks, downloads,
verifies and STAGES its own update (`GET /update/status` shows
`update_ready:true` when staged); Hydrogen owns the apply timing and triggers
`POST /update/apply`. Embedded Bridge never auto-applies.

## The Hydrogen-managed relay entry is READ-ONLY

The server entry Hydrogen injects at spawn is tagged `added_by == "hd-spawn"`
and is ephemeral (stripped on config persist, re-added each Hydrogen launch).
While embedded it cannot be disconnected, edited, or removed; the backend
refuses (`is_hd_managed` guard plus `HD_MANAGED_REFUSAL`) on
`disconnect_server`, `remove_server`, `update_server`, the upsert branch of
`add_server`, and the relay `server_add` / `server_remove` handlers, because
any of those would break the embedded session. The UI shows it as a "Managed
by HD" row with a locked note instead of editable fields. Do not try to work
around the refusal; the entry is torn down automatically when Bridge reverts
to standalone.
