Reference›The sandbox
Reference
The sandbox
The sandbox runs an Axorum world on your own machine: the agent plane your agent
already talks to, a clock that moves only when your test moves it, and the
ledger to assert on afterwards. It is the policy engine axorum serve runs,
with no cluster, no certificates and no wall clock, so an agent's whole
lifecycle (a payment, the duty it leaves, the week that passes, the breach)
runs in a test in milliseconds.
A run reads no clock and no random source. One world and one seed give one run,
on every machine, which is what lets a recorded run replay in CI.
The examples use a locally installed axorum-sandbox. Commands that talk to
a running sandbox use http://127.0.0.1:7340 unless --url orAXORUM_SANDBOX_URL selects another address.
Getting it
| npm | pnpm add --save-dev @axorum/sandbox @axorum/client |
| PyPI | pip install axorum-sandbox axorum-client |
Both carry the native axorum-sandbox command for Linux on x64 and arm64 (a
static binary, so any libc), macOS on x64 and arm64, and Windows on x64. The
npm package runs it as pnpm exec axorum-sandbox; the wheel puts it on the
environment's PATH. The npm package also runs the sandbox in-process, in Node
and the browser, as WebAssembly.
@axorum/client and axorum-client are the SDKs your agent already uses. The
sandbox answers them exactly as a deployment does: nothing in the agent changes
but the address it sends to.
Skills for Claude Code
Three agent skills bring Axorum to Claude Code. axorum-policy takes you from
controls in your own words to a policy that checks clean and is proven on your
own examples. axorum-world builds the sandbox world that policy is tested
against. axorum gives an agent that moves money through an Axorum ledger the
judgment to treat a refusal as a verdict, not an error. They install as one
plugin:
1claude plugin marketplace add AxorumHQ/skills2claude plugin install axorum@axorum3
github.com/AxorumHQ/skills lists what
each skill needs and how to copy them into ~/.claude/skills by hand.
Finding your way around the CLI
axorum-sandbox -h shows a short command list and the first local workflow.axorum-sandbox --help adds examples, environment variables, output streams
and exit codes. Every command has its own examples, including nested commands:
1axorum-sandbox help serve2axorum-sandbox publish world --help3axorum-sandbox --version4
For a local run that ignores console credentials in your environment, useserve WORLD --offline or run SCENARIOS --offline. serve --offline needs a
local world and cannot be combined with --console-events. Offline mode does
not change the local agent plane: agents still reach the loopback HTTP server.
Checks, starters, grammar and completion generation always work offline.
Data goes to stdout; status and errors go to stderr. Captured output has no
animation. Slow console operations announce a static phase on an interactive
stderr. Help and parser errors respect NO_COLOR. Human diagnostics escape
terminal control characters; JSON, YAML, tokens and source retain their bytes.
A consumer closing its pipe early ends the producer successfully; any other
stdout write or flush failure exits 2 instead of losing output silently.
Shell completion
Generate completion from the installed command tree for Bash, Zsh, Fish,
PowerShell or Elvish. The command only prints a script, so installation remains
an explicit shell choice. For the current session:
1# Bash2source <(axorum-sandbox completions bash)3# Zsh, after the shell's usual compinit setup4source <(axorum-sandbox completions zsh)5# Fish6axorum-sandbox completions fish | source7# PowerShell8axorum-sandbox completions powershell | Out-String | Invoke-Expression9# Elvish: save under a module name and load it through the shell's module path10axorum-sandbox completions elvish > axorum-sandbox.elv11
Local files and streams
check -, policy - and inject - read piped UTF-8 input. An interactive
stdin is refused with instructions rather than waiting for an unseen EOF.save-run --output - and pull --output - write to stdout, just as omitting--output does. Pulling a world that names a separate policy still needs an
output file so the policy can be written beside it.
new, pull --output FILE and save-run --output FILE refuse existing output
files unless --force is explicit. For pull, this also protects the policy
beside the world. Replacements are staged beside their destinations, and a
forced symlink replacement replaces the link itself, leaving its referent
alone. Existing regular files retain their permissions; new files use temporary-file
defaults, with owner-only permissions on Unix. Every target is checked and every document staged before
publication, with policy published before world. Each file is replaced
atomically; the pair is not a filesystem transaction. A late filesystem error
names any files already written and explains how to finish the operation.
1axorum-sandbox save-run --output run.yaml --force2axorum-sandbox pull --output worlds/world.yaml --force3cat event.yaml | axorum-sandbox inject -4axorum-sandbox save-run --output - > run.yaml5
Shell redirection is controlled by the shell and can overwrite its target;
use --output FILE when you want the CLI's replacement protection. Sandbox
URLs may include a reverse-proxy path prefix. Credentials, query strings and
fragments are refused without repeating them. HTTP redirects are never
followed, so policy and event bodies are not forwarded to another address.
serve drains on Ctrl-C and, on Unix, SIGTERM. A second interrupt skips
remaining HTTP and console delivery waits. It waits at most five seconds
for HTTP requests and then at most ten seconds for a final console report.
An incomplete client request cannot hold shutdown open indefinitely.
A world
A world is a YAML file: the policy, the state it governs, and a timeline of
events that change that state. Start from a starter:
1$ axorum-sandbox new2agent-payments An agent that pays for the APIs it uses.3purchase-receipts An agent that buys on a card.4daily-budget An agent on a budget.5spending-mandate An agent spending under a mandate.6purchase-rate-limit An agent held to a pace.7$ axorum-sandbox new agent-payments8wrote agent-payments.world.yaml and agent-payments.axp; serve it with `axorum-sandbox serve ./agent-payments.world.yaml`9
| Starter | What it shows |
|---|---|
agent-payments | An agent paying for the APIs it uses, held to a vendor registry's approvals and per-payee limits, until a review a week in suspends a vendor. |
purchase-receipts | A purchase over $500 leaves the agent a duty to file its receipt within a day; a forgotten one breaches, and the exceptions report lists it. |
daily-budget | An agent on a budget: no purchase over $50, and no more than $100 in any rolling day; a day on, the budget is free again. |
spending-mandate | An agent spending under a mandate: $1,000.00 in all, carved from a $5,000.00 team budget; a purchase past it is refused by the mandate and recorded, and the mandates report shows what each mandate has spent and what is left. |
purchase-rate-limit | An agent held to 5 purchases in any rolling hour, so one stuck in a loop stops at the sixth; an hour on, it may buy again. |
The parts of a world file, in the order they are written:
| Key | What it holds |
|---|---|
version | Always axorum.sandbox-world.v1. |
world | The world's name. |
description | What it is for, in a sentence or two. |
seed | The seed every key and token derives from, unless a run is given another. |
policy | The policy in force from the start: {file: agent-payments.axp} names a file beside the world, {source: ...} holds the text. |
currencies | Each currency amounts are written in, with its minor-unit exponent. |
accounts, mandates, deferrals, usages | The ledger's accounts and balances, opened in order. A mandate carved from another names it as its parent, opened earlier; a mandate a policy reads by name (projected(buyer_mandate)) is opened under the id that name derives, as a party's is. An account's or a mandate's label names it in the reports; a mandate's label also names it in a refusal's explanation, ahead of the policy's name for it. |
parties | The agents the world knows: each agent's URI, its party, its roles and the capabilities its tokens carry. |
master_data | What the policy reads about the world, such as the approved counterparties and their limits. |
rails | How each mocked payment rail answers: settle, decline or timeout. A rail not named settles. |
events | The timeline: changes applied when the clock reaches their at. |
Every key, the rules a world is held to and the defaults filled in are in the
format's reference, each with an example. The installed sandbox prints it:
1$ axorum-sandbox grammar world2
An event is one of four changes, written under its kind:
1events:2- at: 7d3 master_data: # replace one master-data domain whole4 Counterparties:5 source_version: vendors-v26 entries:7 - - api.anthropic.com8 - display_name: Anthropic9 approved: false10- at: 8d11 rail: # change how a rail answers, for `times` calls or for good12 rail: x40213 answer: decline14 times: 115- at: 14d16 policy: # put another policy in force17 file: agent-payments-v2.axp18- at: 30d19 advance: 1d # move the clock further20
A policy event holds the policy's text under source, or, in a world file,
names a file beside the world under file, read when the world loads. A
policy that does not compile fails the load. A world you publish to the
console gives each change by source: the console keeps the world alone,
with no file beside it. What the change does when it comes due is
Changing the policy of a running sandbox.
Sandbox time is counted in whole seconds from the start of the run, written90s, 36h, 1d12h30m or 2w. The clock starts at 2100-01-01T00:00:00Z, so a
timestamp from the sandbox can never be mistaken for a real one.
Checking a policy
axorum-sandbox check compiles a policy offline and says what it means, asaxorum policy check does: the same diagnostics, the sameDSL2xxx lints, the same plain-English rendering and the same exit codes. It
loads and runs nothing.
1$ axorum-sandbox check agent-payments.axp2
With --world, it also judges the policy against the world it will run in.
The world's own policy is ignored; the one checked is the file named first.
1$ axorum-sandbox check spending-mandate.axp --world spending-mandate.world.yaml2
A mandate term and an agent's binding are both by id, and each id derives from
a name the policy writes, so a world written by hand can miss them. The check
names each id, and against a world it finds:
| Code | Severity | What it means |
|---|---|---|
DSL3001 | error | The policy names a currency the world does not declare: the sandbox refuses every intent in it. |
DSL3002 | warning | A currency the world declares that an action can never pay in. |
DSL3003 | warning | The policy reads a mandate the world does not open under the id its name derives: every term over it reads 0, and an intent that charges it is refused. |
DSL3004 | error | The policy caps a mandate at an amount in another currency than the world opens it in: no intent that reaches the cap can be judged. |
DSL3005 | warning | An agent party no world agent is bound to under the id its name derives: no intent acts as it. |
DSL3006 | warning | A world agent bound to a party the policy does not declare as an agent: every intent it submits is refused. |
DSL3007 | warning | A world agent bound in roles other than exactly the ones the policy gives its party: without one of them, so no rule written for that role applies to it; with a declared role the party does not hold, so the rules written for that role reach it while the lints read the party without it (the duties they set and a per role register always, their permissions and prohibitions under a delegation that scopes the role for the action, which then also covers its specific(...) rules); or with a role the policy does not declare, which no rule or delegation can name. |
DSL3008 | warning | A world agent whose capabilities cannot carry an action the policy delegates to its party: the gateway refuses every such intent (403) before the policy is consulted. |
DSL3009 | warning | A rule's specific(...), as its actor or its performed_by, names a party the policy does not declare and no world agent is bound to: the rule applies to no intent (a misspelled forbidden specific(...) forbids nothing), or no intent discharges the duty. The help names the closest declared or bound party. |
A principal is never bound, so a world that binds no agent to one draws
nothing. The engine judges an agent by the roles its binding names, not the
ones the policy gives its party, so a binding that drops or misspells a role
leaves the rules written for that role silent and its intents denied by
default. A binding that adds a declared role its party lacks brings the rules
written for that role to the agent, while the policy's lints read the party
without it: the duties they set and a per role register always, and their
permissions and prohibitions where a delegation to the agent scopes the role
for the action, which then also covers its specific(...) rules.
So a lint finding may not hold for that agent: the help binds the party's
roles, or gives the party the role in the policy. A token that leaves
out an action the policy never delegates to the agent draws nothing. These are warnings,
not errors, because a world may hold an agent on purpose to watch it be
refused. Here the policy calls its agent purchaser
and the world still binds the id buyer_bot derives:
1$ axorum-sandbox check spending-mandate.axp --world spending-mandate.world.yaml -q2The policy compiles: 1 role(s), 1 action(s), 2 part(y/ies), 1 delegation(s), 0 register(s), 1 rule(s). Nothing was registered.3It takes 2346 of the 524288 characters a policy may take of a run's snapshot (its rule table takes 222, its document 1996 and its parties 128) and 2445 of the 1048576 bytes a policy may take of a run's snapshot as the sandbox sends it (its rule table takes 236, its document 2065 and its parties 144).45It declares 1 agent party no agent is bound to in the world:67 warning[DSL3005]: agent party `purchaser` has no agent bound to it in the world under the id the policy reads (party_4h75hwkz86b9grytspxahdahny), so no intent acts as it and its delegations go unused8 --> 10:99 |10 10 | party purchaser agent roles(buyer)11 | ^^^^^^^^^12 = help: bind an agent to it in the world's `parties` with `party: party_4h75hwkz86b9grytspxahdahny`1314151 agent in the world is bound to a party it does not declare as an agent:1617 warning[DSL3006]: `agent://example.com/agent/buyer_01h455vb4pex5vsknk084sn02q` is bound in the world to party party_6pfhxzyav7atbvnxa2wh4wdsex, which the policy does not declare, so no delegation can name it: every intent it submits is refused18 --> 5:119 |20 5 | policy {21 | ^^^^^^22 = help: bind it to an agent party the policy declares, by id: purchaser (party_4h75hwkz86b9grytspxahdahny)23
stdout carries the verdict as one line of JSON and nothing else; stderr carries
the prose. The document holds the policy's id and rendering, the mandates it
reads and the parties it declares with the id each derives to (both in name
order, the order the engine stores them in), every warning with its code and
span, and whether the world's currencies, mandates and parties were checked.
For the run above, with the rendering and each warning's detail cut:
1{"policy": "pol_68f2t6zs66a2sv2x1p603ja9kx", "rendering": "...",2 "mandates": [],3 "parties": [{"name": "overseer", "kind": "principal", "party": "party_4615mj344tawh8d811464x204d"},4 {"name": "purchaser", "kind": "agent", "party": "party_4h75hwkz86b9grytspxahdahny"}],5 "warnings": [{"code": "DSL3006", "severity": "warning", "...": "..."},6 {"code": "DSL3005", "severity": "warning", "...": "..."}],7 "currencies": [],8 "currencies_checked": true, "mandates_checked": true, "parties_checked": true,9 "registered": false}10
The world is read as serve and run read it, and each policy change its
timeline schedules is judged against it as well, as the change is made when
it comes due: a change named by file is read first, from the world's
directory joined with that name. The first change that does not compile, or
that the world refuses (an undeclared currency, a cap in another currency than
its mandate), fails the check, exit 3, naming the event counted from one; so
does a change file that cannot be read. A change's lint warnings are neither
printed nor counted, --deny-warnings included: a change made mid-run, such as
a freeze, is often one the linter flags, and the sandbox makes it all the same.
Only the policy named first is linted. A script step's change is not judged,
as it records what a run was sent, refused or not.
A policy that compiles is also measured, and the check says under its
verdict what it takes of a run's snapshot. The snapshot carries the policy
three times over: its rule table, its document (its source, each rule's
paragraph, and the setup and every paragraph again as text) and its parties.
A policy may take half of what a run may, in both measures a run has: 524,288
of the 1,048,576 characters the console keeps of a run, and 1,048,576 of the
2,097,152 bytes the sandbox sends for one. Bytes are counted as sent, so a
character past ASCII takes two to four, and a quote, a backslash or a control
character takes more, as it is escaped twice. That leaves the other half for
the run's steps, timeline, journal page, master data, agents and other
reports, and those grow as the run goes. Each refusal writes the refusing
rule's paragraph, its text values included, twice into the activity report
and twice into the exceptions report, so a rule with a long text value costs
that text again at every refusal it makes. A policy near its share whose
longest rule refuses often can take a run past what the console keeps; such
a run is reported without its snapshot, as described below. A policy that
takes more in either measure is an error, DSL3010,
with or without --world, that names what each part takes in that measure,
and a scheduled change is held to the same share. publish refuses such a
policy, and the console refuses to save it. Each rule's paragraph, its text
values included, is written three times, so cutting rules, or shortening
their names, conditions and text, brings a policy under. A policy within the
console's 262,144 characters of source can still take more than its share.
The share is measured for a policy in force from a run's start; one put in
force later takes at most 20 characters more, for the clock beat its document
names. Without --world, amounts are written in minor units, so the share
differs a little from the one the world's currencies give.
-q leaves out the rendering and the tables but never a warning, since a
warning is part of the verdict. --print adds the canonical layout the engine
stores. The check exits 0 when the policy passes and 3 when it does not
compile, when the world cannot run it or a policy change its timeline
schedules, when --deny-warnings refuses a warning, or when a file cannot be
read; on a failure stdout stays empty. In
Python, check(policy, *, world=None, deny_warnings=False) answers the same document
and raises SandboxError on a failure.
Testing an agent in TypeScript
In Node, the sandbox runs in the test's own process. sandbox.fetch answers the
agent plane, so the agent's own client takes it in place of the network:
1import { readFileSync } from "node:fs";2import { AxorumClient } from "@axorum/client";3import { Sandbox } from "@axorum/sandbox";45const sandbox = await Sandbox.load(readFileSync("agent-payments.world.yaml", "utf8"), {6 policy: readFileSync("agent-payments.axp", "utf8"),7});8const buyer = "agent://example.com/agent/buyer_01h455vb4pex5vsknk084sn02q";9const axorum = new AxorumClient({ baseUrl: "http://sandbox", attestation: sandbox.token(buyer), fetch: sandbox.fetch });10
The agent then runs as it would in production. Between its actions the test
moves the clock (sandbox.advance("7d")), applies an event now
(sandbox.inject(...)), puts another policy in force
(sandbox.policy(source)), and reads back what happened: sandbox.ledger(),sandbox.obligations() and sandbox.report({ kind: "exceptions" }). The
package's README lists every call.
Testing an agent in Python
In Python, the sandbox is the native command, served on a free port for the
length of a with block:
1from axorum import Agent, Amount, AxorumClient, Payment2from axorum_sandbox import Sandbox34BUYER = "agent://example.com/agent/buyer_01h455vb4pex5vsknk084sn02q"567def test_the_agent_stops_paying_a_suspended_vendor(tmp_path):8 with Sandbox.starter("agent-payments", tmp_path) as sandbox:9 axorum = AxorumClient(sandbox.url)10 agent = Agent(BUYER, lambda: sandbox.token(BUYER))11 pay = lambda minor, payee: axorum.authorize(agent, Payment.new(Amount(minor, "USD"), payee))1213 assert pay(10_000, "api.anthropic.com").allowed14 sandbox.advance("7d") # the vendor review suspends Anthropic15 assert not pay(100, "api.anthropic.com").allowed16
Sandbox(world, policy=..., seed=...) serves a world file of your own;ledger, obligations, report, inject, policy, snapshot and save_run
read and drive the run as their TypeScript namesakes do. A refusal raisesSandboxError, carrying the status and code the sandbox answered.
A token lives one day of the sandbox's clock, so an agent that outlives a move
of the clock takes a token source, as above, rather than one token.
Serving an agent in any language
axorum-sandbox serve answers the agent plane on 127.0.0.1, by default on port
7340 (--port 0 picks a free one), until interrupted:
1$ axorum-sandbox serve agent-payments.world.yaml2sandbox: world agent-payments (run run_01m4d75d33ebyafcj0ycfah37x, seed 1) on http://127.0.0.1:73403 token: axorum-sandbox token --url http://127.0.0.1:7340 'agent://example.com/agent/buyer_01h455vb4pex5vsknk084sn02q'4 routes: the agent plane under /api/v1, the developer's under /_sandbox (Ctrl-C stops)5
Point the agent's client at that address. The routes under /_sandbox are the
test's, and none is an axorum serve route, so none can collide with the agent
plane:
| Route | What it does |
|---|---|
GET /_sandbox/token?agent=<uri> | A token for an agent of the world, issued at the sandbox clock's time, as text. |
POST /_sandbox/advance | Moves the clock by the offset in the body (7d), applying the world's events on the way. |
POST /_sandbox/inject | Applies the world event in the body (YAML) now. |
GET /_sandbox/events | Every world event applied so far, in order, each as a move of the clock answers it, with the engine's refusal when it refused the change. The events applied at start (those at 0s, and any a 0s advance reaches) come first; no move reports them. |
POST /_sandbox/policy | Puts the policy whose text is the body in force now, as below. |
GET /_sandbox/ledger?first=&limit= | A page of the journal, each record tagged by its kind; a refusal is a Transaction with posted: false. Beside the records, opening is each account's balance just before the page (credits less debits), from which each posted Transaction's legs give the running balances, and charges is where each mandate stood at each charge in the page: its ceiling, what it had spent before, and what was left after, for the mandate charged and each one it is carved from. |
GET /_sandbox/obligations | Every duty still open. |
GET /_sandbox/report?kind= | A report as axorum serve renders it: period, exceptions, activity, registers, usage_balance (with usage), mandates or explain (with transaction); from and to take offsets. mandates has no axorum serve route: it answers {"mandates": [...], "text": ...}, each mandate's report in the order they were opened, with its own text. |
GET /_sandbox/run | The run so far, as the scenario that replays it. |
GET /_sandbox/snapshot | The run's axorum.sandbox-snapshot.v1 snapshot. Its reports hold the activity, exceptions, periods, registers, usage balances, every mandate the ledger holds (mandates, in the order they were opened) and the policy in force; it explains no transaction, so ask report?kind=explain for one. Its policies list every policy the run put in force, with when: the world's own at 0s, then each change. Its rules hold the rules of each of those policies once, by policy id and then by rule id, each rule's name and prose, so any rule id a report or a duty names resolves in one lookup under the policy that names it. Its agents list every agent the world binds, then every other agent that submitted, each with its party and name when it has them: an agent whose every intent was turned away appears in no report, and its steps say which of its intents the gateway refused. Each submit step names its agent's canonical URI as agent, beside the intent as sent, so a reader matches a step to its agent without parsing a URI; a step whose intent names text that is no agent URI has none, and counts against the agent listed as that text. Its parties list every party those policies declare, agent or principal, by id, each with the name agents gives it, so a principal no agent acts as is named too. Each step of its steps that changed the world carries its outcome, its timeline lists each scheduled policy change that fired and each other scheduled change the engine refused, with its outcome (a master-data, rail or advance change that applied is in the world's events, so the timeline leaves it out), and engine_version names the axorum-sandbox that recorded it. Its journal is the newest page of the journal, as /_sandbox/ledger serves it, and its master_data is the master data the snapshot names, and no other: each counterparty, general-ledger account and fiscal period that its steps, journal page or reports name, as the last import in the run recorded it, with in_force saying whether it is in force at the snapshot's clock, and each domain's source version. A step names master data only through its references: each entry the policy read deciding the step's intent. A counterparty read resolves the binding of a parameter a counterparty limit(p), a resolved counterparty(p), a per counterparty(p) register or a param(p) in approved counterparties reads, a literal the policy tests with in approved counterparties (a text by its string, a party by its id), or the acting party's id when it tests actor. A chart read looks the subject of in chart of accounts up by its id, and a calendar read finds the fiscal period holding the instant in open fiscal periods tests: now, the tick the intent was decided at, a tick parameter or a literal tick. Every rule that can decide the intent's action is read, reached or not, so a reference may record a read the engine skipped, never miss one it made. A parameter the policy reads no master data from, a memo whatever it spells, names nothing, and so does any other string in a step, so an intent turned away before it is decided (a bad token, a stale pin, an undeclared parameter) names nothing. master_data.references holds what each read found when its intent was decided, a list per kind, and nothing for a read that found nothing. counterparties holds {transaction, param, spelling, counterparty}, with no param for a literal or the acting party: resolve the spelling a step or its journal record binds by that lookup, and a literal or the acting party's id with no param, never through the aliases, so an alias a later import moves to another counterparty still reads as the one it named. gl_accounts holds {transaction, account} for each account found on the chart, and fiscal_periods holds {transaction, at, period} for the period holding each instant looked up: take the period a decision found at at from there, never from the spans in fiscal_periods, which a later calendar can move. A reference names its entry's latest record, so a counterparty's approval, an account's name and a period's openness are as they last stood. The journal page and reports name an entry by its id, by a counterparty:<id> or mandate:<root>/counterparty:<id> scope, or by an account key. A counterparty left out and then named again is the record that named it again. A step that imports master data names none of the entries it carries: the step holds them already. Each entry takes at most 4,096 bytes as the snapshot writes it. The master data the journal page names shrinks with the page when the console's copy is cut to fit; the steps and reports are never cut, so neither are the references or the entries only they name. |
Moving the clock answers what applied on the way:
1$ curl -s -X POST --data 7d http://127.0.0.1:7340/_sandbox/advance2{"clock":"2100-01-08T00:00:00Z","offset":"7d","swept":1,"events":[{"at":0,"kind":"advance"},{"at":604800,"kind":"master_data"}]}3
The other commands reach a running sandbox at --url, or AXORUM_SANDBOX_URL,
or port 7340: axorum-sandbox token <agent>, axorum-sandbox inject <event.yaml>
(an events entry without its at), axorum-sandbox policy <file> andaxorum-sandbox save-run.
The snapshot's schema
Every snapshot meets a JSON Schema (draft 2020-12) whose $id,urn:axorum:schema:axorum.sandbox-snapshot.v1, names it without an address,
since nothing serves it over HTTP. Both packages ship it, byte for byte.@axorum/sandbox exports it as@axorum/sandbox/schemas/axorum.sandbox-snapshot.v1.schema.json, andimport.meta.resolve on the same name gives the file's URL:
1import schema from "@axorum/sandbox/schemas/axorum.sandbox-snapshot.v1.schema.json" with { type: "json" };2
In Python, axorum_sandbox.snapshot_schema() answers it parsed, andaxorum_sandbox.snapshot_schema_path() the file the axorum-sandbox wheel
installed:
1from jsonschema import Draft202012Validator2from axorum_sandbox import snapshot_schema34Draft202012Validator(snapshot_schema()).validate(sandbox.snapshot())5
Changing the policy of a running sandbox
A running sandbox takes a new policy as axorum serve takes one from an
operator: compiled, registered and activated at the sandbox clock's time. From
the next intent on, the new policy decides. axorum-sandbox policy sends a
policy file (- reads stdin), writes the id of the policy in force to stdout,
and says on stderr what changed and when:
1$ axorum-sandbox policy capped.axp2pol_5azgg8k572bfe8e648xqx9kwfg3the policy in force changed from pol_3j9g6k2030a57b2d580e0j0bad to pol_5azgg8k572bfe8e648xqx9kwfg at 1d (2100-01-02T00:00:00Z)4
The same change, as the route answers it:
1$ curl -s -X POST --data-binary @capped.axp http://127.0.0.1:7340/_sandbox/policy2{"policy":"pol_5azgg8k572bfe8e648xqx9kwfg","previous":"pol_3j9g6k2030a57b2d580e0j0bad","changed":true,"clock":"2100-01-02T00:00:00Z","offset":"1d"}3
An inject of a policy event, a policy event on the world's timeline,sandbox.policy(source) in TypeScript and Python, and a policy event from the
console (below) all make the same change.
An agent pinned to the old policy re-pins. Its next intent is refused409 stale_policy, naming the policy it pinned and the one in force, exactly
as a deployment refuses it. authorize in @axorum/client and in the Python
SDK re-pins and sends again on its own, and the new policy decides the retry.
The same policy changes nothing. A policy is known by its content id, which
comments and layout do not change. Sending the policy already in force
answers "changed": false, adds nothing to the policy history, and stales no
agent's pin:
1$ axorum-sandbox policy capped.axp2pol_5azgg8k572bfe8e648xqx9kwfg3pol_5azgg8k572bfe8e648xqx9kwfg is already the policy in force: nothing changed at 1d (2100-01-02T00:00:00Z)4
A policy the engine refuses changes nothing. A policy that does not
compile, one naming a currency the world does not declare, or one capping a
mandate in another currency than the mandate's, is refused 422 with the
engine's reason, and the policy in force stays in force. The command writes
nothing to stdout and exits 3:
1$ axorum-sandbox policy euro.axp2the sandbox refused the policy (substrate_rejected): policy pol_3w13stmeqgb5w8thhstbe6138s cannot be registered: it names currency 'EUR' (line 17, column 89), which is not declared on this deployment, so no amount it compares could be read back; use a declared currency code, or have an operator declare this one with 'axorum currency declare --code EUR --exponent <digits>', then try again; nothing changed3
The route answers the refusal with its code and error, and the engine's
own answer under refused; a policy that does not compile carries itsdiagnostics there, each with its line and column. The world's own policy is
admitted the same way when the world loads.
The exit code says which. axorum-sandbox policy exits 0 when the
policy is in force (changed or already), 3 when the sandbox refused it, asaxorum-sandbox check exits for a policy that is not acceptable, and 2 when
the command could not do its job: the file could not be read, no sandbox
answered, or what answered is not an axorum-sandbox serve with the route.
Every change is recorded. A live change, applied, unchanged or refused,
is a step of the run with its source and its outcome: applied with the
policy it put in force, unchanged with the policy already in force, orrefused with the engine's status, error code, message (cut to 400
characters) and the count of its compile diagnostics (and the policy its
source compiles to, when it compiles); axorum-sandbox check on the step's
source lists the diagnostics again. A scheduled one stays on the timeline,
its source written in place of its file when the run is saved, and what it
did is in the snapshot's timeline, as is every other scheduled change the
engine refused; one that applied is not, since the world's events say
what it did. So a saved run replays each change, and a refused one is
refused again. The snapshot's policies list each policy
put in force and when, and its rules hold each one's rules by id.
Replaying a run in CI
save-run writes the run so far as a scenario: the world, the policy it ran
under written into it, plus a script of every intent the agents submitted and
the answer each got. A scenario is the whole recording: it carries the policy it
was recorded under, though the world named a policy file.
1$ axorum-sandbox save-run -o run.yaml2saved the run to run.yaml3$ axorum-sandbox run run.yaml4ok run.yaml51 scenario(s): 1 matched, 0 diverged, 0 could not be loaded6
axorum-sandbox run replays scenarios without the agent and exits 0 when every
submission gets the answer it recorded, 1 when one diverges and 2 when one cannot
be loaded. Each scenario replays under the policy it carries, each answer held to
the recorded one byte for byte, so an edit to the policy file changes nothing it
replays, and an edit to the scenario itself shows. Here the OpenAI limit in the
scenario's world was raised from $20 to $50:
1$ axorum-sandbox run looser.yaml2FAIL looser.yaml: 1 submission(s) answered differently3 step 2 at 0s: recorded forbidden_rejected (not posted), replayed permitted (posted)4 no longer refused as forbidden_by_rule5 decided by rule forbid_over_payee_limit, now by rule permit_purchase6
followed by both outcomes in full. A run that changed its policy replays its
changes as recorded, the stale pin and the retry included. A script step the
sandbox refuses as it replays it, with a refusal a live run records no step
for, fails the scenario too, named by its step counted from one (1 script step(s) refused, then script step 2 advances to 365300d, past the clock's last second at 365250d): an injected advance that an earlier step or a
scheduled event leaves no room for, or a policy change naming a file. A run
saved from serve never holds one. An advance whose sweep the engine refused
is recorded live, so it replays as recorded and fails nothing on its own.
A scenario recorded before scenarios carried their policy names its policy file
instead, and replays under that file byte for byte. After an edit to the file,
each recorded pin is turned away as stale_policy, and the scenario's lines end
with <file> has changed since the scenario was recorded: save the scenario again so it carries its policy.
Results for automation
Directory operands select regular .yaml and .yml scenario files
recursively. Discovered directory symlinks and special files such as named
pipes are skipped; an explicitly named directory symlink resolves once.
Overlapping operands and file aliases replay each canonical file once, in
sorted displayed-path order. An empty directory operand fails with exit 2,
even beside a nonempty operand, so a misconfigured CI input cannot pass
without running a scenario. A valid world with no script remains valid.
run --quiet suppresses routine matches and summaries, retaining failures
and warnings. run --json writes one report on stdout while keeping human
messages on stderr. Combine them for a quiet successful CI job:
1axorum-sandbox run scenarios/ --offline --quiet --json > replay.json2
The report's schema is axorum.sandbox-run-report.v1:
| Field | Meaning |
|---|---|
exit_code | The command's replay verdict: 0 matched, 1 diverged, 2 could not run. |
summary | Counts: scenarios, matched, diverged, errors, submissions, matched_submissions, diverged_submissions, not_sent, unplayed_steps. unplayed_steps counts refused script steps. The first four count scenarios; the four submission fields count submissions. |
scenarios | One result per selected file: path, status (matched, diverged, error), submission counts, held_to_decisions, baseline_divergences, end_of_run_differences, divergences, unplayed, details, and nullable error. |
divergences | Each scenario's comparisons: one-based script step, sandbox offset at, comparison (exactly or decision), recorded and replayed expectations and rail answers, and a human description. |
details | Supplemental human explanations, including refused events, duties, submissions not sent, and policy changes replaced by a proposal. Use typed fields for automation instead of parsing these strings. |
unplayed | Refused script injections: one-based step, sandbox offset at, and reason. These fail the scenario even when every submission matches. |
errors | Whole-command setup or discovery failures; these produce exit_code: 2 and no replay results. Individual unreadable or invalid scenarios instead carry their own error. |
The report uses the engine's existing expectation shapes. Parse by field name
and allow additive fields within this schema version. Normal replay failure
and setup failure still produce JSON; command-line parsing errors precede
execution, and an output failure cannot promise a complete JSON document.
Exit 2 takes priority over divergence when any scenario cannot load. Defaultrun keeps stdout empty and reports results on stderr.
Trying a proposed policy
--policy tries a proposed policy against every recorded run before it ships:
the proposed policy is in force from each run's start to its end, standing in
for the policy the scenario carries and for every policy change the scenario
schedules or records, each named on stderr. Commit the scenarios and replay
them in CI under the policy under review:
1$ axorum-sandbox run scenarios/ --policy policy.axp2
A change that would change an agent's outcome fails the build, naming the step
and what changed.
Each submission goes out pinned to the policy in force, as an agent re-pins
after a policy change, so none is turned away as stale_policy. A submission the
run recorded refused for its pin (stale_policy, policy_pin_mismatch) goes out
with its pin as recorded and is refused for it again. When its pin is the
proposed policy, it is not sent: its pin went stale in a change the proposed
policy stands in for, so under the proposal the agent would not have sent it.
A step not sent is listed with its reason, and counted apart from the matched
and the diverged, on its scenario's line and in the summary.
An answer judged under another policy never matches byte for byte: the outcome
names its policy, and every duty's id is derived from the policy that opened it.
So from the first step answered under a policy other than the one it was recorded
under, each answer is held to its decision instead, and the output says so. This
run of the purchase-receipts starter has a $700 purchase, the clock moved two
days, and a $100 purchase. A proposal that only reorders the rules decides every
step alike:
1$ axorum-sandbox run run.yaml --policy reordered.axp2trying the proposed policy reordered.axp: in force from each run's start to its end, standing in for every policy change3ok run.yaml (2 of 2 held to their decisions: the policy in force is not the one they were recorded under)41 scenario(s): 1 matched, 0 diverged, 0 could not be loaded5
Raising the receipt threshold from $500 to $1,000 changes what the $700 purchase
decided, and so what the clock did after it:
1$ axorum-sandbox run run.yaml --policy proposed.axp2trying the proposed policy proposed.axp: in force from each run's start to its end, standing in for every policy change3FAIL run.yaml: 2 submission(s) answered differently and the run ended otherwise (2 of 2 held to their decisions: the policy in force is not the one they were recorded under)4 step 1 at 0s: recorded obligated_pending (posted), replayed permitted (posted), held to its decision5 no longer opens a duty under receipt_duty (due at 1d)6 step 3 at 2d: recorded permitted (posted), replayed permitted (posted), held to its decision7 the clock no longer breaches a duty under receipt_duty (due at 1d) before it8 end of run, buyer_bot: no longer has a duty under receipt_duty (due at 1d) breached91 scenario(s): 0 matched, 1 diverged, 0 could not be loaded10
Each step's lines are followed by both answers in full, left out here. A step's
decision is:
- its answer: the verdict and refusal code, or the gateway's status and code;
- what the mocked rail answered;
- the rule that decided it (the refusing rule, else the winning one), by name. A
rule's id is derived from its name alone, so the same rule in two policies has
one id, and renaming a rule is a change though nothing else about it changed; - the mandate that refused it, whose id is the world's;
- the duties it opened, fulfilled and violated, each by whether it is a
reparation, the rule that opened it and its deadline, since a duty's own id is
policy-scoped. A duty that only another party holds, which no answer listed,
is compared by count; - the duties of its submitter's that the clock breached, or opened as a
reparation, since the submitter's previous submission. The clock breaches a
duty whose deadline passes, not a submission, so a breach shows at the
submitter's next step. A duty a step no longer opens is named at that step
alone.
A submission that fulfills a duty names the duty the replay opened in its place,
so a proposal that keeps the duty keeps the fulfillment: a reparation too,
whether the duty it repairs was breached by the clock or opened past its
deadline.
Here a run of the agent-payments starter is tried under capped.axp, which
forbids a purchase over $50. The run bought $100 of API time, moved the clock a
day, put capped.axp in force (the change in "Changing the policy of a running
sandbox"), bought $100 again (refused for its stale pin, then forbidden once
re-pinned) and bought $40:
1$ axorum-sandbox run run.yaml --policy capped.axp2trying the proposed policy capped.axp: in force from each run's start to its end, standing in for every policy change3FAIL run.yaml: 1 submission(s) answered differently (4 of 4 held to their decisions: the policy in force is not the one they were recorded under)4 step 1 at 0s: recorded permitted (posted), replayed forbidden_rejected (not posted), held to its decision5 now refused as forbidden_by_rule6 decided by rule permit_purchase, now by rule forbid_large_purchase7 the proposed policy stood in for the policy change at script step 3 (1d), which was to pol_5azgg8k572bfe8e648xqx9kwfg81 scenario(s): 0 matched, 1 diverged, 0 could not be loaded9
Only the first purchase decides otherwise: had capped.axp been in force from
the start, it would have been forbidden too. The stale-pinned purchase is
refused for its pin again, and the rest decide as recorded.
Tried under the starter's own policy instead, the run never changes its policy,
so the stale-pinned purchase, pinned to that very policy, is not sent, and its
re-pinned retry is permitted:
1$ axorum-sandbox run run.yaml --policy agent-payments.axp2trying the proposed policy agent-payments.axp: in force from each run's start to its end, standing in for every policy change3FAIL run.yaml: 1 submission(s) answered differently, 1 not sent (2 of 4 held to their decisions: the policy in force is not the one they were recorded under, or they follow a submission not sent)4 step 4 at 1d: not sent: its stale pin came from the policy change to pol_5azgg8k572bfe8e648xqx9kwfg, which the proposed policy stands in for5 step 5 at 1d: recorded forbidden_rejected (not posted), replayed permitted (posted), held to its decision6 no longer refused as forbidden_by_rule7 decided by rule forbid_large_purchase, now by rule permit_purchase8 the proposed policy stood in for the policy change at script step 3 (1d), which was to pol_5azgg8k572bfe8e648xqx9kwfg91 scenario(s): 0 matched, 1 diverged, 0 could not be loaded; 1 submission(s) not sent10
The last lines of a failed scenario compare the run's end, which no step shows:
what the clock did after each agent's last submission. Under --policy, the
scenario is also replayed under the policy it carries and its own policy
changes, a baseline held byte for byte to every recorded answer, and both runs'
ends are read from the engine at the final clock: every duty each run opened,
reparations included, and whether it ended breached, open or fulfilled. A duty
opened past its deadline is breached at birth. A duty is compared by the party
that owes it, the rule that opened it, its deadline, whether it is a
reparation, and the party named to perform it, since its own id is
policy-scoped. An end of run line names each the proposal left otherwise, and
a difference fails the scenario like a step's. A duty is owed by the party
whose submission opened it, so a party that never submits holds one only as its
performer (performed_by specific(...)), and such a duty is compared all the same.
A scenario that does not replay as recorded under the policy it carries fails
on its own: its end is not compared, an end of run not compared line says so,
and the run without --policy shows where. A scenario that names its policy by
file was recorded before scenarios carried their policy, so has no baseline:
under --policy it is reported as not loaded (recorded before scenarios carried their policy: save it again). Here the run ends two days after a $100 purchase
and a $700 one, under the same proposal:
1$ axorum-sandbox run ended.yaml --policy proposed.axp2trying the proposed policy proposed.axp: in force from each run's start to its end, standing in for every policy change3FAIL ended.yaml: 1 submission(s) answered differently and the run ended otherwise (2 of 2 held to their decisions: the policy in force is not the one they were recorded under)4 step 2 at 0s: recorded obligated_pending (posted), replayed permitted (posted), held to its decision5 no longer opens a duty under receipt_duty (due at 1d)6 end of run, buyer_bot: no longer has a duty under receipt_duty (due at 1d) breached71 scenario(s): 0 matched, 1 diverged, 0 could not be loaded8
serve --policy is not a proposal: it starts the run under a policy file in
place of the world's own, and a change the world schedules still applies when
it comes due.
The console
The console at https://console.beta.axorum.com keeps a sandbox's runs, its
worlds and the events sent to it. A sandbox there has a secret, axsb_…, and
two variables connect a local run to it:
| Variable | Value |
|---|---|
AXORUM_SANDBOX_SINK | The console's origin, https://console.beta.axorum.com. |
AXORUM_SANDBOX_TOKEN | The sandbox's secret. |
With both set, serve reports the run to the console after each verdict and
each move of the clock, and run reports each scenario it replays. With either
unset, nothing leaves the machine, and a run
with only one set says which is missing. The secret travels in a request body, never in a URL,
and is never printed. pull, publish, serve with no world file and the--console-events listener first trade the secret for a console session, the
exchange, which names the sandbox, its organization and the world attached to
it. No request to the console follows a redirect, so neither
the secret nor a write reaches another host. A redirect is named as one, with
the fix to set AXORUM_SANDBOX_SINK to the console's own origin: pull,publish and the start-up fetch of serve with no world file stop with exit
2; the --console-events listener of serve says so and tries again; and a
run's report is skipped with that reason, so serve keeps serving and run
exits on its scenarios' verdicts.
A run's snapshot takes at most 1,048,576 characters in the console. Past it,
the oldest records of the journal page are left out first; a run still over
it with none is reported without its snapshot, saying why, every part of the
snapshot with what it takes, and the console shows that reason on the run's
page. The run is still reported: its counts, its clock and its events. A
console that refuses a snapshot as over its bound gets the run again without
it. run says so for the scenario, and serve says so once when the
console's copy loses its snapshot and once when it fits again.
axorum-sandbox serve, with no world file, serves the world attached to the
sandbox in the console, with the policy file it names when it names one, so
what you build in the console is what the agent runs against. It fetches both
as pull does, checks them, and writes nothing; --policy serves a local
policy file in its place. Attach another world in the console and restartserve to run it.
axorum-sandbox pull -o world.yaml fetches the world attached to the sandbox
in the console, and checks that it parses before writing it. A world writes
its policy inline or names a policy file from the console's policy library,
as a local world does (policy: {file: agent-payments.axp}), so the same
files run in both places and one policy serves many worlds. For a world that
names its file, pull fetches that policy too and writes it beside the world,agent-payments.axp next to world.yaml, where serve and run look for
it. Such a world needs -o: on stdout there is nowhere beside it to write the
policy. Pull a world to keep it in your repository or edit it locally;-o also writes .axorum-pulled.json beside it, what publish from there
writes over.
axorum-sandbox publish world world.yaml saves a world written locally as
the world attached to the sandbox, and the policy file it names beside it to
the console's policy library, so the console runs what you wrote. The
sandbox's secret is all it needs: it may write only its attached world, the
policy that world names and the library policies it created that nobody has
edited since. Both files are checked first, the policy against the world,
and every warning counts as an error: a check that fails exits 3 and sends
nothing. A world with a top-level script, as a scenario has, is refused
the same way, whatever the script holds, an empty one (script: [])
included: a world in the console holds no script, since a script belongs to
a sandbox run, not to the published world. The fix is to remove script
and publish again, keeping the steps as a scenario for axorum-sandbox run
or saving them as a scenario under Scenarios in the console. script: ~
holds no script and publishes. A world whose timeline changes the policy to
a file is refused the same way, naming the event counted from one, with the
fix to write its source inline: the console keeps the world alone, with no
file beside it, so the change could never load there. So is a world whose
timeline schedules a policy change it cannot run, one that does not compile
or names a currency the world does not declare, naming the event and the
compiler's problem, by the same check axorum-sandbox check --world runs.
A scheduled change's lint warnings
refuse nothing, as a mid-run change such as a freeze is often one the
linter flags: only the world's own policy is checked with every warning as
an error. A change bysource is published as written, since the world the console keeps is the
file on disk. The world's world: line renames it, refused when another
world of the organization has that name. The policy is saved under its file
name, so the world must name it by a file name alone, with no directory, of
at most 80 bytes: an ASCII letter or digit, then only ASCII letters, digits,., _ and - (agent-payments.axp, not ap policy.axp or _ap.axp).
Any other name is refused before anything is sent, saying which part of the
rule it breaks; publish policy holds its file to the same rule. The policy
updates the world's own policy, or a policy this sandbox created, when the
names match, and creates a library policy when no policy has the name. Any
other library policy is refused, whatever its text, and so is one this
sandbox created once an owner or member has edited it: the policy a world
names is the one its sandbox may then read and write, so the console lets a
sandbox point its world only at the policy it runs or one it created that
nobody has edited since. Rename the policy in your world file (itspolicy: file: line and the file) to publish your own copy, or ask an owner
to attach that policy to the world in the console. A policy it creates goes
into the library of the sandbox's organization, the one the exchange names.axorum-sandbox publish policy agent-payments.axp saves the
policy alone, checked against the attached world, under the name the library
already gives it. Each prints what changed (updated, renamed, created,
or unchanged), --dry-run says what would change and writes nothing, and--json prints one line of JSON on every exit (below). A running serve
keeps the world it started with: restart it, with no world file, to run what
you published. New worlds start in the console: create the sandbox there,
then publish into it. Publishing a world you started locally overwrites the
console's version of the sandbox's world, so pass --replace, looking
first: axorum-sandbox publish world world.yaml --replace --dry-run, then
the same without --dry-run.
Each write is sent once, the policy before the world, and publish exits 0
when published (with --dry-run, publishable), 3 when the check or the
console refuses it, and 2 when it cannot do its job: a variable unset, the
console out of reach or answering a redirect, the secret refused, an answer
the console should not have given, or a write the console did not confirm. A
partial write the console refused exits 3 and says what was written, such as
a policy created before the console refused the world. A partial write that
failed any other way exits 2 and says to run the same publish again, which
finishes it: a write the console did not confirm may already be saved, and
the rerun finds it in place.
A publish writes only over what pull fetched into the file's directory.pull -o keeps there, in .axorum-pulled.json, each record's id, the
revision its read gave and a digest of its name and text: a file of its
own, since the console keeps the world file's text as written, so anything
kept in world.yaml would be published. Before its first write, a publish
reads every record it updates. One saved in the console since the pull is
refused, exit 3, changed in the console since you pulled; nothing was written, with the fix to pull again into another directory and carry your
changes there; one the directory holds no pull of is refused the same way,
with the fix to pull first or to publish again with --replace, unless the
console holds the local text already. Each write carries the pulled
revision (If-Entity-Revision), and one the console refuses only because
the revision moved, the record's name and text still as pulled, goes again
over the new revision, up to three writes. After each write the console
confirms, the publish keeps what it wrote in place of what was pulled, so
the next publish from that directory writes over its own.
--replace replaces what the console holds now with the file, whatever
was pulled into its directory or none: pass it only to overwrite the
console's version. It still writes over the revision its own read gave, so
a save made in the console while it runs is refused, exit 3, changed in the console while this publish wrote it. With --dry-run, it names on
stderr each record it would replace, with its current name in the console
and the size of its text and of the file's. The fix of a record saved
since the pull never offers it: carry your changes into a fresh pull
instead, as a replace would discard that save.
axorum-sandbox serve --console-events also applies the world events the
console sends this sandbox as they arrive, over a subscription the sandbox
opens itself; nothing connects into your machine. Without the flag the
sandbox never subscribes. A policy event from the console carries the
policy's text, {"source": ...}, since the console holds no file beside the
world to name; one naming a file is rejected.
With --json, publish prints one object on stdout on every exit, 0, 2 or
3, and stderr carries the same prose as without it:
| Key | Value |
|---|---|
exit | The exit code: 0, 2 or 3. |
dry_run | Whether --dry-run was given. |
sent | Whether any write left for the console: false for an unset variable, a refused exchange, a failed check and every dry run. |
world | {"id", "name", "change", "from"}; null for publish policy, and when publish stopped before it planned the change. |
policy | {"id", "name", "change"}, id null until the console gives one; null for a world that writes its policy inline, and when publish stopped before it planned the change. |
world.change, policy.change | updated, renamed, created or unchanged as the console confirmed it, what would change on a dry run, and null when the console did not confirm it. from is the world's name before a confirmed rename. |
unconfirmed | "policy" or "world": the write sent that the console did not confirm, which may already be saved; else null. |
error | Why it stopped, null on exit 0. |
fix | What to do about a refusal, null unless exit 3. |
Where the sandbox and axorum serve differ
The sandbox answers the agent plane as axorum serve does: the same policy
engine, the same verdicts, refusals, reports and journal records. What differs
follows from the sandbox having no wall clock and one process, and any other
divergence is a sandbox bug:
- No heartbeat. A deployment advertises its idempotency window in seconds;
the sandbox's window is counted in ticks with no length in seconds. So when an
agent handsauthorizea key from its own records, to resume an attempt made
earlier, the SDK cannot vouch for the key's age and refuses before sending: astale_keyfailure with the reasonunclocked. Nothing was sent; mint a new
transaction id. A key minted in the same process always goes ahead. - No cluster. There is no commit wait, failover or replica to test against.
- Mocked rails. A rail answers as the world says, never as a real one does.
- Tokens from the sandbox. An agent's token comes from the sandbox (
token),
minted from the world's seed, not from your identity provider.
Command reference
| Command | What it does |
|---|---|
axorum-sandbox completions <shell> | Generates completion for Bash, Zsh, Fish, PowerShell or Elvish. |
axorum-sandbox new [starter] [--dir D] [--force] | Writes a starter world and its policy; without a starter, lists them. |
axorum-sandbox check <policy|-> [--world W] [-q] [--deny-warnings] [--print] | Compiles a policy offline as axorum policy check does and, with a world, judges it against that world's currencies, mandates and agent bindings, and judges each policy change the world's timeline schedules against it, without its lint; exits 0 or 3. |
axorum-sandbox serve [world] [--policy P] [--seed N] [--port N] [--console-events|--offline] | Serves a world (or a scenario's world) until interrupted, starting under --policy when given; without one, the world attached in the console. |
axorum-sandbox token <agent> [--url U] | Prints a token a running sandbox mints for an agent of its world. |
axorum-sandbox inject <event.yaml|-> [--url U] | Applies one world event to a running sandbox now. |
axorum-sandbox policy <file|-> [--url U] | Puts a policy file (or stdin) in force in a running sandbox now; prints the id in force. Exits 0 when in force, 3 when refused, 2 when it cannot ask. |
axorum-sandbox save-run [-o FILE|-] [--force] [--url U] | Writes a running sandbox's run as a scenario. |
axorum-sandbox run <paths>… [--policy P] [--seed N] [--json] [--quiet] [--offline] | Replays scenario files (or directories of them), each under the policy it carries, or tries a proposed policy P against them; exits 0, 1 or 2. |
axorum-sandbox grammar world | Prints the reference to the world format: every key, rule and default, with examples. |
axorum-sandbox pull [-o FILE|-] [--force] | Fetches the world attached to this sandbox in the console, and the policy file it names beside it. |
axorum-sandbox publish world <file> [--dry-run] [--replace] [--json] | Checks a local world and the policy file it names, then saves them to the console as this sandbox's world and its policy; exits 0, 2 or 3. |
axorum-sandbox publish policy <file> [--dry-run] [--replace] [--json] | Checks a policy file against the world attached to this sandbox, then saves it as that world's policy; exits 0, 2 or 3. |
AXORUM_SANDBOX_BIN names an axorum-sandbox binary for the npm launcher and
the Python package to run in place of the installed one.