mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-15 01:55:13 +02:00
feat(cli): operator aliases — pure bindings invoking stored queries (RFC-007 PR 3, part 2)
aliases: in the operator config bind a personal name to (server, graph,
stored-query NAME, positional arg mapping, fixed param defaults, format)
— zero content, per the ratified bindings-not-content model. Invocation
goes through the server's stored-query endpoint (POST
{base}/graphs/{g}/queries/{name}) with the keyed credential resolving via
the ordinary URL match; param precedence --params > positionals > fixed
defaults; the result renders through the existing format cascade with the
alias's format as its hop. A legacy omnigraph.yaml alias with the same
name wins during the RFC-008 window, with a warning naming both.
E2e (spawned policy-gated server, invoke_query granted via a per-graph
bundle): the alias invokes with name + one positional and nothing else —
server, graph, query, and token all from the operator layer; --server/
--graph explicit targeting; unknown --server lists defined names;
--server exclusive with a positional URI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2b33ab64f2
commit
dc91c55970
6 changed files with 256 additions and 6 deletions
|
|
@ -296,6 +296,57 @@ pub(crate) fn resolve_server_flag(
|
|||
}))
|
||||
}
|
||||
|
||||
/// Execute an OPERATOR alias (RFC-007 PR 3): a pure binding invoking a
|
||||
/// stored query by name on a named server — POST {base}/queries/{name}.
|
||||
/// Param precedence: --params > positional args > the alias's fixed
|
||||
/// params. The keyed token applies via the ordinary URL match.
|
||||
pub(crate) async fn execute_operator_alias(
|
||||
client: &reqwest::Client,
|
||||
config: &OmnigraphConfig,
|
||||
alias_name: &str,
|
||||
alias: &crate::operator::OperatorAlias,
|
||||
alias_args: &[String],
|
||||
explicit_params: Option<Value>,
|
||||
) -> 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 mut params = serde_json::Map::new();
|
||||
for (key, value) in &alias.params {
|
||||
let Some(key) = key.as_str() else {
|
||||
bail!("alias '{alias_name}': params keys must be strings");
|
||||
};
|
||||
params.insert(key.to_string(), serde_json::to_value(value)?);
|
||||
}
|
||||
if alias_args.len() > alias.args.len() {
|
||||
bail!(
|
||||
"alias '{alias_name}' takes {} positional arg(s) ({}), got {}",
|
||||
alias.args.len(),
|
||||
alias.args.join(", "),
|
||||
alias_args.len()
|
||||
);
|
||||
}
|
||||
for (name, value) in alias.args.iter().zip(alias_args) {
|
||||
params.insert(name.clone(), parse_alias_value(value));
|
||||
}
|
||||
if let Some(Value::Object(explicit)) = explicit_params {
|
||||
for (key, value) in explicit {
|
||||
params.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let body = (!params.is_empty()).then(|| serde_json::json!({ "params": params }));
|
||||
remote_json(
|
||||
client,
|
||||
Method::POST,
|
||||
remote_url(&uri, &format!("/queries/{}", alias.query)),
|
||||
body,
|
||||
bearer_token.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 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(
|
||||
|
|
|
|||
|
|
@ -746,6 +746,34 @@ async fn main() -> Result<()> {
|
|||
}
|
||||
|
||||
let config = load_cli_config(config.as_ref())?;
|
||||
// Operator aliases (RFC-007 PR 3): pure bindings to stored
|
||||
// queries. A legacy file-alias with the same name wins during
|
||||
// the RFC-008 window (with a warning); an alias name found
|
||||
// only in the operator layer takes the invoke path here.
|
||||
if let Some(alias_name) = alias.as_deref() {
|
||||
let operator_config = crate::operator::load_operator_config()?;
|
||||
if let Some(operator_alias) = operator_config.aliases.get(alias_name) {
|
||||
if config.alias(alias_name).is_ok() {
|
||||
eprintln!(
|
||||
"warning: alias '{alias_name}' is defined in both omnigraph.yaml (legacy, wins during the deprecation window) and the operator config; the legacy definition applies"
|
||||
);
|
||||
} else {
|
||||
let output = execute_operator_alias(
|
||||
&http_client,
|
||||
&config,
|
||||
alias_name,
|
||||
operator_alias,
|
||||
&alias_args,
|
||||
load_params_json(¶ms)?,
|
||||
)
|
||||
.await?;
|
||||
let format =
|
||||
resolve_read_format(&config, format, json, operator_alias.format);
|
||||
print_read_output(&output, format, &config)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -38,12 +38,38 @@ pub(crate) struct OperatorConfig {
|
|||
/// can redefine an entry here. No tokens in this file, ever.
|
||||
#[serde(default)]
|
||||
pub(crate) servers: BTreeMap<String, OperatorServer>,
|
||||
/// Personal alias bindings (RFC-007 PR 3); see OperatorAlias.
|
||||
#[serde(default)]
|
||||
pub(crate) aliases: BTreeMap<String, OperatorAlias>,
|
||||
/// Everything this CLI version doesn't know. Warned once at load,
|
||||
/// otherwise ignored (forward compatibility within the operator layer).
|
||||
#[serde(flatten)]
|
||||
unknown: serde_yaml::Mapping,
|
||||
}
|
||||
|
||||
/// A personal alias: a pure BINDING to a stored query on a named server —
|
||||
/// never content, never a file (RFC-007 §D2 "Aliases are bindings, not
|
||||
/// content"). The stored query is the team's contract; the alias, its
|
||||
/// defaults, and its name are the operator's.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OperatorAlias {
|
||||
/// Names an entry under `servers:`.
|
||||
pub(crate) server: String,
|
||||
/// Graph id for multi-graph servers (appends `/graphs/<id>`).
|
||||
pub(crate) graph: Option<String>,
|
||||
/// The STORED query's name on that server.
|
||||
pub(crate) query: String,
|
||||
/// Positional CLI args bind to these param names, in order.
|
||||
#[serde(default)]
|
||||
pub(crate) args: Vec<String>,
|
||||
/// Fixed default params; positionals and `--params` override per key.
|
||||
#[serde(default)]
|
||||
pub(crate) params: serde_yaml::Mapping,
|
||||
pub(crate) format: Option<ReadOutputFormat>,
|
||||
#[serde(flatten)]
|
||||
unknown: serde_yaml::Mapping,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct OperatorServer {
|
||||
pub(crate) url: String,
|
||||
|
|
@ -163,6 +189,9 @@ impl OperatorConfig {
|
|||
for (name, server) in &self.servers {
|
||||
collect(&server.unknown, &format!("servers.{name}."));
|
||||
}
|
||||
for (name, alias) in &self.aliases {
|
||||
collect(&alias.unknown, &format!("aliases.{name}."));
|
||||
}
|
||||
warnings
|
||||
}
|
||||
}
|
||||
|
|
@ -425,10 +454,8 @@ mod tests {
|
|||
let config = load_operator_config_at(&path).unwrap();
|
||||
assert_eq!(config.actor(), Some("act-a"));
|
||||
let warnings = config.unknown_key_warnings();
|
||||
// `servers` became a known key in PR 2; `aliases` stays unknown
|
||||
// until PR 3.
|
||||
assert_eq!(warnings.len(), 2, "{warnings:?}");
|
||||
assert!(warnings.iter().any(|w| w.contains("`aliases`")));
|
||||
// `servers` (PR 2) and `aliases` (PR 3) are known keys now.
|
||||
assert_eq!(warnings.len(), 1, "{warnings:?}");
|
||||
assert!(warnings.iter().any(|w| w.contains("`operator.color`")));
|
||||
assert_eq!(config.servers["prod"].url, "https://example.com");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue