Skip to content

feat(server)!: assert the expected actor with Omnigraph-If-Actor - #1074

Merged
ragnorc merged 13 commits into
ModernRelay:mainfrom
ragnorc:feat/expect-actor
Oct 20, 2026
Merged

ragnorc merged 13 commits into
ModernRelay:mainfrom
ragnorc:feat/expect-actor

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What & why

A request carrying omnigraph-if-actor is refused with 412 actor_mismatch when its credential resolves to another actor (or to none), after authentication and the contract check and before graph lookup, body work or effects. A match changes nothing, so the header only narrows a request and never supplies an actor. A duplicate, empty or padded value is a 400. Every protected router and /mcp check it.

The CLI sends it for --as on a server target, for data commands and for cluster plan and cluster apply, which no longer refuse --as; a mismatch exits 4, like a failed graph-commit precondition. A managed target still refuses --as.

Backing issue / RFC

Checklist

  • Change is focused (one logical change)
  • Tests: auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor, cli_data::as_on_a_served_write_asserts_the_actor, OpenAPI variants
  • Public docs: server guide ("Assert the actor"), CLI reference and index, Blob, cluster and policy guides, the skill; changelog.d/if-actor-header.breaking.md
  • Reviewed against docs/dev/invariants.md: identity is still resolved only at the boundary (invariant 10)

Local verification

  • RUST_MIN_STACK=16777216 cargo test -p omnigraph-server and cargo test -p omnigraph-cli (with the server binary built) — all pass
  • cargo clippy for both crates with -D warnings, cargo fmt --all --check, python3 scripts/check-docs.py, typos — clean

Notes for reviewers

Breaking only for scripts that relied on served commands refusing --as. openapi.json describes the optional header and its 412 on every route.

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.

ragnorc commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Why

A served request must act as the actor its credential resolves to. --as on that path must only check that actor. The header must not set an actor.

What

This pull request adds the request header omnigraph-expect-actor. The server compares it with the resolved actor after authentication and the contract check, and before graph lookup. A mismatch is 412 actor_mismatch. A match leaves the request unchanged. The CLI sends the header for --as on a served write, and for cluster plan and cluster apply with --server.

How

require_expected_actor is on the graph, registry, cluster, catalogue, and /mcp routers. GraphHttpClient::send adds the header when the client was built with --as. A served data write classifies actor_mismatch as exit 4.

Impact

The bearer credential stays the only actor. The header can only narrow the request. That matches invariant 10 and RFC 1017 rule 5, on a server that implements the check.

A server that still speaks contract 0.13 and does not know the header accepts the request and ignores the header. The CLI on main (0a62bb7b) refuses --as on a served write before it sends the request. After this pull request, a token that matches --as runs the write on that old server.

Blocking

  1. Keep the CLI refusal, or move HTTP_API_CONTRACT, before this behavior is releasable. See the comment on EXPECT_ACTOR_HEADER.
  2. skills/omnigraph/references/cluster.md still says cluster apply --server refuses --as. The code sends the header. See the comment on SKILL.md.
  3. The changelog says a cluster --server mismatch exits 4. cluster plan and cluster apply do not. See the comment on the changelog.

Optional

  • The note is changelog.d/expect-actor-header.added.md. A matching --as on a served write now runs a write that the CLI used to refuse. The release-note guide puts that upgrade action in a .breaking note.
  • crates/omnigraph-cli/src/planes.rs still says a served write is "rejected downstream" for --as. This pull request removed that refusal.
  • The next test in cli_data.rs still names as_on_a_served_write_is_rejected. That test is now as_on_a_served_write_asserts_the_actor.
  • The server guide says the comparison follows the contract check, and that /mcp is included. /mcp does not run require_contract. The same guide already says MCP does not use omnigraph-http-api. The actor check on /mcp does run after authentication.

Tradeoff and liability

One middleware and one header is the right shape. The header does not add a second identity, a queue, or a shadow actor store. Five more checks of this kind belong in the same middleware, on one contract version.

The pull request splits RFC 1017 phase 1 so each review stays small. That choice weakens the upgrade fence. The RFC puts this header, the access route, outcome, and the code registry on one contract version so an old server refuses a new client. This client change removes the local refusal and leaves the version at 0.13. The local refusal was the fence. The version must replace it before a release.

RFC 1017 at 7c72c7e

These parts match rule 5 (Identity):

  • The server resolves the actor from the credential.
  • --as is an assertion. operator.actor is not sent to a server.
  • The comparison is after authentication and the contract check, and before graph lookup, for the graph, registry, cluster, and catalogue routers.
  • A mismatch is 412 actor_mismatch and the manifest version does not move.
  • A duplicate or padded value is 400 bad_request. An empty value is also 400 (the new test does not cover empty).
  • A matching value does not change the actor. actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers still passes.
  • A server with no authentication resolves no actor and returns 412.

These parts do not match:

  • Compatibility: one contract bump carries the header, and clients and servers upgrade together. This head keeps HTTP_API_CONTRACT at "0.13".
  • "--as is never ignored." It is ignored by a 0.13 server that predates this middleware. It is also still ignored on served reads that the plane accepts and that call GraphClient::resolve (snapshot, export, schema show, branch list). Pull request 1059 owns that read gap by refusing --as where the command records no actor. This pull request should not grow a second fix for that gap.
  • The exit table maps an --as assertion to exit 4. Served data writes do that. cluster plan and cluster apply with --server do not.
  • skills/omnigraph/references/cluster.md still documents the refusal this rule removes.

outcome on every error, the access route, and the general exit table are pull requests 1076, 1073, and 1078. This head correctly leaves them out.

Sibling pull requests

  • 1076 is stacked on this head. It adds outcome. It does not fix the changelog sentence (checked at 558889830d2f).
  • 1078 is stacked on 1076 and owns the general exit table. This pull request must not document exit 4 for cluster until the code does it.
  • 1073 adds GET /graphs/{id}/access. It will conflict in openapi.json, handlers.rs, lib.rs, auth_policy.rs, and the server and policy guides. It also leaves the contract at 0.13. Its route is a new path. This header changes an existing safety check. The version fence belongs with this header.
  • 1075 and 1077 build local servers through build_app. They inherit this middleware when they rebase. They also edit lib.rs, auth_policy.rs, and the server guide.
  • 1059 refuses --as where no actor is recorded. It touches planes.rs. Rebase will conflict on the cluster --server test this pull request deletes. After both land, served writes assert and the read gap refuses.
  • 1080 and 1081 add named targets and omnigraph access. They should keep sending this header on served requests. They do not change this contract.
  • 1012 and draft 675 (RFC 0059) do not overlap this header. RFC 1017 appendix A5 leaves RFC 0059 as a separate decision.

Validation

Head bf64f4e28cf5d58941c35053dd9a7297d9616bb7, base 0a62bb7b2aa5b99b012f413807c216277f7f9596.

Local, after a clean compile of this head:

  • cargo test -p omnigraph-server --test integration --locked auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor -- --exact — pass
  • cargo test -p omnigraph-server --test integration --locked auth_policy::actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers -- --exact — pass
  • cargo test -p omnigraph-server --test integration --locked auth_policy::authenticated_change_stamps_actor_on_commits -- --exact — pass
  • cargo test -p omnigraph-server --test integration --locked openapi::error_code_schema_has_expected_variants -- --exact — pass
  • cargo test -p omnigraph-server --test integration --locked openapi::openapi_contains_all_expected_paths -- --exact — pass
  • cargo test -p omnigraph-cli --test integration --locked cli_data::as_on_a_served_write_asserts_the_actor -- --exact — pass (mutate, commit publish, commit drop; header act-nope; exit 4)
  • cargo test -p omnigraph-cli --lib --locked planes::tests::live_cluster_scope_conflicts_refuse_before_discovery -- --exact — pass

Cluster exit, on this head, then the edit was removed: command_outcome::applies is false for cluster plan --server --as. finish of a 412 actor_mismatch RemoteErrorCli with no evidence returns Err for a human report and exit 1 for --json, with no command_outcome field. main maps that Err to process exit 1.

GitHub Actions on this head are green, including Test Workspace, Lint (clippy), Format (rustfmt), and the GQT jobs. That is CI evidence, not a substitute for the cluster exit check above.

Regression proof

This is a feature. The contract is an HTTP header and a CLI exit. A .gqt case cannot express either one. The owners in the testing guide are omnigraph-server integration/auth_policy.rs and omnigraph-cli integration/cli_data.rs. This pull request extends both. No temporary .gqt case was added.

Limitations

I did not run the full auth_policy or cli_data modules. I did not execute /mcp or /cluster/* with the header. I did not boot a 0a62bb7b server against this CLI. The ignore follows from the base tree: it has no require_expected_actor, and both trees set HTTP_API_CONTRACT to 0.13. This change does not use Lance, so I did not re-fetch Lance format pages.

Inline comment — crates/omnigraph-api-types/src/lib.rs:39

HTTP_API_CONTRACT on the line above is still "0.13". RFC 1017 says one contract version carries this header, the access route, outcome, and the code registry, so clients and servers upgrade together.

A server at 0a62bb7b accepts Omnigraph-Http-Api: 0.13 and has no reader for this header. This CLI sends both. That server runs the request. --as is ignored. The CLI on main refuses --as on a served write before any request.

This pull request must not treat a 2xx from contract 0.13 as proof that the server checked the actor. Bump HTTP_API_CONTRACT in the release that contains this CLI change, or keep the local refusal until that bump is in the same release. Pull requests 1073 and 1076 can share that one new value. An old server then returns 400 api_contract_mismatch and cannot ignore the header.

Inline comment — changelog.d/expect-actor-header.added.md:4

cluster plan and cluster apply with --server do send this header. A mismatch does not exit 4.

Those commands are outside command_outcome::applies, so finish does not classify the error. A human report returns Err, and the process exits 1. --json writes the error body and exits 1, with no command_outcome. I passed a 412 actor_mismatch RemoteErrorCli through finish with no evidence and got that result. Served data writes do exit 4 (cli_data::as_on_a_served_write_asserts_the_actor).

Change this sentence so it names only served data writes, or classify this refusal as exit 4 on the cluster path. Pull request 1078 owns the general exit table. This note is already false on this head, and it is unchanged on 1076 (558889830d2f).

Inline comment — skills/omnigraph/SKILL.md:169

skills/omnigraph/references/cluster.md lines 114–117 still say cluster apply --server refuses --as, because the server takes the actor from the bearer token. This pull request removes that refusal and sends omnigraph-expect-actor.

The skill reference must match this paragraph. An agent that follows cluster.md will expect a local refusal. A token that matches --as now runs the deployment.

Recommendation: request changes.

…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`.
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.
@cursor

cursor Bot commented Oct 18, 2026

Copy link
Copy Markdown

Recommendation

Request changes. Two blocking findings remain. The cluster skill still documents the refusal this pull request removed. HEAD /healthz advertisement does not replace the HTTP contract version bump in draft RFC #1017.

Reviewed head: 2fff68e8d99958d5b951b5180732bca72f0d3e8a. Base: 0a62bb7b2aa5b99b012f413807c216277f7f9596. Earlier review: #1074 (comment) at bf64f4e2. No later review comment covers this head.

Why

A served write must run as the actor its credential resolves to. --as on that path must only check that actor. The header must not choose an actor.

What this head changes

2fff68e8 answers that earlier review. The CLI reads omnigraph-http-api-assertions on HEAD /healthz. When that header omits expect-actor, the CLI returns before the data request. Cluster plan and apply with --server and --as exit 4 on 412 actor_mismatch.

Earlier findings

  1. Partly resolved. An old server that omits the assertions header no longer receives the write. Proof is in Validation. HTTP_API_CONTRACT is still "0.13". The write response is not required to list expect-actor. The remainder is blocking finding 1.
  2. Still open. skills/omnigraph/references/cluster.md still says cluster apply --server refuses --as. Blocking finding 2. This file is outside the diff, so the finding is only in this text.
  3. Resolved. Cluster plan and apply with --server exit 4 on 412 actor_mismatch. cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor passed. The changelog sentence matches the code. I am not restating this as a new defect.

Optional items from that review:

  • A, still open. The note is still changelog.d/expect-actor-header.added.md. Optional finding 1.
  • B, still open. crates/omnigraph-cli/src/planes.rs still says a served --as is "rejected downstream". Optional finding 2. That comment is outside this diff.
  • C, still open. cli_data.rs still names as_on_a_served_write_is_rejected. Optional finding 3. That comment is outside this diff.
  • D, still open. The server guide now says every response lists expect-actor. That sentence is false for /mcp. Optional finding 4.

Blocking

1. Health advertisement is not the contract fence

HTTP_API_CONTRACT stays "0.13". GraphHttpClient::send treats a successful HEAD /healthz that lists expect-actor as proof. It then sends the data request with Omnigraph-Http-Api: 0.13. The data response is checked only for that contract header. It is not checked for omnigraph-http-api-assertions.

The comment on send says the probe is discovery, not a fence against a changed backend. A check of the write response cannot undo the write.

I ran mutate --as act-nope against a server that advertises expect-actor only on /healthz and answers POST /graphs/knowledge/mutate with 200 as actor act-real. The CLI at this head exited 0 and printed actor_id act-real. The POST carried omnigraph-expect-actor: act-nope and omnigraph-http-api: 0.13.

Draft RFC #1017 at 7c72c7e7 says one contract bump carries the actor header, the access route, outcome, and the code registry. Clients and servers upgrade together. A client reads each deployment's version from /healthz and speaks that version. This pull request adds a second compatibility header. The RFC text does not define it. Two binaries that both speak 0.13 can disagree about the assertion. That is the rolling case the version bump is for.

An honest server that omits the header is closed. The head CLI does not send the write. See Validation. The supported single-process topology is closed for that server. The open case is a backend that lists the assertion on discovery and ignores the header on the write.

identify_response adds the assertions header on every response except /mcp and the OAuth resource-metadata paths. /mcp runs require_expected_actor and does not run require_contract. Advertisement and enforcement already diverge inside one binary.

Please bump HTTP_API_CONTRACT in the release that ships this CLI change, together with #1073 and #1076, or keep the local refusal until that bump. Do not use the assertions header as a substitute for the version.

If the header remains as advertisement, do not freeze a one-value enum on every OpenAPI response. describe_contract sets that enum to only expect-actor. The type comment says the value is comma-separated. The next assertion would rewrite every response schema. That schema is most of this diff's line count.

2. The embedded cluster skill still says the server refuses --as

skills/omnigraph/references/cluster.md still says:

cluster apply --server refuses it, because the server takes the actor from the bearer token

This pull request removed that refusal. A matching token now runs the deployment. skills/omnigraph/SKILL.md was updated. This reference was not. The CLI embeds skills/omnigraph.

Please say that --as sends omnigraph-expect-actor, and that a mismatch exits 4 with nothing run.

Optional

  1. Rename changelog.d/expect-actor-header.added.md to a .breaking.md note. docs/dev/documentation.md uses breaking for an upgrade action: who is affected and what to do. The note already says to upgrade the server or drop --as. The pull request title already has !.
  2. In crates/omnigraph-cli/src/planes.rs, the ScopeFlag::As comment still says a served write is "rejected downstream" for --as. That refusal is gone. The header is sent. Please update the comment.
  3. In crates/omnigraph-cli/tests/integration/cli_data.rs, the comment on commit_publish_and_drop_take_as_on_a_direct_graph_issue_1023 still names as_on_a_served_write_is_rejected. The test is as_on_a_served_write_asserts_the_actor.
  4. docs/user/operations/server.md says every response lists expect-actor, and that the /mcp comparison follows the contract check. identify_response returns before those headers on /mcp and on resource-metadata paths. /mcp does enforce the request header. A client that reads only an /mcp response will not see the advertisement, and will not send the header. Please name the responses that list the header. Please say that /mcp compares the actor after authentication and before ingress.
  5. ApiContractError::assertion_unsupported reports code api_contract_mismatch and http_status 200 when discovery succeeded and the contract header matched. The versions match. The server uses that code for a missing or wrong contract header, with status 400. This refusal happens before any data request. Please use a distinct code, or a local refusal that does not claim a contract mismatch. Do not report the discovery status 200 as the error's HTTP status. feat(server)!: say on every error body whether the request ran #1076 and feat(cli)!: exit with what a failed command's outcome says #1078 own the general code registry. This slice should not overload the existing code ahead of that registry.

Where the code and draft RFC #1017 disagree

RFC head: 7c72c7e7ed2b42fb0e5b3f5140d8235444ddcf33. The RFC is still a draft. This pull request says it merges only after that RFC is accepted.

  • The RFC requires one contract version bump for the actor header, the access route, outcome, and the code registry. The code stays at 0.13.
  • The RFC does not define omnigraph-http-api-assertions.
  • The RFC says the server compares the header after authentication and the contract-header check. /mcp skips the contract check. Comparison on the other routers does follow that order.
  • The RFC says --as is never ignored. The new CLI refusal matches that rule when discovery omits the token. The split in blocking finding 1 still runs the write as the token's actor.
  • The RFC gives exit 4 to a failed caller condition on every command. This pull request covers data writes and cluster commands. feat(cli)!: exit with what a failed command's outcome says #1078 owns the general exit table. I am not asking this pull request to invent that table.
  • A matching header still changes nothing, and the header still cannot supply an actor. That part matches the RFC and invariant 10.

Siblings

Tradeoff and liability

One middleware and one request header is the right enforcement shape. The header only narrows. A match continues with the token's actor. A mismatch returns 412 before graph lookup. The client does not supply a trusted actor.

The new response header is a second source of truth beside the contract version. The RFC already chose one version bump so an old server refuses the data request. The header lets this slice land before the access route and the error outcome. Contract 0.13 then names two servers.

The local refusal removes the fail-open path against a server that omits the header. It adds a discovery probe on every data request and a misleading error code. It does not close a backend that advertises the assertion and then ignores it.

Five similar assertion headers would each extend the same OpenAPI enum on every response. Prefer one contract version that old servers already reject.

Validation

GQT cannot express an HTTP header or a CLI exit code. I did not add a .gqt case. The owner is the existing CLI integration test. The pull request already has the after-case for a fixture that omits the header (cli_data::as_on_a_served_write_asserts_the_actor).

Regression of the author's fix. Same command and same harness. The harness answers contract 0.13, does not enforce an actor, and returns one valid mutate body. It does not advertise expect-actor.

Command:

omnigraph mutate -e 'query add($name: String) { insert Person { name: $name } }' --params '{"name":"X"}' --server http://127.0.0.1:<port> --graph knowledge --as act-nope --json

  • Before, CLI built from bf64f4e28cf5d58941c35053dd9a7297d9616bb7. Exit 0. Requests: HEAD /healthz, then POST /graphs/knowledge/mutate with omnigraph-expect-actor: act-nope and omnigraph-http-api: 0.13. Printed actor_id act-real.
  • After, CLI built from 2fff68e8d99958d5b951b5180732bca72f0d3e8a. Exit 1. Only HEAD /healthz. JSON code api_contract_mismatch, request_dispatched false, http_status 200.

Split, same head CLI. /healthz advertises expect-actor. The mutate route returns 200 as act-real and does not enforce the header. Exit 0. The POST was sent with omnigraph-expect-actor: act-nope. Printed actor_id act-real.

Local tests at this head, each with --exact. All passed:

  • cargo test --locked -p omnigraph-server --test integration auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor
  • cargo test --locked -p omnigraph-server --test integration auth_policy::actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers
  • cargo test --locked -p omnigraph-server --test integration auth_policy::authenticated_change_stamps_actor_on_commits
  • cargo test --locked -p omnigraph-server --test integration openapi::openapi_spec_is_up_to_date
  • cargo test --locked -p omnigraph-server --test integration openapi::error_code_schema_has_expected_variants
  • cargo test --locked -p omnigraph-server --test integration openapi::openapi_contains_all_expected_paths
  • cargo test --locked -p omnigraph-cli --test integration cli_data::as_on_a_served_write_asserts_the_actor
  • cargo test --locked -p omnigraph-cli --test integration cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor
  • cargo test --locked -p omnigraph-cli --lib planes::tests::live_cluster_scope_conflicts_refuse_before_discovery

GitHub Actions run 39173261495 on this head is green, including Test Workspace, Lint, Format, and the GQT jobs. That run is separate from these local tests.

Limitations

I did not boot a real server at 0a62bb7b. The harness stands in for a 0.13 peer that ignores the new header. I did not run the full auth_policy or cli_data modules. I did not send a live /mcp request. This change does not touch Lance storage. I did not re-read Lance format pages.

Comment thread crates/omnigraph-server/src/http_contract.rs Outdated
Comment thread crates/omnigraph-cli/src/graph_http.rs Outdated
Comment thread crates/omnigraph-cli/src/graph_http.rs Outdated
Comment thread docs/user/operations/server.md Outdated
Comment thread changelog.d/expect-actor-header.breaking.md Outdated
Comment thread crates/omnigraph-api-types/src/lib.rs Outdated
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 19, 2026
…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.
@ragnorc

ragnorc commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 8a4288a (fixes 2fff68e and 8a4288a).

Blocking

  1. Upgrade fence. Each response now lists the assertions its server enforces in Omnigraph-Http-Api-Assertions (expect-actor). GraphHttpClient::send refuses before dispatch when discovery doesn't list it, with api_contract_mismatch and request_dispatched: false. That fences 0.13 servers built before this change without moving the contract. Tests: cli_data::as_on_a_served_write_asserts_the_actor adds a predecessor-server case that sees only the HEAD, and auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor asserts the header on HEAD /healthz.
  2. Skill. references/cluster.md now describes the assertion that cluster plan and cluster apply send, and its exit 4.
  3. Served cluster plan/apply exit 4. Cluster commands are observed, not classified. command_outcome::actor_assertion_refused (a 412 actor_mismatch with nothing effectful sent before it) now decides exit 4 for them and for data writes. Test: cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor, with and without --json; it asserts exactly one GET and no POST.

Optional: all four done. The note is now .breaking, and the planes.rs comment, the test reference and the /mcp wording are corrected. An empty assertion joined the malformed-header rows.

@cursor

cursor Bot commented Oct 19, 2026

Copy link
Copy Markdown

Recommendation

Request changes. One blocking finding is still open.

Reviewed head: 8a4288adbaf3bbed087e311db91fb08b6babdb19. Base: 0a62bb7b2aa5b99b012f413807c216277f7f9596. Earlier review: #1074 (comment) at 2fff68e8. First review: #1074 (comment) at bf64f4e2. The reply https://github.com/ModernRelay/omnigraph/issues/comments/6257097000 does not move the head.

Why

A served request must run as the actor its credential resolves to. --as on that path must only check that actor. The header must not choose an actor.

What this head changes

8a4288ad is documentation and one test row. It renames the release note to .breaking. It updates the cluster skill, two comments, and the server guide's contract-check clause. The actor test now also sends an empty header. GraphHttpClient::send and HTTP_API_CONTRACT are unchanged.

How

require_expected_actor runs after authentication. On the graph, registry, cluster, and catalogue routers it also runs after require_contract, and before graph lookup. A match continues with the token's actor. A mismatch is 412 actor_mismatch. The CLI sends omnigraph-expect-actor only after HEAD /healthz lists expect-actor.

Impact

A matching token now runs a write that the CLI on main refused. A server that omits the assertions header does not receive that write. A server that lists expect-actor on /healthz and ignores the header on the write still runs the write as the token's actor. That is the remaining hole.

Earlier findings

From the review at 2fff68e8:

  1. Blocking, still open. HTTP_API_CONTRACT is still "0.13". GraphHttpClient::send has no diff in 2fff68e8..8a4288ad. Blocking finding 1. The new reply treats the discovery header as the upgrade fence. That is the fence the earlier review already measured.
  2. Resolved. skills/omnigraph/references/cluster.md no longer says cluster apply --server refuses --as. It says cluster plan and cluster apply with --server send the assertion, and a mismatch exits 4. That matches cluster_remote::dispatch and cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor. The last sentence of that paragraph is optional finding 3.
  3. Resolved at 2fff68e8, and still resolved. Cluster plan and apply exit 4 on 412 actor_mismatch. The test passed again at this head. The changelog sentence matches that behavior.

Optional items from that review:

  • A, resolved. The note is changelog.d/expect-actor-header.breaking.md. It says a script that relied on the local refusal now runs the write when the token's actor matches.
  • B, resolved. The ScopeFlag::As comment in planes.rs now says a served write sends --as as omnigraph-expect-actor. That matches GraphClient::resolve_with_policy_scope.
  • C, resolved. The comment in cli_data.rs now names as_on_a_served_write_asserts_the_actor.
  • D, partly resolved. The server guide now says /mcp has no contract check. That matches mcp::router: bearer auth, then require_expected_actor, then ingress, and no require_contract. The same paragraph still says every response lists expect-actor. Optional finding 1.
  • E, still open. ApiContractError::assertion_unsupported is unchanged. Optional finding 2. The reply's "four optional items" does not include this one.

The first review's three blocking items are the same three. Item 1 is still open. Item 2 is resolved. Item 3 was resolved at 2fff68e8.

Blocking

1. The health advertisement is not the contract fence

HTTP_API_CONTRACT stays "0.13". GraphHttpClient::send treats a successful HEAD /healthz that lists expect-actor as proof. It then sends the data request with Omnigraph-Http-Api: 0.13. has_contract checks the data response for that contract header only. It does not check omnigraph-http-api-assertions.

The comment on send says the probe is discovery, not a fence against a changed backend. A check of the write response cannot undo the write.

cli_data::as_on_a_served_write_asserts_the_actor covers a server that omits the assertions header. That case exits 1 and sends only HEAD /healthz. It does not cover a server that lists expect-actor on discovery and ignores the header on the write. graph_http.rs is not in this commit, so the harness from the review at 2fff68e8 still applies: mutate --as act-nope exited 0 and printed actor_id act-real. The POST carried omnigraph-expect-actor: act-nope and omnigraph-http-api: 0.13.

Draft RFC #1017 at 7c72c7e7 says one contract bump carries the actor header, the access route, outcome, and the code registry. Clients and servers upgrade together. A client reads each deployment's version from /healthz and speaks that version. This pull request adds omnigraph-http-api-assertions. The RFC text does not define it. Two binaries that both speak 0.13 can disagree about the assertion.

Bump HTTP_API_CONTRACT in the release that ships this CLI change, together with #1073 and #1076. Or keep the local refusal until that bump is in the same release. Do not use the assertions header as a substitute for the version. The existing inline comment on GraphHttpClient::send still applies.

Optional

  1. docs/user/operations/server.md and the breaking note still say every response lists expect-actor. identify_response returns before those headers when the path is /mcp or an OAuth resource-metadata path (is_resource_metadata_path). /mcp does enforce the request header. Name the responses that carry the token. Say that /mcp compares the actor after authentication and before ingress. The new parenthetical covers only the missing contract check. The existing inline comment on the server guide still applies. The breaking note repeats the same sentence.

  2. ApiContractError::assertion_unsupported still reports code api_contract_mismatch and the discovery status 200. Discovery succeeded and the contract header matched. The server uses that code for a missing or wrong contract header, with status 400. This refusal happens before any data request. Use a distinct code, or a local refusal that does not claim a contract mismatch. Do not publish the discovery 200 as http_status. The predecessor case in as_on_a_served_write_asserts_the_actor now pins code == api_contract_mismatch and exit 1. feat(server)!: say on every error body whether the request ran #1076 and feat(cli)!: exit with what a failed command's outcome says #1078 own the general code registry. This slice must not lock the overloaded code ahead of that registry.

  3. In skills/omnigraph/references/cluster.md, the --as paragraph ends with "The other cluster subcommands reject this flag." cluster upgrade-ledger accepts --as and labels the direct actor (ScopeFlag::As for UpgradeLedger). Direct cluster plan also accepts --as. status, validate, observe, and force-unlock reject it, and so do the --managed forms. Name those commands. The condensed line in skills/omnigraph/SKILL.md still lists cluster apply with direct writes. It does not say that cluster plan and cluster apply with --server send the assertion.

  4. The server guide says a repeated or padded header is 400 bad_request. An empty value is also 400 (require_expected_actor rejects is_empty). The new loop in expected_actor_header_narrows_a_request_and_never_supplies_an_actor covers "". Add empty to that sentence.

  5. If the assertions header remains, do not freeze a one-value enum on every OpenAPI response. describe_contract sets that enum to only expect-actor. The comment on HTTP_API_ASSERTIONS_HEADER says the value is comma-separated. The next assertion would rewrite every response schema. The existing inline comment on describe_contract still applies.

Where the code and draft RFC #1017 disagree

RFC head: 7c72c7e7ed2b42fb0e5b3f5140d8235444ddcf33. The RFC is still a draft. This pull request says it merges only after that RFC is accepted. The docs commit does not add a disagreement. It records the /mcp exception the code already has.

  • The RFC requires one contract version bump for the actor header, the access route, outcome, and the code registry. The code stays at 0.13.
  • The RFC does not define omnigraph-http-api-assertions.
  • The RFC says the server compares the header after authentication and the contract-header check. /mcp skips the contract check. The other routers follow that order.
  • The RFC says --as is never ignored. The CLI refusal matches that rule when discovery omits the token. Blocking finding 1 still runs the write as the token's actor when discovery lists the token and the data route ignores the header.
  • The RFC gives exit 4 to a failed caller condition on every command. This pull request covers data writes and cluster plan and apply. feat(cli)!: exit with what a failed command's outcome says #1078 owns the general exit table.
  • A matching header still changes nothing, and the header still cannot supply an actor. That part matches the RFC and invariant 10.

Siblings

Tradeoff and liability

One middleware and one request header is the right enforcement shape. The header only narrows. A match continues with the token's actor. A mismatch returns 412 before graph lookup. The client does not supply a trusted actor. That matches invariant 10. The change adds no second identity, no queue, and no shadow actor store.

The response header is a second compatibility record beside the contract version. The RFC already chose one version bump so an old server refuses the data request. The header lets this slice land before the access route and outcome. Contract 0.13 then names two servers. Five more assertions of this kind would each extend the same OpenAPI enum on every response. One contract version that old servers already reject is the smaller long-term surface.

The docs commit removes the stale refusal text. An operator who follows the cluster skill no longer expects a local refusal. That text change does not close a backend that advertises the assertion and then ignores it.

Validation

GQT cannot express an HTTP header or a CLI exit code. This delta does not change query rows, counts, column types, or query errors. I did not add a .gqt case. The empty-header row covers a refusal require_expected_actor already had at 2fff68e8 (!actor.is_empty()). It is not a new bug fix, so there is no before/after GQT pair.

Local tests at this head, each with --exact. All passed:

  • cargo test --locked -p omnigraph-server --test integration auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor (includes the empty header; 1 passed)
  • cargo test --locked -p omnigraph-server --test integration auth_policy::actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers (1 passed)
  • cargo test --locked -p omnigraph-server --test integration auth_policy::authenticated_change_stamps_actor_on_commits (1 passed)
  • cargo test --locked -p omnigraph-server --test integration openapi::openapi_spec_is_up_to_date (1 passed)
  • cargo test --locked -p omnigraph-server --test integration openapi::error_code_schema_has_expected_variants (1 passed)
  • cargo test --locked -p omnigraph-server --test integration openapi::openapi_contains_all_expected_paths (1 passed)
  • cargo test --locked -p omnigraph-cli --test integration cli_data::as_on_a_served_write_asserts_the_actor (1 passed)
  • cargo test --locked -p omnigraph-cli --test integration cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor (1 passed)
  • cargo test --locked -p omnigraph-cli --lib planes::tests::live_cluster_scope_conflicts_refuse_before_discovery (1 passed)

GitHub Actions run 39179059242 on this head: Lint, Format, and the GQT jobs passed. Test Workspace failed. The only failed test is engine_v2_plan_replay::in_flight_wildcard_keeps_its_captured_schema_while_an_owner_adds_an_edge_type_issue_659 (assertion failed: pause.is_paused()). That file is not in this diff. Nextest then stopped, at 3054 of 4716 tests, before the server and CLI tests. CI Gate failed because Test Workspace failed. That run does not prove the header tests. The local runs above do.

I did not repeat the lying-server harness. crates/omnigraph-cli/src/graph_http.rs has no diff in 2fff68e8..8a4288ad.

Limitations

I did not boot a real server at 0a62bb7b. I did not run the full auth_policy or cli_data modules. I did not send a live /mcp request. This change does not touch Lance storage. I did not re-read Lance format pages.

Inline comments that could not be attached

  • skills/omnigraph/references/cluster.md:120 — Optional. This sentence says the other cluster subcommands reject --as. cluster upgrade-ledger accepts --as and labels the direct actor. Direct cluster plan also accepts it. status, validate, observe, and force-unlock reject it, and so do the --managed forms. Name those commands. The condensed line in skills/omnigraph/SKILL.md still lists cluster apply with direct writes. It does not say that cluster plan and cluster apply with --server send this assertion.
  • changelog.d/expect-actor-header.breaking.md:7 — Optional. "Every response lists expect-actor" is too broad. identify_response returns before these headers on /mcp and on OAuth resource-metadata paths. /mcp does enforce omnigraph-expect-actor. The server guide has the same sentence. Name the responses that list the token.
  • docs/user/operations/server.md:118 — Optional. An empty value is also 400 bad_request. require_expected_actor rejects is_empty(), and expected_actor_header_narrows_a_request_and_never_supplies_an_actor now sends "". Add empty beside repeated and padded.

Comment thread changelog.d/expect-actor-header.breaking.md Outdated
Comment thread skills/omnigraph/references/cluster.md Outdated
Comment thread docs/user/operations/server.md Outdated
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`.
@ragnorc

ragnorc commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 76cc941. #1073 now merges this branch (be18e46), so the access route ships under the same bump, and its policy example sends 0.14.

Blocking

  1. The contract fence. HTTP_API_CONTRACT is now 0.14, and the assertions header is gone: omnigraph-http-api-assertions, EXPECT_ACTOR_ASSERTION, the CLI's discovery check and ApiContractError::assertion_unsupported are removed, along with the per-response enum in openapi.json. A server that would ignore omnigraph-expect-actor speaks 0.13, so the CLI refuses it at discovery and the server refuses a 0.13 request before reading the header. as_on_a_served_write_asserts_the_actor now answers discovery with 0.13 and expects exit 1, api_contract_mismatch, request_dispatched: false and only the discovery request. A server that advertises 0.14 and ignores the header breaks that contract; the CLI does not defend against it, as with any other 0.14 rule. This is the one bump the draft RFC describes. feat(server): report what a credential may do through an access route #1073 and feat(server)!: say on every error body whether the request ran #1076 ride it and must ship in the same release. RFC rfc: graph access and cross-process writes #1017 now says so (HTTP API section and decision log), and it no longer needs a capability header.

Optional

  1. Moot: no response lists an assertion. The server guide now says /mcp compares the actor after authentication and before admission, since it has no contract check.
  2. Moot: assertion_unsupported is removed. The refusal is the existing discovery mismatch.
  3. skills/omnigraph/references/cluster.md now names who takes --as: cluster plan, cluster apply and cluster upgrade-ledger. Every other subcommand and every --managed form rejects it. The condensed line in SKILL.md lists cluster plan --server and cluster apply --server with served writes as assertions.
  4. The guide says a repeated, padded or empty header is 400 bad_request.
  5. Moot, as in 1.

The bump also updates every Omnigraph-Http-Api: 0.13 in the user guides and the skill. It updates the version samples, the skill's declared contract (which check-docs holds to the constant), the bench and contract-test fixtures, and a new http-contract-014.breaking.md note. Studio and other HTTP clients must move to 0.14 together with the server.

Validation: cargo test -p omnigraph-server (all targets), cargo test -p omnigraph-cli -- --test-threads=6 (lib and integration), the bench served tests (gqt_tests::served), the OpenAPI drift test, check-docs, typos and clippy on the touched crates. The earlier Test Workspace failure was engine_v2_plan_replay::in_flight_wildcard_keeps_its_captured_schema_while_an_owner_adds_an_edge_type_issue_659, an engine timing test outside this diff. The rerun passed.

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.
@ragnorc
ragnorc enabled auto-merge October 19, 2026 22:23
@cursor

cursor Bot commented Oct 19, 2026

Copy link
Copy Markdown

Recommendation

Approve. There is no blocking finding.

Reviewed head: 7474e61add68d097bfca3cbab56f3aa1d223fc7b. The delta is bf954ade111b2bb8dfe6c758d96848f9fa4a75a6..7474e61a. Current main is 9e2425d26a03b1b5628030a359ee51f096d3ca1c. The previous review is #1074 (comment) at bf954ade. No later review covers this head.

Why

A served request must run as the actor its credential resolves to. --as on that path must only check that actor. The header must not choose an actor.

Omnigraph-If-Actor is a request precondition. A failed precondition answers 412. That matches Omnigraph-If-Graph-Commit and invariant 10.

What this delta changes

Two commits landed after bf954ade.

1b77b698 changes one sentence in docs/user/deployment.md. Servers use the v0.14 HTTP contract.

7474e61a merges main (9e2425d2). That merge brings the integer key-set endpoint check (#1085) and the six smaller GQT cases (#1087). Those commits do not edit the server router, HTTP_API_CONTRACT, openapi.json, CLI discovery, error bodies, or changelog.d.

How the merge resolved

The merge parents are 1b77b698 and 9e2425d2. Their merge base is 1998e945. No file changes on both sides. git merge-tree --write-tree of the two parents produces tree 815035cacb75ffd8e7f44d93b769087db6d8ee14. That tree matches 7474e61a. The merge kept both sides.

Earlier optional findings

  1. Resolved. docs/user/deployment.md now says the v0.14 HTTP contract. HTTP_API_CONTRACT is "0.14".
  2. Resolved. The title is feat(server)!: assert the expected actor with Omnigraph-If-Actor. The "What & why" section says omnigraph-if-actor. The checklist still names changelog.d/if-actor-header.added.md. The file on this head is changelog.d/if-actor-header.breaking.md. See optional finding 2.
  3. Still open. Draft RFC rfc: graph access and cross-process writes #1017 at e15d2f5b85a759b053dda73082a5c9e7fff8e121 still names omnigraph-expect-actor. Optional finding 1.

The whole pull request, after the merge

IF_ACTOR_HEADER is omnigraph-if-actor. No file on this head contains omnigraph-expect-actor, Omnigraph-Expect-Actor, or EXPECT_ACTOR_HEADER. openapi.json has 72 omnigraph-if-actor names and no 0.13.

require_expected_actor runs after authentication. On the graph, registry, cluster, and catalogue routers it also runs after require_contract, and before graph lookup. On /mcp it runs after authentication and before admission. A match continues with the token's actor. A mismatch is 412 actor_mismatch. A repeated, padded, or empty value is 400 bad_request. An absent header leaves the request unchanged.

GraphHttpClient::send refuses discovery that does not carry exactly 0.14, before it sends the data request. It then sets IF_ACTOR_HEADER when the client carries --as.

Blocking

None.

Optional

  1. Draft RFC rfc: graph access and cross-process writes #1017 at e15d2f5b still says omnigraph-expect-actor. This head sends omnigraph-if-actor. Please update that draft before it is accepted. The old name is in the rule index (line 57), the Identity section (line 550), the HTTP contract table (line 1225), and the decision log (lines 1252, 1355, 1492, 1552, 1590, 1617, and 1639). Draft RFC rfc: standardize HTTP API conventions #1089 at 2b90ba4c already names Omnigraph-If-Actor and limits status 412 to an If-* precondition.

  2. The pull request checklist names changelog.d/if-actor-header.added.md. The note on this head is changelog.d/if-actor-header.breaking.md. Please point the checklist at that file.

Where the code and the drafts disagree

RFC #1017 head: e15d2f5b85a759b053dda73082a5c9e7fff8e121. The RFC is still a draft. This pull request says it merges only after that RFC is accepted.

The code and that RFC disagree on the header name. The RFC says omnigraph-expect-actor. IF_ACTOR_HEADER is omnigraph-if-actor. The user guide says Omnigraph-If-Actor. Header names ignore case. The lowercase spelling matches omnigraph-if-graph-commit and omnigraph-http-api.

These parts of RFC #1017 match the code:

  • The server resolves the actor from the credential.
  • The header only narrows a request. It does not supply an actor.
  • The comparison follows authentication and the contract check, and precedes graph lookup, on the graph, registry, cluster, and catalogue routers.
  • On /mcp the comparison follows authentication and precedes admission.
  • A mismatch is 412 actor_mismatch. A duplicate, padded, or empty value is 400.
  • HTTP_API_CONTRACT is 0.14.

Draft RFC #1089 at 2b90ba4c93ab10f93c4407351f425505dc8f7e2b agrees with this head. A request precondition is Omnigraph-If-* and answers 412. The 0.14 batch includes Omnigraph-If-Actor. Only an Omnigraph-If-* header may narrow a request. No header supplies identity. That matches invariant 10.

OpenAPI writes every header name in lowercase, including this one. RFC #1089's Title-Case check is a later lint over the whole spec. This head keeps one spelling with the other headers.

Siblings

Tradeoff and liability

One constant and one header are the right shape. The header only narrows the request. A match continues with the token's actor. A mismatch returns 412 before graph lookup. The client does not supply a trusted actor. The change adds no second identity, no queue, and no shadow actor store.

This delta does not add a primitive. The deployment sentence now matches HTTP_API_CONTRACT. The merge takes the load work from main beside the actor check. Five more preconditions belong as Omnigraph-If-* headers on this same middleware, on a later contract bump.

The remaining cost is the draft name. An accepted RFC #1017 that still says omnigraph-expect-actor would document a header this server does not read. An alias for an unreleased name would add a second header to test and later remove. This tree has one name. Please update the draft.

Validation

Local tests ran at 7474e61add68d097bfca3cbab56f3aa1d223fc7b in a detached worktree. CARGO_TARGET_DIR=/workspace/target. Each command used --locked, --offline, and --exact. All passed.

  • cargo test -p omnigraph-server --test integration -- auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor auth_policy::actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers openapi::openapi_spec_is_up_to_date openapi::error_code_schema_has_expected_variants openapi::openapi_contains_all_expected_paths. Result: 5 passed, 0 failed, 0 ignored, 266 filtered out, 0.10s.
  • cargo test -p omnigraph-cli --test integration -- cli_data::as_on_a_served_write_asserts_the_actor cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor. Result: 2 passed, 0 failed, 0 ignored, 341 filtered out, 0.19s.
  • cargo test -p omnigraph-cli --lib -- planes::tests::live_cluster_scope_conflicts_refuse_before_discovery. Result: 1 passed, 0 failed, 0 ignored, 242 filtered out, 0.01s.

The actor test covers a match (200), a mismatch (412, manifest version unchanged), a repeated, padded, or empty value (400), and a server with no authentication (412). actor_id_resolves_from_bearer_token_ignoring_client_supplied_headers sends no omnigraph-if-actor header. The bearer still selects the actor. That is the absent-header path.

GitHub Actions run 39331978861 on this head is green. That includes Test Workspace, Lint, Format, CI Gate, and the GQT jobs. That run is separate from the local tests.

Regression proof

This pull request is a feature. The delta does not fix a bug. The regression-proof rule does not apply.

The contract is an HTTP header and a CLI exit code. A .gqt case cannot express either one. I added no temporary .gqt case. The testing guide assigns this contract to omnigraph-server integration/auth_policy.rs and integration/openapi.rs, and to omnigraph-cli integration/cli_data.rs and integration/cli_cluster.rs. Those tests passed at this head.

Limitations

I did not run the full auth_policy or cli_data modules. I did not send a live /mcp request. mcp.rs calls require_expected_actor, which reads IF_ACTOR_HEADER. I did not boot a server from main. This delta does not read or write Lance storage. I did not re-read Lance format pages.

@cursor

cursor Bot commented Oct 20, 2026

Copy link
Copy Markdown

Recommendation

Request changes. One blocking finding is open.

Reviewed head: 0e54ed2ea2b6d616675d89d099d348d15740cac3. The delta is 7474e61add68d097bfca3cbab56f3aa1d223fc7b..0e54ed2e. Current main is b8e82abfbaa02a0e14b65e3bb034d1a81f281864. The previous review is #1074 (comment) at 7474e61a. No later review covers this head.

Why

A served request must run as the actor its credential resolves to. --as on that path must only check that actor. The header must not choose an actor.

Omnigraph-If-Actor is a request precondition. A failed precondition answers 412. That matches Omnigraph-If-Graph-Commit and invariant 10.

What this delta changes

One commit landed after 7474e61a.

0e54ed2e merges main (b8e82abf, "feat(branch): start a branch at a graph commit (#1016)"). The first parent is 7474e61a. The second parent is b8e82abf. The merge base is 9e2425d2.

That merge brings a branch start at a published graph commit, and a cleanup fix for an interrupted local sweep. POST /branches gains from_commit. The CLI gains --from-commit. The MCP branch tool gains from_commit. The collector treats an absent index directory as already deleted.

How the merge resolved

git merge-tree --write-tree of the two parents produces tree 0d4179d075a0080db9b529374f5556ec5cc2bf43. That tree is the tree of 0e54ed2e. Git reports no conflict. It auto-merges twelve files, including openapi.json, the branch handler, the server error map, the CLI client, and the policy guide.

The auto-merge keeps both sides in the Rust files. IF_ACTOR_HEADER stays omnigraph-if-actor. HTTP_API_CONTRACT stays "0.14". from_commit and branch_start_refusal from main are present. docs/user/cli/reference.md keeps the --as assertion sentence and adds create --from-commit.

The auto-merge of openapi.json is wrong. See the blocking finding.

Actor check on the routes this merge adds

#1016 does not add a router. from_commit is a field of POST /branches. That route stays on the per-graph router. The layers run in this order: bearer authentication, require_contract, require_expected_actor, then graph lookup. A mismatch is 412 actor_mismatch before server_branch_create reads the body. A match leaves AuthenticatedActor unchanged. The handler passes that actor into branch_create_from_as. The header is not an engine argument.

/mcp branches_create calls the same handler. The MCP router runs require_expected_actor after authentication and before admission. /mcp still has no contract check. The server guide already says that.

CLI branch create uses resolve_with_policy. A remote client calls with_expected_actor. branch_create sends from or from_commit through GraphHttpClient::send. Discovery must answer exactly 0.14 before the data request. The data request then carries omnigraph-if-actor when --as is set.

The cleanup change is inside the engine collector. It adds no HTTP route. It does not read an actor.

A policy denial of a commit start is still 404 after the actor check. A retained-history refusal is 410 after the actor check. Those outcomes are for a request that already passed the assertion.

Earlier optional findings

  1. Still open. Draft RFC rfc: graph access and cross-process writes #1017 at e15d2f5b85a759b053dda73082a5c9e7fff8e121 still names omnigraph-expect-actor. Optional finding 1.
  2. Still open. The checklist still names changelog.d/if-actor-header.added.md. The file is changelog.d/if-actor-header.breaking.md. Optional finding 2.

Blocking

1. Two createBranch response enums still say contract 0.13

openapi.json for POST /graphs/{graph_id}/branches says the omnigraph-http-api response enum is 0.13 on status 404 and status 409. Every other response on that operation, including the new 410, says 0.14. HTTP_API_CONTRACT is "0.14". describe_contract writes that constant onto every response header when it builds the spec.

I generated the spec in an isolated checkout of this head with OMNIGRAPH_UPDATE_OPENAPI=1 and compared it with the committed file. The only difference is those two enums, 0.13 to 0.14. I restored the file. openapi::openapi_spec_is_up_to_date fails on this head for that difference. openapi::openapi_describes_api_contract_admission_and_response_identity passes, because it reads the generated spec.

The running server still writes 0.14. identify_response uses the constant. A client that trusts the checked-in spec reads the wrong contract on those two statuses. Please regenerate openapi.json with the documented update command.

Optional

  1. Draft RFC rfc: graph access and cross-process writes #1017 at e15d2f5b still says omnigraph-expect-actor. This head sends omnigraph-if-actor. Please update that draft before it is accepted. The old name is in the rule index (line 57), the Identity section (line 550), the HTTP contract table (line 1225), and the decision log (lines 1252, 1355, 1492, 1552, 1590, 1617, and 1639).

    These open siblings still compile the old constant EXPECT_ACTOR_HEADER (omnigraph-expect-actor) on contract 0.14: feat(server)!: say on every error body whether the request ran #1076 at 78963e94, feat(cli)!: exit with what a failed command's outcome says #1078 at 0a11b0da, feat(cli): say who you are at a target and what you may do #1081 at 3a3f8375, feat(cli): serve a target for you on loopback #1083 at 7175120b, and feat(cli): bridge an agent's MCP over stdio to a target #1084 at 24be23f4. They need this name when they update. feat(server): serve one standalone graph to the same account on loopback #1075, feat(server): serve a cluster to the same account on loopback #1077, feat(cli)!: replace legacy routing with explicit targets #1090, fix(server): delete a branch named like a static branch route #1091, and fix(server): scope an unknown storage outcome to its key and branch #1096 are still on contract 0.13 and do not define the header. fix(server): delete a branch named like a static branch route #1091 moves the branch routes and leaves them on the protected router. Draft RFC rfc: standardize HTTP API conventions #1089 at 2b90ba4c already names Omnigraph-If-Actor and limits status 412 to an If-* precondition. Draft RFC rfc: storage outcomes per operation and restartable ownership #1093 at acc6535c does not name this header.

  2. The pull request checklist names changelog.d/if-actor-header.added.md. The note on this head is changelog.d/if-actor-header.breaking.md. Please point the checklist at that file.

Tradeoff and liability

One middleware and one header remain the right shape. The header does not add a second identity. The merge does not add a queue, a shadow actor store, or a Lance actor. Lance branch metadata is a per-dataset ref (_refs/branches/{name}.json and a shallow clone under tree/). It has no actor. This engine does not call Lance create_branch for a graph branch. It clones the __manifest version that published the commit, then enforces Cedar with the actor the HTTP layer already resolved.

The new liability is a checked-in spec that can drift from the one constant when a merge inserts response objects from a tree that still speaks 0.13. The generator already has one source of truth. The freshness test is the gate. This head fails that gate. Later merges of the same kind stay safe when that test is green before the merge commit.

No new fix is in this merge, so no new regression case is required. The existing owner openapi::openapi_spec_is_up_to_date already fails here for the predicted reason. It passes after the file matches the generator. GQT cannot express an OpenAPI enum.

Validation

Isolated checkout of 0e54ed2e.

  • git merge-tree --write-tree 7474e61a b8e82abf produced tree 0d4179d0, equal to 0e54ed2e^{tree}, with no conflicts.
  • Server integration binary from cargo test -p omnigraph-server --test integration --locked --no-run, then:
    • openapi::openapi_spec_is_up_to_date — FAILED
    • openapi::openapi_describes_api_contract_admission_and_response_identity — ok
    • auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor — ok
    • auth_policy::branch_statements_take_their_routes_policy_decision — ok
    • data_routes::remote_branch_list_create_merge_flow_works — ok (from_commit create, unknown commit 404, both fields 400)
    • data_routes::branch_statements_dispatch_to_their_route_bodies — ok
  • OMNIGRAPH_UPDATE_OPENAPI=1 on the freshness test, then a diff: only the two enums. The file was restored. Nothing was pushed.
  • cargo clippy -p omnigraph-server -p omnigraph-cli -p omnigraph-api-types --all-targets --locked -- -D warnings -W clippy::dbg_macro — exit 0.
  • python3 scripts/check-docs.py — Documentation OK (346 Markdown files).

Limitations

I did not run the full workspace suite, the CLI process tests cli_data::as_on_a_served_write_asserts_the_actor and cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor, GQT, or S3/Azure. The CLI --as path for branch create is from reading resolve_with_policy, with_expected_actor, and branch_create. The actor test does not send from_commit. That body uses the same POST /branches layer the other protected routes use. json_post in the passing branch-flow test sends Omnigraph-Http-Api: 0.14.

The main merge kept two createBranch responses, 404 and 409, from a
tree that still spoke contract 0.13. The generated spec names 0.14 on
every response, as the server sends it.
@ragnorc

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Addressed at 0432c9d.

Blocking

  1. The two createBranch enums. The 404 and 409 responses of POST /graphs/{graph_id}/branches now name contract 0.14. They were the only difference from the generated spec, as your diff found; no response in openapi.json names 0.13 now.

Optional

  1. RFC rfc: graph access and cross-process writes #1017. It names Omnigraph-If-Actor throughout as of c4641d7. The siblings get the name when feat(server)!: say on every error body whether the request ran #1076 merges this branch, and it flows on from there.
  2. The checklist. It now points at changelog.d/if-actor-header.breaking.md.

ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 20, 2026
Brings in ModernRelay#1074's Omnigraph-If-Actor header through ModernRelay#1076, and ModernRelay#1076's
final error-code registry.
@cursor

cursor Bot commented Oct 20, 2026

Copy link
Copy Markdown

Recommendation

Approve. There is no blocking finding.

Reviewed head: 0432c9d7455477ace2f56373ed3f311eaacde7a3. The delta is 0e54ed2ea2b6d616675d89d099d348d15740cac3..0432c9d7. Current main is 85a4613831e5047a0c988ea0ad37768fa7dff4bb. The previous review is #1074 (comment) at 0e54ed2e. The reply is #1074 (comment). No other review covers this head.

Why

A served request must run as the actor its credential resolves to. --as on that path must only check that actor. The header must not choose an actor.

Omnigraph-If-Actor is a request precondition. A failed precondition answers 412. That matches Omnigraph-If-Graph-Commit and invariant 10.

What this commit changes

0432c9d7 changes one file, openapi.json, by two lines. The 404 and 409 responses of POST /graphs/{graph_id}/branches (cluster_createBranch) now name contract 0.14.

How

describe_contract writes HTTP_API_CONTRACT ("0.14") onto every response header when it builds the spec. The running server uses the same constant in identify_response. The checked-in file now matches that constant on these two statuses, as it already did on the other statuses of this operation (400, 410, 412 included).

Impact

A client that trusts the checked-in spec now reads contract 0.14 on the branch-create refusals. The server already sent 0.14. This commit changes the document, not the runtime check.

Earlier findings

From the review at 0e54ed2e:

  1. Blocking, resolved. Those two enums are 0.14. A full walk of openapi.json finds 355 omnigraph-http-api response headers, and every enum is ["0.14"]. The file contains no 0.13. The four responses with no contract header are the OAuth metadata operations, which describe_contract skips. All 36 protected operations declare optional omnigraph-if-actor and a 412 response. The only 0.13 strings left in server and CLI Rust are refusal cases: a request or a discovery answer that speaks 0.13 must fail before the data call.

  2. Optional, resolved. Draft RFC rfc: graph access and cross-process writes #1017 at 99e595fe191e877ae1e7bf1e9b4c7817c512213a names Omnigraph-If-Actor and contains no expect-actor.

  3. Optional, resolved. The pull request checklist names changelog.d/if-actor-header.breaking.md. That file is on this head.

Sibling branches and draft RFCs

#1076 at ccdfdbd9 and #1078 at e5b11dd6 now use IF_ACTOR_HEADER (omnigraph-if-actor) on contract 0.14. #1081 at 8cc5a930, #1083 at 7175120b, and #1084 at 24be23f4 still compile EXPECT_ACTOR_HEADER (omnigraph-expect-actor) on contract 0.14. They need this name when they update. #1075, #1077, #1090, #1096, #1059, #1012, #1092, and #1097 are still on contract 0.13 and do not define the header.

Draft RFC #1089 at 2b90ba4c names Omnigraph-If-Actor and limits status 412 to an If-* precondition. Draft RFC #1093 at e3b7f138 does not name this header. None of that asks for a change on this head.

Merge with main, and the #1091 routes

git merge-tree --write-tree 0432c9d7 85a46138 exits 0 and writes tree 13caec7c5a62c4db81313cfc0b0bb01eb1bea98c. A local merge in the isolated checkout produces that same tree. I did not push it. Git reports no conflict. It auto-merges five files: handlers.rs, lib.rs, control-plane.md, versioning.md, and skills/omnigraph/SKILL.md. openapi.json is unchanged by the merge, and the merged file still has no 0.13. #1091 edits the router and does not edit openapi.json, so this merge does not repeat the stale-enum class.

The merged per-graph router registers /branches/{branch} (DELETE plus the action fallback) and then applies require_expected_actor, after require_contract and before graph lookup. POST /branches/merge, GET /branches/diff, GET /branches/merge-base, and DELETE /branches/merge all go through that layer. A mismatch is 412 actor_mismatch before the handler. A match leaves AuthenticatedActor unchanged. The header is not an engine argument.

Lance 13 BranchContents (lance-13.0.0 src/dataset/refs.rs) has parent, identifier, parent version, create time, manifest size, and a string metadata map. It has no actor field. A graph branch is created through the catalog manifest (graph_coordinator), not through Dataset::create_branch. This commit does not change that path.

Tradeoff and liability

One middleware and one header remain the right shape. The header does not add a second identity. This commit does not add a queue, a shadow actor store, or a Lance actor.

The fix puts the checked-in spec back on the one constant. The generator stays the source of truth. The freshness test stays the gate. The liability this removes is a client that reads 0.13 from two branch-create refusals while every other response, and the running server, say 0.14. The liability it adds is none: the same test already fails when a later merge keeps a response object from a tree that still speaks 0.13. Five more merges of that kind stay safe when that test is green before the merge commit. A second generator or a merge hook would add a mechanism the test already covers.

Validation

Isolated checkout of 0432c9d7. The parent and this head differ only in openapi.json.

  • openapi::openapi_spec_is_up_to_date on this head — ok.
  • Same test with the parent file 0e54ed2e:openapi.json in place of the committed file, same binary — FAILED (BEFORE_EXIT:101, panic at openapi.rs:2273). A JSON walk of the two files shows exactly two differences: POST /graphs/{graph_id}/branches responses 404 and 409, 0.14 on this head and 0.13 on the parent. I restored the file. git status was clean. Nothing was pushed.
  • openapi::openapi_describes_api_contract_admission_and_response_identity — ok. This test reads the generated spec.
  • auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor — ok.
  • auth_policy::branch_statements_take_their_routes_policy_decision — ok.
  • data_routes::remote_branch_list_create_merge_flow_works — ok.
  • data_routes::branch_statements_dispatch_to_their_route_bodies — ok.

Command: cargo test -p omnigraph-server --test integration --locked with those filters. Result: 6 passed; 0 failed.

GQT cannot express an OpenAPI response enum or this header. The existing owner is the freshness test. It fails on the parent for the predicted reason and passes on this head. No new case is required.

Local merge with 85a46138 (tree 13caec7c, not pushed):

  • openapi::openapi_spec_is_up_to_date — ok.
  • openapi::openapi_describes_api_contract_admission_and_response_identity — ok.
  • data_routes::remote_branch_delete_flow_works — ok (includes delete of a branch named merge).
  • data_routes::remote_branch_list_create_merge_flow_works — ok.
  • The actor test, with four temporary requests added in that checkout only (POST /branches/merge, GET /branches/diff, GET /branches/merge-base, DELETE /branches/merge, mismatched omnigraph-if-actor) — ok. Each answer was 412 actor_mismatch, and the manifest version did not move. I removed those lines. They are not on the pull request.

cargo clippy -p omnigraph-server -p omnigraph-cli -p omnigraph-api-types --all-targets --locked -- -D warnings -W clippy::dbg_macro on the merged tree — exit 0.

python3 scripts/check-docs.py — Documentation OK (346 Markdown files on this head, 348 on the merged tree).

Limitations

I did not run the full workspace suite, workspace Clippy, the CLI process tests, GQT, or S3/Azure. The merged tree compiled the server integration tests and the three Clippy packages above. The temporary branch-route requests ran only in the isolated merge checkout and are not in the pull request.

Main replaced the legacy routing flags with explicit targets (ModernRelay#1090). The
actor assertion sits on that structure: `GraphClient::from_target` and the
served cluster client send `--as` as `Omnigraph-If-Actor` on a server
target, where ModernRelay#1090 refused it, and only a managed target still refuses
`--as`. `accepts_actor` keeps deciding which commands take it. The HTTP
contract stays 0.14.
…ges within the cap

The served cluster entry now takes the actor `--as` asserts. The server, CLI reference and cluster guides fold the merged text back under 350 lines.
@ragnorc
ragnorc added this pull request to the merge queue Oct 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 20, 2026
@ragnorc
ragnorc enabled auto-merge October 20, 2026 19:19
@ragnorc
ragnorc added this pull request to the merge queue Oct 20, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 20, 2026

ragnorc commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation

Approve. There is no blocking finding.

Reviewed head: 54f08e347f1c87ac2e536403621e185ac77307bd. The delta is 0432c9d7455477ace2f56373ed3f311eaacde7a3..54f08e34. Current main is b692610c4aa31066cb6a9fd34830f56977288943. The previous review is #1074 (comment) at 0432c9d7. No other review covers this head.

Why

A served request must run as the actor its credential resolves to. --as on that path must only check that actor. The header must not choose an actor.

Omnigraph-If-Actor is a request precondition. A failed precondition answers 412. That matches Omnigraph-If-Graph-Commit and invariant 10.

What this delta changes

Three commits landed after 0432c9d7.

e4a3e82d merges main at 4ba904d3. That brings explicit targets (#1090), load-plan measurements (#1099), and ANN diagnostics (#1086). The merge conflicts in the CLI, the skill, and the user guides. The resolution keeps one rule. A server target sends --as as omnigraph-if-actor. A managed target still refuses --as. A direct write still uses --as as the actor.

43534757 makes the cluster remote test compile, and folds three user pages back under 350 lines.

54f08e34 merges main at b692610c (#1100, pipeline breakers). git merge-tree --write-tree of the two parents writes tree 096f636a846fcdbbf585b4912bf854faf17bebc7. That tree is the tree of this head. Git reports no conflict. #1100 does not change the server, the CLI, or openapi.json.

How

GraphClient::from_target calls with_expected_actor for a server target. The actor argument is --as. operator.actor stays on the direct path.

Write commands pass that actor. Read commands pass None. accepts_actor allows --as on the same writes, including blob put, blob clear, commit publish, commit drop, cluster plan, and cluster apply. validate_target refuses --as for a managed target.

cluster_remote::run takes expected_actor and puts it on every request from that client. commands::cluster::run passes scope.actor, which is --as. The test that refuses a source-free server apply passes None, and it returns before RemoteCluster::new.

The server routers still run require_expected_actor after authentication and the contract check, and before graph lookup or admission. /mcp still has no contract check. The header is not an engine argument. GET /graphs/{id}/access is on that same per-graph router.

Lance 13 BranchContents (lance 13.0.0, src/dataset/refs.rs) holds the parent branch, the identifier, the parent version, the create time, the manifest size, and a string metadata map. It has no actor field. A graph branch is created through the catalog manifest, not through Dataset::create_branch.

Impact

A served --as still cannot select an actor after the move to explicit targets. A mismatch is still 412 actor_mismatch and CLI exit 4, with no effect. A server that speaks contract 0.13 is still refused at discovery, before the data request.

Earlier findings

From the review at 0432c9d7:

  1. Blocking, resolved. The 404 and 409 responses of POST /graphs/{graph_id}/branches name contract 0.14. openapi.json on this head contains no 0.13. openapi::openapi_spec_is_up_to_date passes.
  2. Optional, resolved. Draft RFC rfc: graph access and cross-process writes #1017 at eca33ef17b76 names Omnigraph-If-Actor. It contains no expect-actor.
  3. Optional, resolved. The pull request checklist names changelog.d/if-actor-header.breaking.md. That file is on this head.

Blocking

None.

Optional

  1. The Blob commands section of docs/user/cli/reference.md still says put and clear accept --as on direct storage targets. This pull request also accepts --as on a served blob put and blob clear, and sends Omnigraph-If-Actor. docs/user/blobs.md says that, and so does the introduction of the same reference page. Please make the Blob commands section say the same thing. The sentence is the #1090 wording. On current main, every served command refuses --as, so the restriction matches main. This merge updates the introduction and leaves the Blob section.

Tradeoff and liability

One middleware and one header remain the right shape. The header does not add a second identity. This delta does not add a queue, a shadow actor store, or a Lance actor.

The compile fix supplies the expected_actor argument the merge left off one test call. None is the right value there, because that case stops before any request. The production path passes --as.

The page edit keeps the same sentences and brings three guides under the 350-line cap. It does not add a second documentation mechanism.

This delta removes a cluster test that does not compile, and three user pages that fail the documentation check, after the target merge. It adds one obligation: a later command that accepts --as must pass the actor into from_target or cluster_remote::run, and a read must pass None. planes::tests::actor_flag_matrix_matches_consumption_issue_1023 is the list of commands that accept the flag. Five more commands of this kind stay on this one header. #1104 already sends it on GET /graphs/{id}/access.

#1076 at 3a35355e and #1078 at 21cea205 use IF_ACTOR_HEADER (omnigraph-if-actor) on contract 0.14. Draft RFC #1089 at a85ffdca limits status 412 to an If-* precondition and names Omnigraph-If-Actor. Draft RFC #1093 does not name this header.

Validation

Isolated checkout of 54f08e34.

  • git merge-tree --write-tree 43534757113a b692610c4aa3 wrote 096f636a846fcdbbf585b4912bf854faf17bebc7, equal to 54f08e34^{tree}. No conflicts.
  • python3 scripts/check-docs.py on this head: Documentation OK (356 Markdown files). Exit 0.
  • The same check with docs/user/cli/reference.md, docs/user/clusters/index.md, and docs/user/operations/server.md taken from e4a3e82d: failed, 3 errors, each page exceeds 350 lines. The three files were restored. git diff was empty.
  • cargo test -p omnigraph-server --test integration --locked -- --exact for openapi::openapi_spec_is_up_to_date, openapi::openapi_describes_api_contract_admission_and_response_identity, auth_policy::expected_actor_header_narrows_a_request_and_never_supplies_an_actor, auth_policy::branch_statements_take_their_routes_policy_decision, data_routes::remote_branch_list_create_merge_flow_works, and data_routes::remote_branch_delete_flow_works. Result: 6 passed; 0 failed.
  • cargo test -p omnigraph-cli --lib --locked -- --exact cluster_remote::tests::apply_output_failure_preserves_identity_and_never_replays_submission planes::tests::actor_flag_matrix_matches_consumption_issue_1023. Result: 2 passed; 0 failed.
  • Before the compile fix, in this checkout only, the run call matches e4a3e82d (5 arguments). The same lib test fails with error[E0061]: run takes 6 arguments and 5 were supplied. Exit 101. The file was restored. The same test on this head then passed (1 passed).
  • cargo test -p omnigraph-cli --test integration --locked as_on_a_served. Result: cli_data::as_on_a_served_write_asserts_the_actor ok, cli_cluster::as_on_a_served_cluster_command_exits_4_on_another_actor ok. 2 passed; 0 failed.

GQT cannot express this header, a CLI exit code, an OpenAPI enum, or a documentation line cap. The owners above already fail for the predicted reason before the fix and pass on this head. No new case is required.

GitHub Actions on this head are green (runs/39482479639, plus the GQT and DST runs), including Test Workspace, Clippy, GQT, RustFS, and Azurite. That is CI evidence. It is separate from the local commands above.

Limitations

The full workspace suite, workspace Clippy, GQT, and S3/Azure were not run locally. The temporary test edit and the temporary guide swap were restored. Nothing was pushed.

@ragnorc
ragnorc merged commit 083cdd7 into ModernRelay:main Oct 20, 2026
32 checks passed
ragnorc added a commit to ragnorc/omnigraph that referenced this pull request Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant