Files
gbrain/skills/migrations/v0.36.5.0.md
T
e227965024 v0.36.5.0 feat: secure DATABASE_URL access for shell jobs (inherit: ["database_url"]) (#1192)
* 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>
2026-05-19 13:12:40 -07:00

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 BEFORE MinionQueue.add(): gbrain jobs submit shell (CLI) and submit_job MCP op for name='shell'. Validates cmd/argv/cwd/env shape, inherit array of snake_case names, and fail-fasts if the worker can't resolve a requested name. No rejected payload ever lands in minion_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_urlGBRAIN_DATABASE_URL (plain DATABASE_URL is ambiguous in most Postgres-app contexts).
  • Prototype-pollution defense: snake_case regex blocks __proto__ / _leading / uppercase. Value-resolver uses Object.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_worktree check warns when ~/.gbrain/ lives inside a git worktree. Handles .git as directory or file.
  • ~/.gbrain/.gitignore retroactive via ensureGitignore() called from saveConfig() AND gbrain post-upgrade. Idempotent, never clobbers.
  • Output-side redaction (opt-in): set redact_secrets: true on the job params (or pass --redact-secrets on the CLI) and the worker scrubs every occurrence of resolved inherit: values from stdout_tail / stderr_tail / error_text before persistence. Replacement token: <REDACTED:name>. Literal-string replace — defeats accidental echo "$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 vs localOnly admin ops via shell-job inherit:.

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_urlGBRAIN_DATABASE_URL, anthropic_api_keyANTHROPIC_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.