Skip to content

Latest commit

 

History

History
115 lines (97 loc) · 5.32 KB

File metadata and controls

115 lines (97 loc) · 5.32 KB
version v0.36.5.0
date 2026-05-18
feature_pitch 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.