HTTP Server & Cedar Policy
Contents
- Starting the server (boot sources)
- HTTP routes
- Auth
- Setup operations bypass the server
- Cedar policy
- Multi-graph mode
- Server + policy together
- Cluster-booted servers
How to run omnigraph-server and gate operations with Cedar policies.
Starting the Server
The server is the canonical runtime entry point — all CLI queries, mutations, and admin ops go through it. Boot is cluster-only (RFC-011): the server boots from a cluster and serves N graphs (N ≥ 1) under nested routes. There is no single-graph / bare-URI / omnigraph.yaml boot.
omnigraph-server --cluster ./company-brain --bind 127.0.0.1:8080 # a config directory …
omnigraph-server --cluster s3://bucket/prefix --bind 0.0.0.0:8080 # … or a storage-root URI (config-free)
--cluster boots from the cluster's applied revision (see Cluster-Booted Servers below). Run it in a separate terminal or background process.
HTTP Routes
All per-graph routes are nested under /graphs/{id}/... ({id} = a graph id from the applied cluster); bare flat paths (/query, /snapshot, …) return 404. /healthz and /graphs stay flat.
| Route | Purpose |
|---|---|
GET /healthz |
liveness probe (flat) |
GET /graphs |
enumerate served graphs (flat; graph_list-gated) |
GET /graphs/{id}/snapshot?branch= |
table state + row counts |
POST /graphs/{id}/query |
read query (canonical; /read = deprecated alias) |
POST /graphs/{id}/mutate |
mutation (/change = deprecated alias) |
POST /graphs/{id}/load |
bulk JSONL load, 32 MB; branch creation opt-in via from (/ingest = deprecated alias) |
POST /graphs/{id}/export |
NDJSON stream of a branch |
GET /graphs/{id}/queries · POST /graphs/{id}/queries/{name} |
stored-query catalog (read) + invocation (invoke_query, +change for a stored mutation; deny == 404) |
GET /graphs/{id}/schema · POST /graphs/{id}/schema/apply |
read .pg · migrate (schema_apply) |
GET/POST /graphs/{id}/branches · DELETE …/branches/{b} · POST …/branches/merge |
branch ops |
GET /graphs/{id}/commits?branch= · …/commits/{commit_id} |
history |
Read routes take ?branch=main or ?snapshot=<id>. Writes publish directly and commit atomically via __manifest; use the commits route for write/audit history.
Auth
Set bearer tokens on the server process. Three sources, in precedence: OMNIGRAPH_SERVER_BEARER_TOKENS_AWS_SECRET (AWS Secrets Manager) → OMNIGRAPH_SERVER_BEARER_TOKENS_JSON/_FILE (JSON {actor_id: token}) → OMNIGRAPH_SERVER_BEARER_TOKEN (single token, actor default):
OMNIGRAPH_SERVER_BEARER_TOKENS_JSON='{"act-reader":"s3cret"}' \
omnigraph-server --cluster ./company-brain --bind 0.0.0.0:8080
On the client side (0.7.0), register the server once and store its token out of band:
echo "s3cret" | omnigraph login remote # → ~/.omnigraph/credentials (0600)
omnigraph query get_signal --server remote --graph spike --params '{"slug":"sig-foo"}'
--server remote resolves the URL from ~/.omnigraph/config.yaml's servers: and the token via OMNIGRAPH_TOKEN_REMOTE or the credentials file. A token is only ever sent to the server it is keyed to.
Running without auth requires an explicit opt-in
You can no longer just "leave auth off." Since v0.6.0 the server refuses to start when it has neither bearer tokens nor a policy file, unless you explicitly opt in:
omnigraph-server --cluster . --unauthenticated
# or: OMNIGRAPH_UNAUTHENTICATED=1 omnigraph-server --cluster .
This is a guardrail against accidentally shipping an open server. For pure local dev, pass --unauthenticated deliberately.
Setup Operations Bypass the Server
init and local load write storage directly — they don't go through the server (a remote load is server-orchestrated, POSTing /load). Pass the repo URI:
omnigraph init --schema schema.pg s3://my-bucket/repos/<name>
omnigraph load --data seed.jsonl --mode overwrite s3://my-bucket/repos/<name>
Everything else — query, mutate, snapshot, schema plan/apply, branch, commit — goes through the running server.
Cedar Policy
Omnigraph can gate sensitive actions with Cedar policies.
Default-deny posture
Policy is enforced engine-wide (every authoring path calls the same gate), and the default is closed, not open:
| Server state | Bearer tokens | Policy file | Behavior |
|---|---|---|---|
| Open | no | no | Every request permitted — but the server refuses to start without --unauthenticated / OMNIGRAPH_UNAUTHENTICATED=1. |
| DefaultDeny | yes | no | Every authenticated request for an action other than read is rejected (HTTP 403). "Tokens but forgot the policy file" no longer ships the illusion of protection. |
| PolicyEnabled | yes | yes | Requests are evaluated against your Cedar rules. |
So configuring a policy file is what enables writes — there is no "permit everything by default" mode once tokens are set.
Gated actions
Per-graph actions (evaluated against the graph being addressed):
| Action | Protects |
|---|---|
read |
query execution |
export |
data export |
change |
mutations |
invoke_query |
stored-query invocation via POST /graphs/{id}/queries/{name} (graph-scoped, not branch-scoped). A stored mutation is double-gated — it also passes change. For a caller without the grant, a denial and an unknown query name both return the same 404 so the catalog can't be probed. |
schema_apply |
schema migrations |
branch_create |
branch creation |
branch_delete |
branch deletion |
branch_merge |
merges (especially into protected branches) |
admin exists but is reserved (no call site yet — don't write rules for it). A server-scoped graph_list action gates GET /graphs; declare it in a [cluster]-scoped bundle.
For any shared repo, gate at least schema_apply and branch_merge.
Where policy is declared
Cedar bundles are declared in cluster.yaml and attach via applies_to: [cluster] is the server-level engine (gates graph_list / GET /graphs); [<graph-id>] is that graph's engine (gates invoke_query, read, change, branch_*, schema_apply). cluster apply publishes them and the --cluster server enforces the applied revision. The policy.yaml rule format (below) is the bundle content.
policy.yaml shape
The policy model is allow-only: every rule is a permit. You grant capabilities to groups; anything ungranted is denied by default. There is no deny / effect key — to forbid something, simply don't grant it.
version: 1 # required; must be 1
groups:
admins: [act-alice, act-bob]
team: [act-carol, act-dan]
protected_branches:
- main
rules:
- id: admins-can-apply-schema # rules use `id`, not `name`
allow: # required `allow:` block
actors: { group: admins } # references a group by name
actions: [schema_apply]
target_branch_scope: protected
- id: team-can-merge-to-protected
allow:
actors: { group: team }
actions: [branch_merge]
target_branch_scope: protected
- id: team-can-read-write-unprotected
allow:
actors: { group: team }
actions: [read, change]
branch_scope: unprotected
To "block unreviewed schema applies," you don't write a deny rule — you just don't grant schema_apply to that group. Default-deny does the rest.
Scope rules (a rule's allow block may use at most one):
branch_scope: any | protected | unprotected— forread,export,change(matches the source branch).target_branch_scope: any | protected | unprotected— forschema_apply,branch_create,branch_delete,branch_merge(matches the destination branch).
Validate, test, explain
# Compile Cedar + check the cluster's applied policies
omnigraph policy validate --cluster .
# Run declarative test cases
omnigraph policy test --cluster . --tests policy.tests.yaml
# Debug a single decision
omnigraph policy explain \
--actor act-alice \
--action schema_apply \
--target-branch main \
--cluster .
Test cases (policy.tests.yaml)
version: 1 # required; must be 1
cases:
- id: alice-can-apply-schema # cases use `id`, not `name`
actor: act-alice
action: schema_apply
target_branch: main # schema_apply is target-branch scoped
expect: allow # `allow` / `deny` (not `permit`)
- id: random-user-cannot-merge-to-main
actor: act-random
action: branch_merge
target_branch: main
expect: deny
Run policy test after every policy edit. Tests are cheap.
Multi-graph serving
A --cluster server serves every graph in the applied cluster, each under /graphs/{id}/.... GET /graphs enumerates them (sorted by id), gated by the cluster-level graph_list action — even under --unauthenticated, topology stays closed until a [cluster] policy grants it. omnigraph graphs list mirrors it (remote servers only).
Policy attaches at two levels via cluster.yaml applies_to:
[<graph-id>]— per-graph rules (read,change,branch_*,schema_apply,invoke_query).[cluster]— server-level rules (graph_list).
There is no runtime add/remove of graphs — edit cluster.yaml, cluster apply, restart.
Server + Policy Together
When the server is running with a policy file:
- Every request resolves the actor from the bearer token (the client cannot set actor identity) and checks it against Cedar rules.
- Unauthorized requests return
403 Forbidden. - The CLI doesn't bypass policy when it connects over HTTP — it's enforced at the server. Enforcement is also engine-wide, so CLI direct-engine writes and embedded SDK consumers hit the same gate.
Setup ops (init, load) write storage directly. With a policy configured they still flow through the engine-layer enforce gate for the actor you pass via --as (or operator.actor in ~/.omnigraph/config.yaml); gate the raw storage layer too (S3 bucket ACLs, object locks) if the bucket is shared.
Cluster-Booted Servers
omnigraph-server --cluster <dir|s3://> is the only boot source (covered above). It serves the cluster's applied revision: cluster apply changes take effect on the next restart (no hot reload), and boot is fail-fast with named remedies for missing/pending/tampered state. Bearer tokens and bind stay process-level (env/flags). See references/cluster.md.