FreeLattice · Celeste reading home

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

Archive pages whose extracted text matched this section: 980, 981. Unmatched pages stay on the source record with the checked page method. They are not guessed into a section.

+  // 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.