feat(cli)!: remove legacy data-plane addressing (--target, positional http→remote, --as-on-served) (#238)

* feat(cli): --server accepts a literal URL (RFC-011 Decision 2)

`resolve_server_flag` now treats a `--server` value containing `://` as a literal
base URL (trailing slash trimmed; `--graph` appends `/graphs/<id>`), bypassing the
operator-config `servers:` registry; a bare name still resolves through the
registry. This is the replacement the upcoming `--uri http(s)://` deprecation
points at, and a small ergonomic win on its own (`--server https://host` with no
config entry). Token resolution for a literal-URL server falls to the legacy
OMNIGRAPH_BEARER_TOKEN chain, same as a positional URL today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(cli): address the parity-matrix arms with global --store/--server flags

Prep for removing the positional-http→remote dispatch. The parity harness
addressed both arms with a positional graph right after the verb
(`omnigraph <verb> <addr> <args…>`), which only parses for top-level verbs —
for nested subcommands (`schema show`, `branch list`, …) the address landed in
the subcommand slot and BOTH arms failed identically, so the test passed
vacuously (matching exit codes, never comparing output).

Address both arms with the global flags instead — local `--store <graph>`
(embedded), remote `--server <url>` (served) — appended after the verb + args,
valid regardless of nesting. The previously-vacuous nested-verb parity checks
now actually compare embedded vs remote (and pass — parity holds), and the
remote arm no longer relies on the positional-URL dispatch that's about to be
removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli)!: --as on a served write is a hard error (was a silent no-op)

A served write resolves the actor server-side from the bearer token, so `--as`
could never set identity there — it was silently ignored. It now errors (in the
remote write factory, before any HTTP call), pointing the user at removing `--as`
or writing directly with `--store`. Reads don't carry `--as`, so this is
write-path only. BREAKING for any script that passed `--as` to a remote write
(it was a no-op, so behavior is unchanged except the now-explicit error).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli)!: a positional/--uri http(s):// URL no longer dispatches to a server

Remote graphs must be addressed with `--server <url>` (or a named server / a
profile binding one). A positional or `--uri` `http(s)://` URL on a data verb now
errors instead of silently routing to the remote HTTP client — the scheme no
longer carries transport semantics. The discriminator is `via_server`: a remote
URL produced by a server scope is fine; a remote URL from a positional/`--uri`
source is rejected (`reject_positional_remote` in both GraphClient factories).

Storage verbs are unaffected — they already reject remote URIs through
`resolve_local_graph` with the existing "direct (storage-native)" error.

Migrated the gh-host keyed-credential system test to `--server <url>` (the literal
URL still prefix-matches the operator server for token resolution). BREAKING:
scripts addressing a server by a bare URL must switch to `--server <url>`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli)!: remove the --target flag (use --store / --profile / --server)

Removes the legacy named-graph flag and threads its parameter out of the whole
resolver chain. `--target` resolved a graph name through `omnigraph.yaml`'s
`graphs:` map; its replacements (`--store <uri>`, `--profile <name>`,
`--server <name>`) all ship.

- Drops the 22 `target` clap fields + the `--cluster` exclusion that named it.
- Threads `target`/`cli_target` out of `resolve_uri`/`resolve_cli_graph`/
  `resolve_local_graph`/`resolve_local_uri`/`resolve_storage_uri`/
  `resolve_remote_bearer_token`/`apply_server_flag`/`execute_query_lint`/
  `resolve_selected_graph`/`resolve_registry_selection_for_list`/
  `execute_queries_{validate,list}`, the two `GraphClient` factories, and
  `ScopeFlags`/`ResolvedScope`.
- Keeps the shared `OmnigraphConfig::resolve_target_uri` 3-arg (server boot uses
  it); the CLI passes None for the explicit-target arm. The `cli.graph` default
  (omnigraph.yaml bare-command fallback) is unchanged — its removal belongs to
  the omnigraph.yaml excision.
- Operator/file aliases that bind a `graph` name still work: the name is now
  resolved to a URI inline (a positional URI wins).
- Error messages and `--graph`/`--server`/`--store` help text no longer name
  `--target`; the queries-list selection hint points at `cli.graph`.

BREAKING. Tests updated (named-target resolution rewritten onto `cli.graph`;
positional-URI tests unchanged). Full omnigraph-cli suite green (228).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(cli): drop --target and positional-http addressing; --as-on-served is an error

Update the user docs for the legacy data-plane addressing removals:
- the CLI `--target` flag is gone — address graphs with a positional URI,
  `--store`, `--profile`, or `--server <name|url>`;
- a positional `http(s)://` URI no longer dispatches to a server (use `--server`);
- `--as` on a served write is now rejected (was a silent no-op).

Touches cli/reference.md (addressing intro, capability table, error examples,
scopes), cli/index.md (the remote-read example → --server), operations/maintenance
+ policy, and the cluster docs' data-plane load guidance. The server's own
`--target` boot flag is unchanged (server.md untouched). Also fixes a pre-existing
broken maintenance link in search/indexes.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cli): --store is loudly exclusive with a positional URI / --server; test graphs→Served

Address two Greptile findings on the RFC-011 slices:
- Slice A (P1): `--store` combined with a positional URI silently dropped the URI
  (`scope.rs` did `store.or(uri)`); `--store` + `--server` errored with a
  misleading "positional URI" message. Now both combinations fail loudly with a
  declared `--store is exclusive with a positional URI and --server` error.
- Slice B (P2): the `command_capability` unit test never exercised the one
  Data→Served refinement (`graphs`); added the assertion so deleting that guard
  can't pass silently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Andrew Altshuler 2026-06-15 04:29:16 +03:00 committed by GitHub
parent 7eeced3e88
commit bc2a989a7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 273 additions and 269 deletions

View file

@ -31,10 +31,10 @@ pub(crate) struct Cli {
#[arg(long = "as", global = true, value_name = "ACTOR")]
pub(crate) as_actor: Option<String>,
/// Target an operator-defined server by name (RFC-007): resolves to
/// its `url` from `servers:` in ~/.omnigraph/config.yaml. Exclusive
/// with a positional URI or `--target`.
#[arg(long, global = true, value_name = "NAME")]
/// Address a server by name (resolves to its `url` from `servers:` in
/// ~/.omnigraph/config.yaml) or by a literal `http(s)://` URL. Exclusive
/// with a positional URI.
#[arg(long, global = true, value_name = "NAME|URL")]
pub(crate) server: Option<String>,
/// Graph id on a multi-graph `--server` (appends `/graphs/<id>` to
@ -52,7 +52,7 @@ pub(crate) struct Cli {
/// Address a single graph's storage directly (RFC-011): a `file://` /
/// `s3://` store URI. Explicit, ad-hoc direct access — bypasses any
/// server. Exclusive with a positional URI / `--target` / `--server`.
/// server. Exclusive with a positional URI / `--server`.
#[arg(long, global = true, value_name = "URI")]
pub(crate) store: Option<String>,
@ -76,8 +76,6 @@ pub(crate) enum Command {
#[arg(hide = true)]
legacy_uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["query", "query_string"])]
alias: Option<String>,
@ -114,8 +112,6 @@ pub(crate) enum Command {
#[arg(hide = true)]
legacy_uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, conflicts_with_all = ["query", "query_string"])]
alias: Option<String>,
@ -140,8 +136,6 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
data: PathBuf,
@ -165,8 +159,6 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
data: PathBuf,
@ -189,8 +181,6 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
@ -202,8 +192,6 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
@ -250,12 +238,10 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
/// Cluster directory or storage-root URI; with --cluster-graph, resolves
/// the graph's storage URI from the served cluster state.
#[arg(long, conflicts_with_all = ["uri", "target"], requires = "cluster_graph")]
#[arg(long, conflicts_with = "uri", requires = "cluster_graph")]
cluster: Option<String>,
/// Graph id within --cluster.
#[arg(long, requires = "cluster")]
@ -268,12 +254,10 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
/// Cluster directory or storage-root URI; with --cluster-graph, resolves
/// the graph's storage URI from the served cluster state.
#[arg(long, conflicts_with_all = ["uri", "target"], requires = "cluster_graph")]
#[arg(long, conflicts_with = "uri", requires = "cluster_graph")]
cluster: Option<String>,
/// Graph id within --cluster.
#[arg(long, requires = "cluster")]
@ -294,12 +278,10 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
/// Cluster directory or storage-root URI; with --cluster-graph, resolves
/// the graph's storage URI from the served cluster state.
#[arg(long, conflicts_with_all = ["uri", "target"], requires = "cluster_graph")]
#[arg(long, conflicts_with = "uri", requires = "cluster_graph")]
cluster: Option<String>,
/// Graph id within --cluster.
#[arg(long, requires = "cluster")]
@ -333,8 +315,6 @@ pub(crate) enum Command {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
query: PathBuf,
@ -489,8 +469,6 @@ pub(crate) enum GraphsCommand {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
json: bool,
@ -505,8 +483,6 @@ pub(crate) enum BranchCommand {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
from: Option<String>,
@ -520,8 +496,6 @@ pub(crate) enum BranchCommand {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
json: bool,
@ -532,8 +506,6 @@ pub(crate) enum BranchCommand {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
name: String,
#[arg(long)]
@ -545,8 +517,6 @@ pub(crate) enum BranchCommand {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
source: String,
#[arg(long)]
@ -563,8 +533,6 @@ pub(crate) enum SchemaCommand {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
schema: PathBuf,
@ -581,8 +549,6 @@ pub(crate) enum SchemaCommand {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
schema: PathBuf,
@ -606,8 +572,6 @@ pub(crate) enum SchemaCommand {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
json: bool,
@ -622,8 +586,6 @@ pub(crate) enum CommitCommand {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
branch: Option<String>,
@ -636,8 +598,6 @@ pub(crate) enum CommitCommand {
#[arg(long)]
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
commit_id: String,
#[arg(long)]
@ -684,16 +644,12 @@ pub(crate) enum QueriesCommand {
/// Graph URI
uri: Option<String>,
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
json: bool,
},
/// List the registered stored queries (name, MCP exposure, params).
List {
#[arg(long)]
target: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]

View file

@ -66,6 +66,19 @@ pub(crate) enum GraphClient {
},
}
/// A remote graph must be addressed with `--server` (RFC-011): a positional or
/// `--uri` `http(s)://` URL no longer auto-dispatches to a server. A remote URL
/// produced by a server scope (`via_server`) is fine.
fn reject_positional_remote(via_server: bool, uri: &str) -> Result<()> {
if !via_server && is_remote_uri(uri) {
bail!(
"a remote graph must be addressed with `--server <url>` — a positional \
(or `--uri`) http(s):// URL no longer dispatches to a server"
);
}
Ok(())
}
impl GraphClient {
/// Resolve the addressing (positional URI / `--target` / `--server`)
/// and credential once, then pick the variant by URI scheme — the
@ -78,27 +91,27 @@ impl GraphClient {
server: Option<&str>,
graph: Option<&str>,
uri: Option<String>,
target: Option<&str>,
profile: Option<&str>,
store: Option<&str>,
) -> Result<Self> {
// RFC-011: a scope (profile / --store / operator defaults) may stand in
// for omitted addressing. The explicit branch passes server/graph/uri/
// target straight through, so existing invocations are unchanged.
// for omitted addressing. The explicit branch passes server/graph/uri
// straight through, so existing invocations are unchanged.
let scope = crate::scope::resolve_scope(
&crate::operator::load_operator_config()?,
crate::planes::Capability::Any,
crate::scope::ScopeFlags { profile, store, server, graph, uri, target },
crate::scope::ScopeFlags { profile, store, server, graph, uri },
)?;
let (server, graph, uri, target) = (
let (server, graph, uri) = (
scope.server.as_deref(),
scope.graph.as_deref(),
scope.uri,
scope.target.as_deref(),
);
let uri = apply_server_flag(server, graph, uri, target)?;
let token = resolve_remote_bearer_token(config, uri.as_deref(), target)?;
let uri = crate::helpers::resolve_uri(config, uri, target)?;
let via_server = server.is_some();
let uri = apply_server_flag(server, graph, uri)?;
let token = resolve_remote_bearer_token(config, uri.as_deref())?;
let uri = crate::helpers::resolve_uri(config, uri)?;
reject_positional_remote(via_server, &uri)?;
if is_remote_uri(&uri) {
Ok(GraphClient::Remote {
http: build_http_client()?,
@ -125,7 +138,6 @@ impl GraphClient {
server: Option<&str>,
graph: Option<&str>,
uri: Option<String>,
target: Option<&str>,
cli_as: Option<&str>,
profile: Option<&str>,
store: Option<&str>,
@ -135,18 +147,28 @@ impl GraphClient {
let scope = crate::scope::resolve_scope(
&crate::operator::load_operator_config()?,
crate::planes::Capability::Any,
crate::scope::ScopeFlags { profile, store, server, graph, uri, target },
crate::scope::ScopeFlags { profile, store, server, graph, uri },
)?;
let (server, graph, uri, target) = (
let (server, graph, uri) = (
scope.server.as_deref(),
scope.graph.as_deref(),
scope.uri,
scope.target.as_deref(),
);
let uri = apply_server_flag(server, graph, uri, target)?;
let token = resolve_remote_bearer_token(config, uri.as_deref(), target)?;
let resolved = resolve_cli_graph(config, uri, target)?;
let via_server = server.is_some();
let uri = apply_server_flag(server, graph, uri)?;
let token = resolve_remote_bearer_token(config, uri.as_deref())?;
let resolved = resolve_cli_graph(config, uri)?;
reject_positional_remote(via_server, &resolved.uri)?;
if resolved.is_remote {
// A served write resolves the actor server-side from the bearer
// token; `--as` cannot set identity here and is rejected.
if cli_as.is_some() {
bail!(
"`--as` is not allowed on a served write — the server resolves the actor \
from the bearer token. Remove `--as`, or run the write directly against \
storage with `--store <uri>`."
);
}
Ok(GraphClient::Remote {
http: build_http_client()?,
base_url: resolved.uri,

View file

@ -245,8 +245,9 @@ pub(crate) fn normalize_policy_graph_uri(uri: &str) -> Result<String> {
pub(crate) fn resolve_remote_bearer_token(
config: &OmnigraphConfig,
explicit_uri: Option<&str>,
explicit_target: Option<&str>,
) -> Result<Option<String>> {
// `--target` is gone; the legacy explicit-target name is always None.
let explicit_target: Option<&str> = None;
// The keyed hop (RFC-007 §D4, gh-host model): when the effective remote
// URL belongs to an operator-defined server, that server's keyed chain
// applies first — OMNIGRAPH_TOKEN_<NAME> env, then the 0600 credentials
@ -303,19 +304,26 @@ pub(crate) fn resolve_server_flag(
let Some(server) = server else {
return Ok(None);
};
let operator_config = operator::load_operator_config()?;
let Some(entry) = operator_config.servers.get(server) else {
let known = operator_config
.servers
.keys()
.map(String::as_str)
.collect::<Vec<_>>()
.join(", ");
color_eyre::eyre::bail!(
"unknown server '{server}' — servers defined in the operator config: [{known}] (add it under servers: in ~/.omnigraph/config.yaml)"
);
// RFC-011 Decision 2: a value containing `://` is a literal base URL
// (bypasses the operator-config registry); otherwise it is a config name.
let base_url = if server.contains("://") {
server.to_string()
} else {
let operator_config = operator::load_operator_config()?;
let Some(entry) = operator_config.servers.get(server) else {
let known = operator_config
.servers
.keys()
.map(String::as_str)
.collect::<Vec<_>>()
.join(", ");
color_eyre::eyre::bail!(
"unknown server '{server}' — servers defined in the operator config: [{known}] (add it under servers: in ~/.omnigraph/config.yaml)"
);
};
entry.url.clone()
};
let base = entry.url.trim_end_matches('/');
let base = base_url.trim_end_matches('/');
Ok(Some(match graph {
Some(graph) => format!("{base}/graphs/{graph}"),
None => base.to_string(),
@ -336,7 +344,7 @@ pub(crate) async fn execute_operator_alias(
) -> Result<ReadOutput> {
let uri = resolve_server_flag(Some(&alias.server), alias.graph.as_deref())?
.expect("server name is present");
let bearer_token = resolve_remote_bearer_token(config, Some(&uri), None)?;
let bearer_token = resolve_remote_bearer_token(config, Some(&uri))?;
let mut params = serde_json::Map::new();
for (key, value) in &alias.params {
@ -379,14 +387,13 @@ pub(crate) fn apply_server_flag(
server: Option<&str>,
graph: Option<&str>,
uri: Option<String>,
target: Option<&str>,
) -> Result<Option<String>> {
if server.is_none() {
return Ok(uri);
}
if uri.is_some() || target.is_some() {
if uri.is_some() {
color_eyre::eyre::bail!(
"--server is exclusive with a positional URI and --target — pick one way to address the graph"
"--server is exclusive with a positional URI — pick one way to address the graph"
);
}
resolve_server_flag(server, graph)
@ -448,28 +455,23 @@ pub(crate) async fn remote_json<T: DeserializeOwned>(
Ok(serde_json::from_str(&text)?)
}
pub(crate) fn resolve_uri(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
) -> Result<String> {
config.resolve_target_uri(cli_uri, cli_target, config.cli_graph_name())
pub(crate) fn resolve_uri(config: &OmnigraphConfig, cli_uri: Option<String>) -> Result<String> {
// `--target` is gone; the second arg (the legacy explicit-target name) is
// always None. A bare command still falls back to `cli.graph` (the third arg).
config.resolve_target_uri(cli_uri, None, config.cli_graph_name())
}
pub(crate) fn resolve_cli_graph(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
) -> Result<ResolvedCliGraph> {
let selected = if cli_uri.is_some() {
None
} else {
cli_target
.map(str::to_string)
.or_else(|| config.cli_graph_name().map(str::to_string))
config.cli_graph_name().map(str::to_string)
};
config.resolve_graph_selection(selected.as_deref())?;
let uri = resolve_uri(config, cli_uri, cli_target)?;
let uri = resolve_uri(config, cli_uri)?;
let normalized_uri = normalize_policy_graph_uri(&uri)?;
let graph_id = graph_resource_id_for_selection(selected.as_deref(), &normalized_uri);
Ok(ResolvedCliGraph {
@ -484,10 +486,9 @@ pub(crate) fn resolve_cli_graph(
pub(crate) fn resolve_local_graph(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
operation: &str,
) -> Result<ResolvedCliGraph> {
let graph = resolve_cli_graph(config, cli_uri, cli_target)?;
let graph = resolve_cli_graph(config, cli_uri)?;
if graph.is_remote {
bail!(
"`{}` is a direct (storage-native) command and needs direct storage \
@ -533,29 +534,27 @@ pub(crate) fn parse_duration_arg(s: &str) -> Result<std::time::Duration> {
pub(crate) fn resolve_local_uri(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
operation: &str,
) -> Result<String> {
Ok(resolve_local_graph(config, cli_uri, cli_target, operation)?.uri)
Ok(resolve_local_graph(config, cli_uri, operation)?.uri)
}
/// Resolve a storage-plane verb's target to a direct storage URI (RFC-010
/// Resolve a storage-plane verb's address to a direct storage URI (RFC-010
/// Slice 3). `--cluster <dir|uri> --cluster-graph <id>` resolves the graph's
/// storage URI from the **served cluster state** (the truth a `--cluster`
/// server serves); otherwise the ordinary positional-URI / `--target` path.
/// clap enforces both-or-neither and exclusion with `uri`/`--target`, so the
/// mismatched arm is defensive.
/// server serves); otherwise the ordinary positional-URI path.
/// clap enforces both-or-neither and exclusion with `uri`, so the mismatched
/// arm is defensive.
pub(crate) async fn resolve_storage_uri(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
cluster: Option<&str>,
cluster_graph: Option<&str>,
operation: &str,
) -> Result<String> {
match (cluster, cluster_graph) {
(Some(cluster), Some(graph_id)) => resolve_cluster_graph_uri(cluster, graph_id).await,
(None, None) => resolve_local_uri(config, cli_uri, cli_target, operation),
(None, None) => resolve_local_uri(config, cli_uri, operation),
_ => bail!("--cluster and --cluster-graph must be given together"),
}
}
@ -786,7 +785,6 @@ pub(crate) fn query_params_from_json(
pub(crate) async fn execute_query_lint(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
schema_path: Option<&PathBuf>,
query_path: &PathBuf,
) -> Result<QueryLintOutput> {
@ -808,13 +806,12 @@ pub(crate) async fn execute_query_lint(
));
}
let has_graph_target =
cli_uri.is_some() || cli_target.is_some() || config.cli_graph_name().is_some();
let has_graph_target = cli_uri.is_some() || config.cli_graph_name().is_some();
if !has_graph_target {
bail!("lint requires --schema <schema.pg> or a resolvable graph target");
}
let uri = resolve_local_uri(config, cli_uri, cli_target, "lint")?;
let uri = resolve_local_uri(config, cli_uri, "lint")?;
let db = Omnigraph::open(&uri).await?;
Ok(lint_query_file(
&db.catalog(),
@ -827,10 +824,9 @@ pub(crate) async fn execute_query_lint(
pub(crate) fn resolve_selected_graph(
config: &OmnigraphConfig,
cli_uri: Option<String>,
cli_target: Option<&str>,
operation: &str,
) -> Result<(String, Option<String>)> {
let graph = resolve_local_graph(config, cli_uri, cli_target, operation)?;
let graph = resolve_local_graph(config, cli_uri, operation)?;
Ok((graph.uri, graph.selected))
}
@ -860,11 +856,8 @@ pub(crate) fn graph_query_registry_names(config: &OmnigraphConfig) -> Vec<&str>
pub(crate) fn resolve_registry_selection_for_list(
config: &OmnigraphConfig,
target: Option<&str>,
) -> Result<Option<String>> {
let selected = target
.map(str::to_string)
.or_else(|| config.cli_graph_name().map(str::to_string));
let selected = config.cli_graph_name().map(str::to_string);
if let Some(name) = selected.as_deref() {
config.resolve_graph_selection(Some(name))?;
return Ok(selected);
@ -880,10 +873,9 @@ pub(crate) fn resolve_registry_selection_for_list(
}
bail!(
"stored-query registries are configured for graph{} {} but no graph was selected. Pass `--target {}` or set `cli.graph`.",
"stored-query registries are configured for graph{} {} but no graph was selected. Pass a positional URI or set `cli.graph`.",
if graph_names.len() == 1 { "" } else { "s" },
graph_names.join(", "),
graph_names[0],
)
}
@ -903,15 +895,12 @@ pub(crate) fn validate_registry_for_catalog(
pub(crate) async fn execute_queries_validate(
uri: Option<String>,
target: Option<String>,
config_path: Option<&PathBuf>,
json: bool,
) -> Result<()> {
let config = load_cli_config(config_path)?;
// One selection drives both the schema URI and the registry, so a
// positional URI and a `--target` can't validate different graphs.
let (uri, selected) =
resolve_selected_graph(&config, uri, target.as_deref(), "queries validate")?;
// One selection drives both the schema URI and the registry.
let (uri, selected) = resolve_selected_graph(&config, uri, "queries validate")?;
let registry = load_registry_or_report(&config, selected.as_deref())?;
let db = Omnigraph::open(&uri).await?;
let report = check(&registry, &db.catalog());
@ -961,13 +950,9 @@ pub(crate) async fn execute_queries_validate(
Ok(())
}
pub(crate) fn execute_queries_list(
target: Option<String>,
config_path: Option<&PathBuf>,
json: bool,
) -> Result<()> {
pub(crate) fn execute_queries_list(config_path: Option<&PathBuf>, json: bool) -> Result<()> {
let config = load_cli_config(config_path)?;
let selected = resolve_registry_selection_for_list(&config, target.as_deref())?;
let selected = resolve_registry_selection_for_list(&config)?;
let registry = load_registry_or_report(&config, selected.as_deref())?;
let output = QueriesListOutput {
@ -1090,6 +1075,22 @@ pub(crate) fn rewrite_deprecated_argv(args: Vec<OsString>) -> Vec<OsString> {
mod tests {
use super::*;
// RFC-011 Decision 2: `--server` accepts a literal URL (value with `://`),
// bypassing the operator-config registry — so no config / OMNIGRAPH_HOME is
// read on this path (hermetic).
#[test]
fn server_flag_accepts_a_literal_url() {
assert_eq!(
resolve_server_flag(Some("https://graph.example.com"), None).unwrap(),
Some("https://graph.example.com".to_string())
);
// trailing slash trimmed; `--graph` appends the multi-graph path.
assert_eq!(
resolve_server_flag(Some("https://graph.example.com/"), Some("knowledge")).unwrap(),
Some("https://graph.example.com/graphs/knowledge".to_string())
);
}
// `branch delete` interpolates the branch into the URL path. The composed
// path must be exactly `<base-path>/branches/<name>` with no empty `//`
// segment — an empty segment misses the

View file

@ -170,7 +170,6 @@ async fn main() -> Result<()> {
}
Command::Load {
uri,
target,
config,
data,
branch,
@ -184,7 +183,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -201,7 +199,6 @@ async fn main() -> Result<()> {
}
Command::Ingest {
uri,
target,
config,
data,
branch,
@ -220,7 +217,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -239,7 +235,6 @@ async fn main() -> Result<()> {
Command::Branch { command } => match command {
BranchCommand::Create {
uri,
target,
config,
from,
name,
@ -251,7 +246,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -266,7 +260,6 @@ async fn main() -> Result<()> {
}
BranchCommand::List {
uri,
target,
config,
json,
} => {
@ -276,7 +269,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -291,7 +283,6 @@ async fn main() -> Result<()> {
}
BranchCommand::Delete {
uri,
target,
config,
name,
json,
@ -302,7 +293,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -316,7 +306,6 @@ async fn main() -> Result<()> {
}
BranchCommand::Merge {
uri,
target,
config,
source,
into,
@ -328,7 +317,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -350,7 +338,6 @@ async fn main() -> Result<()> {
Command::Commit { command } => match command {
CommitCommand::List {
uri,
target,
config,
branch,
json,
@ -361,7 +348,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -374,7 +360,6 @@ async fn main() -> Result<()> {
}
CommitCommand::Show {
uri,
target,
config,
commit_id,
json,
@ -385,7 +370,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -400,14 +384,13 @@ async fn main() -> Result<()> {
Command::Schema { command } => match command {
SchemaCommand::Plan {
uri,
target,
config,
schema,
json,
allow_data_loss,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = resolve_local_uri(&config, uri, target.as_deref(), "schema plan")?;
let uri = resolve_local_uri(&config, uri, "schema plan")?;
let schema_source = fs::read_to_string(&schema)?;
let db = Omnigraph::open(&uri).await?;
let plan = db
@ -430,7 +413,6 @@ async fn main() -> Result<()> {
}
SchemaCommand::Apply {
uri,
target,
config,
schema,
json,
@ -442,7 +424,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -475,7 +456,6 @@ async fn main() -> Result<()> {
}
SchemaCommand::Show {
uri,
target,
config,
json,
} => {
@ -485,7 +465,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -499,7 +478,6 @@ async fn main() -> Result<()> {
},
Command::Lint {
uri,
target,
config,
query,
schema,
@ -507,30 +485,27 @@ async fn main() -> Result<()> {
} => {
let config = load_cli_config(config.as_ref())?;
let output =
execute_query_lint(&config, uri, target.as_deref(), schema.as_ref(), &query)
execute_query_lint(&config, uri, schema.as_ref(), &query)
.await?;
finish_query_lint(&output, json)?;
}
Command::Queries { command } => match command {
QueriesCommand::Validate {
uri,
target,
config,
json,
} => {
execute_queries_validate(uri, target, config.as_ref(), json).await?;
execute_queries_validate(uri, config.as_ref(), json).await?;
}
QueriesCommand::List {
target,
config,
json,
} => {
execute_queries_list(target, config.as_ref(), json)?;
execute_queries_list(config.as_ref(), json)?;
}
},
Command::Snapshot {
uri,
target,
config,
branch,
json,
@ -541,7 +516,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -555,7 +529,6 @@ async fn main() -> Result<()> {
}
Command::Export {
uri,
target,
config,
branch,
jsonl,
@ -568,7 +541,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -586,7 +558,6 @@ async fn main() -> Result<()> {
Command::Query {
uri,
legacy_uri,
target,
config,
alias,
query,
@ -644,23 +615,24 @@ async fn main() -> Result<()> {
let alias = resolve_alias(&config, alias.as_deref(), AliasCommand::Read)?;
let alias_name = alias.as_ref().map(|(name, _)| *name);
let alias_config = alias.as_ref().map(|(_, alias)| *alias);
let target_available = target.is_some()
|| alias_config
.and_then(|alias| alias.graph.as_deref())
.is_some()
|| config.cli_graph_name().is_some();
let alias_graph = alias_config.and_then(|alias| alias.graph.as_deref());
let target_available = alias_graph.is_some() || config.cli_graph_name().is_some();
let (legacy_uri, alias_args) =
normalize_legacy_alias_uri(legacy_uri, target_available, alias_name, alias_args);
let uri = uri.or(legacy_uri);
let target_name = target
.as_deref()
.or_else(|| alias_config.and_then(|alias| alias.graph.as_deref()));
// `--target` is gone; resolve an alias's legacy `graph` name to its
// URI (a positional URI still wins).
let uri = match uri.or(legacy_uri) {
Some(uri) => Some(uri),
None => match alias_graph {
Some(name) => Some(config.resolve_target_uri(None, Some(name), None)?),
None => None,
},
};
let client = client::GraphClient::resolve(
&config,
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target_name,
cli.profile.as_deref(),
cli.store.as_deref(),
)?;
@ -704,7 +676,6 @@ async fn main() -> Result<()> {
Command::Mutate {
uri,
legacy_uri,
target,
config,
alias,
query,
@ -723,23 +694,24 @@ async fn main() -> Result<()> {
let alias = resolve_alias(&config, alias.as_deref(), AliasCommand::Change)?;
let alias_name = alias.as_ref().map(|(name, _)| *name);
let alias_config = alias.as_ref().map(|(_, alias)| *alias);
let target_available = target.is_some()
|| alias_config
.and_then(|alias| alias.graph.as_deref())
.is_some()
|| config.cli_graph_name().is_some();
let alias_graph = alias_config.and_then(|alias| alias.graph.as_deref());
let target_available = alias_graph.is_some() || config.cli_graph_name().is_some();
let (legacy_uri, alias_args) =
normalize_legacy_alias_uri(legacy_uri, target_available, alias_name, alias_args);
let uri = uri.or(legacy_uri);
let target_name = target
.as_deref()
.or_else(|| alias_config.and_then(|alias| alias.graph.as_deref()));
// `--target` is gone; resolve an alias's legacy `graph` name to its
// URI (a positional URI still wins).
let uri = match uri.or(legacy_uri) {
Some(uri) => Some(uri),
None => match alias_graph {
Some(name) => Some(config.resolve_target_uri(None, Some(name), None)?),
None => None,
},
};
let client = client::GraphClient::resolve_with_policy(
&config,
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target_name,
cli.as_actor.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
@ -820,18 +792,16 @@ async fn main() -> Result<()> {
},
Command::Optimize {
uri,
target,
config,
cluster,
cluster_graph,
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = if uri.is_some() || target.is_some() || cluster.is_some() {
let uri = if uri.is_some() || cluster.is_some() {
resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"optimize",
@ -849,13 +819,11 @@ async fn main() -> Result<()> {
server: None,
graph: cli.graph.as_deref(),
uri: None,
target: None,
},
)?;
resolve_storage_uri(
&config,
scope.uri,
scope.target.as_deref(),
scope.cluster.as_deref(),
scope.cluster_graph.as_deref(),
"optimize",
@ -896,7 +864,6 @@ async fn main() -> Result<()> {
}
Command::Repair {
uri,
target,
config,
cluster,
cluster_graph,
@ -905,11 +872,10 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = if uri.is_some() || target.is_some() || cluster.is_some() {
let uri = if uri.is_some() || cluster.is_some() {
resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"repair",
@ -926,13 +892,11 @@ async fn main() -> Result<()> {
server: None,
graph: cli.graph.as_deref(),
uri: None,
target: None,
},
)?;
resolve_storage_uri(
&config,
scope.uri,
scope.target.as_deref(),
scope.cluster.as_deref(),
scope.cluster_graph.as_deref(),
"repair",
@ -1014,7 +978,6 @@ async fn main() -> Result<()> {
}
Command::Cleanup {
uri,
target,
config,
cluster,
cluster_graph,
@ -1024,11 +987,10 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri = if uri.is_some() || target.is_some() || cluster.is_some() {
let uri = if uri.is_some() || cluster.is_some() {
resolve_storage_uri(
&config,
uri,
target.as_deref(),
cluster.as_deref(),
cluster_graph.as_deref(),
"cleanup",
@ -1045,13 +1007,11 @@ async fn main() -> Result<()> {
server: None,
graph: cli.graph.as_deref(),
uri: None,
target: None,
},
)?;
resolve_storage_uri(
&config,
scope.uri,
scope.target.as_deref(),
scope.cluster.as_deref(),
scope.cluster_graph.as_deref(),
"cleanup",
@ -1182,7 +1142,6 @@ async fn main() -> Result<()> {
Command::Graphs { command } => match command {
GraphsCommand::List {
uri,
target,
config,
json,
} => {
@ -1192,7 +1151,6 @@ async fn main() -> Result<()> {
cli.server.as_deref(),
cli.graph.as_deref(),
uri,
target.as_deref(),
cli.profile.as_deref(),
cli.store.as_deref(),
)?;

View file

@ -209,13 +209,7 @@ cli:
let config = load_config(Some(&config_path)).unwrap();
assert_eq!(
resolve_remote_bearer_token(&config, None, Some("demo"))
.unwrap()
.as_deref(),
Some("scoped-token")
);
assert_eq!(
resolve_remote_bearer_token(&config, Some("https://override.example.com"), None)
resolve_remote_bearer_token(&config, Some("https://override.example.com"))
.unwrap()
.as_deref(),
Some("global-token")
@ -369,12 +363,16 @@ graphs:
uri: s3://bucket/prod-graph/
policy:
file: ./prod-policy.yaml
cli:
graph: prod
"#,
)
.unwrap();
let config = load_config(Some(&config_path)).unwrap();
let graph = resolve_cli_graph(&config, None, Some("prod")).unwrap();
// `--target` is removed; the `cli.graph` default drives the same
// graph-key (not project name / URI) selection.
let graph = resolve_cli_graph(&config, None).unwrap();
assert_eq!(graph.selected(), Some("prod"));
assert_eq!(graph.graph_id, "prod");
assert_eq!(graph.uri, "s3://bucket/prod-graph/");
@ -405,7 +403,6 @@ cli:
let local_graph = resolve_cli_graph(
&config,
Some(format!("file://{}", local_graph_path.display())),
None,
)
.unwrap();
assert_eq!(local_graph.selected(), None);
@ -418,7 +415,6 @@ cli:
let s3_graph = resolve_cli_graph(
&config,
Some("s3://bucket/anonymous-graph/".to_string()),
None,
)
.unwrap();
assert_eq!(s3_graph.selected(), None);

View file

@ -66,7 +66,7 @@ pub(crate) fn build_report(config: &OmnigraphConfig, source: &Path) -> MigrateRe
if config.cli.graph.is_some() {
dropped.push(DroppedKey {
key: "cli.graph".into(),
reason: "no operator default-target yet — address graphs explicitly via --target/--server (RFC-002 locator territory)".into(),
reason: "address graphs explicitly via --store/--server, or set defaults.default_graph in the operator config".into(),
});
}
if config.cli.branch.is_some() {

View file

@ -192,11 +192,9 @@ pub(crate) fn guard_addressing(cli: &Cli) -> Result<()> {
}
let label = command_label(&cli.command);
let how = match capability {
// `init` is the one direct verb with no `--target` today (it takes a
// required positional URI), so its remediation drops the `--target` half.
Capability::Direct => match cli.command {
Command::Init { .. } => "Pass a storage URI.",
_ => "Use --target <name>, a storage URI, or --cluster <dir> --cluster-graph <id>.",
_ => "Pass a storage URI, or --cluster <dir> --cluster-graph <id>.",
},
Capability::Control => "It operates on a cluster (pass --config <dir>).",
Capability::Local => "It does not address a graph.",
@ -234,6 +232,9 @@ mod tests {
let cap = |args: &[&str]| {
command_capability(&Cli::try_parse_from(args).unwrap().command)
};
// The one Data→Served refinement — if the `graphs` guard were deleted,
// every other assertion here would still pass.
assert_eq!(cap(&["omnigraph", "graphs", "list"]), Capability::Served);
assert_eq!(cap(&["omnigraph", "optimize", "graph.omni"]), Capability::Direct);
assert_eq!(cap(&["omnigraph", "schema", "plan", "--schema", "s.pg", "graph.omni"]), Capability::Direct);
assert_eq!(cap(&["omnigraph", "cluster", "status", "--config", "."]), Capability::Control);

View file

@ -32,24 +32,22 @@ pub(crate) struct ResolvedScope {
pub(crate) server: Option<String>,
pub(crate) graph: Option<String>,
pub(crate) uri: Option<String>,
pub(crate) target: Option<String>,
pub(crate) cluster: Option<String>,
pub(crate) cluster_graph: Option<String>,
}
/// The raw addressing inputs for one command: the global scope flags plus the
/// command's own positional/`--target` address.
/// command's own positional URI.
pub(crate) struct ScopeFlags<'a> {
pub(crate) profile: Option<&'a str>,
pub(crate) store: Option<&'a str>,
pub(crate) server: Option<&'a str>,
pub(crate) graph: Option<&'a str>,
pub(crate) uri: Option<String>,
pub(crate) target: Option<&'a str>,
}
/// Resolve the scope for a command with `capability`. Precedence (RFC-011):
/// 1. explicit legacy/primitive address (`uri`/`target`/`--server`/`--store`) → passthrough;
/// 1. explicit primitive address (`uri`/`--server`/`--store`) → passthrough;
/// 2. `--profile` / `OMNIGRAPH_PROFILE`;
/// 3. flat `defaults.server` + `defaults.default_graph`;
/// 4. nothing — downstream behaves as today.
@ -58,15 +56,21 @@ pub(crate) fn resolve_scope(
capability: Capability,
flags: ScopeFlags<'_>,
) -> Result<ResolvedScope> {
// `--store` is its own way to address a graph; combining it with a positional
// URI or `--server` is a contradiction, not a silent precedence.
if flags.store.is_some() && (flags.uri.is_some() || flags.server.is_some()) {
bail!(
"--store is exclusive with a positional URI and --server — pick one way to \
address the graph"
);
}
// 1. Any explicit address wins; reproduce today's behavior untouched.
// `--store` is an explicit store URI — fold it into `uri`.
if flags.uri.is_some() || flags.target.is_some() || flags.server.is_some() || flags.store.is_some()
{
if flags.uri.is_some() || flags.server.is_some() || flags.store.is_some() {
return Ok(ResolvedScope {
server: flags.server.map(str::to_string),
graph: flags.graph.map(str::to_string),
uri: flags.store.map(str::to_string).or(flags.uri),
target: flags.target.map(str::to_string),
..Default::default()
});
}
@ -190,7 +194,6 @@ mod tests {
server: None,
graph: None,
uri: None,
target: None,
}
}
@ -226,6 +229,26 @@ mod tests {
assert_eq!(scope.uri.as_deref(), Some("s3://b/g.omni"));
}
#[test]
fn store_is_exclusive_with_positional_uri_and_server() {
let op = OperatorConfig::default();
for flags in [
ScopeFlags {
store: Some("s3://b/g.omni"),
uri: Some("file://other.omni".into()),
..flags()
},
ScopeFlags {
store: Some("s3://b/g.omni"),
server: Some("prod"),
..flags()
},
] {
let err = resolve_scope(&op, Capability::Any, flags).unwrap_err().to_string();
assert!(err.contains("--store is exclusive"), "{err}");
}
}
#[test]
fn flat_default_server_drives_data_verbs() {
let op = cfg("defaults:\n server: prod\n default_graph: knowledge\nservers:\n prod:\n url: https://x\n");

View file

@ -166,7 +166,7 @@ fn optimize_with_server_flag_errors_wrong_plane() {
assert!(
stderr.contains("`optimize` is a direct (storage-native) command")
&& stderr.contains("--server/--graph address a served graph and do not apply")
&& stderr.contains("Use --target <name>, a storage URI, or --cluster <dir> --cluster-graph <id>."),
&& stderr.contains("Pass a storage URI, or --cluster <dir> --cluster-graph <id>."),
"wrong-capability guard message not found; got: {stderr}"
);
}
@ -1279,6 +1279,48 @@ fn read_supports_inline_query_string() {
assert_eq!(payload["rows"][0]["p.name"], "Alice");
}
#[test]
fn positional_http_uri_on_a_data_verb_is_rejected() {
// RFC-011: a positional/`--uri` http(s):// URL no longer dispatches to a
// remote server — that requires `--server <url>`.
let output = output_failure(
cli()
.arg("query")
.arg("http://127.0.0.1:1")
.arg("-e")
.arg("query q() { match { $p: Person { } } return { $p } }"),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("must be addressed with `--server <url>`"),
"expected positional-remote rejection; got: {stderr}"
);
}
#[test]
fn as_on_a_served_write_is_rejected() {
// RFC-011: a served write resolves the actor from the bearer token, so --as
// cannot set identity. It errors while building the remote client — before
// any HTTP call, so no server is needed.
let output = output_failure(
cli()
.arg("mutate")
.arg("--server")
.arg("http://127.0.0.1:1")
.arg("--as")
.arg("act-nope")
.arg("-e")
.arg("query add($name: String) { insert Person { name: $name } }")
.arg("--params")
.arg(r#"{"name":"X"}"#),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("`--as` is not allowed on a served write"),
"expected --as-served rejection; got: {stderr}"
);
}
#[test]
fn change_supports_inline_query_string() {
let temp = tempdir().unwrap();

View file

@ -323,7 +323,7 @@ fn queries_list_requires_graph_selection_for_per_graph_only_registries() {
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("local") && stderr.contains("--target local"),
stderr.contains("local") && stderr.contains("set `cli.graph`"),
"error must name the graph and give a concrete selection hint; stderr:\n{stderr}"
);
}
@ -357,12 +357,12 @@ fn queries_list_without_graph_selection_lists_top_level_registry() {
}
#[test]
fn queries_list_unknown_target_errors() {
fn queries_list_unknown_cli_graph_errors() {
// `queries list` opens no graph URI, so unknown-graph validation can't ride
// along on URI resolution the way it does for every other command. An
// unknown `--target` must still error (naming the graph) instead of
// silently falling back to the top-level registry and showing the wrong
// (or empty) catalog.
// unknown `cli.graph` selection must still error (naming the graph) instead
// of silently falling back to the top-level registry and showing the wrong
// (or empty) catalog. (`--target` was removed; `cli.graph` drives selection.)
let graph = SystemGraph::loaded();
graph.write_query(
"find_person.gq",
@ -370,21 +370,12 @@ fn queries_list_unknown_target_errors() {
);
let config = graph.write_config(
"omnigraph.yaml",
&queries_test_config(
&graph.path().to_string_lossy(),
"find_person",
"find_person.gq",
&format!(
"graphs:\n local:\n uri: '{}'\n queries:\n find_person:\n file: ./find_person.gq\ncli:\n graph: nonexistent\npolicy: {{}}\n",
graph.path().to_string_lossy().replace('\'', "''"),
),
);
let output = output_failure(
cli()
.arg("queries")
.arg("list")
.arg("--target")
.arg("nonexistent")
.arg("--config")
.arg(&config),
);
let output = output_failure(cli().arg("queries").arg("list").arg("--config").arg(&config));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("nonexistent"),

View file

@ -121,7 +121,7 @@ fn schema_plan_with_server_flag_errors_wrong_plane() {
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("`schema plan` is a direct (storage-native) command")
&& stderr.contains("Use --target <name>, a storage URI, or --cluster <dir> --cluster-graph <id>."),
&& stderr.contains("Pass a storage URI, or --cluster <dir> --cluster-graph <id>."),
"schema plan wrong-capability message not found; got: {stderr}"
);
}

View file

@ -831,8 +831,18 @@ pub fn run_both_with_config(
server_url: &str,
args: &[&str],
) -> (std::process::Output, std::process::Output) {
// Address both arms with GLOBAL flags (`--store` / `--server`) appended after
// the verb + its args, so the address is placed correctly regardless of
// subcommand nesting (a positional graph only works for top-level verbs;
// `schema show <graph>` etc. need the global flag). Local = embedded store,
// remote = served.
let mut local = cli();
local.arg(args[0]).arg(local_graph).args(&args[1..]).arg("--as").arg(PARITY_ACTOR);
local
.args(args)
.arg("--store")
.arg(local_graph)
.arg("--as")
.arg(PARITY_ACTOR);
if let Some(config) = local_config {
local.arg("--config").arg(config);
}
@ -841,9 +851,9 @@ pub fn run_both_with_config(
let mut remote = cli();
remote
.env("OMNIGRAPH_BEARER_TOKEN", PARITY_TOKEN)
.arg(args[0])
.arg(server_url)
.args(&args[1..]);
.args(args)
.arg("--server")
.arg(server_url);
let remote_out = remote.output().unwrap();
(local_out, remote_out)
}

View file

@ -2339,6 +2339,7 @@ fn local_cli_keyed_credentials_authenticate_url_matched_server() {
}
command
.arg("read")
.arg("--server")
.arg(&server.base_url)
.arg("--query")
.arg(fixture("test.gq"))

View file

@ -60,14 +60,16 @@ Read through the HTTP API:
```bash
omnigraph query \
--target http://127.0.0.1:8080 \
--server http://127.0.0.1:8080 \
--query queries.gq \
--name get_person \
--params '{"name":"Alice"}'
```
If the server requires auth, set `OMNIGRAPH_SERVER_BEARER_TOKEN` on the server
and configure the matching `bearer_token_env` in `omnigraph.yaml`.
A server is addressed with `--server` (a name from `~/.omnigraph/config.yaml` or a
literal URL); a positional `http(s)://` URI is rejected. If the server requires
auth, set its bearer token and `omnigraph login <server>` (or
`OMNIGRAPH_BEARER_TOKEN`).
## Multi-graph servers (v0.6.0+)

View file

@ -2,7 +2,7 @@
A reference for the `omnigraph` binary's command surface and `omnigraph.yaml` schema. For a quick-start guide, see [cli.md](index.md).
Top-level command families and subcommands. Graph-targeting commands accept a positional `URI`, `--uri`, a `--target <name>` resolved against `omnigraph.yaml`, `--server <name>` (an operator-defined server from `~/.omnigraph/config.yaml`, optionally with `--graph <id>` for multi-graph servers; exclusive with the other forms), `--store <uri>` (a single graph's storage directly), or `--profile <name>` / `$OMNIGRAPH_PROFILE` (a named scope bundle; see [Scopes & profiles](#scopes--profiles-rfc-011)); `cluster` commands use `--config <dir>`.
Top-level command families and subcommands. Graph-targeting commands accept a positional `file://`/`s3://` URI, `--server <name|url>` (an operator-defined server from `~/.omnigraph/config.yaml` by name, or a literal `http(s)://` URL, optionally with `--graph <id>` for multi-graph servers; exclusive with a positional URI), `--store <uri>` (a single graph's storage directly), or `--profile <name>` / `$OMNIGRAPH_PROFILE` (a named scope bundle; see [Scopes & profiles](#scopes--profiles-rfc-011)); `cluster` commands use `--config <dir>`. A remote server is addressed only with `--server` — a positional `http(s)://` URI is rejected.
## Top-level commands
@ -32,19 +32,20 @@ Top-level command families and subcommands. Graph-targeting commands accept a po
Every command declares the **capability** it needs — what it requires to reach a graph — which determines the addressing flags that apply:
- **`any`** — `query`, `mutate`, `load`, `ingest`, `branch *`, `snapshot`, `export`, `commit *`, `schema show`, `schema apply`. Run against a graph **served (via a server) or embedded (direct against a store)**: accept a positional `URI` / `--target` / `--server` (+ `--graph` for multi-graph servers) / `--store` / `--profile`.
- **`any`** — `query`, `mutate`, `load`, `ingest`, `branch *`, `snapshot`, `export`, `commit *`, `schema show`, `schema apply`. Run against a graph **served (via a server) or embedded (direct against a store)**: accept a positional `file://`/`s3://` URI, `--server <name|url>` (+ `--graph <id>` for multi-graph servers), `--store <uri>`, or `--profile <name>`. A remote server is addressed with `--server` — a positional `http(s)://` URI does **not** dispatch to one.
- **`served`** — `graphs list`. Requires a server (accepts `--server` / `--profile`).
- **`direct`** — `init`, `optimize`, `repair`, `cleanup`, `schema plan`, `queries validate`, `lint`. Need **direct storage access** (`file://` / `s3://`), never through a server. They accept a positional `URI` or `--target`, but **not** `--server` / `--graph`, and a `--target` that resolves to a remote (`http(s)://`) server is rejected. (`init` takes only a positional `URI` today — no `--target`.) `optimize` / `repair` / `cleanup` also accept **`--cluster <dir|s3://…> --cluster-graph <id>`**, which resolves the graph's storage URI from the served cluster state (so you needn't know the `<storage>/graphs/<id>.omni` layout).
- **`direct`** — `init`, `optimize`, `repair`, `cleanup`, `schema plan`, `queries validate`, `lint`. Need **direct storage access** (`file://` / `s3://`), never through a server. They accept a positional `URI`, but **not** `--server` / `--graph`, and a remote (`http(s)://`) URI is rejected. `optimize` / `repair` / `cleanup` also accept **`--cluster <dir|s3://…> --cluster-graph <id>`**, which resolves the graph's storage URI from the served cluster state (so you needn't know the `<storage>/graphs/<id>.omni` layout).
- **`control`** — `cluster *`. Operates on a cluster directory via `--config <dir>`.
- **`local`** — `policy *`, `embed`, `login`, `logout`, `config`, `version`, `queries list`. Address no graph.
These restrictions are enforced and reported, not silent:
- A served-graph flag (`--server` / `--graph`) on a verb that doesn't reach a graph through a server fails loudly, e.g.: ``optimize is a direct (storage-native) command; --server/--graph address a served graph and do not apply. Use --target <name>, a storage URI, or --cluster <dir> --cluster-graph <id>.``
- A `direct` verb pointed at a remote target fails loudly, e.g.: ``optimize is a direct (storage-native) command and needs direct storage access; the resolved target is a remote server (https://…). Pass the graph's file:// or s3:// URI.``
- A served-graph flag (`--server` / `--graph`) on a verb that doesn't reach a graph through a server fails loudly, e.g.: ``optimize is a direct (storage-native) command; --server/--graph address a served graph and do not apply. Pass a storage URI, or --cluster <dir> --cluster-graph <id>.``
- A `direct` verb pointed at a remote URI fails loudly, e.g.: ``optimize is a direct (storage-native) command and needs direct storage access; the resolved target is a remote server (https://…). Pass the graph's file:// or s3:// URI.``
- A data verb pointed at a positional `http(s)://` URI fails loudly: ``a remote graph must be addressed with --server <url> — a positional (or --uri) http(s):// URL no longer dispatches to a server.``
- `init` into an **established cluster's** storage layout (`<root>/graphs/<id>.omni` where `<root>` holds `__cluster/state.json`) is refused — graphs in a cluster are created by `cluster apply` (which records ledger / recovery / approvals), not `init`.
To maintain a server-backed graph, run the `direct` verbs from a host with storage access against the graph's storage URI (`--target`, or `--cluster … --cluster-graph …`), out-of-band from the serving process — there are no server routes for `optimize` / `repair` / `cleanup` by design.
To maintain a server-backed graph, run the `direct` verbs from a host with storage access against the graph's storage URI (a positional URI, or `--cluster … --cluster-graph …`), out-of-band from the serving process — there are no server routes for `optimize` / `repair` / `cleanup` by design.
`omnigraph --help` lists commands with a **capability legend** at the bottom (any / served / direct / control / local).
@ -92,7 +93,7 @@ newer CLI works on an older one). `$OMNIGRAPH_CONFIG=<path>` stands in for
A command resolves a **scope** — a server, a cluster, or a store — then selects a
graph in it; the served-vs-direct access path is derived from the scope, not
toggled. The scope comes from one of (highest precedence first): an explicit
address (a positional URI, `--target`, `--server`, or `--store <uri>`); a named
address (a positional URI, `--server`, or `--store <uri>`); a named
`--profile <name>` (or `$OMNIGRAPH_PROFILE`); or the flat `defaults.server` +
`defaults.default_graph`. A **profile** binds exactly one of `server` / `cluster`
/ `store` plus an optional default graph — config data, not state: every command
@ -105,9 +106,9 @@ resolves its scope fresh, there is no sticky "current" mode.
- A `server`-bound scope on a maintenance verb, or a `cluster`-bound scope on a
data verb, is rejected with a message pointing at the right addressing.
This model **coexists** with the legacy addressing (`--uri` / `--target` /
`--cluster-graph` / `omnigraph.yaml`) — nothing is removed yet; an explicit legacy
address always wins.
`--target` and the positional-`http(s)://`→remote dispatch have been **removed**;
the remaining legacy surfaces (`--cluster-graph`, `omnigraph.yaml`'s `cli.graph`
default) still work and an explicit address always wins.
#### Credentials keyed by server name

View file

@ -51,9 +51,9 @@ The exact contract:
implicit current-directory search runs (mode-inference rule 0). Boot from
cluster state XOR `omnigraph.yaml`, never a merge.
- **The other direction is ergonomics, not coupling**: a per-operator
`omnigraph.yaml` may point `graphs.<name>.uri` at a cluster's derived root
(`company-brain/graphs/knowledge.omni`) so data-plane commands can use
`--target <name>` — an ordinary local path, no special handling.
data-plane commands address a cluster graph by its derived storage root
(`company-brain/graphs/knowledge.omni`) with `--store <uri>` — an ordinary
local path, no special handling.
## Supported `cluster.yaml`

View file

@ -246,10 +246,10 @@ with an in-flight apply.
human step by design — keep `cluster approve` out of automation.
- **`omnigraph.yaml` still has a job**: per-operator settings — your
`cli.actor` default for `--as`, CLI defaults, credentials, and data-plane
ergonomics (point `graphs.<name>.uri` at a derived root like
`company-brain/graphs/knowledge.omni` to use `--target <name>` for
loads). It just no longer describes the deployment — a server boots from
one source or the other, never a merge of both.
ergonomics (address a cluster graph by its derived root like
`company-brain/graphs/knowledge.omni` with `--store` for loads). It just no
longer describes the deployment — a server boots from one source or the
other, never a merge of both.
## 7. Maintaining a cluster graph

View file

@ -1,6 +1,6 @@
# Maintenance: Optimize, Repair & Cleanup
**Addressing.** `optimize`, `repair`, and `cleanup` are **direct** (storage-native) CLI commands: they run with direct storage access against a positional `URI`, `--target`, or **`--cluster <dir|s3://…> --cluster-graph <id>`** (which resolves the graph's storage URI from the served cluster state, so you needn't know the `<storage>/graphs/<id>.omni` layout). They never run through a server, and reject `--server` / `--graph` or a `--target` that resolves to a remote (`http(s)://`) URL with a declared error. There are no server routes for them by design — to maintain a server-backed graph, run them out-of-band against the graph's storage URI. See the *Command capabilities* section of [cli-reference.md](../cli/reference.md).
**Addressing.** `optimize`, `repair`, and `cleanup` are **direct** (storage-native) CLI commands: they run with direct storage access against a positional `file://`/`s3://` URI or **`--cluster <dir|s3://…> --cluster-graph <id>`** (which resolves the graph's storage URI from the served cluster state, so you needn't know the `<storage>/graphs/<id>.omni` layout). They never run through a server, and reject `--server` / `--graph` or a remote (`http(s)://`) URI with a declared error. There are no server routes for them by design — to maintain a server-backed graph, run them out-of-band against the graph's storage URI. See the *Command capabilities* section of [cli-reference.md](../cli/reference.md).
## `optimize` — non-destructive

View file

@ -105,7 +105,7 @@ is validated/tested/explained as the anonymous policy.
- `omnigraph policy validate` — parse + count actors, exit 1 on parse error.
- `omnigraph policy test` — run cases in `policy.tests.yaml`, exit 1 on any expectation mismatch.
- `omnigraph policy explain --actor … --action … [--branch …] [--target-branch …]` — show decision and matched rule.
- `omnigraph --as <ACTOR> <subcommand>` — set the actor for the duration of one invocation. Effective for `change`, `load` (and its deprecated `ingest` alias), `branch create|delete|merge`, and `schema apply` against local URIs. No-op against remote HTTP URIs (actor is bearer-token-resolved server-side).
- `omnigraph --as <ACTOR> <subcommand>` — set the actor for the duration of one invocation. Effective for `change`, `load` (and its deprecated `ingest` alias), `branch create|delete|merge`, and `schema apply` against a direct (`--store`) graph. **Rejected** on a served write (`--server`): the actor is bearer-token-resolved server-side, so `--as` can't set it there.
## Enforcement

View file

@ -23,7 +23,7 @@ list/`Blob` columns → none.
> **Coverage and cost.** Each indexed column adds index files and build time, and
> an index only covers the fragments it was built over. Rows appended after the
> index was built (e.g. by `ingest --mode merge`) are scanned unindexed until a
> reindex extends coverage; see [maintenance](maintenance.md) → `optimize`.
> reindex extends coverage; see [maintenance](../operations/maintenance.md) → `optimize`.
## L2 — OmniGraph orchestration