Repository navigation
Conversation
A request carrying omnigraph-expect-actor is refused with 412 actor_mismatch, after authentication and the contract check and before graph lookup, body work or effects, when its credential resolves to another actor. A match changes nothing, so the header only narrows a request and never supplies an actor. Every protected router and /mcp check it. The CLI sends it for --as on served writes and cluster --server commands, which no longer refuse --as. A mismatch is a not-started refusal and exits 4, like a failed graph-commit precondition.
Every error body carries an outcome object (execution, effects, action) that the server computes for the request it answered: not_started with no effects when it refused before submitting a write, or refused the write's one publication with a typed reason, which an engine gives only before it publishes; unknown otherwise, including a typed refusal after a load forked its branch. The next step a caller takes is retry, refresh, recover or reconcile. A write whose completion is uncertain answers outcome_unknown instead of internal. Errors that carried a reason only in their details gain registry codes: precondition_failed, recovery_required, external_blob_source and change_feed_gap. CLI data-write failures print the same object as outcome, in place of command_outcome, and take the server's when its request was the command's only effectful one.
The exit follows from the failure's outcome: a command that did not start exits 1, one whose effects are unknown 2, one refused for a condition the caller set 4, and one refused before starting with retry 75. A refused command line exits 64 (EX_USAGE) instead of Clap's 2. Read commands report the same failure envelope as data writes, always not_started with no effects, so a failed read prints its outcome too. An embedded engine write's own error now carries the engine's completion evidence, as a server's does: a typed refusal before any request, outside a load that forks its branch, did not start, so embedded load conflicts and failed conditions exit as served ones instead of claiming unknown effects. Any later error, such as one rendering a committed receipt, stays unknown. Embedded writes refuse a noncanonical branch name and read their data file before opening storage. Managed commands keep their own exits until the managed relay lands.
A target is one complete binding in ~/.omnigraph/config.yaml: a direct graph or cluster root, or a server with its credential owner or anonymous access. --target NAME, else defaults.target, addresses a command; it refuses every other address and --profile beside it, and resolves through the scope resolver profiles use. A server target's owner keys the existing token chain and is bound to the target's endpoint, so its token goes only to URLs under it; the file refuses an owner at two endpoints, two owners for one endpoint and an anonymous target beside a keyed one. target list|show|add|remove lists the targets without secrets or network, with a binding digest, and edits one entry at a time: an edit holds config.yaml.lock, changes only that entry's lines, proves by parsing that every other key is unchanged, and replaces the file by rename. login/logout --target store under the target's owner, and credential get --target hands a server target's token to a pipe, never a terminal. Refusals carry registry codes (target_unknown, target_invalid, target_exists, target_is_default, config_not_editable, target_kind_unsupported, credential_terminal, credential_anonymous, credential_missing). version --json reports protocols.target and protocols.credential. Existing servers, profiles and their flags keep working.
…es it A server built before `omnigraph-expect-actor` speaks the same HTTP contract and ignores the header, so `--as` on a served write would run as the credential's actor. Every response now lists the assertions its server enforces in `omnigraph-http-api-assertions`, and the CLI refuses before sending when discovery does not list `expect-actor`. A served `cluster plan` or `cluster apply` refused for another actor now exits 4 as a data write does: its requests are observed, and a refused assertion with nothing effectful sent before it is a condition, with or without `--json`.
`main` returned an undelivered failure as an error, so the process exited 1, the status that says nothing changed, after a write whose receipt met a closed stdout. It now reports the error as before and exits with the code the outcome chose. `alias` is a read, as `query` is, so its failures report an outcome and exit by it. Command classification is now an exhaustive match, so a new command declares how its failure reads.
…s everywhere Credential lookup now reads the same validated bindings as the ownership checks, so a server target that fails validation can no longer win a request's credential and send another owner's token to its endpoint. `queries` and `policy` resolve `--target` and `defaults.target` like every other command: a target is a complete address that excludes `--cluster` and `--profile`, only a cluster-root target names a cluster, and its default graph applies. `login` and `logout` check after parsing that exactly one of a server name, `--api` and `--target` names the credential, so a global `--target` before the subcommand works and a conflict refuses. A configuration refusal keeps its code through the invocation's captured configuration, and a target edit is a local write: a failure after its rename no longer reports that nothing changed.
# Conflicts: # crates/omnigraph-cli/src/command_outcome.rs
The cluster skill page still said `cluster apply --server` refuses `--as`; it now names the assertion `cluster plan` and `cluster apply` send and its exit 4. The release note moves to `breaking`, since a matching `--as` now runs a write the CLI used to refuse. The server guide says `/mcp` has no contract check, an empty assertion joins the malformed-header test, and stale comments name the current behaviour.
RecommendationRequest changes. Do not merge this pull request until the six blocking items below are fixed, and until draft RFC #1017 and RFC 0059 (#675) have accepted the target format. This branch also contains #1074, #1076, and #1078. Those should land first. Why, what, how, impactAn operator needs one name for one place a command runs, with one credential bound to that place. This change adds That is the right primitive: one binding, turned into the address tuple the CLI already uses. The file editor changes one entry, keeps the rest of the text, and refuses when it cannot. Tokens stay in the existing credential file and The new liability is that several paths still change the address after the target is chosen. A cluster-root target can be dropped, remapped through Blocking1.
|
Write facts survive task boundaries. Whether a request had submitted an owned write, and whether its load forked a branch, lived in a task-local, so an MCP tool call, which runs its handler in an owned read task and builds the error body in another, lost both: a load whose `from` forked a new branch and then met a key conflict reported not_started with no effects. ApiError now carries `compound` beside `owned_write`, an owned read keeps the request's record in its own task, and an error leaving it takes that record along; the outcome reads the error's own fields. The outcome unit test gains cases run through an owned read, the MCP write test a load that forks and then conflicts, and the HTTP load test its twin. A load is compound only when it forks. Both load routes marked it whenever `from` was set, so a refused load onto an existing branch reported unknown although its one publication never ran. The routes now mark it from the branch listing their authorization already reads, and only on the owned write's error. An earlier step's publication also outranks a refusal the write met before it started. The HTTP load test covers a key conflict onto an existing branch with `from`, which reports not_started. Every error the server answers has a code. ApiError's code is no longer optional, and the deployment status read that races an owner change answers 503 `service_unavailable` with a retry outcome instead of no code. Malformed input, unknown routes and unserved methods answer the error body. Handlers took axum's Json, Query and Path, whose rejections are plain text (422 for a JSON body of the wrong shape), and unrouted requests had empty bodies. ApiJson, ApiQuery and ApiPath refuse with ApiError (400, or the transport status for 413 and 415), the router's fallbacks answer not_found and method_not_allowed, and a source guard refuses a handler parameter of axum's own extractors. The three query structs whose parameter location came from the extractor's name declare it, so their optional parameters render as the other query parameters do. Embedded callers can render an engine failure as a served request with the same facts would: engine_error_output_after takes whether a write was submitted and whether an earlier step published, and engine_read_error_output renders a read, which never answers outcome_unknown. The RustFS S3 test expected the 424 external Blob source refusal to carry no code; it now asserts its code and outcome. The troubleshooting guide and the changelog note state the outcome decision table the server implements.
…efusals as the server does The merge keeps this branch's command kinds and ModernRelay#1074's served cluster `--as` exit: a cluster command is observed without being classified. An embedded `load --from` marked its write compound whenever `from` was set, so a refusal before its fork exited 2. It now marks it only when the branch is missing, as the server's load does, and an embedded write's own error goes through `engine_error_output_after`: a policy refusal precedes every publication, the fork's included, and any other refusal of a compound load follows the fork. A read's engine error keeps its own code instead of `outcome_unknown`, and the failure reporter no longer panics on a wrapped engine write error. The crossversion journeys read a `--json` read's refusal from stdout, where the failure now goes. The exit documentation says once that a failed condition exits 4 embedded or served, names the data commands the outcome table covers and the commands that keep their own exits, and says that only the argument parser's refusals exit 64.
…teral A cluster-root target on a command that addresses one standalone graph was dropped: `schema apply` lost the root and failed asking for an address. Every scope resolver call now declares whether it reads a root, and one that does not (`schema apply`, `schema upgrade-system-columns`, `upgrade` and the data commands) refuses a resolved root by kind. A target's kind refusal carries `target_kind_unsupported`; a profile's keeps its untyped message. `queries` and `policy` resolve their cluster through the same scope resolver as every other command, so `OMNIGRAPH_PROFILE` beside `defaults.target` is a conflict, and an explicit profile or target that names no cluster root refuses instead of falling through to the default target. A target's default graph still applies there and a profile's still does not. A target's root URI is used as written. Only `--cluster` and profile cluster names are looked up under `clusters:`, so a `clusters:` key spelled like a target's URI no longer redirects it. A server target's endpoint must be a whole server URL, one the request target parser accepts, with no user, password, query or fragment; `https://` alone and an endpoint carrying a password no longer pass as bindings. An incomplete entry is listed with its address redacted. `servers:` urls keep their existing rules. A direct target cannot name `s3://` or `az://` storage and refuses with `target_invalid`: the store's endpoint and account come from the environment when it opens, and this format cannot pin them in the binding. Cloud storage keeps its `--store` and `--cluster` addresses. `login --target` and `logout --target` report the target and its owner, and a target's credential owner counts as declared, so `login` no longer suggests a `servers:` entry that would bind the owner a second time. `target list` no longer lists aliases, which still name `servers:` entries rather than targets. A direct target's binding digest hashes its normalized storage, so `/data/g` and `file:///data/g` are one binding; the default graph stays out of it. The scope flag matrix test compares `--target` as its seventh column. `target add` and `target remove` refuse with `config_not_editable` off Unix, where no lock holds the edit.
…target's default graph The merge keeps this branch's typed-refusal branch in the failure reporter beside ModernRelay#1078's projection of embedded engine errors. `queries` and `policy` keep their explicit `--graph` filter and no longer take a target's `default_graph`, as RFC 0059 specifies for root tools, so their all-graph and ambiguity behaviour is the same whatever selected the root.
|
Addressed at 6516703 (fix e2eeff1, plus the merge of #1078's fixes). Blocking
Optional, all done:
RFC 0059 (#675) has since been updated: the end state is one breaking release with no mixed format and with inline targets. This PR stays additive until that is accepted. |
The stack's other changes to this page need its last line.
RecommendationRequest changes. One blocking item remains. Merge this pull request after that fix, and after draft RFC #1017 and RFC 0059 (#675) accept the target format. Land #1074, #1076, and #1078 first. Earlier findings at f7fe98cAll six blocking items and all four optional items from the review at
Optional items:
Delta since f7fe98c
Why, what, how, impactAn operator needs one name for one place and one credential. This change adds The fix makes every scope resolution say whether the command reads a cluster root. A target URI is used as written. A server endpoint is a URL the request matcher accepts. A direct target refuses cloud storage whose endpoint still comes from the environment. Those six paths now keep the address the target named. Blocking
|
A server that predates `omnigraph-expect-actor` ignores it, so `--as` on a served write would run as the credential's actor. The earlier fence, a response header listing the assertions a server enforces, was a second compatibility record beside the contract version: two servers speaking 0.13 could disagree about the assertion, and every further assertion would extend an enum on every response. The contract moves to 0.14 instead, the one bump the access route, `outcome` and the code registry also ride. A 0.13 server answers discovery with its own contract, so the CLI refuses it before sending, and it refuses a 0.14 request before reading the header. The assertions header, its constant and the CLI's discovery check are gone, and so is the enum on every OpenAPI response. The guides, the developer pages, the skill, the version samples and the fixtures name 0.14, and a breaking note says to upgrade the CLI, server, Studio and HTTP integrations together. The server guide says a repeated, padded or empty header is refused, and that `/mcp` compares it after authentication; the cluster skill names the subcommands that take `--as`.
Brings in HTTP contract 0.14 and the removal of the assertions response header, so this branch speaks the contract its outcome field rides.
…failed A load names whether it forked its branch only from inside the engine: the routes took that fact from the branch listing their authorization reads, before the load lists the branches again and decides. A branch deleted between the two, as one can be while an NDJSON body is still arriving, was forked by the load, and a refusal of its rows then answered `not_started` with no effects although the fork had published. `CompletionEvidence::PartlyPublished` records that an earlier step of the owning operation published and a later one failed. The load marks every failure after its fork with it, the window between the fork and the rows included. It outranks the definite `BeforeEffect` and `Unpublished`, which no longer cover the operation, survives composition, and never hides `Uncertain`. The server reads it as the compound fact and leaves containment where no evidence leaves it, so a typed refusal keeps its code with `unknown` and `refresh`. The routes' list-derived flag and `in_compound_write` are gone, and `engine_error_output_after` drops its `compound` argument: embedded callers get the fact from the error. A route test deletes the branch while the NDJSON body waits and expects the fork reported; the core test pins the composition rules. The key conflict and `graph_unavailable` descriptions say what they now mean, and the breaking note names the plain query-parameter types in openapi.json.
Brings in HTTP contract 0.14 and the engine's mark of an earlier publication. The embedded load no longer lists the branches to predict a fork: the engine marks every failure after its fork with `PartlyPublished`, so `embedded_write_output` reads that mark, and `Evidence.compound` and `command_outcome::compound` are gone. A branch deleted after a list and before the load's own check can no longer report `not_started` after the fork published.
The outcome unit test passed even if `load` predicted a fork for every `--from`, because a policy refusal ignores the prediction. A process test now drives the call site. Onto a branch that exists, `--from` forks nothing, and a refused payload exits 1 with `not_started`. Onto a missing branch the fork publishes first, and the same refusal exits 2 with `unknown` while the branch stays. The troubleshooting guide's "exit alike" sentence is narrowed to those cases: a policy refusal is `not_started` on both planes, and a refusal after a fork that did publish is `unknown` on both.
Brings in HTTP contract 0.14 and the engine's fork mark for embedded loads. The command-outcome helpers keep `local_effect` and drop the fork prediction, and the skill's `--as` line keeps `--target` and names the served cluster commands that assert the actor.
`schema plan` refuses `--cluster` and `--graph`, yet the shared maintenance resolver declared that every caller reads a cluster root, so a cluster-root target was opened as a cluster and then needed the `--graph` the flag guard refuses. `resolve_maintenance_uri` now takes the caller's own `reads_root`: `schema plan` passes false and refuses a root target with `target_kind_unsupported` as `schema apply` does, while optimize, repair, cleanup, the full-text rebuild and lint keep reading roots. The guide lists `schema plan` with the graph-only commands. A server target's binding digest hashes its endpoint as requests are matched to it (`RequestTarget::canonical`: the host as the URL parser folds it, the default port filled in, no trailing slash), so `https://h`, `https://h/`, `https://h:443` and `https://H` are one binding, before a client stores a protocol 1 digest.
|
Addressed at 42d5998, after 7901604 (the merge of the updated #1078). Blocking:
Optional: server endpoint digest. Validation: |
The access route from ModernRelay#1073 now rides HTTP contract 0.14 with the actor header: its policy guide example sends 0.14, and its OpenAPI operation lists the contract value and the optional omnigraph-expect-actor parameter like every other protected route.
Brings in main through ModernRelay#1074: the access route from ModernRelay#1073 under HTTP contract 0.14.
Brings in main through ModernRelay#1076: the access route from ModernRelay#1073 under HTTP contract 0.14.
Brings in main through ModernRelay#1078: the access route from ModernRelay#1073 under HTTP contract 0.14.
Brings in main through ModernRelay#1080: the access route as ModernRelay#1073 landed, whose handler and output type this branch already carries in their evaluated form (the conflict keeps this side), HTTP contract 0.14, and `schema plan` refusing a root target.
The key-conflict description said the refused rows wrote nothing; the OpenAPI vocabulary guard keeps graph vocabulary in the published description, so it now says the refused write changed nothing.
Brings in the key-conflict description in graph vocabulary.
Brings in the key-conflict description in graph vocabulary.
A server target's binding digest spelled a Unix socket path with its lossy display, so two socket paths that differ only in bytes no display can print, `unix:///tmp/%FE.sock` and `unix:///tmp/%FF.sock`, digested alike although they connect to different sockets. The canonical endpoint now spells the path's bytes in hex, and the digest test pins that two such paths are two bindings.
RecommendationComment. The blocking item and the optional item from the review at Earlier findings at 267c5d1Blocking:
|
|
Closing in favour of #1090, which implements the accepted breaking design of RFC 0059: one |
Stacked on #1078 (CLI exit table), itself on #1076 and #1074; review only the last commit.
What & why
The operator configuration can name targets, the addressing RFC 0059 proposes, as an additive first step: each target is one complete binding, and a command selects one with
--target NAME, elsedefaults.target.--targetis a complete address: it refuses--server,--store,--cluster,--profileand a positional URI beside it, and resolves through the same scope resolver profiles use, so capability checks are unchanged.defaults.targetapplies when nothing is named, and refuses besidedefaults.server/store/default_graphor an ambientOMNIGRAPH_PROFILE. Commands that do not resolve a scope (init,cluster, most local commands) refuse--targetrather than ignore it.credentialowner keys the existing token chain (OMNIGRAPH_TOKEN_<OWNER>, then the credentials file) and is bound to the target's endpoint, so its token goes only to URLs under it, by the same sent-URL matchingservers:use. The file refuses an owner at two endpoints, two owners for one endpoint, and an anonymous target beside a keyed one.login/logout --targetstore under the owner.target list|show|add|remove. Listing needs no network and prints no secret, with abindingdigest.addreads one entry as JSON on stdin;addandremoveholdconfig.yaml.lock, change only that entry's lines, prove by parsing that every other key is unchanged, and replace the file by rename, refusing withconfig_not_editablewhen they cannot.credential get --targethands a server target's token to a pipe, never a terminal, for clients such as Studio.version --jsonreportsprotocols.targetandprotocols.credential.Existing
servers:,profiles:,clusters:and their flags keep working; retiring them is a later step.Backing issue / RFC
Checklist
targetstests (bindings, file-wide checks, owner matching, resolution, text edits, digests); CLIcli_data::targets_address_commands_and_carry_only_their_owners_token,cli_data::target_commands_edit_one_entry_and_keep_the_rest_of_the_file,cli_data::login_and_credential_get_follow_the_target_credential_owner; the version contract; skill driftdocs/user/cli/targets.md, CLI reference rows, user index, skill command index and condensed reference;changelog.d/cli-named-targets.added.mdLocal verification
cargo build -p omnigraph-server && RUST_MIN_STACK=16777216 cargo test -p omnigraph-cli -- --test-threads=6— 242 lib + 335 integration passRUST_MIN_STACK=16777216 cargo test -p omnigraph-server— all pass (OpenAPI regenerated for the new codes)cargo clippy -p omnigraph-cli -p omnigraph-server -p omnigraph-api-types --all-targets --locked -- -D warnings -W clippy::dbg_macro,cargo fmt --all --check,python3 scripts/check-docs.py,typos— cleanNotes for reviewers
The serialized format is the open question; it follows #675's sketch (
kind,scope,uri,endpoint,credential,default_graph) plusauth: anonymousfor explicit anonymous access. Managed targets are not included yet. Thetargetcommand group is the one exception to RFC 0059's "no target administration group" that RFC #1017 argues for.