BF-08 — AI-native client layer: typed SDK + reactive useClinicalQuery (LISTEN/NOTIFY) + propose-only local MCP server - #33
Conversation
…rkspace scaffolds + gate reconciles
Operator prep, all off the maker floor (drizzle/**, docker/**, gate configs,
loop/**) or pre-landed floor scaffolds (BP-023 new-workspace convention):
- drizzle/0011_projection_notify: statement-level AFTER I/U/D triggers emitting
pg_notify('bonfire_projection_change', TG_TABLE_NAME) — constant, tenant-free,
PHI-free payload (channels have no ACLs and payloads are spoofable; subscribers
re-query through their own RLS-scoped tx, so tenant scoping of data never rides
the wire). Stamped by a ddl_command_end event trigger (0004/0005 ratchet
pattern) so triggers survive projections:rebuild's DROP+CREATE — proven live
post-migration (8 triggers before AND after a full rebuild).
- packages/sdk + packages/mcp minimal scaffolds: exports map with @bonfire/source
first (BP-031), NodeNext, deps frozen into bun.lock (@modelcontextprotocol/
sdk@1.29.0, @microsoft/api-extractor@7.58.9 exact pins). Maker owns everything
inside src/ from here; dependency blocks + bun.lock stay operator-owned.
- docker/api.Dockerfile: COPY both new workspace manifests (BP-023 guard).
- .dependency-cruiser.cjs: mcp-depends-only-on-core -> mcp-depends-on-sdk-and-core.
Two independent design reviews converged: the MCP must consume the product
through @bonfire/sdk's session-bound client (ONE identity/tenant boundary);
re-implementing verifyToken->resolveMembership->withTenant inside mcp is a
second authz boundary that drifts. sdk -> mcp stays forbidden; deep src/
imports stay blocked.
- sgrules/no-egress-in-mcp (+tests): no shell/fs/network/hosted-SDK imports and
no fetch/WebSocket/Bun.spawn under packages/mcp/src (ratchet candidate BP-038).
- sgrules/no-authz-attr-from-request widened: args|input|toolArgs|toolArguments
join the untrusted-source list (MCP tool-argument objects), + fixtures.
- knip.json / root tsconfig references for both workspaces.
- tasks.ts BF-08 contract-drift reconcile (disclosed): drizzle/migrations/** and
loop/evals/bf-08/** were DEAD paths; depcruise topology prose updated to the
reviewed mcp->sdk edge. Ledger: BF-08 active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…calQuery) + @bonfire/mcp (propose-only 3-tool allowlist) Maker diff, floor = packages/sdk/**, packages/mcp/** (root tsconfig untouched — prep wired refs). Strict slice gate PASS 14/14 vs base 4af2037; sdk 17/0, mcp 13/0, turbo gate 33/33 (incl. both api-extractor CI gates). @bonfire/sdk: - auth/session.ts (U2): authenticate = verifyToken -> resolveMembership -> BF-13 audit -> BonfireSession (#private-branded, type-only export, no deserializer); createSessionVerifier REQUIRES maxTokenAgeSeconds. - run-op.ts + ops.ts: ONE executor (withTenant + session-derived subject + Result flatten + SDK_UNEXPECTED catch); method inputs Omit<'subject'>, effective input = { ...callerInput, subject } session-LAST (U2 control). - ir.ts + gen/main.ts + generated/client.gen.ts: deterministic byte-idempotent codegen (sorted, no timestamps); createBonfireClient(db, session). - reactive/: 8-view whitelist (compile+zod), LISTEN-first with snapshot inside onListen (no missed-commit window; reconnect resync same path), 150ms trailing debounce, single-flight, sha256 content-diff emission (a foreign practice's activity can't surface even as timing), sql(view) only after whitelist; structural ProjectionListener (no raw postgres import in src). - api-extractor surface pin (etc/sdk.api.md, CI mode; flip-check proven). @bonfire/mcp: - tools.ts (U1/U4): static 3-tool ALLOWLIST (search_clinical, get_context, propose_resource), strict schemas with NO identity/scope field, purpose pinned to core's TREAT (literal-typed against enum reorder), fixed code->static-text error table (error.message never forwarded), search text JSON-wraps every stored-data field (D3), get_context chains search->buildCcp entirely server-side (D4 — response never round-trips through the model) and emits CcpDocument.text VERBATIM, propose_resource description is HONEST (live ungoverned write; BF-09 owns governance) with minimal confirmation. - server.ts: per-session McpServer (CVE-2026-25536 posture), single registerTool callsite iterating ALLOWLIST, handlers close over the session-bound client only (D2); sole @modelcontextprotocol/sdk importer. - main.ts (D5): env-only identity, fail-closed non-zero exit + zero tools on auth failure, db.end() on shutdown; api-extractor pin (etc/mcp.api.md). - Package-level turbo.json (both): test dependsOn @bonfire/sql-on-fhir#test — joins the serialized DB lane (BP-036) since both suites touch fhir_resources and read vd_* under turbo concurrency. Tests pin the danger checks: propose-only (exact tools/list + unlisted-tool deny with zero side effects + api.md has no approve/commit), cross-tenant (B-session search of A's term = 0 hits + receipt(B); reactive B-update suppressed for A with re-query PROVEN ran), fail-open (off-schema pre-handler deny + handler-ran spy; deny-audit oracle), injection (instruction-in-arg stays data; forged-span leaf stays one JSON-escaped token), U2 inversion (smuggled clinician subject overwritten by session's biller). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…(U2) Adversarial harden pass (6 probe lenses x independent Opus refuters over the security modules U1-U4). One finding survived refutation; five were killed as non-defects (allowlist array not frozen = no public mutation path; lookup-fault audited as no-membership = fail-closed, not attacker-inducible; etc.). FINDING (LOW, test gap — product code was already correct): session.test.ts asserted only the deny CODE on every authenticate() failure path, so the smallest mutation "denyAudited -> skip the auditAuthFailure call" left the whole suite GREEN while failed-auth forensics silently disappeared (BF-13 acceptance #7: every failed auth appends a deny row to the SYSTEM hash chain). FIX: the no-membership deny test now keys a SYSTEM-chain oracle off a unique actor_id (iss#sub with a random sub) and asserts exactly one row, decision=deny. Keyed on actor_id, NOT a global count(*) — core's own auth-audit test warns the SYSTEM chain is shared across suites, so a count-delta would be racy under parallel/turbo runs. The verify-deny path is not oracled this way (its actor is the constant "unverified", so it cannot be isolated per-test). Empirically re-verified by the operator what three blocked probe lenses could not (their shell probes were refused by a safety check): - core DOES enforce maxTokenAgeSeconds (verify-token.ts passes jose maxTokenAge and adds "iat" to requiredClaims) — the SDK's required field is real, not decorative. - the MCP off-schema test smuggles a practiceId extra key and asserts isError PLUS zero audit rows, i.e. a true pre-handler oracle (strict rejection), not an isError-only tautology; tools/list pins additionalProperties:false, and no tool schema exposes an identity/scope key. - main.ts env coercion is fail-closed in the directions that matter: BONFIRE_MAX_TOKEN_AGE_SECONDS="" -> reject (exit 1); BONFIRE_ALGORITHMS="" -> reject; BONFIRE_CLOCK_TOLERANCE_SECONDS="" -> 0, which is STRICTER than the 30s default, never looser. DB-free CI parity green at this commit: tsc -b, eslint (full program), biome ci, ast-grep test 12/12 + scan, check:comments, knip, jscpd, depcruise, semgrep scan + rule-tests 4/4, no inline suppressions. The DB-backed suites (sdk 17, mcp 13) and the strict slice gate were green at d45ce01; they must be RE-RUN against this commit once the local Docker stack is available (the host ran out of disk and the daemon will not start). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st (positive control)
5-agent panel (contract verifier + Opus security auditor + 3 Opus danger-class
refuters + synthesis). Zero HIGH/BLOCKING code defects: all three refuters broke
nothing at high confidence, the auditor returned PASS, and the verifier's "FAIL"
is explicitly a NEEDS-LIVE-PROOF placeholder (the local Docker stack is down;
its two BLOCKING items are missing proof, not broken code). Fixes applied:
R1 [LOW, real deviation from the U3 delta] packages/sdk/src/reactive/use-clinical-query.ts
The snapshot digest hashed JSON.stringify(rows), not the mandated canonical
form, so the "practice B's write must not emit for practice A" suppression
leaned on postgres.js key order staying accidentally stable. Now:
sha256Hex(canonicalizeJson(JSON.parse(JSON.stringify(rows)))).
The JSON round-trip is LOAD-BEARING and comes FIRST: a timestamptz arrives as
a JS Date, and canonicalizeJson(Date) would take the object branch, find no
own enumerable keys, and emit "{}" — collapsing every timestamp to identical
bytes, so a last_updated-only change would emit NOTHING. The panel's literal
suggestion (canonicalizeJson over driver rows) would have introduced exactly
that missed-emission bug; the round-trip normalizes Dates to ISO strings via
toJSON first, then RFC-8785 sorts keys so the digest cannot drift on column
order. Verified against packages/core/src/db/canonical-json.ts:16-42.
L4 [LOW] packages/sdk/src/reactive/use-clinical-query.test.ts
loadRows orders EVERY whitelisted view by (id, row_index) but only
vd_patient_demographics is exercised end-to-end. A view missing either column
errors -> loadRows returns null -> the store keeps a stale snapshot forever
(fail-safe, never cross-tenant, but silently non-reactive). New catalog test
pins the column convention across all eight CLINICAL_VIEWS.
M2 [MED, auditor] sgrules/mcp-import-allowlist.yml (+ fixtures) — POSITIVE CONTROL
no-egress-in-mcp is a DENYLIST and therefore evadable by anything it forgot.
The agent-facing import surface is now an ALLOWLIST: packages/mcp/src may
import only @bonfire/sdk, @bonfire/core, @modelcontextprotocol/sdk, zod, and
its own modules; ANY other specifier (every node builtin included) fails the
gate closed. Covers `export ... from` too. Tests exempt (they seed/spy to
prove the invariants). Ratchet candidate BP-039.
INVERSION-PROVEN: clean tree -> ast-grep scan exit 0; a real `import
{ readFileSync } from "node:fs"` added to server.ts -> exit 1 with BOTH rules
firing; restored -> exit 0. 21 rule fixtures (10 valid / 11 invalid) pass.
REJECTED FIRST: the same control as a dependency-cruiser rule is INERT — the
config's `includeOnly: "^(apps|packages|loop|...)"` filters node builtins and
node_modules out of the graph, so a node:fs edge is never a candidate and the
rule cannot fire. Proven by injecting the import: depcruise still reported
"no dependency violations". A guard that cannot fire is worse than none, so it
was reverted in favour of the ast-grep rule above.
M2b sgrules/no-egress-in-mcp.yml — closed the GLOBAL-reachable bypasses an import
allowlist structurally cannot see: Bun.$`...`, Bun.file, Bun.write, Bun.connect,
Bun.listen (joining fetch/globalThis.fetch/WebSocket/EventSource/Bun.spawn).
N1 [NIT] packages/mcp/src/tools.ts — ALLOWLIST is Object.freeze()d, so the
three-tool surface is immutable at runtime, not merely `readonly` to tsc.
Panel findings deliberately NOT fixed here (disclosed in the PR body):
- The write path runs no decide() and appends no audit row (core/write-resource.ts).
Real intra-tenant authz/audit gap, RLS-scoped and honestly described; BF-09 owns
propose->approve->commit governance. No fabricated receipt is returned.
- core/src/ccp/serialize.ts header fields are not JSON-encoded (BF-07 floor, out of
this slice). Non-exploitable today: every such field is a z.uuid(), a fixed FHIR
literal, a DB-generated timestamp, or a static leaf-path entry — none can carry a
newline. Flagged to the BF-07 owner.
- Per-statement pg_notify takes a cluster-wide commit lock: a single tenant spamming
writes is a throughput ceiling. Deliberate v0 posture; upgrade path is one
coalesced NOTIFY per tx.
- authenticate() has no outer try/catch (it trusts each dependency's Result
contract). Refuter-3 proved this still fails CLOSED: a throwing verifier rejects
main(), the process exits non-zero, and the server is never constructed.
- Tool name propose_resource connotes staging that does not exist yet; the emphatic
description and the minimal confirmation output are the mitigation (delta D1).
DB-free CI parity green at this commit: tsc -b, eslint (full program), biome ci,
ast-grep test 13/13 + scan, check:comments, knip, jscpd, depcruise, semgrep,
no inline suppressions. The DB-backed suites (sdk 17, mcp 13) and the strict slice
gate were green at d45ce01 and MUST be re-run once the local Docker stack is back
(host disk exhausted; daemon will not start). Remaining live probes tracked in the
PR: 7 danger inversions, clean-room fresh-volume boot, strict gate --strict, plus
the panel's two extra probes (live tools/list inputSchema shape; commit-boundary
attribution — the latter already settled: d45ce01 touches only packages/sdk|mcp/**).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI green at
|
…ard + a coin-flip CI flake), BP-038/BP-039 guarded, ledger done (#34) * BF-08 close-out (part 1): BP-038 + BP-039 guarded, ledger BF-08 done Operator wave on merged main (5efa4c2). Harness-only diff (loop/**, docs/loop/**). RATCHET 36 -> 38 guarded / 1 open (BP-003 greptile, deferred — not wired): - BP-038 (mcp-egress) GUARDED by sgrules/no-egress-in-mcp.yml. The MCP server is the first place an untrusted agent enters the system; nothing structurally kept a handler from importing node:child_process/node:fs/a network client and turning a tool call into shell execution or PHI egress. The rule bans the import vectors AND the global-reachable ones an import allowlist structurally cannot see (fetch, globalThis.fetch, WebSocket, EventSource, Bun.spawn/spawnSync/$/file/ write/connect/listen), plus require()/dynamic import() of the banned modules. - BP-039 (mcp-import-surface) GUARDED by sgrules/mcp-import-allowlist.yml. A denylist is only as good as its enumeration — a client it never heard of, a transitive re-export, or a package added next quarter fails OPEN. The import surface of packages/mcp/src is now an ALLOWLIST: its own modules, @bonfire/sdk, @bonfire/core, @modelcontextprotocol/sdk, zod. Everything else — every node builtin included — fails the structural gate. Recorded in the KB entry because it will save the next reader a day: the SAME control written as a dependency-cruiser rule is INERT. `.dependency-cruiser.cjs` sets `options.includeOnly` to the source dirs, which filters node builtins and node_modules out of the graph, so a `from packages/mcp/src, to node:fs` edge is never a candidate and the rule can never fire. Proven by injecting the import: depcruise still reported "no dependency violations". Inversion-proven in its ast-grep form instead — clean tree `ast-grep scan` exit 0; a real `import { readFileSync } from "node:fs"` in server.ts exit 1 (both rules fire); restored exit 0. Ledger: BF-08 done. `loop state list` folds BF-01..08 + BF-13 all done. Gates green (DB-free): tsc -b, eslint (full program), biome ci, ast-grep test 13/13 + scan, knip, jscpd, depcruise, semgrep, loop ratchet (38 guarded/1 open). DEFERRED to close-out part 2 (needs a live PG18+pgvector stack; the dev host's Docker is currently unavailable): the bf08-* Stage-2 execution evals with their mutation canaries, the 7 danger inversions, the clean-room fresh-volume boot, and the strict-gate re-run. The slice's acceptance criteria were proven by CI on PR #33 (sdk 18 + mcp 13 + loop eval 29/0 against a real Postgres); what remains is proving the tests are load-bearing, which no CI run can establish. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * BF-08 close-out (part 2): 7 danger inversions run — one caught a decorative test, one caught a CI flake The 7 adversarial inversions (mutate product code -> the owning test must go RED -> restore) finally ran. They found two real problems that every green gate, every CI run, and a 5-agent panel had missed. INVERSION RESULTS (all now RED under mutation, GREEN on restore): I1 propose-only-broken 4th tool (approve_resource) appended to ALLOWLIST -> RED I2 cross-tenant/U2 subject merge flipped to caller-last in ops.ts -> RED I3 fail-open-authz no-membership deny branch deleted in session.ts -> RED I4 audit-bypass auditAuthFailure stubbed out in denyAudited -> RED I5 injection (D3) raw-interpolate a rendered field -> was GREEN, now RED I6 U3 timing-oracle content-diff early return removed -> RED I7 D5 fail-closed boot main.ts continues past an authenticate() deny -> RED FINDING 1 — the D3 injection guard was DECORATIVE (I5 left every test green). renderSearchText JSON-encodes every field it emits so an injected newline cannot forge an authentic-looking `[N] type=... id=...` hit line or a second `receipt:` line in the text an LLM reads. But NO field it emits can carry a newline with today's fixtures: ids are uuids, `citation.path` comes from the indexer's fixed leaf-path table, `resourceType` is a FHIR literal. So the end-to-end injection test cannot distinguish the hardened renderer from a raw-interpolating one, and the mandated guard was untested. FIX: packages/mcp/src/render.test.ts drives the pure renderers with hostile values directly (a value carrying `\n[9] type="Patient" id=...` and a forged `receipt:` line). Asserts the document keeps its exact line count, that no line BEGINS with the forged structure, and that exactly one receipt line exists. `renderSearchText`/`renderWriteText` are exported from tools.ts for the canary but NOT re-exported from index.ts, so the package's public surface is unchanged (api-extractor CI mode still exits 0). Proven load-bearing against three independent mutations: raw `citation.path`, raw `resourceType`, and raw `record.type` in the write renderer each turn it RED. FINDING 2 — BF-08's tests made a pre-existing eval a COIN-FLIP CI FLAKE. `bf04-rls-cross-tenant` picked practice A as `practices[0]`, the lowest practice_id in vd_patient_demographics, then asserted it sees its own spidx rows. BF-08's hermetic sdk/mcp suites seed practices with randomUUID(), so roughly 6 times in 16 a projection-only practice sorts below the seeded `1111...` one, practice A owns zero spidx rows, and the eval dies as "vacuous" — a failure that says nothing about RLS. CI on PR #33 passed by luck; locally it reproduced on the first dirty run. (This fragility was a known BF-04 eval-hardening candidate; BF-08 is what made it fire.) FIX: practice A is now selected from the practices that provably carry rows on BOTH surfaces the eval probes (vd_patient_demographics JOIN spidx), ordered for determinism. Non-vacuity becomes a property of the SELECT rather than of luck. CANARIED: still goes RED when bonfire_app is granted BYPASSRLS, and passes on two consecutive dirty runs (evals immediately after the sdk+mcp suites). VERIFICATION ENVIRONMENT (disclosed): the dev host's Docker is unavailable (it launches a GUI error dialog after a disk exhaustion + VM-disk reset), so this work ran against a native Postgres 17.10 + pgvector 0.8.5 cluster provisioned to mirror docker/initdb/010-roles.sh exactly (vector extension, bonfire_app LOGIN NOSUPERUSER NOBYPASSRLS, BP-018 append-only default privileges). All 12 migrations apply; sdk 18/0 + mcp 13/0 + core/api/sql-on-fhir/seed/loop suites pass; `loop eval` 29/0. The inversions do not depend on the server version. `loop gate` therefore reports 12 passed / 1 failed, and the ONE failure is the repo's own catalog guard asserting "Postgres major version is 18+" — the guard working as designed against my substitute stack. The strict slice gate on PG18 remains owed and is what CI provides on this PR. Gates green here: turbo gate, eslint (full program), biome ci, ast-grep test 13/13 + scan, knip, jscpd, depcruise, semgrep, api-extractor CI mode (both packages), loop eval 29/0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
BF-08 — the AI-native client layer
Ships the typed SDK, the reactive
useClinicalQuerystore over Postgres LISTEN/NOTIFY, and a local propose-only MCP server. This is the first place an untrusted agent enters the system, so the slice makes two things structural rather than hoped-for:subjectcomes from the BF-13-verified membership row, never from client input. The last two slices' panels kept flagging thatbuildCcp/searchClinicaltrust an in-processsubject. BF-08 closes it: generated method inputs areOmit<CoreInput, "subject">and the client merges{ ...callerInput, subject }session-last, so asubjectsmuggled in at runtime is always overwritten by the membership-derived one. This is the real control —subject.practiceIdalready failed closed against the GUC, butsubject.roleandsubject.idare trusted straight intodecide()and the audit row.db/sqlhandle ever enters handler scope.Commit chain
4af2037mcp → sdkreconcile, sgrules, contract-drift reconcile, ledgerd45ce01packages/sdk/**+packages/mcp/**only — 30 files, 30 tests97a741c403aec3Object.freezeAllowed-paths attribution verified:
git diff --name-only 4af2037..d45ce01lists onlypackages/sdk/**andpackages/mcp/**. Every off-floor file (drizzle, sgrules, depcruise, knip, docker, loop) lives in the disclosed operator commits.Design decisions (two independent Opus design reviews, pre-implementation)
mcp → @bonfire/sdk, notmcp → core. Both reviewers converged: making the MCP re-implementverifyToken → resolveMembership → withTenantwould create a second authz boundary that drifts — the classic fail-open. The depcruise rule was reconciled in prep (disclosed);sdk → mcpstays forbidden.get_contextchainssearch → buildCcpentirely server-side. TheSearchResponsenever round-trips through the model, which removes the forged-receipt surface and the unbounded-resultsDoS lever (ccpInputSchema.response.resultshas no.max).writeScribeResourceruns nodecide()and appends no audit row — it is a live, ungoverned commit. The tool says so, returns a minimal confirmation, and fabricates no policy receipt. Contract acceptance H4 — memory spine: STATE ledger + bug-patterns KB + loop ratchet + VERDICT schema #8's receipt requirement is scoped to "the underlying read", which the two read tools satisfy.onListen, so one idempotent path covers both first load and every reconnect; snapshot-then-listen would leave a missed-commit window.pg_notifyany channel, so tenant scoping of data never rides the wire: a spoofed NOTIFY is a harmless re-query, and emission requires the session's own RLS-scoped content digest to change.De-risk (proven live before any code)
projections:rebuild(8 triggers before and after); delivery is at-commit only (rollback → silence); listener latency ~2 ms against a 2 s budget.@modelcontextprotocol/sdk@1.29.0+ zod 4 + bun: unknown tool and off-schema argument are rejected before the handler runs, returned asisErrorresults rather than JSON-RPC rejections (tests assertresult.isError, not rejected promises).@microsoft/api-extractor@7.58.9against TS 6.0.3: CI mode exits 0 on an identical report and 1 on drift; needsnewlineKind: "lf"andcustomConditions: [](BP-031's@bonfire/sourcecondition would otherwise resolve deps tosrc).Review: 5-agent panel + 6-lens adversarial harden pass
Zero HIGH/BLOCKING code defects. All three danger-class refuters (propose-only-broken, cross-tenant-leak, fail-open+injection) broke nothing at high confidence; the security auditor returned PASS. Attacks attempted and blocked: subject smuggling via spread order /
__proto__/ getters (defeated byOmit+ session-last merge + zod strip), forged receipt intoget_context(server-side chain +receiptMatches), spoofed NOTIFY (constant payload + content-diff), off-schema and unlisted-tool calls (double validation, zero side effects), error-text injection (fixed code→static-string table;error.messagenever forwarded).Findings that were real and are fixed here:
97a741c):session.test.tsasserted only the deny code, so the mutation "stop callingauditAuthFailure" left the whole suite green while failed-auth forensics vanished. Now oracled against the SYSTEM hash chain, keyed on a uniqueactor_id— deliberately not acount(*)delta, which races under turbo's parallel suites.403aec3): the snapshot hash usedJSON.stringify(rows), leaning on postgres.js key order staying accidentally stable. Nowsha256Hex(canonicalizeJson(JSON.parse(JSON.stringify(rows)))). The JSON round-trip is load-bearing and comes first:canonicalizeJsonover driver rows would serialize aDateas{}(no own enumerable keys), collapsing every timestamp so alast_updated-only change would emit nothing. The round-trip normalizes Dates to ISO strings, then RFC-8785 sorts keys.403aec3):no-egress-in-mcpwas a denylist, evadable by anything it forgot.packages/mcp/srcmay now import only@bonfire/sdk,@bonfire/core,@modelcontextprotocol/sdk,zod, and its own modules; everything else fails closed. Inversion-proven: clean tree →ast-grep scanexit 0; a realimport { readFileSync } from "node:fs"→ exit 1; restored → exit 0. The same control written as a dependency-cruiser rule was rejected as inert — the config'sincludeOnlyfilters node builtins out of the graph, so it could never fire (proven by injection). Global-reachable vectors an import allowlist structurally cannot see (Bun.$,Bun.file,Bun.write,Bun.connect) were added to the denylist.Verification status — please read before merging
Green at
d45ce01: strict slice gate 14/14 (loop gate --slice BF-08 --base 4af2037 --strict), sdk 17/0, mcp 13/0, turbo gate 33/33 including both api-extractor CI gates.Green at
403aec3(this HEAD), DB-free:tsc -b, full-programeslint .,biome ci, ast-grep 13/13 + scan,check:comments, knip, jscpd, depcruise, semgrep, no inline suppressions.Not re-run locally at this HEAD: the DB-backed suites and the strict gate. The dev machine's disk filled and the Docker daemon will not start, so
97a741c(test-only) and403aec3(which touches the reactive digest) have not been exercised against a live Postgres locally. CI is the live proof for this PR — it boots its own stack and runs the suites,bun run loop eval, conformance, and the round-trip/PHI gates. Do not merge on red CI.Still owed after CI is green (operator, next session): the 7 danger inversions (mutate → RED → restore) against a live stack, a clean-room fresh-volume boot, and the panel's extra probe of the live
tools/listinputSchemashape.Deferred / accepted residuals (disclosed)
decide()and appends no audit row — a real intra-tenant authz/audit gap, RLS-scoped and honestly described. BF-09 owns propose → approve → commit governance; a biller-session-proposes-must-be-denied eval belongs to that slice.packages/core/src/ccp/serialize.tsheader fields are not JSON-encoded, contradicting that file's own docblock. Out of this slice's floor; non-exploitable today (each such field is az.uuid(), a fixed FHIR literal, a DB-generated timestamp, or a static leaf-path entry — none can carry a newline). Flagged to the BF-07 owner.pg_notifytakes a cluster-wide commit lock: a tenant spamming writes is a throughput ceiling. Deliberate v0 posture; upgrade path is one coalesced NOTIFY per transaction.authenticate()has no outer try/catch (it trusts each dependency'sResultcontract). Proven still fail-closed: a throwing verifier rejectsmain(), the process exits non-zero, and the server is never constructed.propose_resourceconnotes staging that does not exist yet; the emphatic description and minimal confirmation output are the mitigation. Revisit the name when BF-09 lands real governance.refresh()is the escape hatch, polling is the upgrade path. Onlydb.end()reclaims the listen socket.bf08-*Stage-2 evals + theno-raw-response-string-in-mcp-serializesgrule are the post-merge close-out wave (loop/**is maker-forbidden). Ratchet candidates: BP-038 (mcp egress denylist), BP-039 (mcp import allowlist).🤖 Generated with Claude Code