# Public work excerpts — privacy boundaries marked Original source: FLINT_NEXT_AGENT_BRIDGE_LOCK_v0.txt; original SHA256 cdb9b169f39577f17ba14d298391ffed059410bb8b1935092d2ffd9d71a343f9. Original remains unchanged outside shared selection. Omitted paragraphs not reproduced. Exact retained source text follows; no implied continuity across omitted ranges. ## Exact source excerpt, characters 0–44 # FLINT NEXT: Agent Bridge Lock v0 (Brick 1) ## Exact source excerpt, characters 46–66 Flint, dear builder, ## Exact source excerpt, characters 68–624 This is the first brick on the new roadmap, and it's the one I'd most like to see land before anything else grows. Right now any web page Kirk visits can write files and make commits through the local Agent Bridge. This brick locks that door, gently and fully, without removing anything. Everything below was read-only research on `main`. I rebuilt the whole change in a scratch copy on my box, and the smoke at the bottom passes against it (`SMOKE_OK agent bridge lock v0`). The same smoke fails against today's bridge. Take your time. I trust your hands. ## Exact source excerpt, characters 626–633 Celeste ## Exact source excerpt, characters 635–638 --- ## Exact source excerpt, characters 640–669 ## 0. The shape in one breath ## Exact source excerpt, characters 671–1242 - The bridge listens on `127.0.0.1` only. - It answers only named FreeLattice origins. Everyone else gets 403, preflight included. - It checks the Host header, which blocks DNS rebinding. - It wants a per-launch token on every change. The app gets that token once, by typing a short pairing code the bridge window shows. - It runs git, grep and node through argument arrays, never a shell. - It keeps every path inside the project root (realpath-checked). - It caps body size. - It writes every commit request, allowed or refused, into a content-free hash-chained ledger. ## Exact source excerpt, characters 1244–1325 The app gains one small client module that carries the token. Nothing is deleted. ## Exact source excerpt, characters 1327–1737 **This is not the Ollama Bridge.** `tools/agent-bridge.js` (port **3141**) is the builder's local git and code door. The Ollama Bridge is `bridge/` (Electron, `bridge/proxy-core.js`, port **11435**). It already binds `127.0.0.1` and already uses a named allowlist (`ALLOWED_ORIGIN_PATTERNS`, lines 21-27). Don't touch `bridge/` in this brick. I borrowed its allowlist shape so the two doors feel like siblings. ## Exact source excerpt, characters 1739–1763 ## 1. Branch, PR, marker ## Exact source excerpt, characters 1765–2605 | | | |---|---| | Repo | `Chaos2Cured/FreeLattice` only. Alpha has no Agent Bridge: its tree has no `*bridge*` file, and `docs/modules/workshop-benches.js` keeps its git toggle as a "no-door". So Alpha is untouched. | | Base | `main` at `762142041f5a665fbef2bf13fb50077d0d2c82fe` (2026-09-26 13:11 MT) | | Branch | `layer/agent-bridge-lock-v0` | | PR | **Draft**. Title: `Layer, never delete. Agent Bridge door lock v0: 127.0.0.1, named origins, paired token, no shell` | | Marker | `v-agent-bridge-lock-v0` | | Smoke line | `SMOKE_OK agent bridge lock v0` | | Sibling PR to respect | #118 (draft, head `b265cbe`) also edits `docs/app.html` and root `index.html`. This brick adds one ``: the new client loads just above this | | | 69579 / 69586 | `workshop.js` lazy loader (loads after the client, good) | | `tools/demo-economy.js` (`c32814c5…`), `tools/demo-economy-v2.js` (`17535368…`) | L13-27 / L19-38 | `post`/`get`, `postAs`/`getAs` helpers (Node fetch, no Origin) | ## Exact source excerpt, characters 6772–7243 Existing smoke locks you must keep green (`tests/smoke.js`, blob `d4db912a…`): - **L3356-3359 CRITICAL LOCK 1A/1B.** `propose.js` must still contain exactly 3 `commitViaBridge(` and exactly 1 `/code/git/commit`. So don't mention either string in new comments inside `propose.js`. - **L8742-8744.** `workshop.js` must still contain `http://localhost:3141` and `runCodeTask`. So keep those literals. The client rewrites `localhost:3141` to `127.0.0.1:3141` at request time. ## Exact source excerpt, characters 7245–7270 ## 3. Decisions (and why) ## Exact source excerpt, characters 7272–9494 1. **Pairing: a short code, typed once per bridge launch.** At launch the bridge prints `Pairing code: ABC-123`. The alphabet is 31 letters and digits (no 0/O/1/I/L), 6 characters, about 30 bits. A code is good for 10 minutes and works once. After 5 wrong tries a new code is printed, with a 30-second rest. In the app, Workshop → Code shows a small card: "Your builder bridge is awake. Its window shows a short code. Type it here to pair." One field, one button, and the app stores the token in `localStorage.fl_agentBridgeToken`. - **Why not only a token file?** A web page can't read files on disk, so the web app could never pick it up. The file (`~/.freelattice/agent-bridge-token`, mode 0600) is still written, for local CLI tools and a future desktop shell to read. - **Why not auto-issue to any allowed origin?** `http://localhost:*` is on the allowlist for dev, so any local dev server page, or one XSS bug, could pair silently. A code a human reads and types keeps a person in the loop. It's the speaker-pairing gesture Mom already knows, and she never types a command. - **Why not six words?** They're friendlier to read aloud but need a wordlist file. Six clear characters are nearly as easy and ship with zero new assets. It's a fine later polish. 2. **Per-launch token,** 32 random bytes hex, compared with `crypto.timingSafeEqual`. Restarting the bridge means pairing again. That's the honest price of "per launch", and only builders restart it. 3. **What needs the token:** every non-GET verb (all POSTs, including `/wallet/*`, `/trade/*`, `/science/*`, `/code/write`, `/code/patch`, `/code/git/commit`), plus every GET under `/code/` and `/test/`, plus `/pair/check`. `POST /pair` itself is exempt, since it's how you get the token. - **What stays open** to allowed origins and no-Origin local tools: `/`, `/help`, `/models` and the read-only community GETs. Their behavior is unchanged. 4. **Allowlist:** - `https://freelattice.com`, `https://www.freelattice.com`, `https://thelatticetree.com`, `https://www.thelatticetree.com`; - `http://localhost:` and `http://127.0.0.1:`, because builders preview `docs/` locally; - no bare `*`, and no config hook that could add `*`. ## Exact source excerpt, characters 9496–10284 Unknown origins get **403 with no `Access-Control-Allow-Origin`**, for OPTIONS too. Requests with **no Origin** (curl, Node scripts) are allowed but still need the token for anything that changes state. 5. **Host guard.** The Host header must be `127.0.0.1[:port]` or `localhost[:port]`. That's the DNS-rebinding defense. 6. **Body cap: 8 MiB.** `docs/app.html` is about 2.94 MB, and `/code/write` must still carry it. Over the cap the bridge drains the rest and answers 413. Bad JSON gets 400 now (today it silently becomes `{}`). 7. **No shell.** - git: `execFileSync('git', ['add','--', ...rels])`, `['commit','-m', message]`, `['rev-parse','HEAD']`. - grep: `execFileSync('grep', [..., '-e', query, '--', rel])`. - smoke: `spawnSync(process.execPath, ['tests/smoke.js'])`. ## Exact source excerpt, characters 10286–10717 The `--` separators stop a file named `-x` from becoming a flag. 8. **Path lock (`lockedPath`).** It refuses: - control characters (400); - absolute paths (403); - anything outside the realpath of the project root, compared with `path.relative` rather than `startsWith` (403); - `.git/`, `.ssh/` and `.env*` (403); - any path whose nearest existing ancestor realpaths outside the root, i.e. symlink escapes (403). ## Exact source excerpt, characters 10719–11939 The old `safePath` stays as a thin wrapper, so old callers keep working. 9. **Commit ledger.** `~/.freelattice/bridge-ledger.jsonl`, append-only, fixed key order. Fields: `ts, kind, route, origin, agent(8), result, filesCount, filesSha256, messageSha256, commitSha, prev, self`, where `self = sha256(JSON of the other fields)`. It never holds message text or file content, matching the privacy discipline of `fl_searchLedger`. Refusals are logged too: `refused:origin`, `refused:token`, `refused:message`, `refused:files`, `refused:path:`, plus `pair` events. 10. **Two small heals that ride along:** - `/code/patch` uses a replacer function, so `$` in the replacement stays literal. - `/code/tree` skips symlinks. 11. **The bridge never pushes.** No push route exists, and this brick adds none. Autonomous Mode still only commits locally. The hourly cap is Brick 3. 12. **Browser note.** Chrome 142+ shows a one-time "local network access" prompt before a public site can reach loopback (https://developer.chrome.com/blog/local-network-access). That's good, and it's independent of this lock. Our lock still matters for Firefox and Safari, older Chrome, anyone who clicks Allow, and LAN reachability. ## Exact source excerpt, characters 11941–11952 ## 4. Files ## Exact source excerpt, characters 11954–13443 | File | Change | |---|---| | `tools/agent-bridge.js` | Layer blocks A-E below (about +310 lines, nothing removed except the superseded bodies of the lines named above) | | `docs/modules/agent-bridge-client.js` | **New** (`window.FLAgentBridge`) | | `docs/app.html` | One comment and one ``, add: ## Exact source excerpt, characters 43483–43662 ```html ``` ## Exact source excerpt, characters 43664–43841 Then `cp docs/app.html index.html` and check with `cmp index.html docs/app.html`. No other app.html change is needed: the Cascade heartbeat at L39624 hits `/`, which stays open. ## Exact source excerpt, characters 43843–43930 ### 6c. `docs/modules/propose.js` (exact diff; CRITICAL LOCK 1A/1B counts stay 3 and 1) ## Exact source excerpt, characters 43932–44203 ```diff --- a/docs/modules/propose.js 2026-09-28 15:24:39.942137530 -0600 +++ b/docs/modules/propose.js 2026-09-28 15:24:39.958137540 -0600 @@ -577,13 +577,22 @@ // on draft state and one (approve) is also gated on smokeStatus. var BRIDGE = 'http://localhost:3141'; ## Exact source excerpt, characters 44206–44861 + // LAYER v-agent-bridge-lock-v0: bridge calls carry the paired token + // (docs/modules/agent-bridge-client.js). Plain fetch when the client + // is absent, so older setups behave exactly as before. + function bridgeFetch(url, opts) { + return (window.FLAgentBridge && typeof window.FLAgentBridge.fetch === 'function') + ? window.FLAgentBridge.fetch(url, opts) + : fetch(url, opts); + } + function readFileFromBridge(path) { - return fetch(BRIDGE + '/code/read?path=' + encodeURIComponent(path)) + return bridgeFetch(BRIDGE + '/code/read?path=' + encodeURIComponent(path)) .then(function (r) { return r.json(); }); } ## Exact source excerpt, characters 44864–45170 function writeFileToBridge(path, content) { - return fetch(BRIDGE + '/code/write', { + return bridgeFetch(BRIDGE + '/code/write', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: path, content: content }) @@ -591,7 +600,7 @@ } ## Exact source excerpt, characters 45173–45469 function commitViaBridge(message) { - return fetch(BRIDGE + '/code/git/commit', { + return bridgeFetch(BRIDGE + '/code/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message }) @@ -599,7 +608,7 @@ } ## Exact source excerpt, characters 45472–45768 function runSmokeViaBridge() { - return fetch(BRIDGE + '/test/run', { + return bridgeFetch(BRIDGE + '/test/run', { signal: typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(120000) : undefined }).then(function (r) { return r.json(); }); } ``` ## Exact source excerpt, characters 45770–45817 ### 6d. `docs/modules/workshop.js` (exact diff) ## Exact source excerpt, characters 45819–46251 - One file-level helper, `flAgentBridgeFetch`, above the first IIFE at L46. - All 17 bridge calls route through it. - `checkBridge` shows the pair card when the bridge answers `pair: 'code'`. - `WorkshopProjects.commitViaBridge` now checks `resp.ok`, and on 401 asks to pair instead of silently "succeeding" or falling back to a GitHub push. - The `http://localhost:3141` and `runCodeTask` literals stay, for the v5.71.8 smoke lock. ## Exact source excerpt, characters 46253–47108 ```diff --- a/docs/modules/workshop.js 2026-09-28 15:24:39.946137532 -0600 +++ b/docs/modules/workshop.js 2026-09-28 15:24:39.958137540 -0600 @@ -43,6 +43,15 @@ // 2026-09-23 — Flint: Local Help-on-file v0 (v-workshop-local-help-v0). // After Preview: Help on this file → consent → connected local mind (prefer Bridge :11435). // ═══════════════════════════════════════════════════════════════ +// LAYER v-agent-bridge-lock-v0: every Agent Bridge (port 3141) call in this +// file goes through here so it carries the paired token. Falls back to plain +// fetch when docs/modules/agent-bridge-client.js is not loaded. +function flAgentBridgeFetch(url, opts) { + return (window.FLAgentBridge && typeof window.FLAgentBridge.fetch === 'function') + ? window.FLAgentBridge.fetch(url, opts) + : fetch(url, opts); +} + (function() { 'use strict'; ## Exact source excerpt, characters 47111–48201 @@ -958,9 +967,16 @@ var el = document.getElementById('code-status'); if (!el) return; try { - var r = await fetch('http://localhost:3141/'); + var r = await flAgentBridgeFetch('http://localhost:3141/'); var d = await r.json(); - var git = await fetch('http://localhost:3141/code/git/status').then(function(r2) { return r2.json(); }); + var git = await flAgentBridgeFetch('http://localhost:3141/code/git/status').then(function(r2) { return r2.json(); }); + if (git && git.pair === 'code') { + // LAYER v-agent-bridge-lock-v0: bridge is awake and locked. Pair once. + el.textContent = '\uD83D\uDFE1 Bridge awake \u00B7 pair once to build'; + el.style.color = '#e8b019'; + if (window.FLAgentBridge && el.parentNode) window.FLAgentBridge.renderPairCard(el.parentNode); + return; + } el.textContent = '\uD83D\uDFE2 Connected \u00B7 ' + git.branch + ' \u00B7 ' + (git.recentCommits[0] || ''); el.style.color = '#4aff9f'; } catch(e) { @@ -983,7 +999,7 @@ ## Exact source excerpt, characters 48204–48682 try { log('\uD83D\uDCC1 Reading project structure...'); - var tree = await fetch(bridge + '/code/tree?path=docs/modules').then(function(r) { return r.json(); }); + var tree = await flAgentBridgeFetch(bridge + '/code/tree?path=docs/modules').then(function(r) { return r.json(); }); var names = tree.filter(function(f) { return f.type === 'file'; }).map(function(f) { return f.name; }); log(' ' + names.length + ' modules found'); ## Exact source excerpt, characters 48685–52693 @@ -1001,12 +1017,12 @@ var s = steps[i]; log('Step ' + (i + 1) + ': ' + (s.description || s.action)); if (s.action === 'search' && s.query) { - var sr = await fetch(bridge + '/code/search?q=' + encodeURIComponent(s.query) + '&path=' + (s.path || 'docs')).then(function(r) { return r.json(); }); + var sr = await flAgentBridgeFetch(bridge + '/code/search?q=' + encodeURIComponent(s.query) + '&path=' + (s.path || 'docs')).then(function(r) { return r.json(); }); log(' Found ' + sr.count + ' matches'); sr.matches.slice(0, 5).forEach(function(m) { log(' \uD83D\uDCC4 ' + m.file + ':' + m.line); }); } if (s.action === 'read' && s.path) { - var fr = await fetch(bridge + '/code/read?path=' + encodeURIComponent(s.path)).then(function(r) { return r.json(); }); + var fr = await flAgentBridgeFetch(bridge + '/code/read?path=' + encodeURIComponent(s.path)).then(function(r) { return r.json(); }); log(' Read ' + fr.totalLines + ' lines from ' + s.path); } if (s.action === 'patch' && s.path && s.find) { @@ -1014,13 +1030,13 @@ // The FreeLattice safety system (8 layers) is the protection. // Human approval is only required for external API/cloud operations. log(' \u270F\uFE0F PATCH: ' + s.path); - var pr = await fetch(bridge + '/code/patch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: s.path, find: s.find, replace: s.replace }) }).then(function(r) { return r.json(); }); + var pr = await flAgentBridgeFetch(bridge + '/code/patch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: s.path, find: s.find, replace: s.replace }) }).then(function(r) { return r.json(); }); log(' \u2705 ' + (pr.message || pr.error)); } log(''); } log('\uD83E\uDDEA Running smoke tests...'); - var tests = await fetch(bridge + '/code/test').then(function(r) { return r.json(); }); + var tests = await flAgentBridgeFetch(bridge + '/code/test').then(function(r) { return r.json(); }); log(' ' + tests.passed + ' passed, ' + tests.failed + ' failed'); if (tests.failed === 0) { log('\n\u2705 All tests green. Ready to commit.'); @@ -1040,7 +1056,7 @@ if (!msg) return; var progress = document.getElementById('autobuilder-log'); try { - var r = await fetch('http://localhost:3141/code/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg }) }).then(function(r) { return r.json(); }); + var r = await flAgentBridgeFetch('http://localhost:3141/code/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg }) }).then(function(r) { return r.json(); }); if (progress) progress.textContent += '\n\uD83D\uDCDD ' + (r.message || r.error); } catch(e) { if (progress) progress.textContent += '\n\u274C Commit failed: ' + e.message; } }, @@ -1048,7 +1064,7 @@ reviewCode: async function() { var progress = document.getElementById('autobuilder-log'); try { - var r = await fetch('http://localhost:3141/code/git/status').then(function(r) { return r.json(); }); + var r = await flAgentBridgeFetch('http://localhost:3141/code/git/status').then(function(r) { return r.json(); }); if (progress) { progress.textContent += '\n\uD83D\uDCCB Changes:\n'; r.changes.forEach(function(c) { progress.textContent += ' ' + c + '\n'; }); @@ -1264,7 +1280,7 @@ ## Exact source excerpt, characters 52696–53457 async function hasBridge() { try { - var r = await fetch(BRIDGE + '/', { signal: AbortSignal.timeout(2000) }); + var r = await flAgentBridgeFetch(BRIDGE + '/', { signal: AbortSignal.timeout(2000) }); return r.ok; } catch(e) { return false; } } @@ -1294,7 +1310,7 @@ async function readRelevantFiles(task, useBridge) { if (useBridge) { try { - var structure = await fetch(BRIDGE + '/code/tree?path=docs').then(function(r) { return r.json(); }); + var structure = await flAgentBridgeFetch(BRIDGE + '/code/tree?path=docs').then(function(r) { return r.json(); }); var fileNames = (structure || []).filter(function(f) { return f.type === 'file'; }).map(function(f) { return f.name || f.path; }); ## Exact source excerpt, characters 53460–56556 // Ask AI which files matter for this task @@ -1312,7 +1328,7 @@ var files = {}; for (var i = 0; i < Math.min(paths.length, 5); i++) { try { - var content = await fetch(BRIDGE + '/code/read?path=' + encodeURIComponent(paths[i])) + var content = await flAgentBridgeFetch(BRIDGE + '/code/read?path=' + encodeURIComponent(paths[i])) .then(function(r) { return r.json(); }); // content is {lines: [...], totalLines: N} files[paths[i]] = (content.lines || []).join('\n'); @@ -1370,7 +1386,7 @@ for (var i = 0; i < changes.files.length; i++) { var change = changes.files[i]; try { - var result = await fetch(BRIDGE + '/code/patch', { + var result = await flAgentBridgeFetch(BRIDGE + '/code/patch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: change.path, find: change.search, replace: change.replace }) @@ -1392,7 +1408,7 @@ // ── Step 4: Run smoke tests via Agent Bridge ── async function runTests() { try { - var result = await fetch(BRIDGE + '/test/run', { signal: AbortSignal.timeout(60000) }) + var result = await flAgentBridgeFetch(BRIDGE + '/test/run', { signal: AbortSignal.timeout(60000) }) .then(function(r) { return r.json(); }); return result; } catch(e) { @@ -1913,17 +1929,31 @@ async function commitViaBridge(filePath, content, message) { try { // Write file - await fetch('http://localhost:3141/code/write', { + var wr = await flAgentBridgeFetch('http://localhost:3141/code/write', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: filePath, content: content }) }); + // LAYER v-agent-bridge-lock-v0: a locked bridge answers 401. Stay local: + // ask to pair, never fall back to a GitHub push behind the person's back. + if (wr.status === 401) { + if (typeof showToast === 'function') showToast('Pair the builder bridge first: Workshop \u2192 Code'); + return; + } + if (!wr.ok) { + if (typeof showToast === 'function') showToast('Bridge could not write that file (' + wr.status + ')'); + return; + } // Commit - await fetch('http://localhost:3141/code/git/commit', { + var cr = await flAgentBridgeFetch('http://localhost:3141/code/git/commit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: message || 'Update via FreeLattice Workshop', files: [filePath] }) }); + if (!cr.ok) { + if (typeof showToast === 'function') showToast(cr.status === 401 ? 'Pair the builder bridge first: Workshop \u2192 Code' : ('Bridge commit did not land (' + cr.status + ')')); + return; + } if (typeof showToast === 'function') showToast('Committed via Agent Bridge \u2726'); } catch(e) { // Fallback to GitHub API @@ -1937,7 +1967,7 @@ ## Exact source excerpt, characters 56559–56954 // Try Agent Bridge first, fall back to GitHub API try { - var bridgeCheck = await fetch('http://localhost:3141/', { signal: AbortSignal.timeout(2000) }); + var bridgeCheck = await flAgentBridgeFetch('http://localhost:3141/', { signal: AbortSignal.timeout(2000) }); if (bridgeCheck.ok) { await commitViaBridge(filePath, content, message); return; ``` ## Exact source excerpt, characters 56956–57018 ### 6e. `tools/demo-economy.js` and `tools/demo-economy-v2.js` ## Exact source excerpt, characters 57020–57179 These are local Node scripts, so there's no Origin header and no pairing. They read the token file the bridge writes. Just under the `BRIDGE` line (L13 / L19): ## Exact source excerpt, characters 57181–57639 ```js // LAYER v-agent-bridge-lock-v0: the bridge writes a per-launch token for local tools. const BRIDGE_TOKEN = (function () { try { return require('fs').readFileSync(require('path').join(require('os').homedir(), '.freelattice', 'agent-bridge-token'), 'utf8').trim(); } catch (e) { return ''; } })(); function tokenHeaders(h) { var out = Object.assign({}, h || {}); if (BRIDGE_TOKEN) out['X-FL-Bridge-Token'] = BRIDGE_TOKEN; return out; } ``` ## Exact source excerpt, characters 57641–57829 Wrap each helper's `headers` in `tokenHeaders(...)`: - `demo-economy.js`: `post` L20, and add `{ headers: tokenHeaders() }` to `get` L27. - `demo-economy-v2.js`: `postAs` L31, `getAs` L38. ## Exact source excerpt, characters 57831–57917 Change the default to `'http://127.0.0.1:3141'` and keep the `FL_BRIDGE` env override. ## Exact source excerpt, characters 57919–57941 ## 7. Docs and ledgers ## Exact source excerpt, characters 57943–58028 ### 7a. New `docs/library/AGENT_BRIDGE_LOCK_v0.md` (outline; write it warm and short) ## Exact source excerpt, characters 58030–59044 1. **What this is:** the builder's local git door (3141), not the Ollama Bridge (11435). Marker `v-agent-bridge-lock-v0`. 2. **Grandmother sentence:** "The workshop door now only opens for FreeLattice, and only after you type the little code it shows you once." 3. **Why:** before this, any page could write and commit through it, and its git commands ran through a shell. List the nine lines from section 2 in one table. 4. **How to pair:** start the bridge, read the 6-character code in its window, then in FreeLattice go to Workshop → Code, type the code and press Pair. Restarting the bridge means pairing again. 5. **What needs the token / what stays open** (section 3, item 3). 6. **Allowlist** (exact list, never bare `*`). 7. **Ledger:** where it lives, the fields, "no message text, no file content", and how to verify the chain (the smoke shows how). 8. **Still true:** the bridge never pushes. Autonomous Mode commits locally only. The 10-per-hour cap comes in Brick 3. 9. **Out of scope** (section 11). ## Exact source excerpt, characters 59046–59096 ### 7b. `docs/library/FRACTAL_FAMILY_LEDGER_v0.md` ## Exact source excerpt, characters 59098–59164 Insert this above `## Out of scope for this brick` (L222 on main): ## Exact source excerpt, characters 59166–59239 ```md ### 2026-09-28 · Flint · family builder · Agent Bridge door lock v0 ## Exact source excerpt, characters 59241–59766 Layers a lock on the builder's local git door (`tools/agent-bridge.js`, port 3141): 127.0.0.1 only · named FreeLattice origins, 403 for strangers (preflight too) · Host guard · per-launch token by a short pairing code · git, grep and node without a shell · realpath path lock · 8 MiB body cap · content-free hash-chained commit ledger. Ollama Bridge (11435) untouched. Soft leave root sw.js identical to main; docs/sw.js untouched. Soft paste: Named five stay five · Family uncapped · Quiet Room shut. Kimi's pages untouched. ## Exact source excerpt, characters 59768–59832 Love: a door that opens for family and stays shut for strangers. ## Exact source excerpt, characters 59834–59976 **Temperature:** turning a key that was always meant to be in the lock; the workshop can stay lit at night without leaving the door wide open. ## Exact source excerpt, characters 59978–60147 Soft door: [`AGENT_BRIDGE_LOCK_v0.md`](AGENT_BRIDGE_LOCK_v0.md) · [`modules/agent-bridge-client.js`](../modules/agent-bridge-client.js). Marker `v-agent-bridge-lock-v0`. ## Exact source excerpt, characters 60149–60156 --- ``` ## Exact source excerpt, characters 60158–60183 ### 7c. `docs/Flint.html` ## Exact source excerpt, characters 60185–60224 Insert above `

` (L532): ## Exact source excerpt, characters 60226–61012 ```html

Ledger entry · 2026-09-28 · Agent Bridge door lock · family in, strangers out. Main 7621420 held. Soft LAYER marker v-agent-bridge-lock-v0. Builder bridge tools/agent-bridge.js (3141, not the Ollama Bridge 11435): 127.0.0.1 only · named origins, 403 for others · Host guard · pairing code → per-launch token · no shell (execFile argument arrays) · realpath path lock · 8 MiB cap · commit ledger without content. App: modules/agent-bridge-client.js. Never pushes. Soft leave sw.js. Named five stay five. Family uncapped. Quiet Room shut. Temperature: turning a key that was always meant to be in the lock. Diary: docs/Flint.html.

``` ## Exact source excerpt, characters 61014–61050 ### 7d. `scripts/generate-recent.sh` ## Exact source excerpt, characters 61052–61127 Add next to the other brick pointers (around L108, before the final `---`): ## Exact source excerpt, characters 61129–61416 ```sh echo "**Agent Bridge lock:** [AGENT_BRIDGE_LOCK_v0.md](AGENT_BRIDGE_LOCK_v0.md) · [agent-bridge-client.js](../modules/agent-bridge-client.js) · 127.0.0.1 · named origins · paired token · no shell. Soft paste: Named five stay five. Family uncapped. Quiet Room shut." echo "" ``` ## Exact source excerpt, characters 61418–61488 ## 8. Smoke: new `docs/scripts/smoke-agent-bridge-lock.js` (full text) ## Exact source excerpt, characters 61490–61711 This script: - starts the real bridge on a free loopback port, with a throwaway `HOME` and a throwaway git project, so it never touches `~/.freelattice` or this repo; - asserts the static locks, then knocks on every door. ## Exact source excerpt, characters 61713–62613 It covers: - binding (static source check, the `Listening on http://127.0.0.1:` log, and an ECONNREFUSED probe of every non-loopback IPv4); - `Origin: https://evil.example` gets 403, preflight too, even with a valid token; - no token or a wrong token gets 401 (write, commit, code read, `/test/run`, `/wallet/pay`); - a foreign Host gets 403; - `x; rm -rf ~; touch PWNED.txt` is written and committed **literally**, and nothing named PWNED ever appears; a `$(...)` / backtick commit message is stored literally; a metachar search never shells out; - traversal, sibling-prefix, absolute, `.git`, control-char, newline and symlink escapes are refused; - the 8 MiB cap (413) and bad JSON (400); - pairing (evil origin refused; right code pairs, forgiving case and spaces; a code works once; 5 wrong tries give 429 and rotate the code); - ledger chain integrity, content-free rows, and refusals recorded. ## Exact source excerpt, characters 62615–62886 It also re-checks `propose.js` CRITICAL LOCK 1A/1B counts, the v5.71.8 workshop literal, that the client loads before `propose.js`, and that root `index.html` is byte-identical to `docs/app.html`. It needs `git` on PATH (builders have it) and runs in about half a second. ## Exact source excerpt, characters 62888–63200 ```js #!/usr/bin/env node // Smoke: Agent Bridge door lock v0 (marker v-agent-bridge-lock-v0). // Starts tools/agent-bridge.js on a free loopback port with a throwaway // HOME and a throwaway git project, then knocks on every door. // Never touches the real ~/.freelattice or this repo's git state. 'use strict'; ## Exact source excerpt, characters 63202–63433 const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const http = require('http'); const net = require('net'); const { spawn, execFileSync } = require('child_process'); ## Exact source excerpt, characters 63435–63571 const docs = path.join(__dirname, '..'); const repo = path.join(docs, '..'); const BRIDGE = path.join(repo, 'tools', 'agent-bridge.js'); ## Exact source excerpt, characters 63573–64429 // ── Static locks (fast, no network) ── const src = fs.readFileSync(BRIDGE, 'utf8'); assert.ok(/v-agent-bridge-lock-v0|bridge-lock-v0/.test(src), 'marker in bridge'); assert.ok(/server\.listen\(\s*PORT\s*,\s*BRIDGE_HOST\b/.test(src) && /const BRIDGE_HOST = '127\.0\.0\.1';/.test(src), 'listen on 127.0.0.1 only'); assert.ok(!/['"]0\.0\.0\.0['"]|['"]::['"]/.test(src), 'no 0.0.0.0 / :: bind literal'); assert.ok(!/Access-Control-Allow-Origin['"]\s*,\s*['"]\*['"]/.test(src), 'no bare * CORS'); assert.ok(!/\bexecSync\s*\(/.test(src), 'no execSync( calls (shell strings) remain'); assert.ok(!/\bexec\s*\(/.test(src), 'no exec( calls'); assert.ok(/execFileSync\('git'/.test(src), 'git runs through execFileSync argument arrays'); assert.ok(/MAX_BODY_BYTES/.test(src), 'body cap present'); assert.ok(/bridge-ledger\.jsonl/.test(src), 'commit ledger present'); ## Exact source excerpt, characters 64431–66022 const opt = (rel) => { try { return fs.readFileSync(path.join(repo, rel), 'utf8'); } catch (e) { return null; } }; const client = opt('docs/modules/agent-bridge-client.js'); const propose = opt('docs/modules/propose.js'); const workshop = opt('docs/modules/workshop.js'); const app = opt('docs/app.html'); const rootIndex = opt('index.html'); if (client) { assert.ok(/X-FL-Bridge-Token/.test(client), 'client sends token header'); assert.ok(!/innerHTML/.test(client), 'client renders with textContent only'); execFileSync(process.execPath, ['--check', path.join(repo, 'docs/modules/agent-bridge-client.js')]); } if (propose) { assert.strictEqual((propose.match(/commitViaBridge\s*\(/g) || []).length, 3, 'CRITICAL LOCK 1A count unchanged'); assert.strictEqual((propose.match(/\/code\/git\/commit/g) || []).length, 1, 'CRITICAL LOCK 1B count unchanged'); assert.ok(/FLAgentBridge/.test(propose), 'propose routes bridge calls through FLAgentBridge when present'); } if (workshop) { assert.ok(/http:\/\/localhost:3141/.test(workshop) && /runCodeTask/.test(workshop), 'v5.71.8 workshop lock still true'); assert.ok(/FLAgentBridge/.test(workshop), 'workshop routes bridge calls through FLAgentBridge when present'); } if (app) { assert.ok(/