CLI: - Add -e / --query-string <STRING> to omnigraph read and omnigraph change - Exactly one of --query, --query-string, --alias is required (3-way XOR) - Empty --query-string is rejected with a clear error HTTP: - New POST /query (read-only, clean field names: query/name/params/branch/snapshot) - Mutations on /query are rejected with 400 -- use POST /change instead - ChangeRequest fields polished: query (alias query_source), name (alias query_name) - POST /read and POST /change remain byte-compatible for existing clients Tests: - cli.rs: -e happy-path on read/change, mutex error vs --query, empty -e rejected - system_local.rs: inline -e read and -e change exercise the local flow - system_remote.rs: inline -e read/change over HTTP plus direct /query 200/400 - server.rs: /query 200, /query 400 on mutation, /change legacy field alias - openapi.rs: new /query path, QueryRequest schema, ChangeRequest field-name polish Docs: cli.md (-e examples), cli-reference.md (read/change rows), server.md (/query) Co-Authored-By: Ragnor Comerford <ragnor.comerford@gmail.com>
6.1 KiB
HTTP Server (omnigraph-server)
Axum 0.8 + tokio + utoipa-generated OpenAPI. Single repo per process; deploy multiple processes for multi-tenant.
Endpoint inventory
| Method | Path | Auth | Action | Handler |
|---|---|---|---|---|
| GET | /healthz |
none | — | server_health |
| GET | /openapi.json |
none | — | server_openapi (strips security if auth disabled) |
| GET | /snapshot?branch= |
bearer + read |
snapshot of branch | server_snapshot |
| POST | /read |
bearer + read |
run named query (legacy field names query_source/query_name) |
server_read |
| POST | /query |
bearer + read |
run inline read query (clean field names query/name; mutations → 400) |
server_query |
| POST | /export |
bearer + export |
NDJSON stream | server_export |
| POST | /change |
bearer + change |
mutation (query/name; accepts legacy query_source/query_name as serde aliases) |
server_change |
| GET | /schema |
bearer + read |
get current .pg source |
server_schema_get |
| POST | /schema/apply |
bearer + schema_apply (target=main) |
migrate | server_schema_apply |
| POST | /ingest |
bearer + branch_create (if new) + change |
bulk load | server_ingest (32 MB body limit) |
| GET | /branches |
bearer + read |
list branches | server_branch_list |
| POST | /branches |
bearer + branch_create |
create | server_branch_create |
| DELETE | /branches/{branch} |
bearer + branch_delete |
delete | server_branch_delete |
| POST | /branches/merge |
bearer + branch_merge |
merge source → target |
server_branch_merge |
| GET | /commits?branch= |
bearer + read |
list | server_commit_list |
| GET | /commits/{commit_id} |
bearer + read |
show | server_commit_show |
Inline read queries (POST /query)
POST /query is the read-only, agent-friendly twin of POST /read. The
request body uses clean field names that match the CLI -e flag and the GQ
query keyword:
{
"query": "query find($n: String) { match { $p: Person { name: $n } } return { $p.name } }",
"name": "find",
"params": { "n": "Alice" },
"branch": "main",
"snapshot": null
}
Response shape is identical to /read (ReadOutput). If the inline source
contains mutations (insert / update / delete), the request is rejected
with HTTP 400 and an error pointing the caller at POST /change — the
read-only contract is enforced at the URL.
POST /change accepts the same clean field names (query, name); the
legacy field names query_source and query_name continue to deserialize as
serde aliases so existing clients keep working without changes. POST /read
is byte-stable and unchanged.
Streaming
Only /export streams (application/x-ndjson, MPSC channel + Body::from_stream). Everything else is buffered JSON.
Error model
Uniform ErrorOutput { error, code?, merge_conflicts[], manifest_conflict? } with code ∈ unauthorized | forbidden | bad_request | not_found | conflict | too_many_requests | internal. Merge conflicts attach structured MergeConflictOutput { table_key, row_id?, kind, message }.
manifest_conflict is set on publisher CAS rejections (HTTP 409): the
caller's pre-write view of one table's manifest version was stale.
ManifestConflictOutput { table_key, expected, actual } tells the client
which table to refresh and retry. This is the conflict shape produced by
concurrent /change or /ingest calls landing the same (table, branch)
race.
HTTP status codes used: 200, 400, 401, 403, 404, 409, 429, 500.
Per-actor admission control
Disjoint
(table, branch) writes from different actors now run concurrently,
guarded only by the engine's per-(table, branch) write queue. To keep
one heavy actor from exhausting shared capacity (Lance I/O, manifest
churn, network), the server gates mutating handlers through a
WorkloadController configured per-process from environment variables:
| Env var | Default | Purpose |
|---|---|---|
OMNIGRAPH_PER_ACTOR_INFLIGHT_MAX |
16 | Concurrent in-flight mutations per actor |
OMNIGRAPH_PER_ACTOR_BYTES_MAX |
4 GiB | In-flight estimated bytes per actor |
When an actor exceeds its in-flight count or byte budget, the server
returns HTTP 429 Too Many Requests with code: too_many_requests
and a Retry-After header (seconds). The actor should back off; other
actors are unaffected.
Cedar policy authorization runs before admission accounting so denied requests don't consume admission slots.
Today admission gates every mutating handler: /change, /ingest,
/branches/{create,delete,merge}, and /schema/apply. Read-only
endpoints (/snapshot, /read, /export, /branches GET, /commits,
/schema GET) are not admission-gated.
Body limits
- Default: 1 MB
/ingest: 32 MB
Auth model (bearer + SHA-256)
- Tokens are SHA-256 hashed on startup; plaintext is never persisted in memory.
- Constant-time comparison via
subtle::ConstantTimeEq. - Three sources, in precedence:
OMNIGRAPH_SERVER_BEARER_TOKENS_AWS_SECRET— AWS Secrets Manager (build with--features aws)OMNIGRAPH_SERVER_BEARER_TOKENS_FILEorOMNIGRAPH_SERVER_BEARER_TOKENS_JSON— JSON{actor_id: token, …}OMNIGRAPH_SERVER_BEARER_TOKEN— single legacy token, actordefault
- If no tokens configured, server runs unauthenticated (local dev) and
/openapi.jsonstrips the security scheme.
See deployment.md for token-source operational details.
Tracing & observability
tower_http::TraceLayer::new_for_http()- Policy decisions logged at INFO level with actor, action, branch, decision, matched rule
- Startup logs: token source name, repo URI, bind address
- Graceful SIGINT shutdown
Not implemented (by design or "TBD")
- CORS — not configured; add
tower_http::corsif needed. - Rate limiting — per-actor admission control gates
/change,/ingest,/branches/{create,delete,merge},/schema/apply(see "Per-actor admission control" above). No global rate limiter is configured; addtower_http::limitif a graph-wide cap is needed. - Pagination — none (commits/branches return everything; export streams).
- Multi-tenant routing — one repo per process.