mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-24 02:38:06 +02:00
feat(cli)!: excise omnigraph.yaml from the CLI; policy/queries tooling reads --cluster (#251)
The server already dropped omnigraph.yaml (cluster-only boot). This removes the CLI's last use of the legacy `OmnigraphConfig`: graphs are addressed only via `--store`/`--server`/`--cluster`/`--profile`/operator defaults, and actor, output format, and bearer credentials come from `~/.omnigraph/config.yaml`. After this change no CLI command reads `omnigraph.yaml` except `config migrate`. Resolvers (helpers.rs): drop every legacy fallback — - `resolve_actor` → `--as` > `operator.actor` (no `cli.actor`); - `resolve_read_format` → `--json`/`--format` > alias > `defaults.output`; - `resolve_branch`/`resolve_read_target` → `--branch` > alias > "main"; - `resolve_uri`/`resolve_cli_graph` → scope path only; an absent address is a loud error; - `resolve_remote_bearer_token` → operator keyed chain + `OMNIGRAPH_BEARER_TOKEN`. `GraphClient::resolve`/`resolve_with_policy` drop the `&OmnigraphConfig` param; direct-store access carries no Cedar policy (policy lives in the cluster/server). Flags (cli.rs): remove `--config` from every data/query command; it stays only on `cluster *` (the cluster dir) and `config migrate` (the legacy path). Re-home control-plane tooling to `--cluster` (RFC-011): - `policy validate|test|explain` source the Cedar bundle from the cluster's applied policies; `--graph` picks a graph's bundle; `policy test` takes `--tests <file>`; - `queries list|validate` source the registry + schemas from the cluster serving snapshot; `--graph` scopes to one graph; - `lint` requires `--schema` (offline) or a direct/cluster graph target; - `schema plan`/`lint` route their graph-target through the shared direct-scope resolver so `--store`/`--profile`/`defaults.store` addressing works. Tests migrate from `omnigraph.yaml` fixtures to `--store`/operator-config/ `--cluster` (converged-cluster fixtures); the now-impossible command-path RFC-008 tests are deleted (`config migrate` coverage kept). The `OmnigraphConfig` type, `load_config`/deprecation machinery, and `config migrate` are removed in a follow-up. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8b01c6e547
commit
0bee746a31
15 changed files with 1464 additions and 2262 deletions
|
|
@ -18,10 +18,10 @@ any — run against a graph, served (--server / --profile) or embedded (--store
|
||||||
URI): query, mutate, load, branch, snapshot, export, commit, schema show/apply.\n \
|
URI): query, mutate, load, branch, snapshot, export, commit, schema show/apply.\n \
|
||||||
served — require a server: graphs.\n \
|
served — require a server: graphs.\n \
|
||||||
direct — direct storage access; reject --server (init, optimize, repair, cleanup, \
|
direct — direct storage access; reject --server (init, optimize, repair, cleanup, \
|
||||||
schema plan, lint, queries validate).\n \
|
schema plan, lint).\n \
|
||||||
control — manage a cluster via --config: cluster.\n \
|
control — manage or inspect a cluster (cluster via --config; policy & queries via \
|
||||||
local — no graph; local config & tooling: policy, embed, login, logout, config, \
|
--cluster).\n \
|
||||||
version, queries list.\n\
|
local — no graph; local config & tooling: embed, login, logout, config, version.\n\
|
||||||
See the 'Command capabilities' section of the CLI reference for which flags apply where.")]
|
See the 'Command capabilities' section of the CLI reference for which flags apply where.")]
|
||||||
pub(crate) struct Cli {
|
pub(crate) struct Cli {
|
||||||
/// Actor id for direct-engine writes; overrides `cli.actor`. No effect on
|
/// Actor id for direct-engine writes; overrides `cli.actor`. No effect on
|
||||||
|
|
@ -96,8 +96,6 @@ pub(crate) enum Command {
|
||||||
/// the catalog (served — addressed via --server/--profile). With
|
/// the catalog (served — addressed via --server/--profile). With
|
||||||
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
|
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
/// Ad-hoc query file (a `.gq` you're authoring / break-glass).
|
/// Ad-hoc query file (a `.gq` you're authoring / break-glass).
|
||||||
#[arg(long, conflicts_with = "query_string")]
|
#[arg(long, conflicts_with = "query_string")]
|
||||||
query: Option<PathBuf>,
|
query: Option<PathBuf>,
|
||||||
|
|
@ -126,8 +124,6 @@ pub(crate) enum Command {
|
||||||
/// from the catalog (served — addressed via --server/--profile). With
|
/// from the catalog (served — addressed via --server/--profile). With
|
||||||
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
|
/// `--query`/`-e`, selects which query in that ad-hoc source to run.
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
/// Ad-hoc mutation file (a `.gq` you're authoring / break-glass).
|
/// Ad-hoc mutation file (a `.gq` you're authoring / break-glass).
|
||||||
#[arg(long, conflicts_with = "query_string")]
|
#[arg(long, conflicts_with = "query_string")]
|
||||||
query: Option<PathBuf>,
|
query: Option<PathBuf>,
|
||||||
|
|
@ -154,8 +150,6 @@ pub(crate) enum Command {
|
||||||
name: String,
|
name: String,
|
||||||
/// Positional args bound to the alias's declared `args` params, in order.
|
/// Positional args bound to the alias's declared `args` params, in order.
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
params: ParamsArgs,
|
params: ParamsArgs,
|
||||||
#[arg(long, conflicts_with = "json")]
|
#[arg(long, conflicts_with = "json")]
|
||||||
|
|
@ -168,8 +162,6 @@ pub(crate) enum Command {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
data: PathBuf,
|
data: PathBuf,
|
||||||
/// Target branch (defaults to main). Without --from it must exist.
|
/// Target branch (defaults to main). Without --from it must exist.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -191,8 +183,6 @@ pub(crate) enum Command {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
data: PathBuf,
|
data: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
|
|
@ -213,8 +203,6 @@ pub(crate) enum Command {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -224,8 +212,6 @@ pub(crate) enum Command {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
#[arg(long, hide = true)]
|
#[arg(long, hide = true)]
|
||||||
jsonl: bool,
|
jsonl: bool,
|
||||||
|
|
@ -270,16 +256,12 @@ pub(crate) enum Command {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Classify and explicitly repair manifest/head drift
|
/// Classify and explicitly repair manifest/head drift
|
||||||
Repair {
|
Repair {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
/// Publish verified maintenance drift. Without this flag, repair only
|
/// Publish verified maintenance drift. Without this flag, repair only
|
||||||
/// previews what it would do.
|
/// previews what it would do.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -295,8 +277,6 @@ pub(crate) enum Command {
|
||||||
Cleanup {
|
Cleanup {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
/// Number of recent versions to keep per table. Either `--keep` or
|
/// Number of recent versions to keep per table. Either `--keep` or
|
||||||
/// `--older-than` (or both) must be set.
|
/// `--older-than` (or both) must be set.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -326,8 +306,6 @@ pub(crate) enum Command {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
query: PathBuf,
|
query: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
schema: Option<PathBuf>,
|
schema: Option<PathBuf>,
|
||||||
|
|
@ -480,8 +458,6 @@ pub(crate) enum GraphsCommand {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -494,8 +470,6 @@ pub(crate) enum BranchCommand {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
from: Option<String>,
|
from: Option<String>,
|
||||||
name: String,
|
name: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -507,8 +481,6 @@ pub(crate) enum BranchCommand {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Delete a branch
|
/// Delete a branch
|
||||||
|
|
@ -516,8 +488,6 @@ pub(crate) enum BranchCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
name: String,
|
name: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -527,8 +497,6 @@ pub(crate) enum BranchCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
source: String,
|
source: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
into: Option<String>,
|
into: Option<String>,
|
||||||
|
|
@ -544,8 +512,6 @@ pub(crate) enum SchemaCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
schema: PathBuf,
|
schema: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -560,8 +526,6 @@ pub(crate) enum SchemaCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
schema: PathBuf,
|
schema: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -583,8 +547,6 @@ pub(crate) enum SchemaCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -597,8 +559,6 @@ pub(crate) enum CommitCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
|
||||||
branch: Option<String>,
|
branch: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -608,8 +568,6 @@ pub(crate) enum CommitCommand {
|
||||||
/// Graph URI
|
/// Graph URI
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
commit_id: String,
|
commit_id: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
@ -618,20 +576,24 @@ pub(crate) enum CommitCommand {
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub(crate) enum PolicyCommand {
|
pub(crate) enum PolicyCommand {
|
||||||
/// Validate policy YAML and compiled Cedar policy state
|
/// Compile and validate the Cedar policy bundle(s) applied in a cluster.
|
||||||
Validate {
|
///
|
||||||
#[arg(long)]
|
/// Sources the bundle(s) from the cluster's applied policies
|
||||||
config: Option<PathBuf>,
|
/// (`--cluster <dir>`); pass the global `--graph <id>` to pick one
|
||||||
},
|
/// graph's bundle when several apply.
|
||||||
/// Run declarative policy tests from policy.tests.yaml
|
Validate {},
|
||||||
|
/// Run declarative policy tests against a cluster's applied bundle.
|
||||||
|
///
|
||||||
|
/// The cluster model has no per-bundle tests file, so the cases are
|
||||||
|
/// supplied explicitly with `--tests <file>` and checked against the
|
||||||
|
/// bundle selected by `--cluster` (+ optional `--graph`).
|
||||||
Test {
|
Test {
|
||||||
|
/// Path to a policy.tests.yaml file.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
config: Option<PathBuf>,
|
tests: PathBuf,
|
||||||
},
|
},
|
||||||
/// Explain one policy decision locally
|
/// Explain one policy decision against a cluster's applied bundle.
|
||||||
Explain {
|
Explain {
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
actor: String,
|
actor: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
@ -645,24 +607,19 @@ pub(crate) enum PolicyCommand {
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub(crate) enum QueriesCommand {
|
pub(crate) enum QueriesCommand {
|
||||||
/// Type-check the stored-query registry against the live schema.
|
/// Type-check a cluster's stored-query registry against its schemas.
|
||||||
///
|
///
|
||||||
/// Distinct from `omnigraph lint` (which lints one `.gq` file):
|
/// Distinct from `omnigraph lint` (which lints one `.gq` file): this
|
||||||
/// this validates the whole `queries:` registry — opening the graph
|
/// validates the whole `queries:` registry of a cluster (`--cluster
|
||||||
/// to read its schema and confirming every stored query still
|
/// <dir>`, optional `--graph <id>`) by reading each graph's applied
|
||||||
/// type-checks. Exits non-zero on any breakage.
|
/// schema and confirming every stored query still type-checks. Exits
|
||||||
|
/// non-zero on any breakage.
|
||||||
Validate {
|
Validate {
|
||||||
/// Graph URI
|
|
||||||
uri: Option<String>,
|
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// List the registered stored queries (name, MCP exposure, params).
|
/// List a cluster's registered stored queries (name, params).
|
||||||
List {
|
List {
|
||||||
#[arg(long)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -40,22 +40,20 @@ use serde_json::Value;
|
||||||
|
|
||||||
use crate::cli::CliLoadMode;
|
use crate::cli::CliLoadMode;
|
||||||
use crate::helpers::{
|
use crate::helpers::{
|
||||||
ResolvedCliGraph, apply_bearer_token, apply_server_flag, build_http_client, is_remote_uri,
|
apply_bearer_token, apply_server_flag, build_http_client, is_remote_uri,
|
||||||
legacy_change_request_body, open_local_db_with_policy, query_params_from_json,
|
legacy_change_request_body, query_params_from_json,
|
||||||
remote_json, remote_url, resolve_cli_actor, resolve_cli_graph, resolve_remote_bearer_token,
|
remote_json, remote_url, resolve_cli_actor, resolve_cli_graph, resolve_remote_bearer_token,
|
||||||
resolve_server_flag, select_named_query,
|
resolve_server_flag, select_named_query,
|
||||||
};
|
};
|
||||||
use crate::output::{LoadOutput, load_output_from_result, load_output_from_tables};
|
use crate::output::{LoadOutput, load_output_from_result, load_output_from_tables};
|
||||||
use omnigraph_server::config::OmnigraphConfig;
|
|
||||||
|
|
||||||
pub(crate) enum GraphClient {
|
pub(crate) enum GraphClient {
|
||||||
/// Local engine at `uri`. Reads (`resolve()`) leave `graph`/`actor`
|
/// Local engine at `uri`. Reads (`resolve()`) leave `actor` empty;
|
||||||
/// empty and open without policy; writes (`resolve_with_policy()`)
|
/// writes (`resolve_with_policy()`) attribute the resolved actor.
|
||||||
/// fill them, opening through `open_local_db_with_policy` and
|
/// Direct-store access carries no Cedar policy (RFC-011: policy lives
|
||||||
/// attributing the resolved actor.
|
/// in the cluster/server, not in per-operator addressing).
|
||||||
Embedded {
|
Embedded {
|
||||||
uri: String,
|
uri: String,
|
||||||
graph: Option<ResolvedCliGraph>,
|
|
||||||
actor: Option<String>,
|
actor: Option<String>,
|
||||||
},
|
},
|
||||||
/// Remote HTTP server. The actor is resolved server-side from the
|
/// Remote HTTP server. The actor is resolved server-side from the
|
||||||
|
|
@ -75,7 +73,6 @@ pub(crate) enum GraphClient {
|
||||||
/// is then correct, or the real request surfaces the failure. Only fires on the
|
/// is then correct, or the real request surfaces the failure. Only fires on the
|
||||||
/// no-graph path, so a `--graph`/`default_graph` happy path does no extra I/O.
|
/// no-graph path, so a `--graph`/`default_graph` happy path does no extra I/O.
|
||||||
async fn require_graph_for_multi_graph_server(
|
async fn require_graph_for_multi_graph_server(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
scope: &crate::scope::ResolvedScope,
|
scope: &crate::scope::ResolvedScope,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (Some(server), None) = (scope.server.as_deref(), scope.graph.as_deref()) else {
|
let (Some(server), None) = (scope.server.as_deref(), scope.graph.as_deref()) else {
|
||||||
|
|
@ -84,7 +81,7 @@ async fn require_graph_for_multi_graph_server(
|
||||||
let Some(base) = resolve_server_flag(Some(server), None)? else {
|
let Some(base) = resolve_server_flag(Some(server), None)? else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let token = resolve_remote_bearer_token(config, Some(&base))?;
|
let token = resolve_remote_bearer_token(Some(&base))?;
|
||||||
let probe = GraphClient::Remote {
|
let probe = GraphClient::Remote {
|
||||||
http: build_http_client()?,
|
http: build_http_client()?,
|
||||||
base_url: base,
|
base_url: base,
|
||||||
|
|
@ -126,7 +123,6 @@ impl GraphClient {
|
||||||
/// path, not the policy-bearing `resolve_cli_graph`). Used by reads
|
/// path, not the policy-bearing `resolve_cli_graph`). Used by reads
|
||||||
/// and `query` (which opens without policy, like the reads).
|
/// and `query` (which opens without policy, like the reads).
|
||||||
pub(crate) async fn resolve(
|
pub(crate) async fn resolve(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
server: Option<&str>,
|
server: Option<&str>,
|
||||||
graph: Option<&str>,
|
graph: Option<&str>,
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
|
|
@ -141,7 +137,7 @@ impl GraphClient {
|
||||||
crate::planes::Capability::Any,
|
crate::planes::Capability::Any,
|
||||||
crate::scope::ScopeFlags { profile, store, server, cluster: None, graph, uri },
|
crate::scope::ScopeFlags { profile, store, server, cluster: None, graph, uri },
|
||||||
)?;
|
)?;
|
||||||
require_graph_for_multi_graph_server(config, &scope).await?;
|
require_graph_for_multi_graph_server(&scope).await?;
|
||||||
let (server, graph, uri) = (
|
let (server, graph, uri) = (
|
||||||
scope.server.as_deref(),
|
scope.server.as_deref(),
|
||||||
scope.graph.as_deref(),
|
scope.graph.as_deref(),
|
||||||
|
|
@ -149,8 +145,8 @@ impl GraphClient {
|
||||||
);
|
);
|
||||||
let via_server = server.is_some();
|
let via_server = server.is_some();
|
||||||
let uri = apply_server_flag(server, graph, uri)?;
|
let uri = apply_server_flag(server, graph, uri)?;
|
||||||
let token = resolve_remote_bearer_token(config, uri.as_deref())?;
|
let token = resolve_remote_bearer_token(uri.as_deref())?;
|
||||||
let uri = crate::helpers::resolve_uri(config, uri)?;
|
let uri = crate::helpers::resolve_uri(uri)?;
|
||||||
reject_positional_remote(via_server, &uri)?;
|
reject_positional_remote(via_server, &uri)?;
|
||||||
if is_remote_uri(&uri) {
|
if is_remote_uri(&uri) {
|
||||||
Ok(GraphClient::Remote {
|
Ok(GraphClient::Remote {
|
||||||
|
|
@ -159,11 +155,7 @@ impl GraphClient {
|
||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(GraphClient::Embedded {
|
Ok(GraphClient::Embedded { uri, actor: None })
|
||||||
uri,
|
|
||||||
graph: None,
|
|
||||||
actor: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,7 +166,6 @@ impl GraphClient {
|
||||||
/// resolution order matches the write arms exactly: server flag →
|
/// resolution order matches the write arms exactly: server flag →
|
||||||
/// bearer token → graph.
|
/// bearer token → graph.
|
||||||
pub(crate) async fn resolve_with_policy(
|
pub(crate) async fn resolve_with_policy(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
server: Option<&str>,
|
server: Option<&str>,
|
||||||
graph: Option<&str>,
|
graph: Option<&str>,
|
||||||
uri: Option<String>,
|
uri: Option<String>,
|
||||||
|
|
@ -189,7 +180,7 @@ impl GraphClient {
|
||||||
crate::planes::Capability::Any,
|
crate::planes::Capability::Any,
|
||||||
crate::scope::ScopeFlags { profile, store, server, cluster: None, graph, uri },
|
crate::scope::ScopeFlags { profile, store, server, cluster: None, graph, uri },
|
||||||
)?;
|
)?;
|
||||||
require_graph_for_multi_graph_server(config, &scope).await?;
|
require_graph_for_multi_graph_server(&scope).await?;
|
||||||
let (server, graph, uri) = (
|
let (server, graph, uri) = (
|
||||||
scope.server.as_deref(),
|
scope.server.as_deref(),
|
||||||
scope.graph.as_deref(),
|
scope.graph.as_deref(),
|
||||||
|
|
@ -197,8 +188,8 @@ impl GraphClient {
|
||||||
);
|
);
|
||||||
let via_server = server.is_some();
|
let via_server = server.is_some();
|
||||||
let uri = apply_server_flag(server, graph, uri)?;
|
let uri = apply_server_flag(server, graph, uri)?;
|
||||||
let token = resolve_remote_bearer_token(config, uri.as_deref())?;
|
let token = resolve_remote_bearer_token(uri.as_deref())?;
|
||||||
let resolved = resolve_cli_graph(config, uri)?;
|
let resolved = resolve_cli_graph(uri)?;
|
||||||
reject_positional_remote(via_server, &resolved.uri)?;
|
reject_positional_remote(via_server, &resolved.uri)?;
|
||||||
if resolved.is_remote {
|
if resolved.is_remote {
|
||||||
// A served write resolves the actor server-side from the bearer
|
// A served write resolves the actor server-side from the bearer
|
||||||
|
|
@ -216,10 +207,9 @@ impl GraphClient {
|
||||||
token,
|
token,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
let actor = resolve_cli_actor(cli_as, config)?;
|
let actor = resolve_cli_actor(cli_as)?;
|
||||||
Ok(GraphClient::Embedded {
|
Ok(GraphClient::Embedded {
|
||||||
uri: resolved.uri.clone(),
|
uri: resolved.uri,
|
||||||
graph: Some(resolved),
|
|
||||||
actor,
|
actor,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -233,28 +223,15 @@ impl GraphClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The selected graph name, when a policy-bearing embedded client was
|
|
||||||
/// resolved against a named graph. `None` for remote and for reads.
|
|
||||||
pub(crate) fn selected(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
GraphClient::Embedded { graph, .. } => graph.as_ref().and_then(ResolvedCliGraph::selected),
|
|
||||||
GraphClient::Remote { .. } => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn is_remote(&self) -> bool {
|
pub(crate) fn is_remote(&self) -> bool {
|
||||||
matches!(self, GraphClient::Remote { .. })
|
matches!(self, GraphClient::Remote { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the local engine the way the resolved client demands: with
|
/// Open the local engine. Direct-store access carries no Cedar policy
|
||||||
/// policy when a `graph` context is present (write path), bare
|
/// (RFC-011), so both read and write paths open bare; the actor is still
|
||||||
/// otherwise (read/`query` path). Captures today's two open paths in
|
/// attributed on the write via the `_as` engine APIs.
|
||||||
/// one place so each verb stays a single match arm.
|
async fn open_embedded(uri: &str) -> Result<Omnigraph> {
|
||||||
async fn open_embedded(uri: &str, graph: &Option<ResolvedCliGraph>) -> Result<Omnigraph> {
|
Ok(Omnigraph::open(uri).await?)
|
||||||
match graph {
|
|
||||||
Some(graph) => open_local_db_with_policy(graph).await,
|
|
||||||
None => Ok(Omnigraph::open(uri).await?),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn branch_list(&self) -> Result<BranchListOutput> {
|
pub(crate) async fn branch_list(&self) -> Result<BranchListOutput> {
|
||||||
|
|
@ -416,8 +393,8 @@ impl GraphClient {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(load_output_from_tables(base_url, branch, mode.as_str(), &output))
|
Ok(load_output_from_tables(base_url, branch, mode.as_str(), &output))
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let result = db
|
let result = db
|
||||||
.load_file_as(branch, from, data, mode.into(), actor.as_deref())
|
.load_file_as(branch, from, data, mode.into(), actor.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -459,8 +436,8 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let result = db
|
let result = db
|
||||||
.load_file_as(branch, Some(from), data, mode.into(), actor.as_deref())
|
.load_file_as(branch, Some(from), data, mode.into(), actor.as_deref())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -498,10 +475,10 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let (selected_name, query_params) = select_named_query(query_source, query_name)?;
|
let (selected_name, query_params) = select_named_query(query_source, query_name)?;
|
||||||
let params = query_params_from_json(&query_params, params_json)?;
|
let params = query_params_from_json(&query_params, params_json)?;
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let actor = actor.as_deref();
|
let actor = actor.as_deref();
|
||||||
let result = db
|
let result = db
|
||||||
.mutate_as(branch, query_source, &selected_name, ¶ms, actor)
|
.mutate_as(branch, query_source, &selected_name, ¶ms, actor)
|
||||||
|
|
@ -552,10 +529,10 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, .. } => {
|
GraphClient::Embedded { uri, .. } => {
|
||||||
let (selected_name, query_params) = select_named_query(query_source, query_name)?;
|
let (selected_name, query_params) = select_named_query(query_source, query_name)?;
|
||||||
let params = query_params_from_json(&query_params, params_json)?;
|
let params = query_params_from_json(&query_params, params_json)?;
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let result = db
|
let result = db
|
||||||
.query(target.clone(), query_source, &selected_name, ¶ms)
|
.query(target.clone(), query_source, &selected_name, ¶ms)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -631,8 +608,8 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let actor = actor.as_deref();
|
let actor = actor.as_deref();
|
||||||
db.branch_create_from_as(ReadTarget::branch(from), name, actor)
|
db.branch_create_from_as(ReadTarget::branch(from), name, actor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -662,8 +639,8 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let actor = actor.as_deref();
|
let actor = actor.as_deref();
|
||||||
db.branch_delete_as(name, actor).await?;
|
db.branch_delete_as(name, actor).await?;
|
||||||
Ok(BranchDeleteOutput {
|
Ok(BranchDeleteOutput {
|
||||||
|
|
@ -694,8 +671,8 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let actor = actor.as_deref();
|
let actor = actor.as_deref();
|
||||||
let outcome = db.branch_merge_as(source, into, actor).await?;
|
let outcome = db.branch_merge_as(source, into, actor).await?;
|
||||||
Ok(BranchMergeOutput {
|
Ok(BranchMergeOutput {
|
||||||
|
|
@ -745,8 +722,8 @@ impl GraphClient {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { uri, graph, actor } => {
|
GraphClient::Embedded { uri, actor } => {
|
||||||
let db = Self::open_embedded(uri, graph).await?;
|
let db = Self::open_embedded(uri).await?;
|
||||||
let result = db
|
let result = db
|
||||||
.apply_schema_as_with_catalog_check(
|
.apply_schema_as_with_catalog_check(
|
||||||
schema_source,
|
schema_source,
|
||||||
|
|
@ -815,9 +792,9 @@ impl GraphClient {
|
||||||
|
|
||||||
/// `graphs list` — enumerate the graphs a remote multi-graph server
|
/// `graphs list` — enumerate the graphs a remote multi-graph server
|
||||||
/// serves (`GET /graphs`). Remote-only by design: there is no local
|
/// serves (`GET /graphs`). Remote-only by design: there is no local
|
||||||
/// enumeration endpoint, so the Embedded arm fails loudly pointing the
|
/// enumeration endpoint, so the Embedded arm fails loudly. Routing it
|
||||||
/// operator at `omnigraph.yaml`. Routing it through the enum still buys
|
/// through the enum still buys the shared `resolve()` addressing/token
|
||||||
/// the shared `resolve()` addressing/token preamble.
|
/// preamble.
|
||||||
pub(crate) async fn list_graphs(&self) -> Result<GraphListResponse> {
|
pub(crate) async fn list_graphs(&self) -> Result<GraphListResponse> {
|
||||||
match self {
|
match self {
|
||||||
GraphClient::Remote {
|
GraphClient::Remote {
|
||||||
|
|
@ -835,9 +812,9 @@ impl GraphClient {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
GraphClient::Embedded { .. } => bail!(
|
GraphClient::Embedded { .. } => bail!(
|
||||||
"`omnigraph graphs list` requires a remote multi-graph server URL \
|
"`omnigraph graphs list` requires a remote multi-graph server \
|
||||||
(http:// or https://). To enumerate local graphs, read `omnigraph.yaml` \
|
(--server <url>). To enumerate the graphs in a cluster, run \
|
||||||
directly."
|
`omnigraph cluster status --config <dir>`."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,231 +119,164 @@ pub(crate) fn bearer_token_from_env(var_name: &str) -> Option<String> {
|
||||||
normalize_bearer_token(std::env::var(var_name).ok())
|
normalize_bearer_token(std::env::var(var_name).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_env_assignment(line: &str) -> Option<(String, String)> {
|
|
||||||
let line = line.trim();
|
|
||||||
if line.is_empty() || line.starts_with('#') {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let line = line.strip_prefix("export ").unwrap_or(line).trim();
|
|
||||||
let (name, value) = line.split_once('=')?;
|
|
||||||
let name = name.trim();
|
|
||||||
if name.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = value.trim();
|
|
||||||
let value = if value.len() >= 2
|
|
||||||
&& ((value.starts_with('"') && value.ends_with('"'))
|
|
||||||
|| (value.starts_with('\'') && value.ends_with('\'')))
|
|
||||||
{
|
|
||||||
&value[1..value.len() - 1]
|
|
||||||
} else {
|
|
||||||
value
|
|
||||||
};
|
|
||||||
|
|
||||||
Some((name.to_string(), value.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bearer_token_from_env_file(path: &Path, var_name: &str) -> Result<Option<String>> {
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
for line in fs::read_to_string(path)?.lines() {
|
|
||||||
let Some((name, value)) = parse_env_assignment(line) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if name == var_name {
|
|
||||||
return Ok(normalize_bearer_token(Some(value)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn load_env_file_into_process(path: &Path) -> Result<()> {
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
for line in fs::read_to_string(path)?.lines() {
|
|
||||||
let Some((name, value)) = parse_env_assignment(line) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if std::env::var_os(&name).is_none() {
|
|
||||||
unsafe {
|
|
||||||
std::env::set_var(name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn load_cli_config(config_path: Option<&PathBuf>) -> Result<OmnigraphConfig> {
|
|
||||||
let config = load_config(config_path)?;
|
|
||||||
if let Some(path) = config.resolve_auth_env_file() {
|
|
||||||
load_env_file_into_process(&path)?;
|
|
||||||
}
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct ResolvedCliGraph {
|
pub(crate) struct ResolvedCliGraph {
|
||||||
pub(crate) uri: String,
|
pub(crate) uri: String,
|
||||||
pub(crate) selected: Option<String>,
|
|
||||||
pub(crate) graph_id: String,
|
|
||||||
pub(crate) policy_file: Option<PathBuf>,
|
|
||||||
pub(crate) is_remote: bool,
|
pub(crate) is_remote: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResolvedCliGraph {
|
/// Resolve the cluster for a control-plane tooling command (`policy`,
|
||||||
pub(crate) fn selected(&self) -> Option<&str> {
|
/// `queries`) from `--cluster`. A configured name (`clusters:` in operator
|
||||||
self.selected.as_deref()
|
/// config) is rewritten to its root; a literal dir / `s3://`/`file://` root is
|
||||||
}
|
/// passed through. A `--profile`/`OMNIGRAPH_PROFILE` cluster binding also
|
||||||
}
|
/// resolves here when `--cluster` is absent. No omnigraph.yaml.
|
||||||
|
pub(crate) fn require_cluster_scope(
|
||||||
pub(crate) struct ResolvedPolicyContext {
|
cluster: Option<&str>,
|
||||||
pub(crate) policy_file: PathBuf,
|
profile: Option<&str>,
|
||||||
pub(crate) graph_id: String,
|
command: &str,
|
||||||
}
|
) -> Result<String> {
|
||||||
|
let op = operator::load_operator_config()?;
|
||||||
pub(crate) fn resolve_policy_context(config: &OmnigraphConfig) -> Result<ResolvedPolicyContext> {
|
let resolve_name = |name: &str| {
|
||||||
let selected = config.resolve_policy_tooling_graph_selection()?;
|
op.cluster_root(name)
|
||||||
let policy_file = config.resolve_policy_file_for(selected).ok_or_else(|| {
|
.map(str::to_string)
|
||||||
color_eyre::eyre::eyre!(
|
.unwrap_or_else(|| name.to_string())
|
||||||
"policy.file or graphs.<name>.policy.file must be set in omnigraph.yaml"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let graph_id = match selected {
|
|
||||||
Some(name) => graph_resource_id_for_selection(Some(name), ""),
|
|
||||||
None => graph_resource_id_for_selection(None, "default"),
|
|
||||||
};
|
};
|
||||||
Ok(ResolvedPolicyContext {
|
if let Some(cluster) = cluster {
|
||||||
policy_file,
|
return Ok(resolve_name(cluster));
|
||||||
graph_id,
|
}
|
||||||
})
|
// A cluster profile (flag, else OMNIGRAPH_PROFILE) binds the cluster too.
|
||||||
|
let profile_name = profile
|
||||||
|
.map(str::to_string)
|
||||||
|
.or_else(|| std::env::var(scope::PROFILE_ENV).ok().filter(|s| !s.is_empty()));
|
||||||
|
if let Some(name) = profile_name {
|
||||||
|
let profile = op.profile(&name).ok_or_else(|| {
|
||||||
|
color_eyre::eyre::eyre!("unknown profile '{name}' (not defined under `profiles:`)")
|
||||||
|
})?;
|
||||||
|
if let crate::operator::ScopeBinding::Cluster(cluster) = profile.binding(&name)? {
|
||||||
|
return Ok(resolve_name(&cluster));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!(
|
||||||
|
"`{command}` needs a cluster — pass --cluster <dir|uri> (or a name from `clusters:` \
|
||||||
|
in ~/.omnigraph/config.yaml), or select a cluster profile"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_policy_engine(context: &ResolvedPolicyContext) -> Result<PolicyEngine> {
|
/// Read a cluster's serving snapshot for a control-plane tooling command,
|
||||||
PolicyEngine::load_graph(&context.policy_file, &context.graph_id)
|
/// flattening the readiness `Diagnostic` list into one loud error. The single
|
||||||
|
/// snapshot entry point for `policy`/`queries` so the not-servable message stays
|
||||||
|
/// identical across them.
|
||||||
|
async fn read_serving_snapshot_or_report(
|
||||||
|
cluster: &str,
|
||||||
|
) -> Result<omnigraph_cluster::ServingSnapshot> {
|
||||||
|
omnigraph_cluster::read_serving_snapshot(cluster)
|
||||||
|
.await
|
||||||
|
.map_err(|diagnostics| {
|
||||||
|
color_eyre::eyre::eyre!(
|
||||||
|
"cluster `{cluster}` is not servable:\n {}",
|
||||||
|
diagnostics
|
||||||
|
.iter()
|
||||||
|
.map(|d| d.message.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n ")
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_policy_engine_for_graph(graph: &ResolvedCliGraph) -> Result<PolicyEngine> {
|
/// Resolve the Cedar policy bundle(s) for a `--cluster` policy-tooling command
|
||||||
let policy_file = graph.policy_file.as_ref().ok_or_else(|| {
|
/// (RFC-011). Sources the applied policies from the cluster's serving snapshot;
|
||||||
color_eyre::eyre::eyre!(
|
/// each `ServingPolicy` carries its `source` (digest-verified content) and the
|
||||||
"policy.file or graphs.<name>.policy.file must be set in omnigraph.yaml"
|
/// scopes it `applies_to` (`cluster` | `graph.<id>`). The optional `graph`
|
||||||
)
|
/// selects a graph's bundle when several apply.
|
||||||
})?;
|
pub(crate) async fn read_cluster_policies(
|
||||||
PolicyEngine::load_graph(policy_file, &graph.graph_id)
|
cluster: &str,
|
||||||
|
) -> Result<Vec<omnigraph_cluster::ServingPolicy>> {
|
||||||
|
Ok(read_serving_snapshot_or_report(cluster).await?.policies)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn open_local_db_with_policy(graph: &ResolvedCliGraph) -> Result<Omnigraph> {
|
/// Pick the single policy bundle that applies to the selection. With `--graph`,
|
||||||
let db = Omnigraph::open(&graph.uri).await?;
|
/// the bundle bound to `graph.<id>` (or the cluster-wide one); without it, the
|
||||||
if graph.policy_file.is_some() {
|
/// sole bundle if there's exactly one. Ambiguity or absence is a loud error.
|
||||||
let engine = Arc::new(resolve_policy_engine_for_graph(graph)?);
|
pub(crate) fn select_cluster_policy<'p>(
|
||||||
Ok(db.with_policy(engine as Arc<dyn omnigraph_policy::PolicyChecker>))
|
cluster: &str,
|
||||||
} else {
|
policies: &'p [omnigraph_cluster::ServingPolicy],
|
||||||
Ok(db)
|
graph: Option<&str>,
|
||||||
|
) -> Result<&'p omnigraph_cluster::ServingPolicy> {
|
||||||
|
if let Some(graph_id) = graph {
|
||||||
|
let graph_ref = format!("graph.{graph_id}");
|
||||||
|
let matching: Vec<&omnigraph_cluster::ServingPolicy> = policies
|
||||||
|
.iter()
|
||||||
|
.filter(|p| {
|
||||||
|
p.applies_to
|
||||||
|
.iter()
|
||||||
|
.any(|s| s == &graph_ref || s == "cluster")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return match matching.as_slice() {
|
||||||
|
[only] => Ok(only),
|
||||||
|
[] => bail!(
|
||||||
|
"cluster `{cluster}` has no policy bundle bound to graph `{graph_id}` \
|
||||||
|
(or to the cluster scope)"
|
||||||
|
),
|
||||||
|
many => bail!(
|
||||||
|
"graph `{graph_id}` in cluster `{cluster}` matches {} policy bundles ([{}]); \
|
||||||
|
the cluster model expects one bundle per graph scope",
|
||||||
|
many.len(),
|
||||||
|
many.iter().map(|p| p.name.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match policies {
|
||||||
|
[only] => Ok(only),
|
||||||
|
[] => bail!("cluster `{cluster}` has no applied policy bundles"),
|
||||||
|
many => bail!(
|
||||||
|
"cluster `{cluster}` has {} policy bundles ([{}]); pass --graph <id> to select one",
|
||||||
|
many.len(),
|
||||||
|
many.iter().map(|p| p.name.as_str()).collect::<Vec<_>>().join(", ")
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// THE actor chain (RFC-007 §D3) — every command that needs an identity
|
/// THE actor chain (RFC-011) — every command that needs an identity
|
||||||
/// resolves through this one function (one path per concern):
|
/// resolves through this one function (one path per concern):
|
||||||
/// `--as` > legacy `cli.actor` in omnigraph.yaml (RFC-008 window) >
|
/// `--as` > `operator.actor` in ~/.omnigraph/config.yaml > none.
|
||||||
/// `operator.actor` in ~/.omnigraph/config.yaml > none.
|
pub(crate) fn resolve_actor(cli_as: Option<&str>) -> Result<Option<String>> {
|
||||||
pub(crate) fn resolve_actor(
|
|
||||||
cli_as: Option<&str>,
|
|
||||||
legacy_config_actor: Option<&str>,
|
|
||||||
) -> Result<Option<String>> {
|
|
||||||
if let Some(actor) = cli_as {
|
if let Some(actor) = cli_as {
|
||||||
return Ok(Some(actor.to_string()));
|
return Ok(Some(actor.to_string()));
|
||||||
}
|
}
|
||||||
if let Some(actor) = legacy_config_actor {
|
|
||||||
return Ok(Some(actor.to_string()));
|
|
||||||
}
|
|
||||||
Ok(operator::load_operator_config()?
|
Ok(operator::load_operator_config()?
|
||||||
.actor()
|
.actor()
|
||||||
.map(str::to_string))
|
.map(str::to_string))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_cluster_actor(cli_as: Option<&str>) -> Result<Option<String>> {
|
pub(crate) fn resolve_cluster_actor(cli_as: Option<&str>) -> Result<Option<String>> {
|
||||||
if let Some(actor) = cli_as {
|
resolve_actor(cli_as)
|
||||||
return Ok(Some(actor.to_string()));
|
|
||||||
}
|
|
||||||
let config = load_config(None).wrap_err(
|
|
||||||
"resolving the default actor from omnigraph.yaml (pass --as <ACTOR> to skip this lookup)",
|
|
||||||
)?;
|
|
||||||
resolve_actor(None, config.cli.actor.as_deref())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_cli_actor(
|
pub(crate) fn resolve_cli_actor(cli_as: Option<&str>) -> Result<Option<String>> {
|
||||||
cli_as: Option<&str>,
|
resolve_actor(cli_as)
|
||||||
config: &OmnigraphConfig,
|
|
||||||
) -> Result<Option<String>> {
|
|
||||||
resolve_actor(cli_as, config.cli.actor.as_deref())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_policy_tests_path(context: &ResolvedPolicyContext) -> PathBuf {
|
/// The bearer token for a remote request (RFC-011): the operator keyed chain
|
||||||
context.policy_file.with_file_name("policy.tests.yaml")
|
/// for the matching server (`OMNIGRAPH_TOKEN_<NAME>` env → 0600 credentials
|
||||||
}
|
/// file), then the default `OMNIGRAPH_BEARER_TOKEN` env. No omnigraph.yaml
|
||||||
|
/// chain.
|
||||||
pub(crate) fn normalize_policy_graph_uri(uri: &str) -> Result<String> {
|
pub(crate) fn resolve_remote_bearer_token(explicit_uri: Option<&str>) -> Result<Option<String>> {
|
||||||
if is_remote_uri(uri) {
|
|
||||||
Ok(uri.trim_end_matches('/').to_string())
|
|
||||||
} else {
|
|
||||||
Ok(normalize_root_uri(uri)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn resolve_remote_bearer_token(
|
|
||||||
config: &OmnigraphConfig,
|
|
||||||
explicit_uri: 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
|
// 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
|
// URL belongs to an operator-defined server, that server's keyed chain
|
||||||
// applies first — OMNIGRAPH_TOKEN_<NAME> env, then the 0600 credentials
|
// applies first — OMNIGRAPH_TOKEN_<NAME> env, then the 0600 credentials
|
||||||
// file. Ok(None) falls through to the legacy chain unchanged, and the
|
// file. The keyed token is structurally scoped to its own server: a URL
|
||||||
// keyed token is structurally scoped to its own server (§D5 rule 3):
|
// matching no operator server never sees it.
|
||||||
// a URL matching no operator server never sees it.
|
if let Some(remote_url) = explicit_uri.filter(|uri| is_remote_uri(uri)) {
|
||||||
if let Some(remote_url) = effective_remote_url(config, explicit_uri, explicit_target) {
|
|
||||||
let operator_config = operator::load_operator_config()?;
|
let operator_config = operator::load_operator_config()?;
|
||||||
if let Some(server) = operator_config.find_server_for_url(&remote_url) {
|
if let Some(server) = operator_config.find_server_for_url(remote_url) {
|
||||||
if let Some(token) = operator::resolve_keyed_token(server)? {
|
if let Some(token) = operator::resolve_keyed_token(server)? {
|
||||||
return Ok(Some(token));
|
return Ok(Some(token));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scoped_env =
|
Ok(bearer_token_from_env(DEFAULT_BEARER_TOKEN_ENV))
|
||||||
config.graph_bearer_token_env(explicit_uri, explicit_target, config.cli_graph_name());
|
|
||||||
let mut env_names = Vec::new();
|
|
||||||
if let Some(name) = scoped_env {
|
|
||||||
env_names.push(name.to_string());
|
|
||||||
}
|
|
||||||
if env_names
|
|
||||||
.iter()
|
|
||||||
.all(|name| name != DEFAULT_BEARER_TOKEN_ENV)
|
|
||||||
{
|
|
||||||
env_names.push(DEFAULT_BEARER_TOKEN_ENV.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let env_file = config.resolve_auth_env_file();
|
|
||||||
for env_name in env_names {
|
|
||||||
if let Some(token) = bearer_token_from_env(&env_name) {
|
|
||||||
return Ok(Some(token));
|
|
||||||
}
|
|
||||||
if let Some(path) = env_file.as_ref() {
|
|
||||||
if let Some(token) = bearer_token_from_env_file(path, &env_name)? {
|
|
||||||
return Ok(Some(token));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `--server <name>` (RFC-007 PR 3): resolve an operator-defined server
|
/// `--server <name>` (RFC-007 PR 3): resolve an operator-defined server
|
||||||
|
|
@ -391,7 +324,6 @@ pub(crate) fn resolve_server_flag(
|
||||||
/// params. The keyed token applies via the ordinary URL match.
|
/// params. The keyed token applies via the ordinary URL match.
|
||||||
pub(crate) async fn execute_operator_alias(
|
pub(crate) async fn execute_operator_alias(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
config: &OmnigraphConfig,
|
|
||||||
alias_name: &str,
|
alias_name: &str,
|
||||||
alias: &crate::operator::OperatorAlias,
|
alias: &crate::operator::OperatorAlias,
|
||||||
alias_args: &[String],
|
alias_args: &[String],
|
||||||
|
|
@ -399,7 +331,7 @@ pub(crate) async fn execute_operator_alias(
|
||||||
) -> Result<ReadOutput> {
|
) -> Result<ReadOutput> {
|
||||||
let uri = resolve_server_flag(Some(&alias.server), alias.graph.as_deref())?
|
let uri = resolve_server_flag(Some(&alias.server), alias.graph.as_deref())?
|
||||||
.expect("server name is present");
|
.expect("server name is present");
|
||||||
let bearer_token = resolve_remote_bearer_token(config, Some(&uri))?;
|
let bearer_token = resolve_remote_bearer_token(Some(&uri))?;
|
||||||
|
|
||||||
let mut params = serde_json::Map::new();
|
let mut params = serde_json::Map::new();
|
||||||
for (key, value) in &alias.params {
|
for (key, value) in &alias.params {
|
||||||
|
|
@ -454,22 +386,6 @@ pub(crate) fn apply_server_flag(
|
||||||
resolve_server_flag(server, 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.
|
|
||||||
fn effective_remote_url(
|
|
||||||
config: &OmnigraphConfig,
|
|
||||||
explicit_uri: Option<&str>,
|
|
||||||
explicit_target: Option<&str>,
|
|
||||||
) -> Option<String> {
|
|
||||||
if let Some(uri) = explicit_uri {
|
|
||||||
return is_remote_uri(uri).then(|| uri.to_string());
|
|
||||||
}
|
|
||||||
let target = config.resolve_target_name(explicit_uri, explicit_target, config.cli_graph_name())?;
|
|
||||||
let uri = &config.graphs.get(target)?.uri;
|
|
||||||
is_remote_uri(uri).then(|| uri.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build_http_client() -> Result<reqwest::Client> {
|
pub(crate) fn build_http_client() -> Result<reqwest::Client> {
|
||||||
Ok(reqwest::Client::new())
|
Ok(reqwest::Client::new())
|
||||||
}
|
}
|
||||||
|
|
@ -510,40 +426,31 @@ pub(crate) async fn remote_json<T: DeserializeOwned>(
|
||||||
Ok(serde_json::from_str(&text)?)
|
Ok(serde_json::from_str(&text)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_uri(config: &OmnigraphConfig, cli_uri: Option<String>) -> Result<String> {
|
/// The graph URI a command addresses (RFC-011): the scope-resolved URI string
|
||||||
// `--target` is gone; the second arg (the legacy explicit-target name) is
|
/// (positional URI / `--store` / `--profile` / `defaults.store`). No
|
||||||
// always None. A bare command still falls back to `cli.graph` (the third arg).
|
/// omnigraph.yaml `cli.graph` fallback — an absent address is a loud error.
|
||||||
config.resolve_target_uri(cli_uri, None, config.cli_graph_name())
|
pub(crate) fn resolve_uri(cli_uri: Option<String>) -> Result<String> {
|
||||||
|
cli_uri.ok_or_else(|| {
|
||||||
|
color_eyre::eyre::eyre!(
|
||||||
|
"no graph addressed — pass a positional URI, --store <uri>, --server <name>, \
|
||||||
|
--profile <name>, or set a default scope in ~/.omnigraph/config.yaml"
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_cli_graph(
|
pub(crate) fn resolve_cli_graph(cli_uri: Option<String>) -> Result<ResolvedCliGraph> {
|
||||||
config: &OmnigraphConfig,
|
let uri = resolve_uri(cli_uri)?;
|
||||||
cli_uri: Option<String>,
|
|
||||||
) -> Result<ResolvedCliGraph> {
|
|
||||||
let selected = if cli_uri.is_some() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
config.cli_graph_name().map(str::to_string)
|
|
||||||
};
|
|
||||||
config.resolve_graph_selection(selected.as_deref())?;
|
|
||||||
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 {
|
Ok(ResolvedCliGraph {
|
||||||
graph_id,
|
|
||||||
is_remote: is_remote_uri(&uri),
|
is_remote: is_remote_uri(&uri),
|
||||||
policy_file: config.resolve_policy_file_for(selected.as_deref()),
|
|
||||||
selected,
|
|
||||||
uri,
|
uri,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_local_graph(
|
pub(crate) fn resolve_local_graph(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
cli_uri: Option<String>,
|
cli_uri: Option<String>,
|
||||||
operation: &str,
|
operation: &str,
|
||||||
) -> Result<ResolvedCliGraph> {
|
) -> Result<ResolvedCliGraph> {
|
||||||
let graph = resolve_cli_graph(config, cli_uri)?;
|
let graph = resolve_cli_graph(cli_uri)?;
|
||||||
if graph.is_remote {
|
if graph.is_remote {
|
||||||
bail!(
|
bail!(
|
||||||
"`{}` is a direct (storage-native) command and needs direct storage \
|
"`{}` is a direct (storage-native) command and needs direct storage \
|
||||||
|
|
@ -586,22 +493,19 @@ pub(crate) fn parse_duration_arg(s: &str) -> Result<std::time::Duration> {
|
||||||
Ok(std::time::Duration::from_secs(secs))
|
Ok(std::time::Duration::from_secs(secs))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_local_uri(
|
pub(crate) fn resolve_local_uri(cli_uri: Option<String>, operation: &str) -> Result<String> {
|
||||||
config: &OmnigraphConfig,
|
Ok(resolve_local_graph(cli_uri, operation)?.uri)
|
||||||
cli_uri: Option<String>,
|
|
||||||
operation: &str,
|
|
||||||
) -> Result<String> {
|
|
||||||
Ok(resolve_local_graph(config, cli_uri, operation)?.uri)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a maintenance verb's (optimize/repair/cleanup) address to a direct
|
/// Resolve a direct (storage-native) verb's address to a storage URI through the
|
||||||
/// storage URI through the one RFC-011 scope path. Every primitive funnels
|
/// one RFC-011 scope path — the maintenance verbs (optimize/repair/cleanup) plus
|
||||||
/// here: a positional URI, `--store`, `--cluster <root> --graph <id>`, a
|
/// `schema plan` and `lint`'s graph-target path. Every primitive funnels here: a
|
||||||
/// `--profile` cluster binding, or operator defaults — all resolved at the
|
/// positional URI, `--store`, `--cluster <root> --graph <id>`, a `--profile`
|
||||||
/// `Direct` capability (so a server scope is rejected, a cluster scope is
|
/// cluster binding, or operator defaults — all resolved at the `Direct`
|
||||||
/// allowed), then mapped to a storage URI by `resolve_storage_uri`.
|
/// capability (so a server scope is rejected, a cluster scope is allowed when the
|
||||||
|
/// verb opts into cluster addressing), then mapped to a storage URI by
|
||||||
|
/// `resolve_storage_uri`.
|
||||||
pub(crate) async fn resolve_maintenance_uri(
|
pub(crate) async fn resolve_maintenance_uri(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
profile: Option<&str>,
|
profile: Option<&str>,
|
||||||
store: Option<&str>,
|
store: Option<&str>,
|
||||||
cluster: Option<&str>,
|
cluster: Option<&str>,
|
||||||
|
|
@ -622,7 +526,6 @@ pub(crate) async fn resolve_maintenance_uri(
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
resolve_storage_uri(
|
resolve_storage_uri(
|
||||||
config,
|
|
||||||
scope.uri,
|
scope.uri,
|
||||||
scope.cluster.as_deref(),
|
scope.cluster.as_deref(),
|
||||||
scope.cluster_graph.as_deref(),
|
scope.cluster_graph.as_deref(),
|
||||||
|
|
@ -639,7 +542,6 @@ pub(crate) async fn resolve_maintenance_uri(
|
||||||
/// automatically, otherwise error and list the candidates so the operator can
|
/// automatically, otherwise error and list the candidates so the operator can
|
||||||
/// pass `--graph <id>`.
|
/// pass `--graph <id>`.
|
||||||
pub(crate) async fn resolve_storage_uri(
|
pub(crate) async fn resolve_storage_uri(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
cli_uri: Option<String>,
|
cli_uri: Option<String>,
|
||||||
cluster: Option<&str>,
|
cluster: Option<&str>,
|
||||||
cluster_graph: Option<&str>,
|
cluster_graph: Option<&str>,
|
||||||
|
|
@ -651,7 +553,7 @@ pub(crate) async fn resolve_storage_uri(
|
||||||
let graph_id = resolve_sole_cluster_graph(cluster).await?;
|
let graph_id = resolve_sole_cluster_graph(cluster).await?;
|
||||||
resolve_cluster_graph_uri(cluster, &graph_id).await
|
resolve_cluster_graph_uri(cluster, &graph_id).await
|
||||||
}
|
}
|
||||||
(None, None) => resolve_local_uri(config, cli_uri, operation),
|
(None, None) => resolve_local_uri(cli_uri, operation),
|
||||||
(None, Some(_)) => {
|
(None, Some(_)) => {
|
||||||
bail!("internal error: a graph was selected without a cluster scope")
|
bail!("internal error: a graph was selected without a cluster scope")
|
||||||
}
|
}
|
||||||
|
|
@ -687,19 +589,16 @@ async fn resolve_cluster_graph_uri(cluster: &str, graph_id: &str) -> Result<Stri
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_branch(
|
pub(crate) fn resolve_branch(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
cli_branch: Option<String>,
|
cli_branch: Option<String>,
|
||||||
alias_branch: Option<String>,
|
alias_branch: Option<String>,
|
||||||
default_branch: &str,
|
default_branch: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
cli_branch
|
cli_branch
|
||||||
.or(alias_branch)
|
.or(alias_branch)
|
||||||
.or_else(|| config.cli.branch.clone())
|
|
||||||
.unwrap_or_else(|| default_branch.to_string())
|
.unwrap_or_else(|| default_branch.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_read_target(
|
pub(crate) fn resolve_read_target(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
cli_branch: Option<String>,
|
cli_branch: Option<String>,
|
||||||
cli_snapshot: Option<String>,
|
cli_snapshot: Option<String>,
|
||||||
alias_branch: Option<String>,
|
alias_branch: Option<String>,
|
||||||
|
|
@ -707,19 +606,15 @@ pub(crate) fn resolve_read_target(
|
||||||
if cli_branch.is_some() && cli_snapshot.is_some() {
|
if cli_branch.is_some() && cli_snapshot.is_some() {
|
||||||
bail!("read target may specify branch or snapshot, not both");
|
bail!("read target may specify branch or snapshot, not both");
|
||||||
}
|
}
|
||||||
Ok(read_target_from_cli(
|
Ok(read_target_from_cli(cli_branch.or(alias_branch), cli_snapshot))
|
||||||
cli_branch
|
|
||||||
.or(alias_branch)
|
|
||||||
.or_else(|| config.cli.branch.clone()),
|
|
||||||
cli_snapshot,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_query_path(
|
pub(crate) fn resolve_query_path(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
explicit_query: Option<&PathBuf>,
|
explicit_query: Option<&PathBuf>,
|
||||||
alias_query: Option<&str>,
|
alias_query: Option<&str>,
|
||||||
) -> Result<PathBuf> {
|
) -> Result<PathBuf> {
|
||||||
|
// The `.gq` path is resolved plainly (cwd-relative) — no omnigraph.yaml
|
||||||
|
// `query.roots` search.
|
||||||
explicit_query
|
explicit_query
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| alias_query.map(PathBuf::from))
|
.or_else(|| alias_query.map(PathBuf::from))
|
||||||
|
|
@ -728,11 +623,9 @@ pub(crate) fn resolve_query_path(
|
||||||
"exactly one of --query, --query-string, or --alias must be provided"
|
"exactly one of --query, --query-string, or --alias must be provided"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.and_then(|query_path| config.resolve_query_path(&query_path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_query_source(
|
pub(crate) fn resolve_query_source(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
explicit_query: Option<&PathBuf>,
|
explicit_query: Option<&PathBuf>,
|
||||||
inline_query: Option<&str>,
|
inline_query: Option<&str>,
|
||||||
alias_query: Option<&str>,
|
alias_query: Option<&str>,
|
||||||
|
|
@ -744,7 +637,6 @@ pub(crate) fn resolve_query_source(
|
||||||
return Ok(inline.to_string());
|
return Ok(inline.to_string());
|
||||||
}
|
}
|
||||||
Ok(fs::read_to_string(resolve_query_path(
|
Ok(fs::read_to_string(resolve_query_path(
|
||||||
config,
|
|
||||||
explicit_query,
|
explicit_query,
|
||||||
alias_query,
|
alias_query,
|
||||||
)?)?)
|
)?)?)
|
||||||
|
|
@ -754,11 +646,9 @@ pub(crate) fn parse_alias_value(value: &str) -> Value {
|
||||||
serde_json::from_str(value).unwrap_or_else(|_| Value::String(value.to_string()))
|
serde_json::from_str(value).unwrap_or_else(|_| Value::String(value.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The format cascade (RFC-007 §D3): `--json` > `--format` > alias format >
|
/// The format cascade (RFC-011): `--json` > `--format` > alias format >
|
||||||
/// legacy `cli.output_format` (RFC-008 window) > operator `defaults.output`
|
/// operator `defaults.output` > table.
|
||||||
/// > table.
|
|
||||||
pub(crate) fn resolve_read_format(
|
pub(crate) fn resolve_read_format(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
cli_format: Option<ReadOutputFormat>,
|
cli_format: Option<ReadOutputFormat>,
|
||||||
json: bool,
|
json: bool,
|
||||||
alias_format: Option<ReadOutputFormat>,
|
alias_format: Option<ReadOutputFormat>,
|
||||||
|
|
@ -768,7 +658,6 @@ pub(crate) fn resolve_read_format(
|
||||||
}
|
}
|
||||||
cli_format
|
cli_format
|
||||||
.or(alias_format)
|
.or(alias_format)
|
||||||
.or(config.cli.output_format)
|
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
operator::load_operator_config()
|
operator::load_operator_config()
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -825,12 +714,11 @@ pub(crate) fn query_params_from_json(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn execute_query_lint(
|
pub(crate) async fn execute_query_lint(
|
||||||
config: &OmnigraphConfig,
|
|
||||||
cli_uri: Option<String>,
|
cli_uri: Option<String>,
|
||||||
schema_path: Option<&PathBuf>,
|
schema_path: Option<&PathBuf>,
|
||||||
query_path: &PathBuf,
|
query_path: &PathBuf,
|
||||||
) -> Result<QueryLintOutput> {
|
) -> Result<QueryLintOutput> {
|
||||||
let resolved_query_path = resolve_query_path(config, Some(query_path), None)?;
|
let resolved_query_path = resolve_query_path(Some(query_path), None)?;
|
||||||
let query_source = fs::read_to_string(&resolved_query_path)?;
|
let query_source = fs::read_to_string(&resolved_query_path)?;
|
||||||
let query_path = resolved_query_path.to_string_lossy().into_owned();
|
let query_path = resolved_query_path.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
|
@ -848,12 +736,14 @@ pub(crate) async fn execute_query_lint(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_graph_target = cli_uri.is_some() || config.cli_graph_name().is_some();
|
if cli_uri.is_none() {
|
||||||
if !has_graph_target {
|
bail!(
|
||||||
bail!("lint requires --schema <schema.pg> or a resolvable graph target");
|
"lint requires --schema <schema.pg> (offline) or a graph target \
|
||||||
|
(--store <uri> / --cluster <dir> --graph <id>)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = resolve_local_uri(config, cli_uri, "lint")?;
|
let uri = resolve_local_uri(cli_uri, "lint")?;
|
||||||
let db = Omnigraph::open(&uri).await?;
|
let db = Omnigraph::open(&uri).await?;
|
||||||
Ok(lint_query_file(
|
Ok(lint_query_file(
|
||||||
&db.catalog(),
|
&db.catalog(),
|
||||||
|
|
@ -863,20 +753,24 @@ pub(crate) async fn execute_query_lint(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn resolve_selected_graph(
|
/// Build a `QueryRegistry` from a cluster serving snapshot's stored queries,
|
||||||
config: &OmnigraphConfig,
|
/// optionally scoped to one graph. The `ServingQuery.source` is the
|
||||||
cli_uri: Option<String>,
|
/// digest-verified `.gq` content, so no file I/O or omnigraph.yaml is involved.
|
||||||
operation: &str,
|
fn registry_from_serving_queries(
|
||||||
) -> Result<(String, Option<String>)> {
|
queries: &[omnigraph_cluster::ServingQuery],
|
||||||
let graph = resolve_local_graph(config, cli_uri, operation)?;
|
graph: Option<&str>,
|
||||||
Ok((graph.uri, graph.selected))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn load_registry_or_report(
|
|
||||||
config: &OmnigraphConfig,
|
|
||||||
selected: Option<&str>,
|
|
||||||
) -> Result<QueryRegistry> {
|
) -> Result<QueryRegistry> {
|
||||||
QueryRegistry::load(config, config.query_entries_for(selected)).map_err(|errors| {
|
let specs: Vec<omnigraph_server::queries::RegistrySpec> = queries
|
||||||
|
.iter()
|
||||||
|
.filter(|q| graph.is_none_or(|g| q.graph_id == g))
|
||||||
|
.map(|q| omnigraph_server::queries::RegistrySpec {
|
||||||
|
name: q.name.clone(),
|
||||||
|
source: q.source.clone(),
|
||||||
|
expose: false,
|
||||||
|
tool_name: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
QueryRegistry::from_specs(specs).map_err(|errors| {
|
||||||
color_eyre::eyre::eyre!(
|
color_eyre::eyre::eyre!(
|
||||||
"stored-query registry failed to load:\n {}",
|
"stored-query registry failed to load:\n {}",
|
||||||
errors
|
errors
|
||||||
|
|
@ -888,83 +782,58 @@ pub(crate) fn load_registry_or_report(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn graph_query_registry_names(config: &OmnigraphConfig) -> Vec<&str> {
|
|
||||||
config
|
|
||||||
.graphs
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(name, graph)| (!graph.queries.is_empty()).then_some(name.as_str()))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn resolve_registry_selection_for_list(
|
|
||||||
config: &OmnigraphConfig,
|
|
||||||
) -> Result<Option<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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.query_entries().is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let graph_names = graph_query_registry_names(config);
|
|
||||||
if graph_names.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
bail!(
|
|
||||||
"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(", "),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn validate_registry_for_catalog(
|
|
||||||
registry: &QueryRegistry,
|
|
||||||
catalog: &omnigraph_compiler::catalog::Catalog,
|
|
||||||
label: &str,
|
|
||||||
) -> omnigraph::error::Result<()> {
|
|
||||||
let report = check(registry, catalog);
|
|
||||||
if report.has_breakages() {
|
|
||||||
return Err(omnigraph::error::OmniError::manifest(
|
|
||||||
format_check_breakages(label, &report),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/// `queries validate --cluster <dir>` (RFC-011): type-check every stored query
|
||||||
|
/// in the cluster catalog against its graph's applied schema. Both the registry
|
||||||
|
/// and the schemas come from the cluster serving snapshot — no omnigraph.yaml.
|
||||||
|
/// With `--graph`, scope to a single graph.
|
||||||
pub(crate) async fn execute_queries_validate(
|
pub(crate) async fn execute_queries_validate(
|
||||||
uri: Option<String>,
|
cluster: &str,
|
||||||
config_path: Option<&PathBuf>,
|
graph: Option<&str>,
|
||||||
json: bool,
|
json: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let config = load_cli_config(config_path)?;
|
let snapshot = read_serving_snapshot_or_report(cluster).await?;
|
||||||
// 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(®istry, &db.catalog());
|
|
||||||
|
|
||||||
let output = QueriesValidateOutput {
|
// Type-check per graph: each graph's stored queries against its own schema
|
||||||
ok: !report.has_breakages(),
|
// (read from the graph's applied storage root). A `--graph` filter scopes to
|
||||||
breakages: report
|
// exactly one graph; an unknown id is a loud error.
|
||||||
.breakages
|
let mut breakages = Vec::new();
|
||||||
.iter()
|
let mut warnings = Vec::new();
|
||||||
.map(|b| QueriesIssue {
|
let mut total = 0usize;
|
||||||
|
let mut matched_any = false;
|
||||||
|
for serving_graph in &snapshot.graphs {
|
||||||
|
if graph.is_some_and(|g| g != serving_graph.graph_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matched_any = true;
|
||||||
|
let registry = registry_from_serving_queries(&snapshot.queries, Some(&serving_graph.graph_id))?;
|
||||||
|
let db = Omnigraph::open(&serving_graph.root.to_string_lossy()).await?;
|
||||||
|
let report = check(®istry, &db.catalog());
|
||||||
|
total += registry.len();
|
||||||
|
for b in &report.breakages {
|
||||||
|
breakages.push(QueriesIssue {
|
||||||
query: b.query.clone(),
|
query: b.query.clone(),
|
||||||
message: b.message.clone(),
|
message: b.message.clone(),
|
||||||
})
|
});
|
||||||
.collect(),
|
}
|
||||||
warnings: report
|
for w in &report.warnings {
|
||||||
.warnings
|
warnings.push(QueriesIssue {
|
||||||
.iter()
|
|
||||||
.map(|w| QueriesIssue {
|
|
||||||
query: w.query.clone(),
|
query: w.query.clone(),
|
||||||
message: w.message.clone(),
|
message: w.message.clone(),
|
||||||
})
|
});
|
||||||
.collect(),
|
}
|
||||||
|
}
|
||||||
|
if let Some(graph_id) = graph {
|
||||||
|
if !matched_any {
|
||||||
|
bail!("graph `{graph_id}` is not applied in cluster `{cluster}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_breakages = !breakages.is_empty();
|
||||||
|
let output = QueriesValidateOutput {
|
||||||
|
ok: !has_breakages,
|
||||||
|
breakages,
|
||||||
|
warnings,
|
||||||
};
|
};
|
||||||
|
|
||||||
if json {
|
if json {
|
||||||
|
|
@ -973,8 +842,8 @@ pub(crate) async fn execute_queries_validate(
|
||||||
if output.breakages.is_empty() {
|
if output.breakages.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"OK {} stored quer{} type-check against the schema",
|
"OK {} stored quer{} type-check against the schema",
|
||||||
registry.len(),
|
total,
|
||||||
if registry.len() == 1 { "y" } else { "ies" }
|
if total == 1 { "y" } else { "ies" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for issue in &output.breakages {
|
for issue in &output.breakages {
|
||||||
|
|
@ -985,17 +854,22 @@ pub(crate) async fn execute_queries_validate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if report.has_breakages() {
|
if has_breakages {
|
||||||
io::stdout().flush()?;
|
io::stdout().flush()?;
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn execute_queries_list(config_path: Option<&PathBuf>, json: bool) -> Result<()> {
|
/// `queries list --cluster <dir>` (RFC-011): list the catalog's stored queries.
|
||||||
let config = load_cli_config(config_path)?;
|
/// With `--graph`, scope to one graph.
|
||||||
let selected = resolve_registry_selection_for_list(&config)?;
|
pub(crate) async fn execute_queries_list(
|
||||||
let registry = load_registry_or_report(&config, selected.as_deref())?;
|
cluster: &str,
|
||||||
|
graph: Option<&str>,
|
||||||
|
json: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let snapshot = read_serving_snapshot_or_report(cluster).await?;
|
||||||
|
let registry = registry_from_serving_queries(&snapshot.queries, graph)?;
|
||||||
|
|
||||||
let output = QueriesListOutput {
|
let output = QueriesListOutput {
|
||||||
queries: registry
|
queries: registry
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use clap::{Arg, ArgAction, Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
use clap::{Arg, ArgAction, Args, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
||||||
use color_eyre::eyre::{Result, WrapErr, bail};
|
use color_eyre::eyre::{Result, bail};
|
||||||
use omnigraph::db::{Omnigraph, ReadTarget, SnapshotId};
|
use omnigraph::db::{Omnigraph, ReadTarget, SnapshotId};
|
||||||
use omnigraph::loader::LoadMode;
|
use omnigraph::loader::LoadMode;
|
||||||
use omnigraph::storage::normalize_root_uri;
|
|
||||||
use omnigraph_cluster::{
|
use omnigraph_cluster::{
|
||||||
ApplyOptions, ApplyOutput, ApproveOutput, DiagnosticSeverity, ForceUnlockOutput, PlanOutput, StateSyncOutput, StatusOutput,
|
ApplyOptions, ApplyOutput, ApproveOutput, DiagnosticSeverity, ForceUnlockOutput, PlanOutput, StateSyncOutput, StatusOutput,
|
||||||
ValidateOutput, apply_config_dir_with_options, approve_config_dir, force_unlock_config_dir, import_config_dir, plan_config_dir,
|
ValidateOutput, apply_config_dir_with_options, approve_config_dir, force_unlock_config_dir, import_config_dir, plan_config_dir,
|
||||||
|
|
@ -26,9 +22,9 @@ use omnigraph_api_types::{
|
||||||
ChangeOutput, CommitOutput, ErrorOutput, IngestOutput, ReadOutput, SchemaApplyOutput,
|
ChangeOutput, CommitOutput, ErrorOutput, IngestOutput, ReadOutput, SchemaApplyOutput,
|
||||||
SnapshotTableOutput,
|
SnapshotTableOutput,
|
||||||
};
|
};
|
||||||
use omnigraph_server::queries::{QueryRegistry, check, format_check_breakages};
|
use omnigraph_server::queries::{QueryRegistry, check};
|
||||||
use omnigraph_server::{
|
use omnigraph_server::{
|
||||||
OmnigraphConfig, PolicyAction, PolicyDecision, PolicyEngine, PolicyRequest,
|
PolicyAction, PolicyDecision, PolicyEngine, PolicyRequest,
|
||||||
PolicyTestConfig, ReadOutputFormat, graph_resource_id_for_selection, load_config,
|
PolicyTestConfig, ReadOutputFormat, graph_resource_id_for_selection, load_config,
|
||||||
};
|
};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
|
|
@ -170,16 +166,13 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Command::Load {
|
Command::Load {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
data,
|
data,
|
||||||
branch,
|
branch,
|
||||||
from,
|
from,
|
||||||
mode,
|
mode,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -188,7 +181,7 @@ async fn main() -> Result<()> {
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let branch = resolve_branch(&config, branch, None, "main");
|
let branch = resolve_branch(branch, None, "main");
|
||||||
if matches!(mode, CliLoadMode::Overwrite) {
|
if matches!(mode, CliLoadMode::Overwrite) {
|
||||||
confirm_destructive("load --mode overwrite", client.uri(), cli.yes, json)?;
|
confirm_destructive("load --mode overwrite", client.uri(), cli.yes, json)?;
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +197,6 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Command::Ingest {
|
Command::Ingest {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
data,
|
data,
|
||||||
branch,
|
branch,
|
||||||
from,
|
from,
|
||||||
|
|
@ -216,9 +208,7 @@ async fn main() -> Result<()> {
|
||||||
"warning: `omnigraph ingest` is deprecated and will be removed in a future release; \
|
"warning: `omnigraph ingest` is deprecated and will be removed in a future release; \
|
||||||
use `omnigraph load --from <base> --mode <mode>` (ingest defaults: --from main --mode merge)"
|
use `omnigraph load --from <base> --mode <mode>` (ingest defaults: --from main --mode merge)"
|
||||||
);
|
);
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -227,8 +217,8 @@ async fn main() -> Result<()> {
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let branch = resolve_branch(&config, branch, None, "main");
|
let branch = resolve_branch(branch, None, "main");
|
||||||
let from = resolve_branch(&config, from, None, "main");
|
let from = resolve_branch(from, None, "main");
|
||||||
echo_write_target(cli.quiet, "ingest", client.uri(), client.is_remote());
|
echo_write_target(cli.quiet, "ingest", client.uri(), client.is_remote());
|
||||||
let payload = client
|
let payload = client
|
||||||
.ingest(&branch, &from, &data.to_string_lossy(), mode)
|
.ingest(&branch, &from, &data.to_string_lossy(), mode)
|
||||||
|
|
@ -242,14 +232,11 @@ async fn main() -> Result<()> {
|
||||||
Command::Branch { command } => match command {
|
Command::Branch { command } => match command {
|
||||||
BranchCommand::Create {
|
BranchCommand::Create {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
from,
|
from,
|
||||||
name,
|
name,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -258,7 +245,7 @@ async fn main() -> Result<()> {
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let from = resolve_branch(&config, from, None, "main");
|
let from = resolve_branch(from, None, "main");
|
||||||
echo_write_target(cli.quiet, "branch create", client.uri(), client.is_remote());
|
echo_write_target(cli.quiet, "branch create", client.uri(), client.is_remote());
|
||||||
let payload = client.branch_create_from(&from, &name).await?;
|
let payload = client.branch_create_from(&from, &name).await?;
|
||||||
if json {
|
if json {
|
||||||
|
|
@ -269,12 +256,9 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
BranchCommand::List {
|
BranchCommand::List {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -293,13 +277,10 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
BranchCommand::Delete {
|
BranchCommand::Delete {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
name,
|
name,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -319,14 +300,11 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
BranchCommand::Merge {
|
BranchCommand::Merge {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
source,
|
source,
|
||||||
into,
|
into,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -335,7 +313,7 @@ async fn main() -> Result<()> {
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let into = resolve_branch(&config, into, None, "main");
|
let into = resolve_branch(into, None, "main");
|
||||||
echo_write_target(cli.quiet, "branch merge", client.uri(), client.is_remote());
|
echo_write_target(cli.quiet, "branch merge", client.uri(), client.is_remote());
|
||||||
let payload = client.branch_merge(&source, &into).await?;
|
let payload = client.branch_merge(&source, &into).await?;
|
||||||
if json {
|
if json {
|
||||||
|
|
@ -353,13 +331,10 @@ async fn main() -> Result<()> {
|
||||||
Command::Commit { command } => match command {
|
Command::Commit { command } => match command {
|
||||||
CommitCommand::List {
|
CommitCommand::List {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
branch,
|
branch,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -376,13 +351,10 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
CommitCommand::Show {
|
CommitCommand::Show {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
commit_id,
|
commit_id,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -401,13 +373,19 @@ async fn main() -> Result<()> {
|
||||||
Command::Schema { command } => match command {
|
Command::Schema { command } => match command {
|
||||||
SchemaCommand::Plan {
|
SchemaCommand::Plan {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
schema,
|
schema,
|
||||||
json,
|
json,
|
||||||
allow_data_loss,
|
allow_data_loss,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
let uri = resolve_maintenance_uri(
|
||||||
let uri = resolve_local_uri(&config, uri, "schema plan")?;
|
cli.profile.as_deref(),
|
||||||
|
cli.store.as_deref(),
|
||||||
|
cli.cluster.as_deref(),
|
||||||
|
cli.graph.as_deref(),
|
||||||
|
uri,
|
||||||
|
"schema plan",
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let schema_source = fs::read_to_string(&schema)?;
|
let schema_source = fs::read_to_string(&schema)?;
|
||||||
let db = Omnigraph::open(&uri).await?;
|
let db = Omnigraph::open(&uri).await?;
|
||||||
let plan = db
|
let plan = db
|
||||||
|
|
@ -430,14 +408,11 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
SchemaCommand::Apply {
|
SchemaCommand::Apply {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
schema,
|
schema,
|
||||||
json,
|
json,
|
||||||
allow_data_loss,
|
allow_data_loss,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -447,25 +422,14 @@ async fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let schema_source = fs::read_to_string(&schema)?;
|
let schema_source = fs::read_to_string(&schema)?;
|
||||||
// The stored-query registry check is an embedded-only concern
|
// The embedded (direct-store) arm carries no stored-query
|
||||||
// (the remote arm ignores the validator — the server runs its
|
// registry — the registry is cluster-owned (RFC-011), so a
|
||||||
// own check); build it only for the local path so the remote
|
// direct apply has nothing to validate against. The served arm
|
||||||
// path keeps its no-registry-load behavior.
|
// runs the server's own catalog check. So the validator is a
|
||||||
let registry = if client.is_remote() {
|
// no-op here on both arms.
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let registry = load_registry_or_report(&config, client.selected())?;
|
|
||||||
(!registry.is_empty()).then_some(registry)
|
|
||||||
};
|
|
||||||
let label = client.selected().unwrap_or(client.uri()).to_string();
|
|
||||||
echo_write_target(cli.quiet, "schema apply", client.uri(), client.is_remote());
|
echo_write_target(cli.quiet, "schema apply", client.uri(), client.is_remote());
|
||||||
let output = client
|
let output = client
|
||||||
.apply_schema(&schema_source, allow_data_loss, |catalog| {
|
.apply_schema(&schema_source, allow_data_loss, |_catalog| Ok(()))
|
||||||
if let Some(registry) = registry.as_ref() {
|
|
||||||
validate_registry_for_catalog(registry, catalog, &label)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
if json {
|
if json {
|
||||||
print_json(&output)?;
|
print_json(&output)?;
|
||||||
|
|
@ -475,12 +439,9 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
SchemaCommand::Show {
|
SchemaCommand::Show {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -498,41 +459,50 @@ async fn main() -> Result<()> {
|
||||||
},
|
},
|
||||||
Command::Lint {
|
Command::Lint {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
query,
|
query,
|
||||||
schema,
|
schema,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
// A graph target (when `--schema` is absent) resolves through the
|
||||||
let output =
|
// direct scope path (positional URI / --store / --profile /
|
||||||
execute_query_lint(&config, uri, schema.as_ref(), &query)
|
// defaults.store). Offline (`--schema`) needs no graph, so leave
|
||||||
.await?;
|
// the uri unresolved in that case.
|
||||||
|
let graph_uri = if schema.is_some() {
|
||||||
|
uri
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
resolve_maintenance_uri(
|
||||||
|
cli.profile.as_deref(),
|
||||||
|
cli.store.as_deref(),
|
||||||
|
cli.cluster.as_deref(),
|
||||||
|
cli.graph.as_deref(),
|
||||||
|
uri,
|
||||||
|
"lint",
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let output = execute_query_lint(graph_uri, schema.as_ref(), &query).await?;
|
||||||
finish_query_lint(&output, json)?;
|
finish_query_lint(&output, json)?;
|
||||||
}
|
}
|
||||||
Command::Queries { command } => match command {
|
Command::Queries { command } => {
|
||||||
QueriesCommand::Validate {
|
let cluster =
|
||||||
uri,
|
require_cluster_scope(cli.cluster.as_deref(), cli.profile.as_deref(), "queries")?;
|
||||||
config,
|
match command {
|
||||||
json,
|
QueriesCommand::Validate { json } => {
|
||||||
} => {
|
execute_queries_validate(&cluster, cli.graph.as_deref(), json).await?;
|
||||||
execute_queries_validate(uri, config.as_ref(), json).await?;
|
}
|
||||||
|
QueriesCommand::List { json } => {
|
||||||
|
execute_queries_list(&cluster, cli.graph.as_deref(), json).await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QueriesCommand::List {
|
}
|
||||||
config,
|
|
||||||
json,
|
|
||||||
} => {
|
|
||||||
execute_queries_list(config.as_ref(), json)?;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Command::Snapshot {
|
Command::Snapshot {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
branch,
|
branch,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -540,7 +510,7 @@ async fn main() -> Result<()> {
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let branch = resolve_branch(&config, branch, None, "main");
|
let branch = resolve_branch(branch, None, "main");
|
||||||
let payload = client.snapshot(&branch).await?;
|
let payload = client.snapshot(&branch).await?;
|
||||||
if json {
|
if json {
|
||||||
print_json(&payload)?;
|
print_json(&payload)?;
|
||||||
|
|
@ -550,15 +520,12 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Command::Export {
|
Command::Export {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
branch,
|
branch,
|
||||||
jsonl,
|
jsonl,
|
||||||
type_names,
|
type_names,
|
||||||
table_keys,
|
table_keys,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
@ -566,7 +533,7 @@ async fn main() -> Result<()> {
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let branch = resolve_branch(&config, branch, None, "main");
|
let branch = resolve_branch(branch, None, "main");
|
||||||
if jsonl {
|
if jsonl {
|
||||||
eprintln!("warning: --jsonl is deprecated; `omnigraph export` always emits JSONL");
|
eprintln!("warning: --jsonl is deprecated; `omnigraph export` always emits JSONL");
|
||||||
}
|
}
|
||||||
|
|
@ -579,7 +546,6 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Command::Query {
|
Command::Query {
|
||||||
name,
|
name,
|
||||||
config,
|
|
||||||
query,
|
query,
|
||||||
query_string,
|
query_string,
|
||||||
params,
|
params,
|
||||||
|
|
@ -588,9 +554,7 @@ async fn main() -> Result<()> {
|
||||||
format,
|
format,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
None,
|
None,
|
||||||
|
|
@ -599,12 +563,12 @@ async fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let params_json = load_params_json(¶ms)?;
|
let params_json = load_params_json(¶ms)?;
|
||||||
let target = resolve_read_target(&config, branch, snapshot, None)?;
|
let target = resolve_read_target(branch, snapshot, None)?;
|
||||||
let output: ReadOutput = if query.is_some() || query_string.is_some() {
|
let output: ReadOutput = if query.is_some() || query_string.is_some() {
|
||||||
// Ad-hoc lane: run the source; the positional `name` selects
|
// Ad-hoc lane: run the source; the positional `name` selects
|
||||||
// within it when it holds more than one query.
|
// within it when it holds more than one query.
|
||||||
let query_source =
|
let query_source =
|
||||||
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
|
resolve_query_source(query.as_ref(), query_string.as_deref(), None)?;
|
||||||
client
|
client
|
||||||
.query(target, &query_source, name.as_deref(), params_json.as_ref())
|
.query(target, &query_source, name.as_deref(), params_json.as_ref())
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -624,21 +588,18 @@ async fn main() -> Result<()> {
|
||||||
.invoke_named(&name, false, params_json.as_ref(), branch, snapshot)
|
.invoke_named(&name, false, params_json.as_ref(), branch, snapshot)
|
||||||
.await?
|
.await?
|
||||||
};
|
};
|
||||||
let format = resolve_read_format(&config, format, json, None);
|
let format = resolve_read_format(format, json, None);
|
||||||
print_read_output(&output, format, &config)?;
|
print_read_output(&output, format)?;
|
||||||
}
|
}
|
||||||
Command::Mutate {
|
Command::Mutate {
|
||||||
name,
|
name,
|
||||||
config,
|
|
||||||
query,
|
query,
|
||||||
query_string,
|
query_string,
|
||||||
params,
|
params,
|
||||||
branch,
|
branch,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve_with_policy(
|
let client = client::GraphClient::resolve_with_policy(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
None,
|
None,
|
||||||
|
|
@ -648,11 +609,11 @@ async fn main() -> Result<()> {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let params_json = load_params_json(¶ms)?;
|
let params_json = load_params_json(¶ms)?;
|
||||||
let branch = resolve_branch(&config, branch, None, "main");
|
let branch = resolve_branch(branch, None, "main");
|
||||||
let output: ChangeOutput = if query.is_some() || query_string.is_some() {
|
let output: ChangeOutput = if query.is_some() || query_string.is_some() {
|
||||||
// Ad-hoc lane: run the source; positional `name` selects within it.
|
// Ad-hoc lane: run the source; positional `name` selects within it.
|
||||||
let query_source =
|
let query_source =
|
||||||
resolve_query_source(&config, query.as_ref(), query_string.as_deref(), None)?;
|
resolve_query_source(query.as_ref(), query_string.as_deref(), None)?;
|
||||||
client
|
client
|
||||||
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
|
.mutate(&branch, &query_source, name.as_deref(), params_json.as_ref())
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -677,12 +638,10 @@ async fn main() -> Result<()> {
|
||||||
Command::Alias {
|
Command::Alias {
|
||||||
name,
|
name,
|
||||||
args,
|
args,
|
||||||
config,
|
|
||||||
params,
|
params,
|
||||||
format,
|
format,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let operator_config = crate::operator::load_operator_config()?;
|
let operator_config = crate::operator::load_operator_config()?;
|
||||||
let Some(operator_alias) = operator_config.aliases.get(&name) else {
|
let Some(operator_alias) = operator_config.aliases.get(&name) else {
|
||||||
let defined: Vec<&str> =
|
let defined: Vec<&str> =
|
||||||
|
|
@ -695,59 +654,64 @@ async fn main() -> Result<()> {
|
||||||
};
|
};
|
||||||
let output = execute_operator_alias(
|
let output = execute_operator_alias(
|
||||||
&http_client,
|
&http_client,
|
||||||
&config,
|
|
||||||
&name,
|
&name,
|
||||||
operator_alias,
|
operator_alias,
|
||||||
&args,
|
&args,
|
||||||
load_params_json(¶ms)?,
|
load_params_json(¶ms)?,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let format = resolve_read_format(&config, format, json, operator_alias.format);
|
let format = resolve_read_format(format, json, operator_alias.format);
|
||||||
print_read_output(&output, format, &config)?;
|
print_read_output(&output, format)?;
|
||||||
}
|
}
|
||||||
Command::Policy { command } => match command {
|
Command::Policy { command } => {
|
||||||
PolicyCommand::Validate { config } => {
|
// Policy tooling sources the Cedar bundle(s) from the cluster's
|
||||||
let config = load_cli_config(config.as_ref())?;
|
// applied policies (RFC-011): --cluster <dir>, + the global --graph
|
||||||
let context = resolve_policy_context(&config)?;
|
// to pick a graph's bundle when several apply.
|
||||||
let engine = resolve_policy_engine(&context)?;
|
let cluster =
|
||||||
println!(
|
require_cluster_scope(cli.cluster.as_deref(), cli.profile.as_deref(), "policy")?;
|
||||||
"policy valid: {} [{} actors]",
|
let graph = cli.graph.as_deref();
|
||||||
context.policy_file.display(),
|
let graph_id = match graph {
|
||||||
engine.known_actor_count()
|
Some(id) => graph_resource_id_for_selection(Some(id), ""),
|
||||||
);
|
None => graph_resource_id_for_selection(None, "default"),
|
||||||
}
|
};
|
||||||
PolicyCommand::Test { config } => {
|
let policies = read_cluster_policies(&cluster).await?;
|
||||||
let config = load_cli_config(config.as_ref())?;
|
match command {
|
||||||
let context = resolve_policy_context(&config)?;
|
PolicyCommand::Validate {} => {
|
||||||
let engine = resolve_policy_engine(&context)?;
|
let bundle = select_cluster_policy(&cluster, &policies, graph)?;
|
||||||
let tests_path = resolve_policy_tests_path(&context);
|
let engine = PolicyEngine::load_graph_from_source(&bundle.source, &graph_id)?;
|
||||||
let tests = PolicyTestConfig::load(&tests_path)?;
|
println!(
|
||||||
engine.run_tests(&tests)?;
|
"policy valid: bundle '{}' [{} actors]",
|
||||||
println!("policy tests passed: {} cases", tests.cases.len());
|
bundle.name,
|
||||||
}
|
engine.known_actor_count()
|
||||||
PolicyCommand::Explain {
|
);
|
||||||
config,
|
}
|
||||||
actor,
|
PolicyCommand::Test { tests } => {
|
||||||
action,
|
let bundle = select_cluster_policy(&cluster, &policies, graph)?;
|
||||||
branch,
|
let engine = PolicyEngine::load_graph_from_source(&bundle.source, &graph_id)?;
|
||||||
target_branch,
|
let tests = PolicyTestConfig::load(&tests)?;
|
||||||
} => {
|
engine.run_tests(&tests)?;
|
||||||
let config = load_cli_config(config.as_ref())?;
|
println!("policy tests passed: {} cases", tests.cases.len());
|
||||||
let context = resolve_policy_context(&config)?;
|
}
|
||||||
let engine = resolve_policy_engine(&context)?;
|
PolicyCommand::Explain {
|
||||||
let request = PolicyRequest {
|
actor,
|
||||||
action,
|
action,
|
||||||
branch,
|
branch,
|
||||||
target_branch,
|
target_branch,
|
||||||
};
|
} => {
|
||||||
let decision = engine.authorize(&actor, &request)?;
|
let bundle = select_cluster_policy(&cluster, &policies, graph)?;
|
||||||
print_policy_explain(&decision, &actor, &request);
|
let engine = PolicyEngine::load_graph_from_source(&bundle.source, &graph_id)?;
|
||||||
|
let request = PolicyRequest {
|
||||||
|
action,
|
||||||
|
branch,
|
||||||
|
target_branch,
|
||||||
|
};
|
||||||
|
let decision = engine.authorize(&actor, &request)?;
|
||||||
|
print_policy_explain(&decision, &actor, &request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Command::Optimize { uri, config, json } => {
|
Command::Optimize { uri, json } => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let uri = resolve_maintenance_uri(
|
let uri = resolve_maintenance_uri(
|
||||||
&config,
|
|
||||||
cli.profile.as_deref(),
|
cli.profile.as_deref(),
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
cli.cluster.as_deref(),
|
cli.cluster.as_deref(),
|
||||||
|
|
@ -798,14 +762,11 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Command::Repair {
|
Command::Repair {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
confirm,
|
confirm,
|
||||||
force,
|
force,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let uri = resolve_maintenance_uri(
|
let uri = resolve_maintenance_uri(
|
||||||
&config,
|
|
||||||
cli.profile.as_deref(),
|
cli.profile.as_deref(),
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
cli.cluster.as_deref(),
|
cli.cluster.as_deref(),
|
||||||
|
|
@ -890,15 +851,12 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
Command::Cleanup {
|
Command::Cleanup {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
keep,
|
keep,
|
||||||
older_than,
|
older_than,
|
||||||
confirm,
|
confirm,
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let uri = resolve_maintenance_uri(
|
let uri = resolve_maintenance_uri(
|
||||||
&config,
|
|
||||||
cli.profile.as_deref(),
|
cli.profile.as_deref(),
|
||||||
cli.store.as_deref(),
|
cli.store.as_deref(),
|
||||||
cli.cluster.as_deref(),
|
cli.cluster.as_deref(),
|
||||||
|
|
@ -1036,12 +994,9 @@ async fn main() -> Result<()> {
|
||||||
Command::Graphs { command } => match command {
|
Command::Graphs { command } => match command {
|
||||||
GraphsCommand::List {
|
GraphsCommand::List {
|
||||||
uri,
|
uri,
|
||||||
config,
|
|
||||||
json,
|
json,
|
||||||
} => {
|
} => {
|
||||||
let config = load_cli_config(config.as_ref())?;
|
|
||||||
let client = client::GraphClient::resolve(
|
let client = client::GraphClient::resolve(
|
||||||
&config,
|
|
||||||
cli.server.as_deref(),
|
cli.server.as_deref(),
|
||||||
cli.graph.as_deref(),
|
cli.graph.as_deref(),
|
||||||
uri,
|
uri,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,16 @@
|
||||||
//! In-source test suite for the CLI binary (moved verbatim from
|
//! In-source test suite for the CLI binary (moved verbatim from
|
||||||
//! main.rs; `use super::*` resolves through the #[path] declaration).
|
//! main.rs; `use super::*` resolves through the #[path] declaration).
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
DEFAULT_BEARER_TOKEN_ENV, apply_bearer_token, bearer_token_from_env_file,
|
DEFAULT_BEARER_TOKEN_ENV, apply_bearer_token, legacy_change_request_body,
|
||||||
legacy_change_request_body, load_cli_config, load_env_file_into_process,
|
normalize_bearer_token, resolve_remote_bearer_token,
|
||||||
normalize_bearer_token, parse_env_assignment, resolve_cli_graph, resolve_policy_context,
|
|
||||||
resolve_remote_bearer_token,
|
|
||||||
};
|
};
|
||||||
use omnigraph_server::load_config;
|
|
||||||
use reqwest::header::AUTHORIZATION;
|
use reqwest::header::AUTHORIZATION;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_change_request_body_uses_legacy_field_names() {
|
fn legacy_change_request_body_uses_legacy_field_names() {
|
||||||
// `execute_change_remote` hits `POST /change`, which old
|
// `mutate`'s remote arm hits `POST /change`, which old
|
||||||
// `omnigraph-server` builds deserialize as `ChangeRequest` with
|
// `omnigraph-server` builds deserialize as `ChangeRequest` with
|
||||||
// **required** `query_source` and optional `query_name` keys.
|
// **required** `query_source` and optional `query_name` keys.
|
||||||
// Newer servers accept both spellings via serde alias, but a
|
// Newer servers accept both spellings via serde alias, but a
|
||||||
|
|
@ -96,120 +90,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_env_assignment_supports_plain_and_exported_values() {
|
fn resolve_remote_bearer_token_falls_back_to_default_env() {
|
||||||
assert_eq!(
|
// RFC-011: with no operator server matching the URL, the only chain
|
||||||
parse_env_assignment("DEMO_TOKEN=demo-token"),
|
// left is the default `OMNIGRAPH_BEARER_TOKEN` env (no omnigraph.yaml
|
||||||
Some(("DEMO_TOKEN".to_string(), "demo-token".to_string()))
|
// scoped chain). Hermetic: no operator config is read for a literal URL
|
||||||
);
|
// that matches no `servers:` entry.
|
||||||
assert_eq!(
|
|
||||||
parse_env_assignment("export DEMO_TOKEN=\"quoted-token\""),
|
|
||||||
Some(("DEMO_TOKEN".to_string(), "quoted-token".to_string()))
|
|
||||||
);
|
|
||||||
assert_eq!(parse_env_assignment("# comment"), None);
|
|
||||||
assert_eq!(parse_env_assignment(" "), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bearer_token_from_env_file_reads_named_value() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let env_file = temp.path().join(".env.omni");
|
|
||||||
fs::write(
|
|
||||||
&env_file,
|
|
||||||
"FIRST=ignore\nexport DEMO_TOKEN=\" demo-token \"\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
bearer_token_from_env_file(&env_file, "DEMO_TOKEN")
|
|
||||||
.unwrap()
|
|
||||||
.as_deref(),
|
|
||||||
Some("demo-token")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
bearer_token_from_env_file(&env_file, "MISSING").unwrap(),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_env_file_into_process_sets_missing_values_without_overriding_existing_ones() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let env_file = temp.path().join(".env.omni");
|
|
||||||
fs::write(
|
|
||||||
&env_file,
|
|
||||||
"AUTOLOAD_ONLY=from-file\nAUTOLOAD_PRESET=from-file\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let missing_key = "AUTOLOAD_ONLY";
|
|
||||||
let preset_key = "AUTOLOAD_PRESET";
|
|
||||||
let previous_missing = std::env::var_os(missing_key);
|
|
||||||
let previous_preset = std::env::var_os(preset_key);
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
std::env::remove_var(missing_key);
|
|
||||||
std::env::set_var(preset_key, "from-env");
|
|
||||||
}
|
|
||||||
|
|
||||||
load_env_file_into_process(&env_file).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(std::env::var(missing_key).unwrap(), "from-file");
|
|
||||||
assert_eq!(std::env::var(preset_key).unwrap(), "from-env");
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
if let Some(value) = previous_missing {
|
|
||||||
std::env::set_var(missing_key, value);
|
|
||||||
} else {
|
|
||||||
std::env::remove_var(missing_key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(value) = previous_preset {
|
|
||||||
std::env::set_var(preset_key, value);
|
|
||||||
} else {
|
|
||||||
std::env::remove_var(preset_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_remote_bearer_token_uses_scoped_env_file_with_global_fallback() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
fs::write(
|
|
||||||
temp.path().join("omnigraph.yaml"),
|
|
||||||
r#"
|
|
||||||
graphs:
|
|
||||||
demo:
|
|
||||||
uri: https://example.com
|
|
||||||
bearer_token_env: DEMO_TOKEN
|
|
||||||
auth:
|
|
||||||
env_file: .env.omni
|
|
||||||
cli:
|
|
||||||
graph: demo
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
temp.path().join(".env.omni"),
|
|
||||||
"DEMO_TOKEN=scoped-token\nOMNIGRAPH_BEARER_TOKEN=global-token\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let previous = std::env::var_os(DEFAULT_BEARER_TOKEN_ENV);
|
let previous = std::env::var_os(DEFAULT_BEARER_TOKEN_ENV);
|
||||||
let previous_home = std::env::var_os("OMNIGRAPH_HOME");
|
let previous_home = std::env::var_os("OMNIGRAPH_HOME");
|
||||||
unsafe {
|
unsafe {
|
||||||
std::env::remove_var(DEFAULT_BEARER_TOKEN_ENV);
|
std::env::set_var(DEFAULT_BEARER_TOKEN_ENV, "global-token");
|
||||||
// Hermetic: the keyed hop (RFC-007 PR 2) must not pick up a real
|
std::env::set_var("OMNIGRAPH_HOME", "/nonexistent/omnigraph-test-home");
|
||||||
// ~/.omnigraph on the developer's machine — and with no operator
|
|
||||||
// servers defined, the legacy chain below must behave
|
|
||||||
// byte-identically to pre-PR-2 (tested-as-untouched).
|
|
||||||
std::env::set_var("OMNIGRAPH_HOME", temp.path().join("no-operator-config"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
let config = load_config(Some(&config_path)).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
resolve_remote_bearer_token(&config, Some("https://override.example.com"))
|
resolve_remote_bearer_token(Some("https://override.example.com"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_deref(),
|
.as_deref(),
|
||||||
Some("global-token")
|
Some("global-token")
|
||||||
|
|
@ -228,196 +122,3 @@ cli:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_cli_config_autoloads_env_file_into_process() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
fs::write(
|
|
||||||
temp.path().join("omnigraph.yaml"),
|
|
||||||
r#"
|
|
||||||
auth:
|
|
||||||
env_file: .env.omni
|
|
||||||
graphs:
|
|
||||||
demo:
|
|
||||||
uri: s3://bucket/prefix
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
temp.path().join(".env.omni"),
|
|
||||||
"AUTOLOAD_FROM_CONFIG=loaded\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let key = "AUTOLOAD_FROM_CONFIG";
|
|
||||||
let previous = std::env::var_os(key);
|
|
||||||
unsafe {
|
|
||||||
std::env::remove_var(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
let config = load_cli_config(Some(&config_path)).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.resolve_target_uri(None, Some("demo"), None).unwrap(),
|
|
||||||
"s3://bucket/prefix"
|
|
||||||
);
|
|
||||||
assert_eq!(std::env::var(key).unwrap(), "loaded");
|
|
||||||
|
|
||||||
unsafe {
|
|
||||||
if let Some(value) = previous {
|
|
||||||
std::env::set_var(key, value);
|
|
||||||
} else {
|
|
||||||
std::env::remove_var(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn graph_identity_resolve_policy_context_named_cli_graph_uses_graph_key_not_project_name_or_uri()
|
|
||||||
{
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
fs::write(
|
|
||||||
&config_path,
|
|
||||||
r#"
|
|
||||||
project:
|
|
||||||
name: misleading-project
|
|
||||||
graphs:
|
|
||||||
local:
|
|
||||||
uri: /tmp/local-policy-graph.omni
|
|
||||||
policy:
|
|
||||||
file: ./policy.yaml
|
|
||||||
cli:
|
|
||||||
graph: local
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config = load_config(Some(&config_path)).unwrap();
|
|
||||||
let context = resolve_policy_context(&config).unwrap();
|
|
||||||
assert_eq!(context.graph_id, "local");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn graph_identity_resolve_policy_context_server_graph_uses_graph_key_when_cli_graph_absent() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
fs::write(
|
|
||||||
&config_path,
|
|
||||||
r#"
|
|
||||||
project:
|
|
||||||
name: misleading-project
|
|
||||||
graphs:
|
|
||||||
local:
|
|
||||||
uri: /tmp/local-policy-graph.omni
|
|
||||||
policy:
|
|
||||||
file: ./server-policy.yaml
|
|
||||||
server:
|
|
||||||
graph: local
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config = load_config(Some(&config_path)).unwrap();
|
|
||||||
let context = resolve_policy_context(&config).unwrap();
|
|
||||||
assert_eq!(context.graph_id, "local");
|
|
||||||
assert!(context.policy_file.ends_with("server-policy.yaml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn graph_identity_resolve_policy_context_anonymous_uses_top_level_default_identity() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
fs::write(
|
|
||||||
&config_path,
|
|
||||||
r#"
|
|
||||||
project:
|
|
||||||
name: misleading-project
|
|
||||||
graphs:
|
|
||||||
local:
|
|
||||||
uri: /tmp/local-policy-graph.omni
|
|
||||||
policy:
|
|
||||||
file: ./top-policy.yaml
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config = load_config(Some(&config_path)).unwrap();
|
|
||||||
let context = resolve_policy_context(&config).unwrap();
|
|
||||||
assert_eq!(context.graph_id, "default");
|
|
||||||
assert!(context.policy_file.ends_with("top-policy.yaml"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn graph_identity_resolve_cli_graph_named_target_uses_graph_key_not_project_name_or_uri() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
fs::write(
|
|
||||||
&config_path,
|
|
||||||
r#"
|
|
||||||
project:
|
|
||||||
name: misleading-project
|
|
||||||
graphs:
|
|
||||||
prod:
|
|
||||||
uri: s3://bucket/prod-graph/
|
|
||||||
policy:
|
|
||||||
file: ./prod-policy.yaml
|
|
||||||
cli:
|
|
||||||
graph: prod
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config = load_config(Some(&config_path)).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/");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn graph_identity_resolve_cli_graph_positional_uri_uses_anonymous_normalized_uri() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
fs::write(
|
|
||||||
&config_path,
|
|
||||||
r#"
|
|
||||||
project:
|
|
||||||
name: misleading-project
|
|
||||||
graphs:
|
|
||||||
local:
|
|
||||||
uri: /tmp/configured-graph.omni
|
|
||||||
policy:
|
|
||||||
file: ./policy.yaml
|
|
||||||
cli:
|
|
||||||
graph: local
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let config = load_config(Some(&config_path)).unwrap();
|
|
||||||
let local_graph_path = temp.path().join("explicit-graph.omni");
|
|
||||||
let local_graph = resolve_cli_graph(
|
|
||||||
&config,
|
|
||||||
Some(format!("file://{}", local_graph_path.display())),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(local_graph.selected(), None);
|
|
||||||
assert_eq!(
|
|
||||||
local_graph.graph_id,
|
|
||||||
local_graph_path.to_string_lossy().as_ref()
|
|
||||||
);
|
|
||||||
assert_eq!(local_graph.policy_file, None);
|
|
||||||
|
|
||||||
let s3_graph = resolve_cli_graph(
|
|
||||||
&config,
|
|
||||||
Some("s3://bucket/anonymous-graph/".to_string()),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(s3_graph.selected(), None);
|
|
||||||
assert_eq!(s3_graph.graph_id, "s3://bucket/anonymous-graph");
|
|
||||||
assert_eq!(s3_graph.policy_file, None);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -734,15 +734,10 @@ pub(crate) fn print_snapshot_human(branch: &str, manifest_version: u64, entries:
|
||||||
pub(crate) fn print_read_output(
|
pub(crate) fn print_read_output(
|
||||||
output: &ReadOutput,
|
output: &ReadOutput,
|
||||||
format: ReadOutputFormat,
|
format: ReadOutputFormat,
|
||||||
config: &OmnigraphConfig,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
render_read(
|
render_read(output, format, &resolve_table_render_options())?
|
||||||
output,
|
|
||||||
format,
|
|
||||||
&resolve_table_render_options(config),
|
|
||||||
)?
|
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -892,20 +887,11 @@ pub(crate) fn finish_logout(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Table prefs cascade (RFC-007/008): legacy cli.table_* (window) >
|
/// Table prefs cascade (RFC-011): operator defaults.table_* > built-in.
|
||||||
/// operator defaults.table_* > built-in.
|
pub(crate) fn resolve_table_render_options() -> ReadRenderOptions {
|
||||||
pub(crate) fn resolve_table_render_options(config: &OmnigraphConfig) -> ReadRenderOptions {
|
|
||||||
let operator = crate::operator::load_operator_config().unwrap_or_default();
|
let operator = crate::operator::load_operator_config().unwrap_or_default();
|
||||||
ReadRenderOptions {
|
ReadRenderOptions {
|
||||||
max_column_width: config
|
max_column_width: operator.defaults.table_max_column_width.unwrap_or(80),
|
||||||
.cli
|
cell_layout: operator.defaults.table_cell_layout.unwrap_or_default(),
|
||||||
.table_max_column_width
|
|
||||||
.or(operator.defaults.table_max_column_width)
|
|
||||||
.unwrap_or(80),
|
|
||||||
cell_layout: config
|
|
||||||
.cli
|
|
||||||
.table_cell_layout
|
|
||||||
.or(operator.defaults.table_cell_layout)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,9 +82,7 @@ impl Capability {
|
||||||
/// classifier) plus the one Data→Served refinement: `graphs` is remote-only.
|
/// classifier) plus the one Data→Served refinement: `graphs` is remote-only.
|
||||||
///
|
///
|
||||||
/// This reflects *current enforced behavior*, so messages stay truthful:
|
/// This reflects *current enforced behavior*, so messages stay truthful:
|
||||||
/// `queries list` is `Local` (reads config today) and `queries validate` is
|
/// `queries`/`policy` read a cluster's applied state (`Control`).
|
||||||
/// `Direct` (opens a graph directly today). Both converge to the RFC end-state
|
|
||||||
/// (served / control) only when later slices re-route them.
|
|
||||||
pub(crate) fn command_capability(cmd: &Command) -> Capability {
|
pub(crate) fn command_capability(cmd: &Command) -> Capability {
|
||||||
if let Command::Graphs { .. } = cmd {
|
if let Command::Graphs { .. } = cmd {
|
||||||
return Capability::Served;
|
return Capability::Served;
|
||||||
|
|
@ -120,20 +118,18 @@ pub(crate) fn command_plane(cmd: &Command) -> Plane {
|
||||||
Command::Schema {
|
Command::Schema {
|
||||||
command: SchemaCommand::Plan { .. },
|
command: SchemaCommand::Plan { .. },
|
||||||
} => Plane::Storage,
|
} => Plane::Storage,
|
||||||
Command::Queries {
|
// `queries` and `policy` tooling now source their inputs from a
|
||||||
command: QueriesCommand::Validate { .. },
|
// cluster's applied state (`--cluster`), so they live on the control
|
||||||
} => Plane::Storage,
|
// plane (RFC-011 — omnigraph.yaml excised from the CLI).
|
||||||
Command::Queries {
|
Command::Queries { .. } => Plane::Control,
|
||||||
command: QueriesCommand::List { .. },
|
Command::Policy { .. } => Plane::Control,
|
||||||
} => Plane::Session,
|
|
||||||
Command::Init { .. }
|
Command::Init { .. }
|
||||||
| Command::Optimize { .. }
|
| Command::Optimize { .. }
|
||||||
| Command::Repair { .. }
|
| Command::Repair { .. }
|
||||||
| Command::Cleanup { .. }
|
| Command::Cleanup { .. }
|
||||||
| Command::Lint { .. } => Plane::Storage,
|
| Command::Lint { .. } => Plane::Storage,
|
||||||
Command::Cluster { .. } => Plane::Control,
|
Command::Cluster { .. } => Plane::Control,
|
||||||
Command::Policy { .. }
|
Command::Embed(_)
|
||||||
| Command::Embed(_)
|
|
||||||
| Command::Login { .. }
|
| Command::Login { .. }
|
||||||
| Command::Logout { .. }
|
| Command::Logout { .. }
|
||||||
| Command::Config { .. }
|
| Command::Config { .. }
|
||||||
|
|
@ -188,7 +184,17 @@ pub(crate) fn command_label(cmd: &Command) -> &'static str {
|
||||||
pub(crate) fn accepts_cluster_addressing(cmd: &Command) -> bool {
|
pub(crate) fn accepts_cluster_addressing(cmd: &Command) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
cmd,
|
cmd,
|
||||||
Command::Optimize { .. } | Command::Repair { .. } | Command::Cleanup { .. }
|
Command::Optimize { .. }
|
||||||
|
| Command::Repair { .. }
|
||||||
|
| Command::Cleanup { .. }
|
||||||
|
// `lint` can type-check a `.gq` against a cluster graph's schema
|
||||||
|
// (RFC-011): `--cluster <dir> --graph <id>`.
|
||||||
|
| Command::Lint { .. }
|
||||||
|
// The policy/queries tooling addresses a cluster's applied state
|
||||||
|
// (RFC-011): `--cluster <dir>` selects the cluster, `--graph <id>`
|
||||||
|
// picks a graph's bundle/registry within it.
|
||||||
|
| Command::Policy { .. }
|
||||||
|
| Command::Queries { .. }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,7 +290,12 @@ mod tests {
|
||||||
assert_eq!(cap(&["omnigraph", "schema", "plan", "--schema", "s.pg", "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);
|
assert_eq!(cap(&["omnigraph", "cluster", "status", "--config", "."]), Capability::Control);
|
||||||
assert_eq!(cap(&["omnigraph", "version"]), Capability::Local);
|
assert_eq!(cap(&["omnigraph", "version"]), Capability::Local);
|
||||||
assert_eq!(cap(&["omnigraph", "queries", "list"]), Capability::Local);
|
// `queries`/`policy` tooling reads cluster state now (control plane).
|
||||||
|
assert_eq!(cap(&["omnigraph", "queries", "list"]), Capability::Control);
|
||||||
|
assert_eq!(
|
||||||
|
cap(&["omnigraph", "policy", "validate"]),
|
||||||
|
Capability::Control
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -683,51 +683,8 @@ fn cluster_apply_locked_exits_nonzero() {
|
||||||
assert!(!temp.path().join("__cluster/resources").exists());
|
assert!(!temp.path().join("__cluster/resources").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
/// RFC-011: the actor chain is `--as` > `operator.actor` > none. The CLI no
|
||||||
fn cluster_apply_uses_cli_actor_from_local_config() {
|
/// longer reads omnigraph.yaml `cli.actor`.
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
write_cluster_config_fixture(temp.path());
|
|
||||||
fs::write(
|
|
||||||
temp.path().join("omnigraph.yaml"),
|
|
||||||
"cli:\n actor: act-local\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// Phase 1: import once (setup, not under test).
|
|
||||||
let output = cli()
|
|
||||||
.current_dir(temp.path())
|
|
||||||
.arg("cluster")
|
|
||||||
.arg("import")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(temp.path())
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
assert!(output.status.success(), "{output:?}");
|
|
||||||
|
|
||||||
// Phase 2: apply alone, capturing the echoed actor (idempotent re-runs).
|
|
||||||
let apply = |extra: &[&str]| {
|
|
||||||
let mut command = cli();
|
|
||||||
command.current_dir(temp.path());
|
|
||||||
for arg in extra {
|
|
||||||
command.arg(arg);
|
|
||||||
}
|
|
||||||
let output = command
|
|
||||||
.arg("cluster")
|
|
||||||
.arg("apply")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(temp.path())
|
|
||||||
.arg("--json")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let json: serde_json::Value =
|
|
||||||
serde_json::from_str(String::from_utf8_lossy(&output.stdout).trim()).unwrap();
|
|
||||||
json["actor"].clone()
|
|
||||||
};
|
|
||||||
assert_eq!(apply(&[]), "act-local", "cli.actor is the no-flag default");
|
|
||||||
assert_eq!(apply(&["--as", "andrew"]), "andrew", "--as overrides cli.actor");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RFC-007 PR 1: the operator layer joins the actor chain —
|
|
||||||
/// `--as` > legacy `cli.actor` (RFC-008 window) > `operator.actor` > none.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cluster_apply_uses_operator_actor_from_omnigraph_home() {
|
fn cluster_apply_uses_operator_actor_from_omnigraph_home() {
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
|
|
@ -771,41 +728,31 @@ fn cluster_apply_uses_operator_actor_from_omnigraph_home() {
|
||||||
json["actor"].clone()
|
json["actor"].clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// No --as, no omnigraph.yaml: the operator identity applies.
|
// No --as: the operator identity applies.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
apply(&[]),
|
apply(&[]),
|
||||||
"act-operator",
|
"act-operator",
|
||||||
"operator.actor is the no-flag, no-legacy-config default"
|
"operator.actor is the no-flag default"
|
||||||
);
|
);
|
||||||
// --as still wins over everything.
|
// --as still wins over the operator layer.
|
||||||
assert_eq!(apply(&["--as", "andrew"]), "andrew");
|
assert_eq!(apply(&["--as", "andrew"]), "andrew");
|
||||||
|
|
||||||
// A legacy cli.actor (RFC-008 window) outranks the operator layer.
|
|
||||||
fs::write(
|
|
||||||
temp.path().join("omnigraph.yaml"),
|
|
||||||
"cli:\n actor: act-legacy\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
apply(&[]),
|
|
||||||
"act-legacy",
|
|
||||||
"legacy cli.actor wins over operator.actor during the deprecation window"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cluster_approve_uses_cli_actor_fallback() {
|
fn cluster_approve_uses_operator_actor_fallback() {
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
write_cluster_config_fixture(temp.path());
|
write_cluster_config_fixture(temp.path());
|
||||||
|
let operator_home = tempdir().unwrap();
|
||||||
fs::write(
|
fs::write(
|
||||||
temp.path().join("omnigraph.yaml"),
|
operator_home.path().join("config.yaml"),
|
||||||
"cli:\n actor: act-local\n",
|
"operator:\n actor: act-operator\n",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Converge, then remove the graph so a gated delete is pending.
|
// Converge, then remove the graph so a gated delete is pending.
|
||||||
for command in ["import", "apply"] {
|
for command in ["import", "apply"] {
|
||||||
let output = cli()
|
let output = cli()
|
||||||
.current_dir(temp.path())
|
.current_dir(temp.path())
|
||||||
|
.env("OMNIGRAPH_HOME", operator_home.path())
|
||||||
.arg("cluster")
|
.arg("cluster")
|
||||||
.arg(command)
|
.arg(command)
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
|
|
@ -818,6 +765,7 @@ fn cluster_approve_uses_cli_actor_fallback() {
|
||||||
|
|
||||||
let output = cli()
|
let output = cli()
|
||||||
.current_dir(temp.path())
|
.current_dir(temp.path())
|
||||||
|
.env("OMNIGRAPH_HOME", operator_home.path())
|
||||||
.arg("cluster")
|
.arg("cluster")
|
||||||
.arg("approve")
|
.arg("approve")
|
||||||
.arg("graph.knowledge")
|
.arg("graph.knowledge")
|
||||||
|
|
@ -829,14 +777,17 @@ fn cluster_approve_uses_cli_actor_fallback() {
|
||||||
assert!(output.status.success(), "{output:?}");
|
assert!(output.status.success(), "{output:?}");
|
||||||
let json: serde_json::Value =
|
let json: serde_json::Value =
|
||||||
serde_json::from_str(String::from_utf8_lossy(&output.stdout).trim()).unwrap();
|
serde_json::from_str(String::from_utf8_lossy(&output.stdout).trim()).unwrap();
|
||||||
assert_eq!(json["approved_by"], "act-local");
|
assert_eq!(json["approved_by"], "act-operator");
|
||||||
|
|
||||||
// With neither flag nor config: refused with the actionable message.
|
// With neither flag nor operator config: refused with the actionable
|
||||||
|
// message (an approval without an approver is meaningless).
|
||||||
let bare = tempdir().unwrap();
|
let bare = tempdir().unwrap();
|
||||||
write_cluster_config_fixture(bare.path());
|
write_cluster_config_fixture(bare.path());
|
||||||
|
let bare_home = tempdir().unwrap();
|
||||||
let output = output_failure(
|
let output = output_failure(
|
||||||
cli()
|
cli()
|
||||||
.current_dir(bare.path())
|
.current_dir(bare.path())
|
||||||
|
.env("OMNIGRAPH_HOME", bare_home.path())
|
||||||
.arg("cluster")
|
.arg("cluster")
|
||||||
.arg("approve")
|
.arg("approve")
|
||||||
.arg("graph.knowledge")
|
.arg("graph.knowledge")
|
||||||
|
|
@ -845,11 +796,13 @@ fn cluster_approve_uses_cli_actor_fallback() {
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
assert!(stderr.contains("--as"), "{stderr}");
|
assert!(stderr.contains("--as"), "{stderr}");
|
||||||
assert!(stderr.contains("cli.actor"), "{stderr}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cluster_commands_ignore_malformed_local_config() {
|
fn cluster_commands_ignore_legacy_omnigraph_yaml() {
|
||||||
|
// RFC-011: the CLI never reads omnigraph.yaml for cluster commands — a
|
||||||
|
// present (even malformed) legacy file is inert. The actor falls back to
|
||||||
|
// `operator.actor`, then to none (no loud failure on absence).
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
write_cluster_config_fixture(temp.path());
|
write_cluster_config_fixture(temp.path());
|
||||||
fs::write(temp.path().join("omnigraph.yaml"), "{{{{ not yaml").unwrap();
|
fs::write(temp.path().join("omnigraph.yaml"), "{{{{ not yaml").unwrap();
|
||||||
|
|
@ -873,14 +826,11 @@ fn cluster_commands_ignore_malformed_local_config() {
|
||||||
"cluster {command} touched omnigraph.yaml"
|
"cluster {command} touched omnigraph.yaml"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// import + apply with an explicit --as: the config is never loaded.
|
// import + apply (no --as, no operator config): the legacy file is never
|
||||||
for (command, args) in [("import", vec![]), ("apply", vec!["--as", "andrew"])] {
|
// loaded and the no-actor apply succeeds (actor defaults to none).
|
||||||
let mut invocation = cli();
|
for command in ["import", "apply"] {
|
||||||
invocation.current_dir(temp.path());
|
let output = cli()
|
||||||
for arg in &args {
|
.current_dir(temp.path())
|
||||||
invocation.arg(arg);
|
|
||||||
}
|
|
||||||
let output = invocation
|
|
||||||
.arg("cluster")
|
.arg("cluster")
|
||||||
.arg(command)
|
.arg(command)
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
|
|
@ -893,20 +843,6 @@ fn cluster_commands_ignore_malformed_local_config() {
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Only the no-flag actor lookup is allowed to fail, and loudly.
|
|
||||||
let output = output_failure(
|
|
||||||
cli()
|
|
||||||
.current_dir(temp.path())
|
|
||||||
.arg("cluster")
|
|
||||||
.arg("apply")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(temp.path()),
|
|
||||||
);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(
|
|
||||||
stderr.contains("omnigraph.yaml") && stderr.contains("--as"),
|
|
||||||
"the actor-default config read must fail loudly and actionably: {stderr}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -599,13 +599,15 @@ query list_people() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn query_lint_can_resolve_graph_and_query_from_config() {
|
fn query_lint_can_resolve_graph_from_store_scope() {
|
||||||
|
// RFC-011: lint resolves its graph target through `--store` (the direct
|
||||||
|
// scope), not omnigraph.yaml's cli.graph; the .gq path is plain cwd-relative.
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
let graph = graph_path(temp.path());
|
let graph = graph_path(temp.path());
|
||||||
let config_path = temp.path().join("omnigraph.yaml");
|
|
||||||
init_graph(&graph);
|
init_graph(&graph);
|
||||||
|
let query_path = temp.path().join("queries.gq");
|
||||||
write_query_file(
|
write_query_file(
|
||||||
&temp.path().join("queries.gq"),
|
&query_path,
|
||||||
r#"
|
r#"
|
||||||
query list_people() {
|
query list_people() {
|
||||||
match { $p: Person }
|
match { $p: Person }
|
||||||
|
|
@ -613,16 +615,15 @@ query list_people() {
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
write_config(&config_path, &local_yaml_config(&graph));
|
|
||||||
|
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("query")
|
.arg("query")
|
||||||
.arg("lint")
|
.arg("lint")
|
||||||
.arg("--query")
|
.arg("--query")
|
||||||
.arg("queries.gq")
|
.arg(&query_path)
|
||||||
.arg("--config")
|
.arg("--store")
|
||||||
.arg(&config_path)
|
.arg(&graph)
|
||||||
.arg("--json"),
|
.arg("--json"),
|
||||||
);
|
);
|
||||||
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||||
|
|
@ -690,7 +691,9 @@ query list_people() {
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
assert!(
|
assert!(
|
||||||
stderr.contains("lint requires --schema <schema.pg> or a resolvable graph target")
|
stderr.contains("lint requires --schema <schema.pg>")
|
||||||
|
|| stderr.contains("no graph addressed"),
|
||||||
|
"expected a schema-or-graph-target requirement; got: {stderr}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -987,43 +990,38 @@ fn export_jsonl_outputs_source_rows_for_selected_branch_and_type() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC-011: `policy validate|test|explain` source the Cedar bundle from a
|
||||||
|
// converged cluster's applied policies (`--cluster <dir>` + `--graph <id>`),
|
||||||
|
// not omnigraph.yaml's policy.file.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn policy_validate_accepts_valid_policy_file() {
|
fn policy_validate_accepts_cluster_bundle() {
|
||||||
let temp = tempdir().unwrap();
|
let cluster = converged_loaded_cluster("knowledge", Some(POLICY_YAML));
|
||||||
let (config, _) = write_policy_config_fixture(temp.path());
|
|
||||||
|
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("policy")
|
.arg("policy")
|
||||||
.arg("validate")
|
.arg("validate")
|
||||||
.arg("--config")
|
.arg("--cluster")
|
||||||
.arg(&config),
|
.arg(cluster.path())
|
||||||
|
.arg("--graph")
|
||||||
|
.arg("knowledge"),
|
||||||
);
|
);
|
||||||
let stdout = stdout_string(&output);
|
let stdout = stdout_string(&output);
|
||||||
|
|
||||||
assert!(stdout.contains("policy valid:"));
|
assert!(stdout.contains("policy valid:"));
|
||||||
assert!(stdout.contains("policy.yaml"));
|
|
||||||
assert!(stdout.contains("[2 actors]"));
|
assert!(stdout.contains("[2 actors]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn policy_validate_fails_for_invalid_policy_file() {
|
fn policy_validate_fails_for_invalid_cluster_bundle() {
|
||||||
let temp = tempdir().unwrap();
|
// The cluster does not validate a policy bundle's internal rules, so an
|
||||||
let config = temp.path().join("omnigraph.yaml");
|
// applied-but-malformed bundle reaches `policy validate`, which compiles it
|
||||||
let policy = temp.path().join("policy.yaml");
|
// and surfaces the error (here: a duplicate rule id).
|
||||||
fs::write(
|
let cluster = converged_loaded_cluster(
|
||||||
&config,
|
"knowledge",
|
||||||
r#"
|
Some(
|
||||||
project:
|
r#"
|
||||||
name: policy-test-graph
|
|
||||||
policy:
|
|
||||||
file: ./policy.yaml
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
fs::write(
|
|
||||||
&policy,
|
|
||||||
r#"
|
|
||||||
version: 1
|
version: 1
|
||||||
groups:
|
groups:
|
||||||
team: [act-andrew]
|
team: [act-andrew]
|
||||||
|
|
@ -1039,26 +1037,42 @@ rules:
|
||||||
actions: [export]
|
actions: [export]
|
||||||
branch_scope: any
|
branch_scope: any
|
||||||
"#,
|
"#,
|
||||||
)
|
),
|
||||||
.unwrap();
|
);
|
||||||
|
|
||||||
let output = output_failure(
|
let output = output_failure(
|
||||||
cli()
|
cli()
|
||||||
.arg("policy")
|
.arg("policy")
|
||||||
.arg("validate")
|
.arg("validate")
|
||||||
.arg("--config")
|
.arg("--cluster")
|
||||||
.arg(&config),
|
.arg(cluster.path())
|
||||||
|
.arg("--graph")
|
||||||
|
.arg("knowledge"),
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
||||||
assert!(stderr.contains("duplicate policy rule id"));
|
assert!(
|
||||||
|
stderr.contains("duplicate policy rule id"),
|
||||||
|
"expected a duplicate-rule error; got: {stderr}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn policy_test_runs_declarative_cases() {
|
fn policy_test_runs_declarative_cases_against_cluster_bundle() {
|
||||||
let temp = tempdir().unwrap();
|
let cluster = converged_loaded_cluster("knowledge", Some(POLICY_YAML));
|
||||||
let (config, _) = write_policy_config_fixture(temp.path());
|
let tests = cluster.path().join("policy.tests.yaml");
|
||||||
|
fs::write(&tests, POLICY_TESTS_YAML).unwrap();
|
||||||
|
|
||||||
let output = output_success(cli().arg("policy").arg("test").arg("--config").arg(&config));
|
let output = output_success(
|
||||||
|
cli()
|
||||||
|
.arg("policy")
|
||||||
|
.arg("test")
|
||||||
|
.arg("--cluster")
|
||||||
|
.arg(cluster.path())
|
||||||
|
.arg("--graph")
|
||||||
|
.arg("knowledge")
|
||||||
|
.arg("--tests")
|
||||||
|
.arg(&tests),
|
||||||
|
);
|
||||||
let stdout = stdout_string(&output);
|
let stdout = stdout_string(&output);
|
||||||
|
|
||||||
assert!(stdout.contains("policy tests passed: 2 cases"));
|
assert!(stdout.contains("policy tests passed: 2 cases"));
|
||||||
|
|
@ -1066,15 +1080,16 @@ fn policy_test_runs_declarative_cases() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn policy_explain_reports_decision_and_matched_rule() {
|
fn policy_explain_reports_decision_and_matched_rule() {
|
||||||
let temp = tempdir().unwrap();
|
let cluster = converged_loaded_cluster("knowledge", Some(POLICY_YAML));
|
||||||
let (config, _) = write_policy_config_fixture(temp.path());
|
|
||||||
|
|
||||||
let allow = output_success(
|
let allow = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("policy")
|
.arg("policy")
|
||||||
.arg("explain")
|
.arg("explain")
|
||||||
.arg("--config")
|
.arg("--cluster")
|
||||||
.arg(&config)
|
.arg(cluster.path())
|
||||||
|
.arg("--graph")
|
||||||
|
.arg("knowledge")
|
||||||
.arg("--actor")
|
.arg("--actor")
|
||||||
.arg("act-andrew")
|
.arg("act-andrew")
|
||||||
.arg("--action")
|
.arg("--action")
|
||||||
|
|
@ -1090,8 +1105,10 @@ fn policy_explain_reports_decision_and_matched_rule() {
|
||||||
cli()
|
cli()
|
||||||
.arg("policy")
|
.arg("policy")
|
||||||
.arg("explain")
|
.arg("explain")
|
||||||
.arg("--config")
|
.arg("--cluster")
|
||||||
.arg(&config)
|
.arg(cluster.path())
|
||||||
|
.arg("--graph")
|
||||||
|
.arg("knowledge")
|
||||||
.arg("--actor")
|
.arg("--actor")
|
||||||
.arg("act-bruno")
|
.arg("act-bruno")
|
||||||
.arg("--action")
|
.arg("--action")
|
||||||
|
|
@ -1105,19 +1122,24 @@ fn policy_explain_reports_decision_and_matched_rule() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn read_can_resolve_uri_from_config() {
|
fn read_resolves_uri_from_default_store_scope() {
|
||||||
|
// RFC-011: a zero-flag read resolves its graph from `defaults.store` in the
|
||||||
|
// operator config (the local-dev default scope) — no omnigraph.yaml.
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
let graph = graph_path(temp.path());
|
let graph = graph_path(temp.path());
|
||||||
let config = temp.path().join("omnigraph.yaml");
|
|
||||||
init_graph(&graph);
|
init_graph(&graph);
|
||||||
load_fixture(&graph);
|
load_fixture(&graph);
|
||||||
write_config(&config, &local_yaml_config(&graph));
|
let home = tempdir().unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
home.path().join("config.yaml"),
|
||||||
|
format!("defaults:\n store: {}\n", graph.to_string_lossy()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
|
.env("OMNIGRAPH_HOME", home.path())
|
||||||
.arg("read")
|
.arg("read")
|
||||||
.arg("--config")
|
|
||||||
.arg(&config)
|
|
||||||
.arg("--query")
|
.arg("--query")
|
||||||
.arg(fixture("test.gq"))
|
.arg(fixture("test.gq"))
|
||||||
.arg("get_person")
|
.arg("get_person")
|
||||||
|
|
@ -1278,13 +1300,13 @@ query insert_person($name: String, $age: I32) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn change_can_resolve_uri_and_branch_from_config() {
|
fn change_resolves_uri_and_default_branch_from_store_scope() {
|
||||||
|
// RFC-011: a mutate resolves its graph from `--store` and defaults the
|
||||||
|
// branch to main (no omnigraph.yaml cli.graph / cli.branch).
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
let graph = graph_path(temp.path());
|
let graph = graph_path(temp.path());
|
||||||
let config = temp.path().join("omnigraph.yaml");
|
|
||||||
init_graph(&graph);
|
init_graph(&graph);
|
||||||
load_fixture(&graph);
|
load_fixture(&graph);
|
||||||
write_config(&config, &local_yaml_config(&graph));
|
|
||||||
let mutation_file = temp.path().join("config-mutations.gq");
|
let mutation_file = temp.path().join("config-mutations.gq");
|
||||||
write_query_file(
|
write_query_file(
|
||||||
&mutation_file,
|
&mutation_file,
|
||||||
|
|
@ -1298,8 +1320,8 @@ query insert_person($name: String, $age: I32) {
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("change")
|
.arg("change")
|
||||||
.arg("--config")
|
.arg("--store")
|
||||||
.arg(&config)
|
.arg(&graph)
|
||||||
.arg("--query")
|
.arg("--query")
|
||||||
.arg(&mutation_file)
|
.arg(&mutation_file)
|
||||||
.arg("--params")
|
.arg("--params")
|
||||||
|
|
@ -1896,19 +1918,17 @@ fn snapshot_json_returns_manifest_version_and_tables() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn snapshot_can_resolve_uri_from_config() {
|
fn snapshot_resolves_uri_from_store_scope() {
|
||||||
let temp = tempdir().unwrap();
|
let temp = tempdir().unwrap();
|
||||||
let graph = graph_path(temp.path());
|
let graph = graph_path(temp.path());
|
||||||
let config = temp.path().join("omnigraph.yaml");
|
|
||||||
init_graph(&graph);
|
init_graph(&graph);
|
||||||
load_fixture(&graph);
|
load_fixture(&graph);
|
||||||
write_config(&config, &local_yaml_config(&graph));
|
|
||||||
|
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("snapshot")
|
.arg("snapshot")
|
||||||
.arg("--config")
|
.arg("--store")
|
||||||
.arg(&config)
|
.arg(&graph)
|
||||||
.arg("--json"),
|
.arg("--json"),
|
||||||
);
|
);
|
||||||
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
let payload: Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -94,90 +94,91 @@ fn alias_unknown_name_errors_listing_defined() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC-011: `queries validate`/`list` source the registry + schemas from a
|
||||||
|
// converged cluster's applied state (`--cluster <dir>`), not omnigraph.yaml.
|
||||||
|
|
||||||
|
/// Build a converged single-graph cluster (id `knowledge`) with one stored
|
||||||
|
/// query. `query_block` is the YAML under the graph's `queries:` key.
|
||||||
|
fn converged_cluster_with_query(query_file: &str, query_src: &str, query_block: &str) -> tempfile::TempDir {
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let dir = temp.path();
|
||||||
|
std::fs::copy(fixture("test.pg"), dir.join("graph.pg")).unwrap();
|
||||||
|
write_query_file(&dir.join(query_file), query_src);
|
||||||
|
std::fs::write(
|
||||||
|
dir.join("cluster.yaml"),
|
||||||
|
format!(
|
||||||
|
"version: 1\nmetadata:\n name: sys\nstate:\n backend: cluster\n lock: true\n\
|
||||||
|
graphs:\n knowledge:\n schema: ./graph.pg\n queries:\n{query_block}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
output_success(cli().arg("cluster").arg("import").arg("--config").arg(dir));
|
||||||
|
output_success(cli().arg("cluster").arg("apply").arg("--config").arg(dir));
|
||||||
|
temp
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_validate_exits_zero_on_clean_registry() {
|
fn queries_validate_exits_zero_on_clean_registry() {
|
||||||
let graph = SystemGraph::loaded();
|
let cluster = converged_cluster_with_query(
|
||||||
graph.write_query(
|
|
||||||
"find_person.gq",
|
"find_person.gq",
|
||||||
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
||||||
);
|
" find_person:\n file: ./find_person.gq\n",
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
&queries_test_config(
|
|
||||||
&graph.path().to_string_lossy(),
|
|
||||||
"find_person",
|
|
||||||
"find_person.gq",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("queries")
|
.arg("queries")
|
||||||
.arg("validate")
|
.arg("validate")
|
||||||
.arg("--config")
|
.arg("--cluster")
|
||||||
.arg(&config),
|
.arg(cluster.path()),
|
||||||
);
|
);
|
||||||
let stdout = stdout_string(&output);
|
let stdout = stdout_string(&output);
|
||||||
assert!(stdout.contains("OK"), "stdout:\n{stdout}");
|
assert!(stdout.contains("OK"), "stdout:\n{stdout}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_validate_exits_nonzero_on_type_broken_query() {
|
fn cluster_import_rejects_a_type_broken_query() {
|
||||||
let graph = SystemGraph::loaded();
|
// In the cluster model a stored query is type-checked at the cluster
|
||||||
// `Widget` is not in the fixture schema.
|
// boundary (import/apply), so a broken query can never reach the applied
|
||||||
graph.write_query(
|
// state `queries validate` reads — the gate is upstream. `Widget` is not in
|
||||||
"ghost.gq",
|
// the fixture schema, so import must reject it, naming the query.
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let dir = temp.path();
|
||||||
|
std::fs::copy(fixture("test.pg"), dir.join("graph.pg")).unwrap();
|
||||||
|
write_query_file(
|
||||||
|
&dir.join("ghost.gq"),
|
||||||
"query ghost() { match { $w: Widget } return { $w.name } }",
|
"query ghost() { match { $w: Widget } return { $w.name } }",
|
||||||
);
|
);
|
||||||
let config = graph.write_config(
|
std::fs::write(
|
||||||
"omnigraph.yaml",
|
dir.join("cluster.yaml"),
|
||||||
&queries_test_config(&graph.path().to_string_lossy(), "ghost", "ghost.gq"),
|
"version: 1\nmetadata:\n name: sys\nstate:\n backend: cluster\n lock: true\n\
|
||||||
|
graphs:\n knowledge:\n schema: ./graph.pg\n queries:\n ghost:\n file: ./ghost.gq\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let output = output_failure(cli().arg("cluster").arg("import").arg("--config").arg(dir));
|
||||||
|
let combined = format!(
|
||||||
|
"{}{}",
|
||||||
|
stdout_string(&output),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
);
|
);
|
||||||
let output = output_failure(
|
|
||||||
cli()
|
|
||||||
.arg("queries")
|
|
||||||
.arg("validate")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(&config),
|
|
||||||
);
|
|
||||||
let stdout = stdout_string(&output);
|
|
||||||
assert!(
|
assert!(
|
||||||
stdout.contains("ghost"),
|
combined.contains("ghost"),
|
||||||
"validation should name the broken query; stdout:\n{stdout}"
|
"cluster import must reject the broken query, naming it; got:\n{combined}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_list_prints_registered_query() {
|
fn queries_list_prints_registered_query() {
|
||||||
let graph = SystemGraph::loaded();
|
let cluster = converged_cluster_with_query(
|
||||||
graph.write_query(
|
|
||||||
"find_person.gq",
|
"find_person.gq",
|
||||||
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
||||||
);
|
" find_person:\n file: ./find_person.gq\n",
|
||||||
// Exposed with an explicit tool name so the list shows the MCP suffix.
|
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
&format!(
|
|
||||||
concat!(
|
|
||||||
"graphs:\n",
|
|
||||||
" local:\n",
|
|
||||||
" uri: '{}'\n",
|
|
||||||
" queries:\n",
|
|
||||||
" find_person:\n",
|
|
||||||
" file: ./find_person.gq\n",
|
|
||||||
" mcp: {{ expose: true, tool_name: lookup_person }}\n",
|
|
||||||
"cli:\n",
|
|
||||||
" graph: local\n",
|
|
||||||
"policy: {{}}\n",
|
|
||||||
),
|
|
||||||
graph.path().to_string_lossy().replace('\'', "''")
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("queries")
|
.arg("queries")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
.arg("--config")
|
.arg("--cluster")
|
||||||
.arg(&config),
|
.arg(cluster.path()),
|
||||||
);
|
);
|
||||||
let stdout = stdout_string(&output);
|
let stdout = stdout_string(&output);
|
||||||
assert!(stdout.contains("find_person"), "stdout:\n{stdout}");
|
assert!(stdout.contains("find_person"), "stdout:\n{stdout}");
|
||||||
|
|
@ -185,242 +186,37 @@ fn queries_list_prints_registered_query() {
|
||||||
stdout.contains("$name: String"),
|
stdout.contains("$name: String"),
|
||||||
"list should show typed params; stdout:\n{stdout}"
|
"list should show typed params; stdout:\n{stdout}"
|
||||||
);
|
);
|
||||||
assert!(
|
|
||||||
stdout.contains("[mcp: lookup_person]"),
|
|
||||||
"list should show the MCP tool name for exposed queries; stdout:\n{stdout}"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_list_requires_graph_selection_for_per_graph_only_registries() {
|
fn queries_validate_requires_a_cluster() {
|
||||||
let graph = SystemGraph::loaded();
|
// RFC-011: with no --cluster (and no cluster profile), the command errors
|
||||||
graph.write_query(
|
// loudly rather than reading any omnigraph.yaml.
|
||||||
"find_person.gq",
|
let output = output_failure(cli().arg("queries").arg("validate"));
|
||||||
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
|
||||||
);
|
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
&format!(
|
|
||||||
concat!(
|
|
||||||
"graphs:\n",
|
|
||||||
" local:\n",
|
|
||||||
" uri: '{}'\n",
|
|
||||||
" queries:\n",
|
|
||||||
" find_person:\n",
|
|
||||||
" file: ./find_person.gq\n",
|
|
||||||
"policy: {{}}\n",
|
|
||||||
),
|
|
||||||
graph.path().to_string_lossy().replace('\'', "''")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = output_failure(
|
|
||||||
cli()
|
|
||||||
.arg("queries")
|
|
||||||
.arg("list")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(&config),
|
|
||||||
);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
assert!(
|
assert!(
|
||||||
stderr.contains("local") && stderr.contains("set `cli.graph`"),
|
stderr.contains("needs a cluster") || stderr.contains("--cluster"),
|
||||||
"error must name the graph and give a concrete selection hint; stderr:\n{stderr}"
|
"queries validate must require a cluster; stderr:\n{stderr}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn queries_list_without_graph_selection_lists_top_level_registry() {
|
fn queries_validate_graph_filter_selects_one_graph() {
|
||||||
let graph = SystemGraph::loaded();
|
// A multi-graph cluster: validate scoped to `knowledge` type-checks only
|
||||||
graph.write_query(
|
// that graph's registry, ignoring `engineering`'s.
|
||||||
"top_find.gq",
|
let temp = tempdir().unwrap();
|
||||||
"query top_find($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
let dir = temp.path();
|
||||||
);
|
write_multi_graph_cluster_fixture(dir);
|
||||||
let config = graph.write_config(
|
output_success(cli().arg("cluster").arg("import").arg("--config").arg(dir));
|
||||||
"omnigraph.yaml",
|
output_success(cli().arg("cluster").arg("apply").arg("--config").arg(dir));
|
||||||
concat!(
|
|
||||||
"queries:\n",
|
|
||||||
" top_find:\n",
|
|
||||||
" file: ./top_find.gq\n",
|
|
||||||
"policy: {}\n",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
let output = output_success(
|
|
||||||
cli()
|
|
||||||
.arg("queries")
|
|
||||||
.arg("list")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(&config),
|
|
||||||
);
|
|
||||||
let stdout = stdout_string(&output);
|
|
||||||
assert!(stdout.contains("top_find"), "stdout:\n{stdout}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
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 `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",
|
|
||||||
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
|
||||||
);
|
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
&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("--config").arg(&config));
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(
|
|
||||||
stderr.contains("nonexistent"),
|
|
||||||
"error must name the unknown graph; stderr:\n{stderr}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn queries_commands_reject_named_graph_with_populated_top_level_block() {
|
|
||||||
// A named graph (here via `cli.graph`) uses its own `graphs.<name>` block,
|
|
||||||
// so a populated top-level `queries:` block would be silently ignored — a
|
|
||||||
// config the server REFUSES to boot. `queries validate`/`list` must reject
|
|
||||||
// it too (matching boot) instead of validating/listing the per-graph block
|
|
||||||
// and giving a false green.
|
|
||||||
let graph = SystemGraph::loaded();
|
|
||||||
graph.write_query(
|
|
||||||
"find_person.gq",
|
|
||||||
"query find_person($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
|
||||||
);
|
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
&format!(
|
|
||||||
concat!(
|
|
||||||
"graphs:\n",
|
|
||||||
" local:\n",
|
|
||||||
" uri: '{}'\n",
|
|
||||||
" queries:\n",
|
|
||||||
" find_person:\n",
|
|
||||||
" file: ./find_person.gq\n",
|
|
||||||
"cli:\n",
|
|
||||||
" graph: local\n",
|
|
||||||
"queries:\n", // populated top-level block: the coherence violation
|
|
||||||
" legacy:\n",
|
|
||||||
" file: ./legacy.gq\n",
|
|
||||||
"policy: {{}}\n",
|
|
||||||
),
|
|
||||||
graph.path().to_string_lossy().replace('\'', "''")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// Both resolve `local` from cli.graph (no positional URI), so both must
|
|
||||||
// error and name the graph + the ignored block — like server boot does.
|
|
||||||
for sub in ["validate", "list"] {
|
|
||||||
let output = output_failure(cli().arg("queries").arg(sub).arg("--config").arg(&config));
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(
|
|
||||||
stderr.contains("local") && stderr.contains("queries"),
|
|
||||||
"`queries {sub}` must reject a named graph with a populated top-level block; stderr:\n{stderr}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn queries_validate_exits_nonzero_on_duplicate_tool_name() {
|
|
||||||
// Two exposed queries claiming one MCP tool name is a load-time
|
|
||||||
// collision — `queries validate` must fail (offline, before the engine
|
|
||||||
// opens) and name both queries plus the contested tool.
|
|
||||||
let graph = SystemGraph::loaded();
|
|
||||||
graph.write_query(
|
|
||||||
"a.gq",
|
|
||||||
"query a() { match { $p: Person } return { $p.name } }",
|
|
||||||
);
|
|
||||||
graph.write_query(
|
|
||||||
"b.gq",
|
|
||||||
"query b() { match { $p: Person } return { $p.name } }",
|
|
||||||
);
|
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
&format!(
|
|
||||||
concat!(
|
|
||||||
"graphs:\n",
|
|
||||||
" local:\n",
|
|
||||||
" uri: '{}'\n",
|
|
||||||
" queries:\n",
|
|
||||||
" a:\n",
|
|
||||||
" file: ./a.gq\n",
|
|
||||||
" mcp: {{ expose: true, tool_name: dup }}\n",
|
|
||||||
" b:\n",
|
|
||||||
" file: ./b.gq\n",
|
|
||||||
" mcp: {{ expose: true, tool_name: dup }}\n",
|
|
||||||
"cli:\n",
|
|
||||||
" graph: local\n",
|
|
||||||
"policy: {{}}\n",
|
|
||||||
),
|
|
||||||
graph.path().to_string_lossy().replace('\'', "''")
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let output = output_failure(
|
|
||||||
cli()
|
|
||||||
.arg("queries")
|
|
||||||
.arg("validate")
|
|
||||||
.arg("--config")
|
|
||||||
.arg(&config),
|
|
||||||
);
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(
|
|
||||||
stderr.contains("dup") && stderr.contains("'a'") && stderr.contains("'b'"),
|
|
||||||
"duplicate tool name should be reported naming both queries; stderr:\n{stderr}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn queries_validate_positional_uri_ignores_default_graph() {
|
|
||||||
// A positional URI is anonymous → the schema AND the registry both come
|
|
||||||
// from top-level, even when `cli.graph` names a graph whose per-graph
|
|
||||||
// queries would fail. Pins that the URI and registry can't diverge.
|
|
||||||
let graph = SystemGraph::loaded();
|
|
||||||
graph.write_query(
|
|
||||||
"clean.gq",
|
|
||||||
"query clean($name: String) { match { $p: Person { name: $name } } return { $p.age } }",
|
|
||||||
);
|
|
||||||
// `Widget` is not in the fixture schema — the default graph's per-graph
|
|
||||||
// query would break validate if it were (wrongly) selected.
|
|
||||||
graph.write_query(
|
|
||||||
"broken.gq",
|
|
||||||
"query broken() { match { $w: Widget } return { $w.name } }",
|
|
||||||
);
|
|
||||||
let config = graph.write_config(
|
|
||||||
"omnigraph.yaml",
|
|
||||||
concat!(
|
|
||||||
"cli:\n graph: prod\n",
|
|
||||||
"graphs:\n",
|
|
||||||
" prod:\n",
|
|
||||||
" uri: /nonexistent-prod.omni\n",
|
|
||||||
" queries:\n",
|
|
||||||
" broken:\n",
|
|
||||||
" file: ./broken.gq\n",
|
|
||||||
"queries:\n",
|
|
||||||
" clean:\n",
|
|
||||||
" file: ./clean.gq\n",
|
|
||||||
"policy: {}\n",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// Positional URI = the real loaded graph; selection is anonymous, so the
|
|
||||||
// CLEAN top-level registry validates (not prod's broken one).
|
|
||||||
let output = output_success(
|
let output = output_success(
|
||||||
cli()
|
cli()
|
||||||
.arg("queries")
|
.arg("queries")
|
||||||
.arg("validate")
|
.arg("validate")
|
||||||
.arg(graph.path())
|
.arg("--cluster")
|
||||||
.arg("--config")
|
.arg(dir)
|
||||||
.arg(&config),
|
.arg("--graph")
|
||||||
);
|
.arg("knowledge"),
|
||||||
let stdout = stdout_string(&output);
|
|
||||||
assert!(
|
|
||||||
stdout.contains("OK"),
|
|
||||||
"positional URI must validate the top-level registry, not the cli.graph default; stdout:\n{stdout}"
|
|
||||||
);
|
);
|
||||||
|
assert!(stdout_string(&output).contains("OK"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -546,60 +546,22 @@ fn graphs_subcommand_help_lists_list_only() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn graphs_list_against_local_uri_errors_with_remote_only_message() {
|
fn graphs_list_against_local_uri_errors_with_remote_only_message() {
|
||||||
|
// RFC-011: `graphs list` is served-only; a `--store` (local) address has no
|
||||||
|
// enumeration endpoint, so it fails loudly pointing at a server / cluster.
|
||||||
let output = output_failure(
|
let output = output_failure(
|
||||||
cli()
|
cli()
|
||||||
.arg("graphs")
|
.arg("graphs")
|
||||||
.arg("list")
|
.arg("list")
|
||||||
.arg("--uri")
|
.arg("--store")
|
||||||
.arg("/tmp/local"),
|
.arg("/tmp/local"),
|
||||||
);
|
);
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
|
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
|
||||||
assert!(
|
assert!(
|
||||||
stderr.contains("remote multi-graph server URL"),
|
stderr.contains("remote multi-graph server"),
|
||||||
"expected 'remote multi-graph server URL' rejection in stderr; got:\n{stderr}"
|
"expected a remote-server rejection in stderr; got:\n{stderr}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RFC-008 stage 1: loading a legacy omnigraph.yaml emits the per-key
|
|
||||||
/// deprecation block (the migration map applied to THIS file), suppressible
|
|
||||||
/// via OMNIGRAPH_SUPPRESS_YAML_DEPRECATION.
|
|
||||||
#[test]
|
|
||||||
fn legacy_config_load_warns_per_key_and_suppression_silences() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
fs::write(
|
|
||||||
temp.path().join("omnigraph.yaml"),
|
|
||||||
"cli:\n actor: act-x\ngraphs:\n g:\n uri: /tmp/never-opened\n",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// `graphs list --json` loads the config and exits without touching the
|
|
||||||
// graph URI.
|
|
||||||
let output = cli()
|
|
||||||
.current_dir(temp.path())
|
|
||||||
.arg("graphs")
|
|
||||||
.arg("list")
|
|
||||||
.arg("--json")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(
|
|
||||||
stderr.contains("deprecated (RFC-008)") && stderr.contains("`cli.actor` -> `operator.actor`"),
|
|
||||||
"{stderr}"
|
|
||||||
);
|
|
||||||
assert!(stderr.contains("config migrate"), "{stderr}");
|
|
||||||
|
|
||||||
let output = cli()
|
|
||||||
.current_dir(temp.path())
|
|
||||||
.env("OMNIGRAPH_SUPPRESS_YAML_DEPRECATION", "1")
|
|
||||||
.arg("graphs")
|
|
||||||
.arg("list")
|
|
||||||
.arg("--json")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(!stderr.contains("deprecated (RFC-008)"), "{stderr}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RFC-008 stage 2: `config migrate` proposes the split read-only, applies
|
/// RFC-008 stage 2: `config migrate` proposes the split read-only, applies
|
||||||
/// it with --write (operator merge never clobbers; cluster.yaml emitted),
|
/// it with --write (operator merge never clobbers; cluster.yaml emitted),
|
||||||
/// and a second --write is idempotent.
|
/// and a second --write is idempotent.
|
||||||
|
|
@ -671,38 +633,3 @@ fn config_migrate_splits_legacy_config() {
|
||||||
assert!(temp.path().join("cluster.yaml.proposed").exists());
|
assert!(temp.path().join("cluster.yaml.proposed").exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RFC-008 stage 4: OMNIGRAPH_NO_LEGACY_CONFIG refuses a present legacy
|
|
||||||
/// file (pointing at config migrate) but changes nothing on migrated
|
|
||||||
/// setups with no file.
|
|
||||||
#[test]
|
|
||||||
fn strict_mode_refuses_legacy_file_but_not_its_absence() {
|
|
||||||
let temp = tempdir().unwrap();
|
|
||||||
fs::write(temp.path().join("omnigraph.yaml"), "cli:\n actor: a\n").unwrap();
|
|
||||||
let output = cli()
|
|
||||||
.current_dir(temp.path())
|
|
||||||
.env("OMNIGRAPH_NO_LEGACY_CONFIG", "1")
|
|
||||||
.arg("graphs")
|
|
||||||
.arg("list")
|
|
||||||
.arg("--json")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
assert!(!output.status.success());
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
assert!(
|
|
||||||
stderr.contains("OMNIGRAPH_NO_LEGACY_CONFIG") && stderr.contains("config migrate"),
|
|
||||||
"{stderr}"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Migrated setup (no file): strict mode is a no-op — a config-loading
|
|
||||||
// command that tolerates empty defaults succeeds.
|
|
||||||
let clean = tempdir().unwrap();
|
|
||||||
let output = cli()
|
|
||||||
.current_dir(clean.path())
|
|
||||||
.env("OMNIGRAPH_NO_LEGACY_CONFIG", "1")
|
|
||||||
.arg("queries")
|
|
||||||
.arg("list")
|
|
||||||
.arg("--json")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
assert!(output.status.success(), "{output:?}");
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,12 @@ const KNOWN_DIVERGENCES: &[&str] = &[
|
||||||
// populated by the rows below as they are written
|
// populated by the rows below as they are written
|
||||||
];
|
];
|
||||||
|
|
||||||
/// One matched setup per row: twin graphs + the SAME Cedar bundle on both
|
/// One matched setup per row: twin graphs + the parity Cedar bundle on the
|
||||||
/// arms (the local arm via --config top-level policy.file; the server via
|
/// served arm. The local (`--store`) arm carries no policy (RFC-011); the
|
||||||
/// its config). Returns everything a row needs.
|
/// bundle is permissive for `act-parity`, so the arms still agree.
|
||||||
struct Parity {
|
struct Parity {
|
||||||
_temp: TempDir,
|
_temp: TempDir,
|
||||||
local: std::path::PathBuf,
|
local: std::path::PathBuf,
|
||||||
local_cfg: std::path::PathBuf,
|
|
||||||
server: TestServer,
|
server: TestServer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +39,7 @@ fn parity() -> Parity {
|
||||||
// RFC-011 cluster-only: the remote arm is served from a converged
|
// RFC-011 cluster-only: the remote arm is served from a converged
|
||||||
// cluster directory (one graph, id `parity`), seeded with the same
|
// cluster directory (one graph, id `parity`), seeded with the same
|
||||||
// fixture data as the local twin.
|
// fixture data as the local twin.
|
||||||
let (local_cfg, cluster_dir) = parity_configs(temp.path(), &local, &remote);
|
let cluster_dir = parity_configs(temp.path(), &local, &remote);
|
||||||
let server = spawn_server_with_cluster_env(
|
let server = spawn_server_with_cluster_env(
|
||||||
&cluster_dir,
|
&cluster_dir,
|
||||||
&[(
|
&[(
|
||||||
|
|
@ -51,14 +50,13 @@ fn parity() -> Parity {
|
||||||
Parity {
|
Parity {
|
||||||
_temp: temp,
|
_temp: temp,
|
||||||
local,
|
local,
|
||||||
local_cfg,
|
|
||||||
server,
|
server,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parity {
|
impl Parity {
|
||||||
fn run(&self, args: &[&str]) -> (std::process::Output, std::process::Output) {
|
fn run(&self, args: &[&str]) -> (std::process::Output, std::process::Output) {
|
||||||
run_both_with_config(&self.local, Some(&self.local_cfg), &self.server.base_url, args)
|
run_both(&self.local, &self.server.base_url, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -850,35 +850,25 @@ rules:
|
||||||
/// server is cluster-only, so a graph selector is required).
|
/// server is cluster-only, so a graph selector is required).
|
||||||
pub const PARITY_GRAPH_ID: &str = "parity";
|
pub const PARITY_GRAPH_ID: &str = "parity";
|
||||||
|
|
||||||
/// Build both arms' configuration (RFC-011 cluster-only server).
|
/// Build the remote arm's configuration (RFC-011 cluster-only server).
|
||||||
///
|
///
|
||||||
/// * Local arm: a `--config` file carrying the TOP-LEVEL `policy.file`
|
/// The remote arm is served from a converged cluster directory whose single
|
||||||
/// (single-graph embedded semantics), used as-is by `run_both_with_config`.
|
/// graph (id `parity`) carries the parity Cedar bundle (bound to the graph
|
||||||
/// * Remote arm: a converged cluster directory whose single graph (id
|
/// scope). The cluster's derived graph root (`<dir>/graphs/parity.omni`) is
|
||||||
/// `parity`) carries the SAME Cedar bundle (bound to the graph scope).
|
/// seeded with the SAME fixture data as the local twin so the two arms compare
|
||||||
/// The cluster's derived graph root (`<dir>/graphs/parity.omni`) is
|
/// like-for-like. The local (`--store`) arm carries no Cedar policy (RFC-011),
|
||||||
/// seeded with the SAME fixture data as the local twin so the two arms
|
/// which is fine because the parity bundle is permissive for `act-parity`.
|
||||||
/// compare like-for-like.
|
|
||||||
///
|
///
|
||||||
/// `local_graph` is overwritten with a byte-for-byte copy of the cluster's
|
/// `local_graph` is overwritten with a byte-for-byte copy of the cluster's
|
||||||
/// seeded served graph so identity-bearing values that are NOT scrubbed
|
/// seeded served graph so identity-bearing values that are NOT scrubbed
|
||||||
/// (e.g. `graph_commit_id`, edge `id`s in export) match across the arms —
|
/// (e.g. `graph_commit_id`, edge `id`s in export) match across the arms —
|
||||||
/// the served graph is the source of truth and the local twin mirrors it.
|
/// the served graph is the source of truth and the local twin mirrors it.
|
||||||
///
|
///
|
||||||
/// Returns `(local_config_path, cluster_dir)`. The caller spawns the
|
/// Returns the `cluster_dir`. The caller spawns the server with `--cluster`.
|
||||||
/// server with `--cluster <cluster_dir>`.
|
pub fn parity_configs(root: &Path, local_graph: &Path, _remote_graph: &Path) -> PathBuf {
|
||||||
pub fn parity_configs(root: &Path, local_graph: &Path, _remote_graph: &Path) -> (PathBuf, PathBuf) {
|
|
||||||
let policy = root.join("parity.policy.yaml");
|
let policy = root.join("parity.policy.yaml");
|
||||||
fs::write(&policy, parity_policy_yaml()).unwrap();
|
fs::write(&policy, parity_policy_yaml()).unwrap();
|
||||||
|
|
||||||
// Local arm config: top-level single-graph policy.
|
|
||||||
let local_cfg = root.join("local.omnigraph.yaml");
|
|
||||||
fs::write(
|
|
||||||
&local_cfg,
|
|
||||||
format!("policy:\n file: {}\n", policy.display()),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Remote arm: a cluster directory the server boots from. One graph
|
// Remote arm: a cluster directory the server boots from. One graph
|
||||||
// (`parity`), schema = the shared fixture, policy bound to the graph.
|
// (`parity`), schema = the shared fixture, policy bound to the graph.
|
||||||
let cluster_dir = root.join("parity-cluster");
|
let cluster_dir = root.join("parity-cluster");
|
||||||
|
|
@ -942,7 +932,7 @@ policies:
|
||||||
}
|
}
|
||||||
copy_dir(&served_root, local_graph);
|
copy_dir(&served_root, local_graph);
|
||||||
|
|
||||||
(local_cfg, cluster_dir)
|
cluster_dir
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run one CLI invocation per arm with identical verb args: locally against
|
/// Run one CLI invocation per arm with identical verb args: locally against
|
||||||
|
|
@ -953,21 +943,14 @@ pub fn run_both(
|
||||||
local_graph: &Path,
|
local_graph: &Path,
|
||||||
server_url: &str,
|
server_url: &str,
|
||||||
args: &[&str],
|
args: &[&str],
|
||||||
) -> (std::process::Output, std::process::Output) {
|
|
||||||
run_both_with_config(local_graph, None, server_url, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_both_with_config(
|
|
||||||
local_graph: &Path,
|
|
||||||
local_config: Option<&Path>,
|
|
||||||
server_url: &str,
|
|
||||||
args: &[&str],
|
|
||||||
) -> (std::process::Output, std::process::Output) {
|
) -> (std::process::Output, std::process::Output) {
|
||||||
// Address both arms with GLOBAL flags (`--store` / `--server`) appended after
|
// Address both arms with GLOBAL flags (`--store` / `--server`) appended after
|
||||||
// the verb + its args, so the address is placed correctly regardless of
|
// the verb + its args, so the address is placed correctly regardless of
|
||||||
// subcommand nesting (a positional graph only works for top-level verbs;
|
// subcommand nesting (a positional graph only works for top-level verbs;
|
||||||
// `schema show <graph>` etc. need the global flag). Local = embedded store,
|
// `schema show <graph>` etc. need the global flag). Local = embedded store,
|
||||||
// remote = served.
|
// remote = served. RFC-011: a direct (`--store`) write carries no Cedar
|
||||||
|
// policy — the parity policy is permissive for `act-parity` on the served
|
||||||
|
// arm, so the two arms still agree.
|
||||||
let mut local = cli();
|
let mut local = cli();
|
||||||
local
|
local
|
||||||
.args(args)
|
.args(args)
|
||||||
|
|
@ -975,9 +958,6 @@ pub fn run_both_with_config(
|
||||||
.arg(local_graph)
|
.arg(local_graph)
|
||||||
.arg("--as")
|
.arg("--as")
|
||||||
.arg(PARITY_ACTOR);
|
.arg(PARITY_ACTOR);
|
||||||
if let Some(config) = local_config {
|
|
||||||
local.arg("--config").arg(config);
|
|
||||||
}
|
|
||||||
let local_out = local.output().unwrap();
|
let local_out = local.output().unwrap();
|
||||||
|
|
||||||
let mut remote = cli();
|
let mut remote = cli();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue