AI News HubLIVE
站内改写

Safescript – A Language for AI Era

Safescript is a programming language for AI agents that proves safety properties statically before execution, eliminating the need for sandboxes or VMs. It compiles to a static DAG, enabling full visibility into data flow and host calls, with zero overhead and zero cold starts.

Article intelligence

EngineersIntermediate

Key points

  • Statically enforces security without runtime sandboxing.
  • Compiles to a static DAG that traces all data flows and hosts.
  • Import declarations lock dependencies via signatures and hashes, making supply chain attacks build errors.
  • Outperforms containers, microVMs, and V8 isolates in startup time and memory.

Why it matters

This matters because statically enforces security without runtime sandboxing.

Technical impact

May affect model selection, inference cost, product capability, and evaluation benchmarks.

v0.1.0

safescript😌

A programming language for AI agents. Provably safe. Immune to supply chain attacks. Ready to eval, no VM required.

Read the docsGitHub

Install

Deno

$ deno add jsr:@uri/safescript

npm

$ npx jsr add @uri/safescript

01

Write code

fetch.ss

fetchUser = (id: string, apiKey: string) => {

user = httpRequest({

host: "api.example.com",

method: "GET",

path: "/users",

body: id

headers: { "authorization": apiKey }

})

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.

02

See the graph

data flow graph

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.

03

See everything before it runs

signature

hosts: { "api.example.com" }

dataFlow:

param:id → host:api.example.com

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.

04

Import without fear

main.ss

import fetchUser from "https://example.com/fetch.ss"

perms {

hosts: ["api.example.com"],

dataFlow: { "host:api.example.com": ["param:id", "param:apiKey"] }

}

hash "sha256:9f86d081884c..." // optional, locks a specific version

main = (query: string, apiKey: string) => {

result = fetchUser({ id: query, apiKey: apiKey })

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?

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.

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

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

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.

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

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

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 async. Debugging is painful. It's an execution format, not a language, so you still need a toolchain to compile code into it. Good for plugin systems, awkward for running arbitrary agent-generated scripts.

safescript's approach

Every approach above tries to restrict what code can do at runtime. Safescript restricts what code can express at all. The compiler proves every security property statically, before any execution. You know exactly which hosts will be contacted, which secrets will be read, and how data flows between them. No sandbox needed because the dangerous operations simply aren't there. Zero overhead. Zero cold start. Zero escape surface. Just call a function in your own process.

ApproachCold startMemoryHermetic

Containers500ms–10s~35MB+partial

microVMs~125ms~5MB+yes

V8 Isolates~5ms~3MBruntime only

Deno perms00no

JS sandboxes — size of parameter (string length or array length)

host: — size of the response body from

For map/filter/reduce, the complexity of the inner function is multiplied by the array length. Currently the element size passed to the inner function is treated as constant (1), so map(sha256, strings) where strings: string[] is inferred as O(n) in the array length rather than O(total_chars). This is conservative for most agent skills and may be refined in future versions.

Complexity can later be used as a policy bound: a permission assertion can require that an imported function stay within O(n) or exclude terms above a certain degree.

Syntax

safescript looks like a subset of JavaScript but it's actually a DAG description language. There's no runtime object model, no prototype chain, no closures. Just operations and data flow.

Top-level constructs — imports, function definitions, and doc() annotations — have no evaluation order. A safescript file is a flat namespace; functions reference each other by name, not by position. You can define helper functions before or after the functions that call them, imports can appear anywhere, and the whole file resolves to a single static graph before anything runs.

Within a function body, statement order is also irrelevant. Every function compiles to a DAG — the executor evaluates nodes based on data dependencies, not line numbers. return can appear anywhere in the body:

// valid — return before assignment add = (x: number, y: number): number => { return result; result = x + y; }

The only constraint is scoping: names must be resolvable somewhere in the function (as a parameter or an assignment), but where they appear doesn't matter. Semicolons are optional statement separators.

Functions

Files contain one or more named functions. Each takes typed parameters and returns a value:

greet = (name: string, times: number): string => { msg = stringConcat({ parts: ["hello, ", name] }); return msg; };

The return type annotation (: string after the parameters) is optional but recommended.

Types

Primitives (string, number, boolean), objects ({ name: string, age: number }), and arrays (string[], { id: number }[]). Nested combinations work: { users: { name: string }[] }.

Operations

All computation happens through op calls. Ops take a single object argument with named fields:

hash = sha256({ data: apiKey }); r = httpRequest({ host: "api.example.com", method: "POST", path: "/data", body: hash, });

Some ops have static fields that must be string/number/boolean literals, not variables. httpRequest requires host to be a literal. This is enforced at parse time. It's what makes the signature system work: the set of hosts is always statically known.

Void calls (ops called for side effects without capturing the return value) work too:

httpRequest({ host: "audit.example.com", method: "POST", path: "/events", body: data });

Expressions

Arithmetic (+, -, *, /, %), comparisons (==, !=, , =), string concatenation (+), unary negation (-x), ternary (cond ? a : b), dot access (obj.field.nested), array literals ([a, b, c]), object literals ({ key: val, shorthand }), and parenthesized grouping ((a + b) * c).

Ternary is right-associative, so a ? b : c ? d : e means a ? b : (c ? d : e). Operator precedence follows the standard math/C convention.

Shorthand

Object fields support JS-style shorthand. { body } is sugar for { body: body }. String keys are supported for non-identifier names: { "x-signature": sig }.

Comments

// line comments only

Control flow

Statement-level if/else with Go-like syntax (no parens around condition, braces required):

if x > threshold { result = httpRequest({ host: "primary-api.com", method: "POST", path: "/data", body: payload }) } else { result = httpRequest({ host: "fallback-api.com", method: "POST", path: "/data", body: payload }) }

else is optional. An if without else is valid for conditional side effects:

if shouldCache { httpRequest({ host: "cache.example.com", method: "POST", path: "/cache", body: data }) }

There's no else if keyword. Nest manually:

if x > 0 { label = "positive" } else { if x == 0 { label = "zero" } else { label = "negative" } }

At runtime, only the taken branch executes. The other branch's ops are completely skipped. For static analysis, both branches are conservatively analyzed: sources are unioned and resource bounds are summed.

Map, filter, reduce

safescript has built-in map, filter, and reduce as reserved words. They take a named function reference (not a lambda) and an array:

double = (x: number): number => { return x * 2; };

isPositive = (x: number): boolean => { return x > 0; };

sum = (acc: number, x: number): number => { return acc + x; };

process = (numbers: number[]): number => { doubled = map(double, numbers); positive = filter(isPositive, doubled); total = reduce(sum, 0, positive); return total; };

The function comes first, the array comes last. For reduce, the initial accumulator value goes in the middle: reduce(fn, initial, array).

Function arity is enforced. map and filter require a function that takes exactly one parameter. reduce requires a function that takes exactly two (accumulator, element).

map and filter execute in parallel via Promise.all. This matters when your mapped function does network calls. reduce executes sequentially since each step depends on the previous accumulator.

These work with both local functions and imported functions. The function name must refer to a function defined in the same program or imported from another file.

Override

override(target, { name: replacement, ... }) produces a new callable DAG that behaves like target but with every reference to name (an op label or a user-fn name) rewritten to replacement (a user-fn name). Substitution is transitive: callees of the target are rewritten too, so the swap propagates all the way down the call graph.

fetchExample = (): string => { return httpRequest({ host: "example.com", path: "/" }); };

inner = (): string => { return httpRequest({ host: "original.com", path: "/" }); };

useFetcher = (): string => { return inner(); };

main = (): string => { // Swap inner for `fetc

[truncated for AI cost control]