mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
v0.45.3.0 fix(bootstrap): never offer MCP project scope on Codex — consent-flow honesty (#4029)
* fix(bootstrap): codex scope-note guard + scope-aware wire hint
runHooks: on --harness codex, print a stderr note when an explicit,
non-skipped MCP_SCOPE=project answer is persisted (raw readInterviewState
read, not the project-defaulting consentAnswer resolver) — reachable via
attach from a Claude Code machine or a pre-fix install. consentAnswer is
now shape-tolerant: a hand-edited non-string answer value falls through to
the bank default instead of throwing at a toLowerCase call site.
status.ts: the wire-phase resume_hint states the scope rule (Claude Code
consent, phase 3; Codex always user-global — no scope flag).
Tests: 7-case branch matrix on the note guard + wire-hint pin.
* fix(bootstrap): harness-scope the MCP consent prose (never offer project scope on Codex)
Runbook: the scope consent moves to phase 3 (Claude Code only, recorded with
interview --set BEFORE the read-back so the confirmation covers it — a
wire-time set clears the A8 confirm and regresses status); phase 6's Codex
bullet gains the counter-signal: Do NOT offer an MCP scope choice — codex
mcp add has no scope flag, registrations are always user-global.
Bank: MCP_SCOPE.question gets a read-aloud-safe "(Claude Code only. ...)"
prefix and phase moves wire -> interview to match.
Templates: ACCESS_POLICY's scope section becomes two static harness
paragraphs; CLAUDE.md stops conflating directory-based identity loading
with MCP scope. Vendored template-repo regenerated.
* chore(ci): pin the harness-scoping counter-signals (check-bootstrap-templates §e)
Three SKIP-GRACEFUL tripwires: the runbook must carry "Do NOT offer an MCP
scope choice" and "Claude Code only"; questions.json's MCP_SCOPE.question
must start with "(Claude Code only". Guard-test fixtures gain a compliant
MCP_SCOPE entry + pass/fail cases for each pin.
* docs: Codex user-global scope caveats + A8 consent-semantics follow-up TODO
Install table and KEY_FILES hooks entry now state the split: Claude Code
takes --scope (project default); Codex has no scope flag. TODOS gains the
deferred structural question (consent keys vs the A8 confirm gate).
* fix: pre-landing + red-team review hardening
Guard §(e): a valid-JSON bank missing its questions object now FAILS (it
silently passed both §a and §e); the pin also asserts MCP_SCOPE.phase is
'interview' so the schema half of the fix can't silently revert. Fixtures
for both + entry-vanished. consentAnswer says so on stderr when it discards
a malformed answer value (a silent fall-through could flip a damaged opt-out
to the permissive default) — pinned by a claude-code matrix case asserting
the receipt. Off-ramp texts gain the narrow `codex mcp remove gbrain`
alongside full uninstall (note, runbook, ACCESS_POLICY + vendored copy).
Cross-refs de-ordinaled (phase names, not numbers — master renumbers steps).
TODOS A8 entry gains the healing half (status can't distinguish consent-key
invalidation from tampering).
* fix: adversarial-review hardening — fail-closed consents + honest scope prose
consentAnswer: a present-but-unusable answer (non-string, empty, bare {})
now fails CLOSED to 'no' with a stderr note — a bank-default fall-through
could flip a damaged opt-out into consent (cross-model finding); 'no' is
every consent key's safe reading (no hooks, no cron, project scope). Pinned
by a malformed-HOOKS_CONSENT matrix case asserting hooks are declined.
Codex scope note: names the safe remediation (--skip MCP_SCOPE + reconfirm;
never --set user — the answer git-syncs to paired Claude Code machines and
would widen their scope) [codex review P2]. Prose states the user-global
registration's read+write reach honestly (runbook, ACCESS_POLICY + vendored).
Interview resume_hint carries the consent-recording step (the CLI channel a
resumed install actually reads). Guard §(e): unparseable questions.json now
FAILS instead of passing silently. TODOS: P1 filed for stdio dispatch scope
parity (pre-existing, surfaced by the adversarial pass).
* chore: bump version and changelog (v0.45.3.0)
Runbook stamp + vendored template-repo regenerated to match (the two
version gates check:bootstrap-tag and check:bootstrap-templates enforce).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: update project documentation for v0.45.3.0
KEY_FILES.md: bootstrap.ts entry carries the fail-closed consent
resolution + Codex stale-scope-answer note; CI-guards entry adds the
check-bootstrap-templates §(e) harness-scoping counter-signal pins.
bootstrap.md: degradation matrix Codex row states the no-scope-flag
reality (registrations are user-global). llms bundles regenerated
(byte-identical — these docs are linked, not inlined).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: cross-model doc-review fixes for v0.45.3.0
CHANGELOG: Codex off-ramps are removal commands, not narrowing; the
refresh recipe now covers both harnesses and names the re-confirm step
for pre-fix installs whose confirmation was invalidated. Runbook: the
user-scope tradeoff says read and write, matching ACCESS_POLICY.
bootstrap.md: degradation-matrix Codex row rephrased (lose the ability
to confine reach, not the reach itself). KEY_FILES: the section-(e)
description no longer implies placement pinning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a996e42856
commit
6fae2c10ff
@@ -493,7 +493,7 @@ Normative docs: `docs/designs/AGENT_BOOTSTRAP_DESIGN.md` (scope) +
|
||||
User-facing contract: `docs/guides/bootstrap.md`. Runbook the paste block fetches:
|
||||
`BOOTSTRAP_FOR_AGENTS.md` (root; carries a version stamp CI pins to VERSION).
|
||||
|
||||
- `src/commands/bootstrap.ts` — the `gbrain bootstrap {status,interview,render,repo,hooks,verify,uninstall,attach}` dispatcher. Engine-free everywhere except `verify` (which opens/closes its own engine — safe because verify runs with no live serve, before host registration). Mutating subcommands run under the workspace bootstrap lock; render is gated on interview `complete && confirmed` and hard-refuses when the workspace origin is a PUBLIC remote (identity files must never land in a public repo — the same template-door gate `status` enforces; unverifiable visibility warns and proceeds, treating the origin as public); the provider key routes to the 0600 config sink and never touches interview state; every subcommand appends a line to `<home>/bootstrap/install.jsonl`. `GBRAIN_BOOTSTRAP_ABORT_AFTER` is the deterministic kill-mid-phase test seam.
|
||||
- `src/commands/bootstrap.ts` — the `gbrain bootstrap {status,interview,render,repo,hooks,verify,uninstall,attach}` dispatcher. Engine-free everywhere except `verify` (which opens/closes its own engine — safe because verify runs with no live serve, before host registration). Mutating subcommands run under the workspace bootstrap lock; render is gated on interview `complete && confirmed` and hard-refuses when the workspace origin is a PUBLIC remote (identity files must never land in a public repo — the same template-door gate `status` enforces; unverifiable visibility warns and proceeds, treating the origin as public); the provider key routes to the 0600 config sink and never touches interview state; every subcommand appends a line to `<home>/bootstrap/install.jsonl`. Consent answers resolve FAIL-CLOSED: `consentAnswer` treats a hand-edited/unusable interview value (non-string, empty) as declined — loudly, with a re-record note — never falling through to a permissive bank default; `hooks` on Codex prints a corrective note when a persisted `project` MCP_SCOPE answer is found (raw state read, not the resolver — `codex mcp add` has no scope flag, registrations are always user-global) with safe clear instructions. `GBRAIN_BOOTSTRAP_ABORT_AFTER` is the deterministic kill-mid-phase test seam.
|
||||
- `src/core/bootstrap/format.ts` — `agent.json` manifest (format_version 1, provisional; `initialized` sentinel distinguishes a template clone from a bootstrapped workspace) + the machine-local install receipt (`<home>/bootstrap/receipt.json`) that proves THIS machine ran bootstrap; uninstall is keyed to the receipt, never the repo manifest. Atomic writes; `readManifest` never throws (typed states incl. conflict markers).
|
||||
- `src/core/bootstrap/assets.ts` — every template + the question bank embedded via Bun `with { type: 'file' }` imports (the chunkers/code.ts pattern) so the compiled binary renders with no repo checkout; `DERIVED_TOKENS` (GITHUB_REPO_URL, CORPUS_RETENTION_DAYS) is the non-bank half of the template token set the CI bijection guard checks.
|
||||
- `templates/bootstrap/` — the ten `{{TOKEN}}` identity templates (AGENTS/CLAUDE/SOUL/USER/MEMORY/HEARTBEAT/ACCESS_POLICY/GITHUB/memory-README/gitignore), `questions.json` (12 asked / 6 required; consent keys; `persist:false` sink keys), and `template-repo/` — the VENDORED deterministic render the release job diffs against before publishing the public template repo. Generic placeholder content only (privacy iron rule; CI-asserted).
|
||||
@@ -501,7 +501,7 @@ User-facing contract: `docs/guides/bootstrap.md`. Runbook the paste block fetche
|
||||
- `src/core/bootstrap/render.ts` — token substitution with interview values treated as data (line-leading `#`/`<!--`/fence escaping), hard-fail on unresolved tokens, never-clobber + timestamped backups on `--force`, blank-line collapse, byte floors scaled to answered count. `--minimal` is the deterministic placeholder mode the template-repo generator uses (byte-identical across runs; leaves required tokens as literal fill-me markers; writes `initialized:false`). `--only` never writes agent.json.
|
||||
- `src/core/bootstrap/lock.ts` — the bootstrap-run mutex (atomic mkdir + pid liveness + age guard + ownership token; steal requires dead pid AND stale age) and the family's shared typed `BootstrapError` (GH_MISSING/GH_AUTH carry exit 2 = human action needed).
|
||||
- `src/core/bootstrap/repo.ts` / `attach.ts` / `uninstall.ts` — private-repo lifecycle. `createPrivateRepo`: gh gates, slugified name probe, `gh repo create --private --source --push`, privacy verified via `gh api .private` (rate-limit/5xx is VERIFY_UNAVAILABLE, distinct from not-private) before any push, idempotency keyed off the remote URL. A pre-existing origin is adopted (disposition 'adopted') when the authed gh user owns it, there's no recorded `repo_url`, and it is SAFE — empty or already carrying our history (`assertAdoptableOrigin`; a foreign-content repo is refused `ORIGIN_NOT_EMPTY`, never a silent no-op); this is the create-repo-first path. Org-owned origins and anything else are refused and pointed at attach. Repo-local git identity is set in both create and adopt paths before commit; `repo_url` is recorded only after a successful push. `attachWorkspace` (machine two): requires an `initialized` manifest, writes this machine's receipt, returns structured wiring steps. `uninstallWorkspace`: receipt-keyed, refuses under a live serve (read-only lock probe — never opens the engine), removes exactly receipt-recorded paths + marker-keyed host entries, keeps the brain unless `--delete-brain` AND bootstrap created it; never wholesale-deletes the gbrain home. All gh/git through an injectable ExecRunner seam.
|
||||
- `src/core/bootstrap/hooks.ts` + `host-specs.ts` — host wiring. `host-specs.ts` is the ONE module owning host-format assumptions (dated spec targets with verifiedAt + doc references: claude-code hooks/settings shapes incl. the 10,000-char hook-output cap; codex mcp-add argv; no-TOML-writer-in-v1 decision recorded). `writeClaudeHooks` does a structural JSON merge into `.claude/settings.local.json` keyed by a `_gbrain` marker — foreign hooks and permissions survive, re-runs dedupe, broken JSON is backed up loudly; `registerClaudeMcp`/`registerCodexMcp` build argv only (project scope default, `-e GBRAIN_SOURCE` so MCP writes land in the workspace source, and `serve --surface full` pinned so a pre-existing `mcp_surface: verbs` config row can't silently narrow the bootstrap op surface).
|
||||
- `src/core/bootstrap/hooks.ts` + `host-specs.ts` — host wiring. `host-specs.ts` is the ONE module owning host-format assumptions (dated spec targets with verifiedAt + doc references: claude-code hooks/settings shapes incl. the 10,000-char hook-output cap; codex mcp-add argv; no-TOML-writer-in-v1 decision recorded). `writeClaudeHooks` does a structural JSON merge into `.claude/settings.local.json` keyed by a `_gbrain` marker — foreign hooks and permissions survive, re-runs dedupe, broken JSON is backed up loudly; `registerClaudeMcp`/`registerCodexMcp` build argv only (Claude Code takes `--scope`, project default; Codex has no scope flag — `codex mcp add` is always user-global; `-e`/`--env GBRAIN_SOURCE` so MCP writes land in the workspace source, and `serve --surface full` pinned so a pre-existing `mcp_surface: verbs` config row can't silently narrow the bootstrap op surface).
|
||||
- `src/commands/hook.ts` — engine-free `gbrain hook {session-start,user-prompt,stop,session-end}` (zero engine modules in the import graph; a hook must NEVER contend for the PGLite writer lock). user-prompt: stdin hook JSON → transcript-path confinement → last-4-turns window + cross-turn dedupe (the transcript's `hook_additional_context` attachments — the blocks WE previously injected — ride `priorContextText`, deduplicated and capped at `PRIOR_CONTEXT_MAX_BYTES` (32KB, so the advisory payload can never blow the IPC message cap; one oversized block is skipped without evicting smaller ones), so a page is volunteered once per session, not once per mention; structured extraction only, never raw-turn substring matching) → IPC turn_context (with a feedback-loop `channel`, `--harness <claude-code|codex>`, default claude-code) → `hookSpecificOutput.additionalContext` under an 800ms self-deadline; every path fails open (exit 0, empty stdout) with a typed reason in the heartbeat. Listed in cli.ts's `STARTUP_HOOK_SKIP_COMMANDS` (per-prompt invocations must never spawn a detached check-update child; membership is pinned by a source grep — the runtime path no-ops under NODE_ENV=test). session-start: file-plane digest (allowlisted MEMORY.md sections, push staleness, prior failures) + crashed-session recovery push gated on an initialized manifest. session-end: confined full-transcript parse → redacted corpus write (session-id filename dedup, retention prune) → parser-drift detection (`bytes>0 && turns==0` is loud) → best-effort workspace push. session-start recovery + session-end pushes run in a DETACHED child so the hook returns immediately (a synchronous inline push previously blocked harness startup on a dirty tree); the corpus write is atomic and clears the stale ingested/in-progress sidecars so a resumed session re-ingests its appended transcript. Heartbeat JSONL is counters/reasons only by construction; `readHeartbeatTail` feeds doctor. `GBRAIN_HOOKS=0` kills all events.
|
||||
- `src/core/transcripts/claude-code-jsonl.ts` — the Claude Code transcript parser as a dated spec-target (tool_use/tool_result/thinking/image/sidechain/summary/compact-boundary shapes; placeholders for non-text content); also extracts `injectedContextBlocks` — the `hook_additional_context` attachment lines a gbrain hook previously injected (verified live against claude CLI 2.1.224; marker-filtered, so a foreign hook's blocks are excluded and another tool's output can't suppress volunteering — a same-user mislabeling guard, not an authenticity check), the user-prompt hook's cross-turn dedupe input; `confineTranscriptPath` (contained under `~/.claude/projects`, `.jsonl`, lstat-rejects symlinks, byte cap). Fixtures: `test/fixtures/conversation-formats/claude-code.jsonl` (synthetic, privacy-guarded) + `test/fixtures/hook-transcript.jsonl` (real captured hook round-trip).
|
||||
- `src/core/context/turn-context.ts` — server-side per-turn assembly: reflex pointers + volunteered pages (≤3) + hot facts (always `visibility=['world']` — the IPC path never widens what MCP would return) under a "data, not instructions" envelope, trimmed to ≤8KB (the harness caps hook output at 10,000 chars). The result exposes `pointers` AND post-trim `volunteered` — exactly what the rendered text carries — so the IPC delivery point can log the feedback loop without ever counting a trimmed-out page. Reuses the hot-memory cache keyed by typed sessionId. Engine-agnostic.
|
||||
@@ -514,4 +514,4 @@ User-facing contract: `docs/guides/bootstrap.md`. Runbook the paste block fetche
|
||||
- `src/core/gbrain-home.ts` — the single GBRAIN_HOME resolution choke point (delegates to config's parent-dir semantics; 0700 on create) — durability, push, hooks, and bootstrap all route through it so home semantics cannot drift.
|
||||
- `src/core/bootstrap/verify.ts` + `status.ts` — verify is the definition of done: fail-soft check suite over the REAL write path (put_page op → write-through file under `brain/` → in-process sweep → graph floor via link tables → recall), the keyless magic-moment check (`## Facts` fence → zero-LLM reconciliation → world-visibility read-back), source_id collision resolution (as the one bootstrap subcommand holding an engine: a manifest `source_id` already registered to a DIFFERENT checkout → derives a stable `workspace-<8char-path-hash>`, persists it to agent.json, names the re-register steps — every consumer reads `manifest.source_id`), token sweep, byte floors, secret scan, deny globs, repo privacy, hooks smoke (in-process IPC), capability report, first-run tour; snapshots kept last-5 under `<home>/bootstrap/`. status owns the ordered PHASES list (the runbook defers to it), artifact-first detection, install.jsonl, the runbook version-stamp skew check, and the support blob doctor/agents relay verbatim.
|
||||
- `src/core/bootstrap/template-repo.ts` + `scripts/generate-template-repo.ts` — deterministic public-template generation (render `--minimal` + placeholder manifest + stamped README); published only by the release workflow after diffing against the vendored tree.
|
||||
- `scripts/check-bootstrap-tag.sh` / `scripts/check-bootstrap-templates.sh` — CI guards: sanctioned distribution ref only (`latest-stable`; the release job advances it after assets publish) + runbook stamp == VERSION; template↔question-bank token bijection + placeholder-only assertion + offline generator↔vendored byte-diff + runbook-phase↔status.ts consistency. Both skip gracefully when their subjects are absent.
|
||||
- `scripts/check-bootstrap-tag.sh` / `scripts/check-bootstrap-templates.sh` — CI guards: sanctioned distribution ref only (`latest-stable`; the release job advances it after assets publish) + runbook stamp == VERSION; template↔question-bank token bijection + placeholder-only assertion + offline generator↔vendored byte-diff + runbook-phase↔status.ts consistency + harness-scoping counter-signal pins (the MCP-scope consent is Claude Code only: the runbook must carry the Codex "Do NOT offer an MCP scope choice" bullet and the "Claude Code only" consent scoping, and the bank's MCP_SCOPE question must start with `(Claude Code only` with `phase: interview` — tripwires against deleting the load-bearing prose, not placement proofs). Both skip gracefully when their subjects are absent.
|
||||
|
||||
@@ -19,7 +19,7 @@ follows is `BOOTSTRAP_FOR_AGENTS.md` at the repo root, fetched at the
|
||||
| Identity files (SOUL/USER/MEMORY/AGENTS/CLAUDE/HEARTBEAT/ACCESS_POLICY/GITHUB) | your workspace folder | loaded at session start |
|
||||
| `agent.json` manifest + `brain/`, `memory/`, `skills/`, `state/` | workspace | — |
|
||||
| Local brain (PGLite) | `~/.gbrain/` (never in the repo) | while a session's MCP serve is open |
|
||||
| MCP registration (`gbrain serve`) | project scope by default | spawned by your harness per session |
|
||||
| MCP registration (`gbrain serve`) | Claude Code: project scope by default; Codex: user-global (no scope flag) | spawned by your harness per session |
|
||||
| Hooks (Claude Code, ON by default) | `.claude/settings.local.json` (gitignored) | each prompt; fail-open; `--no-hooks` opts out at install, `GBRAIN_HOOKS=0` disables at runtime |
|
||||
| Session persistence | SessionEnd hook → scan-gated commit+push | at session end |
|
||||
| Optional 15-min push job | launchd/cron (consent-gated) | while logged in |
|
||||
@@ -118,7 +118,7 @@ you'd apply to any journal: write what you'd be comfortable persisting.
|
||||
| API keys | everything (keyless mode) | semantic search, auto-extraction |
|
||||
| GitHub / `gh` | full local agent | off-machine durability (repo re-runnable later) |
|
||||
| Hooks (Claude Code) | pull protocol via AGENTS.md gates | automatic per-turn context + session-end persistence |
|
||||
| Codex (no hook system) | pull protocol + MCP tools | per-turn push (stated plainly; not oversold) |
|
||||
| Codex (no hook system, no MCP scope flag) | pull protocol + MCP tools | per-turn push (stated plainly; not oversold) + the ability to confine MCP reach to one folder (`codex mcp add` is always user-global) |
|
||||
| Second simultaneous session | first session unaffected | second session's brain tools fail politely (one live serve per brain — v1 contract) |
|
||||
|
||||
## Multi-device
|
||||
|
||||
Reference in New Issue
Block a user