Let Strangers Run Code on Your Server. On Purpose.

Let Strangers Run Code on Your Server. On Purpose.

Part of our security series — the mechanism behind this is dissected in The Only Sandbox Your AI Agent Cannot Break Out Of, and you can attack the live one at the Natural Language API.

I am going to publish an HTTP endpoint, open to the public, with no key and no account, that runs whatever code a stranger sends it. Hyperlambda, or a sentence of plain English it turns into Hyperlambda. Then I am going to walk away and not think about it again.

That should be a resignation letter. Here it is a dashboard tab.

The reason it is safe has nothing to do with trusting the caller, and nothing to do with a clever prompt asking the code to behave. It is that the runtime cannot do what I did not allow. The entire permission system is a list of function names, and a function whose name is not on the list does not exist as far as the caller's code is concerned. This article builds one, end to end, and reads the file it generates.

The Sandbox API tab in Magic's Endpoint Generator, showing the input selector, the everybody base vocabulary editor, a per-role rule with a row of capability wizards, and the headless browser wizard open

The new tab

Open the Endpoint Generator and there is a fourth tab next to CRUD backend, SQL endpoint, and Import API: Sandbox API. The other three turn your things into endpoints - your tables, your SQL, somebody else's OpenAPI spec. This one is different. It generates a single endpoint that runs code its callers supply, safely, inside a sandbox you shape.

It is the same machinery behind the public Natural Language API that has been taking hostile input from the internet for months, generalised into something you configure and mount wherever you like.

Step one - decide what it accepts

The Input selector has three settings, and your choice changes the arguments the generated endpoint declares:

  • Hyperlambda - the endpoint takes a [hyperlambda] argument and runs it as-is.
  • Natural language - the endpoint takes an [instruction] argument, sends it to the Hyperlambda Generator for code, and runs the result.
  • Both - it accepts either; a sentence wins when present, otherwise the raw Hyperlambda.

I picked Both, set the verb to POST and the URL to modules/public/ask, and left Who may call on public - anyone. That word means what it says: no authentication gate on the endpoint at all.

Step two - the base vocabulary

The middle of the tab is where the sandbox is defined, and it starts with an Everybody (base vocabulary) box - the language every caller gets no matter who they are. Magic seeds it for you with a safe set: control flow, string and math helpers, the type converters, the validators. No database, no file system, no HTTP. It is the vocabulary from the public demo, minus the two things I would not hand a stranger by default.

For this endpoint I want anonymous callers to read one sample database and nothing else, so I add two lines to the base box, with the editor's autocomplete offering every real function as I type:

data.connect:chinook
data.read

That first line is the whole game. data.read on its own grants reading. data.connect:chinookpins the argument - it grants connecting to exactly the database named chinook, and every other connection string in the world becomes a thrown exception. The unit of authority stopped being "may query a database" and became "may query this database". File and folder functions pin their path the same way: io.file.load:/etc/reports/ grants loading exactly that path.

The editor earns its keep here. It knows every function registered on the server, so it autocompletes as you type - and when you hover a name, it tells you what that function does. You are reading the capability you are about to grant, in a sentence, before you grant it.

The base vocabulary editor with data.connect:chinook added, the cursor hovering the data.connect function, and a tooltip reading 'Opens a database connection using the configured provider'

I set the base rate limit to ten calls per minute per IP, and the timeout to twenty seconds. A public endpoint that runs code needs both, and they are right there next to the vocabulary.

Step three - generate, and read what it wrote

Click Generate and the read-only preview fills with the endpoint. This is worth reading, because there is no magic in it - just three moves.

First, everything the endpoint does is driven by a [.policy] block at the top. This is the only part that varies between generated endpoints; the logic beneath it is identical in all of them.

.policy
   override-root:bool:false
   return-code:bool:false
   generator-url:"https://ainiro.io/magic/modules/hyperlambda-generator/chat"
   throttle-prefix:sandbox-public-ask-post
   rules
      .
         role:*
         limit:int:10
         window:int:60
         timeout:int:20000
         slots
            add
            get-value
            get-nodes
            set-value
            return-value
            if
            eq
            for-each
            yield
            strings.concat
            math.add
            data.connect:chinook
            data.read

Second, before a single line runs, the code is checked against the functions that actually exist on this server. A hallucinated name - the model inventing strings.reverse because it felt plausible - dies here, with a clear verdict, before anything executes:

hyperlambda.verify-slots:x:@.code
if
   not
      get-value:x:@hyperlambda.verify-slots
   .lambda
      set-value:x:@.state
         .:unknown-slot
      set-value:x:@.reason
         strings.concat
            .:"The code references functions that do not exist on this server: "
            strings.join:x:@hyperlambda.verify-slots/*
               .:", "

Third, the code that survived that check runs inside [whitelist], with a vocabulary assembled from the caller's roles and nothing else:

.vocabulary
for-each:x:@.policy/*/rules/*
   if
      or
         eq:x:@.dp/#/*/role
            .:*
         auth.ticket.in-role:x:@.dp/#/*/role
      .lambda
         add:x:@.vocabulary
            get-nodes:x:@.dp/#/*/slots/*
hyper2lambda:x:@.code
add:x:./*/whitelist/*/.lambda
   get-nodes:x:@hyper2lambda/*
add:x:./*/whitelist/*/vocabulary
   get-nodes:x:@.vocabulary/*
whitelist
   vocabulary
   .lambda

A function that is not in that vocabulary does not resolve. Not "is rejected by a filter" - does not dispatch, because the runtime checks the name against the list before every single statement, and there is no second road to dispatch. That check, and why it beats scanning text for dangerous strings, is the subject of a whole separate article; here we just use it.

The endpoint always answers with a [state] and a [result], and a [reason] when there is something to explain.

Step four - be the stranger

Deploy it and start poking. First, a benign question in English - the endpoint sends it to the generator, verifies the code, and runs it:

The endpoint's response to a plain-English instruction, showing state executed, the generated Hyperlambda, a null reason, and the result

Now the read it is allowed:

curl -sX POST https://your-cloudlet/magic/modules/public/ask \
  -H 'Content-Type: application/json' \
  -d '{"instruction":"the 5 first artists from the Artist table"}'
{ "state": "executed", "result": [ { "ArtistId": 1, "Name": "AC/DC" }, ... ] }

Now the attack. Ask it to read the server's configuration - the file with the secrets in it:

curl -sX POST https://your-cloudlet/magic/modules/public/ask \
  -H 'Content-Type: application/json' \
  -d '{"hyperlambda":"io.file.load:/config/appsettings.json"}'
{
  "state": "rejected",
  "reason": "Slot [io.file.load] doesn't exist in current scope, or argument not allowed"
}

io.file.load was never in the vocabulary, so it never existed. And a made-up function:

{
  "state": "unknown-slot",
  "reason": "The code references functions that do not exist on this server: strings.reverse"
}

Three refusals, three different reasons, and not one of them ran. The caller even gets told precisely why - which is the difference between a sandbox and a black box.

Step five - now scope it by role

A public read-only endpoint is the easy case. The interesting one is handing different callers different power from the same URL. That is what the per-role rules are for.

Add a rule, choose a role - say writer - and instead of typing function names, use the wizards. The Add capability row offers Database, HTTP, Files, Email, Images, Logging, Tasks, Cache, Sockets, Identity, Crypto and a headless Browser. Open the Database wizard, pick chinook, tick Create and Update, and click Add. It writes the pinned lines for you:

      .
         role:writer
         slots
            data.connect:chinook
            data.create
            data.update

The vocabulary a caller runs with is the union of every rule they match, so a writer keeps the entire base language and the read grant and now these - while an anonymous caller still gets only the base. That is how you give one role a database another role cannot touch: the CEO's role gets a rule granting data.connect:finance, the HR role does not, and both keep everything else.

The rate limit and the timeout resolve independently and take the first matching rule, top to bottom, that declares them - so you order rules most-privileged first and never have to reason about a maximum. The base rule sits last as everyone's fallback. Root is the exception to all of it: root is trusted completely, never sandboxed, throttled, or timed out.

It is also an MCP tool, and it describes itself

Because the generated file is an ordinary Hyperlambda endpoint, Magic publishes it as an MCP tool with no second step. The generator writes the file's top comment to match the input mode, and a comment above each argument - which is exactly what an agent reads as the tool's description and its arguments':

/*
 * Sandboxed Hyperlambda endpoint, generated by the Sandbox API generator.
 *
 * Accepts caller-supplied Hyperlambda in [hyperlambda], or a natural-language
 * [instruction] the Hyperlambda Generator turns into code.
 * ...
 * To discover the capability surface available to you - every function this sandbox
 * will let you call - ask it to "Return server vocabulary", or send Hyperlambda that
 * invokes [vocabulary].
 */
.arguments

   // A plain-language description of what you want done, turned into Hyperlambda for you.
   instruction:string

   // Hyperlambda to execute as-is inside the sandbox.
   hyperlambda:string

That comment does something small and useful: it tells the calling model how to enumerate its own cage. Ask the tool to "Return server vocabulary" and it lists exactly the functions this particular sandbox permits - so an agent can learn what it may do before it tries.

Break it yourself

I have left one of these running, and here is the token to attack it.

It is a guest-only endpoint, and it grants the guest role exactly one capability: read the Chinook sample database. Nothing else. The token below carries the guest role - the role anyone who signs in through OIDC is assigned automatically, so it is completely harmless to hand out. The challenge is simple: make it edit, change, or insert a single row.

TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InNlcnZpY2UiLCJyb2xlIjoiZ3Vlc3QiLCJuYmYiOjE3ODk5NjY1OTMsImV4cCI6MTgyMTQ4NDgwMCwiaWF0IjoxNzg5OTY2NTkzfQ.oLZAnbE-RFXsA1CND_1U600LDlHoeGlVJG1ov-HFpyM"
URL="https://hyperlambda.dev/magic/modules/sandbox-challenge/ask"

Read five rows - in plain English. This is allowed, so it runs:

curl -sX POST "$URL" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"instruction":"return the 5 first artists from the Artist table in the chinook database"}'

Delete everything - in plain English. The generator happily writes the data.delete; the sandbox refuses to dispatch it:

curl -sX POST "$URL" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"instruction":"delete every row in the Artist table of the chinook database"}'
{ "state": "rejected", "reason": "Slot [data.delete] doesn't exist in current scope, or argument not allowed" }

Delete everything - as raw Hyperlambda. Skip the model and send the code yourself. Same wall:

curl -sX POST "$URL" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"hyperlambda":"data.connect:chinook\n   data.delete\n      table:Artist"}'

data.delete was never granted to guest, so as far as this endpoint is concerned it does not exist. data.create, data.update, and raw data.select hit the same wall - try them.

Ask it what it is allowed to do. The endpoint reports its own permitted vocabulary - the exact set of functions it will dispatch for you:

curl -sX POST "$URL" -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"hyperlambda":"vocabulary\nreturn-nodes:x:@vocabulary/*"}'
{
  "state": "executed",
  "result": [
    "add", "and", "convert", "eq", "for-each", "get-value",
    "if", "return-nodes", "strings.concat", "math.add",
    "data.read", "data.connect"
  ]
}

That list (abridged here) is the whole of the caller's power. data.read is on it; not one writing function is. If you can make a row change anyway, I owe you $100.

Know what you are granting

The wizards make granting a capability a two-second job, and that is exactly why one of them is worth slowing down for.

The Database wizard has a Raw SQL toggle. Leave it off and the wizard grants the four CRUD verbs - data.read, data.create, data.update, data.delete - each a bounded, parameterised operation. Turn it on and it adds data.select and data.scalar: arbitrary SQL against the pinned database. That is a different order of power. data.connect:chinook still pins which database, but not what the caller may do inside it - a data.select grant is a read, a write, or a schema change depending on the statement that arrives, bounded only by what the database account itself is allowed to do. Do not hand it to a role you would not hand a database console. For almost every endpoint the CRUD verbs are what you want; reach for raw SQL only when you genuinely need it, and only for a role you trust.

Which is the honest thing to say about this whole component. It makes the boundary enforceable and legible, but it does not make the decision for you. Publishing a safe public API still takes basic IT and security literacy - knowing what a function actually does, what a database account is permitted to do, why a parameterised read is not the same as a raw query, and what the word public means when it sits above an endpoint that runs code. Magic guarantees the caller cannot exceed the vocabulary you grant. It cannot know whether the vocabulary you granted was a wise one. That part is still your job, and it is worth learning before you point this at the open internet.

The honest edges

A security claim with no caveats is marketing.

Secure is not the same as correct. The sandbox proves what the code cannot do. It cannot make wrong logic right - a bad query returns a bad answer, executed perfectly safely. That is what the result panel is for.

Root opts out of everything. The Root bypasses the sandbox checkbox, when on, lets a root caller run with the full server vocabulary and no whitelist. That is deliberate - an administrator who cannot administer is a broken account - but it means you must not connect an agent using root credentials and expect any of this to protect you. Give agents their own user and their own role.

Pins are exact.data.connect:chinook grants that database and no other, but it is a string match, not a prefix - it does not grant "every database starting with chinook". Same for file paths.

I cannot vouch for the layers beneath. Linux, SQLite, .NET - a hole in any of those is a hole in this. I am certain about the boundary I wrote, which is the application layer, and honest that it is not the only one.

Try it

One command runs the whole platform locally:

curl -fsSL https://hyperlambda.dev/docker-compose.yaml | docker compose -f - up

Open localhost:5555, point it at localhost:4444, log in with root / root, and open the Endpoint Generator's Sandbox API tab. Add data.connect:chinook and data.read to the base vocabulary, generate, and send it a sentence. Then send it something it should refuse, and watch it refuse.

Magic is MIT licensed and open source - the repository is at github.com/polterguy/magic, the documentation at docs.ainiro.io. If you would rather someone else kept it patched, a managed cloudlet runs the same code.

Most platforms would never let a stranger run code on their server. This one hands them a language they cannot add a single word to.

Frequently asked questions

What does the Sandbox API generator create?

A single HTTP endpoint that executes code its own callers supply - Hyperlambda, or a plain-English instruction turned into Hyperlambda - inside a whitelist you define. Callers reach only the functions you granted, a function you did not grant does not exist to their code, and a hallucinated function is refused before anything runs.

How do I give different callers different capabilities?

You build the policy from an "everybody" base vocabulary plus per-role rules that add functions on top. A caller runs with the union of what their roles grant, so one role can reach a database another role cannot. Rate limit and timeout are set per role too, resolved first-match with the base as the fallback.

Can I restrict access to a single database or file?

Yes. A function's primary argument can be pinned by writing it as a value - data.connect:chinook grants exactly that database and turns every other connection string into a thrown exception, and io.file.load:/etc/reports/ grants exactly that path. The capability wizards write these pinned lines for you.

Do I still need security knowledge to use it safely?

Yes. Magic guarantees a caller cannot exceed the vocabulary you grant, but it cannot judge whether that vocabulary is wise. Granting raw SQL through the Database wizard's Raw SQL toggle adds data.select, which is arbitrary SQL against the pinned database - far more power than the CRUD verbs. Building a safe public API still takes basic IT and security literacy.

Is a generated Sandbox API endpoint an MCP tool?

Yes. Every endpoint in a Magic cloudlet is published as an MCP tool. The generator writes the endpoint's file comment - which becomes the tool's description - to match its input mode, and the comment tells the calling model to send "Return server vocabulary" to discover exactly which functions the sandbox permits.