Repository navigation
Conversation
AppState::local opens a standalone graph read-only or read-write behind a generated bearer token and a Cedar policy generated for one actor, which it installs on the engine as well as at the HTTP layer. Every policy action has a local-server class, so an action added later is never granted until it is given one. A read-only local server refuses every write-class route in ingress before reading its body. No local server mounts the cluster deployment routes; each enables MCP ad-hoc reads and lists write tools only when it writes. A root inside a cluster is refused. serve_local runs the deployment's serving lifecycle, now one private type both entry points share, on a loopback listener the caller bound. It writes nothing to stdout, disarms the shutdown watchdog when it returns, and reports an uncertain outcome instead of exiting. install_log_subscriber puts the server's restricted log filter on another writer.
The shutdown listener registered its SIGTERM and SIGINT handlers only when its spawned task first ran, and `serve_local` installed the lifecycle only after the caller had announced readiness. A signal in either window met the default disposition and ended the process without the drain. The lifecycle now registers both handlers before it returns, and `start_local` installs it so a caller announces between `start_local` and `LocalServing::serve`. `serve_local` remains the two calls in one.
|
omnigraph-server can open one standalone graph and serve it on a loopback listener. AppState::local builds one bearer token, keeps only its SHA-256 hash, and installs a Cedar policy for one actor. serve_local runs the same shutdown lifecycle as a deployment, writes nothing to stdout, and returns ServeOutcome::Uncertain instead of exiting the process. That shape is right. One lifecycle, one grant table, no new writer lock. The public binding and the read-write mode do not match draft RFC pull request #1017 at 7c72c7e. Request changes. Blocking
LocalBinding::from_root stores the normalized root. AppState::local then calls Omnigraph::open or Omnigraph::open_read_only. Those calls use storage_for_uri. For s3://, s3_from_root_uri reads AWS_ENDPOINT_URL_S3 and AWS_ENDPOINT_URL. For az://, it uses CanonicalAzureRoot::from_env. The process environment can send the open to a different endpoint than the target names. The RFC local-embedding contract says LocalBinding is the direct binding the resolver captured: the canonical root and the storage provider settings. It says the value is never a bare URI string, so ambient variables cannot redirect catalog, admission, or Lance access. The RFC also says this interface is final when it lands. from_root(&str) freezes the wrong value. Open through a store selection the caller already captured. Do not read the endpoint from the environment inside AppState::local. RFC 0059 (#675) is still a draft, so do not wait for its full target type, and do not publish a string constructor as the stable API. Inline, crates/omnigraph-server/src/local.rs line 42 (RIGHT): LocalBinding::from_root keeps only the normalized root. AppState::local then calls Omnigraph::open / open_read_only on that string. Those opens go through storage_for_uri. s3_from_root_uri reads AWS_ENDPOINT_URL_S3 and AWS_ENDPOINT_URL. az:// uses CanonicalAzureRoot::from_env. The process environment can move the open to a different endpoint than the target names. Draft RFC #1017 says LocalBinding carries the canonical root and the storage provider settings, and that it is never a bare URI string, so ambient variables cannot redirect the open. The same RFC says this interface is final when it lands. from_root(&str) freezes the wrong value. Open through a store selection the caller already captured. Do not read the endpoint again inside AppState::local. RFC 0059 is still a draft. Do not wait for its full target type, and do not publish this string constructor as the stable API.
LocalAccess::ReadWrite opens the graph for write and grants change, branch, and commit actions. serve_local still shuts down when operations.fatal() fires, then returns Uncertain. The new test serve_local_runs_the_lifecycle_and_leaves_stdout_to_its_caller_uncertain shows the child writes Ok(Uncertain) and exits 2. Issue #1067 is open. Main's #1070 keeps the server up after a definite storage failure before publication. An uncertain completion still stops the process. The RFC says read-write local servers wait for the change that keeps the server up, so one storage error does not take the graph away from the client. The changelog and docs/dev/control-plane.md present read-write as available now. Refuse ReadWrite from AppState::local until that shutdown is gone. Remove the read-write claim from the changelog until then. Keep the enum variant if #1077 needs the name. The refusal is the gate. Inline, crates/omnigraph-server/src/local.rs line 30 (RIGHT): ReadWrite opens the graph for write and the grant table allows change, branch, and commit actions. serve_local still treats operations.fatal() as shutdown and returns Uncertain. The new uncertain test shows Ok(Uncertain) and exit 2. Issue #1067 is open. An uncertain completion still stops the process. Draft RFC #1017 says read-write local servers wait until that stop is gone, so one storage error does not take the graph away from the client. The changelog presents this mode as available now. Refuse ReadWrite in AppState::local until the server stays up. Keep the variant if #1077 needs the name. Remove the read-write claim from the changelog until the gate is open.
This change crosses that support boundary. Update the sentence in this pull request so it matches the library that ships here. The omnigraph-server binary stays cluster-only. AppState::local and serve_local serve one standalone graph on loopback and take no cluster admission. Do not document omnigraph serve in that sentence. That command is not in this pull request. Optional
OpenMode::ReadOnly only skips open-time writes. The handle does not store the mode. A later mutate_as runs unless Cedar denies it. The RFC asks for a test where the engine refuses a write even when a route would admit it. The new HTTP test shows ingress returns 403 before the body. Add one mutate_as on the handle from AppState::local(ReadOnly) that expects OmniError::Policy. That pins the layer ingress does not cover.
serve_local rejects a non-loopback bind. It does not check Host or Origin on graph routes. The RFC puts that guard in the CLI extend layer, so a deployment never gains it. docs/dev/control-plane.md is the embedder contract and does not say this. State that a caller which serves a browser adds that guard in extend: an exact loopback Host, a refused Origin, no CORS header, and Cross-Origin-Resource-Policy: same-origin. RFC deltas that are not defects here
Siblings#1077 is stacked on this head and adds LocalScope::Cluster. Review that pull request for cluster admission, the read-only forbid, and lock release. Those are not gaps in this pull request. #1073, #1074, and #1076 edit crates/omnigraph-server/src/lib.rs, handlers.rs, and tests/integration/auth_policy.rs beside this pull request. They do not change this contract. Expect a textual merge conflict. #1059, #1012, and draft #675 do not touch this serving path. #675 is the reason LocalBinding must not freeze a bare URI first. Tradeoffs and liabilityThe shared Lifecycle removes a second copy of the shutdown path. A deployment never disarms the watchdog. A dropped sender still exits at the original deadline. serve_local disarms after drain, so the embedder process stays alive. That split is correct. The grant table is one exhaustive match over PolicyAction. A new action fails compilation until it has a class. ALL_ACTIONS is a second list. If it lags the match, the new action is not granted. That fails closed. Later local policies should extend this match, not add another generator. No new lock, queue, or shadow catalog. A read-write local server is one writer process under the current boundary. The control-plane page says so. That matches the deny-list. The cost is the new public types and the two entry points. That is the right size for one embedding door. The cost becomes permanent if LocalBinding and ReadWrite ship in the wrong shape, because the RFC says this interface does not change later. This change spends its complexity on one shutdown path and one grant table. It does not spend it on a second storage selector. That is the wrong cut. Token generation and policy compile already happen once per open. The binding and the uncertain-write gate are the parts that have to be right before the API is public. ValidationReviewed head 16de8b9. Base 0a62bb7. RFC head 7c72c7e. RUST_MIN_STACK=16777216 cargo test -p omnigraph-server --lib -- local::tests:: shutdown_signal_tests:: 6 passed, 2 ignored. The ignored tests are subprocess helpers. The parent tests run them. This includes the new disarmed-watchdog test and the existing deadline test. RUST_MIN_STACK=16777216 cargo test -p omnigraph-server --test integration -- a_read_only_local_server a_read_write_local_server a_local_server_refuses a_local_server_reads_ad_hoc 4 passed. RUST_MIN_STACK=16777216 cargo test -p omnigraph-server --test boot_settings -- local_serving:: 3 passed, 1 ignored (the child helper). The runs cover the non-loopback refusal, a clean drain, and Ok(Uncertain) with exit 2. RUST_MIN_STACK=16777216 cargo test -p omnigraph-server --test boot_settings -- startup_listener_finish_retains_open_ownership -- --exact 1 passed. This is the existing deployment startup owner on the shared lifecycle. cargo test -p omnigraph-policy --lib -- load_graph_accepts_per_graph_only_policy load_graph_rejects_server_scoped_action 2 passed. graph_from_config and server_from_config use the same kind check as the file loaders. Not run: the full server suite, Clippy, cargo fmt, S3, or Azure. The ambient-endpoint finding is from s3_from_root_uri and CanonicalAzureRoot::from_env, not from a two-endpoint run. This pull request adds a serving API. It does not fix a query result. GQT cannot express a listener, a bearer token, or process shutdown. No before/after GQT regression applies. The server tests above are the owner. |
|
Request changes. An embedder needs one loopback server for one standalone graph, with one token and one shutdown path. This pull request adds that door. Earlier findingsReviewed against
New delta
The split matches the window the RFC gives the caller: bind, install the lifecycle, write the record, then serve. The new test blocks the only runtime thread between Blocking. The release note overstates that window.
The code does what the commit message says. After Optional. The embedder page does not state the process-lifetime signal change. Tokio 1.52.3 installs the libc handler for the rest of the process and never removes it ( No other behavior bug showed up in the delta. RFC deltas that are not defects hereRFC head is still
Actor views, a graph inside a cluster, records, election, the Host guard, Siblings#1077 is stacked on this head. Its history contains #1073, #1074, and #1076 edit #1078, #1080, and #1081 are the CLI stack. #1059 and #1012 do not touch this serving path. Draft #675 is why Tradeoffs and liabilityThe shared Tokio never restores the default disposition. The first The grant table is still one exhaustive match over No new lock, queue, or shadow catalog. A read-write local server would be one writer process under the current boundary. The control-plane page says so. That matches the deny-list. The mode should not ship until #1067 lands. The public types are the right size for one embedding door. They become permanent if ValidationReviewed head Policy: 2 passed. Server lib: 6 passed, 2 ignored. The ignored tests are subprocess helpers. The parent tests run them. 5 passed, 2 ignored. The ignored tests are the child helpers. This includes 4 passed. Regression for the signal fix. GQT cannot express a listener, a bearer token, or a process signal, so the owner is At At Not run: the full server suite, Clippy, Inline anchors
|
A write whose outcome is unknown still stops the server that ran it, so `AppState::local` refuses `LocalAccess::ReadWrite` with `LocalOpenError::ReadWriteUnavailable` until it no longer does. The read-write path stays and stays tested: the `test-util` feature's `with_local_read_write` scope opens it for tests alone, enabled from dev-dependencies only (this crate's own included) and checked by the test-util guard, and a build with the feature still refuses outside the scope. `LocalBinding::local(path)` replaces the string constructor. It takes a root on this machine's file system, made absolute, and refuses a URI: a remote root's endpoint and credentials would come from the process environment when it opens, which a captured binding must not depend on. A read-only local server's engine refuses a direct write, now tested on the handle. The developer guide says what the read-write gate is, that an embedder a browser can reach adds the loopback guard in `extend`, and that Tokio keeps the signal handlers for the rest of the process. The invariants say the library serves one standalone graph on loopback while the binary stays cluster-only, and the release notes no longer claim read-write or a wider signal window than the code covers.
|
Addressed at 4708c68. Blocking
New delta: the signal release note now says when the handlers register (when the lifecycle starts) and drops "before it opens anything" and "always". Optional:
|
|
Request changes. An embedder needs one loopback server for one standalone graph, with one token and one shutdown path. This pull request adds that door. Earlier findingsReviewed against
New delta
The gate is in the right place. The path constructor closes the ambient-endpoint hole without freezing a URI string. The stored value is still a string, and it is a local absolute path. See the RFC section for the remaining disagreement, which is the interim the last review asked for. The uncertain-write shutdown is unchanged, and a production caller can no longer enter it through Blocking
The recovery text was written when the mode was going to ship. The gate did not change the text. Say that the server is read-only and that a read-write local server is not available yet. Do not name #1077 already contains this head, so it carries the same sentence until it takes this fix. #1076, #1078, #1080, and #1081 also edit Inline, Optional
Where the code and the RFC disagreeRFC head is still
Siblings#1077 is stacked on this head ( #1073, #1074, and #1076 edit #1059 does not touch these files. #1012 edits Tradeoffs and liabilityThe shared The path constructor removes ambient endpoint redirect for this API. The cost is a public type that stores a path string until RFC 0059 supplies provider settings. Further bindings should be constructors on this private field. A parent segment is the spelling gap that is still open. It is optional above. The grant table is still one exhaustive match on No new lock, queue, or shadow catalog. The server binary stays cluster-only. A read-only local server is not a writer process. The Registering the handlers before ValidationReviewed head Local, at this head: 9 passed, 2 ignored. The ignored tests are subprocess helpers. The parent tests run them. This includes the binding test, the read-write gate, the engine 5 passed, 2 ignored. The ignored tests are the child helpers. This includes the signal after 4 passed. The read-write test asserts the production gate before it opens the mode inside the test scope. 4 passed. Regression. GQT cannot express a listener, a bearer token, At
At 2 passed. Parent-segment probe, temporary, at CI on this commit, run https://github.com/ModernRelay/omnigraph/actions/runs/39179057728 : Test Workspace, Lint (clippy), Format (rustfmt), GQ Logic Tests, and the S3 and Azure jobs passed. Devin Review skipped (trial expired). Local runs above are the focused suites. The full server suite was not re-run locally. No two-endpoint cloud run was done. The ambient-endpoint proof is the constructor refusal, plus the before probe that accepted |
…-write A read-only local server refused every write with "start it with --write to change the graph", though `AppState::local` refuses read-write outside the test scope and no command accepts `--write` yet. The refusal now says the server is read-only and that a read-write local server is not available yet, and the route test checks that it names no `--write`. `LocalBinding::local` folds `.` and `..` with the storage crate's lexical absolutizer, now re-exported beside `normalize_root_uri`, so one directory has one stored spelling. The embedding page no longer cites a command this change does not ship.
|
Addressed at 60851ab. Blocking
Optional
RFC differences: #1017 now names Validation: |
Brings in the access route from ModernRelay#1073; its tests and the local server's sit side by side at the end of auth_policy.rs.
Brings in main through ModernRelay#1075: the access route from ModernRelay#1073, whose tests follow the local cluster server's at the end of auth_policy.rs.
Brings in ModernRelay#1075's fix through ModernRelay#1077: the read-only refusal names no --write, and a local binding folds `..` before it is stored.
The engine re-exports the storage crate's lexical absolutizer for the local server's binding; the split-crate re-export guard lists every such item by name, so it now names this one.
|
An embedder needs one loopback server for one standalone graph, with one token and one shutdown path. This pull request adds that door. Recommendation: approve. No blocking finding remains. Earlier findingsReviewed against
New delta
The merge touches four files on both sides:
The fold does not call BlockingNone. Optional
Inline, Where the code and the RFC disagreeRFC head is
Siblings#1073 is merged. This pull request's merge of main takes that route without dropping the local contract. #1077, #1083, and #1084 contain #1074, #1076, #1078, #1080, and #1081 do not contain this head. They do not contain the #1059 does not touch these files. #1012 can conflict on Tradeoffs and liabilityThe shared The path constructor now has one stored spelling for The grant table stays one exhaustive match on No new lock, queue, or shadow catalog. The server binary stays cluster-only. A read-only local server is not a writer process. ValidationReviewed head GQT cannot express a listener, a bearer token, an HTTP error body, or At At At 9 passed, 2 ignored. The ignored tests are subprocess helpers. The parent tests run them. This includes the folded-root assertion, the read-write gate, the engine 1 passed. The re-export pin matches the source. 5 passed, 2 ignored. The ignored tests are the child helpers. 4 passed. The read-only test rejects a body that contains CI on this commit: Test Workspace, Lint (clippy), Format (rustfmt), S3, and Azure passed in https://github.com/ModernRelay/omnigraph/actions/runs/39276914562. GQT ordinary passed. GQT (dst) in https://github.com/ModernRelay/omnigraph/actions/runs/39276914393 failed Not run locally: the full server suite, a two-endpoint cloud open, a symlink path, or Windows. The ambient-endpoint hole stays closed because a URI never reaches |
Disposition — 2026-10-20
The second serving model is discontinued. OmniGraph retains one cluster-backed
server model, including on localhost: the ordinary applied state, authentication,
policies, admission, routes and deployment lifecycle.
This PR is closed without merging. The standalone/local serving APIs, generated
local policies and credentials, and local-server discovery/election/reuse
registry are not proceeding. This decision supersedes the implementation and
sequencing proposed below; draft #1017 is being revised accordingly. No part of
this serving stack has shipped. General improvements to the existing server
may be proposed independently without introducing another serving mode.
Historical proposal
What & why
omnigraph-servercan serve one standalone graph to clients of the same account on loopback, for an embedder such as the CLI's comingservecommand:AppState::local(LocalScope::Graph(binding), options)opens the graph read-only or read-write behind one generated bearer token (revealed once, held as a hash) and a Cedar policy generated for one actor, installed on the engine as well as at the HTTP layer. EveryPolicyActionhas a local class by an exhaustive match, so an action added later is never granted until it is classified. A read-only local server refuses every write-class route in ingress before reading its body; no local server mounts/cluster/*; MCP ad-hoc reads are on, write tools only when it writes. A root inside a cluster is refused.serve_local(state, listener, extend)runs the deployment's serving lifecycle, now one privateLifecycleboth entry points share, on a loopback listener the caller bound. It writes nothing to stdout, disarms the shutdown watchdog when it returns, and reportsServeOutcome::Uncertaininstead of exiting.install_log_subscriber(writer)puts the server's restricted log filter on another writer.Backing issue / RFC
omnigraph studiolauncher and graph owner discovery #980. Merges after that RFC is accepted.Checklist
auth_policyread-only and read-write local servers and cluster refusal;mcptool lists;boot_settings::local_serving(loopback refusal, stdout left to the caller, clean drain, uncertain outcome in a child process); a disarmed watchdog testchangelog.d/local-server-embedding.added.mdLocal verification
RUST_MIN_STACK=16777216 cargo test -p omnigraph-server -p omnigraph-policy— all passcargo clippy -p omnigraph-server -p omnigraph-policy --all-targets --locked -- -D warnings -W clippy::dbg_macro,cargo fmt --all --check,python3 scripts/check-docs.py,typos— cleanNotes for reviewers
No CLI command uses this yet;
omnigraph serveneeds the RFC's targets. The deployment's own serving path is unchanged apart from sharing the lifecycle type.