use anyhow::{Result, anyhow};
use colored::Colorize;
use serde_json::{Value, json};
use rigg_core::identity::{Scope, graph_for_docs};
use rigg_core::resources::ResourceKind;
use rigg_core::store::Store;
use rigg_core::workspace::{Project, ResolvedEnv, Workspace};
use crate::cli::VerifyArgs;
use crate::commands::auth_engine;
use crate::commands::az::indexer::run_and_watch;
use crate::commands::remote::Remote;
use crate::commands::{
GlobalContext, confirm_protected_env, load_workspace, resolve_env, select_projects,
};
use crate::say;
const PING: &str = "ping";
const AGENT_PROMPT: &str = "Reply with OK";
pub async fn run(ctx: &GlobalContext, args: VerifyArgs) -> Result<()> {
let ws = load_workspace()?;
let env = resolve_env(&ws, ctx)?;
let projects = select_projects(&ws, args.project.as_deref(), args.all)?;
if !confirm_protected_env(
ctx,
&env,
args.confirm_env.as_deref(),
"verify (runs every indexer)",
"verify",
json!({"env": env.name}),
)? {
say!(ctx, "Aborted.");
return Ok(());
}
run_for(ctx, &ws, &env, &projects).await
}
pub async fn run_for(
ctx: &GlobalContext,
ws: &Workspace,
env: &ResolvedEnv,
projects: &[&Project],
) -> Result<()> {
let mut failures: Vec<String> = Vec::new();
let mut checked = 0usize;
for project in projects {
let store = Store::new(project, &env.name);
let remote = Remote::for_project(env, project);
let mut docs: Vec<(ResourceKind, String, Value)> = Vec::new();
let (mut indexers, mut kbs, mut agents) = (Vec::new(), Vec::new(), Vec::new());
for (r, _) in store.list()? {
let body = store.read(&r)?;
match r.kind {
ResourceKind::Indexer => indexers.push(r.name.clone()),
ResourceKind::KnowledgeBase => kbs.push(r.name.clone()),
ResourceKind::Agent => agents.push(r.name.clone()),
_ => {}
}
docs.push((r.kind, r.name.clone(), body));
}
if indexers.is_empty() && kbs.is_empty() && agents.is_empty() {
continue;
}
say!(
ctx,
"{} project '{}' (env: {})",
"Verify".bold(),
project.name.bold(),
env.name
);
for line in remote.target_lines() {
say!(ctx, "{line}");
}
let attribution = Attribution::build(ws, env, &docs).await;
for name in &indexers {
checked += 1;
let outcome = match run_and_watch(&remote, name, ctx).await {
Ok(o) => o,
Err(e) => {
report_failure(
ctx,
&attribution,
&mut failures,
"indexer",
name,
&e_str(&e),
);
continue;
}
};
if outcome.succeeded() {
say!(
ctx,
" {} indexer '{name}' — {} processed, {} failed",
"✓".green(),
outcome.items,
outcome.items_failed
);
} else {
let detail = outcome
.error
.unwrap_or_else(|| format!("run ended in {}", outcome.status));
report_failure(ctx, &attribution, &mut failures, "indexer", name, &detail);
}
}
for name in &kbs {
checked += 1;
let body = json!({
"intents": [{ "type": "semantic", "search": PING }],
"includeActivity": true
});
match remote.kb_retrieve(name, &body).await {
Ok(result) => match kb_source_error(&result) {
Some(detail) => {
report_failure(
ctx,
&attribution,
&mut failures,
"knowledge base",
name,
&detail,
);
}
None => say!(ctx, " {} knowledge base '{name}' retrieved", "✓".green()),
},
Err(e) => report_failure(
ctx,
&attribution,
&mut failures,
"knowledge base",
name,
&e_str(&e),
),
}
}
for name in &agents {
checked += 1;
match remote.agent_ask(name, AGENT_PROMPT).await {
Ok(_) => say!(ctx, " {} agent '{name}' replied", "✓".green()),
Err(e) => {
report_failure(ctx, &attribution, &mut failures, "agent", name, &e_str(&e))
}
}
}
}
if checked == 0 {
say!(
ctx,
"nothing to verify (no indexers, knowledge bases or agents in this environment)"
);
return Ok(());
}
if failures.is_empty() {
say!(ctx, "{} {checked} check(s) passed", "✓".green().bold());
return Ok(());
}
Err(anyhow!(
"{} of {checked} verification(s) failed: {}",
failures.len(),
failures.join("; ")
))
}
fn e_str(e: &anyhow::Error) -> String {
format!("{e:#}")
}
fn report_failure(
ctx: &GlobalContext,
attribution: &Attribution,
failures: &mut Vec<String>,
kind: &str,
name: &str,
detail: &str,
) {
let likely = attribution.likely(detail);
say!(ctx, " {} {kind} '{name}' — {detail}", "✗".red());
if let Some(edge) = &likely {
say!(ctx, " → likely {edge}");
}
failures.push(match likely {
Some(edge) => format!("{kind} '{name}' ({edge})"),
None => format!("{kind} '{name}'"),
});
}
fn kb_source_error(result: &Value) -> Option<String> {
let records = result.get("activity")?.as_array()?;
let mut problems = Vec::new();
for record in records {
let Some(message) = record.pointer("/error/message").and_then(Value::as_str) else {
continue;
};
let source = record
.get("knowledgeSourceName")
.and_then(Value::as_str)
.unwrap_or("?");
problems.push(format!("knowledge source '{source}': {message}"));
}
(!problems.is_empty()).then(|| problems.join("; "))
}
struct Attribution {
edges: Vec<(String, String)>,
}
impl Attribution {
async fn build(
ws: &Workspace,
env: &ResolvedEnv,
docs: &[(ResourceKind, String, Value)],
) -> Self {
let bindings = auth_engine::bindings_for(ws, env, None).await;
let graph = graph_for_docs(&bindings, docs);
let edges = graph
.edges
.iter()
.filter_map(|e| Some((e.id.clone(), scope_resource(&e.scope)?)))
.collect();
Attribution { edges }
}
fn likely(&self, message: &str) -> Option<String> {
if !auth_engine::looks_like_auth(message) {
return None;
}
let haystack = message.to_lowercase();
self.edges
.iter()
.find(|(_, resource)| haystack.contains(&resource.to_lowercase()))
.map(|(id, _)| id.clone())
}
}
fn scope_resource(scope: &Scope) -> Option<String> {
let name = match scope {
Scope::Resolved(id) => id.rsplit('/').next().unwrap_or(id).to_string(),
Scope::Unresolved { physical, .. } => physical.clone(),
};
(!name.is_empty()).then_some(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attribution_only_fires_for_auth_shaped_messages() {
let a = Attribution {
edges: vec![("search-system|abc|acct".to_string(), "acct".to_string())],
};
assert_eq!(
a.likely("This request is not authorized. Storage account 'acct'"),
Some("search-system|abc|acct".to_string())
);
assert_eq!(a.likely("could not parse document in 'acct'"), None);
assert_eq!(a.likely("403 Forbidden"), None);
}
#[test]
fn kb_source_errors_are_surfaced_from_the_activity_records() {
let result = json!({"activity": [
{"knowledgeSourceName": "docs-ks", "error": {"message": "Forbidden (403)"}},
{"knowledgeSourceName": "other"}
]});
assert_eq!(
kb_source_error(&result),
Some("knowledge source 'docs-ks': Forbidden (403)".to_string())
);
assert_eq!(kb_source_error(&json!({"activity": []})), None);
assert_eq!(kb_source_error(&json!({})), None);
}
#[test]
fn scope_resource_reads_the_last_arm_segment() {
assert_eq!(
scope_resource(&Scope::Resolved("/subscriptions/s/rg/x/acct".into())),
Some("acct".to_string())
);
assert_eq!(
scope_resource(&Scope::Unresolved {
binding: "docs".into(),
kind: None,
physical: "acct".into(),
}),
Some("acct".to_string())
);
}
}