Programs
Effects
An effect is the only way guest code touches the world. The catalog below is the entire host surface — small and stable by design. Everything else a program uses (the any client, LLM adapters, memory, the conversation loop) is itself guest Python loaded with use().
The catalog
| Syscall | Class | Capability | What it does |
|---|---|---|---|
http.get/head/post/put/patch/delete(url, params?, headers?, json?, body?, timeout?, credential?, redirects?) |
route-derived | route-derived | the one outbound door → Response with .status, .headers, .text, .json(), .url |
config.get(key) |
read | config.get |
non-secret config; secret keys are refused |
mailbox.drain() |
read | mailbox.read |
loop control (injected messages) as a recorded effect |
time.now / random.random / uuid4 / sleep / env.get |
read | — | determinism pins — real wall clock and randomness, recorded |
module.resolve(spec, frm?) |
read | — | what use() calls; output carries the source bytes |
batch(name, payloads) |
per item | per item | fan-out; records land in input order |
fuel.state |
read | — | {remaining, budget} — a checkpoint signal for long loops |
oauth.connect(provider, scopes?, timeout?) |
mutate | oauth.connect |
consent via loopback + PKCE; returns {ok, provider, grantedScopes, account}, never tokens |
oauth.status(provider) |
read | oauth.status |
{connected, pending, scopes, account, expiresAt} — no network |
oauth.disconnect(provider) |
mutate | oauth.disconnect |
provider-side revoke (best effort) + local delete |
oauth.refresh(provider) |
mutate | oauth.refresh |
host-emitted only; a direct guest call fails typed host_only |
trace.effects_of(cell?, span?) / trace.effect_get(seq) |
read | — | the program's own trace, as a view |
span.begin/end |
— | — | guest-declared grouping (broker machinery, not registry effects) |
Ergonomic shims sit on top: now(), rand(), env(...) as globals, and proxied datetime / random / time / os.environ modules whose nondeterministic calls route through the same effects. Guest code never sees a fake clock — datetime.now() is a real time.now record.
Pythonic surface
r = http.get("https://api.github.com/repos/anyproto/any",
headers={"Accept": "application/vnd.github+json"}, timeout=20)
if r.status == 200:
print(r.json()["stargazers_count"]) # print() is the output channel
print() is the model-facing output channel: each call is a structured-value record in the trace and the primary input to a cell's result digest. http.* mirrors the requests idiom (params=, headers=, json=); a call returns the response as data, including 3xx when redirects are manual.
Read or mutate is a boundary fact
Every record carries meta.class of read or mutate. For HTTP the class and the required capability derive from (method, url) against a route table scoped to the any server's base URL:
| Route | Class | Capability |
|---|---|---|
any GET … |
read | data.read |
any POST …/query, …/objects/query, …/search, …/aggregate |
read | data.read |
| every other any write | mutate | data.write |
LLM completion endpoints (/v1/messages, /chat/completions) |
read | llm.chat |
| anything else | per verb (get/head read, others mutate) |
net.http |
Guest code cannot declare its own class. A @span(kind="getter") facade whose span contains a mutate is a legible inconsistency a viewer can flag; the log never lets the label override the boundary fact. Reads are safe to re-execute; mutates are not, and replay tooling relies on that distinction.
The pipeline
Every effect call, no exceptions, flows through one path:
normalize → key → capability check → replay/mock consult → execute → record → return
- Normalize produces the canonical input (sorted keys, explicit defaults, headers lowercased); its sha256 is the record's
key. - Capability check intersects grants over the import chain — a program can never lend more authority than it holds. Denials are recorded as
error.type: "capability_denied". - Record always — success, error, denial, mock. A failure is data in the record and a typed
EffectError(seq, type, message)raised into the guest, so a cell may catch it and nothing is silently swallowed.
Why it matters. The guest-side facade (
http.get,print()) holds zero authority — bypassing it gains nothing. Classification, secrets and recording all happen on the host side of one serialized crossing. The cage and its syscalls never change; everything the agent is lives above the boundary as deployed code.
Fan-out
Cells are single-threaded. Concurrency is the host's job: effect("batch", {"name": "http.get", "payloads": [...]}) carries a list across the boundary once, executes on a pool, and appends one record per item in input order regardless of completion order — so strict replay stays deterministic. A per-item failure is an EffectError value in that slot, never a whole-batch exception. Looping the same effect sequentially is correct-but-slow, never wrong.
Introspecting your own run
trace.effects_of(cell) returns a scope's immediate children — bare effect records plus child span-end rows — and trace.effect_get(seq) resolves one full record. This is how the agent answers "why did you do that?" from inside a conversation; the human-side equivalent is anyrt trace show on Traces and replay.