Skip to main content

context_harness/
lua_tool_binding.rs

1//! Explicitly enrolled privileged Lua implementations for local agent bindings.
2//! Manifests and script bytes are inspected without evaluating Lua.
3
4use 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}