// Paired minds: who is trusted, with a Revoke button per row. No confirm dialog: // revoking is Kirk's own local choice, and pairing again takes seconds. function renderPairedList(container) { if (!container) return; var old = container.querySelector('.fl-bridge-minds'); var box = el('details', null, 'margin:8px 0;padding:8px 10px;border:1px solid rgba(255,255,255,0.12);border-radius:10px;'); box.className = 'fl-bridge-minds'; if (old) { box.open = old.open; container.replaceChild(box, old); } else container.appendChild(box); var summary = el('summary', 'Paired minds', 'cursor:pointer;'); box.appendChild(summary); var list = el('div', null, 'margin-top:6px;'); var msg = el('div', null, 'margin-top:6px;opacity:0.8;'); box.appendChild(list); box.appendChild(msg); list.appendChild(el('div', 'Looking\u2026', 'opacity:0.7;')); ## Exact source excerpt, characters 79125–82733 listMinds().then(function (r) { while (list.firstChild) list.removeChild(list.firstChild); if (!r.ok) { list.appendChild(el('div', (r.data && r.data.error) || ('HTTP ' + r.status))); return; } var minds = (r.data && r.data.minds) || []; summary.textContent = 'Paired minds (' + minds.length + ')' + (r.data.persistent === false ? ' \u00B7 ephemeral' : ''); minds.forEach(function (m) { var row = el('div', null, 'padding:6px 0;border-top:1px solid rgba(255,255,255,0.08);'); var title = el('div', null); title.appendChild(el('strong', m.name || 'A trusted mind')); if (m.you) title.appendChild(el('span', ' (this browser)', 'opacity:0.7;')); if (m.kind === 'cli') title.appendChild(el('span', ' (trusted by default)', 'opacity:0.7;')); row.appendChild(title); row.appendChild(el('div', (m.origin || 'no web origin') + ' \u00B7 paired ' + when(m.createdAt) + ' \u00B7 last used ' + when(m.lastUsedAt), 'opacity:0.75;font-size:0.9em;')); var hasSecrets = (m.scopes || []).indexOf('secrets') !== -1; row.appendChild(el('div', 'May: ' + (m.scopes || []).join(', '), 'opacity:0.75;font-size:0.9em;')); var revokeBtn = el('button', 'Revoke', 'margin:4px 6px 0 0;'); revokeBtn.type = 'button'; revokeBtn.addEventListener('click', function () { revokeBtn.disabled = true; revoke(m.id).then(function (res) { msg.textContent = res.ok ? ('Revoked ' + (m.name || m.id) + '.') : ((res.data && res.data.error) || 'Could not revoke.'); if (res.ok && res.data && res.data.you) { renderPairCard(container); box.parentNode && box.parentNode.removeChild(box); return; } renderPairedList(container); }); }); var secretsBtn = el('button', hasSecrets ? 'Stop .env access' : 'Allow .env files', 'margin:4px 6px 0 0;'); secretsBtn.type = 'button'; secretsBtn.addEventListener('click', function () { secretsBtn.disabled = true; setScopes(m.id, hasSecrets ? [] : ['secrets'], hasSecrets ? ['secrets'] : []).then(function (res) { msg.textContent = res.ok ? ((m.name || m.id) + ' may: ' + ((res.data && res.data.scopes) || []).join(', ')) : ((res.data && res.data.error) || 'Could not change that.'); renderPairedList(container); }); }); row.appendChild(revokeBtn); row.appendChild(secretsBtn); list.appendChild(row); }); var foot = el('div', null, 'margin-top:8px;'); var allBtn = el('button', 'Revoke all paired devices', 'margin:0 6px 0 0;'); allBtn.type = 'button'; allBtn.addEventListener('click', function () { allBtn.disabled = true; revokeAll().then(function (res) { msg.textContent = res.ok ? ('Revoked ' + ((res.data && res.data.revoked) || 0) + ' device(s). Local tools stay trusted.') : ((res.data && res.data.error) || 'Could not revoke.'); setToken(''); renderPairCard(container); if (box.parentNode) box.parentNode.removeChild(box); }); }); var forgetBtn = el('button', 'Forget on this browser', 'margin:0 6px 0 0;'); forgetBtn.type = 'button'; forgetBtn.addEventListener('click', function () { setToken(''); msg.textContent = 'This browser forgot its token. It stays listed until you revoke it.'; renderPairCard(container); }); foot.appendChild(allBtn); foot.appendChild(forgetBtn); list.appendChild(foot); }); } ## Exact source excerpt, characters 82735–83270 window.FLAgentBridge = { BASE: BASE, TOKEN_KEY: TOKEN_KEY, NEEDS_PAIR_EVENT: NEEDS_PAIR_EVENT, CHANGED_EVENT: CHANGED_EVENT, fetch: bridgeFetch, pair: pair, status: status, whoami: whoami, listMinds: listMinds, revoke: revoke, revokeAll: revokeAll, setScopes: setScopes, hasToken: function () { return !!getToken(); }, forget: function () { setToken(''); }, defaultName: defaultName, renderPairCard: renderPairCard, renderPairedList: renderPairedList }; })(); ``` ## Exact source excerpt, characters 83272–83333 ### 6b. `docs/app.html` (then byte-copy to root `index.html`) ## Exact source excerpt, characters 83335–83410 Right above L68597 ``, add: ## Exact source excerpt, characters 83412–83604 ```html ``` ## Exact source excerpt, characters 83606–83783 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 L39741 hits `/`, which stays open. ## Exact source excerpt, characters 83785–83872 ### 6c. `docs/modules/propose.js` (exact diff; CRITICAL LOCK 1A/1B counts stay 3 and 1) ## Exact source excerpt, characters 83874–84073 ```diff --- a/docs/modules/propose.js +++ b/docs/modules/propose.js @@ -577,13 +577,22 @@ // on draft state and one (approve) is also gated on smokeStatus. var BRIDGE = 'http://localhost:3141'; ## Exact source excerpt, characters 84076–84731 + // 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 84734–85040 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 85043–85339 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 85342–85638 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 85640–85687 ### 6d. `docs/modules/workshop.js` (exact diff) ## Exact source excerpt, characters 85689–86234 - 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'`, shows which scope is missing when the bridge answers 403 with `scope`, and shows Paired minds after "Connected". - `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 86236–87019 ```diff --- a/docs/modules/workshop.js +++ b/docs/modules/workshop.js @@ -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 87022–88832 @@ -958,11 +967,26 @@ 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; + } + if (git && git.scope) { + // LAYER v-agent-bridge-lock-v0 · v0.1: paired, but this device was not given that scope. + el.textContent = '\uD83D\uDFE1 Bridge paired \u00B7 this device may not ' + git.scope + ' here (see Paired minds)'; + el.style.color = '#e8b019'; + return; + } el.textContent = '\uD83D\uDFE2 Connected \u00B7 ' + git.branch + ' \u00B7 ' + (git.recentCommits[0] || ''); el.style.color = '#4aff9f'; + // LAYER v-agent-bridge-lock-v0 · v0.1: who is trusted, with a Revoke button per row. + if (window.FLAgentBridge && el.parentNode && typeof window.FLAgentBridge.renderPairedList === 'function') window.FLAgentBridge.renderPairedList(el.parentNode); } catch(e) { // No scary error. Gentle guidance. el.textContent = 'For local builds, run: node tools/agent-bridge.js'; @@ -983,7 +1007,7 @@ ## Exact source excerpt, characters 88835–89313 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 89316–93324