Skip to main content

lodestone_client/
tooling.rs

1use std::error::Error;
2use std::fmt::{self, Display, Formatter};
3
4use lodestone_wire::{DiscoverResult, LODESTONE_API_SCHEMA_ID, LODESTONE_API_SPEC_SHA256};
5use serde_json::Value;
6
7use crate::generated_tool_packs::{TOOL_INDEX_JSON, tool_pack_json};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum ToolPackError {
11    DiscoverySchemaMismatch,
12    UnknownFamily,
13    GeneratedArtifactInvalid,
14}
15
16impl Display for ToolPackError {
17    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
18        formatter.write_str(match self {
19            Self::DiscoverySchemaMismatch => "native discovery schema does not match the packed client",
20            Self::UnknownFamily => "native tool capability family is unknown",
21            Self::GeneratedArtifactInvalid => "generated native tool artifact is invalid",
22        })
23    }
24}
25
26impl Error for ToolPackError {}
27
28/// Loads one generated capability-family pack after exact schema identity,
29/// observed capability, and host result-ceiling intersection.
30pub fn load_tool_pack(
31    discovery: &DiscoverResult,
32    family: &str,
33    host_maximum_result_bytes: u64,
34) -> Result<Value, ToolPackError> {
35    if discovery.schema_id != LODESTONE_API_SCHEMA_ID || discovery.schema_digest.value != LODESTONE_API_SPEC_SHA256 {
36        return Err(ToolPackError::DiscoverySchemaMismatch);
37    }
38    let index: Value = serde_json::from_str(TOOL_INDEX_JSON).map_err(|_| ToolPackError::GeneratedArtifactInvalid)?;
39    if index.get("native_schema_sha256").and_then(Value::as_str) != Some(LODESTONE_API_SPEC_SHA256)
40        || !index.get("families").and_then(Value::as_array).is_some_and(|families| {
41            families
42                .iter()
43                .any(|entry| entry.get("id").and_then(Value::as_str) == Some(family))
44        })
45    {
46        return Err(ToolPackError::UnknownFamily);
47    }
48    let source = tool_pack_json(family).ok_or(ToolPackError::UnknownFamily)?;
49    let mut pack: Value = serde_json::from_str(source).map_err(|_| ToolPackError::GeneratedArtifactInvalid)?;
50    if pack.get("native_schema_sha256").and_then(Value::as_str) != Some(LODESTONE_API_SPEC_SHA256) {
51        return Err(ToolPackError::GeneratedArtifactInvalid);
52    }
53    let available = discovery
54        .capabilities
55        .iter()
56        .filter(|capability| capability.state == "available" && capability.capability_family == family)
57        .map(|capability| capability.command.as_str())
58        .collect::<std::collections::BTreeSet<_>>();
59    let operations = pack
60        .get_mut("operations")
61        .and_then(Value::as_array_mut)
62        .ok_or(ToolPackError::GeneratedArtifactInvalid)?;
63    operations.retain(|operation| {
64        let command = operation.get("command").and_then(Value::as_str);
65        let maximum = operation.get("maximum_result_bytes").and_then(Value::as_u64);
66        command.is_some_and(|command| available.contains(command))
67            && maximum.is_some_and(|maximum| maximum <= host_maximum_result_bytes)
68    });
69    Ok(pack)
70}
71
72/// Resolves one command's generated provider-neutral operation descriptor
73/// without requiring a connection or inventing a second command registry.
74pub fn describe_tool_operation(command: &str) -> Result<Option<Value>, ToolPackError> {
75    let index: Value = serde_json::from_str(TOOL_INDEX_JSON).map_err(|_| ToolPackError::GeneratedArtifactInvalid)?;
76    if index.get("native_schema_sha256").and_then(Value::as_str) != Some(LODESTONE_API_SPEC_SHA256) {
77        return Err(ToolPackError::GeneratedArtifactInvalid);
78    }
79    let Some(family) = index.get("families").and_then(Value::as_array).and_then(|families| {
80        families.iter().find(|family| {
81            family
82                .get("commands")
83                .and_then(Value::as_array)
84                .is_some_and(|commands| commands.iter().any(|value| value.as_str() == Some(command)))
85        })
86    }) else {
87        return Ok(None);
88    };
89    let family = family
90        .get("id")
91        .and_then(Value::as_str)
92        .ok_or(ToolPackError::GeneratedArtifactInvalid)?;
93    let source = tool_pack_json(family).ok_or(ToolPackError::GeneratedArtifactInvalid)?;
94    let pack: Value = serde_json::from_str(source).map_err(|_| ToolPackError::GeneratedArtifactInvalid)?;
95    if pack.get("native_schema_sha256").and_then(Value::as_str) != Some(LODESTONE_API_SPEC_SHA256) {
96        return Err(ToolPackError::GeneratedArtifactInvalid);
97    }
98    let mut operation = pack
99        .get("operations")
100        .and_then(Value::as_array)
101        .and_then(|operations| {
102            operations
103                .iter()
104                .find(|operation| operation.get("command").and_then(Value::as_str) == Some(command))
105        })
106        .cloned()
107        .ok_or(ToolPackError::GeneratedArtifactInvalid)?;
108    let definitions = pack
109        .get("$defs")
110        .cloned()
111        .ok_or(ToolPackError::GeneratedArtifactInvalid)?;
112    operation
113        .as_object_mut()
114        .ok_or(ToolPackError::GeneratedArtifactInvalid)?
115        .insert("$defs".to_owned(), definitions);
116    Ok(Some(operation))
117}