diff --git a/docs/dev/index.md b/docs/dev/index.md index 4bc1e6a..b23326b 100644 --- a/docs/dev/index.md +++ b/docs/dev/index.md @@ -76,6 +76,8 @@ Working documents for in-flight feature work. Removed when the work lands. | Future cluster control plane — declarative as-code config, JSON state ledger, reconciler | [cluster-config-specs.md](cluster-config-specs.md), [cluster-axioms.md](cluster-axioms.md), [cluster-config-implementation-spec.md](cluster-config-implementation-spec.md) | | Cluster graph & schema apply — Phase 4 sidecars, roll-forward recovery, approval artifacts | [rfc-004-cluster-graph-schema-apply.md](rfc-004-cluster-graph-schema-apply.md) | | Server boots from cluster state — Phase 5 mode switch, applied-revision serving | [rfc-005-server-cluster-boot.md](rfc-005-server-cluster-boot.md) | +| Per-operator config — `~/.omnigraph/` identity, keyed credentials, named servers (the operator slice of RFC-002) | [rfc-007-operator-config.md](rfc-007-operator-config.md) | +| Deprecate `omnigraph.yaml` — one concern per config surface; key-by-key migration map and staged retirement | [rfc-008-deprecate-omnigraph-yaml.md](rfc-008-deprecate-omnigraph-yaml.md) | ## Boundary diff --git a/docs/dev/rfc-007-operator-config.md b/docs/dev/rfc-007-operator-config.md new file mode 100644 index 0000000..52a446e --- /dev/null +++ b/docs/dev/rfc-007-operator-config.md @@ -0,0 +1,292 @@ +# RFC: Per-Operator Config — the Operator Slice of RFC-002 + +**Status:** Proposed +**Date:** 2026-06-11 +**Builds on:** [rfc-002-config-cli-architecture.md](rfc-002-config-cli-architecture.md) (Proposed; implementation parked — PRs #139/#162 closed over review findings), [rfc-005-server-cluster-boot.md](rfc-005-server-cluster-boot.md) (Landed), RFC-006 storage roots (#186/#190/#194, landed). The #139 review record is a normative input: every design rule in §D6 traces to a confirmed finding. +**Paired with:** [rfc-008-deprecate-omnigraph-yaml.md](rfc-008-deprecate-omnigraph-yaml.md) — together they define the two-surface architecture this RFC's operator half belongs to. +**Target release:** unversioned (staged; see Sequencing). + +## Summary + +Give OmniGraph the operator half of the **two-surface config architecture** +(RFC-008): **cluster config** (team-owned, in a repo — what the system *is*) +and **operator config** (person-owned, in `$HOME` — who *I* am). This is +Terraform's split: `~/.terraformrc` for the operator, the checkout for the +declaration. OmniGraph today has neither half cleanly — `omnigraph.yaml` +mixes both concerns (RFC-008 retires it), and there is no home-level config +at all: identity and credentials get re-declared per working directory, in +files that sit next to repo-committed config. + +This RFC introduces **`~/.omnigraph/config.yaml`** (the operator surface) +and a **keyed credentials chain**, scoped deliberately small: + +1. **Operator identity** — a default actor for every `--as` cascade. +2. **Credentials by server name** — no more inventing env-var names per + server; secrets never inline, never in any repo-committed file. +3. **Named servers** — operator-owned endpoint definitions; nothing a + checkout supplies can redefine them. + +It is explicitly a **subset of RFC-002**, sequenced to land. RFC-002 settled +the right long-term decisions (one `~/.omnigraph/` dir, credentials keyed by +server name, `OMNIGRAPH_CONFIG`/`OMNIGRAPH_HOME` env precedence) but its +implementation arrived as one 4,800-line PR mixing a crate extraction with +behavior changes, and died over ten confirmed findings. This RFC adopts +RFC-002's settled decisions verbatim where they apply, defers everything +else (`GraphLocator`, multi-homing, `omnigraph use`, the State layer), and +encodes the #139 findings as design rules so the same failures cannot recur. + +## Motivation + +Three concrete pains, all hit in real operation this cycle: + +- **Identity repetition.** The cluster actor cascade (#180) resolves + `--as` from the per-operator `omnigraph.yaml` — which means every + operator hand-maintains a copy in every working directory (the + `~/exp/intel` setup needed exactly this). A repo-committed + `omnigraph.yaml` cannot carry `as: act-andrew` without claiming every + contributor is Andrew. +- **Credential ergonomics.** `bearer_token_env` forces three coordinated + steps per server (invent a var name, reference it in config, set it in + the secret store). The peer group — AWS profiles, `gh hosts`, kubeconfig + users — keys secrets by the server's *name*. +- **Cluster-era working shape.** With clusters on object storage (RFC-006), + the project directory is a *declaration checkout* — operators run + `cluster apply --config ./checkout` from anywhere. The things that are + about the *operator* (who am I, which servers do I know, how do I like + output formatted) have no home that travels with them. + +## Non-Goals + +- **`GraphLocator` / multi-homed graph resolution** (RFC-002 §1) — the + biggest and riskiest part of config-v2; untouched here. +- **`omnigraph use` / the State layer** (`~/.omnigraph/state/`) — deferred + with it (finding #2 showed its precedence interacts badly with scaffolds; + that problem belongs to the slice that introduces it). +- **OS keychain integration** — the credentials *chain* (§D4) leaves a slot + for it; this RFC ships env + file sources only. +- **Config-file walk-up.** Terraform does not walk up from subdirectories + and neither do we — `--config` (or running in the directory) stays the + explicit, deterministic story for cluster checkouts. Rejected, not + deferred: walk-up makes "which config am I using" a function of cwd + depth, the class of surprise this RFC exists to remove. +- **Retiring `omnigraph.yaml`** — that is RFC-008's job, with its own + staging. This RFC builds the destination; during RFC-008's deprecation + window the legacy file keeps loading exactly as today. +- **Renaming or removing anything.** No flag renames, no key renames, no + schema-version bumps (findings #1, #3, #10). + +## Background (verified against main) + +- **Project-config lookup today** (`crates/omnigraph-server/src/config.rs:529-553`, + shared by CLI and server): `--config `, else `./omnigraph.yaml` in + cwd, else built-in defaults. Relative paths inside the file resolve + against the file's own directory (`base_dir`). No env var, no home file, + no walk-up. +- **Side-effect on load** (`crates/omnigraph-cli/src/helpers.rs:102-108`): + `load_cli_config` also loads `auth.env_file` into the process env — + this is how `OMNIGRAPH_BEARER_TOKEN` reaches remote commands today. +- **Actor resolution** (`helpers.rs:170`, #180): `--as` flag, else the + project config's actor — currently the end of the chain. +- **Existing credential mechanism**: `TargetConfig.bearer_token_env` names + an env var; `auth.env_file` points at a git-ignored dotenv. Both keep + working indefinitely (RFC-002 already committed to this; finding #3 + showed what happens otherwise). +- **`OMNIGRAPH_CONFIG`** exists today only as the *container entrypoint's* + translation to the server's `--config`. The CLI does not read it. + +## Design + +### D1. Files and discovery + +``` +~/.omnigraph/config.yaml # the operator surface (this RFC) +~/.omnigraph/credentials # keyed secrets, 0600, git-irrelevant (§D4) +./cluster.yaml + checkout # the team surface (unchanged; RFC-004..006) +./omnigraph.yaml # legacy, loads as today through RFC-008's window +``` + +Discovery order for the operator file: `$OMNIGRAPH_HOME/config.yaml` if +`OMNIGRAPH_HOME` is set, else `~/.omnigraph/config.yaml`. Absent file = +empty layer, never an error. `~` is expanded wherever paths are read +(finding #9 — today a literal `./~/...` directory gets created). + +`OMNIGRAPH_CONFIG=` becomes a first-class override for the `--config` +argument in the CLI (highest precedence below the flag itself), aligning the +CLI with the container contract that already uses this variable for the +server. One name, one meaning, both binaries — it points at whatever the +command's `--config` would (a cluster checkout for cluster commands; the +legacy file during RFC-008's window). + +Per RFC-002 §4 (adopted verbatim): `~/.omnigraph/` is the one canonical +dir — cache/state subdirectories arrive with their own slices; XDG roots are +not part of the mental model (`$XDG_CONFIG_HOME` may be honored as a +fallback read location if set, but is never written to). + +### D2. The operator schema (v1 of this layer) + +```yaml +# ~/.omnigraph/config.yaml — about the OPERATOR, never about the system +operator: + actor: act-andrew # default for every --as cascade + +servers: # operator-owned endpoint definitions + intel-dev: + url: http://127.0.0.1:8080 + prod: + url: https://graph.modernrelay.ai + # No token here, ever. Resolution: §D4. + +aliases: # personal shorthand over CLUSTER-owned queries + triage: # (the query is the shared contract; the alias, + server: intel-dev # its defaults, and its name are mine — RFC-008) + graph: spike + query: weekly_triage + +defaults: + output: table # read --format default +``` + +Unknown keys are a **warning, not an error** in this layer (an operator file +written by a newer CLI must not brick an older one; contrast with +`cluster.yaml`, where unknown keys are deliberately fatal because they +change what a *plan* means). + +### D3. Precedence and the merge rule + +The end-state cascade is short, because the team surface (cluster config) +deliberately carries **no operator-resolvable keys** — no actor, no tokens, +no output preferences. Identity can never come from a checkout: + +``` +flag > env > operator config > built-in +``` + +During RFC-008's deprecation window, a legacy `omnigraph.yaml` slots in +between env and operator config (its keys win over operator defaults, +preserving today's behavior for unmigrated setups) — with the §D5 +credential inversion: **credentials and endpoint definitions never come +from a legacy/checkout file when an operator-layer definition exists for +the same server name.** + +Merging is **key-level**: scalars override per key; maps (`servers:`, +`aliases:`) merge per *entry*, and entries merge per *field* (finding #13 — +`merge_map` replacing whole entries silently dropped sibling fields). + +Concretely for the two flows this slice touches: + +- **Actor**: `--as` > legacy `cli.actor` (window only, unchanged semantics) + > `operator.actor` > none (commands that need an actor keep failing + loudly). +- **Output format**: `--format` > legacy default (window only) > + `defaults.output` > `table`. + +### D4. Credentials: keyed by server name, by-reference always + +Adopted from RFC-002 §5 unchanged, minus the keychain (a later source in +the same chain). For a server named ``, the resolution chain is: + +1. `OMNIGRAPH_TOKEN_` (uppercased, `-`→`_`) — explicit env, wins. +2. `[]` section in `~/.omnigraph/credentials` (INI-style, `0600`; + the loader refuses a group/world-readable file). +3. The legacy pair — `bearer_token_env` + `auth.env_file` — exactly as + today, for configs that already use it. + +No inline secrets in any YAML file, anywhere (the existing invariant 12 +posture extended to disk). A future `omnigraph login ` +writes/rotates one section of the credentials file via temp + rename +(finding #7: every operator-layer write is atomic), creating it `0600`. + +### D5. The trust boundary (the security findings, made structural) + +Findings #4, #5, #6 share one root cause: a file that arrives with a +*repo checkout* could redirect where requests go and what secrets they +carry. In the end state this is closed by construction — cluster config has +no server/credential keys at all, and the operator surface never comes from +a checkout. The rules below therefore govern the **RFC-008 window** (while +legacy `omnigraph.yaml` still loads) and stand as the permanent law for any +future checkout-supplied surface: + +1. **A checkout-supplied file may *reference* a server by name; it may not + *redefine* an operator-defined server.** If a legacy `./omnigraph.yaml` + declares `servers.prod.url` and `~/.omnigraph/config.yaml` also defines + `prod`, the operator definition wins and the CLI warns about the + shadowed entry. A legacy-only server name keeps working (compat), but + the keyed-credentials chain (§D4 steps 1–2) never resolves for it — + only the legacy explicit `bearer_token_env` does. Net effect: a + malicious checkout cannot point `prod` at an attacker host and harvest + the operator's `prod` token. +2. **`auth.env_file` keeps auto-loading (compat), but checkout-layer + env-files cannot *override* variables already set in the process or by + the operator layer** — first-set-wins, operator-before-checkout (the + existing real-env-wins rule, extended one layer down). Finding #5's + injection becomes a no-op against any var the operator actually uses. +3. **A token is sent only to the server it is keyed to.** The legacy + single `OMNIGRAPH_BEARER_TOKEN` fallback keeps working for the + single-server shape, but when a request resolves through a *named* + server, only that name's chain applies (finding #6's broadcast). + +### D6. Compatibility rules (the #139 findings as law) + +| Rule | Source finding | +|---|---| +| No flag or key is removed or renamed; new behavior is additive | #1, #3 | +| A config that loads today loads identically after this RFC; new validation applies only to new keys | #3, #8, #10 | +| Every operator-layer file write is temp + rename, never in-place | #7 | +| `~` expands wherever a path is read | #9 | +| Map merges are per-entry, per-field — never wholesale replace | #13 | +| One resolution path per concern — the actor chain and the token chain each have exactly one implementation, called by CLI and server alike | #11, #12 | +| Each slice lands as its own PR with the workspace gate green; no slice mixes mechanical moves with behavior changes | #139's disposition | + +## Sequencing + +Three PRs, each independently useful, each landable without the next: + +1. **PR 1 — the operator file + identity.** Loader for + `~/.omnigraph/config.yaml` (+ `OMNIGRAPH_HOME`, `~`-expansion, warn-only + unknown keys), `operator.actor` joining the `--as` cascade, + `defaults.output` joining the format cascade, `OMNIGRAPH_CONFIG` env for + the CLI's `--config`. Docs: `cli-reference.md` gains the two-surface + table. +2. **PR 2 — keyed credentials.** `servers:` in the operator layer, the + §D4 chain (env + credentials file), the §D5 trust rules, and + `omnigraph login ` (atomic write, `0600`). Legacy mechanisms + untouched and tested-as-untouched. +3. **PR 3 — operator targeting.** `--server ` on remote-capable + commands and `aliases:` in the operator layer (server + graph + query + + default params), resolving through operator-defined servers. This is + the *bridge* toward RFC-002's locator — multi-server addressing in a + safe, minimal form without the `GraphLocator` rework — and the + replacement RFC-008 needs before legacy aliases can migrate. + +RFC-008's deprecation stages begin only after PRs 1–2 are on main: the +operator surface must exist before `config migrate` has somewhere to move +keys to. + +## Open questions + +- Should `operator.actor` apply to *local* (embedded-engine) writes too, or + only where a server/cluster boundary exists? Leaning yes-everywhere: one + identity chain (§D6 one-path rule), and local audit rows get better. +- Does `defaults.output` belong in slice 1, or is identity-only an even + cleaner first PR? (Cost of including it is one cascade hop; value is + immediate.) +- `omnigraph config view --resolved` (RFC-002 had it; #139 shipped a + version) — slice 1 or slice 2? It materially helps debugging precedence, + which argues early. + +## Relationship to RFC-002 and RFC-008 + +**RFC-008 is the other half of this design**: this RFC builds the operator +surface; RFC-008 retires the mixed-ownership file +([rfc-008-deprecate-omnigraph-yaml.md](rfc-008-deprecate-omnigraph-yaml.md)), +leaving exactly two config surfaces — cluster (team) and operator (person). +Every mention of `omnigraph.yaml` in this RFC describes the deprecation +window only. Sequencing couples them: RFC-007 PRs 1–2 land first, then +RFC-008's migration stages run against them. + +RFC-002 remains the umbrella architecture. This RFC implements its §2 +(layered config, global-first), §4 (file naming / one dir), and §5 +(credentials) in their minimal load-bearing form, and explicitly defers §1 +(`GraphLocator`/targets), §3 (roles), and the State layer. If/when the +locator work resumes, it builds on these layers rather than re-landing +them. RFC-002's header should gain a pointer here once this merges. diff --git a/docs/dev/rfc-008-deprecate-omnigraph-yaml.md b/docs/dev/rfc-008-deprecate-omnigraph-yaml.md new file mode 100644 index 0000000..49e2c4b --- /dev/null +++ b/docs/dev/rfc-008-deprecate-omnigraph-yaml.md @@ -0,0 +1,174 @@ +# RFC: Deprecate `omnigraph.yaml` — One Concern per Config Surface + +**Status:** Proposed +**Date:** 2026-06-11 +**Builds on:** [rfc-007-operator-config.md](rfc-007-operator-config.md) (the +operator layer that absorbs the identity/credential keys), +[rfc-005-server-cluster-boot.md](rfc-005-server-cluster-boot.md) (Landed — +cluster-booted serving), RFC-006 storage roots (landed: #186/#190/#194). +**Supersedes in part:** RFC-007's "project layer" framing (§Relationship +below) and [rfc-002-config-cli-architecture.md](rfc-002-config-cli-architecture.md)'s +assumption that `omnigraph.yaml` remains the project manifest. +**Target release:** staged; final removal at the next major (see Sequencing). + +## Summary + +Retire `omnigraph.yaml`. It is three unrelated concerns wearing one +filename — server deployment config, project/CLI conveniences, and operator +identity — and the mixture is not a cosmetic wart but the root cause of a +recurring class of problems: operators keeping personal copies of "project" +files, repo checkouts able to carry credential-adjacent keys (the #139 +security findings), `omnigraph init` scaffolding config into unrelated +directories, and every config discussion needing a paragraph to establish +which of the three files is meant. + +The end state is **two config surfaces with single owners**: + +| Surface | Owner | Declares | +|---|---|---| +| **Cluster config** (`cluster.yaml` + catalog) | the team, in a repo | what the system *is*: graphs, schemas, queries, policies, storage | +| **Operator config** (`~/.omnigraph/`) | one person, in `$HOME` | who *I* am: identity, credentials, known servers, ergonomics | + +plus **flags/env** for the zero-config tier (one graph, one server, no +control plane) — which already works today with no file at all. + +`omnigraph.yaml` has no role left once every key has a better home. This +RFC gives each key that home, and stages the retirement so that no working +setup breaks without a loud warning, a migration command, and a full +deprecation cycle first. + +## Motivation + +- **It breaks the ownership logic.** A config file must have one owner. A + file that carries `graphs:` (team-owned, reviewable) next to `cli.actor` + (one person's identity) and `auth.env_file` (credential loading) can be + neither safely committed nor sensibly personal. Every real deployment + this cycle tripped on it: per-operator copies in `~/exp/intel`, + graph-scoped alias URIs that only make sense per-person, the #139 + findings where a checkout-supplied file could redirect tokens. +- **The cluster made it redundant.** Since RFC-005/006, a cluster + deployment serves from the applied catalog — `--cluster` mode does not + read `omnigraph.yaml` *at all*. Stored queries, policies, bindings, and + graph addressing all have authoritative homes. What remains in + `omnigraph.yaml` for cluster users is dead weight that can silently + disagree with what is actually serving. +- **Two declarative dialects is one too many.** `cluster.yaml` and + `omnigraph.yaml` both declare graphs/queries/policies with different + schemas, different validation strictness, and different lifecycle + guarantees. Maintaining, documenting, and testing both — and explaining + when each applies — is a permanent tax (the "programming integrated over + time" lens says: this forks on every config-surface change). + +## Non-Goals + +- **Breaking anyone now.** Every `omnigraph.yaml` that works today keeps + working through the entire deprecation window, with warnings. +- **Retiring the zero-config tier.** `omnigraph-server s3://bucket/g.omni + --bind …` plus env vars stays first-class forever — that tier needs *no* + file, which is the point. +- **Forcing the control plane on single-graph users.** The migration target + for a multi-graph yaml deployment is a *minimal* cluster (file-rooted, + no bucket required, `cluster.yaml` barely longer than the `graphs:` map + it replaces) — but a single graph never needs even that. +- **Touching `cluster.yaml`** — its schema and strictness are unchanged. + +## Where every key goes (the complete migration map) + +The full `OmnigraphConfig` surface (verified against +`crates/omnigraph-server/src/config.rs:182-207`): + +| `omnigraph.yaml` key | Concern | New home | +|---|---|---| +| `graphs..uri` | what exists / where | `cluster.yaml` `graphs:` (storage-root-derived) — or a flag/env for the zero-config tier | +| `graphs..queries`, top-level `queries:` | what exists | cluster catalog (`.gq` discovery, RFC-004/#183) | +| `graphs..policy.file`, top-level `policy.file`, `server.policy.file` | what's enforced | `cluster.yaml` `policies:` + `applies_to` bindings | +| `server.bind` | deployment runtime | `--bind` / env (already authoritative; the key is a default) | +| `server.graph` | deployment runtime | `--target`-style flag / env in the zero-config tier; meaningless under cluster boot | +| `graphs..bearer_token_env`, `auth.env_file` | credentials | operator credentials chain (RFC-007 §D4) | +| `cli.actor` | identity | `operator.actor` (RFC-007 §D3) | +| `cli.output_format`, `cli.table_*` | personal ergonomics | `defaults:` in operator config (RFC-007 §D2) | +| `cli.graph`, `cli.branch` | personal targeting | operator config: named servers + a per-operator default target (RFC-007 PR 3) | +| `aliases.` | personal ergonomics over shared queries | operator config `aliases:` — the *queries* they invoke are cluster-owned; the *shorthand* is personal | +| `query.roots` | discovery convenience | obsolete — cluster query discovery (#183) replaced it | +| `project.name` | label | dropped (the cluster's `metadata.name` is the deployment label) | + +Two placements worth defending: + +- **Aliases are operator config, not cluster config.** The stored query is + the shared contract (catalog-owned, digest-pinned); an alias is one + person's shorthand with their favorite default params and target. Putting + aliases in the cluster would force team review on personal ergonomics; + leaving them per-directory recreates today's problem. Per-operator, + keyed by server/graph name, is the AWS-profile shape. +- **Multi-graph serving without a control plane migrates to a minimal + cluster, not to a new file.** The honest cost: `cluster import` + `apply` + once, on a `file://` root next to the graphs. The honest benefit: one + declarative dialect, one validation path, one serving source — and the + upgrade path to buckets/approvals is a one-line `storage:` change instead + of a re-platform. + +## Deprecation mechanics + +Per Hyrum's Law (the repo's own deny-list: shipped observable behavior is +contract), retirement is staged, loud, and tooled: + +1. **Warn.** Loading `omnigraph.yaml` emits a one-line deprecation notice + naming the replacement for each key actually present in the file (not a + generic banner — the migration map above, applied to *your* file). + Suppressible per-process (`OMNIGRAPH_SUPPRESS_YAML_DEPRECATION=1`) for + CI logs during the window. +2. **Migrate.** `omnigraph config migrate` reads an existing + `omnigraph.yaml` and writes the split: the team half as a ready-to-review + `cluster.yaml` (+ moves query/policy files into the checkout layout), + the personal half merged into `~/.omnigraph/config.yaml` — printing a + diff-style summary and touching nothing without `--write`. The command + is the test of the migration map's completeness: any key it cannot + place is a bug in this RFC. +3. **Stop scaffolding.** `omnigraph init` stops generating + `omnigraph.yaml` (it currently scaffolds one into cwd — the source of + the test-pollution bug). `omnigraph cluster init` (new, small) scaffolds + a minimal `cluster.yaml` instead. +4. **Opt-in strict.** `OMNIGRAPH_NO_LEGACY_CONFIG=1` turns the warning into + an error — for teams that finished migrating and want regressions caught. +5. **Remove at the next major.** Loading the file becomes an error pointing + at `config migrate`. The `OmnigraphConfig` code path, the dual + query-registry loaders, and the yaml-mode server boot source are deleted + — the payoff that makes the whole exercise worth it. + +Stages 1–3 can land in one release once RFC-007 PRs 1–2 exist (the operator +layer must exist before anything can migrate *to* it). Stage 4 the release +after. Stage 5 at the major, with the removal listed in release notes from +stage 1 onward. + +## What this deletes, eventually + +- The `OmnigraphConfig` struct and its 12-key surface, the + `load_config`/`load_cli_config` pair and its env-side-effect, the + scaffolder, and the legacy resolution paths (`resolve_cli_graph`'s dual + modes — finding #11's root cause). +- The yaml-mode multi-graph server boot (`ServerConfigMode::Multi` keeps + existing — cluster boot constructs it — but its `omnigraph.yaml` source + goes). +- An entire class of documentation ("which file does X go in?") and the + #139 security surface (a checkout cannot hijack what no longer loads). + +## Relationship to RFC-007 and RFC-002 + +RFC-007 ships the operator layer this RFC migrates *to*; its "project +layer" language should be read as transitional — after this RFC, the +project layer **is** the cluster checkout, and RFC-007's PR 3 (project +`server:` references) applies to `cluster.yaml`-adjacent operator targeting +rather than to `omnigraph.yaml`. RFC-002's locator/state-layer work, if +resumed, targets the two-surface world directly. RFC-002's file-naming +decisions (`~/.omnigraph/` as the one dir) are unaffected. + +## Open questions + +- **Window length**: one minor release between warn (stage 1) and strict + (stage 4), or two? Cookbooks, skills, and the deployment docs all need + the same pass; the migration command makes a short window defensible. +- **`omnigraph login` vs `config migrate` ordering** — both write + `~/.omnigraph/`; whichever lands first establishes the file-locking and + atomic-write helpers the other reuses. +- **Does the MCP server config** (RFC-003) reference `omnigraph.yaml` + anywhere that needs the same treatment? To be audited in stage 1.