use super::tools::ToolResult;
use crate::network::{
Rail,
call::{Outcome, request},
};
use serde_json::{Map, Value, json};
use std::io::Read;
const HEX: &[u8; 16] = b"0123456789abcdef";
#[must_use]
pub fn tool_entries() -> Vec<Value> {
vec![
json!({"name":"plan_set","description":"Replace the building service's plan. Only its bound builder may write. Finished steps keep their ordinals and titles.",
"inputSchema":{"type":"object","required":["service","steps"],"additionalProperties":false,
"properties":{"service":{"type":"string"},"operation":{"type":"string"},"steps":{"type":"array","minItems":1,
"items":{"type":"object","required":["title"],"additionalProperties":false,"properties":{"title":{"type":"string"}}}}}}}),
json!({"name":"plan_step","description":"Change one ordinal step. At most one step may be doing. The server returns its exact refusal words.",
"inputSchema":{"type":"object","required":["service","step","status"],"additionalProperties":false,
"properties":{"service":{"type":"string"},"operation":{"type":"string"},"step":{"type":"integer","minimum":1},
"status":{"enum":["todo","doing","done","blocked"]},"note":{"type":"string"}}}}),
json!({"name":"create_site","description":"Propose a contracts-first construction site. A person approves its draft before the network changes.",
"inputSchema":{"type":"object","required":["name","purpose","subjects","contracts"],"additionalProperties":false,
"properties":{"name":{"type":"string"},"purpose":{"type":"string"},"operation":{"type":"string"},
"subjects":{"type":"array","minItems":1,"items":{"type":"string"}},"contracts":{"type":"object",
"additionalProperties":{"type":"object","required":["input_schema_json","output_schema_json"],"additionalProperties":false,
"properties":{"input_schema_json":{"type":"string"},"output_schema_json":{"type":"string"}}}}}}}),
json!({"name":"bind_builder","description":"Propose exactly one active builder and its site-only plan and trial serve grants.",
"inputSchema":{"type":"object","required":["service","agent"],"additionalProperties":false,
"properties":{"service":{"type":"string"},"agent":{"type":"string"},"operation":{"type":"string"}}}}),
json!({"name":"unbind_builder","description":"Propose revoking both recorded builder grants. Approval is required.",
"inputSchema":{"type":"object","required":["service"],"additionalProperties":false,
"properties":{"service":{"type":"string"},"operation":{"type":"string"}}}}),
json!({"name":"complete","description":"Propose completing a site with an approved template and no registered trial. Approval revokes its trial serve grant.",
"inputSchema":{"type":"object","required":["service"],"additionalProperties":false,
"properties":{"service":{"type":"string"},"operation":{"type":"string"}}}}),
]
}
#[must_use]
pub fn tool<R: Rail>(rail: &R, name: &str, arguments: &Map<String, Value>) -> Option<ToolResult> {
if !matches!(
name,
"plan_set" | "plan_step" | "create_site" | "bind_builder" | "unbind_builder" | "complete"
) {
return None;
}
Some(forward(rail, name, arguments))
}
pub(super) fn forward<R: Rail>(rail: &R, name: &str, arguments: &Map<String, Value>) -> ToolResult {
let mut payload = arguments.clone();
let operation = match payload.remove("operation") {
Some(Value::String(operation))
if !operation.is_empty() && !operation.chars().any(char::is_control) =>
{
operation
}
Some(_) => {
return ToolResult::failed(
"operation must be a nonempty string without controls",
json!({"tool":name}),
);
}
None => {
let mut bytes = [0_u8; 16];
if std::fs::File::open("/dev/urandom")
.and_then(|mut file| file.read_exact(&mut bytes))
.is_err()
{
return ToolResult::failed(
"operation_randomness_unavailable",
json!({"tool":name}),
);
}
let digits: String = bytes
.iter()
.flat_map(|byte| {
[
char::from(HEX[usize::from(byte >> 4)]),
char::from(HEX[usize::from(byte & 0x0f)]),
]
})
.collect();
format!("op-{digits}")
}
};
let input = json!({"operation":operation,"payload":payload});
match request(rail, &format!("services.{name}"), &input, true, None) {
Outcome::Reply(answer) => {
let Some(bound) = std::num::NonZeroUsize::new(rail.request_bytes()) else {
return ToolResult::failed("ticket_request_bound_is_zero", json!({"tool":name}));
};
match super::held::from_wire(&answer, bound, bound) {
Ok(Some(held)) => held,
Ok(None) => ToolResult::ok(answer),
Err(error) => ToolResult::failed(&error.to_string(), json!({"tool":name})),
}
}
Outcome::Sent => ToolResult::failed("the server sent no reply", json!({"tool":name})),
Outcome::Refused(words) => ToolResult::failed(&words, json!({"tool":name})),
}
}
#[cfg(test)]
#[path = "plan_tools_tests.rs"]
mod tests;