feat: helper locks GBRAIN_DATABASE_URL at startup, defends against config rewrites

The wireup helper previously read ~/.gbrain/config.json on every gbrain
subprocess invocation. On Garry's Mac, multiple concurrent test runs and
agent integrations were rewriting that file mid-sync, redirecting the
wireup at the wrong brain partway through a 4-min initial import.

This commit adds a `--database-url <url>` flag to the helper and locks
the URL at startup. Precedence:
  1. --database-url flag                       (explicit caller intent)
  2. GBRAIN_DATABASE_URL / DATABASE_URL env    (CI / manual override)
  3. read once from ~/.gbrain/config.json      (default)

Whichever wins gets exported as GBRAIN_DATABASE_URL for every child
`gbrain` invocation. Per gbrain's loadConfig at src/core/config.ts:53,
env-var URLs override the file URL — so a process that flips config.json
between two of our gbrain calls can't redirect us. Defense-in-depth:
once the URL is locked, the wireup completes against the original brain
even under hostile filesystem conditions.

setup-gbrain/SKILL.md.tmpl Step 7 now reads the URL out of config.json
once (via python3 inline) and passes it explicitly with --database-url,
so even the very first wireup call is decoupled from config.json mutability.

Three new test cases cover the lock behavior:
  - --database-url flag is exported to child gbrain calls
  - falls back to ~/.gbrain/config.json when no flag and no env
  - flag overrides env GBRAIN_DATABASE_URL and config.json values

The fake gbrain in the test suite now records GBRAIN_DATABASE_URL alongside
each call so tests can assert the helper exported the locked URL.

Total test count: 13 → 16 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-25 23:13:23 -07:00
parent 570258ae6f
commit e36ff7dd4c
5 changed files with 152 additions and 10 deletions
+74 -1
View File
@@ -42,7 +42,12 @@ function makeFakeGbrain(opts: {
const script = `#!/bin/bash
LOG="${gbrainCallLog}"
STATE="${gbrainStateFile}"
echo "gbrain $@" >> "$LOG"
# Record the call AND any GBRAIN_DATABASE_URL that the parent passed via env.
# Format: "gbrain <args> [GBRAIN_DATABASE_URL=<url>]" so tests can assert
# the wireup helper exported the locked URL into our env.
LINE="gbrain $@"
[ -n "\${GBRAIN_DATABASE_URL:-}" ] && LINE="\$LINE [GBRAIN_DATABASE_URL=\$GBRAIN_DATABASE_URL]"
echo "\$LINE" >> "$LOG"
# --version
if [ "$1" = "--version" ]; then
@@ -286,6 +291,74 @@ describe('gstack-gbrain-source-wireup — wireup mode', () => {
});
});
describe('gstack-gbrain-source-wireup — --database-url lock (defends against external config rewrites)', () => {
test('--database-url flag is exported as GBRAIN_DATABASE_URL to child gbrain calls', () => {
setupGstackRepo('git@github.com:user/gstack-brain-user.git');
makeFakeGbrain({});
const TARGET = 'postgresql://postgres.abc:pw@aws.pooler.supabase.com:5432/postgres';
const r = run(['--database-url', TARGET], { env: { GSTACK_BRAIN_NO_SYNC: '1' } });
expect(r.status).toBe(0);
const calls = gbrainCalls();
// every gbrain invocation should carry the locked URL
const writingCalls = calls.filter((c) => c.includes('sources') || c.includes('sync'));
expect(writingCalls.length).toBeGreaterThan(0);
for (const c of writingCalls) {
expect(c).toContain(`[GBRAIN_DATABASE_URL=${TARGET}]`);
}
});
test('falls back to ~/.gbrain/config.json database_url when no flag and no env', () => {
setupGstackRepo('git@github.com:user/gstack-brain-user.git');
makeFakeGbrain({});
const FILE_URL = 'postgresql://postgres.xyz:pw@aws.pooler.supabase.com:5432/postgres';
fs.mkdirSync(path.join(tmpHome, '.gbrain'), { recursive: true });
fs.writeFileSync(
path.join(tmpHome, '.gbrain', 'config.json'),
JSON.stringify({ engine: 'postgres', database_url: FILE_URL })
);
// Important: don't pass GBRAIN_DATABASE_URL or DATABASE_URL in env; helper
// should read from $HOME/.gbrain/config.json (HOME is tmpHome here).
const r = run([], {
env: {
GSTACK_BRAIN_NO_SYNC: '1',
GBRAIN_DATABASE_URL: '',
DATABASE_URL: '',
},
});
expect(r.status).toBe(0);
const calls = gbrainCalls();
const writingCalls = calls.filter((c) => c.includes('sources add'));
expect(writingCalls.length).toBe(1);
expect(writingCalls[0]).toContain(`[GBRAIN_DATABASE_URL=${FILE_URL}]`);
});
test('--database-url overrides env GBRAIN_DATABASE_URL and config.json', () => {
setupGstackRepo('git@github.com:user/gstack-brain-user.git');
makeFakeGbrain({});
const FLAG_URL = 'postgresql://postgres.flag:pw@a.b:5432/postgres';
const ENV_URL = 'postgresql://postgres.env:pw@x.y:5432/postgres';
const FILE_URL = 'postgresql://postgres.file:pw@p.q:5432/postgres';
fs.mkdirSync(path.join(tmpHome, '.gbrain'), { recursive: true });
fs.writeFileSync(
path.join(tmpHome, '.gbrain', 'config.json'),
JSON.stringify({ engine: 'postgres', database_url: FILE_URL })
);
const r = run(['--database-url', FLAG_URL], {
env: {
GSTACK_BRAIN_NO_SYNC: '1',
GBRAIN_DATABASE_URL: ENV_URL,
},
});
expect(r.status).toBe(0);
const calls = gbrainCalls();
const writingCalls = calls.filter((c) => c.includes('sources add'));
expect(writingCalls.length).toBe(1);
expect(writingCalls[0]).toContain(`[GBRAIN_DATABASE_URL=${FLAG_URL}]`);
expect(writingCalls[0]).not.toContain(ENV_URL);
expect(writingCalls[0]).not.toContain(FILE_URL);
});
});
describe('gstack-gbrain-source-wireup — uninstall mode', () => {
test('after wireup: removes source + worktree', () => {
setupGstackRepo('git@github.com:user/gstack-brain-user.git');