Are you an LLM? You can read better optimized documentation at /docs/agent-guard/custom-runtimes/tutorial.md for this page in Markdown format
Build your first custom runtime
This tutorial takes a short Python program from source to a running custom agent runtime: you build it into an image, name it in an agent definition that gives it one model, and run it under Agent Guard. Along the way you see the three things a custom runtime gets from Agent Guard: its environment, a model it can call without holding a key, and a refusal for a model it was not given.
It pushes to a registry on your machine, so nothing is published. The one model call uses your OpenAI API key.
Prerequisites
- Agent Guard installed (see Quickstart).
- Docker with
buildx. - The
kitCLI. Get it from kitops.org orbrew install kitops. OPENAI_API_KEYset in your shell, for an account with API credit.
Make a directory to work in:
bash
mkdir -p hello-runtime/app hello-runtime/definition hello-runtime/workspace
cd hello-runtimeStep 1: Write the application
The application asks one model one question. Everything it needs comes from environment variables Agent Guard sets in the container: the Jozu AI Gateway's URL, a session token, and the values the agent definition passes it. It holds no provider key, and it names its model assistant, the name of the module the agent definition declares in step 3.
Save this as app/app.py:
python
import json
import os
import sys
import urllib.error
import urllib.request
BASE_URL = os.environ["AGENTGUARD_OPENAI_BASE_URL"]
TOKEN = os.environ["AGENTGUARD_OPENAI_API_KEY"]
MODEL = os.environ.get("HELLO_MODEL", "assistant")
def ask(model, question):
req = urllib.request.Request(
BASE_URL + "/chat/completions",
data=json.dumps({
"model": model,
"messages": [{"role": "user", "content": question}],
}).encode(),
headers={
"Content-Type": "application/json",
"Authorization": "Bearer " + TOKEN,
},
)
try:
with urllib.request.urlopen(req) as resp:
return json.load(resp)["choices"][0]["message"]["content"]
except urllib.error.HTTPError as err:
return "refused (HTTP %d): %s" % (err.code, err.read().decode()[:300])
def main():
print("session: ", os.environ.get("AGENTGUARD_SESSION_ID"))
print("workspace: ", os.environ.get("AGENTGUARD_WORKSPACE", "(none)"))
print("state dir: ", os.environ.get("AGENTGUARD_STATE_DIR"))
print("greeting: ", os.environ.get("GREETING", "(not set)"))
print()
question = " ".join(sys.argv[1:]) or "Say hello in five words."
print("%s> %s" % (MODEL, ask(MODEL, question)))
# A model the agent definition does not declare.
print("gpt-4o> %s" % ask("gpt-4o", question))
if __name__ == "__main__":
main()Step 2: Build the image
Save this as app/Dockerfile:
dockerfile
FROM python:3.12-alpine
COPY app.py /app/app.py
LABEL dev.jozu.agentguard.min-version=0.8.0
ENV HELLO_MODEL=assistant
WORKDIR /app
ENTRYPOINT ["python3", "-u", "/app/app.py"]Start a registry on your machine and push the image to it, built for linux/arm64, the platform of Agent Guard's microVM:
bash
docker run -d -p 127.0.0.1:5050:5000 --name agentguard-tutorial-registry registry:2
docker buildx build --platform linux/arm64 \
-t 127.0.0.1:5050/tutorial/hello-runtime:v1 --push appThe registry listens on port 5050 because port 5000 is often taken on macOS.
Step 3: Write the agent definition
The definition names the image, gives the application one model, and passes it one value. Save this as definition/agent-definition.yaml:
yaml
apiVersion: agentguard.jozu.dev/v1
kind: AgentDefinition
metadata:
name: hello-runtime
spec:
framework: 127.0.0.1:5050/tutorial/hello-runtime:v1
needs:
- name: OPENAI_API_KEY
- name: GREETING
from:
text: hello from the agent definition
includes:
- name: assistant
type: llm
source:
model: openai/gpt-4o-mini
env:
OPENAI_API_KEY: ${needs.OPENAI_API_KEY}frameworkis an image reference, so this definition runs the image rather than a CLI agent.- The
assistantmodule is what the model nameassistantmeans:gpt-4o-miniat OpenAI, with your key. OPENAI_API_KEYis read from your shell and handed to the Jozu gateway, because theassistantmodule uses it. The application never sees it.GREETINGis a literal that reaches the application as an environment variable.
Save this as definition/Kitfile:
yaml
manifestVersion: 1.0.0
package:
name: hello-runtime
version: 1.0.0
description: Agent definition for the custom runtime tutorial
code:
- path: agent-definition.yamlPack the definition and push it to the same registry as the image:
bash
kit pack definition -t 127.0.0.1:5050/tutorial/hello-agent:v1
kit push --plain-http 127.0.0.1:5050/tutorial/hello-agent:v1The definition and the image are on the same registry host on purpose: --plain-http, which the next step needs for a local registry, applies to the definition's registry host.
Step 4: Run it
bash
agentguard run 127.0.0.1:5050/tutorial/hello-agent:v1 \
--plain-http --env hello-runtime -w ./workspaceBefore the microVM boots, Agent Guard resolves the definition and its values, then resolves the image to a digest and fetches it:
▸ Resolving agent runtime needs...
OPENAI_API_KEY resolved (envFrom, target=env)
GREETING resolved (text, target=env)
Agent definition resolved: 1 LLMs, 0 MCPs, 0 policies, 0 skills, 2 needs
▸ Resolving custom runtime image: 127.0.0.1:5050/tutorial/hello-runtime:v1
Runtime image 127.0.0.1:5050/tutorial/hello-runtime:v1 resolved to sha256:67a2bcfe47f5...
Withheld from the agent environment, delivered to the Jozu gateway instead: OPENAI_API_KEYThe last line is the credential boundary: the key went to the Jozu gateway, not to the container.
Then the application runs and prints:
session: 70515-1790373070394882000
workspace: /path/to/hello-runtime/workspace
state dir: /var/lib/agentguard/app-state
greeting: hello from the agent definition
assistant> <the model's reply>
gpt-4o> refused (HTTP 400): {"error":{..."code":"model-not-declared",...}}The assistant line is a reply from gpt-4o-mini, reached through the Jozu gateway with a key the application never had. If your OpenAI account has no API credit, this line is a 429 insufficient_quota error from OpenAI instead, which still shows the request reached the provider.
The gpt-4o line is the Jozu gateway refusing a model the definition does not declare. The request never left the microVM.
The application exits, and so does agentguard run, with the application's exit code.
Step 5: Find the refusal in the audit trail
Every session writes a policy audit file named after its session ID, the value the application printed as session:
bash
jq -c 'select(.allowed == false)' ~/Library/Logs/AgentGuard/policy_audit.<session>.jsonljson
{"allowed":false,"action":"model-not-declared", ...}Step 6: Run it again
Run the same command a second time. The image is already cached, so the host only looks up its digest:
Runtime image 127.0.0.1:5050/tutorial/hello-runtime:v1 is already cached (sha256:67a2bcfe47f5...)The hello-runtime environment also kept the imported image, so the application starts sooner. Anything the application writes under AGENTGUARD_STATE_DIR would still be there too.
What just happened
- On the host, Agent Guard resolved the definition's two needs, resolved the image tag to a linux/arm64 digest, and cached the image under that digest.
- The microVM booted with the Jozu gateway running. The guest imported the image, checked its digest against the one the host resolved, and started it as a rootless container.
- The container received the Jozu gateway's URL, a session token, and
GREETING, and no provider key. The key went to the Jozu gateway's entry for theassistantmodule. - The Jozu gateway mapped the model name
assistanttogpt-4o-miniat OpenAI, and refusedgpt-4obecause no module declares it. The refusal was written to the session's audit file. - When the application exited, the session ended with its exit code.
Clean up
bash
agentguard env delete --yes hello-runtime
docker rm -f agentguard-tutorial-registryThe cached image archive stays in ~/.agentguard/images/ until you delete it.
Next steps
- Build a runtime image: calling tools, skills, state, and exit behavior.
- Declare a runtime in an agent definition: adding MCP servers, skills, and policies to the definition.
- Run and operate a runtime: registries, caching, resources, and records.
- Limitations: the boundaries a custom runtime does not enforce.
