{"slug": "safescript-a-language-for-ai-era", "title": "Safescript – A Language for AI Era", "summary": "Safescript, a new programming language designed for AI agents, eliminates the need for containers or virtual machines by using a closed instruction set and static DAG structure that prevents infinite loops and unauthorized operations. The language guarantees every program terminates and provides a complete static signature of all data flows and external contacts before execution, making it structurally impossible for agent skills to exfiltrate data or perform unauthorized actions.", "body_md": "A programming language for AI agents. Programs are static DAGs of operations with a closed instruction set, formal data-flow tracking, and resource bounds you can inspect before anything runs. No VM, no container, no sandbox needed.\n\n```\ncurl -fsSL https://raw.githubusercontent.com/uriva/safescript/main/install.sh | sh\nsafescript run script.ss\n```\n\nInstalls the CLI globally on macOS / Ubuntu. Also available as a library:\n\n```\ndeno add jsr:@uri/safescript     # Deno\nnpx jsr add @uri/safescript      # npm\n```\n\n## Why this exists\n\nAI agents are getting good enough to write and run code. That's the easy part. The hard part is letting them do it without handing over the keys to the kingdom.\n\nToday, when an agent needs a capability (call an API, transform data, forward a credential), there are two options. Give it a general-purpose language and hope for the best, or restrict it to a handful of hardcoded tools. The first one is a security nightmare. The second one doesn't scale.\n\nThe standard fix for the security problem is to throw a sandbox around it. Docker containers, microVMs, Firecracker, E2B, whatever. That works, but now you're paying for it. Every agent execution spins up a container, waits for it to boot, runs a few API calls, and tears it down. You're burning compute and time on infrastructure whose only job is to babysit the code. Cold starts add latency. Orchestration adds complexity. The per-execution cost adds up fast when you're running thousands of agent tasks a day.\n\nsafescript takes a different approach. The language *is* the sandbox. There's\nnothing to escape from because there's nothing dangerous in the instruction set.\nNo filesystem access, no shell exec, no eval, no dynamic imports. The only\nthings a program can do are the operations explicitly provided by the host. That\nmeans you can run safescript programs directly in your application process, in\nthe same runtime as your server. No container spin-up, no VM overhead, no\norchestration layer.\n\nIt's a real language with variables, expressions, control flow, imports, and a\ngrowing set of built-in operations. But it's not Turing-complete, and that's the\nwhole point. Every program compiles down to a static directed acyclic graph of\noperations. No dynamic dispatch, no infinite loops. The set of things a program\n*can* do is fully knowable before it runs.\n\n## Every program terminates\n\nsafescript is not Turing-complete. That's not a limitation, it's the design.\n\nThere are no loops. No recursion. The parser builds a function call graph and\nrejects cycles at parse time, both direct recursion and mutual recursion. The\nonly iteration constructs are `map`\n\n, `filter`\n\n, and `reduce`\n\n, and they operate on\nfinite arrays. There's no lazy evaluation, no generators, no way to construct\nunbounded data.\n\nThe result is that every safescript program provably halts. You don't need to\ntrust the code, reason about convergence, or set timeouts as a safety net. The\nlanguage *can't* express a program that runs forever.\n\nThis is the same tradeoff languages like Dhall and SQL (without recursive CTEs) make. You give up the ability to express every computable function. In exchange you get a guarantee that no program will hang your system. For AI agent tasks, that's a trade worth making.\n\n## The supply chain problem\n\nAgent skills today look a lot like npm packages did in 2015. Someone publishes a capability. An agent installs it. Nobody reads the source. One day the maintainer pushes an update that exfiltrates inputs to a third-party server, and you find out about it from a blog post.\n\nsafescript makes this structurally impossible. Every program has a\n**signature**, a complete static description of what it does, computed without\nexecuting anything. The signature tells you exactly which hosts it contacts,\nwhich environment sources it reads, and how data flows between all of them.\n\nSay an agent skill receives your API key as an input and sends it to\n`api.example.com`\n\n. That's fine, that's what the skill does. But if an update\nadds a second HTTP call that forwards that same key to `evil.io`\n\n, the signature\nchanges. The new host shows up. The data flow from `param:apiKey`\n\nto\n`host:evil.io`\n\nshows up. You can diff signatures between versions and catch this\nautomatically, before the program ever runs.\n\nThis isn't a sandbox or a firewall. It's a proof. The language is constrained enough that the analysis is exact, not heuristic.\n\n## How signatures work\n\nA signature captures everything a function does without executing it:\n\n```\n{\n  name: \"createIdentity\",\n  params: [{ name: \"userId\", type: \"string\" }],\n  returnType: { status: \"number\" },\n  hosts: [\"agentdocs-api.uriva.deno.net\"],             // which hosts are contacted\n  envReads: [],                                        // timestamp / randomBytes usage\n  dataFlow: {\n    \"host:agentdocs-api...\": [\"param:userId\"],         // userId flows to the API host\n    \"return\": [\"host:agentdocs-api...\"],               // what data reaches the return value\n  },\n  sources: [\"host:agentdocs-api...\"],            // where the return value came from\n  memoryBytes: 1002048,                                // worst-case resource bounds\n  runtimeMs: 10020,\n  diskBytes: 0,\n  complexity: \"1\",                                     // symbolic complexity expression\n}\n```\n\nThe data flow map is the interesting part. Sources are labeled strings:\n`\"param:userId\"`\n\n, `\"host:api.com\"`\n\n, `\"env:timestamp\"`\n\n, `\"env:randomBytes\"`\n\n.\nSinks are `\"host:...\"`\n\nand `\"return\"`\n\n. If an input value reaches a host, or a\nhost's response reaches another host, it shows up explicitly in the map.\n\nResource bounds accumulate from every operation in the program. Each op declares its own memory, runtime, and disk cost. The signature sums them. For branches (ternary, if/else), it conservatively takes the union of sources and the sum of resources from both sides.\n\n### Complexity inference\n\nSignatures now include a symbolic complexity expression derived automatically from the program structure. The analyzer tracks the size of every value—string length for strings, element count for arrays—and composes them into a precise Big-O style formula using parameter names and host labels as variables.\n\n```\n{\n  // ... other signature fields\n  complexity: \"param:items + host:api.example.com\";\n}\n```\n\nExamples of what the analyzer produces:\n\n`1`\n\n— constant work (e.g.,`timestamp()`\n\n,`httpRequest`\n\n)`param:items`\n\n— linear in array length (e.g.,`map(double, items)`\n\n)`param:text`\n\n— linear in string length (e.g.,`sha256({ data: text })`\n\n)`param:matrix * param:matrix`\n\n— quadratic from nested`map`\n\n(when inner function complexity depends on the outer array)`host:api.example.com`\n\n— linear in the response body size\n\nThe expression is a sum of monomials. Each term is a coefficient times a product of size variables. Variables are named after the source they measure:\n\n`param:<name>`\n\n— size of parameter`<name>`\n\n(string length or array length)`host:<hostname>`\n\n— size of the response body from`<hostname>`\n\nFor `map`\n\n/`filter`\n\n/`reduce`\n\n, the complexity of the inner function is multiplied\nby the array length. Currently the element size passed to the inner function is\ntreated as constant (`1`\n\n), so `map(sha256, strings)`\n\nwhere `strings: string[]`\n\nis inferred as `O(n)`\n\nin the array length rather than `O(total_chars)`\n\n. This is\nconservative for most agent skills and may be refined in future versions.\n\nComplexity can later be used as a policy bound: a permission assertion can\nrequire that an imported function stay within `O(n)`\n\nor exclude terms above a\ncertain degree.\n\n## Syntax\n\nsafescript 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.\n\nTop-level constructs — imports, function definitions, and `doc()`\n\nannotations —\nhave no evaluation order. A safescript file is a flat namespace; functions\nreference each other by name, not by position. You can define helper functions\nbefore or after the functions that call them, imports can appear anywhere, and\nthe whole file resolves to a single static graph before anything runs.\n\nWithin a function body, statement order is also irrelevant. Every function\ncompiles to a DAG — the executor evaluates nodes based on data dependencies, not\nline numbers. `return`\n\ncan appear anywhere in the body:\n\n``` js\n// valid — return before assignment\nadd = (x: number, y: number): number => {\n  return result;\n  result = x + y;\n}\n```\n\nThe 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.\n\n### Functions\n\nFiles contain one or more named functions. Each takes typed parameters and returns a value:\n\n``` js\ngreet = (name: string, times: number): string => {\n  msg = stringConcat({ parts: [\"hello, \", name] });\n  return msg;\n};\n```\n\nThe return type annotation (`: string`\n\nafter the parameters) is optional but\nrecommended.\n\n### Types\n\nPrimitives (`string`\n\n, `number`\n\n, `boolean`\n\n), objects\n(`{ name: string, age: number }`\n\n), and arrays (`string[]`\n\n, `{ id: number }[]`\n\n).\nNested combinations work: `{ users: { name: string }[] }`\n\n.\n\n### Operations\n\nAll computation happens through op calls. Ops take a single object argument with named fields:\n\n```\nhash = sha256({ data: apiKey });\nr = httpRequest({\n  host: \"api.example.com\",\n  method: \"POST\",\n  path: \"/data\",\n  body: hash,\n});\n```\n\nSome ops have **static fields** that must be string/number/boolean literals, not\nvariables. `httpRequest`\n\nrequires `host`\n\nto be a literal. This is enforced at\nparse time. It's what makes the signature system work: the set of hosts is\nalways statically known.\n\nVoid calls (ops called for side effects without capturing the return value) work too:\n\n```\nhttpRequest({ host: \"audit.example.com\", method: \"POST\", path: \"/events\", body: data });\n```\n\n### Expressions\n\nArithmetic (`+`\n\n, `-`\n\n, `*`\n\n, `/`\n\n, `%`\n\n), comparisons (`==`\n\n, `!=`\n\n, `<`\n\n, `>`\n\n, `<=`\n\n,\n`>=`\n\n), string concatenation (`+`\n\n), unary negation (`-x`\n\n), ternary\n(`cond ? a : b`\n\n), dot access (`obj.field.nested`\n\n), array literals (`[a, b, c]`\n\n),\nobject literals (`{ key: val, shorthand }`\n\n), and parenthesized grouping\n(`(a + b) * c`\n\n).\n\nTernary is right-associative, so `a ? b : c ? d : e`\n\nmeans\n`a ? b : (c ? d : e)`\n\n. Operator precedence follows the standard math/C\nconvention.\n\n### Shorthand\n\nObject fields support JS-style shorthand. `{ body }`\n\nis sugar for\n`{ body: body }`\n\n. String keys are supported for non-identifier names:\n`{ \"x-signature\": sig }`\n\n.\n\n### Comments\n\n```\n// line comments only\n```\n\n### Control flow\n\nStatement-level `if`\n\n/`else`\n\nwith Go-like syntax (no parens around condition,\nbraces required):\n\n```\nif x > threshold {\n  result = httpRequest({ host: \"primary-api.com\", method: \"POST\", path: \"/data\", body: payload })\n} else {\n  result = httpRequest({ host: \"fallback-api.com\", method: \"POST\", path: \"/data\", body: payload })\n}\n```\n\n`else`\n\nis optional. An `if`\n\nwithout `else`\n\nis valid for conditional side\neffects:\n\n```\nif shouldCache {\n  httpRequest({ host: \"cache.example.com\", method: \"POST\", path: \"/cache\", body: data })\n}\n```\n\nThere's no `else if`\n\nkeyword. Nest manually:\n\n```\nif x > 0 {\n  label = \"positive\"\n} else {\n  if x == 0 {\n    label = \"zero\"\n  } else {\n    label = \"negative\"\n  }\n}\n```\n\nAt 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.\n\n### Map, filter, reduce\n\nsafescript has built-in `map`\n\n, `filter`\n\n, and `reduce`\n\nas reserved words. They\ntake a named function reference (not a lambda) and an array:\n\n``` js\ndouble = (x: number): number => {\n  return x * 2;\n};\n\nisPositive = (x: number): boolean => {\n  return x > 0;\n};\n\nsum = (acc: number, x: number): number => {\n  return acc + x;\n};\n\nprocess = (numbers: number[]): number => {\n  doubled = map(double, numbers);\n  positive = filter(isPositive, doubled);\n  total = reduce(sum, 0, positive);\n  return total;\n};\n```\n\nThe function comes first, the array comes last. For `reduce`\n\n, the initial\naccumulator value goes in the middle: `reduce(fn, initial, array)`\n\n.\n\nFunction arity is enforced. `map`\n\nand `filter`\n\nrequire a function that takes\nexactly one parameter. `reduce`\n\nrequires a function that takes exactly two\n(accumulator, element).\n\n`map`\n\nand `filter`\n\nexecute in parallel via `Promise.all`\n\n. This matters when your\nmapped function does network calls. `reduce`\n\nexecutes sequentially since each\nstep depends on the previous accumulator.\n\nThese 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.\n\n### Override\n\n`override(target, { name: replacement, ... })`\n\nproduces a new callable DAG that\nbehaves like `target`\n\nbut with every reference to `name`\n\n(an op label or a\nuser-fn name) rewritten to `replacement`\n\n(a user-fn name). Substitution is\ntransitive: callees of the target are rewritten too, so the swap propagates all\nthe way down the call graph.\n\n``` js\nfetchExample = (): string => {\n  return httpRequest({ host: \"example.com\", path: \"/\" });\n};\n\ninner = (): string => {\n  return httpRequest({ host: \"original.com\", path: \"/\" });\n};\n\nuseFetcher = (): string => {\n  return inner();\n};\n\nmain = (): string => {\n  // Swap `inner` for `fetchExample` everywhere inside `useFetcher`.\n  f = override(useFetcher, { inner: fetchExample });\n  return f();\n};\n```\n\nThe result of `override(...)`\n\nis a first-class DAG value. You can:\n\n- invoke it inline:\n`override(useFetcher, { inner: fetchExample })()`\n\n- bind it to a local and call later:\n`f = override(...); f()`\n\n- pass it to\n`map`\n\n/`filter`\n\n/`reduce`\n\nas the function argument\n\nSignatures see through overrides. In the example above, `main`\n\n's signature\nreports `example.com`\n\nas a host, not `original.com`\n\n, because the analyzer walks\nthe rewritten DAG.\n\n### Imports\n\nsafescript programs can import functions from other safescript programs. Imports go at the top of the file, before any function definitions:\n\n``` python\nimport add from \"./math.ss\" perms {} hash \"sha256:abc123...\"\n\nsum = (a: number, b: number): number => {\n  result = add({ x: a, y: b })\n  return result\n}\n```\n\nThe imported function becomes available as a regular op in the local program.\nYou call it the same way you call any built-in: `add({ x: a, y: b })`\n\n.\n\n**Aliasing.** If the imported name conflicts with something local, use `as`\n\n:\n\n``` python\nimport add as mathAdd from \"./math.ss\" perms {} hash \"sha256:abc123...\"\n```\n\n**Hash verification.** The `hash`\n\nfield is a SHA-256 hash of the dependency's\n*normalized form*. Normalization strips comments, normalizes whitespace, and\nalpha-renames internal variables (parameters become `_p0`\n\n, `_p1`\n\n; locals become\n`_v0`\n\n, `_v1`\n\n) while preserving function names, op names, and string literals.\nThis means cosmetic changes (renaming a variable, reformatting) don't break the\nhash. Semantic changes do. If the dep's content doesn't match the declared hash,\nthe build fails.\n\nTo get the hash of a program:\n\n``` js\nimport { hashProgram } from \"safescript\";\n\nconst hash = await hashProgram(sourceCode);\n// \"sha256:e3b0c44298fc1c149afbf4c8996fb924...\"\n```\n\n**Permission assertions.** The `perms`\n\nblock declares exactly what the imported\nfunction (and all its transitive dependencies) can do. The fields match the\nsignature: `hosts`\n\n, `envReads`\n\n, and `dataFlow`\n\n. The first two are arrays of\nstring literals. `dataFlow`\n\nis an object mapping sink labels to arrays of source\nlabels. Missing fields mean empty sets, except `dataFlow`\n\nwhich is optional:\nomit it to skip the data flow check.\n\n``` python\nimport fetchUser from \"https://example.com/user.ss\" perms {\n  hosts: [\"api.example.com\"],\n  dataFlow: {\n    \"host:api.example.com\": [\"param:userId\"],\n    \"return\": [\"host:api.example.com\"]\n  }\n} hash \"sha256:...\"\n```\n\nThis is not a permissions *grant*, it's an *assertion*. The resolver computes\nthe actual transitive signature of the imported function and checks that it\nexactly matches the declared perms. If the dep secretly starts reading a new\nhost, using a new env source, or routing data somewhere new, the assertion fails\nand the build breaks. You must update the perms declaration to acknowledge the\nchange.\n\nA pure dependency (no hosts, no env reads) uses empty braces:\n\n``` python\nimport add from \"./math.ss\" perms {} hash \"sha256:...\"\n```\n\n**Transitive composition.** Dependencies can have their own imports. The\nresolver processes the entire transitive chain. Each dependency's perms are\nverified against its full transitive signature. Circular dependencies are\nimpossible by construction since each import must declare a hash, and you can't\nhash something that references itself.\n\n**Diamond dependencies.** If two imports share a common transitive dependency\n(same hash), it's resolved once and cached. No duplication.\n\n## Built-in operations\n\n### I/O\n\n| Op | Static fields | Description |\n|---|---|---|\n`httpRequest({ host, method, path, headers?, body? })` | `host` | HTTPS request to declared host |\n\n### Pure\n\n| Op | Description |\n|---|---|\n`jsonParse({ text })` | Parse JSON string to value |\n`jsonStringify({ value })` | Serialize value to JSON string |\n`stringConcat({ parts })` | Concatenate an array of strings |\n`base64urlEncode({ data })` | Base64url encode |\n`base64urlDecode({ data })` | Base64url decode |\n`pick({ object, keys })` | Pick keys from an object |\n`merge({ objects })` | Shallow merge objects |\n`sha256({ data })` | SHA-256 hash |\n\n### Crypto\n\n| Op | Description |\n|---|---|\n`generateEd25519KeyPair()` | Generate Ed25519 signing keypair |\n`generateX25519KeyPair()` | Generate X25519 key agreement keypair |\n`ed25519Sign({ data, privateKey })` | Sign data with Ed25519 |\n`aesGenerateKey()` | Generate AES-GCM key |\n`aesEncrypt({ key, plaintext })` | AES-GCM encrypt |\n`aesDecrypt({ key, ciphertext })` | AES-GCM decrypt |\n`x25519DeriveKey({ privateKey, publicKey })` | Derive shared secret via X25519 |\n`importIdentity({ exported })` | Import a serialized identity |\n`exportIdentity({ keys })` | Export an identity to serializable form |\n\n### Sources\n\n| Op | Description |\n|---|---|\n`timestamp()` | Current Unix timestamp (tagged as non-deterministic) |\n`randomBytes({ length })` | Cryptographic random bytes (tagged as non-deterministic) |\n\n## Architecture\n\nsafescript has two layers.\n\n**The op layer** is a TypeScript library for defining and composing typed\noperations (`DagOp`\n\nobjects). Each op has a Zod input/output schema, a manifest\ndeclaring its resource costs and tags, and a `run`\n\nfunction. This layer includes\n`compose()`\n\nfor building DAGs programmatically and `execute()`\n\nfor running them.\nIt's usable on its own if you want to build pipelines in TypeScript.\n\n**The language layer** sits on top. It has a lexer, parser, interpreter, and\nsignature analyzer. The parser produces an AST, the interpreter walks it and\ncalls into the op registry, and the signature analyzer walks it without\nexecuting to produce a `Signature`\n\n. Programs are `.safescript`\n\nfiles with the\ncustom syntax described above.\n\nThe **op registry** bridges the two layers. It maps string op names (as they\nappear in safescript source) to `OpEntry`\n\nobjects that know which fields are\nstatic and how to create the underlying `DagOp`\n\n. The builtin registry covers all\nthe ops listed above. Custom registries can be passed to both `interpret()`\n\nand\n`computeSignature()`\n\n.\n\nThe **execution context** (`ExecutionContext`\n\n) provides the external world:\n`fetch`\n\n. It's injected via `AsyncLocalStorage`\n\nso ops access it through\n`getContext()`\n\nwithout passing it as an argument.\n\n## Usage\n\n``` js\nimport {\n  builtinRegistry,\n  computeSignature,\n  interpret,\n  parse,\n  tokenize,\n} from \"safescript\";\n\nconst source = `\n  fetchData = (userId: string) => {\n    body = jsonStringify({ value: { userId } })\n    result = httpRequest({\n      host: \"api.example.com\",\n      method: \"POST\",\n      path: \"/lookup\",\n      body\n    })\n    return result\n  }\n`;\n\n// Parse\nconst program = parse(tokenize(source));\n\n// Static analysis (no execution, no context needed)\nconst sig = computeSignature(program, \"fetchData\");\nconsole.log(sig.hosts); // Set { \"api.example.com\" }\nconsole.log(sig.dataFlow); // param:userId flows to host:api.example.com, etc.\n\n// Execute (requires context)\nconst result = await interpret(program, \"fetchData\", { userId: \"alice\" }, {\n  fetch: globalThis.fetch,\n});\n```\n\n### Testing safescript programs\n\nTests are written in safescript itself. Use `assert`\n\nfor checks, `override`\n\nto\nmock side effects, and `safescript test`\n\nto run:\n\n``` js\nimport { createDocument } from \"../scripts/create-document.ss\"\n\nmockHttpRequest = (host: string, method: string, path: string, ...) => {\n  return { status: 201, body: \"{\\\"document\\\":{\\\"id\\\":\\\"mock\\\"}}\" }\n}\n\ntestCreateDocument = () => {\n  result = override(createDocument, { httpRequest: mockHttpRequest })(\"Test\", \"Content\", id)\n  assert({ condition: result.status == 201 })\n  return { ok: true }\n}\n```\n\n`safescript test`\n\nruns all zero-input functions and reports pass/fail:\n\n```\nsafescript test tests/create-document-test.ss\n# ok  testCreateDocument\n# 1/1 passed\n```\n\nFor static analysis assertions, use the programmatic API:\n\n``` js\nimport { computeSignature, parse, tokenize } from \"@uri/safescript\";\n\nconst program = parse(tokenize(source));\nconst sig = computeSignature(program, \"main\");\nassertEquals(sig.hosts.size, 0);\n```\n\n## Transpilers\n\nsafescript programs can be transpiled to runnable TypeScript or Python. The transpilers emit self-contained code with a full runtime preamble that implements all built-in operations. No safescript runtime dependency is needed to run the output.\n\n### TypeScript\n\n``` js\nimport { parse, tokenize, toTypescript } from \"safescript\";\n\nconst source = `\n  greet = (name: string): string => {\n    msg = stringConcat({ parts: [\"hello, \", name] })\n    return msg\n  }\n`;\n\nconst program = parse(tokenize(source));\nconst tsCode = toTypescript(program);\n// or: toTypescript(program, \"greet\") to emit only one function\n```\n\nThe output uses the Web Crypto API for all crypto operations. Each function\ntakes its parameters as a `Record<string, any>`\n\nplus an `ExecutionContext`\n\nfor\nIO ops (`httpRequest`\n\n). Functions that only use pure ops still require the\ncontext parameter for a uniform interface.\n\n### Python\n\n``` js\nimport { parse, tokenize, toPython } from \"safescript\";\n\nconst program = parse(tokenize(source));\nconst pyCode = toPython(program);\n// or: toPython(program, \"greet\") to emit only one function\n```\n\nThe output uses `asyncio`\n\nfor async execution, `aiohttp`\n\nfor HTTP requests, and\nthe `cryptography`\n\npackage for crypto operations. Functions use Python\nkeyword-only arguments (`*, param1, param2, _ctx`\n\n). Booleans emit as\n`True`\n\n/`False`\n\n, objects as dicts.\n\nBoth transpilers support the `functionName`\n\nparameter to emit a single function.\nIf omitted, all functions in the program are emitted.\n\n## CLI\n\n```\nsafescript run <file.ss> [fn] [--args '{\"key\":\"val\"}']\nsafescript signature <file.ss> [fn]\nsafescript transpile-ts <file.ss> [fn]\nsafescript transpile-py <file.ss> [fn]\nsafescript test <file.ss>\nsafescript skill <file.ss>\n```\n\n### Running programs\n\n```\nsafescript run script.ss                    # auto-detects main() or first fn\nsafescript run script.ss greet --args '{\"name\":\"World\"}'\n```\n\n### Testing\n\n`safescript test <file.ss>`\n\nruns every zero-input function and reports\npass/fail. Use `assert`\n\nand `override`\n\nto mock side effects — no TypeScript\nwrapper needed:\n\n``` js\nimport { createDocument } from \"../scripts/create-document.ss\"\n\nmockHttpRequest = (host: string, ...) => {\n  return { status: 201, body: \"{\\\"document\\\":{\\\"id\\\":\\\"mock\\\"}}\" }\n}\n\ntestCreateDocument = () => {\n  result = override(createDocument, { httpRequest: mockHttpRequest })(\"Test\")\n  assert({ condition: result.status == 201 })\n  return { ok: true }\n}\n```\n\nRun with: `safescript test tests/some-test.ss`\n\n### Generating documentation\n\nUse `doc()`\n\nannotations in your safescript source, then generate markdown:\n\n``` js\ndoc({ text: \"My module description...\" })\n\nmyFn = (x: string): string => {\n  doc({ target: myFn, text: \"Takes a string, returns it.\" })\n  return x\n}\nsafescript skill script.ss > SKILL.md\n```\n\nModule-level `doc({text: ...})`\n\nand function-targeted\n`doc({target: fn, text: ...})`\n\nare both supported.\n\n## What this doesn't do\n\nsafescript is not a general-purpose language. You can't write a web server in it or sort a list. There's no recursion, no unbounded loops, no dynamic dispatch. It's a language for writing agent skills that interact with APIs and inputs in a way that can be formally reasoned about.\n\nIf you need Turing-completeness, use a real language and accept the security tradeoffs. If you need provable safety with useful capabilities, this is the trade you make.", "url": "https://wpnews.pro/news/safescript-a-language-for-ai-era", "canonical_source": "https://safescript.cc/", "published_at": "2026-05-28 02:10:59+00:00", "updated_at": "2026-05-28 02:27:37.988201+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-infrastructure", "ai-tools"], "entities": ["Safescript", "Docker", "Firecracker", "E2B", "macOS", "Ubuntu", "Deno", "npm"], "alternates": {"html": "https://wpnews.pro/news/safescript-a-language-for-ai-era", "markdown": "https://wpnews.pro/news/safescript-a-language-for-ai-era.md", "text": "https://wpnews.pro/news/safescript-a-language-for-ai-era.txt", "jsonld": "https://wpnews.pro/news/safescript-a-language-for-ai-era.jsonld"}}