omnigraph/docs/user/cli/reference.md
Andrew Altshuler d2340f19e9
feat(cli)!: schema apply refuses a cluster-managed graph (RFC-011 D10) (#253)
`omnigraph schema apply` against a cluster-managed graph's storage root bypassed
the cluster ledger/recovery/approvals. Mirror `init`'s refusal: on the embedded
(direct-store) path, if the resolved URI is inside a cluster
(`cluster_root_for_graph_uri`), bail and point at `cluster apply`. The served
(`--server`) path is unaffected — it addresses a server, not a storage root.
`schema plan`/`show` (read-only) are untouched.

Two e2e tests injected "out-of-band drift" via this exact CLI path; since the
CLI now refuses it, they inject drift via a direct engine `apply_schema` against
the storage root instead — a faithful control-plane bypass, which is what
out-of-band drift is. New regression:
`schema_apply_refuses_a_cluster_managed_graph_and_signposts_cluster_apply`.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 23:11:42 +03:00

18 KiB

CLI Reference (omnigraph)

A reference for the omnigraph binary's command surface and the per-operator ~/.omnigraph/config.yaml schema. For a quick-start guide, see cli.md.

Top-level command families and subcommands. Graph-targeting commands accept a positional file:///s3:// URI, --server <name|url> (an operator-defined server from ~/.omnigraph/config.yaml by name, or a literal http(s):// URL, optionally with --graph <id> for multi-graph servers; exclusive with a positional URI), --store <uri> (a single graph's storage directly), or --profile <name> / $OMNIGRAPH_PROFILE (a named scope bundle; see Scopes & profiles); cluster commands use --config <dir>. A remote server is addressed only with --server — a positional http(s):// URI is rejected. query/mutate are the exception: their positional is a stored-query name (RFC-011 D3), not a graph URI, so they address the graph only via --store/--server/--profile/defaults.

Top-level commands

Command Purpose
init --schema <pg> → initialize a graph (start cluster configs from the cluster.md quick-start)
load bulk load a branch, local or remote (--mode overwrite|append|merge is required — overwrite is destructive, so there is no default). Without --from the target branch must exist; --from <base> forks a missing --branch from <base> first
ingest deprecated alias of load --from <base> (defaults: --from main --mode merge); prints a one-line warning to stderr
query <name> (alias: read) run a read query. Catalog lane (default): <name> is a stored query invoked by name from the served catalog (served-only — address with --server/--profile; the verb asserts the query is a read). Ad-hoc lane: with --query <path> or -e/--query-string <GQ>, runs that source (the positional <name> then selects which query in it). No positional graph URI — address via --store/--server/--profile. read is the deprecated previous name (one-line stderr warning)
mutate <name> (alias: change) run a mutation query; same catalog (by-name, served-only, verb asserts mutation) / ad-hoc (--query/-e) lanes as query. change is the deprecated previous name (one-line stderr warning)
alias <name> [args] invoke an operator alias — a personal binding (under aliases: in ~/.omnigraph/config.yaml) to a stored query on a named server (RFC-011 D4; replaces the removed --alias flag)
snapshot print current snapshot (per-table version + row count)
export dump to JSONL on stdout (--type T, --table K filters)
branch create | list | delete | merge branching ops
commit list | show inspect commit graph
schema plan | apply | show (alias: get) migrations. apply refuses a cluster-managed graph (one whose storage is inside a cluster) and points at cluster apply — those graphs evolve through the cluster ledger, not a direct apply
lint (alias: check) offline / graph-backed query validation. Replaces query lint / query check, which are kept as deprecated argv-level shims that print a one-line warning and rewrite to omnigraph lint
cluster validate | plan | apply | approve | status | refresh | import | force-unlock declarative cluster control plane. validate checks a local cluster.yaml folder and referenced schema/query/policy files; plan diffs it against local JSON state at __cluster/state.json, annotates dispositions, and embeds real schema-migration previews; apply converges the cluster — stored-query/policy catalog writes (content-addressed under __cluster/resources/), graph creates, schema updates (soft drops only; --as records the actor), and graph deletes behind a digest-bound approval from cluster approve <resource> --as <actor> (apply/approve default the actor from ~/.omnigraph/config.yaml's operator.actor when --as is omitted); what apply converges is what an omnigraph-server --cluster <dir> deployment serves on its next restart (--cluster is the server's only boot source — RFC-011 cluster-only); status reads the state ledger; refresh/import explicitly update local JSON state from read-only graph observations; force-unlock <LOCK_ID> manually removes a held local state lock by exact id
optimize non-destructive Lance compaction (skips tables with Blob columns or uncovered drift; --json reports skipped)
repair [--confirm] [--force] preview or explicitly publish uncovered manifest/head drift. --confirm heals verified maintenance drift and exits non-zero if suspicious/unverifiable drift is refused; --force --confirm publishes suspicious/unverifiable drift after operator review
cleanup --keep N --older-than 7d --confirm destructive version GC (--confirm to execute; also needs --yes against a non-local s3:// target — see Write diagnostics & destructive confirmation)
embed offline JSONL embedding pipeline
policy validate | test | explain Cedar tooling against a cluster's applied policies (--cluster <dir>; --graph <id> picks a graph's bundle when several apply). test takes --tests <file>; explain takes --actor/--action/--branch/--target-branch
version / -v print omnigraph 0.3.x

Command capabilities

Every command declares the capability it needs — what it requires to reach a graph — which determines the addressing flags that apply:

  • anyquery, mutate, load, ingest, branch *, snapshot, export, commit *, schema show, schema apply. Run against a graph served (via a server) or embedded (direct against a store): accept a positional file:///s3:// URI, --server <name|url> (+ --graph <id> for multi-graph servers), --store <uri>, or --profile <name>. A remote server is addressed with --server — a positional http(s):// URI does not dispatch to one.
  • servedgraphs list. Requires a server (accepts --server / --profile).
  • directinit, optimize, repair, cleanup, schema plan, queries validate, lint. Need direct storage access (file:// / s3://), never through a server. They accept a positional URI, but not --server, and a remote (http(s)://) URI is rejected. optimize / repair / cleanup additionally accept --cluster <dir|s3://…> --graph <id> (--cluster is a cluster directory or storage-root URI, named via clusters: in ~/.omnigraph/config.yaml or a literal root), which resolves the graph's storage URI from the served cluster state (so you needn't know the <storage>/graphs/<id>.omni layout). --graph is the one graph selector across all scopes — on these three verbs it picks the cluster graph; on the other direct verbs it does not apply.
  • controlcluster *. Operates on a cluster directory via --config <dir>.
  • localpolicy *, embed, login, logout, config, version, queries list. Address no graph.

These restrictions are enforced and reported, not silent:

  • A scope flag on a verb that can't consume it fails loudly rather than being silently dropped — --server outside a served scope, --cluster outside the maintenance verbs, or --graph where no multi-graph scope applies, e.g.: optimize is a direct (storage-native) command; --server addresses a served graph and does not apply. Pass a storage URI, or --cluster <dir> --graph <id>.
  • A direct verb pointed at a remote URI fails loudly, e.g.: optimize is a direct (storage-native) command and needs direct storage access; the resolved target is a remote server (https://…). Pass the graph's file:// or s3:// URI.
  • A data verb pointed at a positional http(s):// URI fails loudly: a remote graph must be addressed with --server <url> — a positional (or --uri) http(s):// URL no longer dispatches to a server.
  • init into an established cluster's storage layout (<root>/graphs/<id>.omni where <root> holds __cluster/state.json) is refused — graphs in a cluster are created by cluster apply (which records ledger / recovery / approvals), not init.

To maintain a server-backed graph, run the direct verbs from a host with storage access against the graph's storage URI (a positional URI, or --cluster … --graph …), out-of-band from the serving process — there are no server routes for optimize / repair / cleanup by design.

omnigraph --help lists commands with a capability legend at the bottom (any / served / direct / control / local).

Write diagnostics & destructive confirmation

Two global flags make writes self-documenting and guard the dangerous ones (RFC-011 Decision 9):

  • Every write echoes its resolved target to stderromnigraph load → s3://acme/brain/graphs/knowledge.omni (direct, remote) — so you catch a scope that resolved somewhere unexpected (e.g. prod) before it lands. Applies to load, ingest, mutate, branch create|delete|merge, schema apply, optimize, repair, cleanup. The line is stderr, so --json consumers reading stdout are unaffected; suppress it with --quiet.
  • Destructive writes against a non-local scope require confirmation. cleanup, overwrite load (--mode overwrite), and branch delete proceed freely against a local (file://) graph, but when the resolved target is not local (a served http(s):// graph or an s3:// store/cluster) they require explicit consent: pass --yes to confirm, an interactive terminal is prompted, and a non-interactive run (no TTY, or --json) refuses with an error rather than silently destroying. cleanup still also requires its existing --confirm (preview→execute); --yes is the additional non-local consent.

A "local" target is a bare path or a file:// URI; http(s)://, s3://, and other object-store schemes are non-local.

Config surfaces

Two config surfaces with single owners, plus a zero-config tier:

Surface Owner Location Declares
Cluster config the team, in a repo cluster.yaml + checkout (cluster-config.md) what the system is: graphs, schemas, queries, policies, storage
Operator config one person ~/.omnigraph/config.yaml (override dir with $OMNIGRAPH_HOME) who I am: identity, ergonomics
Flags / env per invocation everything, explicitly

~/.omnigraph/config.yaml (operator)

operator:
  actor: act-andrew     # default identity for the --as cascade: --as > operator.actor > none
servers:                # operator-owned endpoints; names key the credentials
  prod:
    url: https://graph.example.com     # no tokens in this file, ever
defaults:
  output: table         # read format default, below --json/--format/alias
  server: prod          # the everyday SERVED scope when no address is given (RFC-011)
  # store: file:///data/dev.omni   # OR a zero-flag LOCAL default (mutually
  #                                #   exclusive with `server`); the local-dev
  #                                #   counterpart of `server`
  default_graph: knowledge   # graph selected in a server/cluster scope
clusters:               # admin-only: managed-cluster storage roots (RFC-011).
  brain:                #   the ONLY place a storage root lives in this file.
    root: s3://acme/clusters/brain
profiles:               # named scope bundles (RFC-011); pick with --profile
  staging: { server: staging, default_graph: knowledge }   # a served scope
  brain-admin: { cluster: brain, default_graph: knowledge } # a direct cluster scope

Absent file = empty layer. Unknown keys warn and load (a file written for a newer CLI works on an older one). Override the config directory with $OMNIGRAPH_HOME.

Scopes & profiles (RFC-011)

A command resolves a scope — a server, a cluster, or a store — then selects a graph in it; the served-vs-direct access path is derived from the scope, not toggled. The scope comes from one of (highest precedence first): an explicit address (a positional URI, --server, or --store <uri>); a named --profile <name> (or $OMNIGRAPH_PROFILE); or the flat defaults.server + defaults.default_graph (a served default) or defaults.store (a zero-flag local default — mutually exclusive with defaults.server). A profile binds exactly one of server / cluster / store plus an optional default graph — config data, not state: every command resolves its scope fresh, there is no sticky "current" mode.

  • --store <uri> addresses a single graph's storage directly (ad-hoc / break-glass).
  • A cluster-bound profile reaches optimize / repair / cleanup for a managed graph (resolving its storage root from clusters:), the same as --cluster <root> --graph <id>. A --graph flag overrides the profile's default.
  • A server-bound scope on a maintenance verb, or a cluster-bound scope on a data verb, is rejected with a message pointing at the right addressing.
  • No graph selected (RFC-011 D7). When a scope has no --graph and no default_graph, the CLI never silently picks:
    • Cluster scope — exactly one applied graph is used automatically; several errors and lists the candidates (from the served catalog).
    • Server scope — a multi-graph server (any non-empty GET /graphs, even a single entry) errors and lists the candidates: you must pass --graph <id>. A single-graph / flat server (405 on /graphs), or one whose /graphs is policy-gated or unreachable, uses its bare URL as before.

--target, --cluster-graph, and the positional-http(s)://→remote dispatch have been removed (--graph is now the one graph selector across server and cluster scopes); operator defaults/--profile supply the no-flag scope and an explicit address always wins.

Credentials keyed by server name

omnigraph login <name> stores a bearer token in ~/.omnigraph/credentials (created 0600; group/world-readable files are refused). Token from --token, or — preferred, keeps it out of shell history — one line on stdin: echo $TOKEN | omnigraph login prod. omnigraph logout <name> removes it (idempotent).

Operator aliases — bindings, not content

An operator alias is a personal name for invoking a stored query on a named server — it carries no query content (the stored query in the catalog is the team's contract; the alias, its defaults, and its name are yours):

aliases:
  triage:
    server: intel-dev        # names an entry under servers:
    graph: spike             # optional (multi-graph servers)
    query: weekly_triage     # the STORED query's name — never a file
    args: [since]            # positional args -> params, in order
    params: { limit: 20 }    # fixed defaults; positionals/--params win
    format: table

omnigraph alias triage 2026-06-01 invokes POST <server>/graphs/spike/queries/weekly_triage with the keyed credential. Aliases live in their own alias namespace (RFC-011 Decision 4), so an alias can never shadow — or be shadowed by — a built-in verb. (The old --alias <name> flag on query/mutate was removed.)

A remote command whose URL prefix-matches an operator server's url (the gh host model — no flags needed) resolves its token through:

Order Source
1 OMNIGRAPH_TOKEN_<NAME> env (prodOMNIGRAPH_TOKEN_PROD)
2 [<name>] section in ~/.omnigraph/credentials
3 the default OMNIGRAPH_BEARER_TOKEN env

A keyed token is only ever sent to the server it is keyed to: a URL matching no operator server falls back to OMNIGRAPH_BEARER_TOKEN alone.

Cluster config preview

omnigraph cluster validate --config company-brain
omnigraph cluster plan     --config company-brain --json
omnigraph cluster apply    --config company-brain --json
omnigraph cluster approve  graph.<id> --config company-brain --as <actor>
omnigraph cluster status   --config company-brain --json
omnigraph cluster refresh  --config company-brain --json
omnigraph cluster import   --config company-brain --json
omnigraph cluster force-unlock <LOCK_ID> --config company-brain --json

--config is a directory containing cluster.yaml; it defaults to .. Stage 3A accepts graphs, schemas, stored queries, and policy bundle file references. cluster plan reads local JSON state from <config-dir>/__cluster/state.json; a missing file means empty state. Plan, apply, refresh, and import acquire __cluster/lock.json by default and release it before returning. cluster apply executes only stored-query/policy catalog writes (content-addressed under __cluster/resources/) and requires an existing state.json; graph/schema changes are deferred with warnings, and applied resources do not serve traffic until an `omnigraph-server --cluster

` restart picks them up. `cluster status` reads state only and reports any existing lock metadata. `force-unlock` removes a lock only when the supplied id exactly matches the lock file. `refresh` requires an existing `state.json`; `import` creates one only when it is missing. Both observe declared graphs read-only at `/graphs/.omni`. External state backends, graph/schema apply, automatic stale-lock breaking, `plan --refresh`, pipelines, UI specs, embeddings, aliases, and bindings are reserved for later stages. See [cluster-config.md](../clusters/config.md).

Output formats (query command, alias: read)

  • json — pretty-printed object with metadata + rows
  • jsonl — one metadata line then one JSON object per row
  • csv — RFC 4180-ish quoting
  • table — fitted text table, honors table_max_column_width + table_cell_layout
  • kv — grouped per-row key/value blocks

Param resolution

Precedence (high to low): explicit --params / --params-file, alias positional args. JS-safe-integer handling is built in (is_js_safe_integer_i64, JS_MAX_SAFE_INTEGER_U64) so 64-bit ids round-trip safely through JSON clients.

Bearer token resolution (CLI)

  1. graphs.<name>.bearer_token_env
  2. OMNIGRAPH_BEARER_TOKEN global env
  3. auth.env_file referenced .env

Duration parsing (cleanup)

s | m | h | d | w units, e.g. --older-than 7d.