Are you an LLM? You can read better optimized documentation at /docs/agent-guard/custom-runtimes/build-a-runtime-image.md for this page in Markdown format
Build a runtime image
A runtime image is the whole application. Agent Guard adds no agent SDK and reads no launch settings from the agent definition, so the image carries its own command, and it reaches models and tools only through the environment Agent Guard gives it. The full list of variables and mounts is in Environment and mount reference.
What the image must provide
Agent Guard checks three things on the host before it boots the microVM.
linux/arm64. The microVM is arm64 Linux, so the image must have a linux/arm64 manifest. A multi-platform image is fine; Agent Guard selects the arm64 entry. Build with an explicit platform:
bash
docker buildx build --platform linux/arm64 -t jozu.ml/acme/triage-agent:1.0.0 --push .Set the platform on the build command rather than as a constant in FROM --platform=..., which Docker's build checks warn about.
A command. The image must define an ENTRYPOINT, a CMD, or both. The agent definition has no field for a command, so the image is the only place it can come from.
A version floor (optional). Label the image with the oldest Agent Guard release it works with, and older releases refuse it with a message saying to upgrade Agent Guard:
dockerfile
LABEL dev.jozu.agentguard.min-version=0.8.0Nothing else about the image is required: no particular base, no file layout, no agent SDK.
A minimal Dockerfile
dockerfile
FROM python:3.12-alpine
COPY app.py /app/app.py
LABEL dev.jozu.agentguard.min-version=0.8.0
WORKDIR /app
ENTRYPOINT ["python3", "-u", "/app/app.py"]-u makes Python's output unbuffered, which matters for an interactive application: output appears as it is written.
The container user
The container runs rootless, and the image's USER is mapped to Agent Guard's agent account inside the microVM:
- With no
USER, the application runs as uid 0 inside the container, which maps to the unprivileged agent account outside it. - A numeric
USER 1000orUSER 1000:1000is used as written. - A named
USER appis looked up in the image's own/etc/passwd. If the lookup fails, the application runs as uid 0 and the boot log says so.
Do not switch uid at run time (with su, gosu, or setuid binaries): a process that changes uid lands in a range that cannot write the state directory.
Calling models
The application calls models through the OpenAI-compatible route of the Jozu AI Gateway, and names each model by the module name the agent definition gives it. The definition decides which provider, which real model, and which credential that name means. The application never sees a provider key.
The Jozu gateway sets OPENAI_BASE_URL and OPENAI_API_KEY in the container, so an OpenAI SDK works unchanged:
python
from openai import OpenAI
client = OpenAI() # reads OPENAI_BASE_URL and OPENAI_API_KEY
reply = client.chat.completions.create(
model="assistant", # a module name from the agent definition
messages=[{"role": "user", "content": "Summarize the open incidents."}],
)
print(reply.choices[0].message.content)With a plain HTTP client, send the session token as a bearer token:
python
import json, os, urllib.request
req = urllib.request.Request(
os.environ["AGENTGUARD_OPENAI_BASE_URL"] + "/chat/completions",
data=json.dumps({
"model": "assistant",
"messages": [{"role": "user", "content": "Hello"}],
}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + os.environ["AGENTGUARD_OPENAI_API_KEY"],
},
)
print(json.load(urllib.request.urlopen(req))["choices"][0]["message"]["content"])The request format is the OpenAI chat completions format for every provider. The Jozu gateway translates it for Anthropic, Gemini, and the other supported providers (see llm modules).
A model name the definition does not declare is refused. Do not hard-code provider model names; use the module names the definition declares. A convention that works well is an ENV default recording the module name the image expects:
dockerfile
ENV TRIAGE_MODEL=assistantThe application may also name a model by the module's source.model string exactly as the definition writes it, for example openai/gpt-4o-mini. The module name is the better choice, because it survives the definition switching providers.
Calling tools
Every mcp module in the definition is served from one MCP endpoint, AGENTGUARD_MCP_GATEWAY_URL, over streamable HTTP. The MCP route needs no session token, because it reaches no provider. Keep the Mcp-Session-Id the server returns:
python
import json, os, urllib.request
URL = os.environ["AGENTGUARD_MCP_GATEWAY_URL"]
session = None
def mcp(method, params=None, id=1):
global session
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
}
if session:
headers["Mcp-Session-Id"] = session
body = {"jsonrpc": "2.0", "id": id, "method": method, "params": params or {}}
resp = urllib.request.urlopen(urllib.request.Request(URL, json.dumps(body).encode(), headers))
session = resp.headers.get("Mcp-Session-Id", session)
return resp.read()
mcp("initialize", {"protocolVersion": "2025-03-26", "capabilities": {},
"clientInfo": {"name": "triage-agent", "version": "1.0.0"}})
print(mcp("tools/list", id=2))An MCP client library that speaks streamable HTTP works the same way; give it the URL.
Call tools by the names tools/list returns. The Jozu gateway prefixes each tool with its module's name (with hyphens turned into underscores), so two modules can both offer a search tool without a clash. Do not build tool names yourself.
Policies apply at the Jozu gateway. A call a policy denies returns an error naming the policy and rule, and never reaches the MCP server. Treat it as a normal tool failure and show it to the user.
Asking for a decision on your own actions
An action the application takes directly, such as writing a file or calling an internal API, does not pass through the Jozu gateway, so no policy sees it. If you want policy to govern such an action, ask the policy engine before taking it. AGENTGUARD_POLICY_URL needs no session token. The request carries the action as a tool call (name and arguments), and the response's allowed field is the decision:
python
import json, os, urllib.request
def allowed(tool, arguments):
req = urllib.request.Request(
os.environ["AGENTGUARD_POLICY_URL"],
data=json.dumps({"tool": {"name": tool, "arguments": arguments}}).encode(),
headers={"Content-Type": "application/json"},
)
return json.load(urllib.request.urlopen(req))["allowed"]The decision is advisory: only the application enforces it. Treat a failed or unreachable decision request as a refusal, the same fail-closed rule Agent Guard applies to its own policy evaluation.
State, cache, and workspace
AGENTGUARD_STATE_DIR(andXDG_STATE_HOMEandXDG_DATA_HOMEunder it) persists across runs in the same environment. Keep conversation history, indexes and user data here.XDG_CACHE_HOMEis emptied at the start of every session. Keep anything you can rebuild here.AGENTGUARD_WORKSPACEis the user's project directory, read-write, at the same path as on the host. It is absent when the run has no workspace, so check for it.
Everything else the application writes to its own filesystem is discarded when the container exits.
Skills
The definition's skill modules are copied into AGENTGUARD_SKILLS_DIR, one directory per skill, read-only. Each has a SKILL.md. Nothing loads them for you: the application lists the directory and reads what it needs. The directory always exists, and is empty when the definition has no skills.
Input, output, and exit
When agentguard run is started from a terminal, the application gets a terminal: stdin is interactive, and the terminal size follows window resizes. Without a terminal, stdin is empty and output goes to the microVM's boot log.
The application's exit code becomes the exit code of agentguard run. When the session ends first, for example because agentguard run is interrupted, the application gets SIGTERM and 5 seconds to exit before it is killed. Write state as you go rather than on shutdown.
If the Jozu gateway stops while the application is running, Agent Guard ends the session: an application with no gateway has no models, tools, or policies behind it.
Testing outside Agent Guard
Nothing in the image is specific to Agent Guard except the environment it reads, so you can run it anywhere that provides the same variables. Pointing OPENAI_BASE_URL and OPENAI_API_KEY at a real provider, with the model name set to one that provider knows, exercises the model path. To test the Jozu gateway path, including model name resolution and policies, run it under Agent Guard with a definition that names it; see Run and operate a runtime.
