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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions crates/omnigraph-api-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1743,8 +1743,8 @@ pub fn read_target_output(target: &ReadTarget) -> ReadTargetOutput {

/// One entry in the response from `GET /graphs`. Cluster operators
/// consume this list to discover which graphs the server is currently
/// serving. The shape is intentionally minimal — `graph_id` and `uri`
/// are the only fields a routing client needs.
/// serving. This legacy metadata includes the storage `uri`; identity-only
/// existence discovery uses [`GraphDiscoveryEntry`] instead.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct GraphInfo {
pub graph_id: String,
Expand All @@ -1764,6 +1764,22 @@ pub struct GraphListResponse {
pub quarantined: Vec<String>,
}

/// A graph's existence, without storage, schema, data, or serving metadata.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct GraphDiscoveryEntry {
pub graph_id: String,
/// Currently the graph identifier; no separate display name is configured.
pub display_name: String,
}

/// Authenticated minimal inventory from `GET /graphs/discovery`.
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(deny_unknown_fields)]
pub struct GraphDiscoveryResponse {
pub graphs: Vec<GraphDiscoveryEntry>,
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions crates/omnigraph-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ omnigraph-cluster = { path = "../omnigraph-cluster", version = "0.10.0" }
omnigraph-policy = { path = "../omnigraph-policy", version = "0.10.0" }
omnigraph-server = { path = "../omnigraph-server", version = "0.10.0" }
clap = { workspace = true }
base64 = { workspace = true }
color-eyre = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true, features = ["raw_value"] }
Expand Down
9 changes: 6 additions & 3 deletions crates/omnigraph-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,14 +574,14 @@ pub(crate) enum ClusterCommand {
#[command(flatten)]
managed: ManagedRunArgs,
},
/// Cache a scoped data credential for this managed cluster, or forget it locally.
/// Cache an identity credential for this cluster, or forget it locally.
Token {
#[arg(long, default_value = ".")]
config: PathBuf,
#[arg(long)]
json: bool,
/// Comma-separated data actions, such as read,change.
#[arg(long, required_unless_present = "clear", conflicts_with = "clear")]
/// Legacy restricted profile: exact comma-separated actions; requires --graph.
#[arg(long, conflicts_with = "clear")]
actions: Option<String>,
/// Credential lifetime, 60 seconds to 24 hours (default 1h).
#[arg(long, value_parser = crate::managed::data::parse_ttl, conflicts_with = "clear")]
Expand Down Expand Up @@ -758,6 +758,9 @@ pub(crate) enum GraphsCommand {
List {
#[arg(long)]
json: bool,
/// Minimal authenticated graph existence; requires an identity credential.
#[arg(long)]
discovery: bool,
},
}

Expand Down
52 changes: 42 additions & 10 deletions crates/omnigraph-cli/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ use omnigraph_api_types::{
BranchOutcomeOutput, ChangeBaselineOutput, ChangeBaselineRecord, ChangeBaselineRequest,
ChangeFeedOutput, ChangeOpOutput, ChangeOutput, ChangeRequest, CommitChangesOutput,
CommitListOutput, CommitOutput, EntityKindOutput, ErrorOutput, ExportRequest,
GraphBatchLoadOutput, GraphListResponse, IngestOutput, IngestRequest, InvokeStoredQueryRequest,
QueryRequest, ReadOutput, SchemaApplyOutput, SchemaApplyRequest, SchemaOutput, SnapshotOutput,
branch_list_read_output, change_baseline_output, change_feed_output, change_scope,
commit_changes_output, commit_output, ingest_receipt_output, read_output, schema_apply_output,
snapshot_payload,
GraphBatchLoadOutput, GraphDiscoveryResponse, GraphListResponse, IngestOutput, IngestRequest,
InvokeStoredQueryRequest, QueryRequest, ReadOutput, SchemaApplyOutput, SchemaApplyRequest,
SchemaOutput, SnapshotOutput, branch_list_read_output, change_baseline_output,
change_feed_output, change_scope, commit_changes_output, commit_output, ingest_receipt_output,
read_output, schema_apply_output, snapshot_payload,
};
use omnigraph_compiler::catalog::Catalog;
use omnigraph_compiler::query::ast::BranchWrite;
Expand All @@ -52,7 +52,7 @@ use crate::blob_cli::{
};
use crate::cli::CliLoadMode;
use crate::helpers::{
apply_bearer_token, apply_server_flag, branch_statement_change_request,
RemoteErrorCli, apply_bearer_token, apply_server_flag, branch_statement_change_request,
branch_statement_query_request, build_blob_http_client, build_http_client, is_remote_uri,
legacy_change_request_body, precondition_failed_cli, query_params_from_json, remote_json,
remote_json_bounded, remote_url, resolve_cli_actor, resolve_cli_graph,
Expand Down Expand Up @@ -119,13 +119,21 @@ fn reject_positional_remote(via_server: bool, uri: &str) -> Result<()> {
impl GraphClient {
/// An already validated managed credential never enters legacy scope or token resolution.
pub(crate) fn managed(endpoint: &str, graph: &str, token: String) -> Result<Self> {
Self::managed_url(remote_url(endpoint, &["graphs", graph], &[])?, token)
}

pub(crate) fn managed_registry(endpoint: &str, token: String) -> Result<Self> {
Self::managed_url(endpoint.to_owned(), token)
}

fn managed_url(base_url: String, token: String) -> Result<Self> {
Ok(Self::Remote {
http: reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.connect_timeout(std::time::Duration::from_secs(10))
.timeout(std::time::Duration::from_secs(10))
.build()?,
base_url: remote_url(endpoint, &["graphs", graph], &[])?,
base_url,
token: Some(token),
response_limit: Some(8 * 1024 * 1024),
})
Expand Down Expand Up @@ -626,7 +634,7 @@ impl GraphClient {
if !status.is_success() {
let text = response.text().await?;
if let Ok(error) = serde_json::from_str::<ErrorOutput>(&text) {
bail!(error.error);
return Err(RemoteErrorCli { output: error }.into());
}
bail!("server returned {}: {}", status, text);
}
Expand Down Expand Up @@ -707,7 +715,7 @@ impl GraphClient {
let text = response.text().await?;
if !status.is_success() {
if let Ok(error) = serde_json::from_str::<ErrorOutput>(&text) {
bail!(error.error);
return Err(RemoteErrorCli { output: error }.into());
}
bail!("server returned {}: {}", status, text);
}
Expand Down Expand Up @@ -1319,7 +1327,7 @@ impl GraphClient {
if !status.is_success() {
let text = response.text().await?;
if let Ok(error) = serde_json::from_str::<ErrorOutput>(&text) {
bail!(error.error);
return Err(RemoteErrorCli { output: error }.into());
}
bail!("server returned {}: {}", status, text);
}
Expand Down Expand Up @@ -1531,6 +1539,30 @@ impl GraphClient {
),
}
}

/// Minimal existence inventory. No fallback to the metadata-bearing catalog.
pub(crate) async fn discover_graphs(&self) -> Result<GraphDiscoveryResponse> {
match self {
Self::Remote {
http,
base_url,
token,
response_limit,
} => {
remote_json_bounded(
http,
Method::GET,
remote_url(base_url, &["graphs", "discovery"], &[])?,
None,
token.as_deref(),
None,
response_limit.or(Some(8 * 1024 * 1024)),
)
.await
}
Self::Embedded { .. } => bail!("graph discovery requires a server"),
}
}
}

fn validate_content_range(
Expand Down
17 changes: 16 additions & 1 deletion crates/omnigraph-cli/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,21 @@ impl std::fmt::Display for PreconditionFailedCli {

impl std::error::Error for PreconditionFailedCli {}

/// Preserve a typed server refusal through the command dispatch so JSON
/// callers retain its code and detail fields instead of parsing a message.
#[derive(Debug)]
pub(crate) struct RemoteErrorCli {
pub(crate) output: ErrorOutput,
}

impl std::fmt::Display for RemoteErrorCli {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.output.error)
}
}

impl std::error::Error for RemoteErrorCli {}

/// Build the typed CAS-lost error for the embedded transport, mirroring the
/// structured body a server would have returned so `--json` output is
/// transport-uniform. `message` is the engine error's own `Display` text, so
Expand Down Expand Up @@ -575,7 +590,7 @@ pub(crate) async fn remote_json_bounded<T: DeserializeOwned>(
if error.precondition_failure.is_some() {
return Err(PreconditionFailedCli { output: error }.into());
}
bail!(error.error);
return Err(RemoteErrorCli { output: error }.into());
}
bail!("server returned {}: {}", status, text);
}
Expand Down
66 changes: 56 additions & 10 deletions crates/omnigraph-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ fn installed_file_is_current(installed: &fs::File, path: &std::path::Path) -> Re
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
let cli = {
let (cli, json) = {
let raw_args = rewrite_deprecated_argv(std::env::args_os().collect());
let matches = Cli::command()
.arg(
Expand All @@ -147,8 +147,32 @@ async fn main() -> Result<()> {
.help("Print version"),
)
.get_matches_from(raw_args);
Cli::from_arg_matches(&matches)?
let mut command_matches = &matches;
while let Some((_, child)) = command_matches.subcommand() {
command_matches = child;
}
let json = command_matches
.try_get_one::<bool>("json")
.ok()
.flatten()
.copied()
.unwrap_or(false);
(Cli::from_arg_matches(&matches)?, json)
};
match run(cli).await {
Err(error) if json => {
if let Some(remote) = error.downcast_ref::<RemoteErrorCli>() {
print_json(&remote.output)?;
std::io::stdout().flush()?;
std::process::exit(1);
}
Err(error)
}
result => result,
}
}

async fn run(cli: Cli) -> Result<()> {
if let Some(result) = managed::dispatch(&cli).await {
let code = result.emit()?;
if code != 0 {
Expand Down Expand Up @@ -1722,14 +1746,36 @@ async fn main() -> Result<()> {
}
},
Command::Graphs { command } => match command {
GraphsCommand::List { json } => {
// Registry scope (RFC-011): the bare server base URL, resolved
// synchronously — the async D7 require-graph probe cannot run
// here, and no `/graphs/<id>` is ever appended.
let client = client::GraphClient::resolve_registry(
cli.server.as_deref(),
cli.profile.as_deref(),
)?;
GraphsCommand::List { json, discovery } => {
let (client, discovery) = if let Some(client) = managed_data {
(client, true)
} else {
// Explicit operator addressing retains the legacy catalog
// unless discovery is explicitly requested. Token bytes do
// not choose configuration or change static-token behavior.
(
client::GraphClient::resolve_registry(
cli.server.as_deref(),
cli.profile.as_deref(),
)?,
discovery,
)
};
if discovery {
let payload = client.discover_graphs().await?;
if json {
print_json(&payload)?;
} else {
for entry in payload.graphs {
if entry.display_name == entry.graph_id {
println!("{}", entry.graph_id);
} else {
println!("{}\t{}", entry.graph_id, entry.display_name);
}
}
}
return Ok(());
}
let payload = client.list_graphs().await?;
if json {
print_json(&payload)?;
Expand Down
Loading
Loading