use crate::agent::CliError;
use crate::wave7_cmd::error;
use saccade_core::{
root_policy::RootPolicy,
wave7::{
faces::{self, CropSpec, FaceReport},
models::{self, VisionError},
quality::{LearnedMetric, QualityReport},
vision::{LocateReport, VisionImage},
watermark::{self, DwtConfig, WatermarkReport},
},
};
use serde::Deserialize;
use serde_json::{Value, json};
use std::path::PathBuf;
#[derive(Deserialize)]
#[serde(tag = "operation", deny_unknown_fields)]
enum Operation {
#[serde(rename = "models_list")]
Models {
#[serde(default)]
registry: Option<PathBuf>,
#[serde(default)]
cache: Option<PathBuf>,
},
#[serde(rename = "models_pull")]
Pull { id: String },
#[serde(rename = "vision_locate")]
Locate {
image: PathBuf,
phrase: String,
observations: PathBuf,
#[serde(default)]
segment: bool,
overlay: PathBuf,
},
#[serde(rename = "vision_quality")]
Quality {
image: PathBuf,
reference: Option<PathBuf>,
metric: LearnedMetric,
observations: PathBuf,
},
#[serde(rename = "vision_faces")]
Faces {
image: PathBuf,
observations: PathBuf,
blur_faces: Option<PathBuf>,
},
#[serde(rename = "vision_crop")]
Crop {
image: PathBuf,
observations: PathBuf,
crops: Vec<CropSpec>,
focal_point: Option<[f32; 2]>,
blur_faces: Option<PathBuf>,
},
#[serde(rename = "vision_watermark")]
Watermark {
image: PathBuf,
observations: Option<PathBuf>,
expected_payload: Option<Vec<u8>>,
#[serde(default)]
trustmark: bool,
},
#[cfg(feature = "local-vlm")]
#[serde(rename = "vision_local")]
Local {
request: PathBuf,
response: PathBuf,
endpoint: String,
runtime_revision: String,
},
#[cfg(feature = "vision-providers")]
#[serde(rename = "vision_provider")]
Provider {
request: PathBuf,
response: PathBuf,
provider: saccade_core::wave7::providers::Provider,
coordinates: saccade_core::wave7::providers::Coordinates,
},
}
pub(crate) fn handles(name: &str, operation: &str) -> bool {
match name {
"saccade_inspect" => operation == "models_list",
"saccade_measure" => {
matches!(
operation,
"models_pull"
| "vision_locate"
| "vision_quality"
| "vision_faces"
| "vision_crop"
| "vision_watermark"
) || (cfg!(feature = "local-vlm") && operation == "vision_local")
|| (cfg!(feature = "vision-providers") && operation == "vision_provider")
}
_ => false,
}
}
fn input(policy: &RootPolicy, p: PathBuf) -> Result<PathBuf, CliError> {
let p = policy.read(&p)?;
if !p.is_file() {
return Err(CliError::usage("vision input must be a file"));
}
Ok(p)
}
#[cfg(any(feature = "local-vlm", feature = "vision-providers"))]
fn read<T: serde::de::DeserializeOwned>(
policy: &RootPolicy,
p: PathBuf,
limit: u64,
) -> Result<T, CliError> {
Ok(serde_json::from_slice(
&models::read_bounded(&input(policy, p)?, limit).map_err(error)?,
)?)
}
fn image(policy: &RootPolicy, p: PathBuf) -> Result<VisionImage, CliError> {
VisionImage::load(&input(policy, p)?).map_err(error)
}
fn face(policy: &RootPolicy, p: PathBuf, image: &VisionImage) -> Result<FaceReport, CliError> {
let mut r: FaceReport = crate::parse_contract(
&models::read_bounded(&input(policy, p)?, 1024 * 1024).map_err(error)?,
faces::FACES_SCHEMA,
)?;
r.validate(image).map_err(error)?;
r.provenance.runtime = "replay".into();
r.provenance.source_parity = false;
Ok(r)
}
fn redaction(
policy: &RootPolicy,
p: Option<PathBuf>,
image: &VisionImage,
r: &FaceReport,
) -> Result<(), CliError> {
if let Some(p) = p {
crate::wave7_cmd::write_png(
&policy.write(&p)?,
&faces::blur_faces(image, r).map_err(error)?,
)?;
}
Ok(())
}
pub(crate) fn call(
policy: &RootPolicy,
args: &serde_json::Map<String, Value>,
) -> Result<Value, CliError> {
let operation: Operation = serde_json::from_value(Value::Object(args.clone()))?;
match operation {
Operation::Models { registry, cache } => {
let cfg = crate::wave7_cmd::config()?;
let refuse = |e: saccade_core::Error| {
CliError::new("model_location_not_request_controlled", e.to_string())
};
if let Some(r) = registry {
cfg.check_request_location("registry", &policy.read(&r)?)
.map_err(refuse)?;
}
if let Some(c) = cache {
cfg.check_request_location("cache", &policy.read(&c)?)
.map_err(refuse)?;
}
let r = crate::wave7_cmd::registry(None)?;
Ok(crate::wave7_cmd::status(&r, &cfg.dir))
}
Operation::Pull { id } => Err(error(VisionError::Unavailable(format!(
"{id}: MCP cannot authorize model downloads; use explicit CLI models pull"
)))),
Operation::Locate {
image: p,
phrase,
observations,
segment,
overlay,
} => {
let i = image(policy, p)?;
let r = LocateReport::replay(&input(policy, observations)?, &i, &phrase, segment)
.map_err(error)?;
crate::wave7_cmd::write_png(
&policy.write(&overlay)?,
&saccade_core::wave7::vision::overlay(&i, &r.detections).map_err(error)?,
)?;
Ok(serde_json::to_value(r)?)
}
Operation::Quality {
image: p,
reference,
metric,
observations,
} => {
let i = image(policy, p)?;
let reference = reference.map(|p| image(policy, p)).transpose()?;
let mut r: QualityReport = crate::parse_contract(
&models::read_bounded(&input(policy, observations)?, 1024 * 1024).map_err(error)?,
saccade_core::wave7::quality::QUALITY_SCHEMA,
)?;
r.validate(metric, &i, reference.as_ref()).map_err(error)?;
for m in &mut r.named_metrics {
m.provenance.runtime = "replay".into();
m.provenance.source_parity = false;
}
Ok(serde_json::to_value(r)?)
}
Operation::Faces {
image: p,
observations,
blur_faces,
} => {
let i = image(policy, p)?;
let r = face(policy, observations, &i)?;
redaction(policy, blur_faces, &i, &r)?;
Ok(serde_json::to_value(r)?)
}
Operation::Crop {
image: p,
observations,
crops,
focal_point,
blur_faces,
} => {
let i = image(policy, p)?;
let r = face(policy, observations, &i)?;
let crop = faces::crop_check(&i, &r, &crops, focal_point).map_err(error)?;
redaction(policy, blur_faces, &i, &r)?;
Ok(serde_json::to_value(crop)?)
}
Operation::Watermark {
image: p,
observations,
expected_payload,
trustmark,
} => {
let i = image(policy, p)?;
let r = if let Some(p) = observations {
let mut r: WatermarkReport = crate::parse_contract(
&models::read_bounded(&input(policy, p)?, 1024 * 1024).map_err(error)?,
watermark::WATERMARK_SCHEMA,
)?;
r.validate(&i).map_err(error)?;
for f in &mut r.findings {
if let Some(p) = &mut f.provenance {
p.runtime = "replay".into();
p.source_parity = false;
}
f.interpretation = format!("Explicit observation replay. {}", f.interpretation);
}
r
} else {
let c = expected_payload.map(|expected_payload| DwtConfig {
expected_payload,
quantization_step: 36.,
minimum_agreement: 0.9,
});
if trustmark {
crate::wave7_cmd::trustmark_report(&i, c.as_ref(), None, None, None)?
} else {
watermark::inspect(&i, None, c.as_ref()).map_err(error)?
}
};
Ok(serde_json::to_value(r)?)
}
#[cfg(feature = "local-vlm")]
Operation::Local {
request,
response,
endpoint,
runtime_revision,
} => {
let r = read(policy, request, 24 * 1024 * 1024)?;
let p = saccade_core::wave7::local_vlm::LocalVlm {
endpoint,
runtime_revision,
};
let mut report = p
.decode(
&r,
&models::read_bounded(&input(policy, response)?, 1024 * 1024).map_err(error)?,
)
.map_err(error)?;
report.provenance.runtime = "replay".into();
Ok(serde_json::to_value(report)?)
}
#[cfg(feature = "vision-providers")]
Operation::Provider {
request,
response,
provider,
coordinates,
} => {
let r = read(policy, request, 24 * 1024 * 1024)?;
let a = saccade_core::wave7::providers::ProviderAdapter {
provider,
coordinates,
};
Ok(serde_json::to_value(
a.decode(
&r,
&models::read_bounded(&input(policy, response)?, 1024 * 1024).map_err(error)?,
)
.map_err(error)?,
)?)
}
}
}
fn variant(op: &str, properties: Value, required: &[&str]) -> Value {
let mut p = properties.as_object().cloned().unwrap_or_default();
p.insert("operation".into(), json!({"const":op}));
let mut r = vec!["operation"];
r.extend_from_slice(required);
json!({"type":"object","properties":p,"required":r,"additionalProperties":false})
}
pub(crate) fn inspect_schema() -> Value {
variant(
"models_list",
json!({"registry":{"type":"string","description":"Optional; must equal the operator-configured registry"},"cache":{"type":"string","description":"Optional; must equal the operator-configured model cache"}}),
&[],
)
}
pub(crate) fn measure_schemas() -> Vec<Value> {
let mut v = vec![
variant("models_pull", json!({"id":{"type":"string"}}), &["id"]),
variant(
"vision_locate",
json!({"image":{"type":"string"},"phrase":{"type":"string","minLength":1,"maxLength":4096},"observations":{"type":"string"},"segment":{"type":"boolean"},"overlay":{"type":"string"}}),
&["image", "phrase", "observations", "overlay"],
),
variant(
"vision_quality",
json!({"image":{"type":"string"},"reference":{"type":["string","null"]},"metric":{"enum":["lpips_alex_v01","dists","musiq_technical"]},"observations":{"type":"string"}}),
&["image", "metric", "observations"],
),
variant(
"vision_faces",
json!({"image":{"type":"string"},"observations":{"type":"string"},"blur_faces":{"type":["string","null"]}}),
&["image", "observations"],
),
variant(
"vision_crop",
json!({"image":{"type":"string"},"observations":{"type":"string"},"crops":{"type":"array","minItems":1,"maxItems":32,"items":{"type":"object"}},"focal_point":{"type":["array","null"],"items":{"type":"number"},"minItems":2,"maxItems":2},"blur_faces":{"type":["string","null"]}}),
&["image", "observations", "crops"],
),
variant(
"vision_watermark",
json!({"image":{"type":"string"},"observations":{"type":["string","null"]},"trustmark":{"type":"boolean","default":false},"expected_payload":{"type":["array","null"],"items":{"type":"integer","minimum":0,"maximum":255},"minItems":1,"maxItems":64}}),
&["image"],
),
];
if cfg!(feature = "local-vlm") {
v.push(variant("vision_local",json!({"request":{"type":"string"},"response":{"type":"string"},"endpoint":{"type":"string"},"runtime_revision":{"type":"string"}}),&["request","response","endpoint","runtime_revision"]));
}
if cfg!(feature = "vision-providers") {
v.push(variant("vision_provider",json!({"request":{"type":"string"},"response":{"type":"string"},"provider":{"enum":["claude","gpt"]},"coordinates":{"enum":["pixels","unit","thousand"]}}),&["request","response","provider","coordinates"]));
}
v
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn existing_tool_interface_handles_generated_face_and_crop_receipts() {
let d = tempfile::tempdir().unwrap();
let inputs = d.path().join("inputs");
let output = d.path().join("output");
std::fs::create_dir(&inputs).unwrap();
std::fs::create_dir(&output).unwrap();
let p = inputs.join("image.png");
image::RgbImage::new(32, 24).save(&p).unwrap();
let i = VisionImage::load(&p).unwrap();
let r = FaceReport {
schema: faces::FACES_SCHEMA.into(),
image_sha256: i.sha256.clone(),
image_size: i.size(),
faces: vec![faces::Face {
bbox: saccade_core::wave7::vision::Rect {
x: 2.,
y: 4.,
width: 8.,
height: 8.,
},
score: 0.9,
landmarks: vec![],
}],
provenance: saccade_core::wave7::vision::Provenance::fixture("yunet-2026may"),
limitations: faces::FACE_LIMIT.into(),
};
std::fs::write(inputs.join("faces.json"), serde_json::to_vec(&r).unwrap()).unwrap();
let server = crate::mcp::Server::new(&[inputs], Some(&output), false, &[]).unwrap();
let response=server.handle_message(&json!({"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"saccade_measure","arguments":{"operation":"vision_crop","image":"image.png","observations":"faces.json","crops":[{"kind":"ratio","width":1.,"height":1.}]}}})).unwrap();
assert_eq!(response["result"]["isError"], false);
assert_eq!(
response["result"]["structuredContent"]["schema"],
saccade_core::report_links::linked_schema(faces::CROP_SCHEMA)
);
assert_eq!(
response["result"]["structuredContent"]["detection"]["provenance"]["runtime"],
"replay"
);
let crop = &response["result"]["structuredContent"];
assert!(crop["report_id"].as_str().unwrap().starts_with("sha256:"));
assert_eq!(crop["source_refs"], json!([]));
let parsed: faces::CropReport =
crate::parse_contract(&serde_json::to_vec(crop).unwrap(), faces::CROP_SCHEMA).unwrap();
parsed.detection.validate(&i).unwrap();
let linked =
saccade_core::report_links::decorate(&serde_json::to_value(&r).unwrap()).unwrap();
let observations = d.path().join("inputs/faces.json");
std::fs::write(&observations, serde_json::to_vec(&linked).unwrap()).unwrap();
let request = json!({"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"saccade_measure","arguments":{"operation":"vision_crop","image":"image.png","observations":"faces.json","crops":[{"kind":"ratio","width":1.,"height":1.}]}}});
let linked_response = server.handle_message(&request).unwrap();
assert_eq!(
linked_response["result"]["isError"], false,
"{linked_response}"
);
assert_eq!(linked_response["result"]["structuredContent"], *crop);
let mut nested = crop.clone();
nested["detection"] = linked.clone();
let parsed: faces::CropReport =
crate::parse_contract(&serde_json::to_vec(&nested).unwrap(), faces::CROP_SCHEMA)
.unwrap();
parsed.detection.validate(&i).unwrap();
for schema in ["saccade-faces.v3", "saccade-faces.v99"] {
let mut newer = linked.clone();
newer["schema"] = json!(schema);
std::fs::write(&observations, serde_json::to_vec(&newer).unwrap()).unwrap();
let reply = server.handle_message(&request).unwrap();
assert_eq!(reply["result"]["isError"], true, "{reply}");
assert_eq!(
reply["result"]["structuredContent"]["errors"][0]["code"],
"version_skew"
);
}
let mut unknown = linked;
unknown["future_field"] = json!(true);
assert!(
crate::parse_contract::<FaceReport>(
&serde_json::to_vec(&unknown).unwrap(),
faces::FACES_SCHEMA,
)
.is_err()
);
let tools = server
.handle_message(&json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}))
.unwrap();
assert!(
tools["result"]["tools"]
.as_array()
.unwrap()
.iter()
.any(|t| t["name"] == "saccade_measure"
&& t["inputSchema"]["oneOf"]
.as_array()
.unwrap()
.iter()
.any(|v| v["properties"]["operation"]["const"] == "vision_crop"))
);
}
#[test]
fn operations_keep_containment_and_refuse_download_authority() {
let d = tempfile::tempdir().unwrap();
let input = d.path().join("inputs");
let output = d.path().join("outputs");
std::fs::create_dir(&input).unwrap();
std::fs::create_dir(&output).unwrap();
let policy = RootPolicy::new(&[input], Some(&output), false, &[]).unwrap();
let a = json!({"operation":"vision_watermark","image":"../secret.png"});
assert!(call(&policy, a.as_object().unwrap()).is_err());
let a = json!({"operation":"models_pull","id":"yunet-2026may"});
assert!(
call(&policy, a.as_object().unwrap())
.unwrap_err()
.message
.contains("cannot authorize")
);
let a = json!({"operation":"vision_watermark","image":"x.png","allow_download":true});
assert!(call(&policy, a.as_object().unwrap()).is_err());
}
}