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:
aaltshuler 2026-06-11 22:25:42 +03:00
parent 2b33ab64f2
commit dc91c55970
6 changed files with 256 additions and 6 deletions

View file

@ -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");
}