---
title: "Sandboxing and tenancy"
description: "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."
category: concept
surfaces: [sdk, console]
related: [concepts/nodes-packages-flows, concepts/execution-model, concepts/memory, guides/manage-secrets, guides/api-keys, reference/glossary]
last_reviewed: 2026-06-10
---

# Sandboxing and tenancy

Node code runs in an isolated container with no direct access to the
platform. Everything a node does beyond pure computation — logging, reading
secrets, using agent memory, 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`, `ax.secrets`, and `ax.agent.memory` 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; across executions, use agent memory (see
[Agent memory](../concepts/memory.md)).

## 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:

- **Log** — `ax.log.debug/info/warn/error`, structured, per invocation.
- **Read secrets** — `ax.secrets.get(name)` returns the plaintext value of
  any secret the calling tenant has registered, plus a found flag.
- **Use agent memory** — `ax.agent.memory`, durable across executions,
  scoped to the flow and tenant.
- **Inspect the running flow** — `ax.reflection.flow` exposes the nodes,
  edges, and current position, read-only.
- **Mutate the running flow** — `ax.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`:

```python
# 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, memory — is accessed through `ax`. Node code that attempts to
  dial platform endpoints directly will not reach them.
- **Choose or forge its tenant.** The tenant and flow identity used for
  memory access are fixed on the sidecar when the package is deployed —
  they live in the sidecar container, not yours. The memory proxy
  overwrites the tenant and flow fields on every memory call, so whatever
  values node code puts there are ignored.
- **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 three checkpoints, none 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](../guides/api-keys.md)). 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](../guides/manage-secrets.md)).
3. **At the sidecar.** The sidecar's memory proxy stamps the deploy-time
   tenant and flow identity onto every memory read and write, discarding
   anything node code supplied. Agent memory therefore cannot cross tenant
   boundaries even if a node tries.

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.

## 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.
- Agent memory is isolated per tenant and scoped per flow — no other
  tenant, and no other flow, reads your flow's memory.
- One tenant's executions never observe another tenant's payloads,
  secrets, or memory.
- 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.
