Metadata-Version: 2.4
Name: axorum-sandbox
Version: 0.3.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Dist: httpx>=0.27
Summary: The Axorum sandbox: a world's agent plane on 127.0.0.1, a clock that moves only when a test moves it, and the ledger to assert on.
Keywords: axorum,sandbox,ledger,policy,agent,testing
Author-email: Roland Rodriguez <roland@axorum.com>
License-Expression: BUSL-1.1
Requires-Python: >=3.11
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://axorum.com/docs/sandbox

# `axorum-sandbox`

The Axorum sandbox for Python: a world's agent plane served on 127.0.0.1, a clock
that moves only when the test moves it, and the ledger to assert on. The wheel
carries the `axorum-sandbox` binary; the `axorum_sandbox` module runs it.

```sh
pip install axorum-sandbox axorum-client
```

There is a wheel for Linux on x64 and arm64 (any libc), macOS on x64 and arm64,
and Windows on x64.

## An agent's test

```python
from axorum import Agent, Amount, AxorumClient, Payment
from axorum_sandbox import Sandbox

BUYER = "agent://example.com/agent/buyer_01h455vb4pex5vsknk084sn02q"


def test_the_agent_stops_paying_a_suspended_vendor(tmp_path):
    with Sandbox.starter("agent-payments", tmp_path) as sandbox:
        # The agent's own client, unchanged but for where its requests go.
        axorum = AxorumClient(sandbox.url)
        agent = Agent(BUYER, lambda: sandbox.token(BUYER))
        pay = lambda minor, payee: axorum.authorize(agent, Payment.new(Amount(minor, "USD"), payee))

        assert pay(10_000, "api.anthropic.com").allowed
        sandbox.advance("7d")  # the vendor review suspends Anthropic
        assert not pay(100, "api.anthropic.com").allowed
        print(sandbox.report("exceptions")["text"])
```

A token lives a day of the sandbox's clock, so an agent that outlives a move of the
clock takes a token source, `Agent(uri, lambda: sandbox.token(uri))`, rather than
one token. A run reads no clock and no random source: one world and one seed give
one run, on every machine.

## Starter worlds

| 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. |

`starters()` lists them, `new(starter, directory)` writes one's world and policy
to disk, and `Sandbox.starter(name, directory)` writes and serves it. The
[sandbox guide](https://axorum.com/docs/sandbox) shows how to test an agent
with one.

## Checking a policy

`check(policy, *, world=None, deny_warnings=False)` compiles a policy file offline
as `axorum policy check` does and, with a world file, judges it against that
world: the currencies it declares, the mandates it opens and the agents it
binds. It answers the verdict document, with the id each mandate and party name
derives to and every warning by `code`, and raises `SandboxError` when the
policy does not compile, the world cannot run it, or `deny_warnings` refuses a
warning.

```python
from axorum_sandbox import check

checked = check("spending-mandate.axp", world="spending-mandate.world.yaml")
assert checked["warnings"] == [], checked["warnings"]
```

## What a run offers

| | |
|---|---|
| `Sandbox(world, policy=None, seed=None)` | Serves a world file (YAML) until `close()`, or for a `with` block. |
| `sandbox.url` | The agent plane, answered as `axorum serve` answers it. Hand it to `AxorumClient`. |
| `sandbox.token(agent)` | A capability token for an agent the world names. |
| `sandbox.advance(by)` | Moves the clock by an offset (`30s`, `1d`), answering what applied on the way. |
| `sandbox.inject(event)` | Applies one world event (YAML) now. |
| `sandbox.events()` | Every world event applied so far, each as `advance` lists 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 of the clock reports them. |
| `sandbox.policy(source)` | Puts another policy in force now, as `axorum serve` registers and activates one; answers what changed and when. An agent pinned to the old policy is refused `stale_policy` and re-pins; the policy in force sent again changes nothing; a refused policy raises and changes nothing. |
| `sandbox.ledger(first=None, limit=None)` | A page of the journal, each record tagged by its kind: `{"Transaction": …}` for each judgement, a refusal with `"posted": False`. |
| `sandbox.obligations()` | Every duty still open. |
| `sandbox.report(kind, from_=None, to=None, usage=None, transaction=None)` | `period`, `exceptions`, `activity`, `registers`, `usage_balance`, `mandates` or `explain`, rendered as serve renders it. |
| `sandbox.snapshot()`, `sandbox.save_run()` | The console's snapshot, and the scenario that replays the run. |
| `snapshot_schema()`, `snapshot_schema_path()` | The JSON Schema (draft 2020-12) every snapshot meets, `urn:axorum:schema:axorum.sandbox-snapshot.v1`, parsed and as the file the package ships. |

A refusal raises `SandboxError`, with the `status` and `code` the sandbox answered,
and its `body`.
`AXORUM_SANDBOX_BIN` names a binary to run in place of the packaged one.

The same binary is the `axorum-sandbox` command: `axorum-sandbox serve`, `run`
(replays saved scenarios in CI), `new`, `inject`, `policy`, `save-run`, `token`
and `pull`.
With `AXORUM_SANDBOX_SINK` and `AXORUM_SANDBOX_TOKEN` set from the console,
`axorum-sandbox serve --console-events` serves the world attached to the sandbox
there and reports each run back.

