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:
Garry Tan
2026-08-12 08:19:44 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent a996e42856
commit 6fae2c10ff
19 changed files with 520 additions and 34 deletions
+44 -1
View File
@@ -2,7 +2,7 @@
# scripts/check-bootstrap-templates.sh — bootstrap template guard
# [D4, D5, A1 + privacy].
#
# Four independent sections, each SKIP-GRACEFUL (a section whose inputs don't
# Five independent sections, each SKIP-GRACEFUL (a section whose inputs don't
# exist yet prints SKIP and moves on, so CI stays green while the parallel
# bootstrap tasks land):
#
@@ -25,6 +25,14 @@
# (d) Phase-list check [D5]: every `Phase: <name>` in BOOTSTRAP_FOR_AGENTS.md
# must appear in src/core/bootstrap/status.ts (the TS phase list is the
# single source; the runbook defers to it). Skips while either is absent.
# (e) Harness-scoping counter-signal pins: the MCP-scope consent is Claude
# Code only (Codex has no scope flag — `codex mcp add` is user-global).
# Tripwires against accidental deletion of the load-bearing prose, not
# proofs of placement: the runbook must carry the Codex bullet's
# "Do NOT offer an MCP scope choice" and the phase-3 "Claude Code only"
# scoping; questions.json's MCP_SCOPE.question must START WITH
# "(Claude Code only". Intentional rewording updates these pins in the
# same commit. Skips while the runbook/bank are absent.
#
# BSD/GNU grep portable (no \t escapes). Uses `bun` for JSON parsing — the
# check runs via `bun run verify`, so bun is always present.
@@ -187,6 +195,41 @@ else
echo "SKIP: phase-list check (runbook or src/core/bootstrap/status.ts absent)"
fi
# ── (e) harness-scoping counter-signal pins (MCP scope is Claude Code only) ─
if [ -f "$RUNBOOK" ]; then
if ! grep -qF 'Do NOT offer an MCP scope choice' "$RUNBOOK"; then
fail=1
echo "FAIL: BOOTSTRAP_FOR_AGENTS.md lost the Codex counter-signal" >&2
echo " ('Do NOT offer an MCP scope choice'). Codex has no scope flag —" >&2
echo " without this line, Codex-door agents re-ask a dead question." >&2
echo " Rewording intentionally? Update this pin in the same commit." >&2
fi
if ! grep -qF 'Claude Code only' "$RUNBOOK"; then
fail=1
echo "FAIL: BOOTSTRAP_FOR_AGENTS.md lost the 'Claude Code only' scoping on the" >&2
echo " MCP-scope consent (phase 3). Without it the consent reads as" >&2
echo " harness-blind and Codex-door agents ask it." >&2
echo " Rewording intentionally? Update this pin in the same commit." >&2
fi
else
echo "SKIP: harness-scoping pins (runbook absent)"
fi
if [ -f "$QUESTIONS" ] && command -v bun >/dev/null 2>&1; then
if ! GBRAIN_QJSON="$QUESTIONS" bun -e \
'const fs=require("fs");let b;try{b=JSON.parse(fs.readFileSync(process.env.GBRAIN_QJSON,"utf8"));}catch(e){process.exit(1);}if(!b.questions){process.exit(1);}const e=b.questions.MCP_SCOPE;const q=(e&&e.question)||"";process.exit(q.startsWith("(Claude Code only")&&e.phase==="interview"?0:1);'; then
fail=1
echo "FAIL: questions.json MCP_SCOPE.question must start with '(Claude Code only'" >&2
echo " AND MCP_SCOPE.phase must be 'interview' (the consent is recorded" >&2
echo " pre-confirm during the interview; a 'wire' phase re-creates the" >&2
echo " bank-vs-runbook contradiction). Also fails when the questions" >&2
echo " object or the MCP_SCOPE entry is missing, or questions.json fails" >&2
echo " to parse — a bank without them silently passes section (a) too." >&2
echo " Rewording intentionally? Update this pin in the same commit." >&2
fi
else
echo "SKIP: MCP_SCOPE bank pin (questions.json or bun absent)"
fi
if [ "$fail" -ne 0 ]; then
exit 1
fi