Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bf64f4e
feat(server)!: assert the expected actor with omnigraph-expect-actor
ragnorc Oct 18, 2026
5588898
feat(server)!: say on every error body whether the request ran
ragnorc Oct 18, 2026
71dc46f
feat(cli)!: exit with what a failed command's outcome says
ragnorc Oct 18, 2026
178f30e
feat(cli): name targets in the operator configuration
ragnorc Oct 18, 2026
2fff68e
fix(server,cli): send an actor assertion only to a server that enforc…
ragnorc Oct 18, 2026
b19f6c2
fix(cli): keep a classified exit when its failure cannot be delivered
ragnorc Oct 18, 2026
b3d1ab3
fix(cli): bind credentials only from valid targets and resolve target…
ragnorc Oct 18, 2026
f7fe98c
Merge branch 'feat/cli-exit-table' into feat/cli-targets
ragnorc Oct 18, 2026
8a4288a
docs(cli,server): describe `--as` as the assertion it now is
ragnorc Oct 18, 2026
5e46f92
fix(server): keep every error's code and outcome exact
ragnorc Oct 19, 2026
e5d4747
Merge branch 'feat/expect-actor' into feat/error-outcome
ragnorc Oct 19, 2026
04a9da5
Merge feat/error-outcome into feat/cli-exit-table; project embedded r…
ragnorc Oct 19, 2026
e2eeff1
fix(cli): refuse what a target cannot address and keep its address li…
ragnorc Oct 19, 2026
6516703
Merge feat/cli-exit-table into feat/cli-targets; root tools ignore a …
ragnorc Oct 19, 2026
23027f5
docs(server): keep the errors paragraph to its original length
ragnorc Oct 19, 2026
d5cda7d
Merge branch 'feat/error-outcome' into feat/cli-exit-table
ragnorc Oct 19, 2026
267c5d1
Merge branch 'feat/cli-exit-table' into feat/cli-targets
ragnorc Oct 19, 2026
76cc941
fix(server,cli)!: fence the actor assertion with HTTP contract 0.14
ragnorc Oct 19, 2026
722a104
Merge feat/expect-actor into feat/error-outcome
ragnorc Oct 19, 2026
1abd5bb
fix(engine,server): let the engine say a load forked before its rows …
ragnorc Oct 19, 2026
9467f16
Merge feat/error-outcome into feat/cli-exit-table
ragnorc Oct 19, 2026
229c6c2
test(cli): pin that an embedded load reports its fork from the engine
ragnorc Oct 19, 2026
7901604
Merge feat/cli-exit-table into feat/cli-targets
ragnorc Oct 19, 2026
42d5998
fix(cli): refuse a root target where the command's flags refuse a root
ragnorc Oct 19, 2026
9666b14
Merge main into feat/expect-actor
ragnorc Oct 19, 2026
cbd4068
Merge feat/expect-actor into feat/error-outcome
ragnorc Oct 19, 2026
a344f23
Merge feat/error-outcome into feat/cli-exit-table
ragnorc Oct 19, 2026
6adf9c7
Merge feat/cli-exit-table into feat/cli-targets
ragnorc Oct 19, 2026
78963e9
docs(api): describe a key conflict without table vocabulary
ragnorc Oct 19, 2026
0a11b0d
Merge feat/error-outcome into feat/cli-exit-table
ragnorc Oct 19, 2026
8ef3388
Merge feat/cli-exit-table into feat/cli-targets
ragnorc Oct 19, 2026
e67386e
fix(cli): digest a Unix socket endpoint by its bytes
ragnorc Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/cli-exit-follows-outcome.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- **CLI exits follow the outcome.** For the data commands the CLI classifies (reads, data writes, aliases and target commands), a failure whose effects are unknown exits 2 instead of 1; one that did not start still exits 1, a busy refusal 75 and a failed condition 4, and a server refusal stating `not_started` with `retry` now also exits 75. An embedded write's own refusal now carries the engine's outcome, as a server's does, so embedded load conflicts and failed conditions exit as served ones (1 and 4) instead of claiming unknown effects. A command line the argument parser refuses exits 64 instead of 2 (an addressing refusal inside a command still exits 1), and a failure whose diagnostic cannot be delivered keeps its exit. Maintenance commands, `cluster`, `policy` and managed commands keep their own exits for now. Read commands, `alias` included, report the same failure envelope as data writes, with `outcome` (always `not_started` and `effects: none`) on stdout in JSON mode and on stderr in human mode. Managed commands keep their own exits for now. Update scripts that read exit 2 as a usage error or exit 1 as every failure. See [failed commands][cli-exit-outcomes].

[cli-exit-outcomes]: ../docs/user/operations/troubleshooting.md#failed-data-write-commands
3 changes: 3 additions & 0 deletions changelog.d/cli-named-targets.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- The operator configuration can name targets: each one a complete binding, a direct graph or cluster root at a local path, or a server with its credential owner. Cloud storage keeps its `--store` and `--cluster` addresses, since a target cannot yet pin the endpoint and account the environment supplies. `--target NAME`, else `defaults.target`, addresses a command, `target list|show|add|remove` lists and edits them one entry at a time while keeping the rest of the file, `login --target` stores a target's token, and `credential get --target` hands it to another program through a pipe. `version --json` reports their output versions under `protocols`. Existing `servers:`, `profiles:` and their flags keep working. See [named targets][cli-named-targets].

[cli-named-targets]: ../docs/user/cli/targets.md
2 changes: 1 addition & 1 deletion changelog.d/cli-version-json.added.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
requires) and the storage-format stamp it writes with the inclusive range it
serves (`internal_schema.writes`, `internal_schema.serves`), without
contacting a server, so a program can choose among installed CLIs. The text
output gains a third line, `http-contract 0.13`. See [Verify][cli-version-json].
output gains a third line, `http-contract 0.14`. See [Verify][cli-version-json].

[cli-version-json]: ../docs/user/install.md#verify
4 changes: 2 additions & 2 deletions changelog.d/edge-version-014.changed.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
- Development builds now report package version `0.14.0`, distinguishing the
current edge binaries from the `0.13.0` release. The HTTP contract remains
`0.13`; the package version bump does not change storage compatibility.
current edge binaries from the `0.13.0` release. The package version bump
does not change storage compatibility.
See [checking the installed version][installed-version].

[installed-version]: ../docs/user/install.md#verify
25 changes: 25 additions & 0 deletions changelog.d/error-outcome.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
- **Every error says whether it ran.** Server error bodies carry an
`outcome` object (`execution`, `effects`, `action`) computed by the server
for the request it answered. A write whose completion the server could not
settle answers `code: outcome_unknown` instead of `internal`. Errors
before a write, refusals a write met before it started and typed refusals
of its one publication are `not_started`; other errors after a write, a
refusal after a load forked its branch included, are `unknown` and keep
their code. Errors that had no `code` now have one: `precondition_failed`,
`recovery_required`, `external_blob_source` and `change_feed_gap`, beside
their details, and a deployment status read that raced an owner change
answers `service_unavailable`. Refusals that answered in plain text now
answer the same body: an unknown route (`404`), a method the route does
not serve (`405`), and a malformed body, query string or path parameter,
where a JSON body that failed to deserialize answered `422` and now
answers `400`. CLI data-write failures print the same `outcome` field in
place of `command_outcome`, taking the server's when its request was the
command's only effectful one. Update clients that read `command_outcome`,
or that told those errors apart by a missing `code` or a `422`. In
`openapi.json`, the optional query parameters of `GET /snapshot`,
`GET /commits` and `POST /load/ndjson` are declared as their plain type
(`string`, `integer` or `LoadMode`) instead of a union with `null`; they stay
optional, so regenerate clients built from the earlier description. See
[failed data-write commands][error-outcome-guide].

[error-outcome-guide]: ../docs/user/operations/troubleshooting.md#failed-data-write-commands
11 changes: 11 additions & 0 deletions changelog.d/expect-actor-header.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
- **`--as` on a served write is an assertion, no longer a refusal.** A request carrying
`Omnigraph-Expect-Actor: <actor>` is refused with `412 actor_mismatch` before
graph lookup or effects when its credential resolves to another actor. `--as`
on a served write or a served `cluster plan` or `cluster apply` now sends it instead of
being refused, and a mismatch exits 4 with nothing run; a script that relied on
that refusal now runs the write when the token's actor matches. The header
arrives with HTTP contract `0.14`, so a server that would ignore it refuses
the request's contract first. See
[Assert the actor][expect-actor-guide].

[expect-actor-guide]: ../docs/user/operations/server.md#assert-the-actor
8 changes: 8 additions & 0 deletions changelog.d/http-contract-014.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- The CLI and server now require exactly `Omnigraph-Http-Api: 0.14`; `0.13`,
missing, repeated and combined values are refused before graph access or body
decoding, and the CLI refuses a server that answers discovery with `0.13`
before sending. Contract `0.14` adds the `Omnigraph-Expect-Actor` request
header. Upgrade the CLI, server, Studio and HTTP integrations together. See
the [HTTP contract][http-contract-014].

[http-contract-014]: ../docs/user/operations/server.md#http-contract
112 changes: 107 additions & 5 deletions crates/omnigraph-api-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,18 @@ use utoipa::{IntoParams, ToSchema};
/// re-exported so a wire consumer needs no second dependency for it.
pub use omnigraph_compiler::settings;

/// The single request/response discriminator for the v0.13 HTTP contract.
/// The single request/response discriminator for the v0.14 HTTP contract.
/// This is independent of the package version and graph-storage stamp.
pub const HTTP_API_CONTRACT_HEADER: &str = "omnigraph-http-api";
/// Exact header value; consumers must reject missing or repeated values.
pub const HTTP_API_CONTRACT: &str = "0.13";
pub const HTTP_API_CONTRACT: &str = "0.14";
/// An optional request header naming the actor the caller expects its
/// credential to resolve to. The server refuses a mismatch with
/// [`ErrorCode::ActorMismatch`] before graph lookup or effects; a matching
/// value changes nothing, so the header can only narrow a request. Contract
/// 0.14 introduced it, so a server that speaks [`HTTP_API_CONTRACT`] enforces
/// it and an older one refuses the request before reading the header.
pub const EXPECT_ACTOR_HEADER: &str = "omnigraph-expect-actor";

/// How a binary identifies its build: the one derivation the CLI's
/// `omnigraph version`, the server's `/healthz` and its MCP server info all
Expand Down Expand Up @@ -1185,6 +1192,9 @@ pub struct ChangeErrorOutput {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<ErrorCode>,
/// Whether the operation ran and what to do next, as on [`ErrorOutput`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<OutcomeOutput>,
/// Set when a requested limit exceeds a public ceiling, or a single change
/// exceeds the poll's own byte ceiling.
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -1800,6 +1810,7 @@ pub struct IngestRequest {

/// Query parameters for `POST /load/ndjson`.
#[derive(Debug, Clone, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct GraphBatchLoadQuery {
/// Target branch. Defaults to `main`. Without `from`, it must exist.
pub branch: Option<String>,
Expand All @@ -1821,6 +1832,7 @@ pub struct ExportRequest {
}

#[derive(Debug, Clone, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct SnapshotQuery {
pub branch: Option<String>,
}
Expand Down Expand Up @@ -1881,6 +1893,7 @@ pub struct AccessOutput {
}

#[derive(Debug, Clone, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct CommitListQuery {
pub branch: Option<String>,
/// `true` lists the unpublished commits whose recorded target is `branch`
Expand Down Expand Up @@ -1959,6 +1972,25 @@ pub enum ErrorCode {
/// 400: the request lacks the exact supported HTTP contract header.
/// Authentication and contract admission precede graph access and effects.
ApiContractMismatch,
/// 412: the request's `omnigraph-expect-actor` header names another actor
/// than its credential resolves to. Nothing ran.
ActorMismatch,
/// A write whose completion the server could not settle: it may or may not
/// have published. Read the target before writing again; never replay the
/// write blindly.
OutcomeUnknown,
/// 412: a mutation's graph-commit precondition no longer holds; see
/// `precondition_failure`.
PreconditionFailed,
/// 503: an overlapping durable recovery intent must be resolved first; see
/// `recovery_required`.
RecoveryRequired,
/// 424: an admitted external Blob source could not be probed or read; see
/// `external_blob_source`.
ExternalBlobSource,
/// 410: retained history can no longer reconstruct a change continuation;
/// see `change_feed_gap`.
ChangeFeedGap,
NotFound,
/// 405 Method Not Allowed — the route exists but the active server
/// mode doesn't serve this method (e.g. `GET /graphs` in single-graph
Expand All @@ -1975,9 +2007,30 @@ pub enum ErrorCode {
TooManyRequests,
/// 503: operation admission is closed; reconcile any earlier write.
ServiceUnavailable,
/// 503: a known graph is unavailable. Does not authorize replay.
/// 503: a known graph is unavailable. The request did not start, so it
/// may be retried once the graph is served again.
GraphUnavailable,
Internal,
/// CLI only: no target of that name is in the operator configuration.
TargetUnknown,
/// CLI only: a target entry, or the configuration that holds it, is not
/// a complete and consistent binding.
TargetInvalid,
/// CLI only: `target add` names a target that already exists.
TargetExists,
/// CLI only: `target remove` names the default target.
TargetIsDefault,
/// CLI only: the operator configuration cannot be edited while keeping
/// the rest of its text.
ConfigNotEditable,
/// CLI only: the command does not serve the selected target's kind.
TargetKindUnsupported,
/// CLI only: `credential get` refuses to write a token to a terminal.
CredentialTerminal,
/// CLI only: the server target uses anonymous access, so it has no token.
CredentialAnonymous,
/// CLI only: no token is stored for the target's credential owner.
CredentialMissing,
}

/// Structured details for a publisher-level OCC failure. Surfaces alongside
Expand All @@ -2003,8 +2056,10 @@ pub struct ReadSetConflictOutput {
}

/// A strict insert rejected because `entity_id` already names an entity in the
/// selected node or edge type. The operation is effect-free when this output is returned;
/// partial or ambiguous attempts surface `recovery_required` instead.
/// selected node or edge type. The refused write changed nothing; an earlier
/// step of the same request, such as the branch a load forks, may already have
/// published, which the error's `outcome` states. Partial or ambiguous
/// attempts surface `recovery_required` instead.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct KeyConflictOutput {
pub entity_kind: EntityKindOutput,
Expand Down Expand Up @@ -2228,11 +2283,57 @@ impl From<&omnigraph_compiler::QueryDiagnostic> for DiagnosticOutput {
}
}

/// Whether a refused or failed operation ran, whether it changed anything,
/// and what its caller should do next. A server states it on every error
/// response, computed where that knowledge is; a client that lost the response
/// knows less, and says so.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
pub struct OutcomeOutput {
pub execution: OutcomeExecution,
pub effects: OutcomeEffects,
pub action: OutcomeAction,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum OutcomeExecution {
/// The operation took no effect: it was refused, or lost to another before
/// anything it staged became reachable.
NotStarted,
/// The operation may have taken effect.
Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum OutcomeEffects {
None,
Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum OutcomeAction {
/// Repeat the same request later, honoring `Retry-After` when present.
Retry,
/// Read the current state, then decide; repeating unchanged will fail the
/// same way.
Refresh,
/// Resolve the durable recovery intent the error names first.
Recover,
/// Find out whether the operation took effect before writing again.
Reconcile,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ErrorOutput {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<ErrorCode>,
/// Whether the operation ran and what to do next. Servers set it on every
/// error response; it is absent only from a body built outside a server.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub outcome: Option<OutcomeOutput>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub merge_conflicts: Vec<MergeConflictOutput>,
/// Set when the conflict is a publisher CAS rejection. The caller's
Expand Down Expand Up @@ -2306,6 +2407,7 @@ impl ErrorOutput {
Self {
error: error.into(),
code: None,
outcome: None,
merge_conflicts: Vec::new(),
published_dataset_version_conflict: None,
read_set_conflict: None,
Expand Down
2 changes: 1 addition & 1 deletion crates/omnigraph-bench/src/gqt_served_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ mod measured_http {

const TOKEN: &str = "served-token-must-not-be-retained";
const CONTRACT_HEADER: &str = "omnigraph-http-api";
const CONTRACT: &str = "0.13";
const CONTRACT: &str = "0.14";
type Events = Arc<Mutex<Vec<String>>>;

#[derive(Clone, Copy)]
Expand Down
Loading
Loading