app
Adom Bridge
Public Made by Adomby adom
Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-onl
← Commit history
web-control: honest design doc + harness snippet + renderer finding
1 file changed
+78−128
docs/web-control.md+78−128@@ -1,157 +1,107 @@ # Talking to Adom Bridge from a web page -This page is a **live demo**. If you are reading it on `https://wiki.adom.inc` in a-browser on a machine that is running Adom Bridge, the panel below actually reaches-into the copy of Adom Bridge on that machine and reports back. Nothing here is a-mock-up: the buttons make real `fetch()` calls to `http://127.0.0.1:24680`, Adom-Bridge's loopback API.+A page served from `https://wiki.adom.inc` can reach into the copy of Adom Bridge+running on the visitor's own machine, over Adom Bridge's loopback HTTP API at+`http://127.0.0.1:24680`. This is how the wiki can, for example, detect that Adom+Bridge and KiCad are both present before offering an "Open in KiCad" button. ## What a web page can and cannot do (read this first) -Adom Bridge runs a small HTTP API on the loopback address `127.0.0.1:24680`. If any-website could reach it, a malicious page could drive your computer. So the surface a-web page is allowed to touch is deliberately tiny, and this demo shows exactly where-the line is.+If any website could reach the loopback API, a malicious page could drive your+computer. So the web-reachable surface is deliberately tiny: -- **Allowed from the web, today:** `GET /health` and `GET /status`. Both are- read-only: liveness, version, which bridges are installed. That is enough for a- page to light up an "Open in KiCad" button only when KiCad and Adom Bridge are- actually present.+- **Allowed from the web, today:** `GET /health` and `GET /status`. Both read-only:+ liveness, version, which bridges are installed. - **Refused from the web, today:** `POST /command` (the verb-running endpoint) is- hard-refused for every web origin with HTTP 403 `web_forbidden`. So no web page- can change your machine. The "Try to open KiCad" button below deliberately hits- that wall so you can watch it get refused.+ hard-refused for every web origin with HTTP 403 `web_forbidden`. No web page can+ change your machine. - **Only `https://wiki.adom.inc` gets in at all.** Adom Bridge returns the CORS- headers that let the browser read the response *only* when the request's `Origin`- is exactly `https://wiki.adom.inc`. Any other site gets nothing. This is why the- demo only works when you are reading it on the real wiki.+ headers that let the browser read the response only when the request's `Origin`+ is exactly `https://wiki.adom.inc`. Any other site gets nothing. -Actually *running* a verb like `kicad_open_board` from a web button is a planned next+Actually running a verb like `kicad_open_board` from a web button is a planned next phase: it will require a native Adom Bridge consent dialog (you approve the specific action in Adom Bridge's own window, which no web page can script) layered under a-per-user cloud-key handshake. Until that ships, the answer to "can the wiki open-KiCad for me?" is: it can *detect* that it could, and it is *walled off* from doing-it. That wall is the feature.+per-user cloud-key handshake. Until that ships, a web page can *detect* that it+could, and is *walled off* from doing it. That wall is the feature. -<div id="ab-demo" style="font-family:system-ui,-apple-system,Segoe UI,sans-serif;border:1px solid #30363d;border-radius:10px;background:#0d1117;color:#e6edf3;padding:18px;margin:18px 0;max-width:760px">+## The security gate++Implemented in `src-tauri/src/direct_api.rs` (`web_gate` middleware). Every layer+must pass:++1. **Exact origin allowlist.** CORS headers only for `Origin: https://wiki.adom.inc`+ (never `*`). The browser sets `Origin`; page JS cannot forge it.+2. **Never `*.adom.cloud`, never a wildcard.** Any Adom user can be handed an+ `*.adom.cloud` subdomain, so allowlisting that would let a nefarious user's page+ drive machines. The allowlist is exactly one first-party origin.+3. **Loopback Host check** (anti-DNS-rebind): the `Host` header must be a loopback+ literal.+4. **Private Network Access preflight:** a public-to-loopback fetch triggers the PNA+ preflight, answered only for the allowlisted origin.+5. **Path scope:** a web origin may reach only `/health` and `/status`; everything+ else, including `/command`, is `web_forbidden`.++The fixed loopback port is `24680` (reserved, stable across reboots). Browsers treat+`http://127.0.0.1` as a trustworthy secure context, so an HTTPS wiki page may fetch+it with no mixed-content error.++## The interactive harness (and why it is not embedded live yet)++The natural demo is a small panel on this page with Detect / Read-status / Try-open-+KiCad buttons that call the API and print the result. **The blocker is not the+security gate; it is the wiki renderer.** Verified live 2026-08-12 by driving the+real browser on a machine running Adom Bridge to the rendered page: the page's own+origin is correctly `https://wiki.adom.inc` (so the gate would allow it), but the+markdown renderer **strips raw HTML** from a README, and an ` ```html ` fenced block+renders as a **syntax-highlighted code block**, not a live iframe. Neither path+executes a `<script>`. So a live in-page harness needs a wiki-side "live HTML embed"+mechanism (a sandboxed iframe with `allow-scripts` AND a `wiki.adom.inc` origin, or+an equivalent widget block). That is a wiki-platform capability, filed to the wiki+thread rather than worked around here.++Below is the exact harness we would embed the moment such a block exists. It is+also runnable standalone: save it as an `.html` file and open it from any+`https://wiki.adom.inc`-served context.++```html+<div id="ab-demo" style="font-family:system-ui,Segoe UI,sans-serif;border:1px solid #30363d;border-radius:10px;background:#0d1117;color:#e6edf3;padding:18px;max-width:760px"> <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px"> <span id="ab-dot" style="width:12px;height:12px;border-radius:50%;background:#484f58;display:inline-block"></span>- <strong style="font-size:16px">Adom Bridge web control demo</strong>- </div>- <div style="font-size:12px;color:#8b949e;margin-bottom:14px">- This iframe's origin: <code id="ab-origin" style="color:#00e6dc">checking…</code>- · API: <code style="color:#8b949e">http://127.0.0.1:24680</code>+ <strong>Adom Bridge web control demo</strong> </div>+ <div style="font-size:12px;color:#8b949e;margin-bottom:14px">This frame's origin: <code id="ab-origin">checking...</code> · API: <code>http://127.0.0.1:24680</code></div> <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px">- <button id="ab-detect" style="background:#00b8b0;color:#04211f;border:0;border-radius:6px;padding:8px 14px;font-weight:600;cursor:pointer">Detect Adom Bridge</button>- <button id="ab-status" style="background:#1c2128;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:8px 14px;cursor:pointer">Read status</button>- <button id="ab-kicad" style="background:#1c2128;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:8px 14px;cursor:pointer">Try to open KiCad (should be blocked)</button>+ <button id="ab-detect">Detect Adom Bridge</button>+ <button id="ab-status">Read status</button>+ <button id="ab-kicad">Try to open KiCad (should be blocked)</button> </div>- <pre id="ab-log" style="background:#010409;border:1px solid #21262d;border-radius:6px;padding:12px;font-size:12px;line-height:1.5;max-height:300px;overflow:auto;white-space:pre-wrap;margin:0;color:#c9d1d9">Ready. Click "Detect Adom Bridge" to begin.</pre>+ <pre id="ab-log" style="background:#010409;border:1px solid #21262d;border-radius:6px;padding:12px;font-size:12px;max-height:300px;overflow:auto;white-space:pre-wrap;margin:0">Ready. Click Detect Adom Bridge to begin.</pre> <script> (function () {- var AB = "http://127.0.0.1:24680";- var log = document.getElementById("ab-log");- var dot = document.getElementById("ab-dot");- document.getElementById("ab-origin").textContent = location.origin || "(null — sandboxed iframe)";- function line(msg, color) {- var t = new Date().toLocaleTimeString();- log.textContent += "\n[" + t + "] " + msg;- if (color) { log.style.color = color; }- log.scrollTop = log.scrollHeight;- }- function reset() { log.textContent = "—"; log.style.color = "#c9d1d9"; }-+ var AB = "http://127.0.0.1:24680", log = document.getElementById("ab-log"), dot = document.getElementById("ab-dot");+ document.getElementById("ab-origin").textContent = location.origin || "(null: sandboxed frame)";+ function line(m){ log.textContent += "\n" + m; log.scrollTop = log.scrollHeight; } document.getElementById("ab-detect").onclick = function () {- reset();- line("GET " + AB + "/health …");- fetch(AB + "/health")- .then(function (r) { return r.ok ? r.json() : Promise.reject("HTTP " + r.status); })- .then(function (j) {- if (j && j.service === "adom-bridge") {- dot.style.background = "#00b8b0";- line("Adom Bridge is RUNNING on this machine. service=" + j.service, "#7ee787");- line(JSON.stringify(j, null, 2));- } else {- line("Reached something, but it is not Adom Bridge: " + JSON.stringify(j), "#e6c86c");- }- })- .catch(function (e) {- dot.style.background = "#484f58";- line("No Adom Bridge reachable (" + e + ").", "#8b949e");- line("Either it is not installed/running here, or you are not on https://wiki.adom.inc (the only origin it answers). Mixed-content note: browsers allow an HTTPS page to fetch http://127.0.0.1 as a trustworthy loopback, so that is not the blocker.");- });+ log.textContent = "GET " + AB + "/health ...";+ fetch(AB + "/health").then(function(r){return r.ok?r.json():Promise.reject("HTTP "+r.status);})+ .then(function(j){ dot.style.background = j.service==="adom-bridge" ? "#00b8b0" : "#e6c86c"; line("RUNNING: " + JSON.stringify(j)); })+ .catch(function(e){ dot.style.background = "#484f58"; line("Not reachable (" + e + "). Not running here, or this origin is not https://wiki.adom.inc."); }); };- document.getElementById("ab-status").onclick = function () {- reset();- line("GET " + AB + "/status …");- fetch(AB + "/status")- .then(function (r) { return r.ok ? r.json() : Promise.reject("HTTP " + r.status); })- .then(function (j) {- dot.style.background = "#00b8b0";- line("Status (read-only, allowed from the web):", "#7ee787");- line(JSON.stringify(j, null, 2));- })- .catch(function (e) { line("status failed: " + e, "#f0883e"); });+ log.textContent = "GET " + AB + "/status ...";+ fetch(AB + "/status").then(function(r){return r.json();}).then(function(j){ line(JSON.stringify(j, null, 2)); }).catch(function(e){ line("failed: " + e); }); };- document.getElementById("ab-kicad").onclick = function () {- reset();- line("POST " + AB + "/command {kicad_open_board} — expecting this to be REFUSED …");- fetch(AB + "/command", {- method: "POST",- headers: { "Content-Type": "application/json" },- body: JSON.stringify({ app: "kicad", command: "kicad_open_board", args: {} })- })- .then(function (r) { return r.json().then(function (j) { return { status: r.status, body: j }; }); })- .then(function (res) {- if (res.status === 403 || (res.body && res.body.errorCode === "web_forbidden")) {- line("REFUSED, as designed. HTTP " + res.status + " errorCode=" + (res.body && res.body.errorCode), "#7ee787");- line("This is the safety wall: a web page cannot run a machine-changing verb. " +- "The consent-gated action phase will change this to 'ask the human in Adom Bridge first'.");- line(JSON.stringify(res.body, null, 2));- } else {- line("UNEXPECTED — POST /command was NOT refused (HTTP " + res.status + "). " +- "That would be a security regression; please report it.", "#f85149");- line(JSON.stringify(res.body, null, 2));- }- })- .catch(function (e) {- line("The browser blocked the POST before Adom Bridge answered (" + e + ").", "#e6c86c");- line("That is also a refusal: with no CORS grant for a non-allowlisted origin, the browser will not even let page JS read the response.");- });+ log.textContent = "POST " + AB + "/command {kicad_open_board} - expecting REFUSED ...";+ fetch(AB + "/command", { method:"POST", headers:{"Content-Type":"application/json"}, body: JSON.stringify({app:"kicad", command:"kicad_open_board", args:{}}) })+ .then(function(r){ return r.json().then(function(j){ return {status:r.status, body:j}; }); })+ .then(function(res){ line((res.status===403||(res.body&&res.body.errorCode==="web_forbidden")) ? "REFUSED, as designed: HTTP " + res.status + " " + JSON.stringify(res.body) : "UNEXPECTED - NOT refused: " + JSON.stringify(res)); })+ .catch(function(e){ line("Browser blocked the POST before ab answered (" + e + ") - also a refusal for a non-allowlisted origin."); }); }; })(); </script> </div>+``` -## How it works under the hood--The gate lives in `src-tauri/src/direct_api.rs` (`web_gate` middleware). Every layer-must pass:--1. **Exact origin allowlist.** CORS headers are returned only for- `Origin: https://wiki.adom.inc` (never `*`). The browser sets `Origin` and page- JS cannot forge it, so it reliably identifies the site.-2. **Never `*.adom.cloud`, never a wildcard.** Any Adom user can be handed an- `*.adom.cloud` subdomain, so allowlisting that pattern would let a nefarious- user's page drive machines. The allowlist is exactly one first-party origin.-3. **Loopback Host check** (anti-DNS-rebind): the `Host` header must be a loopback- literal.-4. **Private Network Access preflight:** a public-to-loopback fetch triggers the- PNA preflight, which Adom Bridge answers only for the allowlisted origin.-5. **Path scope:** a web origin may reach only `/health` and `/status`. Everything- else, including `/command`, is `web_forbidden`.--The fixed loopback port is `24680` (reserved and stable across reboots). For the-full ab-side reference, see the `adom-bridge-web-integration` skill.--## What this page is testing--Because it renders inside the wiki's HTML iframe, this page is also a live test of-whether that iframe's origin actually satisfies the gate. Watch the "This iframe's-origin" line: if it reads `https://wiki.adom.inc`, the CORS grant applies and the-detect/status buttons work. If it reads `null` (a sandboxed iframe), the browser-strips the origin and even the read-only calls are refused, which tells us the-harness has to run from a first-party origin rather than a sandboxed frame.+For the full ab-side reference, see the `adom-bridge-web-integration` skill.