safescript — a programming language for AI agents😌"/>
v0.1.0<br>safescript😌<br>A programming language for AI agents. Provably safe. Immune to supply chain attacks. Ready to eval, no VM required.<br>Read the docsGitHub
Install<br>Deno<br>$ deno add jsr:@uri/safescript<br>npm<br>$ npx jsr add @uri/safescript
01Write code
fetch.ss<br>fetchUser = (id: string, apiKey: string) => {<br>user = httpRequest({<br>host: "api.example.com",<br>method: "GET",<br>path: "/users",<br>body: id<br>headers: { "authorization": apiKey }<br>})<br>return user
Looks like a normal language. Variables, expressions, function calls. One constraint: when your code calls a host, that host must be a string literal. Not a variable. This is what makes static analysis possible.
02See the graph
data flow graph<br>paramidsecretapi-keyhttpRequest"api.example.com"return
Every program compiles to a static directed acyclic graph. No dynamic dispatch, no runtime surprises. We can trace every piece of data from source to sink without executing anything.
03See everything before it runs
signature<br>hosts: { "api.example.com" }<br>dataFlow:<br>param:id → host:api.example.com<br>param:apiKey → host:api.example.com
Computed statically from the source. No execution needed. Every source, every host contacted, every data flow path. You know everything before it runs, so you can run it in-process. No container, no VM, no cold start. Just call a function.
04Import without fear
main.ss<br>import fetchUser from "https://example.com/fetch.ss"<br>perms {<br>hosts: ["api.example.com"],<br>dataFlow: { "host:api.example.com": ["param:id", "param:apiKey"] }<br>hash "sha256:9f86d081884c..." // optional, locks a specific version
main = (query: string, apiKey: string) => {<br>result = fetchUser({ id: query, apiKey: apiKey })<br>return result
Declare what a dependency is allowed to do. The hash locks the source. The perms assert its signature: hosts, secrets, and data flows. New host or secret read? Build fails. Secret starts flowing somewhere new? Build fails. Code changed? Hash fails. Supply chain attacks become build errors.
Isn't a sandbox enough?<br>The standard answer to running untrusted code is "put it in a sandbox." Every approach has real tradeoffs. Here's what the landscape actually looks like.
ContainersDocker, AWS Lambda, etc.<br>Cold starts range from 500ms to 10 seconds. A bare Node.js process in Lambda uses ~35MB before your code even loads. Each container gets its own OS process, so context switching eats CPU time that should go to your code. AWS Lambda can only handle one concurrent request per instance, so every new request risks another cold start. You're paying for infrastructure overhead, not computation.
microVMsFirecracker, E2B, Deno Sandbox<br>Lighter than full containers. Firecracker boots a VM in ~125ms with about 5MB overhead. That's much better than Docker, but you're still spinning up a Linux kernel for every sandbox. E2B and Deno Sandbox both use microVMs under the hood. For AI agents that might run hundreds of code snippets per session, the latency compounds. These are designed for workloads that run for minutes, not for evaluating a small script in microseconds.
V8 IsolatesCloudflare Workers, Dynamic Workers<br>The best runtime sandbox available today. ~5ms startup, ~3MB memory. Cloudflare calls them 100x faster than containers. The catch: you need their platform to run them. V8 isolates are not something you can trivially embed in your own application. And they're still a runtime boundary. You control what APIs the code can call, but you can't see what the code will dobefore it runs. A compromised dependency inside an isolate can still exfiltrate data through any API you've exposed to it.
Deno Permissions--allow-net, --allow-read, etc.<br>Process-level flags that restrict I/O at the runtime boundary. Better than Node's "everything is allowed" default. But all code on the same thread shares the same privilege level. If you grant network access to call one API, every dependency can use that access too. eval()and dynamic imports run at full privilege. Deno's own docs recommend using additional sandboxing (VMs, seccomp, cgroups) for truly untrusted code. Permissions tell you what the process can do, not what it will do.
JS Sandboxesisolated-vm, quickjs-emscripten, SandboxJS<br>In-process sandboxing. Isolated-vm runs V8 isolates inside Node but has had sandbox escape CVEs. QuickJS-emscripten compiles the QuickJS engine to WASM, which gives real memory isolation at ~2MB overhead, but no native async and a limited standard library. SandboxJS-style libraries just wrap eval with property hiding on the global object, which is trivially escapable. The lightweight options aren't hermetic. The hermetic options aren't lightweight.
WebAssemblyWasm sandboxing, WASI<br>Memory-safe by design: linear memory, no raw pointers, no I/O unless the host provides it. Good security properties. But Wasm modules need explicit host bindings for everything, including basic things like printing or network access. Compilation adds latency. No native...