Programs/Effects

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(). Provider keys ride effects too, as credentials.

The catalog

Syscall Class What it does
http.get/head/post/put/patch/delete(url, params?, headers?, json?, body?, timeout?, credential?, redirects?, response?) route-derived the one outbound door → Response with .status, .headers, .url, .text / .json() for a text body, .blob for bytes
config.get(key) read agent config; secret namespaces are refused
config.set(key, value) mutate writes one agent config row
runtime.get(key) read runtime wiring — any.base_url, overlays.aliases, bao.space, shell
mailbox.drain() read loop control (injected messages, breaks) as a recorded effect
time.now / uuid4 / sleep / env.get read determinism pins — time.now returns {epoch, offset_s, tz}; sleep caps at 300 s
fuel.state read {remaining, budget} — a checkpoint signal for long loops
module.resolve(spec, frm?) read what use() calls; output carries the source
batch(name, payloads) per item fan-out; records land in input order
blob.read(hash, offset, length) / blob.put(data, mime) read bytes behind a Blob handle, held by the host
oauth.connect(provider, scopes?, timeout?) mutate consent via loopback + PKCE; returns {ok, provider, grantedScopes, account}, never tokens
oauth.status(provider) read {connected, pending, scopes, account, expiresAt} — no network
oauth.disconnect(provider) mutate provider-side revoke (best effort) + stored grant delete
oauth.refresh(provider) mutate host-emitted only; a direct guest call fails typed host_only
trace.effects_of(cell?, span?, run?) / trace.effect_get(seq, run?) read a run's trace as a view — this run, or a past one by run id
trace.runs(program?, filter?, sort?, limit?) / trace.stats(run) / trace.query(pipeline, coll?) read the run finder, one run's cost summary, read-only aggregation over the trace store
bao.status(line) mutate the agent's live status line
sh.run/spawn/poll/kill, fs.read/list/write/edit mutate (fs.read/fs.list read) processes and files on the device — only in a runtime built with the shell feature
span.begin/end — guest-declared grouping (broker machinery, not registry effects)

http.* derives its capability from the route (below); every other syscall's capability is its own name.

Ergonomic shims sit on top: now(), rand(), env(...), uuid4() as globals, and proxied datetime / time / os.environ modules whose "what time is it" calls route through the same effects. The present is always a record — datetime.now() and time.time() are real time.now records. Randomness is not one record per draw: random, secrets and uuid run on a stream seeded from the trace header's seed, so a replay re-derives every value.

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 captured as a structured value in the cell's result and is the primary input to its digest. http.* mirrors the requests idiom (params=, headers=, json=); a call returns the response as data, including 3xx when redirects are manual. The host decides text versus bytes from the response itself: a binary body comes back as a Blob handle on .blob (.text raises), and a Blob passed as a request body or inside json is expanded by the host at send time while the trace keeps only the reference.

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
GET / HEAD read data.read on the any server, net.http elsewhere
any server POST …/query, …/objects/query, …/search, …/aggregate read data.read
every other any server call mutate data.write
LLM completion endpoints (/v1/messages, /chat/completions, /v1/complete) read llm.chat
anything else 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 — the payload as JSON with sorted keys; sha256 over the effect name and that JSON is the record's key.
  • Capability check consults the active grant set before anything runs. The runtime ships a permissive default; a denial is recorded as error.type: "capability_denied".
  • Record always — success, error, denial, mock. A failure is data in the record and a typed EffectError ("<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. http.get_many(items) carries a list of requests across the boundary once as a batch effect; the host runs each item as its own effect call and appends one record per item in input 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

effects.of(cell) returns a scope's immediate children — bare effect records plus child span-end rows — and effects.get(seq) resolves one full record; both take run= to read a past run. effects.runs(...) finds runs by program, status or cost, and effects.query(pipeline) aggregates over every recorded run. 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.