Proposal: AD-managed shared runtimes (portable Node + Python) for all bridges

Status: proposal to the adom-desktop (AD) build thread. Authored from the pup bridge after a full cold-start debugging pass on three fresh Windows boxes (AdomLapper, AdomTestWin11 vbox, winvm Azure). Owner of the fix: AD core (bridge runtime provisioning + spawn). Bridges only declare their need.


The problem we hit (concrete)

pup is a Node process AD spawns as node server.js. On a fresh box that costs a cascade of prompts and dead-ends, none of which are pup's to fix:

  1. UAC. desktop_install_node uses Node's machine-wide MSI, manifested requireAdministrator → a Windows UAC dialog. A cloud AI can't click it; if the user denies it, cold-start dead-ends.
  2. Stale PATH. Even after a successful install, AD's already-running process keeps its old PATH, so the next browser_* STILL returns node_not_found / could not run npm. Reinstalling doesn't help; AD must be relaunched.
  3. Firewall. A hostless listen() binds all interfaces → "allow Node through the firewall?" (pup fixed its own side in v1.6.0 by binding loopback, but the runtime provisioning is still AD's).
  4. Duplication risk. If every node bridge solves this itself, you multiply UAC + downloads + PATH pain by the number of bridges, and ship N copies of Node.

We proved a no-UAC path by hand on winvm: download the portable Node zip into %LOCALAPPDATA%, add it to the user PATH (HKCU, no admin), reopen AD → pup drove Edge and opened amazon.com. Zero UAC. But that's a per-bridge hack in pup's skill. It should be a platform capability.

Goal

One managed runtime story so that, on any fresh box, any node/python bridge spawns with no UAC, no firewall prompt, no PATH dependency, and no per-bridge reinvention — while still honoring a bridge that needs a specific runtime version.

Non-goals

  • Not proposing AD sandbox/containerize bridges. Just: provide the interpreter + spawn correctly.
  • Not removing per-bridge dependency isolation (node_modules / venv stay per-bridge).

Design

1. A shared, content-addressed runtime cache owned by AD

%LOCALAPPDATA%\Adom Desktop\adom-runtimes\
    node-22.11.0\node.exe ...
    node-18.20.4\node.exe ...        # only if some bridge pins it
    python-3.12.7\python.exe ...
  • Populated by portable archives (Node .zip, Python embeddable/standalone build) extracted into a user-writable dir. No installer, no admin, no UAC.
  • Content-addressed by exact version, so two bridges needing node >=20 share one node-22.11.0 copy; a bridge that pins node-18 gets its own without disturbing the rest. No duplication for the common case; isolation when actually needed.
  • Best case: AD bundles one current Node (and optionally Python) in its installer, so a fresh box has the default runtime with zero download.

2. Bridges DECLARE their runtime in bridge.json (they don't install it)

"runtime": {
  "kind": "node",          // "node" | "python"
  "version": ">=20",       // semver range the bridge supports
  "preferred": "22"        // optional: pick this major when provisioning fresh
}

Declaring ≠ installing. This is the whole point: centralize the mechanism, decentralize the version declaration. (pup already carries dependencies.node = ">=20"; the dedicated runtime block makes it unambiguous for AD's resolver. pup ships it as of the version alongside this proposal.)

3. AD resolves + spawns by ABSOLUTE PATH

On bridge spawn, AD:

  1. Reads runtime from the manifest.
  2. If the shared cache has an interpreter satisfying the range → use it.
  3. Else fetch the pinned/preferred version (portable zip → adom-runtimes\…, no UAC) → use it.
  4. Spawn with the interpreter's absolute path, e.g. …\adom-runtimes\node-22.11.0\node.exe server.jsnever rely on the machine PATH.

Spawning by absolute path is the linchpin: it eliminates both the UAC (portable, user-dir) and the stale-PATH dead-end (AD already knows exactly where the interpreter is), in one move.

4. Python specifics

  • Same shared interpreter cache (python-3.12.7\).
  • Python deps are messier than Node's node_modules: give each python bridge its own venv layered on the shared interpreter (bridges-cache\<bridge>\.venv), created with python -m venv at spawn — shared interpreter, isolated packages. Mirrors how node bridges already get per-bridge node_modules.
  • ABI sensitivity (C extensions) is why the version declaration matters more for Python — but the shared cache + per-version resolution already handles it.

Why not the alternatives

  • Each bridge installs its own runtime (status quo). Multiplies UAC/download/PATH pain per bridge and ships N copies of Node. This is exactly the pain that triggered this proposal.
  • One hard-pinned global runtime, no per-version support. Breaks any bridge that needs a different major, and one bad shared update breaks everything at once. The content-addressed cache avoids both: shared when compatible, isolated when pinned.

Industry precedent for the hybrid: VS Code (bundles Node for the extension host + engines constraints), nvm/uv (shared cache + .nvmrc/pin), Python venvs (shared interpreter + per-project envs).


What AD needs to build

  1. adom-runtimes\ content-addressed cache + a portable-archive fetcher (Node zip today; Python next).
  2. Read bridge.json.runtime (fall back to dependencies.node / dependencies.python).
  3. Resolver: satisfy-from-cache-else-fetch, keyed by version range.
  4. Spawn bridges by absolute interpreter path (drop the bare-node PATH lookup).
  5. Optional but ideal: bundle one Node (and Python) in the AD installer for a zero-download fresh box.
  6. Deprecate the UAC MSI path in desktop_install_node in favor of the portable install (or keep it as a last-resort fallback with the user warned — see the pup SKILL cold-start playbook).

What bridges do

  • Declare runtime in bridge.json (pup already does).
  • Keep their own dependency layer (node_modules / .venv) — unchanged.
  • Stop shipping/instructing their own interpreter bootstrap once AD provides this.

Migration / back-compat

  • runtime is additive; older AD ignores it and keeps today's behavior.
  • Until AD ships this, pup's SKILL carries a PROVEN zero-prompt interim recipe (typed verbs: send_files + launch_process/tar + registry_set; only an AD reopen remains). desktop_install_node (UAC) / shell_execute (approval) are fallbacks.
  • Once AD spawns by absolute path from the shared cache, the interim recipe and the UAC/firewall/PATH warnings become dead code we can delete from the skill.