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.