omnigraph/docs/dev/rfc-002-config-cli-architecture.md
Ragnor Comerford f2a7ae739b
docs: correct stale-ticket claims and fold init/bootstrap design into RFC-002
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>
2026-05-30 18:13:34 +02:00

38 KiB
Raw Blame History

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:

  1. 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 (the kubectl/aws/gh posture), unlike today's project-anchored behavior.
  2. 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).
  3. 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).
  4. Credentials by reference, reusing OmniGraph's existing mechanism — bearer_token_env: (token resolved by env-var name) + a git-ignored auth.env_file dotenv — extended to servers and the global layer; OS keychain as a future resolver. No new credentials.yaml; every committed/GitOps'd surface stays secret-free.
  5. A file-naming decision: project and server config are the same artifact, same name (omnigraph.yaml); the only differently-named file is the user-global config.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, NOT target/targets. The config key is graphs: in config.rs and the flag is --graph. This RFC uses graphs:/--graph throughout; the unifying noun is a graphs: entry that is embedded (uri:) XOR remote (server: + a remote graph name). Read any lingering targets:/--target below as graphs:/--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 template command). 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 init exists (with scaffold_config_if_missing, main.rs:1415); the rest of the "init family" (quickstart MR-973, serve MR-970, prune/init --force MR-972/975, mcp install/skills MR-974, agent-mode MR-981) are unbuilt tickets, some stale.
  • Config still uses aliases: (no operations: in code; MR-839 unbuilt). §6's reconciliation talks about aliases: as-is, noting operations: is a proposed rename.
  • bearer_token_env exists (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 lint command 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. curl is the fallback. There is no named, switchable, credential-carrying way to say "run this against prod on 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-homeds3://b/prod may be prod on server A, production on server B, and opened directly by the CLI. Today's flat graphs: map (name→storage-URI) can't express "graph production on server prod-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_file dotenv (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 E1E6 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 E1E6 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 sets defaults.target and inherits the global defaults.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 wholesaleno field-level deep-merge within an entry. (kubeconfig: union contexts by name.) The footgun this avoids: global servers.prod = {endpoint, bearer_token_env}, project servers.prod = {endpoint: other} — deep-merge would silently retain the old bearer_token_env; replace makes the project's prod self-contained and predictable.
  • Lists/arraysreplace, 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 .gq path, or a policy.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-origin prints each final value and which layer set it (the git config --show-origin / kubectl config view rule). 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-graph policy.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_HOME optionally 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 the auth.env_file dotenv (named lookup) → (future) OS keychain. Operator-chosen var names use the OMNIGRAPH_ / OG_ prefix by convention; DEFAULT_BEARER_TOKEN_ENV is 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_file gains a global location (~/.omnigraph/credentials) layered under the project .env.omni.
  • Resolution order (existing resolve_remote_bearer_token, extended): process env var → auth.env_file named 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):

  1. Env varsOMNIGRAPH_SERVER=https://… + OMNIGRAPH_TOKEN=…: zero-config remote, no file (the DATABASE_URL floor).
  2. Global config.yaml — named servers: + targets: for multi-server setups (the AWS-profiles convenience).
  3. 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 (.gq source + queries: entry) lives in the deployment manifest (server-role omnigraph.yaml + the .gq files), 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-global config.yaml and 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, explicit command, 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 redundant command); target/branch/format → client invocation context (--target/--branch/--format or defaults:), not baked per-query; positional args → thin CLI sugar or dropped (agents/services use named JSON params). End-state: one queries: registry + the client config model subsumes aliases:.
  • 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 X vs POST /queries/X).

7. CLI surface

  • omnigraph login <server> — interactive auth; writes the token to the auth.env_file dotenv (0600) or the keychain. The gh auth login analog.
  • omnigraph use <graph> — set the active graph (writes the appropriate layer). The kubectl config use-context analog.
  • omnigraph config view [--resolved] [--show-origin] [<graph>] — print the merged config and, with --resolved, the final tuple plus the origin layer of every field (the git config --show-origin / kubectl config view analog). 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) from login (user) — never one command writing to both $HOME and the project (the supabase line, not the dbt line). init=project scaffold; login=user credential + global config.
  • init is in-place + refuse-if-exists (cargo/prisma/terraform default): don't clobber; adopt existing files; require --force to overwrite (and --force purges Lance state per MR-975).
  • Interactive for humans, --auto/agent-mode for automation (npm -y, create-* --CI, MR-981 --machine). In OMNIGRAPH_AGENT_MODE any 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 vs og template pull from a repo vs npm create-*-style delegation) — not an existing foothold (MR-581 stale). Lean: a small set of bundled templates first (generic Person→Knows, plus promote omnigraph-intel-bootstrap), --template <github> later.
  • init/quickstart can scaffold the graphs: map with one or more entries; "init with specific graphs" = the scaffolded graphs: block (embedded uri: locally; the agent/operator adds remote server: entries via login + editing).
  • Secrets-on-scaffold rule (prisma/dbt/supabase all do this): anything that writes a token or .env-shaped file also writes/updates .gitignore to exclude it. init/login must keep the auth.env_file git-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

  1. 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.
  2. Clone-and-go. Project config names servers+graphs; teammate runs omnigraph login staging once and every target resolves. The git + gh auth login model.
  3. Multi-server × multi-graph is the default. Targets reference server by name; servers is a global named map; graphs are per-server. prod-us and prod-eu both serving production is two target entries — Helix cannot express this.
  4. Solo-first. Everything in ~, no project required.
  5. 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)

  1. 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).
  2. 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.
  3. Branch/snapshot-pinned contexts (E4) — hand an agent a branch: review / --snapshot v42 target and its reads are reproducible and cannot see uncommitted main-line state. No kubeconfig analog.
  4. 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.
  5. 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 embedded targets: with a uri:; both resolve.
  • targets: is new and optional. servers: is new and optional. Absent → today's behavior.
  • Global ~/.omnigraph/config.yaml is 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 alias graphs: to embedded targets:.
  • server.bind stays supported but documentation steers operators to --bind / OMNIGRAPH_BIND for portability; no removal.
  • Credentials reuse existing fields. bearer_token_env and auth.env_file are unchanged; the RFC only extends them — bearer_token_env gains a per-servers.<name> form, and auth.env_file gains a global location (~/.omnigraph/credentials) layered under the project .env.omni. No credentials.yaml, no token_env. Existing dotenv setups keep working.

Open questions

  • graphs: vs targets: naming churn. Do we rename graphs:targets: (with a deprecation alias) or keep graphs: for embedded and add targets: for remote? Leaning: keep both, document targets: as the superset.
  • Keychain integration scope. macOS Keychain first (matches operator practice), with a 0600 file 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 support OMNIGRAPH_CONFIG pointing at multiple files (colon-joined), or a single file only? Start single; revisit if demand appears.

Rollout

Phased, each independently useful:

  1. Env-var remote tierOMNIGRAPH_SERVER + OMNIGRAPH_TOKEN, plus an HTTP client mode in the CLI. Zero-config remote; unblocks curl-free server use. (Smallest; highest leverage.)
  2. Global config.yaml + servers:/targets: + resolution + config view. Named multi-server targeting, layered with the project file.
  3. omnigraph login writing the auth.env_file dotenv (and keychain). Credential management on top of the existing dotenv resolver.
  4. omnigraph use + branch/snapshot-pinned targets. Active-context switching; reproducible agent contexts.

Phases 12 close the substantive gap; 34 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.toml local+enterprise instance blocks; ~/.helix/config; ~/.helix/credentials)
  • AWS CLI (~/.aws/config + ~/.aws/credentials split; named profiles; credential_process)
  • git (~/.gitconfig + .git/config; --show-origin)
  • Cargo (Cargo.toml manifest + ~/.cargo/config.toml)
  • Supabase / Prisma (one project manifest; connection via DATABASE_URL env)
  • 12-factor app (config that varies by deploy lives in the environment)