* v0.36.5.0 feat: secure DATABASE_URL access for shell jobs (inherit: ["database_url"]) Replaces PR #1137's plaintext-config / plaintext-env workarounds with code. Shell-job params gain `inherit: ["database_url"]`, validated pre-enqueue in both the CLI (`gbrain jobs submit`) and `submit_job` MCP op handler. Worker resolves the value from its own loadConfig() at child-spawn time; the persisted `minion_jobs.data` row stores only the name. Plain `env: { GBRAIN_DATABASE_URL: ... }` / `env: { DATABASE_URL: ... }` / `env: { GBRAIN_DIRECT_DATABASE_URL: ... }` are rejected pre-enqueue with a paste-ready hint pointing at `inherit:`. Codex pre-landing review caught two bypasses + one missing shadow name: - H1: cmd/argv inline-secret regex scan (cmd:"GBRAIN_DATABASE_URL=... gbrain sync" was a clean bypass — fixed) - H3: GBRAIN_DIRECT_DATABASE_URL added to shadowKeys - H2: honest docs about output-side leakage (stdout_tail/stderr_tail can still carry the value if the script prints it; that's the script author's responsibility, not gbrain's) Also: gbrain doctor learns home_dir_in_worktree (warns when ~/.gbrain lives inside a git worktree); ~/.gbrain/.gitignore retroactive via saveConfig + post-upgrade. New canonical guide: docs/guides/agent-to-gbrain.md (two-domain framing for downstream agent authors: MCP ops via OAuth vs localOnly admin ops via shell-job inherit:). Closes #1137. Tests: +53 new (21 validator + 12 inherit-record + 6 ensureGitignore + 5 doctor + 2 PGLite E2E + 7 codex-driven H1/H3 cases). Credit: @wintermute filed PR #1137 which made the env-stripping gap visible enough to fix in code. Thank you. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * v0.36.5.0 redesign: free-form inherit:, drop closed enum User feedback: "agent spawning minions should have agency to do what it wants with secrets and pass only the ones that it needs. don't be a security nazi please." Replaces the closed INHERITABLE enum (database_url only) with three small helpers in shell-inherit.ts: - INHERIT_NAME_RE: snake_case shape guard. Rejects __proto__, leading underscore, uppercase, path-traversal. Prototype-pollution defense. - deriveEnvKey(name): config-key → child-env-key. Uppercase by default with one override: database_url → GBRAIN_DATABASE_URL. - resolveInheritValue(cfg, name): value lookup with Object.hasOwn. inherit: now accepts any snake_case config-key the worker has. Agent picks what it needs per-job (database_url, anthropic_api_key, voyage_api_key, or any custom field). Validator does NOT police WHICH keys — single-uid trust model treats agent as peer of worker. Drops the v0.36.5.0-RC rules that were paternalistic for the actual threat model: - closed-enum check - env-shadow rejection - cmd/argv inline-secret scan Keeps the parts that defend real problems: - pre-enqueue validation (closes the persistence-before-throw window) - snake_case regex (prototype-pollution + audit-log readability) - fail-fast on missing config value (UX guardrail, not security) Tests: shell-validate (existing rules + new free-form + prototype-pollution defense + T1 regression guard) and shell-inherit (regex matrix, deriveEnvKey per-name, resolveInheritValue with hasOwn defense). E2E case now exercises inherit:["anthropic_api_key"] to prove genuinely free-form. Docs and CHANGELOG rewritten to reflect the open design + the design-arc story (closed → cut → free-form). Migration file too. 7653 unit tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * v0.36.5.0 add: redact_secrets opt-in for stdout/stderr scrubbing Honest defense for the documented output-side leakage. When a script prints an inherited secret, the value lands plaintext in result.stdout_tail / result.stderr_tail / error_text. v0.36.5.0 adds: - `redact_secrets: true` ShellJobParams field - `--redact-secrets` CLI convenience flag on `gbrain jobs submit shell` - shell-redact.ts: pure `redactSecretsInText(text, secrets)` helper (string-mode replaceAll; regex metachars in values stay literal) - Handler post-processes both tails before throw/return, so the persisted row carries `<REDACTED:name>` tokens instead of values Only inherit-resolved values are scrubbed. env: values are not (those are the agent's "fine in the row" channel by design). Heuristic — defeats accidental `echo "$GBRAIN_DATABASE_URL"`, not adversarial encode-then-print. Default false for back-compat. Tests: - test/minions-shell-redact.test.ts (9 cases): pure-function behavior, regex-metachar safety, multi-secret independent redaction, substring overlap, empty-input/map edge cases - test/minions-shell-validate.test.ts: +4 cases for redact_secrets shape - test/e2e/minions-shell-pglite.test.ts: +2 cases proving redact_secrets: true scrubs persisted row AND redact_secrets:false preserves plaintext (back-compat regression guard) Docs + CHANGELOG + migration file + CLAUDE.md updated. 7667 unit tests green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
5.3 KiB
version, date, feature_pitch
| version | date | feature_pitch |
|---|---|---|
| v0.36.5.0 | 2026-05-18 | Shell jobs grow `inherit: [...]` — agent passes any snake_case config-key names; worker resolves values from its own `loadConfig()` at child-spawn. Names persist to the row; values resolve fresh. Validated pre-enqueue. |
v0.36.5.0 — Free-form secret inheritance for shell jobs calling gbrain CLI
What changed
- Shell-job pre-enqueue validator (
src/core/minions/handlers/shell-validate.ts, NEW). Called from BOTH submit surfaces BEFOREMinionQueue.add():gbrain jobs submit shell(CLI) andsubmit_jobMCP op forname='shell'. Validates cmd/argv/cwd/env shape,inheritarray of snake_case names, and fail-fasts if the worker can't resolve a requested name. No rejected payload ever lands inminion_jobs.data. inherit: [...]ShellJobParams field — free-form. Pass any snake_case config-key name (database_url,anthropic_api_key,voyage_api_key,groq_api_key,zeroentropy_api_key,my_custom_field, etc.). The validator does NOT police which keys you choose — same-uid trust model treats the agent as a peer.- Env-key derivation: name uppercased by default. One override:
database_url→GBRAIN_DATABASE_URL(plainDATABASE_URLis ambiguous in most Postgres-app contexts). - Prototype-pollution defense: snake_case regex blocks
__proto__/_leading/ uppercase. Value-resolver usesObject.hasOwn. - Defense-in-depth re-validation in the shell handler at job-pickup catches pre-existing rows AND any future submit path that forgets the pre-enqueue call.
gbrain doctor home_dir_in_worktreecheck warns when~/.gbrain/lives inside a git worktree. Handles.gitas directory or file.~/.gbrain/.gitignoreretroactive viaensureGitignore()called fromsaveConfig()ANDgbrain post-upgrade. Idempotent, never clobbers.- Output-side redaction (opt-in): set
redact_secrets: trueon the job params (or pass--redact-secretson the CLI) and the worker scrubs every occurrence of resolvedinherit:values fromstdout_tail/stderr_tail/error_textbefore persistence. Replacement token:<REDACTED:name>. Literal-string replace — defeats accidentalecho "$GBRAIN_DATABASE_URL", not adversarial encode-then-print. Default false (back-compat). - New canonical guide:
docs/guides/agent-to-gbrain.md. Two-domain framing for downstream agent authors: MCP ops via thin-client OAuth vslocalOnlyadmin ops via shell-jobinherit:.
What downstream agents can do
The agent picks which secrets to inherit, per job, by name:
{
"cmd": "gbrain sync --skip-failed && gbrain embed --stale",
"cwd": "/data/gbrain",
"inherit": ["database_url", "anthropic_api_key", "voyage_api_key"]
}
Worker resolves each name from loadConfig() and injects into the child env
under the derived key (database_url → GBRAIN_DATABASE_URL,
anthropic_api_key → ANTHROPIC_API_KEY, etc.). Names land in
minion_jobs.data and the shell-audit JSONL; values resolve fresh on every
spawn and don't persist from inherit: itself.
If the worker can't resolve a name, the validator fail-fasts at submit time
with a paste-ready gbrain config set <name> <value> hint.
You can still use env: for non-secret values or for secrets you've decided
are OK to persist in the row (e.g. correlation tokens). v0.36.5.0 doesn't
forbid that — the validator trusts the agent.
Verify
# 1. The new pattern works on your worker:
gbrain jobs submit shell --params \
'{"cmd":"gbrain stats","cwd":"/tmp","inherit":["database_url"]}' --follow
# Expect: page count, exit 0.
# 2. The doctor surfaces worktree risk if it exists:
gbrain doctor --json | grep -A1 home_dir_in_worktree
# 3. The retroactive gitignore landed:
test -f ~/.gbrain/.gitignore && cat ~/.gbrain/.gitignore
# Expect: file exists, contents = "*\n"
# 4. The audit-log records names not values:
tail -1 ~/.gbrain/audit/shell-jobs-*.jsonl | grep -o '"inherit":\[[^]]*\]'
# Expect: ["database_url"] (no URL value)
Why this exists
PR #1137 documented two workarounds for the env-stripping behavior of shell
jobs: write database_url plaintext to ~/.gbrain/config.json, or pass
env: { GBRAIN_DATABASE_URL: ... } per-job. Both work. Both leave plaintext
secrets either on disk or in minion_jobs.data rows that travel with brain
DB dumps and shared brains.
The /cso audit cut an earlier proposal (encrypted vault + Unix-socket
broker + SO_PEERCRED + per-call tokens) as theater for a single-uid topology.
Codex's pre-landing review then caught the load-bearing bug in the rewritten
"minimum scope" plan: validation in the handler runs AFTER queue.add(),
so the headline "input-secrets never persist" was technically false. Fixed
pre-implementation by lifting the validator to a shared pre-enqueue module
called from both submit surfaces.
An interim draft used a closed INHERITABLE enum (hardcoded list of
allowed secret names with explicit shadow-key sets and inline-cmd regex
scans). Codex flagged bypasses; the deeper question surfaced: what does the
closed enum actually defend on a single-uid topology? Same-uid trust = same
trust domain; refusing to let the agent inherit voyage_api_key because
it's not on a hand-curated list is paternalism. The shipped design opens
inherit: to any snake_case config-key. The agent decides.