<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Caio Augusto Araujo Oliveira</title>
    <description>The latest articles on DEV Community by Caio Augusto Araujo Oliveira (@caioaao).</description>
    <link>https://dev.to/caioaao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3782687%2F49a1a934-bb42-4552-b4f2-feb25cf78666.jpeg</url>
      <title>DEV Community: Caio Augusto Araujo Oliveira</title>
      <link>https://dev.to/caioaao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/caioaao"/>
    <language>en</language>
    <item>
      <title>Creating a Lua sandbox for my LLM tool</title>
      <dc:creator>Caio Augusto Araujo Oliveira</dc:creator>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/caioaao/creating-a-lua-sandbox-for-my-llm-tool-ofm</link>
      <guid>https://dev.to/caioaao/creating-a-lua-sandbox-for-my-llm-tool-ofm</guid>
      <description>&lt;p&gt;I'm building a &lt;a href="https://dev.to/caioaao/code-execution-for-llms-a-better-approach-than-tool-calls-3m8f"&gt;tool to give LLMs a Lua REPL&lt;/a&gt;. My goal is to make it safe by design, so I had to build a sandbox around it. This post is about how I built this, what I learned, and the final result.&lt;/p&gt;


&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Security is not my specialty. I'm trying to build something safe by design, and researching/learning as I do it. If you spot anything wrong or just weird, &lt;a href="https://caioaao.dev/about/#contact" rel="noopener noreferrer"&gt;let me know&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a sandbox?
&lt;/h2&gt;

&lt;p&gt;Ok, much has been said about how we need to run LLMs/Agents in sandboxes for safety, but what are sandboxes?&lt;/p&gt;

&lt;p&gt;Simply put: it's a contained environment where programs being executed only have access to safe operations. Sounds vague? Because it is.&lt;/p&gt;

&lt;p&gt;In reality, you can have a sandbox in different layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operating system:&lt;/strong&gt; things like &lt;a href="https://github.com/netblue30/firejail" rel="noopener noreferrer"&gt;firejail&lt;/a&gt;, where the sandbox is applied using OS-level isolation primitives (namespaces, seccomp, cgroups)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VM:&lt;/strong&gt; you define a boundary by creating a managed runtime that executes bytecode. This extra layer means you can control the execution, since you control the runtime. A prominent modern example is &lt;a href="https://webassembly.org/" rel="noopener noreferrer"&gt;WASM&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Language:&lt;/strong&gt; the boundary lies in the language runtime. Dangerous library functions are removed from the environment, so untrusted code simply cannot call them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You then choose the level you want to sandbox by what you are planning to run in it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Untrusted binaries? You'll need to secure the OS&lt;/li&gt;
&lt;li&gt;Untrusted modules inside a known runtime? The sandbox usually lies in the VM&lt;/li&gt;
&lt;li&gt;Implementing a plugin system or a DSL? Then the boundary is the language&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, the choice is very natural and derives from the kind of application you are building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sandboxing the REPL
&lt;/h2&gt;

&lt;p&gt;To understand where our boundary lies, let's look at where untrusted programs come from:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;From the LLM itself: the idea is to allow LLMs to run code in this Lua REPL, and we know we can't trust LLMs, &lt;em&gt;right&lt;/em&gt;?&lt;/li&gt;
&lt;li&gt;Plugins: we want to allow users to define packages to extend what the LLMs can do in the REPL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As we can see, we won't have untrusted native binaries — both the LLM output and user plugins are Lua source code running inside our controlled runtime. So language-level sandboxing is the way to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  First implementation: block everything
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/caioaao/onetool/blob/a497421f83eebe88af1a87d6a5909ef521fa37ff/src/runtime/sandbox.rs" rel="noopener noreferrer"&gt;first implementation&lt;/a&gt; was very straightforward: go through the Lua API and block everything that could be dangerous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub fn apply(lua: &amp;amp;mlua::Lua) -&amp;gt; mlua::Result&amp;lt;()&amp;gt; {
    // First, preserve safe os functions before blocking
    sandbox_os_module(lua)?;

    let globals = lua.globals();

    globals.set("io", mlua::Value::Nil)?;
    globals.set("loadstring", mlua::Value::Nil)?;
    globals.set("require", mlua::Value::Nil)?;
    // And everything else that could be dangerous
    // ...

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's great, and it made everything very safe, but resulted in an almost useless tool. You can still do some math and string processing in this REPL, but can't call any external service, read any file, etc. We need a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter policies
&lt;/h2&gt;

&lt;p&gt;One thing I left out of the sandbox explanation is the policy. Basically, for unsafe operations, you define a policy: a piece of logic that judges whether the execution is allowed or not.&lt;/p&gt;

&lt;p&gt;The policy can take any number of inputs, and spit out a decision. In rust, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pub trait Policy {
    fn check_access(&amp;amp;self, action: &amp;amp;Action) -&amp;gt; Decision;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the current implementation, I decided to go with the simplest approach possible. We define an API spec: a list of functions that are either &lt;code&gt;safe&lt;/code&gt;, &lt;code&gt;unsafe&lt;/code&gt;, or &lt;code&gt;forbidden&lt;/code&gt;. If the function is safe, we allow its execution. If it's unsafe, we pass it through a policy. Forbidden functions are not passed to the policy and are rejected right away, to avoid any policy misconfiguration and reduce the chance of a sandbox escape.&lt;/p&gt;

&lt;p&gt;This is a significant improvement over the naive "block-all" implementation. It allows use cases such as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Audit: permit but log all unsafe calls&lt;/li&gt;
&lt;li&gt;Dangerous mode: just whitelist everything and trust the LLM - there are crazy people out there&lt;/li&gt;
&lt;li&gt;User confirmation: like agentic coding tools, we can have a policy that prompts an operator to approve/deny the tool call&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;To demonstrate the tool and the sandbox policy mechanism, I built an interactive notebook TUI. You can check the code out &lt;a href="https://github.com/caioaao/onetool/blob/47c37cb95997d3fb8eefb0ac6fe5c1efb41b2d02/examples/notebook-demo.rs" rel="noopener noreferrer"&gt;here&lt;/a&gt;. In it, we define an &lt;code&gt;AskUserPolicy&lt;/code&gt; that will check with the user whether the execution should be allowed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpb8y7gz2zp41wyl25cgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpb8y7gz2zp41wyl25cgq.png" alt="Notebook TUI demo" width="800" height="552"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The LLM tries to execute a code and, during that execution, the policy asks the operator if it's ok to run io.popen&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This was a fun project. Lua proved once again to be a great foundation for building sandboxed, extensible environments.&lt;/p&gt;

&lt;p&gt;That said, there were a lot of things I left out, and might revisit later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory/CPU limits:&lt;/strong&gt; depending on your threat model, making sure no code can exhaust the host's memory or CPU cycles is important. From the quick research I did, it is doable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take caller into consideration:&lt;/strong&gt; another cool thing would be to be able to discriminate between code that was written by the LLM, vs code that was imported from a module, but this was surprisingly hard in Lua. There's no reliable way of saying where a function was defined, and every approach I took fell short.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In case you want to check out what I'm building, here's the repo: &lt;a href="https://github.com/caioaao/onetool" rel="noopener noreferrer"&gt;caioaao/onetool&lt;/a&gt;. Also check out the first post I wrote about it, where I go in depth why I'm building this in the first place: &lt;a href="https://dev.to/caioaao/code-execution-for-llms-a-better-approach-than-tool-calls-3m8f"&gt;Code Execution for LLMs: A Better Approach Than Tool Calls&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>security</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Code Execution for LLMs: A Better Approach Than Tool Calls</title>
      <dc:creator>Caio Augusto Araujo Oliveira</dc:creator>
      <pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/caioaao/code-execution-for-llms-a-better-approach-than-tool-calls-3m8f</link>
      <guid>https://dev.to/caioaao/code-execution-for-llms-a-better-approach-than-tool-calls-3m8f</guid>
      <description>&lt;p&gt;As part of this year's goals, I decided to invest a lot more time into learning and using AI tools. I'm very skeptical about AI and find it hard to cut through the hype, so I wanted to make sure I'm keeping my bias in check and not missing out on anything really important.&lt;/p&gt;

&lt;p&gt;Because of that, during the Carnival I was playing around with LLM tool calling, as any good Brazilian would&lt;sup&gt;1&lt;/sup&gt;, and it got me thinking: wouldn't it be better if we had a way of defining function calls, but also sequences of computations using the results of these calls, in a way that doesn't need a back and forth with the LLM? And it would be great if this interface was close to natural language, so that LLMs would already be trained and do well in writing it?&lt;/p&gt;

&lt;p&gt;Turns out we already have that, and we've had it at least since the 1960s. They're called &lt;em&gt;programming languages&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Quick side-note: when I started writing this post, I'd already developed the core functionality for &lt;a href="https://github.com/caioaao/onetool" rel="noopener noreferrer"&gt;onetool&lt;/a&gt;. Then when researching for this post, I found out &lt;a href="https://blog.cloudflare.com/code-mode/" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; (and later &lt;a href="https://www.anthropic.com/engineering/code-execution-with-mcp" rel="noopener noreferrer"&gt;Anthropic&lt;/a&gt;) had already beat me to it, so this might not be as fresh of an approach as I initially thought. I still think this is different enough to be useful, and hopefully by making this opensource it can be used in more applications outside of the proprietary tools of these companies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Think about it: we want to give the LLM a way of controlling its environment, extend its capabilities, and interact with external systems. What better way of doing so than a programming language? There are a lot of benefits to it, and you can read all about it in this &lt;a href="https://blog.geta.team/mcp-was-the-wrong-abstraction-for-ai-agents/" rel="noopener noreferrer"&gt;great blog post&lt;/a&gt; about how MCPs were the wrong abstraction, but the one that stands out the most in my opinion is how much it can reduce costs by cutting down the number of tokens you have to exchange with the LLM provider.&lt;/p&gt;

&lt;p&gt;In the next section, I'll discuss why I chose Rust and Lua, technical decisions, and the next steps. If you are only interested in the tool, it's here: &lt;a href="https://github.com/caioaao/onetool" rel="noopener noreferrer"&gt;caioaao/onetool&lt;/a&gt;. It's a Rust crate that defines a way of spawning a sandboxed Lua REPL for LLMs to interact with. It has adapters for some of the most popular Rust LLM libraries/frameworks: &lt;a href="https://github.com/0xPlaygrounds/rig" rel="noopener noreferrer"&gt;rig&lt;/a&gt;, &lt;a href="https://github.com/EricLBuehler/mistral.rs" rel="noopener noreferrer"&gt;mistral.rs&lt;/a&gt;, &lt;a href="https://github.com/jeremychone/rust-genai" rel="noopener noreferrer"&gt;genai&lt;/a&gt;, and &lt;a href="https://github.com/lazy-hq/aisdk" rel="noopener noreferrer"&gt;AI SDK&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's still a toy project though, so use with care. Everything may break, and I might decide to change everything tomorrow :D. If you wanna see it in action, here's an example of solving the "Needle in a Haystack" challenge - where the agent has to find a magic number in a huge context (50mb of random strings) - using onetool: &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/examples/needle-in-haystack.rs" rel="noopener noreferrer"&gt;examples/needle-in-haystack.rs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating onetool
&lt;/h2&gt;

&lt;p&gt;Ok, let's start with the language choice: why Rust? Let's get this out of the way: it's not because I want the safety guarantees it provides. I loved that it helped me, but it wasn't taken into consideration, and my first exploration was actually in Elixir.&lt;/p&gt;

&lt;p&gt;The main reason I chose Rust is because it's a popular language, with great FFI in languages I like (Elixir) or use (Javascript). Having it implemented in such an universally available language will enable me to use it in different contexts.&lt;/p&gt;

&lt;p&gt;As a bonus, I always wanted to give Rust a second try. I 'learned' (by learn I mean I used it in a couple personal projects) it years ago, but never really liked it. I gotta say, I had fun. And learning a new language is a lot easier with LLMs now, so that was a fun experience overall.&lt;/p&gt;

&lt;p&gt;The next thing to decide was: what language should the AI write in?&lt;/p&gt;

&lt;p&gt;These were my criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Interpreted&lt;/strong&gt; : we can't depend on a compile-eval loop, and we need the runtime to persist across calls so the LLM can define variables, inspect them, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to embed&lt;/strong&gt; : we don't want harnesses having to manage the runtime for this&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to sandbox&lt;/strong&gt; : it might be a shocker to some, but giving &lt;em&gt;too much&lt;/em&gt; power to an LLM can be dangerous&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple and expressive&lt;/strong&gt; : allows AI to write small snippets to solve a problem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong standard library&lt;/strong&gt; : especially for string manipulation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mature, and relatively well known&lt;/strong&gt; : so we have editor plugins, people are not so intimidated by it, etc&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the end, I settled for &lt;strong&gt;Lua&lt;/strong&gt;. It's "widespread" enough due to it being the config language for neovim, plus it's very common as a scripting language in games (or so I heard). As a nice bonus, it's Brazilian - bora!&lt;/p&gt;

&lt;p&gt;An honorable mention goes to LISP. I have to admit I was tempted to use some flavor of lisp or even writing my own interpreter, as it's a very simple language with a great track record on being used as a scripting language. But I feel like people tend to be intimidated/put off by the parenthesis - even though it's a feature of the language, not a limitation :(&lt;/p&gt;

&lt;p&gt;Lua was great to work with. It has a very capable standard library, and the &lt;a href="https://github.com/mlua-rs/mlua" rel="noopener noreferrer"&gt;Rust crate&lt;/a&gt; made it very easy to sandbox it.&lt;/p&gt;

&lt;p&gt;Which brings us to the next point: &lt;strong&gt;sandboxing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the greatest benefit of a scripting language, over some more direct control (eg: allowing the LLM to write and execute bash scripts). This sandboxing approach provides strong guarantees: the LLM cannot access file I/O, network resources, or OS functions unless explicitly exposed. Barring implementation bugs in the sandbox itself, the attack surface is well-defined and controllable.&lt;/p&gt;

&lt;p&gt;Since I wanted this to be safe by default, I made it sort of useless to start (more on next steps later). I blocked file access, OS functions, loading modules... Anything that could be used to run malicious code or break out of the sandbox is blocked.&lt;/p&gt;

&lt;p&gt;Ok, now that we have a Lua runtime that can run safe code, how will the LLM interact with it?&lt;/p&gt;

&lt;p&gt;I decided to take some inspiration from &lt;a href="https://en.wikipedia.org/wiki/Literate_programming" rel="noopener noreferrer"&gt;literate programming&lt;/a&gt; tools, such as &lt;a href="https://orgmode.org/" rel="noopener noreferrer"&gt;Org mode&lt;/a&gt; and &lt;a href="https://jupyter.org/" rel="noopener noreferrer"&gt;Jupyter&lt;/a&gt;. In these tools, there is text intermingled with code blocks, backed by a runtime that's active during the whole session. When code blocks run, both the output (stuff sent to STDOUT) and the expression result are printed below it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsv7hhoye05ulo2poohd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbsv7hhoye05ulo2poohd.png" alt="Jupyter demo" width="800" height="465"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Jupyter notebook example. See how output and expression results show up with the code block&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A chat with an LLM could be seen as this sort of notebook interface, where the code that the LLM writes is there, alongside the output, and the user and LLM messages. I think this maps well with the common mental model of an LLM as a chat interface.&lt;/p&gt;

&lt;p&gt;To model this, we take advantage of what's already in the LLM's training data: tool calls. Since LLMs are trained specifically for calling tools, we can leverage that, and expose the code execution as a tool. So a chat with the LLM would look something like this (taken from &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/examples/rig-basic.rs" rel="noopener noreferrer"&gt;a real example&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User: What's the sum of the 10 first prime numbers?&lt;/li&gt;
&lt;li&gt;LLM: &lt;code&gt;{tool: "lua_repl", source_code: "local primes = {}\nlocal num = 2\n...return sum"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Tool call: &lt;code&gt;{output: "", eval_result: "129"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;LLM: "The sum of the first 10 prime numbers is 129."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last thing to solve is tool &lt;strong&gt;discovery&lt;/strong&gt; : how will the LLM know what functions are available in Lua for it to call? For that I took the simplest approach: a global variable &lt;code&gt;docs&lt;/code&gt; that hold description for all the APIs available. Adding a new doc from Rust is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let repl = onetool::Repl::new(...);
repl.with_runtime(|lua| {
    register(lua, &amp;amp;LuaDoc {
        name: "my_fn".to_string(),
        typ: LuaDocTyp::Function,
        description: "Does something useful".to_string(),
    })?;
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we can just instruct the LLM to check the docs variable for info on what is available. Simple, yet effective. The docs are there, and it doesn't bloat the LLM's context&lt;/p&gt;

&lt;p&gt;The final step was to build the adapters so the REPL could be used with the most popular Rust AI libraries. For that I just implemented the integration as a &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/src/genai.rs" rel="noopener noreferrer"&gt;tool for genai&lt;/a&gt;, and for the others I just asked Claude Code to apply the same pattern. After a brief moment where I thought Claude Code had one-shotted everything, I did a second pass to fix its &lt;a href="https://github.com/caioaao/onetool/commit/405732a053b77e5f715d427557098f51db2cd869#diff-3d8878f5d8b563a554381c31cd7e5d9155b1bacf6bb5732ddc00ab004f50cbf2L29" rel="noopener noreferrer"&gt;dumb decisions&lt;/a&gt;&lt;sup&gt;2&lt;/sup&gt;. And that's it! Now we have adapters for &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/src/mistralrs.rs" rel="noopener noreferrer"&gt;mistral.rs&lt;/a&gt;, &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/src/rig.rs" rel="noopener noreferrer"&gt;rig&lt;/a&gt;, &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/src/aisdk.rs" rel="noopener noreferrer"&gt;AI SDK&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Safe access to dangerous APIs
&lt;/h3&gt;

&lt;p&gt;As I said previously, I was very aggressive on limiting what the AI has access to in the Lua runtime. But this makes its application very limited - if we can't write to a file or make a network request, there's little we can do.&lt;/p&gt;

&lt;p&gt;You can still expose anything you want through Rust bindings (and since it's in the harness level, there isn't much we could do to secure it anyway), but this makes for poor DX. I don't want to have to go down to Rust to define everything.&lt;/p&gt;

&lt;p&gt;One idea I want to experiment with is creating an API for access control in Lua. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;io = onetool.require("io")
-- If granted, the script can now use `io` tools normally
io.read(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, &lt;code&gt;onetool.require&lt;/code&gt; would verify if the caller has access to this API. It could even implement an access control policy, where it returns the control over to the operator to decide if the API is allowed or not. And we could scope this access to the session, or the call, or even add time limits, where the LLM would have to re-require after a certain period to ensure it still has access to that API.&lt;/p&gt;

&lt;p&gt;Another dimension we could add to this access policy is the module, so we have a hierarchy of permissions, instead of only controlling at the agent level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agent // only basic access, plus permission to call `explore` and `web_search`
|- modules/explore.lua // access to io.read
|- modules/web_search.lua // access to http.request
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hierarchy means the agent can't make HTTP requests or read files directly - only the submodules it uses can.&lt;/p&gt;

&lt;p&gt;This seems like a fun problem to solve, and something that would differentiate &lt;code&gt;onetool&lt;/code&gt; from other code execution tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Onetool MCP Server
&lt;/h3&gt;

&lt;p&gt;There's an implementation done, but I'd like to improve on that, so it can be used in existing agents, like Claude Code, Cursor, Windsurf, etc. For that I think the bare minimum would be to be able to extend the agent's capabilities only by writing Lua. To make something that could actually replace existing tools, I'll have to solve the permissions system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add MCP compatibility
&lt;/h3&gt;

&lt;p&gt;Even if we decide, as an industry, that MCPs were the wrong abstraction, we still have a huge ecosystem of useful things built as MCPs.&lt;/p&gt;

&lt;p&gt;So how do we leverage that rich ecosystem?&lt;/p&gt;

&lt;p&gt;One idea is to be able to call MCP servers &lt;em&gt;from lua&lt;/em&gt;. Remember, MCP is just a &lt;a href="https://www.jsonrpc.org/" rel="noopener noreferrer"&gt;JSON-RPC&lt;/a&gt; API. So in theory, if we have a json-rpc client in Lua, we can just call them directly, and print/return the result. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- https://github.com/brave/brave-search-mcp-server
config = {
    command = "npx",
    args = {"-y", "@brave/brave-search-mcp-server", "--transport", "http"},
    env = { BRAVE_API_KEY = "..." }
}

-- Uses the config, then communicates with the MCP to get the tools and build a table of methods
brave_search_mcp = onetool.build_mcp_client(config)

-- We could have the tool schema saved in a special key so the LLM can access it
print(brave_search_mcp.__tools)

brave_search_mcp.brave_web_search({query = "How to add MCP to Deepseek?"}) 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would mean LLMs could call MCPs from code, which would combine the power of code execution with the availability of functionality of MCPs. The best of both worlds.&lt;/p&gt;

&lt;p&gt;Another cool benefit would be that this way we could add MCP support to any LLM, just by adding one tool: onetool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluation pipeline
&lt;/h3&gt;

&lt;p&gt;Remember when I said this started with me playing with LLMs? Well, one of the reasons I started this was that I was interested in playing around with evaluating LLM applications. I want to build something in that sense, to compare token usage, accuracy scores, etc.&lt;/p&gt;

&lt;p&gt;It's easy to see the value in things like the &lt;a href="https://github.com/caioaao/onetool/blob/688a946a399f8eff676f1b99ed776e346fb6a79c/examples/needle-in-haystack.rs" rel="noopener noreferrer"&gt;needle in a haystack example&lt;/a&gt;, where some LLMs won't even be able to solve by default due to the context window, but for more interactive or less obvious examples it would be good to have some metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recursive LLMs
&lt;/h3&gt;

&lt;p&gt;From time to time, there's something new that's taking over the LLM landscape, and &lt;a href="https://alexzhang13.github.io/blog/2025/rlm/" rel="noopener noreferrer"&gt;RLMs&lt;/a&gt; are a new thing. It should be trivial (but fun) to build it using onetool, and I want to see if I can show that code execution is a generalization of RLMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This has been fun to build, and I want to see how far I can take this. I also want to see how it fares in the real world: it was very interesting to see how bad my initial prompts were and how much effect it has on the usefulness of the tool!&lt;/p&gt;

&lt;p&gt;All in all, the more I play with it, the more I see this as a useful tool.&lt;/p&gt;




&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Not my first choice, I'll admit. I did manage to squeeze in at least one &lt;a href="https://www.nextstopbrazil.com/post/what-are-blocos-de-carnaval-schedule-brazil-2025" rel="noopener noreferrer"&gt;bloquinho&lt;/a&gt; in the following weekend, so at least they won't cancel my CPF&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Ok, not totally Claude's fault. I had to change some stuff to make sure the &lt;code&gt;LuaRepl&lt;/code&gt; was &lt;a href="https://doc.rust-lang.org/nomicon/send-and-sync.html" rel="noopener noreferrer"&gt;Sync+Send&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
