//! Assist extends the existing review tool; startup authority cannot come from arguments.
use super::{
Server, ToolOutput, ToolResult, arg_bool, arg_f64, arg_str, arg_strings, reject_unknown,
require_str,
};
use crate::{
agent::CliError,
assist_cmd::{self, Common, Routing, VisibleKind},
};
use saccade_core::assist::schema::Task;
use serde_json::{Map, Value, json};
impl Server {
pub(super) fn assist_review(&self, args: &Map<String, Value>) -> ToolResult {
reject_unknown(
args,
&[
"operation",
"artifact",
"out",
"entry",
"experimental",
"run",
"offline",
"replay",
"route",
"jev_routing",
"budget_calls",
"max_spend_usd",
"deadline_secs",
"gemini_revision",
"jev_revision",
"bypass_cache",
"source_evidence",
"incomplete_capture",
"pre_masked",
"condition",
"box",
"kind",
"mask_manifest",
"target",
"second_target",
"response",
],
)?;
let operation = require_str(args, "operation")?;
if let Some(action) = operation.strip_prefix("batch-") {
return self.assist_batch(action, args);
}
let file = self.existing_file("artifact", &require_str(args, "artifact")?)?;
if let Some(reference) = args.get("artifact").and_then(Value::as_object) {
let actual = saccade_core::evidence::canonical::Digest::of_bytes(
&saccade_core::assist::read_bytes(&file, 32 * 1024 * 1024)?,
)
.as_str()
.to_owned();
if reference["sha256"].as_str() != Some(&actual) {
return Err(CliError::new(
"stale_action",
"assist artifact digest changed",
));
}
}
let run = arg_bool(args, "run")?.unwrap_or(false);
let user = crate::review_cmd::load_user(&crate::review_cmd::user_file(
self.providers.user_config.as_deref(),
))?;
let startup_budget = self.providers.budget_calls.unwrap_or(0);
let auth = crate::review_cmd::authorization(
self.providers.allow_provider_calls,
startup_budget,
&self.run_id,
&user,
);
if run {
auth.check().map_err(|_| {
CliError::new(
"network_authorization_required",
"assist requires human MCP startup authorization",
)
})?;
}
let number = |name: &str, default: u64| -> Result<u64, CliError> {
match args.get(name) {
None => Ok(default),
Some(v) => v
.as_u64()
.ok_or_else(|| CliError::usage(format!("{name} must be an integer"))),
}
};
let budget = number("budget_calls", 6.min(startup_budget.max(1)))?;
if run && budget > startup_budget {
return Err(CliError::usage(
"tool request budget exceeds startup authorization",
));
}
let route = match arg_str(args, "route")?.as_deref().unwrap_or("cascade") {
"rules" => Routing::Rules,
"cascade" => Routing::Cascade,
"all-vision" => Routing::AllVision,
_ => return Err(CliError::usage("invalid assist route")),
};
let source_evidence = arg_strings(args, "source_evidence")?
.into_iter()
.map(|p| self.existing_file("source_evidence", &p))
.collect::<Result<Vec<_>, _>>()?;
let replay = arg_str(args, "replay")?
.map(|p| self.existing_file("replay", &p))
.transpose()?;
let offline = arg_bool(args, "offline")?.unwrap_or(false);
if run && offline || replay.is_some() && !offline {
return Err(CliError::usage(
"replay requires offline; offline conflicts with run",
));
}
let common = Common {
#[cfg(feature = "vision-providers")]
vision_provider: None,
#[cfg(feature = "vision-providers")]
vision_response: None,
user_policy_file: self.providers.user_config.clone(),
experimental: arg_bool(args, "experimental")?.unwrap_or(false),
out: self.resolve("out", &require_str(args, "out")?)?,
offline,
replay,
run,
route,
jev_routing: arg_bool(args, "jev_routing")?.unwrap_or(false),
budget_calls: budget,
max_spend_usd: arg_f64(args, "max_spend_usd")?.unwrap_or(0.15),
deadline_secs: number("deadline_secs", 300)?,
gemini_revision: arg_str(args, "gemini_revision")?,
jev_revision: arg_str(args, "jev_revision")?
.unwrap_or_else(|| saccade_core::assist::schema::JEV.into()),
bypass_cache: arg_bool(args, "bypass_cache")?.unwrap_or(false),
source_evidence,
incomplete_capture: arg_bool(args, "incomplete_capture")?.unwrap_or(false),
pre_masked: arg_bool(args, "pre_masked")?.unwrap_or(false),
};
let (value, _exit) = match operation.as_str() {
"explain" | "audit-mask" => {
if args.contains_key("condition")
|| args.contains_key("box")
|| args.contains_key("kind")
|| args.contains_key("target")
|| args.contains_key("second_target")
{
return Err(CliError::usage(
"report assist does not take a single-image condition",
));
}
let masks = arg_str(args, "mask_manifest")?
.map(|p| self.existing_file("mask_manifest", &p))
.transpose()?;
assist_cmd::report(
assist_cmd::ReportArgs {
report: file,
entry: arg_str(args, "entry")?,
mask_manifest: masks,
common,
},
if operation == "explain" {
Task::Explain
} else {
Task::AuditMask
},
false,
Some(&self.policy),
Some(&auth),
)?
}
"check-ui" => {
if args.contains_key("entry") || args.contains_key("mask_manifest") {
return Err(CliError::usage(
"single-image assist does not take report entry or masks",
));
}
let values = args
.get("box")
.and_then(Value::as_array)
.ok_or_else(|| CliError::usage("check-ui requires box [X,Y,W,H]"))?;
let box_: Vec<u32> = values
.iter()
.map(|v| {
v.as_u64()
.and_then(|v| u32::try_from(v).ok())
.ok_or_else(|| CliError::usage("invalid box coordinate"))
})
.collect::<Result<_, _>>()?;
let box_: [u32; 4] = box_
.try_into()
.map_err(|_| CliError::usage("box needs four coordinates"))?;
let kind = match arg_str(args, "kind")?.as_deref().unwrap_or("label-visible") {
"label-visible" => VisibleKind::LabelVisible,
"banner-absent" => VisibleKind::BannerAbsent,
"not-clipped" => VisibleKind::NotClipped,
"non-overlap" => VisibleKind::NonOverlap,
_ => return Err(CliError::usage("unsupported visible condition")),
};
assist_cmd::check(
assist_cmd::CheckArgs {
condition: require_str(args, "condition")?,
image: file,
r#box: box_,
kind,
target: arg_str(args, "target")?,
second_target: arg_str(args, "second_target")?,
grounding: Default::default(),
common,
},
false,
Some(&self.policy),
Some(&auth),
)?
}
_ => return Err(CliError::usage("unknown assist operation")),
};
Ok(ToolOutput {text:"Experimental AI advice. Read outcome, execution, limitations and unchanged deterministic verdict.".into(),structured:value,images:vec![]})
}
}
impl Server {
fn assist_batch(&self, operation: &str, args: &Map<String, Value>) -> ToolResult {
reject_unknown(
args,
&[
"operation",
"artifact",
"out",
"experimental",
"run",
"response",
"budget_calls",
"deadline_secs",
],
)?;
if !["submit", "status", "collect"].contains(&operation) {
return Err(CliError::usage("unknown Batch operation"));
}
let plan = self.existing_file("artifact", &require_str(args, "artifact")?)?;
if let Some(reference) = args.get("artifact").and_then(Value::as_object) {
let hash = saccade_core::evidence::canonical::Digest::of_bytes(
&saccade_core::assist::read_bytes(&plan, 32 * 1024 * 1024)?,
);
if reference["sha256"].as_str() != Some(hash.as_str()) {
return Err(CliError::new("stale_action", "Batch plan digest changed"));
}
}
let user = crate::review_cmd::load_user(&crate::review_cmd::user_file(
self.providers.user_config.as_deref(),
))?;
let auth = crate::review_cmd::authorization(
self.providers.allow_provider_calls,
self.providers.budget_calls.unwrap_or(0),
&self.run_id,
&user,
);
let integer = |name: &str, default: u64| -> Result<u64, CliError> {
args.get(name).map_or(Ok(default), |v| {
v.as_u64()
.ok_or_else(|| CliError::usage("Batch integer control"))
})
};
let (value, _exit) = crate::assist_batch_cmd::execute(
operation,
crate::assist_batch_cmd::BatchCommon {
allow_spend_above_25_usd: false,
plan,
job: self.resolve("out", &require_str(args, "out")?)?,
experimental: arg_bool(args, "experimental")?.unwrap_or(false),
run: arg_bool(args, "run")?.unwrap_or(false),
response: arg_str(args, "response")?
.map(|p| self.existing_file("response", &p))
.transpose()?,
budget_calls: integer(
"budget_calls",
self.providers.budget_calls.unwrap_or(8).min(128),
)?,
deadline_secs: integer("deadline_secs", 300)?,
},
self.providers.user_config.as_deref(),
Some(&self.policy),
Some(&auth),
)?;
Ok(ToolOutput {
text: "Asynchronous experimental Batch receipt; no approval authority.".into(),
structured: value,
images: vec![],
})
}
}
pub(super) fn schemas() -> Vec<Value> {
let mut variants = Vec::new();
for op in ["explain", "audit-mask", "check-ui"] {
let mut props = json!({"operation":{"const":op},"artifact":{"oneOf":[{"type":"string"},{"type":"object","properties":{"path":{"type":"string"},"sha256":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"}},"required":["path","sha256"],"additionalProperties":false}]},"out":{"type":"string"},"experimental":{"type":"boolean","const":true},"run":{"type":"boolean","default":false},"offline":{"type":"boolean","default":false},"replay":{"type":"string"},"route":{"enum":["rules","cascade","all-vision"]},"jev_routing":{"type":"boolean","default":false},"budget_calls":{"type":"integer","minimum":1,"maximum":8},"max_spend_usd":{"type":"number","exclusiveMinimum":0,"maximum":0.15},"deadline_secs":{"type":"integer","minimum":1,"maximum":300},"gemini_revision":{"type":"string"},"jev_revision":{"type":"string"},"bypass_cache":{"type":"boolean"},"source_evidence":{"type":"array","items":{"type":"string"},"maxItems":2},"incomplete_capture":{"type":"boolean"},"pre_masked":{"type":"boolean"}});
let mut required = vec!["operation", "artifact", "out", "experimental"];
if op == "check-ui" {
props["condition"] = json!({"type":"string","maxLength":512});
props["box"] = json!({"type":"array","items":{"type":"integer","minimum":0},"minItems":4,"maxItems":4});
props["kind"] =
json!({"enum":["label-visible","banner-absent","not-clipped","non-overlap"]});
props["target"] = json!({"type":"string"});
props["second_target"] = json!({"type":"string"});
required.extend(["condition", "box"]);
} else {
props["entry"] = json!({"type":"string"});
props["mask_manifest"] = json!({"type":"string"});
}
variants.push(json!({"type":"object","properties":props,"required":required,"additionalProperties":false}));
}
for op in ["batch-submit", "batch-status", "batch-collect"] {
variants.push(json!({"type":"object","properties":{"operation":{"const":op},"artifact":{"oneOf":[{"type":"string"},{"type":"object","properties":{"path":{"type":"string"},"sha256":{"type":"string","pattern":"^sha256:[0-9a-f]{64}$"}},"required":["path","sha256"],"additionalProperties":false}]},"out":{"type":"string"},"experimental":{"type":"boolean","const":true},"run":{"type":"boolean","default":false},"response":{"type":"string"},"budget_calls":{"type":"integer","minimum":1,"maximum":128},"deadline_secs":{"type":"integer","minimum":1,"maximum":300}},"required":["operation","artifact","out","experimental"],"additionalProperties":false}));
}
variants
}