Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/omnigraph-api-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ pub struct InvokeStoredQueryRequest {
/// mutation). Mutually exclusive with `branch`.
#[serde(default)]
pub snapshot: Option<String>,
/// The kind the caller expects (RFC-011 Decision 3): `Some(false)` for
/// The kind the caller expects: `Some(false)` for
/// `omnigraph query <name>`, `Some(true)` for `omnigraph mutate <name>`.
/// When set and it disagrees with the stored query's actual kind, the
/// server rejects the call (400) so the verb asserts the kind. `None`
Expand Down Expand Up @@ -593,7 +593,7 @@ pub struct ManifestConflictOutput {
pub actual: u64,
}

/// Structured authority mismatch for an RFC-022 prepared write. Values are
/// Structured authority mismatch for a prepared write. Values are
/// strings because members include optional graph commit ids and future
/// authority tokens, not only numeric table versions.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
Expand Down
35 changes: 16 additions & 19 deletions crates/omnigraph-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) const DEFAULT_BEARER_TOKEN_ENV: &str = "OMNIGRAPH_BEARER_TOKEN";
COMMANDS BY CAPABILITY:\n \
any — run against a graph, served (--server / --profile) or embedded (--store / a \
URI): query, mutate, load, branch, snapshot, export, commit, schema show/apply.\n \
served — require a server: graphs.\n \
served — require a server (registry scope, --server/--profile only): graphs.\n \
direct — direct storage access; reject --server (init, optimize, repair, cleanup, \
schema plan, lint).\n \
control — manage or inspect a cluster (cluster via --config; policy & queries via \
Expand Down Expand Up @@ -44,21 +44,21 @@ pub(crate) struct Cli {
#[arg(long, global = true, value_name = "GRAPH_ID")]
pub(crate) graph: Option<String>,

/// Select a named scope bundle (RFC-011) from `profiles:` in
/// Select a named scope bundle from `profiles:` in
/// ~/.omnigraph/config.yaml: fills in this command's omitted addressing
/// (server/cluster/store + default graph). Falls back to
/// $OMNIGRAPH_PROFILE. Config data, not state — every command resolves
/// scope fresh.
#[arg(long, global = true, value_name = "NAME")]
pub(crate) profile: Option<String>,

/// Address a single graph's storage directly (RFC-011): a `file://` /
/// Address a single graph's storage directly: a `file://` /
/// `s3://` store URI. Explicit, ad-hoc direct access — bypasses any
/// server. Exclusive with a positional URI / `--server`.
#[arg(long, global = true, value_name = "URI")]
pub(crate) store: Option<String>,

/// Address a cluster-managed graph's storage for maintenance (RFC-011):
/// Address a cluster-managed graph's storage for maintenance:
/// a cluster directory or storage-root URI — named via `clusters:` in
/// ~/.omnigraph/config.yaml, or a literal `file://`/`s3://` root. Pair
/// with `--graph <id>` to select the graph. Used by optimize / repair /
Expand All @@ -67,14 +67,14 @@ pub(crate) struct Cli {
pub(crate) cluster: Option<String>,

/// Skip the confirmation prompt for a destructive write (`cleanup`,
/// overwrite `load`, `branch delete`) against a non-local scope (RFC-011
/// Decision 9). Without it, a non-local destructive write prompts on a TTY
/// overwrite `load`, `branch delete`) against a non-local scope.
/// Without it, a non-local destructive write prompts on a TTY
/// and refuses (errors) when there is no TTY or `--json` is set.
#[arg(long, global = true)]
pub(crate) yes: bool,

/// Suppress the one-line resolved-write-target diagnostic that write
/// commands echo to stderr (RFC-011 Decision 9).
/// commands echo to stderr.
#[arg(long, global = true)]
pub(crate) quiet: bool,

Expand Down Expand Up @@ -137,7 +137,7 @@ pub(crate) enum Command {
#[arg(long)]
json: bool,
},
/// Invoke an operator alias (RFC-011 Decision 4).
/// Invoke an operator alias.
///
/// An alias is a personal binding under `aliases:` in
/// ~/.omnigraph/config.yaml — name → (server, graph, stored-query name,
Expand Down Expand Up @@ -230,7 +230,7 @@ pub(crate) enum Command {
#[command(subcommand)]
command: SchemaCommand,
},
/// Manage graphs on a multi-graph server (MR-668)
/// Manage graphs on a multi-graph server
Graphs {
#[command(subcommand)]
command: GraphsCommand,
Expand All @@ -246,7 +246,7 @@ pub(crate) enum Command {
/// Overwrite existing schema artifacts at the URI. Without
/// this flag, init refuses to touch a URI that already holds
/// `_schema.pg`, `_schema.ir.json`, or `__schema_state.json`
/// — closes the re-init footgun (MR-668 follow-up). With the
/// — closes the re-init footgun. With the
/// flag, the operator opts in to destructive semantics.
#[arg(long)]
force: bool,
Expand Down Expand Up @@ -300,8 +300,7 @@ pub(crate) enum Command {
/// shim that prints a one-line stderr warning and rewrites to
/// `omnigraph lint`. Aliases are deliberately *not* exposed via
/// clap's `visible_alias` because that would advertise two
/// equivalent canonical names, which agents emit interchangeably
/// (see MR-981).
/// equivalent canonical names, which agents emit interchangeably.
Lint {
/// Graph URI
uri: Option<String>,
Expand Down Expand Up @@ -463,17 +462,15 @@ pub(crate) enum ClusterCommand {

/// Operations on the graph registry of a multi-graph server (MR-668).
///
/// All operations target a remote multi-graph server URL (http:// or
/// https://). Local-URI invocations return a clear error. To add or
/// remove graphs, operators edit `omnigraph.yaml` directly and restart
/// the server — runtime mutation is not exposed in v0.6.0.
/// Registry scope (RFC-011): these address the server itself, not a graph
/// within it — `--server <name|url>` / `--profile <name>` apply, while
/// `--graph`, `--store`, and `--as` are rejected by the addressing guard.
/// To add or remove graphs, operators run `cluster apply` and restart the
/// server — runtime mutation is not exposed.
#[derive(Debug, Subcommand)]
pub(crate) enum GraphsCommand {
/// List every graph registered with the multi-graph server.
List {
/// Remote server URL (e.g. `https://server.example.com`).
#[arg(long)]
uri: Option<String>,
#[arg(long)]
json: bool,
},
Expand Down
118 changes: 97 additions & 21 deletions crates/omnigraph-cli/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,7 @@ async fn require_graph_for_multi_graph_server(
let (Some(server), None) = (scope.server.as_deref(), scope.graph.as_deref()) else {
return Ok(());
};
let Some(base) = resolve_server_flag(Some(server), None)? else {
return Ok(());
};
let token = resolve_remote_bearer_token(Some(&base))?;
let probe = GraphClient::Remote {
http: build_http_client()?,
base_url: base,
token,
};
let probe = GraphClient::registry_client(server)?;
if let Ok(resp) = probe.list_graphs().await {
if !resp.graphs.is_empty() {
let ids: Vec<&str> = resp.graphs.iter().map(|g| g.graph_id.as_str()).collect();
Expand Down Expand Up @@ -116,13 +108,72 @@ fn reject_positional_remote(via_server: bool, uri: &str) -> Result<()> {
}

impl GraphClient {
/// The single owner of registry (`GET /graphs`) addressing: the bare base
/// URL of `server` (a config name or literal URL) — never `/graphs/<id>`
/// — with the keyed bearer-token chain. Synchronous: pure config
/// resolution, no I/O. Used by the RFC-011 D7 multi-graph probe and the
/// `graphs list` registry factory.
fn registry_client(server: &str) -> Result<Self> {
let base = resolve_server_flag(Some(server), None)?
.expect("server name is present");
let token = resolve_remote_bearer_token(Some(&base))?;
Ok(GraphClient::Remote {
http: build_http_client()?,
base_url: base,
token,
})
}

/// Served-REGISTRY factory (RFC-011): resolve a server scope (`--server`
/// / `--profile` / `defaults.server`) to the bare server base URL for
/// `graphs list`. Synchronous by design: the RFC-011 D7 multi-graph probe
/// (`require_graph_for_multi_graph_server`) is async, so it structurally
/// cannot run on this path — `graphs list` IS the enumeration the probe
/// performs. There is no graph selection and no `/graphs/<id>` append; a
/// scope's `default_graph` is deliberately ignored (rejecting a config
/// default would make `graphs list` unusable in any profile that sets
/// one, and the registry is server-scoped either way). An explicit
/// `--graph` never reaches here — the addressing guard rejects it.
pub(crate) fn resolve_registry(
server: Option<&str>,
profile: Option<&str>,
) -> Result<Self> {
let scope = crate::scope::resolve_scope(
&crate::operator::load_operator_config()?,
crate::planes::Capability::Served,
crate::scope::ScopeFlags {
profile,
store: None,
server,
cluster: None,
graph: None,
uri: None,
},
)?;
let Some(server) = scope.server.as_deref() else {
bail!(
"`graphs list` needs a server scope — pass --server <name|url> or \
--profile <name>, or set `defaults.server` in ~/.omnigraph/config.yaml"
);
};
let client = Self::registry_client(server)?;
if !is_remote_uri(client.uri()) {
bail!(
"a server scope resolves to an http(s):// URL; `{}` is not one",
client.uri()
);
}
Ok(client)
}

/// Resolve the addressing (positional URI / `--target` / `--server`)
/// and credential once, then pick the variant by URI scheme — the
/// single branch point that replaces every per-command `is_remote`
/// fork. Mirrors the read verbs' current preamble (`resolve_uri`
/// path, not the policy-bearing `resolve_cli_graph`). Used by reads
/// and `query` (which opens without policy, like the reads).
pub(crate) async fn resolve(
capability: crate::planes::Capability,
server: Option<&str>,
graph: Option<&str>,
uri: Option<String>,
Expand All @@ -131,10 +182,14 @@ impl GraphClient {
) -> Result<Self> {
// RFC-011: a scope (profile / --store / operator defaults) may stand in
// for omitted addressing. The explicit branch passes server/graph/uri
// straight through, so existing invocations are unchanged.
// straight through, so existing invocations are unchanged. The caller
// threads its verb's declared capability (planes::command_capability)
// so scope resolution and the addressing guard share one
// classification; every current caller is a data-plane (`Any`) verb —
// registry-scoped `graphs list` uses `resolve_registry` instead.
let scope = crate::scope::resolve_scope(
&crate::operator::load_operator_config()?,
crate::planes::Capability::Any,
capability,
crate::scope::ScopeFlags { profile, store, server, cluster: None, graph, uri },
)?;
require_graph_for_multi_graph_server(&scope).await?;
Expand Down Expand Up @@ -166,6 +221,7 @@ impl GraphClient {
/// resolution order matches the write arms exactly: server flag →
/// bearer token → graph.
pub(crate) async fn resolve_with_policy(
capability: crate::planes::Capability,
server: Option<&str>,
graph: Option<&str>,
uri: Option<String>,
Expand All @@ -174,10 +230,11 @@ impl GraphClient {
store: Option<&str>,
) -> Result<Self> {
// RFC-011 scope translation (see `resolve`); explicit addressing passes
// through unchanged.
// through unchanged, and the caller threads its verb's declared
// capability.
let scope = crate::scope::resolve_scope(
&crate::operator::load_operator_config()?,
crate::planes::Capability::Any,
capability,
crate::scope::ScopeFlags { profile, store, server, cluster: None, graph, uri },
)?;
require_graph_for_multi_graph_server(&scope).await?;
Expand Down Expand Up @@ -813,11 +870,11 @@ impl GraphClient {
}
}

/// `graphs list` — enumerate the graphs a remote multi-graph server
/// serves (`GET /graphs`). Remote-only by design: there is no local
/// enumeration endpoint, so the Embedded arm fails loudly. Routing it
/// through the enum still buys the shared `resolve()` addressing/token
/// preamble.
/// `graphs list` — enumerate the graphs a multi-graph server serves
/// (`GET /graphs`). Reached only through registry-addressed clients
/// (`resolve_registry` / the D7 probe's `registry_client`), which always
/// build the Remote variant — the Embedded arm is unreachable by
/// construction and kept as a defensive internal-invariant bail.
pub(crate) async fn list_graphs(&self) -> Result<GraphListResponse> {
match self {
GraphClient::Remote {
Expand All @@ -835,10 +892,29 @@ impl GraphClient {
.await
}
GraphClient::Embedded { .. } => bail!(
"`omnigraph graphs list` requires a remote multi-graph server \
(--server <url>). To enumerate the graphs in a cluster, run \
`omnigraph cluster status --config <dir>`."
"internal error: `graphs list` reached an embedded client — registry \
addressing always resolves a server"
),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn resolve_registry_is_sync_and_yields_the_bare_base_url() {
// Structural proof the RFC-011 D7 multi-graph probe cannot fire on the
// graphs-list path: resolve_registry is synchronous (called here with
// no tokio runtime), while the probe is async and performs GET
// /graphs. Also pins the URL-corruption fix: the bare base URL with
// the trailing slash trimmed and no `/graphs/<id>` segment. A literal
// `://` --server value bypasses the operator server registry, so a
// developer's real config cannot change the outcome.
let client =
GraphClient::resolve_registry(Some("http://server.invalid:9/"), None).unwrap();
assert_eq!(client.uri(), "http://server.invalid:9");
assert!(client.is_remote());
}
}
Loading