Skip to main content

Realm Config Inheritance — Implementation Plan (amended)

Decisions: (A) owning-realm credential provenance, (B) general parent-chain inheritance. Status: red-team verdict = amend_then_ship. Architecture sound; 14 must-fixes folded in below. Source artifacts: realm_synthesis.json (design), realm_redteam_verdict.json (red-team). This doc is the merged, authoritative plan.

Canonical design

CANONICAL DESIGN — “One chain, two consumers”: a single typed RealmChain (parent edge on RealmConfigSection, terminal global root) drives BOTH (a) an eager whole-Config composition (EffectiveConfig) for top-level fields the agent reads flat, AND (b) a lazy per-resolution walk for connection/credential facts that feeds materialize_connection_target the OWNING realm’s own RealmConnectionSet. WHY THIS SHAPE (resolving the three judges’ fatal flaws):
  1. DECISION A provenance is STRUCTURALLY free and keeps the load-bearing typed invariant intact. The only realm-stamping site that matters (materialize_connection_target, connection.rs:977) sets AuthBindingRef.realm = realm.realm_id. The lazy walk hands materialize the OWNING realm’s own RealmConnectionSet (its realm_id IS the owner). Therefore registry.rs:208 if auth_binding.realm != realm.realm_id (a STRICT typed equality, verified) STAYS UNCHANGED and KEEPS HOLDING. The eager-flatten designs (Approach 1 sidecar maps, Approach 2 single-set-multi-owner) BOTH forced a downgrade of that equality to an unwrap_or/membership check — a silent fallback alongside typed validation = dogma violation. We reject eager flattening of the connection set precisely to preserve this. No new field on AuthBindingRef, BindingOrigin, or RealmConnectionSet → ZERO wire/schema churn for the credential path.
  2. DUAL-TRUTH (top-level model/mcp/skills/hooks/limits unreachable from the connection resolver) is resolved by introducing ONE chain authority (RealmChain) consumed by both layers. Top-level inheritance is done by an EAGER EffectiveConfig composition produced once at the ConfigStore seam (the agent then reads the already-composed flat Config exactly as today — zero changes to the ~30 flat read sites at factory.rs:1022/3039/etc). Connection facts use the SAME RealmChain lazily. There are not two chain definitions; there is one RealmChain::resolve, called by the EffectiveConfig composer and by the connection resolvers.
  3. AUTH/BACKEND cross-realm credential-redirect hazard is resolved by NO-INHERIT for auth & backend profiles: a binding’s referenced backend_profile/auth_profile are resolved ONLY within the owning realm’s own section (the realm that defines the binding). This makes binding-owner == backend-owner == auth-owner an INVARIANT, so lookup_auth_binding (which co-resolves binding+backend+auth in ONE RealmConnectionSet, connection.rs:1200) never sees a split. A child that wants a parent’s backend/auth must inherit the whole binding (which carries them) or redeclare all three. This is the only coherent rule given the single-set lookup coupling.
ABSTRACTION-LEVEL MODEL (filesystem-free):
  • A realm is a node in a config graph. RealmConfigSection.parent: Option<RealmId> is the single outgoing edge (linear chain, no diamonds). A realm with parent=None that is not itself global implicitly parents to the reserved global realm. global.parent MUST be None (root). env_default is NOT a chain node; it remains the single typed post-chain fallback owner.
  • RealmChain::resolve(config, head) walks edges head→…→global, dedup + depth-capped + cycle-checked, producing an ordered Vec<RealmId> (index 0 = most-derived consuming realm; last = global if present). This is the ONE deterministic ordering that replaces the flat cross-realm scan (connection.rs:912) AND the [preferred,“default”] list (connection.rs:678-683).
  • EffectiveConfig: a composer folds the full Config along the chain (parent-first, child-wins per the merge table) to produce the flat Config the agent reads. It is produced by a ChainComposingConfigStore decorator that wraps a per-realm config-section locator. The locator is INJECTED (dyn RealmConfigSource), so the filesystem projection (realm_paths_in<state_root>/<realm>/config.toml) stays in meerkat-cli/meerkat-store, never in core. WASM injects a degenerate single-realm source (its synthesized config.realm[‘default’], lib.rs:441) so its chain is [default] with no global → behaviorally identical to today.
  • Connection/credential resolution: the resolvers walk RealmChain and, per resolved binding, materialize using the OWNING realm’s own RealmConnectionSet::from_config(owner_id, owner_section). AuthBindingRef.realm = owner. Downstream (TokenKey/LeaseKey/FileTokenStore) is unchanged and already correct.
STATE NEVER INHERITS: composition reads only config sections of ancestor realms. Sessions/leases/event-log/ops-snapshots/.rkat projection are keyed by the CONSUMING realm via realm_paths_in(head) only; the composer never touches ancestor RealmPaths except to READ config. The one state-adjacent fact that follows the OWNING realm is the credential TokenKey.realm (decision A) — that is provenance of where a credential is DEFINED, not inheritance of credential state. GLOBAL vs env_default stay DISTINCT and typed: global is a durable Configured realm at the chain root (may hold persisted OAuth tokens at <creds_root>/global/<binding>.json, may publish a durable lease). env_default is SyntheticEnvDefault, ephemeral, never a chain node, appended last only when allow_env_default and nothing on the chain matched. Both recognized by typed predicates (RealmId::is_global mirroring is_env_default), never by raw string compares elsewhere. A parent edge targeting the env_default slug is rejected at chain construction. CLI LOGIN migrates from hardcoded “dev” to global: a single login lands at <creds_root>/global/<binding>.json and every realm inherits it via the default chain. Pre-1.0 clean break: existing dev/* tokens orphaned, one-time re-login.

Provenance design (decision A)

CARRIER (unchanged): AuthBindingRef.realm: RealmId (connection.rs:196) remains THE single fact that determines credential/token scope. TokenKey::from_auth_binding (auth/token_store.rs:59) copies it verbatim; FileTokenStore::path_for projects <root>/<realm>/<binding>[@profile].json (file.rs:33); LeaseKey::from_auth_binding (handles.rs:1320) mirrors it. NO new field is added to AuthBindingRef or BindingOrigin. MECHANISM (the one change): make AuthBindingRef.realm the DEFINING realm, not the consuming realm, by feeding materialize_connection_target the OWNING realm’s OWN RealmConnectionSet. materialize stamps realm.realm_id (connection.rs:977-982, UNCHANGED). Because the lazy walk builds RealmConnectionSet::from_config(owner_id, owner_section) for the chain member that defines the binding, realm_id IS the owner. The existing weak provenance (stamp whatever realm the flat scan happened to find it in) becomes STRONG provenance (the defining chain member). In the degenerate single-realm case behavior is byte-identical. WHY NO new field / NO eager-flattened multi-owner set: a single RealmConnectionSet can carry only ONE realm_id. Approach 1 bolted on sidecar binding_owner maps and Approach 2 used a single set with a separate provenance map — both then had to relax registry.rs:208 auth_binding.realm != realm.realm_id (a STRICT typed equality, verified at the live consumer) into an unwrap_or/membership check, i.e. a silent fallback alongside typed validation (dogma violation). The lazy owning-set design keeps that equality a genuine invariant: the set fed to registry.resolve IS the owner’s set, so realm_id == auth_binding.realm by construction. This is the decisive reason the credential layer is lazy, not eager. RESOLVER (meerkat-auth-core/src/resolver.rs): ZERO logic change. It does no realm selection — it consumes binding.auth_binding_ref().realm verbatim (resolver.rs:437 → TokenKey). Once the connection layer stamps the owning realm, ManagedStore reads <root>/<owner>/<binding>.json, the AuthMachine LeaseKey is keyed by owner, all automatically. One-canonical-path: provenance is decided ONCE upstream, never re-derived at the credential layer (the flat cross-realm scan that smelled like weak provenance is DELETED, not extended). BOTH stamping sites + the model-swap filter are converged in ONE pass (judge requirement): (1) core materialize via the chain walk; (2) factory resolve_selected_binding_for_provider (factory.rs:1838, image/web_search) re-rooted to walk selected_realm’s chain and stamp the owning member; (3) factory model-swap filter (factory.rs:3114) relaxed to accept inherited owners on the chain. Any one left consuming-stamped misses inherited creds. SOURCE-KIND CAVEAT (must be pinned by a test, stated in design): decision A only BITES for ManagedStore (and the OAuth-lease / Command-freshness LeaseKey) because those are realm-namespaced via TokenKey/LeaseKey. CredentialSourceSpec::Env (resolver.rs:45, reads process env globally with RKAT_ override), InlineSecret, and Command MATERIAL are realm-AGNOSTIC — an inherited Env-source binding yields identical creds regardless of owning realm. The design states this explicitly so no operator expects env-key provenance to be realm-scoped. An RCT pins it.

Cycle & determinism

CYCLE / DEPTH / MISSING-PARENT (RealmChain::resolve, private ctor, fail-closed typed Result, NO unwrap/panic):
  • Seed: insert head into seen: BTreeSet<RealmId> first (so a realm naming itself as parent is caught as a 1-cycle), push head into ordered Vec.
  • Loop: read section.parent for the current node. (a) parent not in config.realm AND parent != global → RealmChainError::MissingParent. (b) parent == env_default slug → RealmChainError::ParentIsEnvDefault (env_default may never be a chain node). (c) seen.insert(parent) == false → RealmChainError::Cycle with the captured path. (d) push parent, continue.
  • Depth guard: MAX_REALM_CHAIN_DEPTH = 16; exceeding → RealmChainError::DepthExceeded (belt-and-suspenders even though seen bounds finite configs; bounds work + stack).
  • Termination: when current node’s parent is None. If that terminal node is global, done. Else append global IFF config.realm.contains_key(global) AND global ∉ seen (a realm may legitimately reach global via explicit edges; dedup prevents double-visit). If global absent from config, chain terminates at the explicit root and env_default fallback applies post-chain.
  • global invariant: if the head or any visited node is global and global.parent.is_some() → RealmChainError::GlobalHasParent.
  • ITERATIVE (no recursion) → wasm stack-safe and depth-guard-safe.
DETERMINISM:
  • The chain is a LINEAR sequence (parent is Option<RealmId>, a single edge, NOT a list) → order is fully determined by the edges, ZERO dependence on BTreeMap-key iteration order. This is strictly MORE deterministic than the deleted flat scan (which depended on config.realm BTreeMap alpha order at connection.rs:912).
  • Folding for EffectiveConfig is parent-first (root→head), making child-wins a deterministic last-write-wins over BTreeMaps; within each section BTreeMap iteration preserves existing determinism. No HashMap/HashSet anywhere (seen is BTreeSet).
  • Connection candidate ordering = the chain sequence head→…→global, then env_default — one deterministic order replacing the old (preferred, “default”, alpha-scan, env_default).
  • selected_binding_id_for_provider iterates realm.bindings (BTreeMap) → deterministic per-member pick. First-marked provider_default wins by id order (unchanged).

Contract / schema / wasm / machine

CONTRACT/SCHEMA:
  • RealmConfigSection.parent is SCHEMA-INVISIBLE. Config.realm is wire-projected through the opaque ConfigContractSchema.realm: BTreeMap<String, Value> (contracts/wire/config.rs:62); RealmConfigSection is absent from wire-types.json/params.json/rest-openapi.json (verified). → NO regen-schemas trip from the parent field, verify-schema-freshness does NOT fire, verify-version-parity (version-string only) does NOT fire.
  • AuthBindingRef / BindingOrigin: UNCHANGED (provenance rides the existing realm field, no new variant/field). So the REST bare-AuthBindingRef path (rest-openapi.json via RestCreateSessionRequest, emit.rs:904) and the lossy WireAuthBindingRef projection (drops origin, wire/connection.rs:41) are BOTH untouched → no SDK codegen ripple, no verify-rest/rpc-surface-alignment churn.
  • RealmConnectionSet: gains NO field (lazy approach) → its hand-written WireRealmConnectionSet projection (wire/connection.rs:247) and the auth_binding_wire.rs struct-literal test are untouched. This is the deliberate economy vs Approach 2 (which broke the struct literal + forced threading provenance into meerkat-llm-core + relaxing registry.rs:208).
  • New ConnectionTargetError::RealmChain(RealmChainError) variant: internal Result, NOT wire-exposed → no schema impact.
  • DEFENSIVE GATE STEP after implementation: run make regen-schemas && make verify-version-parity and ASSERT a NO-OP git diff (do not assume). If any schema file changes, a wire type leaked unexpectedly — STOP and re-audit.
  • SDK config-set round-trip: a client setting [realm.X] config cannot see/validate parent (opaque Value map by design) — acceptable; an RCT pins that the unknown field round-trips through the opaque map.
WASM:
  • meerkat-core stays wasm32-clean. RealmChain, RealmChainError, compose_effective_config, the per-area merge, and ChainComposingConfigStore are pure in-memory data/logic over Config (no std::fs, no tokio). RealmConfigSource and any IO are injected behind the existing cfg(not(wasm32)) gates (FileConfigStore already cfg-gated, config.rs:121). The async_trait pattern uses the existing #[cfg_attr(target_arch="wasm32", async_trait(?Send))] shape (config_store.rs:35).
  • WASM web-runtime synthesizes only config.realm[‘default’] (lib.rs:441) and injects a degenerate single-realm RealmConfigSource with no global → its chain is [default], EffectiveConfig == the single doc, behaviorally identical to today. Provenance still flows via AuthBindingRef.realm into the external resolver. ManagedStore stays cfg(not(wasm32)) (resolver.rs:237). GATE: BuildBuddy wasm-check-submit (buildbuddy.yml:439).
MACHINE-AUTHORITY:
  • Realm/parent resolution + Config composition touch NO canonical machine (verified: connection.rs/config.rs have no MachineSchema/generated reducer; AuthMachine, catalog/dsl/auth_machine.rs, governs only per-binding LEASE lifecycle keyed <realm>:<binding>, consuming an already-resolved identity, doing no realm selection). Freeze governance RESPECTED: chain invariants (cycle/depth/single-parent/global-root/env-default-not-parent) enforced by PLAIN RUST — typed RealmChain newtype with private field + fallible ctor + typed RealmChainError. NO new DSL/machine domain.
  • RMAT: ForbiddenShellAuthorityReads (xtask rmat_policy.rs:446) targets machine-owned field reads (.phase(), apply_mode, etc.); parent-chain reads config DATA, not machine-owned semantic state → does NOT trip the read-seam gate, PROVIDED resolution is not (wrongly) routed through a machine field read. GATE: make rmat-audit.
  • The provenance change DOES re-key the AuthMachine LeaseKey to the OWNING realm for inherited bindings — correct (the lease for an inherited binding lives at its owning realm), pre-1.0 clean break (any in-flight lease previously keyed under consuming realm is re-keyed, no migration).
REGEN/GATE SEQUENCE: (1) make regen-schemas → assert no-op diff. (2) make verify-version-parity → green. (3) make verify-schema-freshness → green. (4) ./scripts/repo-cargo clippy --workspace -- -D warnings. (5) MEERKAT_BUILDBUDDY=1 make test (unit+int). (6) make rmat-audit. (7) BuildBuddy wasm-check.

Merge-semantics table

Type / function changes

  • RealmConfigSection.parent [meerkat-core/src/connection.rs:1272 (struct body, add after default_binding at :1280)] — Add #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option<RealmId>. Typed newtype, NOT String. Schema-INVISIBLE: Config.realm is wire-projected through the opaque ConfigContractSchema.realm: BTreeMap<String, Value> (contracts/wire/config.rs:62), and RealmConfigSection is absent from every emitted schema (verified). NO regen needed for this field alone. RealmConfigSection already derives schemars under feature=schema (:1271) so the field compiles under that feature with no extra annotation.
  • GLOBAL_REALM_SLUG + RealmId::is_global + RealmId::global [meerkat-core/src/connection.rs:148 (next to ENV_DEFAULT_REALM_SLUG) and :150-159 (impl RealmId block)] — Add pub const GLOBAL_REALM_SLUG: &str = "global";. Add RealmId::is_global(&self) -> bool { self.as_str() == GLOBAL_REALM_SLUG } mirroring is_env_default. Add RealmId::global() -> RealmId { RealmId::from_known_valid(GLOBAL_REALM_SLUG) } (infallible mint via the existing private-ctor path used for env_default at :1168).
  • RealmChain (new type) [meerkat-core/src/connection.rs (new, near RealmConfigSection ~:1262)]pub struct RealmChain { realms: Vec<RealmId> } — PRIVATE field, single fallible ctor pub fn resolve(config: &Config, head: &RealmId) -> Result<RealmChain, RealmChainError>. Walks parent edges (BTreeSet seen-dedup + Vec order), appends global iff config.realm contains it and not already seen, fail-closed on cycle/depth/missing-parent/global-has-parent/parent-is-env-default. Accessor pub fn realms(&self) -> &[RealmId]. Iterates head→global (child-first). NO unwrap/panic.
  • RealmChainError (new typed error) [meerkat-core/src/connection.rs (new, IdentityError-adjacent family)]#[derive(Debug,Clone,Error,PartialEq,Eq)] pub enum RealmChainError { Cycle{chain:Vec<String>}, DepthExceeded{head:String,max:usize}, MissingParent{realm:String,parent:String}, GlobalHasParent{realm:String}, ParentIsEnvDefault{realm:String} }. NOT wire-exposed (internal Result). Add MAX_REALM_CHAIN_DEPTH: usize = 16 const next to GLOBAL_REALM_SLUG. Add From<RealmChainError> for ConnectionTargetError (new variant ConnectionTargetError::RealmChain(RealmChainError) at connection.rs:617).
  • compose_effective_config (new fn) [meerkat-core/src/config.rs (new, near merge :278)]pub fn compose_effective_config(sections: &BTreeMap<RealmId, Config>, head: &RealmId) -> Result<Config, RealmChainError> — takes the per-realm Config docs along the chain (root-first), folds them via per-area merge rules (see merge table) into the effective flat Config the agent reads. Reuses RealmChain::resolve on the head’s chain (the realm graph is read from the head doc’s .realm map plus injected ancestor docs). The realm MAP itself is also merged (map-key-union child-wins) so an inherited [realm.X] section is visible to the connection resolvers.
  • Config::merge rework (field-level child-wins + realm-map fold) [meerkat-core/src/config.rs:278-357] — Rework so it (1) ACTUALLY folds self.realm (today it is silently dropped — verified, merge never references self.realm) via map-key-union child-wins; (2) uses FIELD-LEVEL child-wins for limits/store/comms/compaction/rest (today whole-section whole-replace at :324-337 prevents partial child override); (3) keeps hooks.entries.extend append (:350) and adds skills/mcp_servers append/union; (4) keeps model_fallback catalog-reset (:352). Presence-aware where serde(default) loses omitted-vs-default: parse ancestor docs from toml::Value for the presence-sensitive scalar fields (see open questions). This is the SINGLE merge engine; compose_effective_config calls it parent-first.
  • RealmConfigSource (new trait) + ChainComposingConfigStore (new decorator) [meerkat-core/src/config_store.rs (new, near ConfigStore trait :37)]pub trait RealmConfigSource: Send+Sync { async fn config_for_realm(&self, realm: &RealmId) -> Result<Option<Config>, ConfigError>; } (?Send on wasm32 via the existing async_trait cfg pattern). pub struct ChainComposingConfigStore { head: RealmId, source: Arc<dyn RealmConfigSource>, catalog: ModelCatalog } implementing ConfigStore: get() resolves the head’s chain, fetches each member Config via source, composes via compose_effective_config, returns the effective Config. set()/patch() operate on the HEAD realm’s own doc only (writes never compose). NO fs in core — source is injected.
  • resolve_realm_binding_target_for_provider — chain-aware + owning-set [meerkat-core/src/connection.rs:665-750] — Replace candidate list [preferred,"default"] (:674-684) and the inline default_binding-ONLY pick (:700-719) with: build RealmChain from head (preferred or “default” or global); iterate chain members; per member build RealmConnectionSet::from_config(member, section) and call selected_binding_id_for_provider (CONVERGING the two divergent selection policies onto one); FIRST member yielding a provider binding is the owner; materialize with THAT member’s own set. Explicit_realm path: walk its chain so an explicit realm inherits a binding from its parent, stamping the OWNING member.
  • resolve_auth_binding_candidates_for_provider — chain-aware + owning-set [meerkat-core/src/connection.rs:886-966] — DELETE the flat scan for realm_id in config.realm.keys() (:912-914), the literal “default” push (:911), and push_candidate_realm_ids (:862, now unused). Build RealmChain from head; for each chain member build its OWN RealmConnectionSet and run selected_binding_id_for_provider; push one materialized candidate per member that has a provider binding (each carrying owner=member via materialize). env_default append (:942) UNCHANGED as the single typed terminal fallback.
  • resolve_auth_binding_or_default_for_provider — explicit ref chain-aware [meerkat-core/src/connection.rs:755-795] — Explicit-AuthBindingRef branch (:762-783): if the named realm’s own section does not define the binding, walk its chain to find the OWNING member, build that member’s set, materialize with owner=member. Keep is_env_default rejection (:764). One canonical path: provenance decided here, once, upstream.
  • materialize_connection_target — UNCHANGED logic, owner fed in [meerkat-core/src/connection.rs:968-1010] — NO code change: it already stamps realm.realm_id (:978). Correctness comes from callers feeding it the OWNING realm’s own RealmConnectionSet (realm_id==owner). registry.rs:208 equality continues to hold. Add a debug_assert documenting the single-writer invariant.
  • factory resolve_selected_binding_for_provider — chain-aware (SECOND stamping site) [meerkat/src/factory.rs:1806-1843] — Replace single-section from_config on selected_realm (:1814-1821) + realm: selected_realm stamp (:1838) with a chain walk rooted at selected_realm: find the owning member that defines a usable binding (reuse selected_binding_id_for_provider :1688), build that member’s OWN set, stamp realm=OWNER. Without this, image/web_search inherited bindings stamp the consuming realm and miss parent creds (judge fatal flaw). image (:1903) and web_search (:1936) callers flow through this fix automatically.
  • factory model-swap candidate filter — accept inherited owners [meerkat/src/factory.rs:3099-3126] — The catalog_default_chain filter target.auth_binding.realm == *realm (:3114) rejects inherited candidates (owner != preferred). Change to: accept any candidate on the preferred realm’s chain (i.e. drop the equality, keep !is_env_default()), since the chain walk already scopes candidates to ancestors. Otherwise hot model-swap silently drops every inherited binding (judge fatal flaw).
  • CLI login realm: dev → global (collapse 3 literals) [meerkat-cli/src/main.rs:4755, :4003, :5391, :5742] — CLI_INTERACTIVE_OAUTH_REALM_ID (:4755) → RealmId::global() (typed). auth_config_realm_or_default (:4003) unwrap_or(“dev”) → unwrap_or(GLOBAL_REALM_SLUG). ensure_cli_interactive_oauth_config (:4886) + resolve_configured_cli_interactive_oauth_target (:4977) take a target RealmId and synthesize/read [realm.global] into the GLOBAL realm’s ConfigStore (new seam: resolve a store for global, not always scope). noninteractive_login (:5322) gains a realm param (currently ignores override); TokenKey::parse(“dev”,..) at :5391/:5742 → resolved owner. All dev:<binding> user strings (:5417/:5489/:5687/:5699) → resolved owner. Update pinned test (not bypass) at :16447.
  • WireRealmConnectionSet / WireAuthBindingRef [meerkat-contracts/src/wire/connection.rs:237 and :22] — NO CHANGE. RealmConnectionSet gains NO field (lazy approach), so the hand-written WireRealmConnectionSet projection (:247) is untouched and realm/get returns the head realm’s OWN composed-but-uncomposed section as today. WireAuthBindingRef (drops origin) untouched. This is the deliberate economy that yields zero SDK codegen / surface-alignment churn.

MUST-FIX amendments (red-team)

  • MF-01 (amends canonical_design + P2; add gate to P2)
    • finding: Verified at connection.rs:681-682 and :911: TWO distinct legacy fallbacks exist. The flat scan for realm_id in config.realm.keys() (the plan deletes it) AND a hard-coded literal "default" realm candidate pushed unconditionally in BOTH resolvers (candidates.push("default") and push_candidate_realm_ids(..., Some("default"))). The plan’s type_changes for resolve_realm_binding_target_for_provider does mention replacing [preferred,"default"], but RCT-09 only pins the flat-scan removal, and no RCT pins that a [realm.default] shared-creds config is NO LONGER consulted for a head realm that does not parent to it. An operator with [realm.default] holding shared OpenAI creds, running in realm prod (no openai binding, no parent edge), resolves openai today via the implicit default candidate; after the cut the chain is [prod, global?] and resolution falls to env_default or hard-fails silently. No existing test covers default as a non-head fallback.
    • fix: State explicitly in canonical_design that the literal "default" realm candidate is REMOVED in BOTH resolvers alongside the flat scan (it is a distinct mechanism, not the same one). Decide and document the head-default: with WorkspaceDerived realms the head is ws-{hash}, not default/dev, so the implicit-global-tail is the only bridge. Add RCT-25 (below) proving a [realm.default] section is no longer consulted and the same creds work once moved to [realm.global]. The migration (MF-08) must rewrite the config SECTION too, not only copy the credential directory, or legacy single-realm users lose their only binding.
  • MF-02 (amends cycle_and_determinism + P2)
    • finding: Verified at connection.rs:764: the explicit-AuthBindingRef branch does config.realm.get(realm_id).ok_or_else(|| UnknownRealm) — an absent explicit realm hard-errors UnknownRealm, while the non-explicit path (resolve_realm_binding_target_for_provider) uses continue on an absent realm. The pinned test auth_binding_candidates_scan_configured_realms_before_env_default (connection.rs:2026) sets preferred_realm=missing (absent from config) and asserts candidates[0].realm==dev via the flat scan. The plan lists this test for rewrite in P2 but never specifies the TARGET behavior when the HEAD realm is absent from config: does RealmChain::resolve error, yield a single-node chain [head] contributing nothing, or implicitly append global? RealmChain::resolve reads section.parent but config.realm.get(head) is None so there is no section to read.
    • fix: Specify: an absent head yields a single-node chain [head] that contributes no sections (head’s own section None → skipped), then implicit-global-tail (if global present) + env_default apply exactly as today’s per-member continue path. Do NOT make an absent head a hard MissingParent/UnknownRealm error in the non-explicit path. Preserve the per-member asymmetry: explicit-named absent realm still errors UnknownRealm; non-explicit/preferred absent realm falls through. Pin with RCT-26.
  • MF-03 (amends type_changes (ConnectionTargetError) + P5/P6 gate)
    • finding: Verified at meerkat-rest/src/auth_endpoints.rs:106: target_error_status is an EXHAUSTIVE match over ConnectionTargetError with no wildcard, and ConnectionTargetError (connection.rs:618) is NOT #[non_exhaustive]. Adding ConnectionTargetError::RealmChain(RealmChainError) (plan type_changes) is a hard COMPILE ERROR in meerkat-rest. (The RPC mapping at handlers/auth.rs:200 uses _ => so RPC compiles — the critiques overstated an RPC compile break.) meerkat-rest is in no phase before P8, so this surfaces only at the broad lane.
    • fix: Add meerkat-rest/src/auth_endpoints.rs:106 to the edit-site list; add the RealmChain arm to target_error_status (map to BAD_REQUEST or NOT_FOUND per the inner error). Add -p meerkat-rest -p meerkat-rpc to the P5/P6 gates so the compile break and behavior change are caught before P8.
  • MF-04 (amends new phase P5b (REST/RPC auth re-root))
    • finding: Verified: REST resolve_binding_identity (auth_endpoints.rs:77-104) and RPC resolve_binding_identity (handlers/auth.rs:151-163) both build AuthBindingRef { realm: realm_id.clone(), ... } from the REQUEST realm and call realm.lookup_auth_binding on a SINGLE realm section (resolve_realm, single-section from_config). These are the OAuth/credential PERSISTENCE paths (require_managed_store_source → PreparedTokenCommitSnapshot → TokenKey). After decision A makes provenance = owning realm, a client POSTing creds for child realm team whose binding lives only in global either 404s (single-section lookup misses) or persists the TokenKey under team (request realm) while the chain-aware factory build path reads from global (owner) — credential written to one dir, read from another = silent auth failure on two network surfaces. REST resolve_oauth_target (auth_endpoints.rs:127) also calls resolve_realm_binding_target_for_provider with an explicit realm, so the explicit-realm chain-widening behavior change hits REST/RPC too. Both files are in NO phase.
    • fix: Add a phase (e.g. P5b) re-rooting resolve_binding_identity in BOTH meerkat-rest and meerkat-rpc onto the chain: resolve the owning chain member for (realm, binding) and stamp AuthBindingRef.realm = owner before lookup/persist, mirroring the factory. Decide explicitly whether REST/RPC credential-persist should inherit (walk chain) or stay strict-owner (write only to the named realm); document it. Add RCT-27 (REST) and RCT-28 (RPC).
  • MF-05 (amends new phase P5b/P6 (per-surface composer wiring))
    • finding: Verified: ChainComposingConfigStore is wired only at the CLI (P6, meerkat-cli/src/main.rs:3737). REST builds its own store at lib.rs:415 (FileConfigStore → TaggedConfigStore → ConfigRuntime), RPC at main.rs:226 (FileConfigStore → ConfigRuntime, set_config_runtime at :348), MCP-server at lib.rs:450 — NONE wrap the composer. ConfigRuntime.get() (config_runtime.rs:128) just returns store.get(). So top-level inheritance (model/mcp/hooks/skills/limits) AND the implicit-global-tail are silently dead on REST/RPC/MCP: config.realm on those surfaces contains only the head realm’s own doc, so the connection-layer implicit-global-tail won’t fire either (config.realm has no global key). Divergent resolution across interchangeable surfaces is exactly the regression class the dogma forbids.
    • fix: Wrap ChainComposingConfigStore at EVERY surface’s store-construction site (rest lib.rs:415, rpc main.rs:226, mcp-server lib.rs:450), OR compose inside ConfigRuntime once. Add surface-level RCTs (RCT-29) asserting an inherited [realm.global] model/mcp/cred is visible through each surface’s config_runtime.get(). Note the set(get()) hazard in MF-06 — the composer must NOT be the symmetric store the write path round-trips.
  • MF-06 (amends type_changes (ChainComposingConfigStore / new EffectiveConfigReader) + P3)
    • finding: Verified: ConfigStore (config_store.rs:37) is a SYMMETRIC trait — get()→Config, set(Config), patch(delta)→Config. The RPC config/set handler (handlers/config.rs:285) does prior = config_runtime.get() then config_runtime.set(config, ...). If ChainComposingConfigStore.get() composes the chain (inherited mcp/hooks/skills flattened in) and set() writes head-only (plan: ‘writes never compose’), then any read-modify-write round-trips the COMPOSED blob into the head doc, durably materializing every parent-inherited entry into the child. This permanently flattens inheritance and violates user decision 3 (children CANNOT remove inherited mcp/hook entries — after one set(get()) they own frozen copies they can never shed, and future parent edits stop propagating). Same shape exists wherever config is read-then-written.
    • fix: Do NOT have ChainComposingConfigStore implement the symmetric ConfigStore trait. Introduce a read-only EffectiveConfigReader (get_effective) consumed ONLY by the agent build path (factory) and surface config/get-for-resolution, and keep ConfigStore.get() returning the RAW head doc everywhere a write may follow. The RPC/REST prior-capture and patch path must read the raw head, never the composed view. ChainComposingConfigStore.set/patch must persist only the delta against the head doc’s PRE-composition state. Add RCT-30 pinning that a config write after a composed read does NOT flatten inherited entries into the head doc.
  • MF-07 (amends type_changes (RealmConfigSource) + P3/P6)
    • finding: Verified at config_store.rs:191: FileConfigStore::get() returns Ok(Config::default()) when the file is absent, NOT None. The plan’s RealmConfigSource trait returns Result<Option<Config>> and RCT-16 asserts ‘absent ancestor doc = empty pass-through (NOT Config::default clobber)’. If P6’s filesystem RealmConfigSource delegates to FileConfigStore::get for ancestor docs, an absent ancestor yields a full Config::default() folded into the chain. Because compose folds parent-first and model_fallback has a catalog-reset branch (config.rs:352, if other.model_fallback.use_catalog_default_chain → reset), a default ancestor can RESET model_fallback mid-chain and clobber any field where ‘default’ is meaningful. RCT-16 passes against a hand-rolled mock source but the real CLI source built on FileConfigStore violates it.
    • fix: The filesystem RealmConfigSource MUST check tokio::fs::try_exists itself and return None on absence (not call FileConfigStore::get). Document that config_for_realm returns None iff the file is absent, Some(parsed) otherwise. Strengthen RCT-16 to assert a None ancestor is byte-identical to that ancestor being omitted from the chain AND that model_fallback catalog-reset is not triggered by an absent ancestor. Add an RCT (RCT-31) exercising the REAL FileConfigStore-backed source, not only the mock.
  • MF-08 (amends new phase P6b (migration) + canonical_design/provenance_design prose)
    • finding: User-confirmed decision 1 is MIGRATE dev creds to global (one-time copy, not orphan). The plan CONTRADICTS this: canonical_design says ‘existing dev/* tokens orphaned, one-time re-login’, provenance_design repeats ‘pre-1.0 clean break, one-time re-login’, and open_question #6 re-litigates it. Verified: no phase contains migration code (grep for ‘migrat’ across P0-P8 = zero), and no RCT covers dev→global copy. Credential root is <base>/meerkat/credentials/<realm>/<binding>.json (mod.rs:78, file.rs path_for), so migration is a credentials/dev/* → credentials/global/* directory copy. Without it, every existing logged-in install fails auth on first run after upgrade because the resolver now stamps owner=global and reads credentials/global/ which is empty.
    • fix: Add an explicit migration phase (P6b) implementing the one-time idempotent copy: copy credentials/dev/<binding>credentials/global/<binding> IFF the target is absent (no clobber); leave dev/ intact or remove per decision. Reconcile canonical_design/provenance_design to say COPY, not orphan; remove open_question #6 (it is a settled decision). Add RCT-32: creates a dev token on disk, runs migration, asserts the credential resolves at owning realm global; re-running migration is a no-op; an existing global cred is not clobbered.
  • MF-09 (amends P5 (add the gate edit to the writes) + RCT-20 fixture)
    • finding: Verified at factory.rs:1894 (image) and :1924 (web_search): the chain-capable branch (resolve_realm_binding_for_provider with Some(selected_realm)) is taken ONLY when !config.realm.contains_key(selected_realm). When the selected realm EXISTS but its own section defines no image/web_search binding (the inheritance case), it falls to resolve_selected_image/web_search_binding_for_provider → resolve_selected_binding_for_provider (factory.rs:1806) which does single-section from_config(selected_realm) and stamps realm=selected_realm. The plan edits resolve_selected_binding_for_provider (P5) to walk the chain but does NOT edit the :1894/:1924 contains_key gate that decides which path is taken. So a child realm that EXISTS but inherits its image/web_search binding silently gets no binding. RCT-20 will pass if its fixture defines the binding directly in selected_realm, masking the gate bug.
    • fix: Edit the contains_key gates at factory.rs:1894 and :1924 so the chain walk is taken whenever the selected realm’s OWN section lacks the binding, not only when the realm is absent (or fold the chain walk into resolve_selected_binding_for_provider and route through it unconditionally for present realms). RCT-20’s fixture MUST define the image binding ONLY in the parent while selecting the child, to exercise the inherited path.
  • MF-10 (amends P5 (factory self_hosted) + P6 (CLI doctor); add to edit-site list)
    • finding: Verified two unlisted explicit-realm callers that P2’s chain-widening behaviorally changes: meerkat/src/factory.rs:3349 configured_self_hosted_connection has a config.realm.contains_key(realm_id) guard then calls resolve_realm_binding_target_for_provider(Some(realm_id), …); its fallthrough swallows errors to Ok(None) at :3377-3383 (silent). And CLI doctor self_hosted at meerkat-cli/src/main.rs:5981/5999 passes Some(preferred_realm). After P2, a self_hosted binding defined only in a PARENT of the explicit realm now resolves where it previously errored (owner=parent stamping) — a behavior change for rkat doctor and self-hosted client construction, with the Ok(None) swallow potentially masking a chain-resolution failure. No RCT covers self_hosted inheritance. These are concrete execution seams the plan’s edit-site enumeration misses.
    • fix: Enumerate ALL explicit-realm callers in the plan (factory.rs:3349, cli main.rs:5981/5999, rest auth_endpoints.rs:127, rpc handlers/auth.rs:188) and decide per-caller whether chain-widening is desired for self_hosted (or explicit no-inherit if that is the product choice). Edit the contains_key guard at factory.rs:3349 for the inherited case, and verify the Ok(None)-on-error swallow at :3377 does not mask a chain-resolution failure. Add RCT-33 for self_hosted inheritance via the explicit-realm seam.
  • MF-11 (amends merge_semantics_table + type_changes (Config::merge) + P3)
    • finding: Verified the merge ambiguity is load-bearing: merge_semantics_table marks realm.X.backend and realm.X.auth as ‘no-inherit (resolved within owning realm’s own section)’ (RCT-08), but compose_effective_config type_changes + RCT-15 say Config::merge ‘folds self.realm via map-key-union child-wins’ so ‘an inherited [realm.X] section is visible’. RealmConnectionSet::from_config (connection.rs:1057-1064, the actual line) resolves each binding’s backend_profile/auth_profile via backends.get()/auth_profiles.get() WITHIN the same section and errors UnknownBackend/UnknownAuth if absent. If ‘fold the realm map child-wins’ is read as unioning the backend/auth/binding SUB-maps across realms, no-inherit is destroyed and the credential-redirect hazard re-opens. If it means only outer realm-key union, the phrase is a no-op when the head doc already has the keys. The plan never states the level unambiguously, and RCT-08 + RCT-15 can both pass under a wrong implementation. Additionally, a child that declares a binding referencing a parent-only backend_profile will fail from_config(child) with a confusing UnknownBackend, with unspecified failure semantics.
    • fix: State explicitly: compose_effective_config unions ONLY the outer realm→section keys (adds ancestor realm entries absent from the head); it MUST NOT union backend/auth/binding sub-maps across realms. Each ancestor RealmConfigSection stays an intact map entry; the chain walk visits them as separate owning sets. Make the no-inherit-for-auth/backend rule fail-closed and validated: a child binding whose backend_profile/auth_profile is not present in the child’s OWN section either errors with a typed message naming the missing profile and realm, or requires redeclaring backend+auth. Add RCT-34: a child binding referencing a parent-only backend_profile fails closed with a typed error (distinct from RCT-08’s inherited-WHOLE-binding case).
  • MF-12 (amends cycle_and_determinism + P2 gate)
    • finding: Nearest-child-wins precedence is unpinned across BOTH chain consumers. resolve_auth_binding_candidates_for_provider returns an ORDERED Vec the factory iterates as fallbacks (build path consumes candidates[0]; model-swap at factory.rs:3114 finds first matching). Today’s order is preferred → literal ‘default’ → alpha-scan → env_default; the plan replaces it with chain order [head, parents…, global, env]. RCT-09 only asserts unrelated siblings are excluded; RCT-06/07 assert owner stamping. NONE assert the relative ORDER when MULTIPLE chain members each define a usable provider binding (head defines anthropic binding A, global defines anthropic binding B — which is selected first?). cycle_and_determinism discusses sequence determinism but never pins the PRODUCT choice of nearest-wins. A regression here silently switches which real credential/account the agent authenticates with. Also default_binding has divergent semantics: the connection path uses selected_binding_id_for_provider PER member (checks default_binding against THAT member’s own bindings), so a child default_binding pointing at a parent-owned binding id silently no-ops in the connection path while appearing ‘set’ in EffectiveConfig.
    • fix: Add RCT-35 pinning nearest-child-wins: when head and an ancestor both define a usable provider binding, candidate[0] is the head, ancestor next, global last, env terminal. Make nearest-child-wins an explicit invariant shared by compose_effective_config.default_binding selection AND the candidate resolver, with shared fixtures. Decide and document child.default_binding pointing at a parent-owned binding id: either resolve against the COMPOSED chain or document it as owner-section-scoped only; pin with an RCT.
  • MF-13 (amends P6 + canonical_design (global location); remove open_question #5)
    • finding: Verified the global-doc location conflicts with the workspace-rooted CLI store, breaking the headline cross-workspace promise of decision 2. default_cli_state_root (main.rs:3733) = context_root/.rkat/realms (context_root defaults to current directory = WORKSPACE-local). resolve_config_store (main.rs:3737-3753) uses realm_paths_in(scope.locator.state_root, realm) → <workspace>/.rkat/realms/<realm>/config.toml. P6 says the RealmConfigSource is ‘realm_paths_in based’, so realm_paths_in(state_root, ‘global’) → <workspace>/.rkat/realms/global/config.toml — a PER-WORKSPACE file, NOT the single ~/.rkat doc decision 2 mandates. global_config_path() (config.rs:217) = ~/.rkat/config.toml exists but is never wired into resolve_config_store. So login-once-to-global inherited across workspaces cannot work: each workspace gets an isolated global doc, or the source reads a location login never wrote to. open_question #5 re-litigates this settled decision.
    • fix: Make the global node’s RealmConfigSource entry HOME-rooted, not state_root-rooted: read the single dedicated doc at ~/.rkat (reuse global_config_path / a ~/.rkat/realms/global location independent of the workspace state_root). The composer must compose [workspace head doc] over [fixed home-rooted global doc]. Login must WRITE to the SAME home-rooted location the composer reads. Remove open_question #5 (settled by decision 2). Add RCT-36: login under workspace A is visible to a composed get() under workspace B (cross-workspace), asserting the exact physical write path equals the composer’s global source path; realm_paths_in(state_root,‘global’) is NOT the global source.
  • MF-14 (amends P3 + open_question #2 resolution)
    • finding: User decision 3 (children CANNOT remove inherited mcp/hook entries — no tombstones) is tested only POSITIVELY. RCT-13 (hooks append parent-first) and RCT-14 (mcp map-union child-wins) verify inherited entries are PRESENT and overridable, but no RCT asserts a child with NO mcp_servers / NO hooks STILL inherits all parent entries, or that a child setting an empty [tools]/[hooks] table does NOT drop inherited entries. Verified today merge_tools whole-replaces mcp_servers when non-empty (config.rs:380-382), so the rework to union must be tested for the no-removal property specifically. A future regression (child empty mcp map silently dropping inherited servers) would pass RCT-13/14 yet violate decision 3 — a child silently disabling an inherited security hook is an auth/safety regression.
    • fix: Add RCT-37: a child realm with NO mcp_servers and NO hooks still inherits ALL parent mcp_servers and hook entries; a child setting an empty [tools]/[hooks] table does not remove inherited entries (emptiness != removal, no tombstone honored). Resolve the hooks/skills dedup identity key (open_question #2) as a typed function before P3 so an inherited-then-redeclared hook does not double-run; extend RCT-13 to a 3-level chain asserting the exact ordered, deduped entry list.

RCT contracts — failing-tests-first

Original (RCT-01..24)

  • RCT-01 [meerkat-core] realm_config_section_parent_roundtrips_and_defaults_none — RealmConfigSection round-trips parent: Option<RealmId> through TOML/serde; absent parent == None; present parent = “global” parses to RealmId. Config.realm map serde unchanged.
  • RCT-02 [meerkat-core] global_realm_is_typed_and_distinct_from_env_default — RealmId::is_global / RealmId::global / GLOBAL_REALM_SLUG are typed; global is distinct from env_default (is_global != is_env_default); both recognized by predicate not raw compare.
  • RCT-03 [meerkat-core] realm_chain_resolves_linear_order_with_implicit_global_tail — RealmChain::resolve walks a linear chain head→parent→global in deterministic order; parentless non-global head implicitly appends global when present; global.parent=None terminates.
  • RCT-04 [meerkat-core] realm_chain_detects_cycle_depth_missing_global_and_env_default — RealmChain::resolve fails closed: self-parent → Cycle; A→B→A → Cycle; missing parent → MissingParent; global with parent → GlobalHasParent; parent=env_default → ParentIsEnvDefault; depth>16 → DepthExceeded. No panic.
  • RCT-05 [meerkat-core] realm_chain_omits_absent_global_and_falls_through_to_env_default — Absent [realm.global] → chain terminates at explicit root, no global contribution; env_default fallback still applies post-chain when allow_env_default.
  • RCT-06 [meerkat-core] inherited_binding_stamps_owning_realm_not_consuming_realm — OWNING != CONSUMING provenance: a binding defined ONLY in parent realm P, resolved while preferred realm is child C, yields AuthBindingRef.realm == P (the owner), NOT C. selected via the auth_binding-omitted path.
  • RCT-07 [meerkat-core] resolved_target_realm_equals_owning_connection_set_realm_id — materialize is fed the owner’s own RealmConnectionSet so registry-equality invariant auth_binding.realm == realm.realm_id HOLDS for inherited bindings (no relaxation).
  • RCT-08 [meerkat-core] inherited_binding_resolves_backend_auth_in_owning_realm_only — no-inherit for auth/backend: a binding inherited from parent P resolves its backend_profile/auth_profile in P’s OWN section; a child C redefining auth-profile-key K does NOT re-source P’s binding (binding-owner==backend-owner==auth-owner).
  • RCT-09 [meerkat-core] candidates_follow_parent_chain_not_flat_realm_scan — the flat cross-realm scan is replaced by the chain: a provider binding in an UNRELATED sibling realm (not an ancestor of head) is NOT auto-discovered; only chain members + env_default are candidates, in chain order.
  • RCT-10 [meerkat-core] single_target_path_uses_unified_selection_policy — single canonical selection policy: resolve_realm_binding_target_for_provider now honors provider_default and single-unambiguous (not default_binding-only) — same policy as selected_binding_id_for_provider, applied per chain member.
  • RCT-11 [meerkat-core] explicit_inherited_binding_resolves_at_owning_realm — explicit AuthBindingRef naming child realm C whose binding is defined only in parent P resolves at P (owner stamped P); explicit env_default ref still rejected.
  • RCT-12 [meerkat-core] effective_config_unions_model_defaults_child_wins — compose_effective_config: Config.models per-provider union child-wins (child anthropic default + inherited parent openai default both present); agent.model child-wins-scalar.
  • RCT-13 [meerkat-core] effective_config_appends_hooks_parent_first — compose_effective_config: hooks.entries append parent-first-then-child; child cannot delete parent hook; scalar hook timeouts child-wins.
  • RCT-14 [meerkat-core] effective_config_unions_mcp_limits_skills — compose_effective_config: tools.mcp_servers map-union child-wins per name; limits per-field child-wins (child tightens max_sessions, inherits other caps); skills append child-wins-on-collision.
  • RCT-15 [meerkat-core] config_merge_folds_realm_map_child_wins — Config::merge now folds self.realm (map-key-union child-wins) instead of silently dropping it; inherited [realm.X] section visible after merge.
  • RCT-16 [meerkat-core] chain_composing_store_produces_effective_config_no_default_clobber — ChainComposingConfigStore.get() composes the head’s chain via injected RealmConfigSource into the effective flat Config; absent ancestor doc = empty pass-through (NOT Config::default clobber).
  • RCT-17 [meerkat-auth-core] managed_store_inherited_binding_reads_owning_realm_token — ManagedStore credential of an inherited binding resolves the TokenStore at the OWNING realm dir (drop hardcoded ‘dev’): binding owned by realm P → TokenKey.realm==P → store key under P.
  • RCT-18 [meerkat-auth-core] env_source_binding_is_realm_agnostic — Env-source binding is realm-AGNOSTIC: an inherited Env-source binding resolves identical creds regardless of owning realm (decision A is a no-op for Env/InlineSecret/Command material).
  • RCT-19 [meerkat-auth-core] file_token_store_path_for_uses_owning_realm — FileTokenStore::path_for projects <root>/<owning_realm>/<binding>[@profile].json (currently ZERO tests on file.rs); owning realm drives the on-disk dir.
  • RCT-20 [meerkat] factory_image_binding_stamps_owning_realm — factory resolve_selected_binding_for_provider (image/web_search) stamps the OWNING realm for an inherited binding, not selected_realm; registry.resolve equality holds at the image path.
  • RCT-21 [meerkat] model_swap_keeps_inherited_chain_candidates — factory model-swap candidate filter accepts inherited candidates (owner != preferred) on the chain; hot model-swap does not drop inherited bindings.
  • RCT-22 [meerkat] build_agent_inherits_global_binding_resolves_at_global — end-to-end build_agent with preferred realm C inheriting binding from global resolves credentials at global (owning) realm and builds the client.
  • RCT-23 [meerkat-contracts] config_set_roundtrips_parent_through_opaque_realm_map — SDK/wire config-set round-trips an unknown [realm.X].parent through the opaque ConfigContractSchema.realm Value map without loss; regen-schemas no-op.
  • RCT-24 [meerkat-cli] cli_login_targets_global_and_realm_flag_composes_chain — CLI login lands credentials under global (resolved owner) not ‘dev’; status/test/refresh/logout read the same owning realm; —realm selects a realm AND composes its chain while NOT touching RuntimeScope (updated pin).

Added by red-team (RCT-25..38)

  • RCT-25 [meerkat-core] default_realm_literal_not_consulted_for_unrelated_head_then_works_under_global — A [realm.default] section holding a provider binding is NO LONGER auto-consulted for a head realm that does not parent to default (the literal "default" candidate is removed in both resolve_realm_binding_target_for_provider and resolve_auth_binding_candidates_for_provider, distinct from the flat scan); the SAME binding moved to [realm.global] resolves via the implicit-global-tail.
  • RCT-26 [meerkat-core] absent_head_realm_yields_single_node_chain_then_env_default — When the preferred/head realm is absent from config.realm, RealmChain::resolve yields [head] (head section None → contributes nothing) plus implicit-global-tail if global present; non-explicit resolution falls through to env_default when allow_env_default; an EXPLICITLY-named absent realm still errors UnknownRealm. Rewrites the pinned auth_binding_candidates_scan_configured_realms_before_env_default (connection.rs:2026).
  • RCT-27 [meerkat-rest] rest_oauth_persist_for_inherited_binding_stamps_owning_realm_and_maps_realmchain_status — POST OAuth/credential target for a child realm whose binding is defined only in a parent resolves at the OWNING realm (AuthBindingRef.realm=owner, TokenKey under owner dir) per the documented design, OR is rejected with a defined status; target_error_status maps the new ConnectionTargetError::RealmChain variant to a sane HTTP status (compiles).
  • RCT-28 [meerkat-rpc] rpc_oauth_persist_for_inherited_binding_stamps_owning_realm — RPC auth credential-persist (resolve_binding_identity / resolve_oauth_target) for an inherited binding resolves at the owning chain member and stamps AuthBindingRef.realm=owner before persist; the RealmChain error variant maps to a defined RPC error code.
  • RCT-29 [meerkat-rest] rest_config_runtime_get_reflects_inherited_global_section — An inherited [realm.global] model/mcp/credential is visible through the REST surface’s config_runtime.get() effective config (ChainComposingConfigStore wired at the surface), matching what the factory build path resolves for the same head realm. (Companion RPC/MCP variants assert the same per-surface composer wiring.)
  • RCT-30 [meerkat-core] config_set_after_composed_read_does_not_flatten_inherited_entries — A read-modify-write through the config surface (get effective → apply delta → set) does NOT durably materialize parent-inherited mcp/hook/skill entries into the head doc; after reload the head doc on disk contains ONLY its own fields plus the patched one, with inherited entries still resolved (not copied) on the next composed read. Proves get-composes/set-head-only is not round-trippable into a flatten.
  • RCT-31 [meerkat-cli] filesystem_realm_config_source_composes_real_temp_chain_no_default_clobber — The PRODUCTION filesystem RealmConfigSource over a real temp state_root with global/config.toml (binding B) and child/config.toml (parent=global, no binding) returns None for an absent ancestor (not Config::default), composes child top-level fields over global without default-clobber, and build_agent stamps owner=global for B.
  • RCT-32 [meerkat-cli] dev_to_global_credential_migration_idempotent_no_clobber — One-time migration copies credentials/dev/<binding>credentials/global/<binding> IFF the target is absent; an existing global credential is NOT clobbered; re-running migration is a no-op; after migration a pre-existing dev token resolves at owning realm global. (Implements user decision 1.)
  • RCT-33 [meerkat] self_hosted_explicit_realm_inherits_binding_at_owning_realm — configured_self_hosted_connection (factory.rs:3349) for an explicit realm that EXISTS but inherits its self_hosted binding from a parent resolves at the owning realm via the chain walk (not single-section), and the Ok(None)-on-error swallow does not mask a chain-resolution failure; the contains_key guard takes the chain path when the realm’s own section lacks the binding.
  • RCT-34 [meerkat-core] child_binding_referencing_parent_only_backend_profile_fails_closed — A child binding whose backend_profile/auth_profile is defined only in a parent realm fails closed with a typed error naming the missing profile and realm (proving compose_effective_config unions ONLY outer realm-keys and does NOT merge backend/auth/binding sub-maps across realms); distinct from RCT-08’s inherited-whole-binding case.
  • RCT-35 [meerkat-core] nearest_child_wins_when_head_and_ancestor_both_define_binding — When head and an ancestor both define a usable provider binding, candidate[0] is the head (nearest-child-wins), ancestor next, global last, env terminal; the same nearest-wins precedence governs compose_effective_config.default_binding selection and the candidate resolver over shared fixtures.
  • RCT-36 [meerkat-cli] global_login_under_workspace_a_inherited_in_workspace_b — Login writes [realm.global] to the home-rooted global doc (assert exact path = ~/.rkat-rooted, NOT <workspace>/.rkat/realms/global); a fresh ChainComposingConfigStore in a DIFFERENT workspace composes that global doc into its effective config and resolves the global-owned binding (cross-workspace inheritance, decision 2); RuntimeScope realm decoupling preserved.
  • RCT-37 [meerkat-core] child_cannot_remove_inherited_mcp_or_hook_entries — A child realm with no mcp_servers and no hooks inherits ALL parent mcp_servers and hook entries; a child setting an empty [tools]/[hooks] table does not drop inherited entries (emptiness != removal, no tombstone). Negative property for user decision 3.
  • RCT-38 [meerkat-core] explicit_env_default_ref_rejected_before_chain_walk — An explicit AuthBindingRef with realm==env_default (origin SyntheticEnvDefault) is rejected with UnknownRealm BEFORE any chain walk in the rewritten resolve_auth_binding_or_default_for_provider; no durable lease/token is created. Standalone negative test (split out of RCT-11) guarding the is_env_default check at connection.rs:764 against reorder during the explicit-path rewrite.

Phases & gates

P0 — Contracts & failing RCTs (Gate 0) (depends_on: none)

  • goal: Write ALL 24 RCTs as compiling-but-failing tests pinning every invariant BEFORE any implementation. Add the type STUBS they reference (RealmConfigSection.parent field, GLOBAL_REALM_SLUG/is_global/global, RealmChain/RealmChainError signatures returning unimplemented!() — wait, no panic: return a placeholder Err so library code stays panic-free; tests assert the real behavior and FAIL).
  • writes: meerkat-core: RealmConfigSection.parent field (connection.rs:1272), meerkat-core: GLOBAL_REALM_SLUG + RealmId::is_global/global + MAX_REALM_CHAIN_DEPTH (connection.rs:148), meerkat-core: RealmChain/RealmChainError type + ctor signature returning a typed Err placeholder (connection.rs), meerkat-core: compose_effective_config + Config::merge realm-fold signatures (config.rs), meerkat-core: RealmConfigSource trait + ChainComposingConfigStore skeleton (config_store.rs), RCT-01..05, 12..16, 23 as failing tests; RCT-06..11 as failing tests; RCT-17..19 (auth-core), 20..22 (meerkat), 24 (cli) as failing tests
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-core -p meerkat-auth-core -p meerkat -p meerkat-contracts -p meerkat-cli compiles; the 24 RCTs RUN and FAIL with the expected assertions (not compile errors). make regen-schemas no-op diff confirmed for the parent field.

P1 — RealmChain (deterministic walk + fail-closed) (depends_on: P0)

  • goal: Implement RealmChain::resolve: linear walk, BTreeSet dedup, depth cap, implicit global tail, all typed errors. Pure, wasm-clean.
  • writes: meerkat-core/src/connection.rs: RealmChain::resolve full impl + RealmChainError + From<RealmChainError> for ConnectionTargetError
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-core — realm_chain — RCT-02,03,04,05 GREEN. clippy -p meerkat-core clean.

P2 — Connection resolvers chain-aware + owning-set (decision A core) (depends_on: P1)

  • goal: Rewrite resolve_auth_binding_candidates_for_provider (delete flat scan), resolve_realm_binding_target_for_provider (unify selection), resolve_auth_binding_or_default_for_provider (explicit-ref chain) to walk RealmChain and feed materialize each owner’s OWN RealmConnectionSet. materialize unchanged.
  • writes: meerkat-core/src/connection.rs:886-966 (candidates), meerkat-core/src/connection.rs:665-750 (single-target), meerkat-core/src/connection.rs:755-795 (explicit ref), delete push_candidate_realm_ids (:862)
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-core — RCT-06,07,08,09,10,11 GREEN; ALL pre-existing connection.rs tests either GREEN or deliberately REWRITTEN (auth_binding_candidates_scan_configured_realms_before_env_default :2027 rewritten to chain order). clippy clean.

P3 — EffectiveConfig composition (top-level inheritance, the headline feature) (depends_on: P1)

  • goal: Implement compose_effective_config + Config::merge rework (fold realm map, field-level child-wins, hooks/skills append, mcp/limits union, presence-aware scalars) + RealmConfigSource trait + ChainComposingConfigStore.get/set/patch.
  • writes: meerkat-core/src/config.rs: compose_effective_config + reworked merge + presence-aware helpers, meerkat-core/src/config_store.rs: RealmConfigSource + ChainComposingConfigStore
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-core — RCT-12,13,14,15,16 GREEN; pre-existing config.rs merge tests (:2387,:2435-2746) GREEN or rewritten. clippy clean.

P4 — Credential resolver provenance verification (bottom-up, auth-core) (depends_on: P2)

  • goal: NO resolver logic change; ADD the missing RCTs proving owning-realm flows through TokenKey/FileTokenStore and Env is realm-agnostic. Fixes any test-double that hardcoded ‘dev’.
  • writes: meerkat-auth-core/src/resolver.rs tests: RCT-17, RCT-18, meerkat-auth-core/src/auth_store/file.rs tests: RCT-19 (first-ever tests on file.rs)
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-auth-core — RCT-17,18,19 GREEN; existing managed_store_* lifecycle tests GREEN. clippy clean.

P5 — Factory: second stamping site + model-swap filter + ConfigStore wiring (depends_on: P3, P4)

  • goal: Re-root resolve_selected_binding_for_provider to walk selected_realm’s chain and stamp owner; relax model-swap filter; wire ChainComposingConfigStore so factory consumes the effective Config.
  • writes: meerkat/src/factory.rs:1806-1843 (image/web_search owning-realm stamp), meerkat/src/factory.rs:3099-3126 (model-swap filter), meerkat/src/factory.rs build path: consume effective Config via ChainComposingConfigStore
  • GATE: ./scripts/repo-cargo nextest run -p meerkat — RCT-20,21,22 GREEN; existing factory provenance tests (factory.rs:7249,:7299,:6888 rewritten to chain) GREEN. clippy clean.

P6 — CLI login → global + ConfigStore chain wiring at surface (depends_on: P5)

  • goal: Collapse the 3 ‘dev’ literals to RealmId::global(); login writes [realm.global] into the global realm’s store; resolve_config_store composes the chain; update the pinned —realm test (not bypass).
  • writes: meerkat-cli/src/main.rs:4755,4003,4886,4977,5322,5391,5742 + user strings, meerkat-cli/src/main.rs:3737 resolve_config_store wraps FileConfigStore in ChainComposingConfigStore via a filesystem RealmConfigSource (realm_paths_in based), meerkat-cli/src/main.rs:16447 updated pin
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-cli (cargo int — rkat is bin-only) — RCT-24 GREEN; existing CLI login/oauth-config tests (:14004-14359) GREEN or rewritten. clippy clean.

P7 — Wire/SDK round-trip + WASM degenerate chain (depends_on: P5)

  • goal: Pin opaque-Value round-trip of parent; confirm WASM single-realm chain is identical to today; inject WASM RealmConfigSource.
  • writes: meerkat-contracts: RCT-23, meerkat-web-runtime/src/lib.rs: degenerate RealmConfigSource (no global) wiring
  • GATE: ./scripts/repo-cargo nextest run -p meerkat-contracts — RCT-23 GREEN. make regen-schemas && make verify-version-parity && make verify-schema-freshness ALL no-op/green. BuildBuddy wasm-check-submit green.

P8 — Broad final verification + governance gates (depends_on: P6, P7)

  • goal: Full-workspace verification, RMAT, e2e.
  • writes: no new code; fix any cross-crate fallout surfaced by the broad lanes
  • GATE: ./scripts/repo-cargo clippy —workspace — -D warnings; MEERKAT_BUILDBUDDY=1 make test (unit+int); make e2e-fast; make rmat-audit; make regen-schemas (no-op diff); make verify-version-parity. ALL green, ZERO failures (never dismiss as pre-existing).

Phase/gate corrections (red-team) — incl. NEW phases P5b, P6b

  • Add a new phase P5b ‘Surface auth re-root + composer wiring’: wrap ChainComposingConfigStore (or a read-only EffectiveConfigReader per MF-06) at REST lib.rs:415, RPC main.rs:226, MCP-server lib.rs:450; re-root resolve_binding_identity in meerkat-rest/src/auth_endpoints.rs:77 and meerkat-rpc/src/handlers/auth.rs:151 onto the owning-chain stamp; add target_error_status RealmChain arm at auth_endpoints.rs:106. Gate runs -p meerkat-rest -p meerkat-rpc -p meerkat-mcp-server with RCT-27/28/29/30.
  • Add a new phase P6b ‘dev→global credential migration’: one-time idempotent copy of credentials/dev/* → credentials/global/* (no clobber); reconcile canonical_design/provenance_design prose from ‘orphan’ to ‘copy’. Gate runs RCT-32. This phase is REQUIRED by user decision 1 and is currently entirely absent.
  • P2 gate: add RCT-25 (literal default removed), RCT-26 (absent head), RCT-35 (nearest-child-wins), RCT-38 (explicit env_default rejection) to the green set; explicitly state the rewritten target behavior for auth_binding_candidates_scan_configured_realms_before_env_default (connection.rs:2026) rather than only listing it for rewrite.
  • P3 gate: forbid REWRITING the existing file-layer merge tests (config.rs:2387, :2419, :2480, :3395) — the single reworked merge engine must leave global-file→project layering precedence unchanged; only NEW realm-fold tests may be added. Add RCT-30 (no set(get()) flatten), RCT-31 (real fs source no-default-clobber), RCT-34 (sub-map no-inherit fail-closed), RCT-37 (no-removal). Resolve the hooks/skills dedup identity key (open_question #2) before this gate.
  • P5 gate: add the factory.rs:1894/:1924 image/web_search contains_key gate edit to the writes (MF-09); add factory.rs:3349 self_hosted explicit-realm seam to the audit (MF-10); require RCT-20’s fixture to define the image binding ONLY in the parent. Add RCT-33.
  • P6 gate: resolve the global-doc location to home-rooted ~/.rkat (decision 2, MF-13) — the RealmConfigSource global node must NOT be realm_paths_in(state_root,‘global’); enumerate and migrate ALL CLI_INTERACTIVE_OAUTH_REALM_ID read sites (main.rs:4887, :4981, :5031) and auth_config_realm_or_default callsites together. Add RCT-36 (cross-workspace).
  • P8 gate: add make e2e-auth (and e2e-system for the token-store path) — this is an auth/credential-provenance change and the listed P8 lanes (unit+int, e2e-fast) do not exercise the real FileTokenStore owning-realm directory layout or OAuth lease re-keying. Add a deterministic int-lane RCT round-tripping a real FileTokenStore at the owning-realm dir for an inherited binding.
  • Gate 0 (P0): require each RCT to fail with the SPECIFIC assertion it pins, not a generic Err/unimplemented from the RealmChain::resolve typed-Err placeholder — otherwise a uniform-Err stub yields a false-green Gate 0. Split the type-stub compile check from the behavior-red check for RCT-03/05 (success-order/tail-omission) which a blanket-Err placeholder cannot fail-as-intended.

Risks & mitigations

  • risk: Eager-flatten temptation re-introduced for the connection set (sidecar maps or single-multi-owner set), forcing the registry.rs:208 equality downgrade — the exact dogma violation the judges flagged.
    • mitigation: HARD RULE in the design: RealmConnectionSet gains NO field; materialize is ALWAYS fed the owner’s own from_config set; RCT-07 pins that auth_binding.realm == resolved set realm_id for inherited bindings WITHOUT relaxing the equality. Any PR diff touching registry.rs:208 is a red flag.
  • risk: Top-level config inheritance becomes a SECOND uncoordinated chain authority (dual-truth), diverging from the connection-fact chain ordering.
    • mitigation: ONE RealmChain::resolve is the sole chain authority; compose_effective_config and the connection resolvers BOTH call it. RCT-09 (connection order) and RCT-12/13/14 (effective config) share the same chain fixtures so divergence fails a test.
  • risk: auth/backend cross-realm credential-redirect (a child redefining auth-profile-key K silently re-sources a parent-owned binding).
    • mitigation: no-inherit for auth/backend: binding’s profiles resolved ONLY in the owning realm’s section → binding-owner==backend-owner==auth-owner invariant. RCT-08 pins it with a child-redefines-K counterexample.
  • risk: Second stamping site (factory.rs:1838) or model-swap filter (factory.rs:3114) left consuming-stamped → decision A silently broken for image/web_search and inherited bindings dropped on hot swap.
    • mitigation: P5 converges BOTH in one pass; RCT-20 (image owning-realm) and RCT-21 (model-swap keeps inherited) gate it. Grep for realm: selected_realm and auth_binding.realm == *realm in final review.
  • risk: serde(default) loses omitted-vs-default for non-Option scalars (model, max_tokens, limits) → a child’s default value ‘overrides’ an inherited non-default via naive presence check.
    • mitigation: Presence-aware merge for the affected scalar fields: parse ancestor docs from toml::Value to detect key presence before applying child-wins-scalar (the same pattern that forced merge_*_from_toml_presence helpers, config.rs:412). RCT-12 includes an omitted-vs-explicit-default case. (Surfaced as open question for product confirmation of which fields are presence-sensitive.)
  • risk: Login-to-global requires resolve_config_store to open a store OTHER than scope.locator.realm; partial wiring (login writes one realm, resolution composes from another) is the concrete trap.
    • mitigation: P6 introduces an explicit global-realm store seam; RCT-24 asserts login lands at global AND status/test/refresh read the same owning realm; the 5 token-path sites move together in one phase.
  • risk: ChainComposingConfigStore re-reads ancestor docs on every get() — cost + cache-invalidation vs patch()/mcp-reload live config.
    • mitigation: get() composes fresh (always-correct, depth-capped, cheap); set()/patch() operate on the HEAD doc only (writes never compose), so a patch to an ancestor is naturally reflected on the next composed get(). No memoization in v1 (correctness over micro-opt); RCT-16 pins compose-on-get semantics.
  • risk: Deleting the flat scan breaks operators who relied on sibling-realm binding pickup.
    • mitigation: Documented behavior tightening: shared bindings move to [realm.global] (inherited by all via the implicit tail) or a named parent. RCT-09 pins the narrowing; CLI login-to-global makes the common shared-creds case work by default.
  • risk: Existing pinned tests (connection.rs:2027, factory.rs:6888, cli:16447) encode the OLD flat-scan / dev-realm behavior.
    • mitigation: REWRITE (not bypass) each to the chain/owning-realm semantics in the phase that owns the change; never #[ignore] them. Listed explicitly in P2/P5/P6 gates.
  • risk: CLI tests run only under cargo int (rkat bin-only), so a core-only ‘green’ skips every login/realm CLI test.
    • mitigation: P6 gate runs -p meerkat-cli under the int lane; P8 runs MEERKAT_BUILDBUDDY=1 make test (unit+int) workspace-wide.

Residual risks (red-team)

  • The lazy owning-set core (decision A) is genuinely sound and VERIFIED: feeding materialize_connection_target the owner’s own RealmConnectionSet keeps the strict equality at meerkat-llm-core/src/provider_runtime/registry.rs:208 (if auth_binding.realm != realm.realm_id) a real invariant without relaxation, adds no AuthBindingRef/RealmConnectionSet field, and yields zero wire/schema churn. This part needs no rework — guard it: any PR diff touching registry.rs:208 (qualify the crate path everywhere, it is meerkat-llm-core not core) is a red flag, and RCT-07 should ideally exercise the llm-core registry.resolve path across the core→llm-core boundary, not only core’s materialize stamping.
  • Several critiques framed settled user decisions as ‘open questions’ (plan open_question #5 = global location, #6 = orphan vs migrate). These are NOT relitigation candidates — they are plan-vs-decision contradictions that MUST be fixed (MF-08, MF-13). Do not let the implementer treat them as live design choices.
  • DISMISSED as overstated: the RPC compile break. handlers/auth.rs:200 target_error_response uses a wildcard _ =>, so the new ConnectionTargetError::RealmChain variant does NOT break RPC compilation — only REST’s exhaustive target_error_status (auth_endpoints.rs:106) breaks. The RPC BEHAVIOR change (credential-persist provenance) is still real and must be covered (MF-04).
  • DISMISSED as minor/non-blocking: the pinned-test line is 16447 (the plan is correct; one critique’s ‘16448’ was wrong). The registry crate misattribution (registry.rs:208 is in meerkat-llm-core) is a documentation fix, not a behavioral risk, but qualify it so an implementer does not edit meerkat-tools/src/registry.rs or meerkat-core by mistake.
  • validate_explicit_realm_id (runtime_bootstrap.rs:127) does not reserve global/env_default — a user can run rkat --realm global, creating a session realm whose state lands under <state_root>/global, potentially colliding with the credential-namespace assumptions of decision A, and the chain resolver only catches global.parent=Some lazily at resolve time. Lower severity than the must-fixes (fail-closed at resolve), but worth adding global/env_default to the reserved set and validating global.parent==None at config LOAD time for a clear config error instead of a runtime auth failure.
  • Presence-sensitive scalar field set (open_question #4) remains the single reworked merge engine’s sharpest edge: a child explicitly setting agent.model to the empty-string default to mean ‘use catalog default’ will be treated as unset by a naive != Default check and the parent’s model leaks through. Enumerate the exact set (agent.model, max_tokens, agent.max_tokens_per_turn, limits.*) and require toml::Value presence parsing per field before P3; add per-field omitted-vs-explicit-default RCT cases. This is correctness, not polish.
  • The ‘one chain, two consumers’ invariant (EffectiveConfig composer chain == connection resolver re-walk) is asserted via shared fixtures but never pinned by a single equality test. Strongly prefer threading the composer’s already-computed Vec<RealmId> through to the connection resolvers (literally one walk, two readers) over recomputing RealmChain::resolve from the composed config; if recomputed, add a direct equivalence RCT (walk over raw per-realm sources == walk over composed config) to prevent a lossy/ordering-sensitive fold from silently diverging the two.
  • The child-wins union on the .realm MAP can let a stale [realm.parent] copy in a child’s own doc shadow the authoritative parent-file section (which still passes the realm-ID equality at registry.rs:208 because it only compares IDs, not section provenance). Enforce that a member’s own [realm.<member>] section comes ONLY from config_for_realm(<member>), and reject (fail-closed) a doc declaring a [realm.X] section for X != its owning realm. Folds into MF-11’s outer-key-only union rule.

Settled product decisions (do NOT relitigate)

  1. MIGRATE existing dev creds to global (one-time idempotent copy, no clobber). Not orphan.
  2. global realm = dedicated home-rooted doc at ~/.rkat (single well-known location), not per-workspace.
  3. Children can add/override but cannot remove inherited mcp/hook entries (no tombstones).
  4. CLI login defaults to global. --realm selects+composes a chain but does NOT touch RuntimeScope.
  5. REST/RPC credential PERSIST = strict-owner write (MF-04 resolved 2026-06-16). Credential reads inherit down the chain; credential writes land ONLY in the realm that DEFINES the binding. Persisting for a child realm whose binding is inherited is REJECTED with a typed error naming the owning realm (prevents a child-scoped request from overwriting a parent/global credential). RCT-27/28 assert the rejection + owning-realm stamp.

Greenlight

The ARCHITECTURE is sound and the core mechanism is verified against the code: the lazy owning-set walk preserves the load-bearing strict equality at meerkat-llm-core/src/provider_runtime/registry.rs:208 without relaxation, RealmConfigSection.parent is genuinely schema-invisible (wire/config.rs:62 projects realm as opaque BTreeMap<String, Value>), Config::merge does silently drop self.realm today, and the WASM single-realm path is degenerate-clean. Direction: GREENLIGHT after must-fixes — do NOT rework the design. But it is NOT yet implementable: 14 must-fixes block it, four of them ship a wrong/insecure auth result a real config will hit on day one — (a) the dev→global MIGRATION is entirely absent and the plan prose actively contradicts the user’s copy-not-orphan decision, so every existing install loses auth on upgrade; (b) the global config doc is wired workspace-rooted (realm_paths_in) while decision 2 mandates a single ~/.rkat doc, so the headline cross-workspace-login promise is silently dead; (c) ChainComposingConfigStore is wired only in CLI, leaving top-level inheritance dead and surface-divergent on REST/RPC/MCP, and REST/RPC credential-persist endpoints (resolve_binding_identity) still single-section-stamp the request realm, writing inherited credentials to the wrong dir; (d) the symmetric ConfigStore.get()-composes/set()-head-only design lets one read-modify-write permanently flatten inherited entries into the child doc, violating the no-tombstone decision. Add phases P5b (surface re-root + composer wiring) and P6b (migration), split ConfigStore into a read-only effective reader plus a head writer, fix the global-doc location to home-rooted, edit the image/web_search and self_hosted contains_key gates, fix the REST exhaustive-match compile break, resolve the realm-map fold level to outer-key-only with fail-closed sub-map validation, and add RCTs 25-38 to close the coverage holes (absent-head, literal-default removal, nearest-child-wins, no-removal, real-fs source, cross-workspace, REST/RPC provenance, migration idempotence). SINGLE BIGGEST REMAINING RISK: the cross-surface gap — the plan was scoped to CLI+factory while the same resolution must hold identically on REST/RPC/MCP credential-persist and config-get, and the symmetric ConfigStore trait makes ‘compose on read’ silently corrupt the child doc on the very next write; getting the read/write split and per-surface wiring wrong reintroduces dual-truth on the network surfaces where users actually persist credentials.”