Public beta — not for production use. Data may be wiped at any time. Questions? Contact us.
Documentation menu

Sandboxing and tenancy

What node code can and cannot do at runtime: the sidecar is a node's only channel to the platform, and tenant isolation is enforced by the platform — never by your code.

View as Markdown

Node code runs in an isolated container with no direct access to the platform. Everything a node does beyond pure computation — logging, reading secrets, inspecting the running flow — goes through the sidecar, and the sidecar enforces tenant isolation. You never write isolation code yourself, and node code cannot opt out of it. This page explains the boundary: how the sandbox works, what node code can and cannot do, and which guarantees you can build on.

How node code is sandboxed

Every published package runs as two containers side by side in one auto-scaling service:

  • user-node — the image built from your package. It runs the generated service wrapper around your handler functions and listens on a Unix socket at /tmp/axiom.sock, on a volume shared only with its sidecar.
  • sidecar — the platform-owned proxy. It is the only container that receives execution traffic, and it relays each invocation to the user-node over the shared socket.

Node code has no platform endpoint to call. All platform capabilities reach node code exclusively through ax on AxiomContext: the sidecar relays every ax.log and ax.secrets call on the node's behalf, and there is no separate address a node can dial to reach the platform directly.

Each invocation delivers exactly this to your handler: the serialized input message, the execution ID, the flow ID, the tenant identity, a variables map holding resolved flow config values and secrets, and a read-only structural view of the running flow (ax.reflection.flow). Nothing else about the platform is visible. Log lines written with ax.log travel back through the same sidecar channel and are stamped with the execution ID and trace context, so they are searchable per execution in production.

The variables map is for flow config values and secrets only — it is not working memory or agent state. Within one execution, state belongs in your typed messages. Cross-execution agent memory is out of scope for the beta.

What node code can do

A node handler is a plain function: it receives AxiomContext (ax) and a typed input message, and returns a typed output message. Through ax it can:

  • Logax.log.debug/info/warn/error, structured, per invocation.
  • Read secretsax.secrets.get(name) returns the plaintext value of any secret the calling tenant has registered, plus a found flag.
  • Inspect the running flowax.reflection.flow exposes the nodes, edges, and current position, read-only.
  • Mutate the running flowax.mutation.flow.add_node and add_edge, available only to nodes declared mutation_capable: true in axiom.yaml; every mutation is validated by the platform.

Node code can also make ordinary outbound network calls. Calling an external API — an LLM provider, a database you host — is the normal use of ax.secrets:

# nodes/call_model.py — scaffolded by:
#   axiom create message Prompt --fields "text:string"
#   axiom create message Reply --fields "text:string"
#   axiom create node CallModel --input Prompt --output Reply
from gen.messages_pb2 import Prompt, Reply
from gen.axiom_context import AxiomContext


def call_model(ax: AxiomContext, input: Prompt) -> Reply:
    api_key, ok = ax.secrets.get("OPENAI_KEY")
    if not ok:
        ax.log.error("secret not registered", name="OPENAI_KEY")
        return Reply()
    ax.log.info("calling model", prompt_chars=len(input.text))
    # Use api_key with any HTTP client here — outbound calls are allowed.
    return Reply()

What node code cannot do

  • Call platform services directly. No platform service address is exposed to the node container. Every platform capability — logging, secrets — is accessed through ax. Node code that attempts to dial platform endpoints directly will not reach them.
  • Read another tenant's secrets. The platform resolves secrets for the tenant invoking the flow before any node code runs; the variables map a node receives only ever contains that tenant's secrets.
  • See engine internals. Flow reflection is a read-only structural mirror; compiled adapters and engine-internal types are not exposed. When the platform rejects a mutation, the node sees only a human-readable error message — structured engine error codes never cross the boundary.
  • Mutate a flow that does not allow it. ax.mutation.flow calls are buffered and validated by the platform after the handler returns; they are accepted only when the compiled flow contains a node declared mutation_capable: true, and every accepted mutation is type-checked.

How tenant isolation is enforced

Tenant isolation happens at two checkpoints, neither of them in node code:

  1. At the platform edge. Every invocation is authenticated, and the tenant identity is resolved from the caller's credentials before any node code runs (see Create and manage API keys). That identity travels with the execution through every hop.
  2. At secret resolution. When an execution starts, the platform fetches the calling tenant's secrets — stored encrypted at rest — and injects the plaintext values into the invocation's variables map. A node can only read what was injected for its caller's tenant (see Manage secrets in a flow).

Because enforcement lives in the platform and the sidecar, a bug in node code — yours or a marketplace package's — cannot widen its own access.

Cross-execution agent memory would add a third checkpoint, but it is out of scope for the beta SDK — there is no node-side memory call to isolate at this layer today.

What you can rely on, and what you own

You can rely on:

  • Secrets stored on the Secrets page of the console (/console/secrets) are encrypted at rest and readable only by executions invoked under your tenant.
  • One tenant's executions never observe another tenant's payloads or secrets.
  • New platform capabilities arrive as new properties on ax — node function signatures never change when the platform grows.

You still own:

  • Where your data goes. The sandbox does not restrict outbound network calls; what a node sends to external services is your code's responsibility.
  • Secret hygiene in your own code. ax.secrets.get returns the plaintext value — do not write it to ax.log or copy it into an output message.
  • Your dependencies. Third-party libraries in your package run inside your node container with the same access your code has.