Verify against code, not ticket statuses (MR-581 is marked done but is stale/unbuilt): no ~/.omnigraph usage, no template/serve/quickstart/ prune/login commands exist; config still uses aliases: (no operations:). So ~/.omnigraph/ stands on peer-convention merits alone, and templates are a design question, not a foothold. Add §7.5: the three-tier init model (user route = login + ~/.omnigraph/config.yaml; thin project init; fat quickstart + templates) with first-principles positions (split init/login, in-place refuse-if-exists, interactive vs --auto/agent-mode, --template flag, secrets-on-scaffold gitignore rule). This RFC owns only the user route; the rest are sibling tickets (MR-973/970/972/974/981). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
38 KiB
RFC: Config & CLI Architecture — Layered Config, Client Targeting, File Naming
Status: Proposed Date: 2026-05-30 Tickets: MR-668 (multi-graph server, shipped — the dependency this builds on), MR-969 (stored queries + MCP — supplies the in-repo agent tool surface), MR-973 (quickstart / onboarding), MR-974 (agent setup surface), MR-981 (agent-friendly CLI hardening) Target release: v0.8.x (tentative; phased — see Rollout)
Summary
OmniGraph today has a single config file, omnigraph.yaml, read both by the CLI (operating the embedded engine) and by omnigraph-server (hosting graphs). There is no client-side configuration that targets a running server — to talk to a deployed omnigraph-server you drop to curl or the omnigraph-ts client. This is the one real gap in an otherwise coherent design (storage-URI addressing, multi-graph routing, per-graph policy).
This RFC defines the config and CLI architecture that closes that gap, derived from first principles — working backwards from what OmniGraph uniquely enables rather than copying kubeconfig / helix.toml. The result:
- A global-first layered config — user-global (
~/.omnigraph/) is the primary, self-sufficient default; per-project (./omnigraph.yaml) is an optional override + deployment manifest. One uniform schema, both layers optional; the CLI works from any directory with no project file (thekubectl/aws/ghposture), unlike today's project-anchored behavior. - A single unifying noun — the target — that resolves a name to a concrete
(locus, graph, sub-state, credential)tuple, where the locus is embedded (storage URI) XOR remote (server endpoint). - A multi-server × multi-graph client model (OmniGraph hosts N graphs per server and there are M servers — unlike Helix's one-cluster-one-graph).
- Credentials by reference, reusing OmniGraph's existing mechanism —
bearer_token_env:(token resolved by env-var name) + a git-ignoredauth.env_filedotenv — extended to servers and the global layer; OS keychain as a future resolver. No newcredentials.yaml; every committed/GitOps'd surface stays secret-free. - A file-naming decision: project and server config are the same artifact, same name (
omnigraph.yaml); the only differently-named file is the user-globalconfig.yaml, justified by scope, not role.
The design optimizes jointly for DX (one command surface across embedded and remote; clone-and-go) and AX (agent experience: one flat resolved context, secrets structurally unreachable, branch-pinned reproducible reads, and a GitOps'd capability surface).
Reconciliation with shipped / planned CLI work
Verified against the code, not ticket statuses (which are unreliable — e.g. MR-581 is marked done but is stale and unbuilt). Findings and the corrections they force:
- Noun is
graph/graphs, NOTtarget/targets. The config key isgraphs:inconfig.rsand the flag is--graph. This RFC usesgraphs:/--graphthroughout; the unifying noun is agraphs:entry that is embedded (uri:) XOR remote (server:+ a remote graph name). Read any lingeringtargets:/--targetbelow asgraphs:/--graph. ~/.omnigraph/stands on its own merits (Helix/aws/kube peer convention), not on precedent — there is no~/.omnigraph/usage in the code today. (MR-581 / MR-531 templates-into-~/.omnigraph/are stale tickets, unbuilt.)- Templates do not exist in the code (no
templatecommand). The template mechanism is a design question for this RFC / the init family, not an existing foothold. - What actually exists in the CLI (verified):
init, query(read), mutate(change), load, ingest, branch, schema, lint, snapshot, export, commit, policy, optimize, cleanup, graphs. Not built:serve, quickstart, template, prune, login.omnigraph initexists (withscaffold_config_if_missing,main.rs:1415); the rest of the "init family" (quickstartMR-973,serveMR-970,prune/init --forceMR-972/975,mcp install/skills MR-974, agent-mode MR-981) are unbuilt tickets, some stale. - Config still uses
aliases:(nooperations:in code; MR-839 unbuilt). §6's reconciliation talks aboutaliases:as-is, notingoperations:is a proposed rename. bearer_token_envexists (per-graph,config.rs); MR-971 flags a CLI-parity / server-side gap. The per-servers.<name>extension lands on top of that.- A top-level
omnigraph lintcommand exists (verified). A stored-query registry validator must pick a verb that doesn't read as a competing lint/check.
Motivation
Three problems, in priority order:
- No client→server targeting config. The moment an operator stands up
omnigraph-server— for bearer auth + Cedar at a network boundary + admission control + multi-graph routing — the CLI can't address it.curlis the fallback. There is no named, switchable, credential-carrying way to say "run this againstprodon the team server." - Multi-server × multi-graph has no first-class expression. OmniGraph genuinely runs N graphs per server across M servers. The same graph is multi-homed —
s3://b/prodmay beprodon server A,productionon server B, and opened directly by the CLI. Today's flatgraphs:map (name→storage-URI) can't express "graphproductionon serverprod-eu." - Solo-first and embedded-first are unserved by the remote story. A solo developer with no projects should define everything in
~. A developer iterating locally (embedded, no server) and then pointing at staging (remote) should change one word, not learn a second command surface.
MR-668 shipped the server side (multiple graphs per server). MR-969 ships the in-repo agent tool surface (stored queries / MCP). This RFC supplies the client and config layer that lets humans and agents target that surface coherently — the foundation under MR-973 / MR-974 / MR-981.
Non-Goals
- A control plane / dashboard for config. Operators edit files and (for servers) restart. No runtime config-mutation API. Matches the MR-668 / MR-969 operational model.
- Hot reload. Restart-only for server-side config, matching MR-668 and MR-969.
- Embedding secrets in any config file. Credentials are by-reference; the git-ignored
auth.env_filedotenv (or, later, the OS keychain) holds tokens. Never a committable*.yaml. - Renaming the project manifest by role. No
omnigraph.server.yaml/omnigraph.client.yaml. Role lives in sections, not filenames (see Design §3). - Dropping embedded mode. Embedded-first is load-bearing for the file-naming decision; this RFC assumes it stays.
- Cross-graph / cross-server tool listing in MCP. Clients loop over per-graph catalogs (a MR-969 non-goal, restated).
Background
OmniGraph runs on Lance 6.x: typed nodes/edges in per-type Lance datasets, atomic multi-table commits via a __manifest table, branchable and time-travelable. The CLI (omnigraph) operates the embedded engine directly against a storage URI — no HTTP client in its runtime dependencies. omnigraph-server (Axum) is a separate HTTP front-end over the same engine, with bearer auth + per-graph Cedar (MR-668). The two read the same omnigraph.yaml but never connect to each other.
OmniGraph already has a credentials-by-reference mechanism, which this RFC builds on rather than replacing: TargetConfig.bearer_token_env names the env var holding a graph's bearer token, and auth.env_file points at a git-ignored dotenv (.env.omni) that the CLI auto-loads into the process (load_env_file_into_process) with real-env-vars-win precedence; resolve_remote_bearer_token resolves a token via env var then dotenv named lookup. .env.omni is already in .gitignore.
The six irreducible enablers that drive the design (referenced as E1–E6 below):
| # | Enabler | Consequence |
|---|---|---|
| E1 | A graph is a self-contained storage URI; the substrate (object store + manifest CAS) is the source of truth — no server required to read/write. | A graph is addressable directly (embedded), not only via a server. |
| E2 | A server hosts many graphs; many servers exist. | The remote address space is {server} × {graph_id}. |
| E3 | The same graph is multi-homed under different per-locus names. | Name ≠ identity. Resolution is mandatory. |
| E4 | Branch / commit / snapshot are first-class addressable sub-state. | An address is graph @ branch/snapshot, not just graph. |
| E5 | Enforcement is two-layered: engine-layer Cedar (_as writers, works embedded) + HTTP-boundary bearer+Cedar (server only). |
How you reach a graph determines which enforcement applies. |
| E6 | Stored queries / MCP tools are a per-graph registry defined in the project config (MR-969). | The agent tool surface is version-controlled in the repo. |
Competitors collapse dimensions OmniGraph keeps live: Helix fuses E2+E3 (one cluster = one graph); namidb fuses E1+E3 into the URI (s3://b?ns=prod) and serves one namespace per process. OmniGraph has all of E1–E6 at once, so its config resolves a richer space — but the richness is earned by capability.
Design
1. The address space and the target abstraction
Every OmniGraph address is a tuple:
(locus, graph, sub-state, credential)
locus = embedded(URI) XOR remote(server-endpoint) # E1, E2
graph = a URI (embedded) | a graph_id on a server (remote) # E3
sub-state = branch | snapshot # E4
credential = cloud-storage creds (embedded) | bearer token (remote) # E5
The config's only job is name → this tuple. Define one noun — a target — that resolves to either shape:
targets:
dev: # embedded — substrate-direct (E1)
uri: s3://team-bucket/dev.omni
branch: main # sub-state (E4)
staging: # remote — resolves a server by reference (E2/E3)
server: staging # → looked up in `servers`
graph: prod # graph_id on that server
branch: review
--target staging resolves: project targets.staging → {server: staging, graph: prod, branch: review} → servers.staging → {endpoint, token-by-ref} → final (remote(https://…), prod, review, $TOKEN). Embedded targets skip the server hop and use cloud-storage credentials.
Two concepts, not kubeconfig's three. kube splits cluster / user / context; that 3-way split is its most-cursed UX. A target bundles server+graph+branch+defaults under one name; the only thing split out is servers, because endpoints+credentials are shared across many targets and are secret-bearing (different ownership and rate-of-change; see §2). Result: 2 nouns — servers and targets. Embedded targets (uri:) subsume today's graphs: entries.
2. Layered config — global-first, uniform schema, project-optional
Posture: global-first, project-optional. OmniGraph's CLI is primarily a client (it operates against graphs and servers, embedded or remote), so it sits on the global-first side of the CLI-config axis — like kubectl / aws / gh / docker, and unlike project-first tools (git / cargo / terraform) whose primary config is per-repo. The global user config is the primary, self-sufficient default; the project file is an optional repo-scoped override (and, when present, the deployment manifest). omnigraph query --target prod must work from any directory with no project file, exactly as kubectl get pods --context prod works from anywhere. (This is a deliberate flip from today, where the CLI reads ./omnigraph.yaml and does not even walk parent dirs — i.e. today it is project-anchored.)
Rule: the two layers share ONE schema, and each is fully self-sufficient (the git-layering mechanism — same schema at both levels; you never need a repo to have a working config). Do not specialize the layers. Anything — servers, targets, defaults, queries, aliases, policy — is expressible at either layer.
This makes the zero-project case the default, not an edge case: a solo user (or an agent) defines everything in ~/.omnigraph/config.yaml — servers, embedded + remote targets, defaults, even a personal server's graphs:/queries: — and never creates a project file. A team adds ./omnigraph.yaml only when it wants repo-scoped overrides or a committed, GitOps'd deployment manifest. Global-first does not forbid project files; it stops requiring them (the kubectl model: ~/.kube/config is sufficient and default; per-project kubeconfigs are opt-in via KUBECONFIG).
| Layer | Required? | Typical use | Path |
|---|---|---|---|
| Global | no | the default — solo/agent's entire config; shared servers+creds for teams; even a personal server's graphs/queries | ~/.omnigraph/config.yaml |
| Project | no | opt-in — repo-scoped overrides + the committed deployment manifest (graphs, queries, policy) | ./omnigraph.yaml |
Precedence (low → high): built-in defaults < global < project < env vars < CLI flags. With no project file it collapses to built-in < global < env < flags — the common global-only path.
Merge semantics — "closest layer wins, at the smallest meaningful unit" (the field consensus: git / kubeconfig / cargo / Helm / VS Code):
- Settings objects (
defaults,auth,server) → deep-merge per field: a project setsdefaults.targetand inherits the globaldefaults.output_format. (VS Code / cargo behavior.) - Named-resource maps (
servers,targets,queries,aliases) → union by key; on a collision the higher layer's entry REPLACES the lower wholesale — no field-level deep-merge within an entry. (kubeconfig: union contexts by name.) The footgun this avoids: globalservers.prod = {endpoint, bearer_token_env}, projectservers.prod = {endpoint: other}— deep-merge would silently retain the oldbearer_token_env; replace makes the project'sprodself-contained and predictable. - Lists/arrays → replace, never append (Helm convention; appending is order-sensitive and surprising).
- Scalars → higher layer wins.
- Relative paths carry their origin's base_dir. A
queries:entry's.gqpath, or apolicy.file, resolves against the directory of the layer it was defined in — global entries under~/.omnigraph/, project entries under the project dir. - Inspectable (non-negotiable):
omnigraph config view --resolved --show-originprints each final value and which layer set it (thegit config --show-origin/kubectl config viewrule). A layered config without origin-tracing is a debugging trap.
3. Roles, and the file-naming decision (same name for project = server)
omnigraph.yaml carries two roles that diverge in prod and collapse on a laptop:
- Server role (read by
omnigraph-server):graphs:(graph_id → URI), per-graphpolicy.file,queries:— the stored-query/MCP registry lives here, plus serving knobs. - Client role (read by the CLI/agent):
servers:,targets:,defaults:,aliases:.
Project config and server config are the same artifact, hence the same name. The server serves the project: the file that says "these graphs exist, with these stored queries and this policy" is simultaneously the project manifest and the server's deploy config. Role is distinguished by which sections are populated, never by filename. Readers ignore sections that are not theirs (today's file already does this with cli: vs server:).
Why not kube's role-split. Two coherent models exist: (A) one project file with role-sections (Helix helix.toml holds both [local.dev] and [enterprise.production]; compose; Cargo), and (B) deployment-manifest strictly separate from client config (kubectl — you never put a context in deployment.yaml). kube is the sharpest topological analog (multi-server × multi-graph, one client targeting many), so B has a real claim. The tiebreaker is E1: OmniGraph is embedded-first. In embedded mode the manifest's graphs: is the local target list — manifest and local-client-view are the same object, so splitting them (B) fights the grain and forces two files for local work. kube splits because it has no embedded mode (client always remote+global). So: take the half kube is right about — remote client targeting (servers:, endpoints, creds) is a separate concern in a separate user-global file (config.yaml, like ~/.kube/config); reject the half it is wrong about for us — do not split the project layer by role. The second name (config.yaml) is justified by scope (user-global), not role. (If OmniGraph ever dropped embedded mode and went pure-remote, model B's strict split would become cleanest.)
4. File naming
Principles from the field: one global dir ~/.omnigraph/ (like ~/.aws/~/.kube/~/.helix), with config/cache/state as subdirectories (separation without XDG's three-root scatter); secrets in a separate, git-ignored file inside that dir (OmniGraph's existing auth.env_file dotenv, not a new credentials.yaml); project-root manifest keeps the app-named file (Cargo.toml, package.json); .yaml, not .yml; keep OmniGraph's established names. The only genuinely new decision is the global dir's existence; credentials reuse the existing bearer_token_env + auth.env_file mechanism.
| Artifact | Path / name | Why |
|---|---|---|
| Project = server config (one artifact) | ./omnigraph.yaml |
Keep. Root manifest like Cargo.toml / compose.yaml / helix.toml. Same name for both roles because it is one file. In prod the server's deploy repo and an app repo each have their own omnigraph.yaml — same name, different repos. |
| Global user config | ~/.omnigraph/config.yaml |
One dir (~/.omnigraph/, like ~/.aws/~/.kube/~/.helix). Named config.yaml not omnigraph.yaml — the name signals scope (and ~/.aws/config, ~/.kube/config, ~/.helix/config all do this). Holds the full schema so a solo user needs nothing else. |
| Credentials | Existing auth.env_file dotenv — ./.env.omni (project), ~/.omnigraph/credentials (global); 0600, git-ignored, inside the one dir. OS keychain is a future option. |
Reuse what exists — OmniGraph already loads a git-ignored dotenv (load_env_file_into_process, env-vars-win precedence) and resolves a token by env-var name (bearer_token_env). Do not invent a parallel credentials.yaml; the dotenv is the separate, secret-only, uncommittable credential store (matches ~/.helix/credentials). |
| Cache / state | ~/.omnigraph/cache/, ~/.omnigraph/state/ |
Subdirs of the one dir (like ~/.aws/sso/cache/, ~/.kube/cache/) — cache is rm -rf-safe and backup-excludable without scattering across XDG roots. |
| Cedar policy | ./policies/<env>.yaml + <env>.tests.yaml |
Keep. Referenced by policy.file. |
| Schema | ./*.pg (e.g. schema.pg) |
Keep. |
| Stored queries | ./queries/*.gq |
Keep. .gq sources referenced by the queries: registry. |
Global dir: ~/.omnigraph/ — one place, with subdirectories. Everything OmniGraph keeps for a user lives under a single ~/.omnigraph/ directory, matching the peer group (~/.aws, ~/.kube, ~/.docker) and the direct competitor (~/.helix). This is what DB/cloud-CLI users expect and the lowest-cognitive-load shape.
Separation and "one place" are not in conflict — the decisive realization. The peer tools get config/cache/state separation via subdirectories inside the one dir, not via XDG's three scattered roots: ~/.aws/sso/cache/, ~/.kube/cache/. So OmniGraph keeps ~/.omnigraph/config.yaml, ~/.omnigraph/credentials, ~/.omnigraph/cache/ (catalogs — rm -rf-safe, backup-excludable), ~/.omnigraph/state/ (session, logs) — getting cache hygiene and a single discoverable location, without the XDG scatter. An earlier draft argued XDG on a false dichotomy (it assumed single-dir ⇒ mixed); subdirs dissolve it. ~/.omnigraph/ is canonical and documented; $XDG_CONFIG_HOME may optionally be honored if a user has set it, but XDG is not part of the mental model.
Env / override precedence (the KUBECONFIG analog):
OMNIGRAPH_CONFIG=/path— explicit config file, highest precedence.OMNIGRAPH_HOME=/path→ the global dir (default~/.omnigraph/);$XDG_CONFIG_HOMEoptionally honored if a user has set it, but~/.omnigraph/is canonical.- Cache and state are subdirs of the one dir:
~/.omnigraph/cache/(cached remote catalogs),~/.omnigraph/state/(session, logs). - Per-server/-graph token resolution (existing mechanism, extended to servers):
bearer_token_env: <VAR>names the var → resolved from a real process env var → else theauth.env_filedotenv (named lookup) → (future) OS keychain. Operator-chosen var names use theOMNIGRAPH_/OG_prefix by convention;DEFAULT_BEARER_TOKEN_ENVis the fallback name.
5. Credentials, connection tiers, and bind portability (12-factor)
Credentials are by-reference everywhere, never inlined at any layer — and the mechanism already exists. OmniGraph today resolves a bearer token by env-var name (bearer_token_env: on a graph) and loads secrets from a git-ignored dotenv (auth.env_file:, e.g. .env.omni) via load_env_file_into_process — which sets only vars not already in the environment, so real env vars win over the file (standard dotenv precedence). This RFC extends that mechanism, it does not replace it:
bearer_token_env: <VAR>gains a per-server form (servers.<name>.bearer_token_env) alongside the existing per-graph form.auth.env_filegains a global location (~/.omnigraph/credentials) layered under the project.env.omni.- Resolution order (existing
resolve_remote_bearer_token, extended): process env var →auth.env_filenamed lookup → (future) OS keychain.
There is no new credentials.yaml — the dotenv is the separate, secret-only, git-ignored credential store (it already appears in .gitignore). The reason secrets gravitate to ~/the dotenv rather than the committed config is that the project manifest is shareable — not a schema constraint. This keeps the design safe for git (manifest shareable) and for agents (no inline secrets anywhere they can read). The keychain is an additive future resolver, not a replacement for the dotenv default.
Three connection tiers (Supabase/Prisma teach the zero-config floor):
- Env vars —
OMNIGRAPH_SERVER=https://…+OMNIGRAPH_TOKEN=…: zero-config remote, no file (theDATABASE_URLfloor). - Global
config.yaml— namedservers:+targets:for multi-server setups (the AWS-profiles convenience). - Project
omnigraph.yaml— project-pinned targets/graphs, committed.
Keep omnigraph.yaml a portable manifest (12-factor). Deploy-specific runtime that varies per environment — the bind host/port, worker counts — should be supplied by --bind / OMNIGRAPH_BIND (flags/env), not a committed server.bind: baked into the manifest. A manifest that hardcodes 0.0.0.0:8080 is not portable across deploys and leaks an environment detail into a version-controlled file. The same-named omnigraph.yaml stays portable across deploys precisely because the volatile, per-environment knobs live in env/flags (12-factor config), while the stable, portable definition (graphs, queries, policy) lives in the file. This is the one concrete lesson taken from kube's model-B without adopting its file split: portability via env/flags, not via a second file.
6. Where stored queries live: defined locally, invoked remotely
A stored query splits across two axes; do not conflate them:
- Definition (
.gqsource +queries:entry) lives in the deployment manifest (server-roleomnigraph.yaml+ the.gqfiles), GitOps'd. Local to the deployment/project, never in a client's connection config. - Discovery ("what tools exist for me?") is fetched from the server (Cedar-filtered
GET /queries/ MCP catalog) at connect time. - Invocation is remote (client → server, HTTP/MCP) — or embedded (the CLI opens the graph directly and reads the same manifest).
So the client carries pointers to servers, not query definitions; it discovers and invokes, never defines. This is the capability-as-code guarantee for agents: an agent can only invoke tools the server's committed, reviewed config exposes — it cannot define a new tool at runtime. Definition is structurally outside the agent's reach.
queries: (manifest, server/remote, Cedar-gated, MCP) and aliases: (manifest, embedded CLI only) overlap — both are "named .gq queries in the manifest." This RFC keeps them siblings (the MR-969 decision); the clean long-term is one registry, two invocation surfaces (embedded + remote), with aliases: subsumed. Out of scope here.
Reconciling aliases: with the role model
aliases: is the pre-MR-969, client-role, embedded-only, ungated ancestor of queries:. An alias bundles command (read/change), query (.gq path), name (symbol), args (positional param names), and graph/branch/format defaults; the CLI runs it embedded. The server never reads it. So:
- Role:
aliases:is client-role (CLI behavior) → it may live in both the user-globalconfig.yamland the project manifest, layered.queries:is server-role → it lives only in the deployment manifest (the server reads the manifest, not~). Who reads it determines where it can live. - Difference:
aliases:= embedded invocation, no gating, explicitcommand, bundles client defaults + positional args.queries:= remote (+future embedded), Cedar +mcp.expose, infers read/mutate, bundles only MCP settings. - Convergence: decompose an alias — definition (name→.gq+symbol) →
queries:(the superset: typed, validated, gated, multi-surface, no redundantcommand); target/branch/format → client invocation context (--target/--branch/--formatordefaults:), not baked per-query; positionalargs→ thin CLI sugar or dropped (agents/services use named JSON params). End-state: onequeries:registry + the client config model subsumesaliases:. - v1: keep
aliases:unchanged. Footgun worth a load-time warn: an alias and a query with the same name in one manifest are different namespaces invoked differently (--alias XvsPOST /queries/X).
7. CLI surface
omnigraph login <server>— interactive auth; writes the token to theauth.env_filedotenv (0600) or the keychain. Thegh auth loginanalog.omnigraph use <graph>— set the active graph (writes the appropriate layer). Thekubectl config use-contextanalog.omnigraph config view [--resolved] [--show-origin] [<graph>]— print the merged config and, with--resolved, the final tuple plus the origin layer of every field (thegit config --show-origin/kubectl config viewanalog). Resolution is never a mystery.- All existing verbs (
query,mutate,load,schema,branch, …) gain--graph <name>; resolution decides embedded vs remote transparently.
7.5 Init, login, and bootstrap — three tiers (folds in the Q2 design)
Scaffolding splits into three tiers by scope and fatness, mirroring the field (supabase init vs login; HelixDB thin init vs fat chef). Most of this lives in sibling tickets; this RFC owns only the user route.
| Tier | Command | Scope | What it does | Model | Status |
|---|---|---|---|---|---|
| User route | omnigraph login [<server>] |
user (~/.omnigraph/) |
auth + write ~/.omnigraph/config.yaml / credentials; first-run global setup |
gh / supabase login |
this RFC (unbuilt) |
| Thin project init | omnigraph init |
project, in-place | create graph + scaffold_config_if_missing (omnigraph.yaml + minimal .pg/.gq); refuse-if-exists or --force |
cargo init, prisma init |
exists; --force purge = MR-975 |
| Fat bootstrap | omnigraph quickstart [--template <t>] [--auto] |
project, possibly new-dir | scaffold + seed data + serve start + agent prompt file |
HelixDB chef, create-next-app |
MR-973 (unbuilt) |
Design positions (first-principles, since none of the fat tier is built):
- Split
init(project) fromlogin(user) — never one command writing to both$HOMEand the project (the supabase line, not the dbt line).init=project scaffold;login=user credential + global config. initis in-place + refuse-if-exists (cargo/prisma/terraform default): don't clobber; adopt existing files; require--forceto overwrite (and--forcepurges Lance state per MR-975).- Interactive for humans,
--auto/agent-mode for automation (npm-y, create-*--CI, MR-981--machine). InOMNIGRAPH_AGENT_MODEany prompt → fail with a repair hint. - Templates are a
--template <name>flag on the fat tier (create-vite model), with the content (schema + queries + seed) coming from a template source. Mechanism is a design question (bundled-in vsog template pullfrom a repo vsnpm create-*-style delegation) — not an existing foothold (MR-581 stale). Lean: a small set of bundled templates first (genericPerson→Knows, plus promoteomnigraph-intel-bootstrap),--template <github>later. init/quickstartcan scaffold thegraphs:map with one or more entries; "init with specific graphs" = the scaffoldedgraphs:block (embeddeduri:locally; the agent/operator adds remoteserver:entries vialogin+ editing).- Secrets-on-scaffold rule (prisma/dbt/supabase all do this): anything that writes a token or
.env-shaped file also writes/updates.gitignoreto exclude it.init/loginmust keep theauth.env_filegit-ignored.
8. Concrete shape
Global ~/.omnigraph/config.yaml (per-user, secret-free):
servers:
prod-us: { endpoint: https://og-us.internal:8080, bearer_token_env: OG_PROD_US_TOKEN }
prod-eu: { endpoint: https://og-eu.internal:8080, bearer_token_env: OG_PROD_EU_TOKEN }
staging: { endpoint: https://og-staging.internal:8080, bearer_token_env: OG_STAGING_TOKEN }
auth:
env_file: ~/.omnigraph/credentials # git-ignored dotenv holding OG_*_TOKEN values
defaults:
graph: dev
Project ./omnigraph.yaml (committed, secret-free, portable — no server.bind). Note the shipped noun is graphs: (MR-603); an entry is embedded (uri) XOR remote (server + remote graph name):
graphs:
dev: { uri: s3://team-bucket/dev.omni, branch: main } # embedded
staging: { server: staging, graph: prod, branch: review } # remote → graph `prod` on server `staging`
prod-us: { server: prod-us, graph: production }
prod-eu: { server: prod-eu, graph: production } # multi-server, same remote graph name
defaults: { graph: dev, output_format: table }
queries: { find_user: { file: ./queries/find_user.gq, mcp: { expose: true } } }
operations: { ... } # the soon-to-be-renamed `aliases:` (MR-839)
Select with --graph <name> (shipped flag, MR-603).
Credentials the git-ignored auth.env_file dotenv (~/.omnigraph/credentials, 0600) holds the OG_*_TOKEN values; real env vars override it. No committable secrets.
DX
- One command surface, two loci.
query --target dev(embedded) and--target staging(remote) are the same command; only resolution differs. Change one word, not a mental model. - Clone-and-go. Project config names servers+graphs; teammate runs
omnigraph login stagingonce and every target resolves. The git +gh auth loginmodel. - Multi-server × multi-graph is the default. Targets reference
serverby name;serversis a global named map; graphs are per-server.prod-usandprod-euboth servingproductionis two target entries — Helix cannot express this. - Solo-first. Everything in
~, no project required. - Laptop-to-fleet on one schema. Local = one
omnigraph.yaml(both roles); prod = role-split across repos. No second format to learn.
AX (agent experience)
- One flat resolved context, never a config to navigate. target→server→endpoint→token resolves before the agent sees anything. The agent reasons about tools, not topology (the LLM-safe-surface principle extended to config).
- Secrets are structurally outside the agent's reach. The repo it operates in has no tokens; they are in the global layer / keychain, outside its view. An agent cannot exfiltrate a prod token from project config because it is not there.
- Branch/snapshot-pinned contexts (E4) — hand an agent a
branch: review/--snapshot v42target and its reads are reproducible and cannot see uncommitted main-line state. No kubeconfig analog. - The agent's capabilities are a GitOps'd artifact (E6) — which graphs exist, which stored-query tools it may call, and which Cedar rules gate them are all in the version-controlled server config. Powers change only via a reviewed PR, deployed by restart. Infrastructure-as-code for what the AI can do.
- Config + policy compose. Config = "where am I pointed + which token"; Cedar = "what may I do there." Orthogonal; no enforcement logic leaks into config.
GitOps — three surfaces, secrets in none
| Surface | Repo | Contents | Deploy | Secrets |
|---|---|---|---|---|
| Server deployment config | infra/deploy repo | graphs:, policy, queries: + .gq files |
commit → CI → server restart (no hot reload) | none — by-reference |
| Project client config | app repo | targets: → servers/graphs |
committed, read by CLI/agent | none |
| Global user config | not GitOps'd — machine-local ~ |
servers: + creds-by-ref |
omnigraph login writes it |
refs only (like ~/.kube/config) |
Comparison
| Property | kubeconfig | Helix | git | compose | OmniGraph (this RFC) |
|---|---|---|---|---|---|
| Named remote endpoints + creds-by-ref | ✅ | ✅ | partial | partial | ✅ (global servers) |
| Global + project layering, uniform schema | ✗ | ✗ | ✅ | ✗ | ✅ |
| Embedded OR remote under one name | ✗ | ✗ | n/a | ✗ | ✅ (E1) |
| Multi-server × multi-graph | ✅ | ✗ | n/a | n/a | ✅ (E2) |
| Branch/snapshot in the address | ✗ | ✗ | partial | ✗ | ✅ (E4) |
| Agent tool surface in the repo | ✗ | ✗ (separate bundle) | n/a | n/a | ✅ (E6) |
| Project manifest renamed by role | — | no | — | no | no |
| Concept count | 3 | 1 | 2 | 1 | 2 (servers/targets) |
Migration / backwards compatibility
- Additive. Today's
omnigraph.yaml(graphs:,cli:,server:,aliases:,policy:) keeps working unchanged.graphs:entries are equivalent to embeddedtargets:with auri:; both resolve. targets:is new and optional.servers:is new and optional. Absent → today's behavior.- Global
~/.omnigraph/config.yamlis new. Absent → only project + env + flags, exactly as now. Its addition is the global-first posture flip: today the CLI is project-anchored (reads./omnigraph.yaml, no parent walk); the global config becomes the new primary discovery path so the CLI works with no project file. Existing project-only workflows are unchanged (project still overrides global); the flip is additive — it adds a fallback layer below the project file, it does not remove the project file. graphs:→targets:is an evolution, not a break. Both can coexist;targets:is the superset (adds remote + branch pinning). A future cleanup may aliasgraphs:to embeddedtargets:.server.bindstays supported but documentation steers operators to--bind/OMNIGRAPH_BINDfor portability; no removal.- Credentials reuse existing fields.
bearer_token_envandauth.env_fileare unchanged; the RFC only extends them —bearer_token_envgains a per-servers.<name>form, andauth.env_filegains a global location (~/.omnigraph/credentials) layered under the project.env.omni. Nocredentials.yaml, notoken_env. Existing dotenv setups keep working.
Open questions
graphs:vstargets:naming churn. Do we renamegraphs:→targets:(with a deprecation alias) or keepgraphs:for embedded and addtargets:for remote? Leaning: keep both, documenttargets:as the superset.- Keychain integration scope. macOS Keychain first (matches operator practice), with a
0600file fallback; Linux Secret Service later? - Project-local
servers:. Allowed (e.g. a localhost dev server), merged with global. Confirm creds stay by-reference even for project-local servers (yes). aliases:⇄queries:convergence. Out of scope here; tracked separately. One registry with embedded + remote invocation surfaces is the target end state.- Single-file
KUBECONFIG-style list. Do we supportOMNIGRAPH_CONFIGpointing at multiple files (colon-joined), or a single file only? Start single; revisit if demand appears.
Rollout
Phased, each independently useful:
- Env-var remote tier —
OMNIGRAPH_SERVER+OMNIGRAPH_TOKEN, plus an HTTP client mode in the CLI. Zero-config remote; unblockscurl-free server use. (Smallest; highest leverage.) - Global
config.yaml+servers:/targets:+ resolution +config view. Named multi-server targeting, layered with the project file. omnigraph loginwriting theauth.env_filedotenv (and keychain). Credential management on top of the existing dotenv resolver.omnigraph use+ branch/snapshot-pinned targets. Active-context switching; reproducible agent contexts.
Phases 1–2 close the substantive gap; 3–4 are ergonomics. Evaluate after phase 2 against early-adopter and agent-onboarding (MR-973 / MR-974) signal.
Prior art
- kubeconfig (clusters / users / contexts;
KUBECONFIG;kubectl config view) - Helix CLI v2 (
helix.tomllocal+enterprise instance blocks;~/.helix/config;~/.helix/credentials) - AWS CLI (
~/.aws/config+~/.aws/credentialssplit; named profiles;credential_process) - git (
~/.gitconfig+.git/config;--show-origin) - Cargo (
Cargo.tomlmanifest +~/.cargo/config.toml) - Supabase / Prisma (one project manifest; connection via
DATABASE_URLenv) - 12-factor app (config that varies by deploy lives in the environment)