diff --git a/BOOTSTRAP_FOR_AGENTS.md b/BOOTSTRAP_FOR_AGENTS.md index 3c75be81c..fb7e166ab 100644 --- a/BOOTSTRAP_FOR_AGENTS.md +++ b/BOOTSTRAP_FOR_AGENTS.md @@ -83,6 +83,15 @@ you needed; report the count at the end (it feeds the install-time measurement). 3. **Interview.** `gbrain bootstrap interview --init`, then ask the questions from the bank (the CLI prints them) in three batches, recording each answer verbatim with `--set KEY "value"`. Push once on vague answers to the required questions. + Claude Code only: with the final batch, also ask the ONE operational consent — + MCP scope. It is not one of the 12 interview questions; consents ride alongside + the bank. The choice: project (recommended — any other repo you open cannot + read your brain) vs user (your agent everywhere, but any repo you open can + reach it — read and write — and two open sessions contend for the database). + Record it with + `gbrain bootstrap interview --set MCP_SCOPE ` BEFORE the + read-back, so the confirmation covers it. On Codex, skip this question + entirely — the wiring step states the Codex reality instead. After the last batch: read ALL answers back in one compact block, ask "Is this the thing you want in the room?", and only then run `gbrain bootstrap interview --confirm ` with the hash `--status` printed @@ -96,12 +105,15 @@ you needed; report the count at the end (it feeds the install-time measurement). - Claude Code: installs per-turn hooks ON by default — do NOT ask; loading the brain every turn is the whole point of installing gbrain for your agent. Tell the human it is on and how to turn it off (`GBRAIN_HOOKS=0`, or re-run with - `--no-hooks`, or `gbrain bootstrap uninstall`). The ONE consent to actually - ask in this phase is MCP scope: project (recommended — any other repo you open - cannot read your brain) vs user (your agent everywhere, but any repo you open - can query it, and two open sessions contend for the database). + `--no-hooks`, or `gbrain bootstrap uninstall`). MCP scope is NOT asked here — + `hooks` consumes the MCP_SCOPE answer recorded during the interview. - Codex: registers MCP (`codex mcp add`) and relies on the AGENTS.md protocol — say plainly that Codex gets pull-based context, not per-turn push. + Do NOT offer an MCP scope choice: `codex mcp add` has no scope flag, so + the registration is always user-global. State it as fact — any repo opened + on this machine can reach the brain (read and write) through its MCP + tools; the off-ramps are `codex mcp remove gbrain` (registration only) or + `gbrain bootstrap uninstall` (full teardown). 7. **Private repo.** `gbrain bootstrap repo` — creates a PRIVATE GitHub repo from the workspace, verifies the privacy bit through the API, pushes. If the human started from a repo they created themselves (create-repo-first: an EMPTY private diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3c0c3a1..dd40252b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,23 @@ say "fresh" forever. --repo `) so the generated wrapper picks up the self-disable guard. - Keyless installs: your sync-and-embed cron chain now exits 0; no action needed beyond upgrading. +## [0.45.3.0] - 2026-08-12 + +**Codex installs stop asking a question Codex can't honor.** The bootstrap used to offer every install a choice of MCP scope — this folder only, or the whole machine — but Codex has no per-folder registrations, so picking "this folder" led to a confusing round-trip where the agent asked permission to keep what it had already done. Now each harness gets the honest version: Claude Code records your scope choice during the interview (where it actually sticks), and Codex simply tells you the truth — its registration reaches the whole machine, read and write — along with the exact commands to remove it (just the registration, or the whole install). + +### Added +- CI tripwires pin the harness-scoping language in the runbook and question bank, so the dead question can't quietly come back in a future edit. +- `gbrain bootstrap status` hints now carry the scope rule on both the interview and wiring phases, so an agent resuming from a stale runbook still sees it. + +### Changed +- The workspace's ACCESS_POLICY.md now describes MCP reach per harness — the project/user tradeoff on Claude Code, the always-machine-wide reality on Codex — and states plainly that reach means read and write, not just queries. +- The Claude Code scope consent is asked and recorded during the interview, before the answer read-back, so your confirmation covers it and the wiring step just consumes it. + +### Fixed +- A hand-damaged answer file no longer crashes `gbrain bootstrap hooks` — and no longer silently grants anything: an unreadable consent answer is treated as declined, with a note telling you how to re-record it. +- A leftover "this folder only" answer on a Codex machine (for example, carried over from a paired Claude Code machine) now gets a clear explanation and a safe way to clear it, instead of a silent mismatch between what you chose and what got registered. + +To take advantage of v0.45.3.0: upgrade with `bun install -g github:garrytan/gbrain#latest-stable`. New installs pick everything up automatically. Existing workspaces (either harness) keep their previously rendered policy files; refresh the two that changed with `gbrain bootstrap render --force --only ACCESS_POLICY.md --only CLAUDE.md` (originals are backed up first). If render refuses because the answers show as not confirmed — a pre-fix install that recorded the scope answer after the read-back — run `gbrain bootstrap interview --show`, then `--confirm `, and retry. Or leave the files as they are — the CLI's own notes and status hints carry the correction either way. ## [0.45.2.0] - 2026-08-11 diff --git a/TODOS.md b/TODOS.md index aad1be7d5..7982f61a1 100644 --- a/TODOS.md +++ b/TODOS.md @@ -5116,6 +5116,34 @@ respective shapes. Small, mechanical; pinned by `test/init-embed-check.test.ts` ## Agent-bootstrap wave follow-ups (filed at build time) +- [ ] **P1 — enforce op scope/localOnly on the stdio MCP dispatch when no auth + context is present, and consider a narrower default surface for pull-mode + harness registrations.** HTTP dispatch enforces `scope`/`localOnly` before + handlers run; the stdio surface should reach parity so a registration that is + user-global by host design (no per-project scoping available) does not expose + more authority than the session needs. Surfaced by the v0.45.x ship + adversarial pass (cross-model); pre-existing behavior, not introduced by the + Codex scope-consent fix — that fix's prose now states the read+write reality + honestly. Needs its own design pass (interaction with `--surface` pinning, + MEMORY_VERBS, and the trust-boundary invariant in CLAUDE.md). +- [ ] **P2 — consent-key answers vs the A8 confirm gate.** Decide whether + `consent: true` bank keys should be exempt from `setAnswer`'s confirmation + invalidation (`src/core/bootstrap/interview.ts:308-309` `[A8]` deletes + `state.confirmed` on ANY set) so operational consents can be recorded at their + designed phase-contextual moment post-confirm without regressing + `bootstrap status` to "answers complete but not confirmed" (status.ts + interview detector). Deferred from the Codex MCP-scope fix (eng review option + 3B chose prose realignment instead: the runbook now records `MCP_SCOPE` in + phase 3, pre-confirm, so the confirm hash covers it). An exemption touches a + tamper-tripwire — a post-confirm flip of `PERSIST_CRON` (background-push + consent) would no longer invalidate anything — so it needs its own + adversarial review before landing. Also cover the healing half: pre-fix + installs that recorded `MCP_SCOPE` at the old wire-phase moment have a + permanently-invalidated confirm, and `bootstrap status` can't distinguish a + consent-key invalidation from a tampered answer set — a status detail for + that case would stop resumed installs being steered into a redundant + re-confirm loop (ship-review data-migration finding). Context: eng review + + codex consult of the Codex scope fix, 2026-08-11. - [ ] **P2 — bootstrap first-push secret scan reads the working tree, not the index blobs; fail-open on binary/large files.** `secretScanOrThrow` / `scanFiles` (src/core/bootstrap/repo.ts + src/core/secret-scan.ts) read @@ -5128,7 +5156,6 @@ respective shapes. Small, mechanical; pinned by `test/init-embed-check.test.ts` v0.45.2.0 /ship Codex adversarial pass (P0 there; scoped to P2 here as a shared-scanner hardening that needs its own tests, deliberately out of the create-repo-first change). - - [x] **P2 — compiled `gbrain` binary can now `serve` a PGLite brain.** FIXED: `src/core/pglite-embedded-assets.ts` embeds PGLite's runtime payload (`pglite.wasm`, `initdb.wasm`, `pglite.data`, `vector.tar.gz`, diff --git a/docs/architecture/KEY_FILES.md b/docs/architecture/KEY_FILES.md index e69a995e3..1489e34d3 100644 --- a/docs/architecture/KEY_FILES.md +++ b/docs/architecture/KEY_FILES.md @@ -495,7 +495,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 `/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 `/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 (`/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). @@ -503,7 +503,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 `#`/`\nPhase: preflight\nPhase: not_a_phase\n', + `\n${RUNBOOK_PINS}Phase: preflight\nPhase: not_a_phase\n`, 'src/core/bootstrap/status.ts': "export const PHASES = ['preflight', 'interview'] as const;\n", }, (dir) => { @@ -272,6 +285,150 @@ describe('check-bootstrap-templates.sh', () => { }, ); }); + + test('(e) clean: runbook counter-signals + compliant MCP_SCOPE prefix pass', () => { + withFixture( + { + 'templates/bootstrap/questions.json': BANK_JSON, + 'templates/bootstrap/SOUL.md.template': '# {{AGENT_NAME}}\n', + 'BOOTSTRAP_FOR_AGENTS.md': `\n${RUNBOOK_PINS}`, + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(0); + expect(r.out).toContain('check-bootstrap-templates: ok'); + }, + ); + }); + + test('(e) fails when the runbook loses the Codex do-not-offer counter-signal', () => { + withFixture( + { + 'templates/bootstrap/questions.json': BANK_JSON, + 'templates/bootstrap/SOUL.md.template': '# {{AGENT_NAME}}\n', + 'BOOTSTRAP_FOR_AGENTS.md': + '\nClaude Code only\n(counter-signal deleted)\n', + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(1); + expect(r.out).toContain('Codex counter-signal'); + }, + ); + }); + + test("(e) fails when the runbook loses the 'Claude Code only' consent scoping", () => { + withFixture( + { + 'templates/bootstrap/questions.json': BANK_JSON, + 'templates/bootstrap/SOUL.md.template': '# {{AGENT_NAME}}\n', + 'BOOTSTRAP_FOR_AGENTS.md': + '\nDo NOT offer an MCP scope choice\n', + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(1); + expect(r.out).toContain("'Claude Code only'"); + }, + ); + }); + + test('(e) fails when the questions object vanishes (valid JSON that silently passes §a)', () => { + withFixture( + { + 'templates/bootstrap/questions.json': '{"version":1,"maxQuestions":12,"interviewKeys":[],"consentKeys":[]}', + 'templates/bootstrap/SOUL.md.template': '# plain\n', + 'BOOTSTRAP_FOR_AGENTS.md': `\n${RUNBOOK_PINS}`, + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(1); + expect(r.out).toContain("must start with '(Claude Code only'"); + }, + ); + }); + + test('(e) fails when the MCP_SCOPE entry vanishes from the bank', () => { + const bankNoEntry = JSON.stringify({ + version: 1, + maxQuestions: 12, + interviewKeys: ['AGENT_NAME'], + consentKeys: ['HOOKS_CONSENT'], + questions: { + AGENT_NAME: { maxLength: 64 }, + HOOKS_CONSENT: { consent: true, maxLength: 8 }, + }, + }); + withFixture( + { + 'templates/bootstrap/questions.json': bankNoEntry, + 'templates/bootstrap/SOUL.md.template': '# {{AGENT_NAME}}\n', + 'BOOTSTRAP_FOR_AGENTS.md': `\n${RUNBOOK_PINS}`, + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(1); + expect(r.out).toContain("must start with '(Claude Code only'"); + }, + ); + }); + + test('(e) fails when MCP_SCOPE.phase reverts to wire (schema-vs-runbook contradiction)', () => { + const bankWirePhase = JSON.stringify({ + version: 1, + maxQuestions: 12, + interviewKeys: ['AGENT_NAME'], + consentKeys: ['HOOKS_CONSENT', 'MCP_SCOPE'], + questions: { + AGENT_NAME: { maxLength: 64 }, + HOOKS_CONSENT: { consent: true, maxLength: 8 }, + MCP_SCOPE: { + consent: true, + phase: 'wire', + question: '(Claude Code only. Codex has no scope flag.) Register for this folder or the whole machine?', + maxLength: 8, + }, + }, + }); + withFixture( + { + 'templates/bootstrap/questions.json': bankWirePhase, + 'templates/bootstrap/SOUL.md.template': '# {{AGENT_NAME}}\n', + 'BOOTSTRAP_FOR_AGENTS.md': `\n${RUNBOOK_PINS}`, + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(1); + expect(r.out).toContain("MCP_SCOPE.phase must be 'interview'"); + }, + ); + }); + + test('(e) fails when MCP_SCOPE.question loses its harness prefix (or the entry vanishes)', () => { + const bankNoPrefix = JSON.stringify({ + version: 1, + maxQuestions: 12, + interviewKeys: ['AGENT_NAME'], + consentKeys: ['HOOKS_CONSENT', 'MCP_SCOPE'], + questions: { + AGENT_NAME: { maxLength: 64 }, + HOOKS_CONSENT: { consent: true, maxLength: 8 }, + MCP_SCOPE: { consent: true, phase: 'interview', question: 'Register for this folder or the whole machine?', maxLength: 8 }, + }, + }); + withFixture( + { + 'templates/bootstrap/questions.json': bankNoPrefix, + 'templates/bootstrap/SOUL.md.template': '# {{AGENT_NAME}}\n', + 'BOOTSTRAP_FOR_AGENTS.md': `\n${RUNBOOK_PINS}`, + }, + (dir) => { + const r = runGuard(TPL_GUARD, dir); + expect(r.status).toBe(1); + expect(r.out).toContain("must start with '(Claude Code only'"); + }, + ); + }); }); describe('verify + workflow wiring', () => {