1use crate::{
5 agent_resource::Capability,
6 config::Config,
7 tool_binding::{
8 validate_binding_arguments, RestrictionKind, ToolBindingRequest,
9 ToolImplementationDescriptor, ToolImplementationFactory, ToolTrustClass,
10 },
11 tool_script::{execute_tool, validate_params, ToolDefinition},
12 traits::{Tool, ToolContext, ToolPreparation},
13};
14use anyhow::{ensure, Context, Result};
15use async_trait::async_trait;
16use serde::Serialize;
17use serde_json::Value;
18use sha2::{Digest, Sha256};
19use std::{
20 path::PathBuf,
21 sync::{
22 atomic::{AtomicBool, Ordering},
23 Arc,
24 },
25};
26
27const ALL_HOST_APIS: [&str; 9] = [
28 "base64", "context", "crypto", "env", "fs", "http", "json", "log", "sleep",
29];
30
31#[derive(Debug, Clone, Serialize)]
32pub struct LuaToolManifest {
33 pub implementation_id: String,
34 pub script_path: PathBuf,
35 pub implementation_description: String,
36 pub default_public_description: String,
37 pub config_schema: Value,
38 pub input_schema: Value,
39 pub capabilities: Vec<Capability>,
40 pub permitted_host_apis: Vec<String>,
41 pub timeout_seconds: u64,
42 pub max_output_bytes: u64,
43}
44
45pub struct LuaToolFactory {
46 descriptor: ToolImplementationDescriptor,
47 manifest: LuaToolManifest,
48 script_source: String,
49 config: Arc<Config>,
50}
51
52impl LuaToolFactory {
53 pub fn from_manifest(mut manifest: LuaToolManifest, config: Arc<Config>) -> Result<Self> {
54 ensure!(
55 manifest.implementation_id.starts_with("lua."),
56 "Lua implementation IDs must use the lua namespace"
57 );
58 ensure!(
59 (1..=300).contains(&manifest.timeout_seconds),
60 "Lua timeout must be in 1..=300 seconds"
61 );
62 ensure!(
63 (1..=1024 * 1024).contains(&manifest.max_output_bytes),
64 "Lua output limit must be in 1..=1048576 bytes"
65 );
66 manifest.permitted_host_apis.sort();
67 manifest.permitted_host_apis.dedup();
68 ensure!(
69 manifest.permitted_host_apis == ALL_HOST_APIS,
70 "version 1 privileged Lua manifests must declare every exposed host API"
71 );
72 for required in [
73 Capability::ReadOnly,
74 Capability::Network,
75 Capability::ExternalSideEffect,
76 ] {
77 ensure!(
78 manifest.capabilities.contains(&required),
79 "privileged Lua manifest is missing required capability"
80 );
81 }
82 manifest.script_path = manifest
83 .script_path
84 .canonicalize()
85 .context("canonicalizing Lua script")?;
86 let script_source = std::fs::read_to_string(&manifest.script_path)
87 .context("reading enrolled Lua script")?;
88 let manifest_bytes = serde_json::to_vec(&manifest)?;
89 let mut digest = Sha256::new();
90 digest.update(&manifest_bytes);
91 digest.update(script_source.as_bytes());
92 let descriptor = ToolImplementationDescriptor {
93 id: manifest.implementation_id.clone(),
94 version: format!("sha256:{:x}", digest.finalize()),
95 implementation_description: manifest.implementation_description.clone(),
96 default_public_description: manifest.default_public_description.clone(),
97 config_schema: manifest.config_schema.clone(),
98 input_schema: manifest.input_schema.clone(),
99 capabilities: manifest.capabilities.clone(),
100 supported_restrictions: vec![RestrictionKind::MaxOutputBytes],
101 trust_class: ToolTrustClass::LuaPrivileged,
102 };
103 Ok(Self {
104 descriptor,
105 manifest,
106 script_source,
107 config,
108 })
109 }
110}
111
112#[async_trait]
113impl ToolImplementationFactory for LuaToolFactory {
114 fn descriptor(&self) -> &ToolImplementationDescriptor {
115 &self.descriptor
116 }
117
118 async fn bind(&self, request: ToolBindingRequest) -> Result<Box<dyn Tool>> {
119 let configured_limit = request
120 .resolved
121 .restrictions
122 .max_output_bytes
123 .unwrap_or(self.manifest.max_output_bytes)
124 .min(self.manifest.max_output_bytes);
125 Ok(Box::new(BoundLuaTool {
126 definition: ToolDefinition {
127 name: request.resolved.name.clone(),
128 description: request.resolved.description.clone(),
129 parameters_schema: self.descriptor.input_schema.clone(),
130 script_path: self.manifest.script_path.clone(),
131 script_source: self.script_source.clone(),
132 config: request.resource.config.clone(),
133 timeout: self.manifest.timeout_seconds,
134 },
135 binding: request.resolved,
136 config: self.config.clone(),
137 prepared: AtomicBool::new(false),
138 output_limit: configured_limit,
139 }))
140 }
141}
142
143struct BoundLuaTool {
144 definition: ToolDefinition,
145 binding: crate::tool_binding::ResolvedToolBinding,
146 config: Arc<Config>,
147 prepared: AtomicBool,
148 output_limit: u64,
149}
150
151impl BoundLuaTool {
152 fn effective_arguments(&self, arguments: Value) -> Result<Value> {
153 validate_binding_arguments(&self.binding.public_schema, &arguments)?;
154 let mut arguments = arguments
155 .as_object()
156 .context("arguments must be an object")?
157 .clone();
158 for (name, value) in self
159 .binding
160 .fixed
161 .as_object()
162 .context("fixed arguments must be an object")?
163 {
164 ensure!(
165 !arguments.contains_key(name),
166 "fixed argument cannot be overridden"
167 );
168 arguments.insert(name.clone(), value.clone());
169 }
170 let arguments = Value::Object(arguments);
171 validate_binding_arguments(&self.definition.parameters_schema, &arguments)?;
172 validate_params(&self.definition.parameters_schema, &arguments)
173 }
174}
175
176#[async_trait]
177impl Tool for BoundLuaTool {
178 fn capabilities(&self) -> Option<Vec<Capability>> {
179 Some(self.binding.capabilities.clone())
180 }
181 fn binding_metadata(&self) -> Option<Value> {
182 Some(crate::tool_binding::binding_metadata(&self.binding))
183 }
184 fn preparation(&self) -> Option<ToolPreparation> {
185 Some(ToolPreparation {
186 name: format!("runtime.lua.prepare.{}", self.binding.implementation_id),
187 capabilities: self.binding.capabilities.clone(),
188 arguments: serde_json::json!({
189 "binding": self.binding.name,
190 "implementation": self.binding.implementation_id,
191 "version": self.binding.implementation_version,
192 }),
193 })
194 }
195 async fn prepare(&self, _ctx: &ToolContext) -> Result<()> {
196 self.prepared.store(true, Ordering::SeqCst);
197 Ok(())
198 }
199 fn name(&self) -> &str {
200 &self.binding.name
201 }
202 fn description(&self) -> &str {
203 &self.binding.description
204 }
205 fn parameters_schema(&self) -> Value {
206 self.binding.public_schema.clone()
207 }
208 fn validate_arguments(&self, arguments: &Value) -> Result<()> {
209 validate_binding_arguments(&self.binding.public_schema, arguments)
210 }
211 fn approval_arguments(&self, arguments: &Value) -> Result<Value> {
212 self.effective_arguments(arguments.clone())
213 }
214 async fn execute(&self, arguments: Value, _ctx: &ToolContext) -> Result<Value> {
215 ensure!(
216 self.prepared.load(Ordering::SeqCst),
217 "Lua tool was not prepared"
218 );
219 let result = execute_tool(
220 &self.definition,
221 self.effective_arguments(arguments)?,
222 &self.config,
223 )
224 .await?;
225 ensure!(
226 serde_json::to_vec(&result)?.len() as u64 <= self.output_limit,
227 "Lua tool result exceeds output limit"
228 );
229 Ok(result)
230 }
231}