feat(cli): --server <name> targeting (RFC-007 PR 3, part 1)

Global flags --server (operator-defined server name) and --graph (graph id
on a multi-graph server, requires --server) resolve to the effective
remote URI through one helper and feed the ordinary uri slot — graph
resolution and the PR-2 keyed-token URL match work unchanged; the flag is
sugar for a URI the operator already owns. Exclusive with a positional
URI and --target (loud error, never silent precedence). Unknown names
fail listing the servers that ARE defined.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
aaltshuler 2026-06-11 22:19:25 +03:00
parent 65160cc060
commit 2b33ab64f2
4 changed files with 109 additions and 0 deletions

View file

@ -21,6 +21,17 @@ 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")]
pub(crate) server: Option<String>,
/// Graph id on a multi-graph `--server` (appends `/graphs/<id>` to
/// the server url). Requires --server.
#[arg(long, global = true, value_name = "GRAPH_ID", requires = "server")]
pub(crate) graph: Option<String>,
#[command(subcommand)]
pub(crate) command: Command,
}

View file

@ -264,6 +264,57 @@ pub(crate) fn resolve_remote_bearer_token(
Ok(None)
}
/// `--server <name>` (RFC-007 PR 3): resolve an operator-defined server
/// name (+ optional `--graph` for multi-graph servers) to the effective
/// remote URI. The result feeds the ordinary `uri` slot, so graph
/// resolution and the keyed-token URL match work unchanged — the flag is
/// sugar for a URI the operator already owns. Unknown names fail loudly,
/// listing what IS defined.
pub(crate) fn resolve_server_flag(
server: Option<&str>,
graph: Option<&str>,
) -> Result<Option<String>> {
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)"
);
};
let base = entry.url.trim_end_matches('/');
Ok(Some(match graph {
Some(graph) => format!("{base}/graphs/{graph}"),
None => base.to_string(),
}))
}
/// Apply `--server`/`--graph` to a command's uri/target slots: exclusive
/// with both (loud error, not silent precedence), no-op when absent.
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() {
color_eyre::eyre::bail!(
"--server is exclusive with a positional URI and --target — pick one way to address the graph"
);
}
resolve_server_flag(server, graph)
}
/// The remote base URL a token resolution is FOR — the same scoping
/// `graph_bearer_token_env` uses: an explicit http(s) `--uri` wins, else
/// the config-resolved target's uri (when remote). Local URIs → None.

View file

@ -130,6 +130,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -198,6 +200,8 @@ async fn main() -> Result<()> {
use `omnigraph load --from <base> --mode <mode>` (ingest defaults: --from main --mode merge)"
);
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -250,6 +254,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -293,6 +299,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -328,6 +336,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -367,6 +377,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -417,6 +429,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let uri = resolve_uri(&config, uri, target.as_deref())?;
@ -456,6 +470,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let uri = resolve_uri(&config, uri, target.as_deref())?;
@ -519,6 +535,8 @@ async fn main() -> Result<()> {
allow_data_loss,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let graph = resolve_cli_graph(&config, uri, target.as_deref())?;
@ -576,6 +594,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let uri = resolve_uri(&config, uri, target.as_deref())?;
@ -640,6 +660,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let uri = resolve_uri(&config, uri, target.as_deref())?;
@ -675,6 +697,8 @@ async fn main() -> Result<()> {
table_keys,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let uri = resolve_uri(&config, uri, target.as_deref())?;
@ -736,6 +760,7 @@ async fn main() -> Result<()> {
let target_name = target
.as_deref()
.or_else(|| alias_config.and_then(|alias| alias.graph.as_deref()));
let uri = apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target_name)?;
let bearer_token = resolve_remote_bearer_token(&config, uri.as_deref(), target_name)?;
let graph = resolve_cli_graph(&config, uri, target_name)?;
let uri = graph.uri.clone();
@ -822,6 +847,7 @@ async fn main() -> Result<()> {
let target_name = target
.as_deref()
.or_else(|| alias_config.and_then(|alias| alias.graph.as_deref()));
let uri = apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target_name)?;
let bearer_token = resolve_remote_bearer_token(&config, uri.as_deref(), target_name)?;
let graph = resolve_cli_graph(&config, uri, target_name)?;
let uri = graph.uri.clone();
@ -1177,6 +1203,8 @@ async fn main() -> Result<()> {
json,
} => {
let config = load_cli_config(config.as_ref())?;
let uri =
apply_server_flag(cli.server.as_deref(), cli.graph.as_deref(), uri, target.as_deref())?;
let bearer_token =
resolve_remote_bearer_token(&config, uri.as_deref(), target.as_deref())?;
let uri = resolve_uri(&config, uri, target.as_deref())?;

View file

@ -467,6 +467,25 @@ mod tests {
assert_eq!(config.find_server_for_url("http://other:9999"), None);
}
#[test]
fn server_lookup_supports_targeting() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.yaml");
fs::write(
&path,
"servers:\n intel-dev:\n url: http://127.0.0.1:8080/\n",
)
.unwrap();
let config = load_operator_config_at(&path).unwrap();
// the --server resolution shape: bare url and graph-scoped url
let base = config.servers["intel-dev"].url.trim_end_matches('/');
assert_eq!(base, "http://127.0.0.1:8080");
assert_eq!(
format!("{base}/graphs/spike"),
"http://127.0.0.1:8080/graphs/spike"
);
}
#[test]
fn token_env_name_uppercases_and_underscores() {
assert_eq!(token_env_name("intel-dev"), "OMNIGRAPH_TOKEN_INTEL_DEV");