A3: keep a project's DATABASE_URL away from a PGLite brain

With a PGLite config, buildGbrainEnv returned the caller env unchanged, so
a project's .env DATABASE_URL (autoloaded by Bun) reached gbrain: a false
broken-db at best, gbrain talking to the app's own Postgres at worst.
Inherited DATABASE_URL and GBRAIN_DATABASE_URL are now blanked for PGLite
configs (GSTACK_RESPECT_ENV_DATABASE_URL=1 still keeps them), and every
gbrain child runs from gbrain's config dir instead of the user's project,
so it cannot reload the project's .env / .env.local.

The old test that pinned pass-through for an engine:pglite config is
migrated: pass-through now applies only to configs that name neither a
database_url nor PGLite.

Co-authored-by: maxpetrusenkoagent <max.petrusenko.agent@gmail.com>
This commit is contained in:
garrytanandmaxpetrusenkoagent committed 2026-10-03 20:41:22 +00:00
1 parent 7e2edf2a3a
commit f20cd9a3a3
4 files changed
+109 -10

No files matched your search

+33 -6
View File
@@ -35,11 +35,13 @@
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import { homedir } from "os";
import { homedir, tmpdir } from "os";
import { spawnSync, spawn, execFileSync, type SpawnSyncReturns, type ChildProcess, type SpawnOptions } from "child_process";
interface GbrainConfig {
database_url?: string;
engine?: string;
database_path?: string;
}
export interface BuildGbrainEnvOptions {
@@ -95,9 +97,15 @@ export function gbrainConfigDir(env: NodeJS.ProcessEnv = process.env): string {
* unchanged when:
* - `GSTACK_RESPECT_ENV_DATABASE_URL=1` (intentional opt-out),
* - the config file is missing or unparseable,
* - the config has no `database_url`,
* - the config has no `database_url` and is not a PGLite config,
* - the caller already set DATABASE_URL to the same value.
*
* A PGLite config has no URL to seed, so an inherited DATABASE_URL /
* GBRAIN_DATABASE_URL (Bun autoloads a project's `.env`) would make gbrain
* connect to the app's own database instead of its file brain (#1917). Both
* are blanked, not deleted: gbrain treats an empty value as unset, and an
* existing (empty) variable is never refilled by a child's dotenv autoload.
*
* GBRAIN_PREPARE is never set here (#1965): gbrain auto-disables prepared
* statements on transaction-mode poolers itself, and forcing them on breaks
* every write with "prepared statement does not exist". A caller-set
@@ -122,7 +130,13 @@ export function buildGbrainEnv(opts: BuildGbrainEnvOptions = {}): NodeJS.Process
} catch {
return out;
}
if (!cfg.database_url) return out;
if (!cfg.database_url) {
if (cfg.engine === "pglite" || cfg.database_path) {
out.DATABASE_URL = "";
out.GBRAIN_DATABASE_URL = "";
}
return out;
}
const hadCaller = baseEnv.DATABASE_URL !== undefined;
const alreadyMatch = baseEnv.DATABASE_URL === cfg.database_url;
@@ -249,6 +263,19 @@ export function gbrainInvocation(args: string[]): { cmd: string; argv: string[];
: { cmd: "gbrain", argv: args, shell: false };
}
/**
* Working directory for a gbrain child (#1917). gbrain (a Bun program) and
* its own config loader read `.env` / `.env.local` from the working
* directory, so a child started inside the user's project can pick the
* project's DATABASE_URL back up. Children run from gbrain's config
* directory (or the OS temp dir before init) unless the caller names a cwd.
*/
export function gbrainChildCwd(cwd?: string, env: NodeJS.ProcessEnv = process.env): string {
if (cwd) return cwd;
const dir = gbrainConfigDir(env);
return existsSync(dir) ? dir : tmpdir();
}
export interface SpawnGbrainOptions {
/** Timeout in milliseconds. Defaults to 30s. */
timeout?: number;
@@ -276,7 +303,7 @@ export function spawnGbrain(args: string[], opts: SpawnGbrainOptions = {}): Spaw
return spawnSync(inv.cmd, inv.argv, {
encoding: "utf-8",
timeout: opts.timeout ?? 30_000,
cwd: opts.cwd,
cwd: gbrainChildCwd(opts.cwd, opts.baseEnv),
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
env: buildGbrainEnv({ baseEnv: opts.baseEnv, announce: opts.announce }),
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
@@ -311,7 +338,7 @@ export function spawnGbrainAsync(
const inv = gbrainInvocation(args);
return spawn(inv.cmd, inv.argv, {
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
cwd: opts.cwd,
cwd: gbrainChildCwd(opts.cwd, opts.baseEnv),
env: buildGbrainEnv({ baseEnv: opts.baseEnv, announce: false }),
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
@@ -326,7 +353,7 @@ export function execGbrainText(args: string[], opts: SpawnGbrainOptions = {}): s
return execFileSync(inv.cmd, inv.argv, {
encoding: "utf-8",
timeout: opts.timeout ?? 30_000,
cwd: opts.cwd,
cwd: gbrainChildCwd(opts.cwd, opts.baseEnv),
stdio: opts.stdio || ["ignore", "pipe", "pipe"],
env: buildGbrainEnv({ baseEnv: opts.baseEnv, announce: opts.announce }),
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
+2 -1
View File
@@ -48,7 +48,7 @@ import {
import { atomicWriteSync } from "./fs-atomic";
import { homedir } from "os";
import { dirname, join } from "path";
import { buildGbrainEnv, gbrainConfigDir, isExecTimeout, NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
import { buildGbrainEnv, gbrainChildCwd, gbrainConfigDir, isExecTimeout, NEEDS_SHELL_ON_WINDOWS } from "./gbrain-exec";
import { resolveStateRoot } from "./state-root";
export type LocalEngineStatus =
@@ -458,6 +458,7 @@ function freshClassify(env?: NodeJS.ProcessEnv): LocalEngineStatus {
timeout: probeTimeoutMs(env),
stdio: ["ignore", "pipe", "pipe"],
env: buildGbrainEnv({ baseEnv: env ?? process.env }),
cwd: gbrainChildCwd(undefined, env ?? process.env),
shell: NEEDS_SHELL_ON_WINDOWS, // #1731: gbrain is a .cmd shim on Windows
});
return "ok";
+5 -1
View File
@@ -12,7 +12,7 @@
import { execFileSync, spawnSync } from "child_process";
import { realpathSync } from "fs";
import { withErrorContext } from "./gstack-memory-helpers";
import { execGbrainJson, gbrainInvocation } from "./gbrain-exec";
import { execGbrainJson, gbrainChildCwd, gbrainInvocation } from "./gbrain-exec";
import {
detectAutopilot,
decideSourceRemove,
@@ -123,6 +123,7 @@ export function probeSource(id: string, env?: NodeJS.ProcessEnv): SourceState {
timeout: 30_000,
stdio: ["ignore", "pipe", "pipe"],
env,
cwd: gbrainChildCwd(undefined, env),
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
} catch (err) {
@@ -230,6 +231,7 @@ export async function ensureSourceRegistered(
encoding: "utf-8",
timeout: 30_000,
env,
cwd: gbrainChildCwd(undefined, env),
shell: rmInv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
if (rm.status !== 0) {
@@ -246,6 +248,7 @@ export async function ensureSourceRegistered(
encoding: "utf-8",
timeout: 30_000,
env,
cwd: gbrainChildCwd(undefined, env),
shell: addInv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
if (add.status !== 0) {
@@ -273,6 +276,7 @@ export function sourcePageCount(id: string, env?: NodeJS.ProcessEnv): number | n
timeout: 30_000,
stdio: ["ignore", "pipe", "pipe"],
env,
cwd: gbrainChildCwd(undefined, env),
shell: inv.shell, // #1731: gbrain is a .cmd shim on Windows (+#2471 quoting)
});
} catch {