Serving surface
The in-process trait is the semantic contract. ox runtime serve is the first-party helper around it for tools that need a process boundary, specified by RFD-0014. It exposes a versioned HTTP surface:
| Route | Meaning |
|---|---|
GET /v1/health | service status, storage mode + reachability, active module hash, artifact schema hash |
GET /healthz | liveness — process is up (does not probe storage) |
GET /readyz | readiness — probes storage; 503 when the backend is unreachable |
GET /v1/module | active module status, source path, diagnostics, artifact schema hash |
GET /v1/schema | generated-schema-equivalent TBox metadata, plus the artifact schema hash a generated SDK checks for version skew |
POST /v1/dispatch/query | execute a declared pub query descriptor |
POST /v1/dispatch/mutation | execute a declared pub mutate descriptor |
POST /v1/dispatch/compute | execute a supported pure pub fn descriptor |
POST /v1/batch | execute an ordered array of mutation dispatches atomically |
POST /v1/forks/{fork}/dispatch/* | fork-scoped descriptor dispatch |
GET /v1/forks, POST /v1/forks | list and create forks |
GET /v1/forks/{fork} | inspect one fork record |
DELETE /v1/forks/{fork} | abort a fork |
GET /v1/forks/{fork}/diff/{other} | compare visible ABox events |
POST /v1/forks/{fork}/promote | promote fork-local events into main |
POST /v1/forks/{fork}/derive | run buffered derive projection |
POST /v1/forks/{fork}/derive/trace | return buffered derive trace events |
GET /v1/snapshot | materialize TBox, ABox (paginated), and optional derivations |
GET /v1/checks | enumerate currently-violated checks in a scope |
GET /v1/derived/individuals/{id} | derived facts mentioning an individual |
GET /v1/derived/individuals/{id}/explain | explanations for that individual’s derived facts |
GET /v1/derived/facts/{fact_id}/explain | explanation for one derived fact |
Dispatch routes take a runtime context from trusted headers (X-Tenant-Id, X-Fork-Id, X-Principal-Id, X-Request-Id, X-Standpoint) or the request body; headers win when both are present. Missing tenant defaults are a CLI/helper concern; missing fork defaults to main.
Storage modes. mem keeps Store state in process memory — useful for local experiments, tests, and short-lived sandboxes; durability ends with the process. It is the default runtime backend. pg is an append-only axiom event log in Postgres (oxc-storage-pg): scoped tenant/fork/standpoint/module scans, AsOf::Now/AtVt/AtTt/full-bitemporal reads, durable fork records and lineage, generation counters bumped atomically with appends, projection-cache invalidation in the same transaction, and decoded body-field lookups for iof assertions and relation tuples. ox runtime serve --storage pg selects it. The source of truth in either mode is the event log; snapshots, derived facts, and projection caches are recomputable views over .oxbin plus visible events.
Ad-hoc query/mutation submission (RFD 0033). Beyond declared-descriptor dispatch, the API accepts arbitrary query/mutation bodies as source text — POST /v1/query/adhoc and POST /v1/mutation/adhoc (plus the per-fork analogs). A submitted body is parsed, fully type-checked against the loaded module (the same checker the build runs, via the runtime Schema backend — an ill-typed body is refused with ARGON_RUNTIME_ADHOC_TYPE_ERROR and never executes), lowered, and run through the same path declared pub query/pub mutate use; the declared forms are a named convenience layer over this generic path. Ad-hoc invocation carries the same context/tenant/fork/standpoint scoping as declared dispatch — there is no ad-hoc-specific restriction on what it may read or write, with one capability exception: forget (physical erasure) requires the build-time #[allow_forget] grant, which a runtime-submitted body cannot self-confer, so an ad-hoc forget is refused (OE0730). Ad-hoc submission is on by default; a deployment may lock down to declared invocables only (--no-adhoc / --no-adhoc-mutation), in which case the endpoint returns ARGON_RUNTIME_ADHOC_DISABLED.
Generic entity writes remain declaration-typed. There is no untyped entity-write surface (no POST /v1/entities): every write — declared or ad-hoc — routes through the typed pub mutate / Operation mechanism, never a raw entity blob. Compute dispatch supports the pure CoreIR subset evaluable without Store effects (literals, parameters, tuples, lists, boolean/int/text operators, membership, conditionals, ascription); calls, projections, and Store-dependent terms are refused until their runtime substrate is specified.
Entity-reference arguments — new vs existing. A pub mutate parameter typed as a declared concept/struct is an entity reference. Over HTTP such an argument takes a JSON string in one of two forms, and the form the client chooses decides the meaning:
- An
#i<N>string (e.g."#i7") names an existing individual — one already minted in this scope (typically returned by an earlier dispatch). It is validated against the store’s next-fresh floor: a reference at or above the floor cannot name an existing individual and is refused (ARGON_RUNTIME_VALIDATION_FAILED). - Any other string (e.g.
"alice") is a client-supplied symbolic name for a NEW individual. The server mints a fresh#i<N>identity for it inside the per-scope critical section, runs the mutation with that identity substituted, and returns the mapping on the receipt undermintedEntities: an array of{ "name", "id", "concept" }records. The client uses the returnedidto refer to that individual on subsequent dispatches.
This is what makes the flagship models writable over HTTP: a mutation such as openRentObligation(landlord: Person, tenant: Person, …) whose body classifies its parameters via insert iof(landlord, Person) is dispatched by supplying symbolic names ("alice", "bob"), and a later recordPayment(satisfactionAccount: SatisfactionAccount, payment: SatisfactionRecord, …) supplies the minted #i<N> for the existing account and a fresh symbolic name for the new payment. The in-process / demo.toml path mints symbolic names deterministically from the name (so the same name is the same individual); the HTTP path mints fresh per request with no idempotency-by-name — the same name in two requests is two individuals.
Atomic batches. POST /v1/batch executes an ordered array of mutation dispatches as one unit: { "context": …, "mutations": [ { "qualifiedPath", "args" }, … ] }. Every step runs in one per-scope critical section against a single hydrated store, so step N sees the effects of steps 0..N and the check delta-guard evaluates each step against the accumulating state. On success the union of emitted events is persisted in one transaction; the response carries committed: true, a per-step steps array (each with its result / mintedEntities / diagnostics), and eventsCommitted. Any step failure rolls the whole batch back — nothing is persisted — and the error envelope reports the failing step under error.details.batchStep. The request body cap and the result-row cap apply to the batch as a whole. New-entity symbolic-name minting is per step: a symbolic name repeated across two steps mints two individuals (each step is a distinct mutation). To thread one new individual through several steps, mint it in the first step and reference the returned #i<N> in the later ones.
Named individuals in outputs. Individuals declared by pub fact carry their source name into the artifact’s name-index, and every entity rendered by a query / extent / snapshot / dispatch result includes that name under $name alongside the opaque $id (so a served scope reads with its modeler-chosen names). HTTP-minted individuals (above) carry their client label out on the dispatch receipt’s mintedEntities; the durable identity is always the #i<N> id.
Runtime error codes. Failures on the /v1 surface use a stable error.code string in the ARGON_RUNTIME_* family (distinct from the compile-time OE#### catalog of Appendix C). Every 4xx/5xx response uses the same structured envelope — {error:{code,message,details},requestId,moduleHash} — including routing, method, and request-body parse failures (a malformed JSON body is ARGON_RUNTIME_VALIDATION_FAILED, an unknown route is ARGON_RUNTIME_UNKNOWN_ROUTE, a wrong method is ARGON_RUNTIME_METHOD_NOT_ALLOWED), so a client keys on error.code uniformly for every failure class. The full set of codes, their HTTP statuses, and their meanings is the published failure reference in Appendix E, checked against the runtime’s canonical registry. A batch-step failure rides the same envelope with the failing step index under error.details.batchStep.
Pagination. A list-shaped read (pub query, /v1/snapshot) larger than the result-row cap is read page by page rather than refused. A query takes an optional page: { limit, cursor } in its request body; /v1/snapshot takes ?limit=&cursor=. The response carries a page object — { total, returned, nextCursor } (the snapshot’s under abox.page) — and a nextCursor of null marks the last page. The cursor is an opaque server-issued token: pass back the previous page’s nextCursor verbatim, do not parse or construct it (a foreign cursor is refused with ARGON_RUNTIME_INVALID_CURSOR). Page size is min(limit, cap), so one page never exceeds the cap; an un-paged read still refuses (ARGON_RUNTIME_RESULT_TOO_LARGE) when the whole result is over the cap — a truncated answer is indistinguishable from a complete one.
The cursor pins a read point, so a multi-page walk is consistent against one fixed view rather than re-evaluated against live state each page. Page 1 reads at the request’s asOf (or Now); the server resolves Now to the transaction time of the store that served the page and embeds it in the nextCursor. Every later page inherits that pinned point from the cursor (it overrides the request’s asOf), so the offset is always taken against the same bitemporal snapshot. A concurrent insert lands past the pinned point and stays invisible to the walk; a concurrent retraction before the cursor stays visible (it existed at the pinned point) — so the walk neither skips nor duplicates a row, and total is stable across pages. To page over a historical point, pass asOf (the bitemporal read point of AsOf semantics — bitemporal point) on the first request; the whole walk then follows it.
/v1/snapshot carries the same pin in its cursor. On the durable pg backend the pinned page re-hydrates the ABox at the pinned transaction time, so the snapshot walk is consistent under concurrent writes. The in-process mem backend keeps only current-state events (it has no historical scan), so a mem snapshot walk is a live positional read; production serving is pg-backed.
Checks surface. GET /v1/checks enumerates the checks currently violated in a scope, evaluated against live state — the compliance/violations view, complementing the per-mutation observe channel (which only fires on a write). The response carries violated (any blocking Error firing), violationCount, diagnosticCount, and a diagnostics array of {code,message,severity,check,args} (the same shape a mutation response’s observe-channel diagnostics use). ?check=<name> filters to one check (an unknown name is ARGON_RUNTIME_UNKNOWN_CHECK). The generated SDK exposes this as client.checks(), and the mutation receipt now carries the observe-channel diagnostics (previously dropped for typed-SDK consumers).
Liveness and readiness. GET /healthz is liveness — 200 while the process is up and serving; it deliberately does not probe storage, so a transient storage outage does not trigger a process restart. GET /readyz is readiness — it probes the storage backend (SELECT 1 on pg; always ready on mem) and returns 503 ARGON_RUNTIME_STORAGE_UNREACHABLE when storage is down, so a load balancer stops routing to a dead-storage instance. GET /v1/health folds both into one human/debug body and is honest about storage: ok:false + 503 when the backend is unreachable. See the deployment runbook for how an orchestrator should wire these.
Hot reload. The serving layer may watch the active .oxbin path. On a changed artifact it loads the new bytes into a fresh Module, validates, computes the new module hash and declaration signatures, probes live ABox state, accepts additive schema changes, and rejects declaration removal or changed signatures when live ABox state exists — keeping the previous Module active and recording diagnostics on failure. Failed reloads never discard ABox state.
Capability and provenance limits. The serving layer enforces fork on nested forks. forget mutations are refused at the serving layer; unsafe_logic queries are refused (lowering emits unsafe_logic = false for supported rules). The derive/explain endpoints expose buffered rule/tuple provenance only; full PosBool DNF witness trees are reserved for the formal provenance substrate and must not be implied by the response shape.