diff --git a/.claude/inject-org-context.sh b/.claude/inject-org-context.sh index d9d50f6..495d0d2 100644 --- a/.claude/inject-org-context.sh +++ b/.claude/inject-org-context.sh @@ -1,8 +1,25 @@ #!/usr/bin/env bash # SessionStart hook — inject the bounded-systems canonical Claude context. # Canonical source since .github#175: bounded-systems/.github -> claude/context.md -# (PUBLIC — the #491 audit found nothing private; the .github-private copy stays -# byte-identical until #494 retires it and remains the fallback below). +# (PUBLIC — the #491 audit found nothing private; the .github-private copy is a +# duplicate that remains a fallback below). Since infra#415 that public copy is +# reached through boot.bounded.tools/public-context.md rather than raw +# .githubusercontent.com — same bytes, one fewer host a session must reach. See +# step 0. +# +# THE TWO COPIES ARE NOT GUARANTEED IDENTICAL, and this header used to say they +# were "byte-identical until #494 retires it". Both halves were wrong (#581): +# #494 is a different subject (it retired PATHBASE_LEASE_KEY, closed 2026-08-16), +# and the copies diverged on 2026-08-15 when 50bc53d taught the three-door claim +# ladder in .github-private and never propagated here. For two days every session +# in the fleet was injected the OLD single-door convention — the one keycard#7 +# proved unsound and #530 built claim-relay.yml to displace — because step 0 +# below fetches THIS repo's copy and it was the stale one. +# +# A comment asserting an invariant is not a check (agentic-code-hygiene rule 3). +# So this script no longer assumes it: step L prefers a local checkout when the +# session has one, and SAYS SO OUT LOUD when two local copies disagree, which is +# the only moment a session can notice the drift that affects it. # Fail OPEN but never SILENT: anything that goes wrong yields no context and one # status line saying so — a degraded session must be able to tell (#491). set -uo pipefail @@ -11,13 +28,67 @@ command -v jq >/dev/null 2>&1 || exit 0 path='repos/bounded-systems/.github-private/contents/claude/context.md' ctx="" -# 0) The public canonical copy — anonymous raw fetch, no token, no clone. This -# is the source that works in a cloud session with NOTHING attached; every -# fallback below was measured failing there on 2026-07-31, when the file -# lived only in the private repo. -if command -v curl >/dev/null 2>&1; then +# L) THE LOCAL CHECKOUT, when the session has one. Tried before any network +# source because a fetched copy can be OLDER than the tree the session was +# created from, and on 2026-08-15 it was: this session's own correct +# context.md sat two directories away on disk while step 0 pulled the stale +# one over the wire (#581). A session should not be taught a convention its +# own checkout contradicts. +# +# Root resolution mirrors org-repair.sh's, deliberately — never assume +# /home/user. .github-private is preferred when both are present because it +# is the copy boot-deploy.yml publishes into CONTEXT_KV, and empirically the +# one that gets maintained first. +# +# When both exist and DIFFER, that is the #581 defect recurring, and it is +# reported rather than silently resolved: the elected copy still loads (a +# session with context beats one without), but the divergence is named in +# the injected text so whoever reads it can see the org is inconsistent. +R="${CLAUDE_SESSION_ROOT:-}" +[ -f "$R/.github/.claude/boot.sh" ] 2>/dev/null || R="$PWD" +[ -f "$R/.github/.claude/boot.sh" ] || R="${PWD%/*}" +[ -f "$R/.github/.claude/boot.sh" ] || R=/home/user +priv="$R/.github-private/claude/context.md" +pub="$R/.github/claude/context.md" +drift="" +if [ -f "$priv" ] && [ -f "$pub" ] && ! cmp -s "$priv" "$pub"; then + drift="⚠ org context DRIFT: ${priv} and ${pub} differ. Using the .github-private copy. The .github copy is what a session WITHOUT a checkout is served, so bare sessions are getting the other text — see .github-private#581." +fi +if [ -f "$priv" ]; then + ctx="$(cat "$priv" 2>/dev/null || true)" +elif [ -f "$pub" ]; then + ctx="$(cat "$pub" 2>/dev/null || true)" +fi + +# 0) The public canonical copy — anonymous, no token, no clone. This is the +# source that works in a cloud session with NOTHING attached; every fallback +# below was measured failing there on 2026-07-31, when the file lived only in +# the private repo. +# +# SERVED THROUGH THE BOOT WORKER since infra#415, not fetched from +# raw.githubusercontent.com directly. THE BYTES ARE THE SAME FILE: the Worker +# fetches bounded-systems/.github's claude/context.md from CLOUDFLARE'S +# network and streams it through verbatim — verified byte-identical against +# both the checkout and the raw upstream at deploy time (all three hashed to +# 99f98f39… on 2026-08-17). What changes is only which host THIS session +# contacts, and that is the entire point: this line was the last session-side +# use of raw.githubusercontent.com, and the only reason that host was still in +# the front-desk egress allowlist (.github-private#534 item 2). +# +# NOT the Worker's /context.md — that route is lease-gated and serves the +# .github-PRIVATE copy out of CONTEXT_KV, a different file that #581 recorded +# diverging from this one. /public-context.md is the public copy, which is +# what this hook has always fetched. +# +# NO raw.githubusercontent FALLBACK is kept, deliberately. A fallback to the +# host we are retiring would stop working the moment the allowlist entry is +# dropped, leaving dead code that reads like resilience; and the failure it +# would cover — the Worker being unreachable — is already covered by steps 1-3 +# over github.com, a host that is not going anywhere. Fail open, loudly, per +# the message at the bottom. +if [ -z "$ctx" ] && command -v curl >/dev/null 2>&1; then ctx="$(curl -fsSL --connect-timeout 5 --max-time 15 \ - https://raw.githubusercontent.com/bounded-systems/.github/main/claude/context.md \ + https://boot.bounded.tools/public-context.md \ 2>/dev/null || true)" fi @@ -53,8 +124,30 @@ fi # the silent variant made a context-less session indistinguishable from a # healthy one, and the verbspec worker shipped work without knowing. if [ -z "$ctx" ]; then - echo "org context NOT loaded — all sources failed (public raw, gh api, git clone, curl+token). Degraded mode: see the org stanza in CLAUDE.md." + echo "org context NOT loaded — all sources failed (boot.bounded.tools/public-context.md, gh api, git clone, curl+token). Degraded mode: see the org stanza in CLAUDE.md." + exit 0 +fi + +# One session start injects this block ONCE (#508, measured 2026-08-16): the +# attached repo's own SessionStart hook and the boot-installed dispatcher both +# run this script, in separate envelopes no in-process merge can reach — so the +# second run detects the first through a short-lived marker and stays silent. +# The predicate is "already INJECTED", never "other hook installed" — installed +# is not fired (#427, #506). Keyed by session id and aged 90s: sibling hooks in +# one start fire seconds apart, while distinct starts (create → resume, or the +# snapshot's --init-only pass → the first real session) are minutes apart or +# carry a different id. Marker only on SUCCESS: a degraded run writes nothing, +# so a later sibling that can reach the context still injects it. +m="/tmp/.bounded-org-context.${CLAUDE_CODE_SESSION_ID:-any}" +now="$(date +%s)" +mt="$(stat -c %Y "$m" 2>/dev/null || stat -f %m "$m" 2>/dev/null || echo 0)" +if [ -f "$m" ] && [ $((now - mt)) -lt 90 ]; then exit 0 fi +touch "$m" 2>/dev/null || true +# The drift notice rides INSIDE the injected context, not on a bare stdout line: +# only additionalContext reaches the session, and a warning the session cannot +# read is the silent failure this script's #491 lesson is about. +[ -n "$drift" ] && ctx="${drift}"$'\n\n'"${ctx}" jq -n --arg c "$ctx" \ '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$c}}' \ No newline at end of file diff --git a/.claude/org-repair.sh b/.claude/org-repair.sh index c802bbd..7bafae8 100644 --- a/.claude/org-repair.sh +++ b/.claude/org-repair.sh @@ -3,13 +3,28 @@ # as a committed file, so .claude/settings.json can pre-approve exactly this # invocation: `bash .claude/org-repair.sh` (#491). Canonical logic lives in # bounded-systems/.github-private CLAUDE.md; adopt-claude-harness keeps this -# copy in sync. Verifies every byte against the dialog-recorded -# $ORG_BOOT_SHA256 and REFUSES rather than running anything unverified. +# copy in sync. Verifies every byte against a RESOLVED digest and REFUSES rather +# than running anything unverified. +# +# THE DIGEST SOURCE CHANGED, AND THIS SCRIPT WAS INERT FLEET-WIDE UNTIL IT WAS +# FIXED (2026-08-17, .github-private#534/#581). It used to REQUIRE +# $ORG_BOOT_SHA256 and exit 0 with "not a bounded-systems cloud session" when it +# was unset. That variable was RETIRED OUTRIGHT on 2026-08-16 evening +# (.github#192): the boot digest now rides the channel manifest, written only by +# the OIDC-pinned boot-manifest lane on main, and the front-desk dialog is down +# to a single variable (BS_ROUTES_CONFIG). Measured 2026-08-17 from a live +# front-desk session whose dialog matches the recorded union exactly: the dialog +# carries BS_ROUTES_CONFIG and nothing else, ORG_BOOT_SHA256 is unset, and this +# script printed "not a bounded-systems cloud session; nothing to do" and +# installed nothing — in a session that was exactly that. It exited 0 while +# doing so, so the failure was SILENT and every adopter had a dead step 1. +# +# The resolution order below matches CLAUDE.md's step-1 block, which was already +# corrected: legacy $ORG_BOOT_SHA256 honored first WHILE IT EXISTS, otherwise the +# channel manifest. With neither resolvable it refuses rather than falling +# through to an unverified file — `bash` only ever runs bytes that hashed to the +# resolved digest. set -uo pipefail -[ -n "${ORG_BOOT_SHA256:-}" ] || { - echo "org-repair: ORG_BOOT_SHA256 unset — not a bounded-systems cloud session; nothing to do" - exit 0 -} R="${CLAUDE_SESSION_ROOT:-}" # resolve the checkout root — never assume /home/user [ -f "$R/.github/.claude/boot.sh" ] || R="$PWD" [ -f "$R/.github/.claude/boot.sh" ] || R="${PWD%/*}" @@ -19,18 +34,26 @@ if cmp -s "$C/stop-hook-git-check.sh" "$R/.github/.claude/stop-hook-git-check.sh echo "bootstrap in effect" exit 0 fi -B="$R/.github/.claude/boot.sh" # attached checkout — same bytes, no egress -echo "$ORG_BOOT_SHA256 $B" | sha256sum -c --status - 2>/dev/null || { +F="curl -fsSL --retry 3 --retry-connrefused --retry-max-time 60 --connect-timeout 5 --max-time 30" +# Legacy variable first while it still exists anywhere, then the channel +# manifest (.github#192). The manifest is written only by the OIDC-pinned +# boot-manifest lane on main, so it is a trust source, not a convenience. +S="${ORG_BOOT_SHA256:-$($F https://boot.bounded.tools/channel/front-desk.json 2>/dev/null \ + | sed -n 's/.*"boot":"\([0-9a-f]\{64\}\)".*/\1/p')}" +# The attached checkout is tried BEFORE the network on purpose: the repair must +# work when egress is down, which is exactly when a resumed session is most +# likely to be repairing by hand. +B="$R/.github/.claude/boot.sh" +{ [ -n "$S" ] && echo "$S $B" | sha256sum -c --status - 2>/dev/null; } || { B="" - curl -fsSL --retry 3 --retry-connrefused --retry-max-time 60 --connect-timeout 5 \ - --max-time 30 "https://boot.bounded.tools/$ORG_BOOT_SHA256.sh" -o /tmp/boot.sh \ - && echo "$ORG_BOOT_SHA256 /tmp/boot.sh" | sha256sum -c --status - && B=/tmp/boot.sh; } + [ -n "$S" ] && $F "https://boot.bounded.tools/$S.sh" -o /tmp/boot.sh \ + && echo "$S /tmp/boot.sh" | sha256sum -c --status - && B=/tmp/boot.sh; } if [ -n "$B" ]; then CLAUDE_SESSION_ROOT="$R" bash "$B" D="$R/.github/.claude/session-start-dispatch.mjs" [ -f "$D" ] || D=/opt/bounded-boot/session-start-dispatch.mjs CLAUDE_SESSION_ROOT="$R" node "$D" else - echo "org-repair: REFUSED — no copy matched \$ORG_BOOT_SHA256; installing nothing" + echo "org-repair: REFUSED — no verified copy (no digest source, or no bytes matched); installing nothing" exit 1 fi \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 8dc78cd..1ee3ebe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -262,16 +262,30 @@ deno task docs 1. **Bootstrap**: run `bash .claude/org-repair.sh` — pre-approved where this repo's settings carry the org floor; generator-owned repos may still prompt. - Its only trust anchor is the dialog-recorded `$ORG_BOOT_SHA256` (typed by a - human in the environment dialog, never fetched): it executes a local or - fetched copy ONLY if the bytes hash to that value, and refuses otherwise. + It executes a local or fetched copy ONLY if the bytes hash to a resolved + digest, and refuses otherwise. The digest comes from the channel manifest + (written only by the OIDC-pinned `boot-manifest` lane on main), with the + legacy `$ORG_BOOT_SHA256` honored first while it still exists — that + variable was retired outright on 2026-08-16 (`.github`#192) and is absent + from the dialog, so the manifest is the live source. `bootstrap in effect` → continue. `REFUSED` or a permission denial → stop and report; do not work around it. -2. **Claim before working**: dispatch `claim-ticket.yml` in - `bounded-systems/.github` (workflow_dispatch: `repo`, `issue`, `claimant`), - then confirm the claim comment ON THE ISSUE names your claimant. Any - assignee or `claimed` label → someone else's. Window unreachable → claim by - hand (assign + comment) and say the window was down. No issue → open one. +2. **Claim before working** — doors, best reachable first (`#529`): + 1. `claim-ticket.yml` in `bounded-systems/.github` (workflow_dispatch: + `repo`, `issue`, `claimant`) — the real door, lease-backed. Reachable + only from a session created with `.github` attached; mid-session + `add_repo` refuses it. + 2. `claim-relay.yml` in `bounded-systems/.github-private` + (workflow_dispatch: `issue`, `claimant`) — for `.github-private` issues + when door 1 is unreachable. Bot-authored, run-backed record; it does + **not** authenticate the claimant (`#530`). + 3. Hand-claim (assign + comment) — **last resort only**, when no door is + reachable. It provides **no exclusion** (keycard#7, `signerSelfAsserted`) + — a marker, not a claim; say plainly the window was down. + + Whichever door: confirm the claim comment ON THE ISSUE names your claimant. + Any assignee or `claimed` label → someone else's; do not start. No issue → + open one and claim it. 3. **Degraded mode**: no "bounded-systems — Claude context" block in your session context means the org context did not load. You may claim and work THIS repo only — no org-level `[settings]`/`[org]` changes, no cross-repo