Public, free to read, reuse, and adapt — Kirk Patrick Miller's notice for this edition. Not a patent grant.
FLINT_HEAL_AGENT_BRIDGE_ENV_v0.1.1_tiny.txt · c02
5e76fcf9e7 · words 1525 · characters 12966–25684
Previous section · Next section
Exact section text · Full source · Source index
- Continuity — explicit wording in the guide row
+ // T16b LAYER v-agent-bridge-env-heal-v0.1.1: a commit with no file list never carries secrets
+ // unless the mind was given 'secrets'. Filtered, never refused. (proj2 has no .gitignore on purpose.)
+ const SECRETS = ['.env', '.env.production', 'config/.env.local', 'keys/dev.pem', 'id_ed25519'];
+ const g2 = (args) => execFileSync('git', args, { cwd: proj2, encoding: 'utf8' });
+ const inHead = () => g2(['ls-tree', '-r', '--name-only', 'HEAD']).split('\n').filter(Boolean);
+ const staged2 = () => g2(['diff', '--cached', '--name-only']).split('\n').filter(Boolean);
+ fs.mkdirSync(path.join(proj2, 'config'), { recursive: true });
+ fs.mkdirSync(path.join(proj2, 'keys'), { recursive: true });
+ for (const f of SECRETS) fs.writeFileSync(path.join(proj2, f), 'SECRET=' + f + '\n');
+ const head0 = g2(['rev-parse', 'HEAD']).trim();
+ // (a) only secrets changed, no secrets scope (local tools token): nothing committed, 200, all held.
+ r = await req(port, { method: 'POST', path: '/code/git/commit', headers: withTok({ Origin: GOOD }) }, { message: 'sweep, secrets only', root: 'second' });
+ assert.strictEqual(r.status, 200, 'secrets-only sweep answers 200 (never a gate)');
+ assert.strictEqual(r.json.commit, null, 'secrets-only sweep commits nothing');
+ assert.deepStrictEqual(r.json.heldBack.slice().sort(), SECRETS.slice().sort(), 'every secret-shaped file named as kept');
+ assert.ok(/Kept on this computer, not committed/.test(r.json.message), 'plain kept line');
+ assert.strictEqual(g2(['rev-parse', 'HEAD']).trim(), head0, 'HEAD unchanged');
+ assert.deepStrictEqual(staged2(), [], 'nothing left staged');
+ for (const f of SECRETS) assert.ok(fs.existsSync(path.join(proj2, f)), f + ' still on disk');
+ // (b) normal work flows: no file list, no secrets scope -> notes committed, secrets stay unstaged.
+ fs.writeFileSync(path.join(proj2, 'notes.txt'), 'normal work\n');
+ r = await req(port, { method: 'POST', path: '/code/git/commit', headers: withTok({ Origin: GOOD }) }, { message: 'sweep with work', root: 'second' });
+ assert.strictEqual(r.status, 200, 'sweep with work commits');
+ assert.ok(/^[a-f0-9]{40}$/.test(r.json.commit), 'sweep returns a sha');
+ assert.ok(inHead().includes('notes.txt'), 'normal file committed');
+ for (const f of SECRETS) assert.ok(!inHead().includes(f), f + ' not committed without secrets');
+ assert.deepStrictEqual(staged2(), [], 'secrets left unstaged, not staged');
+ // (c) a folder path sweeps too: still filtered.
+ fs.writeFileSync(path.join(proj2, 'config', 'app.json'), '{}\n');
+ r = await req(port, { method: 'POST', path: '/code/git/commit', headers: withTok({ Origin: GOOD }) }, { message: 'folder sweep', files: ['config'], root: 'second' });
+ assert.strictEqual(r.status, 200, 'folder commit ok');
+ assert.ok(inHead().includes('config/app.json') && !inHead().includes('config/.env.local'), 'folder commit keeps .env.local out');
+ // (d) something already staged by hand is unstaged, not committed.
+ g2(['add', '--', '.env']);
+ fs.writeFileSync(path.join(proj2, 'notes.txt'), 'more work\n');
+ r = await req(port, { method: 'POST', path: '/code/git/commit', headers: withTok({ Origin: GOOD }) }, { message: 'pre-staged', files: ['notes.txt'], root: 'second' });
+ assert.strictEqual(r.status, 200, 'pre-staged commit ok');
+ assert.ok(!inHead().includes('.env'), 'hand-staged .env not committed without secrets');
+ assert.deepStrictEqual(staged2(), [], 'hand-staged .env unstaged');
+ // (e) with the permission (P1 was given secrets in T11): the same sweep carries them.
+ r = await req(port, { method: 'POST', path: '/code/git/commit', headers: as(P1, { Origin: GOOD }) }, { message: 'sweep with secrets scope', root: 'second' });
+ assert.strictEqual(r.status, 200, 'secrets-scope sweep commits');
+ assert.deepStrictEqual(r.json.heldBack, [], 'nothing held for a mind with secrets');
+ for (const f of SECRETS) assert.ok(inHead().includes(f), f + ' committed by the mind given secrets');
+ // (f) tracked .env changed later, no secrets scope: the change stays unstaged, HEAD keeps the old one.
+ fs.writeFileSync(path.join(proj2, '.env'), 'SECRET=changed\n');
+ fs.writeFileSync(path.join(proj2, 'notes.txt'), 'even more work\n');
+ r = await req(port, { method: 'POST', path: '/code/git/commit', headers: withTok({ Origin: GOOD }) }, { message: 'tracked secret changed', root: 'second' });
+ assert.strictEqual(r.status, 200, 'tracked-secret sweep commits the rest');
+ assert.deepStrictEqual(r.json.heldBack, ['.env'], 'tracked .env change held');
+ assert.strictEqual(g2(['show', 'HEAD:.env']), 'SECRET=.env\n', 'HEAD keeps the old .env');
+ assert.ok(g2(['diff', '--name-only']).split('\n').includes('.env'), '.env change still waiting, unstaged');
+ console.log('SMOKE_OK agent bridge env heal v0.1.1');
+
// T17 local tools can be switched off and on again.
assert.strictEqual(cli(env, proj, ['--untrust-local-tools']).status, 0, '--untrust-local-tools ok');
assert.ok(!fs.existsSync(tokenFile), 'token file removed');
@@ -438,6 +502,9 @@ function cli(env, cwd, args) {
assert.ok(rows.some((x) => x.kind === 'pair' && x.result === 'ok' && x.device === P1id), 'pairing ledgered with device id');
assert.ok(rows.some((x) => x.result === 'revoked' && x.device === P2id), 'command-line revoke ledgered');
assert.ok(rows.some((x) => x.result === 'revoked-all'), 'revoke-all ledgered');
+ assert.ok(rows.some((x) => x.result === 'held:secrets-only'), 'secrets-only sweep ledgered (v0.1.1)');
+ assert.ok(rows.some((x) => x.result === 'ok:held-secrets' && /^[a-f0-9]{40}$/.test(x.commitSha)), 'held-secrets commit ledgered (v0.1.1)');
+ assert.ok(!ledgerText.includes('.env.production') && !ledgerText.includes('SECRET='), 'ledger never names or holds secrets (v0.1.1)');
await b.stop();
// T21 ephemeral opt-in: FL_BRIDGE_EPHEMERAL=1 forgets paired minds on restart.
diff --git a/tools/agent-bridge.js b/tools/agent-bridge.js
index 0d21eeb..90dab49 100755
--- a/tools/agent-bridge.js
+++ b/tools/agent-bridge.js
@@ -431,6 +431,44 @@ function runGit(args, root) {
return execFileSync('git', args, { cwd: root || projectRootReal(), encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000 });
}
+// ═══════════════════════════════════════════════════
+// LAYER v-agent-bridge-env-heal-v0.1.1 (2026-09-28): secrets stay on this computer.
+// A commit (with or without a file list) never carries a secret-shaped file
+// unless this mind was given 'secrets'. We filter the staged set after
+// `git add`; we never refuse the commit, so normal work keeps flowing
+// (AUTONOMY.md Principle 1: no gates on local work). git runs through
+// execFileSync argument arrays only. The bridge still never pushes.
+// ═══════════════════════════════════════════════════
+const SECRET_FILE_PATTERNS = [
+ /^\.env$/i, /^\.env\..+$/i, // .env, .env.local, .env.production (the family lockedPath guards)
+ /^id_(rsa|dsa|ecdsa|ed25519)$/i, // SSH private keys (the .pub halves are fine)
+ /\.(pem|key|p12|pfx|keystore|jks)$/i, // private keys and key stores
+ /^\.(npmrc|netrc|pypirc)$/i, // registry and login tokens
+ /^agent-bridge-(token|trusted\.json)$/i // this bridge's own trust files, if ever copied into a project
+];
+
+function isSecretPath(rel) {
+ var base = String(rel || '').split(/[\\/]/).pop();
+ return SECRET_FILE_PATTERNS.some(function (re) { return re.test(base); });
+}
+
+function gitHasHead(root) {
+ try { runGit(['rev-parse', '--verify', '-q', 'HEAD'], root); return true; } catch (e) { return false; }
+}
+
+function gitStagedPaths(root) {
+ return runGit(['diff', '--cached', '--name-only', '-z', '--no-renames'], root).split('\0').filter(Boolean);
+}
+
+/** Unstage secret-shaped files. Returns the list that stayed behind (never their contents). */
+function holdBackSecrets(root) {
+ var held = gitStagedPaths(root).filter(isSecretPath);
+ if (!held.length) return [];
+ if (gitHasHead(root)) runGit(['reset', '-q', 'HEAD', '--'].concat(held), root);
+ else runGit(['rm', '--cached', '-q', '--'].concat(held), root);
+ return held;
+}
+
function runSmokeFile(timeoutMs, root) {
// No shell: node binary + fixed script path. stdout and stderr joined.
var r = spawnSync(process.execPath, ['tests/smoke.js'], {
@@ -1844,10 +1882,19 @@ function route(url, method, data, res, actingAs, meta) {
lg.messageSha256 = sha256Hex(data.message);
try {
runGit(['add', '--'].concat(rels), PROJECT_ROOT);
+ // LAYER v-agent-bridge-env-heal-v0.1.1: secret-shaped files stay unstaged unless this mind has 'secrets'.
+ var heldBack = SECRETS_OK ? [] : holdBackSecrets(PROJECT_ROOT);
+ var keptLine = heldBack.length
+ ? ' Kept on this computer, not committed: ' + heldBack.slice(0, 5).join(', ') + (heldBack.length > 5 ? ' and ' + (heldBack.length - 5) + ' more' : '') + '.'
+ : '';
+ if (heldBack.length && !gitStagedPaths(PROJECT_ROOT).length) {
+ lg.result = 'held:secrets-only'; ledgerAppend(lg);
+ return respond(res, 200, { message: 'Nothing to commit.' + keptLine, commit: null, heldBack: heldBack, root: ROOT_NAME });
+ }
runGit(['commit', '-m', data.message], PROJECT_ROOT);
var headSha = runGit(['rev-parse', 'HEAD'], PROJECT_ROOT).trim();
- lg.result = 'ok'; lg.commitSha = headSha; ledgerAppend(lg);
- return respond(res, 200, { message: 'Committed: ' + data.message, commit: headSha, root: ROOT_NAME });
+ lg.result = heldBack.length ? 'ok:held-secrets' : 'ok'; lg.commitSha = headSha; ledgerAppend(lg);
+ return respond(res, 200, { message: 'Committed: ' + data.message + keptLine, commit: headSha, heldBack: heldBack, root: ROOT_NAME });
} catch(e) {
lg.result = 'error'; ledgerAppend(lg);
return respond(res, 500, { error: 'Commit failed: ' + String((e.stderr || e.message || '')).slice(0, 400) });
----- END DIFF -----
What T16b proves (live, real git, real files):
(a) only secrets changed, no secrets scope: 200, commit null, heldBack names all 5, HEAD unchanged,
nothing staged, every file still on disk.
(b) no file list with normal work: notes.txt committed; .env, .env.production, config/.env.local,
keys/dev.pem, id_ed25519 not in HEAD and not staged.
(c) files: ['config'] (a folder): config/app.json committed, config/.env.local kept out.
(d) .env staged by hand in a terminal, then a bridge commit of notes.txt: .env unstaged, not committed.
(e) the same sweep from the mind given secrets: all 5 committed, heldBack [].
(f) a tracked .env changed later, no secrets scope: the rest commits, heldBack ['.env'], HEAD keeps the
old .env, the change still waits unstaged.
Ledger: rows with 'held:secrets-only' and 'ok:held-secrets' (with a commit sha); the ledger never
contains '.env.production' or 'SECRET='. The whole hash chain still verifies.
Fails without the fix: on main's agent-bridge.js the smoke stops at "FAIL secrets-only sweep commits nothing".
======================================================================
5. ACCEPTANCE (paste the output into the PR body)
======================================================================
node docs/scripts/smoke-agent-bridge-lock.js
-> SMOKE_OK agent bridge env heal v0.1.1
-> SMOKE_OK agent bridge lock v0.1
node tests/smoke.js -> ALL 3597 CHECKS PASSED (unchanged)
for f in docs/scripts/smoke-*.js; do node "$f" >/dev/null 2>&1 && echo "ok $f" || echo "FAIL $f"; done
-> 53 ok, and only the five that already fail on main: smoke-carry-forward, smoke-connect-card-yesno-wizard,
smoke-fable-tech, smoke-phone-shine, smoke-proof-60s (install.html pointers and the old home wizard;
not this brick)
git diff --name-only origin/main -> exactly: .gitignore, docs/library/AGENT_BRIDGE_LOCK_v0.md,
docs/library/FRACTAL_FAMILY_LEDGER_v0.md, docs/scripts/smoke-agent-bridge-lock.js, tools/agent-bridge.js
git diff origin/main -- sw.js docs/sw.js index.html docs/app.html -> empty
git diff origin/main | grep '^+' | grep -c $'\u2014' -> 0 (no em dashes added)
grep -n "server.listen(PORT, BRIDGE_HOST" tools/agent-bridge.js -> still there
Manual one-minute check (optional): start the bridge in a scratch repo with a .env and a changed file,
curl -X POST -H "X-FL-Bridge-Token: $(cat ~/.freelattice/agent-bridge-token)" -H 'Content-Type: application/json' \
-d '{"message":"sweep"}' http://127.0.0.1:3141/code/git/commit
-> "Committed: sweep Kept on this computer, not committed: .env." and git show --stat HEAD has no .env.