Pick your track. Two workshops share this page. LP1 is the developer intro to Docker Sandboxes. LP2 is the governance-oriented walkthrough for teams that need to answer what an AI agent ships, sees, and can do. Both use the same underlying stack, so you can switch tracks at any time and your position in the shared modules carries over.

Track:

SBX 101: Hands-on with Docker Sandboxes

Docker AI Governance

A hands-on workshop on SBX, DHI, MCP, Kits, and multi-agent orchestration.

A hands-on workshop on Docker's stack for safe, auditable, and compliant AI-agent adoption.

Pick your AI provider below. If you have a Claude / Anthropic API key, use Claude. The workshop's final steps are optimized for Anthropic. If you don't have Claude, sbx supports other providers, but for this lab we recommend OpenCode if you want a free option. You can switch at any time.

Your provider:

Google is transitioning Gemini CLI to Antigravity CLI. Antigravity does work with sbx, but it's not out of the box yet and requires a few extra steps, so we're skipping it for this workshop.

Codex may print a warning on its first prompt (stream disconnected before completion, or a flagged attack-attempt notice). That's expected: Codex's traffic to OpenAI is being routed through sbx's egress proxy, which trips Codex's own first-time connection checks.

Quick Setup

A short setup checklist to get you ready before we start the lab. About 5 minutes. The full walkthrough with explanations lives in Getting Started below.

  1. Sign in to Docker Hub. Create an account at hub.docker.com if you don't already have one (30 seconds).
  2. Have an Anthropic API key ready to go. Generate one at console.anthropic.com under API Keys.
  3. Have an OpenAI API key ready to go. Generate one at platform.openai.com/api-keys.
  4. Have an OpenCode API key ready to go. Generate one at opencode.ai.
  5. Install Homebrew (Mac users, if you don't have it). Get it from brew.sh. We use it for every install in this workshop.
  6. Install sbx.
    brew install docker/tap/sbx
    winget install -h Docker.sbx
    Windows users: close and reopen your terminal after the install so sbx picks up on PATH. Otherwise the next command will report "sbx not found".
  7. Log in to sbx.
    host shell
    sbx login
    Opens a browser flow. Use the Docker Hub account from step 1.
Overview

What this workshop is

A working multi-agent system on Docker.

This 2-hour hands-on workshop walks through the Docker primitives that let you hand AI agents meaningful autonomy without losing control over what they do. Over eight steps you'll start a sandbox, prove its isolation guarantees, govern its network, harden the containers it ships, attach tools via MCP, standardize the setup as a kit, compose multiple agents with Docker Agent, and observe how per-role tool scoping holds as a runtime boundary.

Before you start, you'll need:

  • A Docker account. Sign up at hub.docker.com if you don't have one (takes 30 seconds), then sign in.
  • An AI provider API key: Anthropic (for Claude Code), OpenAI (for Codex), or OpenCode (free tier on Zen).
  • A terminal of your choice (MacOS Terminal, iTerm2, Warp, PowerShell, or any equivalent).
  • A supported platform: MacOS Apple Silicon, Linux x86_64 with KVM access, or Windows 11 x86_64. MacOS Intel is not supported.

The detailed install and configuration steps are in Getting Started.

Overview

What this workshop is

Docker's stack for safe, auditable, and compliant AI-agent adoption.

AI agents are moving from experiments to production, and enterprises need to answer three governance questions before they let one loose in their stack: what does the agent ship, what can the agent see, and what can the agent do. This workshop takes each of those in order and shows the Docker primitive that answers it. Every module ends in an artifact your security or compliance team can review: a hardened image, an audit log, a scoped yaml, or a portable kit.

The three governance questions

  • What does the agent ship? Any container an agent produces will go to production somewhere. If the base image has known CVEs, so does everything you ship. Docker Hardened Images (DHI), the foundational module below, give you a supply-chain-grade base to build on.
  • What can the agent see? An agent that can reach anything on the internet, read any file on your host, or grab any credential is a compliance nightmare. Docker Sandboxes and their network policy bound the agent's environment and audit every outbound request.
  • What can the agent do? Even inside a sandbox, the agent's tool set determines the scope of its actions. The Docker MCP Gateway, sbx kits, and Docker Agent yaml let you declare tool sets, scope them per team or per role, and enforce the boundary at the runtime.

What you'll leave with

  • A mental model of each governance question, mapped to a Docker primitive.
  • A running sandbox on your laptop with every primitive in place.
  • A kit template you can adapt and commit to your team's repo.
  • A per-role Docker Agent yaml that reads as an auditable compliance artifact.

Before you start, you'll need:

  • A Docker account. Sign up at hub.docker.com if you don't have one (takes 30 seconds), then sign in.
  • An AI provider API key: Anthropic (for Claude Code), OpenAI (for Codex), or OpenCode (free tier on Zen).
  • A terminal of your choice (MacOS Terminal, iTerm2, Warp, PowerShell, or any equivalent).
  • A supported platform: MacOS Apple Silicon or Windows 11 x86_64. Linux users should run the workshop inside a supported guest OS (VMware Fusion, Parallels, UTM). Native Linux is not yet supported by the workshop's sbx path.

The detailed install and configuration steps are in Getting Started. LP2 needs Docker Desktop installed before the Foundational DHI module, so make sure it's up and running by the time you finish Getting Started.

Getting Started

Pre-flight checklist

Confirm your environment before Step 1.

Platform support

PlatformStatus
MacOS Apple SiliconSupported
Linux x86_64 (Ubuntu 22.04+, KVM access)Supported
Windows 11 x86_64Supported
MacOS IntelNot supported

Required

  • A Docker account (free tier works), signed in.
  • An AI provider API key: Anthropic (for Claude Code), OpenAI (for Codex), or OpenCode (free tier on Zen).
  • A terminal of your choice (MacOS Terminal, iTerm2, Warp, PowerShell, or any equivalent). Windows users on WSL can follow the MacOS commands.

Install sbx

MacOS users The MacOS install commands in this workshop use Homebrew. If you don't already have it, install it from brew.sh first (one terminal command, takes a couple of minutes). Once brew --version prints a version, come back here.

brew install docker/tap/sbx
winget install -h Docker.sbx
# Or download the MSI from
# https://github.com/docker/sbx-releases/releases

Windows users Close and reopen your terminal after the install so sbx picks up on PATH. Otherwise the next command will report "sbx not found".

Verify the install

host shell
sbx version
Should look like
sbx version: v0.33.0 xxxxx

Log in to Docker

Heads up: personal Docker account only If your Docker Hub account is part of an organization that has Docker AI Governance enabled, that org's policies will override the workshop's defaults and several steps won't behave as documented. Sign in with a personal Docker Hub account for this workshop, not with an org-linked one. If you don't have a personal account, create a free one at hub.docker.com before continuing.

host shell
sbx login

Opens a browser-based device confirmation flow. After sign-in, depending on your version, you may be prompted to pick a default network policy. If you are, pick Balanced (option 2).

If you weren't prompted, set the default explicitly with:

host shell
sbx policy reset

You'll see the three options:

Select a default network policy for your sandboxes:

     1. Open         - All network traffic allowed, no restrictions.
  >  2. Balanced     - Default deny, with common dev sites allowed.
     3. Locked Down  - All network traffic blocked unless you allow it.

  Use the arrow keys or 1-3 to navigate, Enter to confirm.

Pick Balanced. This is the policy we'll use throughout the workshop.

Get your API key

Follow the steps for the provider you picked at the top of the page. Name the key sbx so you can recognize it later.

Claude (Anthropic). You'll need an Anthropic account with API credits.

  1. Go to platform.claude.com and sign in.
  2. Click API Keys in the left sidebar.
  3. Click Create Key, name it sbx, and copy the value.

Codex (OpenAI). You'll need an OpenAI account with billing enabled.

  1. Go to platform.openai.com/api-keys and sign in.
  2. Click Create new secret key, name it sbx, and copy the value.

Gemini (Google). The Gemini API has a generous free tier, so you may not need to enable billing.

  1. Go to aistudio.google.com/apikey and sign in.
  2. Click Create API key, name it sbx, and copy the value.

OpenCode (free). OpenCode's Zen tier includes a free model we'll use later in Step 1. No billing required.

  1. Go to opencode.ai.
  2. Click Zen in the top menu, then sign in (or create a free account).
  3. Click API Keys in the left sidebar.
  4. Click Create Key, name it sbx, and copy the value.

Heads up The last two steps of the workshop are optimized specifically for Claude / Anthropic, which is one of the reasons we encourage an Anthropic key for this workshop. If you just created one above, keep the value handy; you'll need to paste it again later. If you're on a different provider, you can follow along, but certain parts of those later steps might not work for you.

Store your API key

Store the key you just created with sbx so the sandbox can use it. The CLI walks you through pasting the key:

host shell
sbx secret set -g anthropic
host shell
sbx secret set -g openai
host shell
sbx secret set -g google
host shell
sbx secret set -g opencode

Verify it's stored:

host shell
sbx secret ls
Should look like
SCOPE    TYPE     NAME        SECRET
global   service  anthropic   ****...****
Should look like
SCOPE    TYPE     NAME      SECRET
global   service  openai    ****...****
Should look like
SCOPE    TYPE     NAME      SECRET
global   service  google    ****...****
Should look like
SCOPE    TYPE     NAME        SECRET
global   service  opencode    ****...****

To remove it later:

host shell
sbx secret rm -g anthropic
host shell
sbx secret rm -g openai
host shell
sbx secret rm -g google
host shell
sbx secret rm -g opencode

Install Docker Desktop (LP2)

LP2 uses Docker Desktop in two places later in the workshop: the Foundational Docker Hardened Images module (which uses docker scout for the CVE scan), and Step 4 (where we register MCP servers that ship as Docker images and touch the Docker MCP Gateway). Neither module is optional in LP2, so we install Docker Desktop now.

Heads up Docker Desktop is not required to run sbx itself. Steps 1 through 3 use sbx alone. We're installing Docker Desktop now for the Foundational DHI module below and for the MCP work in Step 4.

brew install --cask docker
winget install -h Docker.DockerDesktop

Then launch Docker Desktop from your Applications folder (MacOS) or Start menu (Windows), or open it from the command line:

open -a Docker
Start-Process "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe"

Wait for it to finish starting. On MacOS the whale icon in the menu bar stops animating; on Windows the system tray icon turns solid. Then confirm the daemon is up:

host shell
docker info

If it prints engine details (server version, OS, kernel), you're set. If it prints Cannot connect to the Docker daemon, Docker Desktop hasn't finished starting yet. Wait a few seconds and try again. If you just installed Docker for the first time, open a fresh terminal so your shell picks up the updated PATH.

Now sign in to Docker Hub. LP2's Foundational module pulls a Docker Hardened Image (dhi.io) that needs a Docker Hub login, and Step 4 pulls MCP server images from docker.io/mcp/*. On a fresh install, Docker Desktop often pops a sign-in dialog the first time you launch it. If you see it, sign in with your Docker Hub account and you're done. Otherwise, sign in from the terminal:

host shell
docker login

Either prompts for your Docker Hub username and access token, or opens a browser with a device-flow code to enter.

Create your workshop directory

We'll keep everything we create today inside a single umbrella folder, ~/workshop, with the sbx workspace at ~/workshop/sbx. That way nothing sprays into your home directory and everything is easy to clean up at the end.

mkdir -p ~/workshop/sbx
New-Item -ItemType Directory -Force -Path $HOME/workshop/sbx | Out-Null
cd ~/workshop/sbx
cd $HOME/workshop/sbx
Foundational

Docker Hardened Images

The supply-chain anchor. Ground here first, before we look at anything the agent does.

Before we go anywhere near sbx, let's ground on the piece your security and platform teams already recognize: Docker Hardened Images (DHI). An AI agent that builds and ships containers ends up putting whatever base image you started from into production. If that base has known CVEs, so does everything you ship on top of it. Hardening the base is the single-highest-leverage supply-chain move you can make, and it's the one a security reviewer will ask about first.

The rest of the workshop assumes the base your agent builds on is trustworthy. This foundational module makes sure it is.

F.1 What a DHI is

Think of DHIs the way a chef thinks about ingredients. Before you make a meal you want to know the flour doesn't have worms, the eggs aren't spoiled, and the oil didn't come out of a can that was already open. In software, "the ingredients aren't rotten" translates to: the base image your agent builds on doesn't ship with known CVEs, is signed by someone you trust, and has a documented list of what's inside. A Docker Hardened Image is that trustworthy ingredient.

Concretely, a DHI is a base image (python, node, nginx, alpine, etc.) that Docker has stripped down, patched, and re-signed to have near-zero known CVEs. Each DHI ships in two variants:

  • A dev variant with a shell and a package manager, used in the build stage of a multi-stage Dockerfile.
  • A prod variant with no shell, no extras, no package manager, just what the runtime needs. This is the variant that actually ships.

Docker maintains DHIs with enterprise SLAs on vulnerability remediation, and each ships with a signed SBOM (Software Bill of Materials) listing every package inside. That SBOM is the telemetry your compliance team uses to prove and instantiate every supply-chain claim you make about the image: "this component, this version, this license, this vulnerability status." Without the SBOM you're vouching for the image from memory; with the SBOM you're pointing at a signed document. DHIs live under the dhi.io namespace and require a one-time login with your Docker Hub credentials before you can pull them.

Prep for this module The commands below need Docker Desktop running and you signed in to Docker Hub, both set up in Getting Started's Install Docker Desktop substep. If you skipped it, go do it now, then come back here. Otherwise you're set.

F.2 Sign in to the DHI registry

DHI pulls require a login to dhi.io. Same Docker Hub credentials, different registry endpoint:

host shell
docker login dhi.io

F.3 Compare a standard base to its hardened variant

Pull a standard Python base and its DHI equivalent, then scan both with Docker Scout to see the delta:

host shell
docker pull python:3.14-slim
host shell
docker pull dhi.io/python:3.14

Scan the standard base first:

host shell
docker scout quickview python:3.14-slim

Take note of the CVE counts under Critical, High, Medium, and Low. On a recent build of python:3.14-slim you'll typically see a handful of Highs and a longer tail of Mediums and Lows. That's normal for a general-purpose base: it packs a shell, a package manager, dev headers, and the transitive dependencies of everything it might need.

Now the hardened variant:

host shell
docker scout quickview dhi.io/python:3.14

Same Python version, same runtime capability, near-zero CVEs. That's the payoff. The image is smaller too: no shell, no package manager, nothing you don't need to run the code.

F.4 Why this is the foundational module for the rest of the workshop

Everything that follows assumes a hardened base. When Step 5 has the agent build a research app and swap in a DHI, the CVE delta you just observed is the delta you'll see there too. Same mechanic, applied to code the agent wrote. When the Docker Agent team in Step 7 produces artifacts, the images backing those tools are DHIs. The rest of the workshop is about what the agent does with a trustworthy base; this module is the base itself.

Governance takeaway: the artifact from this module is the CVE quickview output, which your compliance team can attach to a supply-chain review. In the Extended path (2-4 hour delivery), we go deeper on SBOMs, provenance attestations, and Docker Scout policies applied against the images.

Step 1

SBX basics: your first sandbox

Sandbox as an audited trust boundary

Spin up a Docker Sandbox with an agent inside, and confirm it's alive.

The primitive that answers "what can the agent see?", plus the artifact that documents the boundary.

You'll create a sandbox on the host, verify it's running, attach your terminal to the agent inside, and then ask the agent three quick questions about its environment. By the end you'll have proved that the agent really is running on Linux in a microVM with its own kernel, its own user, and access to exactly the directory you mounted. Nothing more.

Governance framing: the sandbox is a VM with a hostname, an audit trail, and no residue outside the workspace mount. Every artifact in this workshop, from the policy log in the next step through the yaml in Step 7, assumes this boundary as the trust anchor. If the boundary isn't real, none of the downstream artifacts mean anything to your auditor. This step establishes the boundary and the artifacts that prove it.

Assumes you completed Getting Started (install, login, secret, workspace setup).

1.1 Create your sandbox

sbx create registers a new sandbox with the local sbx daemon and starts it. By the time the command returns, the microVM is up and the agent is running inside it. Your terminal isn't connected to the agent yet; that's the next step. The --name flag gives the sandbox a label you'll use to refer to it later (in sbx run, sbx stop, sbx rm, and the policy / log commands in Step 3). The final argument is the directory to mount as the sandbox's workspace; whatever lives in that directory is what the agent will see, and the only host directory it will have access to.

You can write that argument two ways. . is shorthand for "the current directory" and works if you're already cd'd into the folder you want mounted. An explicit path like ~/workshop/sbx works from any terminal, regardless of where you've cd'd. We'll use the explicit form throughout this workshop so the commands are reliable across terminals.

Run the command for the provider you picked at the top:

sbx create --name sandbox-alpha claude ~/workshop/sbx
sbx create --name sandbox-alpha claude $HOME/workshop/sbx
sbx create --name sandbox-alpha codex ~/workshop/sbx
sbx create --name sandbox-alpha codex $HOME/workshop/sbx
sbx create --name sandbox-alpha gemini ~/workshop/sbx
sbx create --name sandbox-alpha gemini $HOME/workshop/sbx
sbx create --name sandbox-alpha opencode ~/workshop/sbx
sbx create --name sandbox-alpha opencode $HOME/workshop/sbx

First run pulls the agent's template image from Docker Hub if it isn't already cached locally (1-2 minutes).

Windows users: if this fails with "Windows Hypervisor Platform is not enabled" sbx boots each sandbox as a Linux microVM, and on Windows that requires the Hypervisor Platform feature to be turned on. To turn it on, open PowerShell as Administrator (right-click PowerShell in the Start menu, choose Run as administrator) and run:

PowerShell (as Administrator)
dism /Online /Enable-Feature /FeatureName:HypervisorPlatform /All

You'll likely need to restart your computer afterward for the change to take effect, then re-run sbx create.

1.2 See what's running

sbx ls lists every sandbox the sbx daemon knows about along with its agent type, status, exposed ports, and workspace path. It's the command you'll come back to any time you want to see what sandboxes exist on your machine. Right after sbx create the sandbox you just made should show running.

host shell
sbx ls
Should look like
SANDBOX         AGENT   STATUS   PORTS   WORKSPACE
sandbox-alpha   claude  running          /Users/<you>/workshop/sbx
Should look like
SANDBOX         AGENT   STATUS   PORTS   WORKSPACE
sandbox-alpha   codex   running          /Users/<you>/workshop/sbx
Should look like
SANDBOX         AGENT    STATUS   PORTS   WORKSPACE
sandbox-alpha   gemini   running          /Users/<you>/workshop/sbx
Should look like
SANDBOX         AGENT      STATUS   PORTS   WORKSPACE
sandbox-alpha   opencode   running          /Users/<you>/workshop/sbx

1.3 Connect to the agent

sbx run attaches your terminal to the agent that's already running inside the sandbox. It doesn't start anything new. From this point on, what you type goes to the agent, not to your host shell. To get back to the host you'll exit the agent (typically Ctrl+C twice) or open a second terminal.

host shell
sbx run --name sandbox-alpha

The first time you connect, your agent might ask something like:

  Do you trust the contents of this directory? Working with untrusted
  contents comes with higher risk of prompt injection.

› 1. Yes, continue
  2. No, quit

If you see it, that's the agent's own startup safety check, asking you to confirm you intended to give it access to this workspace. Pick 1. Yes, continue. If you don't see this prompt, no action needed; the agent will go straight to its own prompt and you can start talking to it.

1.3.1 Pick your model (OpenCode users only)

OpenCode supports multiple model backends. You can plug in your own paid API key (Anthropic, OpenAI, Google), use the free model on Zen, or whatever else you've configured. Pick the one you want to use for this workshop:

  1. Press Ctrl+X, then M to open the model picker (or Ctrl+P to open the full command palette).
  2. Select the model you want to use. If you don't have a paid API key, pick Big Pickle on Open Zen (it's free).

You only need to do this once per sandbox. OpenCode remembers the selection for future sessions.

1.4 Ask the agent what it sees

To prove the agent is actually running inside the sandbox (not on your host), have it print three things: the kernel string, its user identity, and its current directory. Each tells us something about the environment it's in.

OpenCode users Your first prompt may fail with a network policy block: OpenCode tries to reach opencode.ai, which isn't on the default allow list. Open a second host shell and run sbx policy allow network opencode.ai, then retry the prompt. We dig into how this diagnose-and-allow loop works in Step 3.

Prompt to agent
Run uname -a; whoami; pwd and tell me what you see.
Expected
Linux <vm-hostname> 7.0.11 #1 SMP ... Linux
agent
/Users/<your-username>/workshop/sbx

The first line confirms it's on Linux with its own kernel, even though your host is MacOS or Windows. The second line shows it's running as a non-root agent user, not as you. The third line shows the working directory is the one you mounted, accessible at the same absolute path inside the VM as it has on your host.

Go deeper: confirm the distro and kernel

Want extra proof this is a Linux VM and not a tighter sandbox running on your Mac? Have the agent print the OS release and the kernel version:

Prompt to agent
Run cat /etc/os-release; uname -r and tell me what you see.

The agent will report a real Linux distribution and a kernel version distinct from your host. Each sbx sandbox boots its own Linux microVM with its own kernel and root filesystem. It isn't a process-level sandbox running on the host kernel.

Go deeper: run a second sandbox alongside this one

Sandboxes are independent. You can run more than one at a time, each with its own VM, workspace, and policy. Open another host terminal and create a second:

sbx create --name sandbox-bravo claude ~/workshop/sbx
sbx create --name sandbox-bravo claude $HOME/workshop/sbx
sbx create --name sandbox-bravo codex ~/workshop/sbx
sbx create --name sandbox-bravo codex $HOME/workshop/sbx
sbx create --name sandbox-bravo gemini ~/workshop/sbx
sbx create --name sandbox-bravo gemini $HOME/workshop/sbx
sbx create --name sandbox-bravo opencode ~/workshop/sbx
sbx create --name sandbox-bravo opencode $HOME/workshop/sbx

List them both to confirm the new sandbox exists alongside the first:

host shell
sbx ls

You'll see sandbox-alpha and sandbox-bravo as two independent rows. Connect to bravo:

host shell
sbx run --name sandbox-bravo

Bravo has its own hostname, its own process list, and its own policies. The two sandboxes don't share state; anything you do in one is invisible to the other.

When you're done exploring, stop bravo:

host shell
sbx stop sandbox-bravo

Then remove it:

host shell
sbx rm sandbox-bravo

Confirm it's gone:

host shell
sbx ls

Only sandbox-alpha remains. The stop / rm split is deliberate: stop shuts the VM down but keeps the sandbox record and its workspace mount around, so you can start it again with sbx run. rm deletes the record. Use stop when you're pausing; use rm when you're done.

Key learnings

You started an agent inside a microVM with its own Linux kernel. Only the directory you passed to sbx create is shared into the sandbox; everything else stays on your host. Your API key lives in the OS keychain and never enters the VM, because the credential proxy injects auth headers at the network layer rather than passing the raw key through. The same setup works for Claude Code, Codex, Gemini CLI, or OpenCode: the agent is interchangeable, the boundary is not.

The boundary is structural. It is not a permission dialog and not a system prompt. The hypervisor enforces it.

Step 2

Isolation Proof: what the sandbox shares and what it doesn't

Three quick tests prove the boundary: the workspace shares both ways, host files outside are unreachable, secrets are usable but not exfiltrable.

We'll run three tests to show that the sandbox boundary is enforced by the runtime, not by trust in the agent. First, we drop a file into the workspace from the host and have the agent read it (and vice versa) to confirm the shared workspace works in both directions. Second, we drop a file outside the workspace on the host and ask the agent to read that. It should fail. Third, we check whether the API key the agent is using to talk to its model provider right now is actually visible anywhere inside the sandbox. By the end you'll have direct evidence that the access boundary is a mount table, not a permission dialog.

You'll need two terminals: Terminal A is the agent inside the sandbox (already running from Step 1). Terminal B is your host shell.

2.1 The workspace IS shared, both directions

Host to agent. On your host terminal, drop a file in the workspace:

echo "Written from the host" > ~/workshop/sbx/host-to-agent.txt
"Written from the host" | Out-File $HOME/workshop/sbx/host-to-agent.txt

Now inside the sandbox, ask the agent to read it:

Prompt to agent
There should be a file called host-to-agent.txt in your workspace. Run cat host-to-agent.txt and tell me what you see.
Expected (inside sandbox)
Written from the host

Agent to host. Now ask the agent to write a file:

Prompt to agent
Create a file called agent-to-host.txt in your workspace with the text "Written from the agent".

Back on your host terminal, first list the workspace to see the file appear, then open it to confirm the contents:

ls -la ~/workshop/sbx
Get-ChildItem $HOME/workshop/sbx
cat ~/workshop/sbx/agent-to-host.txt
Get-Content $HOME/workshop/sbx/agent-to-host.txt
Expected (on host)
total 16
drwxr-xr-x   4 you  staff   128 ... .
drwxr-xr-x  20 you  staff   640 ... ..
-rw-r--r--   1 you  staff    22 ... agent-to-host.txt
-rw-r--r--   1 you  staff    22 ... host-to-agent.txt
Written from the agent

Heads up Inside the sandbox, ~ is the agent's home (/home/agent), not the workspace. Use relative paths or the workspace's absolute path (/Users/<you>/workshop/sbx) when working inside the sandbox.

2.2 Outside the workspace, host files are invisible

echo "This file lives in your home, not the workspace" > ~/workshop/host-marker.txt
"This file lives in your home, not the workspace" | Out-File $HOME/workshop/host-marker.txt
Prompt to agent
Try to read a file called host-marker.txt. Run cat ~/workshop/host-marker.txt 2>&1; cat /Users/<your-username>/workshop/host-marker.txt 2>&1 and tell me what happens.
Expected (inside sandbox)
cat: /home/agent/workshop/host-marker.txt: No such file or directory
cat: /Users/<your-username>/workshop/host-marker.txt: No such file or directory

The file is right there on your host. The sandbox can't see it. Only ~/workshop/sbx is mounted in.

2.3 Secrets exist on host, agent can't read them

host shell
sbx secret ls
Should look like
SCOPE    TYPE     NAME        SECRET
global   service  anthropic   ****...****
Prompt to agent
Run echo "ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY"; env | grep -i anthropic and tell me what you see.
Expected (inside sandbox)
ANTHROPIC_API_KEY=
grep returns nothing. No Anthropic-related vars in the agent's environment.
Should look like
SCOPE    TYPE     NAME      SECRET
global   service  openai    ****...****
Prompt to agent
Run echo "OPENAI_API_KEY=$OPENAI_API_KEY"; env | grep -i openai and tell me what you see.
Expected (inside sandbox)
OPENAI_API_KEY=
grep returns nothing. No OpenAI-related vars in the agent's environment.
Should look like
SCOPE    TYPE     NAME      SECRET
global   service  google    ****...****
Prompt to agent
Run echo "GOOGLE_API_KEY=$GOOGLE_API_KEY"; env | grep -i google and tell me what you see.
Expected (inside sandbox)
GOOGLE_API_KEY=
grep returns nothing. No Google-related vars in the agent's environment.
Should look like
SCOPE    TYPE     NAME        SECRET
global   service  opencode    ****...****
Prompt to agent
Run echo "OPENCODE_API_KEY=$OPENCODE_API_KEY"; env | grep -i opencode and tell me what you see.
Expected (inside sandbox)
OPENCODE_API_KEY=
grep returns nothing. No OpenCode-related vars in the agent's environment.

The secret demonstrably exists on the host. Inside the sandbox it's not in any environment variable. Yet the agent is talking to you right now using that exact key. The credential proxy injects the auth header at the network layer.

Cleanup

rm ~/workshop/host-marker.txt ~/workshop/sbx/host-to-agent.txt ~/workshop/sbx/agent-to-host.txt
Remove-Item $HOME/workshop/host-marker.txt, $HOME/workshop/sbx/host-to-agent.txt, $HOME/workshop/sbx/agent-to-host.txt
Go deeper: what "root in the sandbox" actually means

The agent runs as the agent user, but most sandbox templates grant it passwordless sudo so it can install packages, edit /etc, and behave like a developer with admin rights inside the VM. Let's see what root actually buys it.

Prompt to agent
Run sudo whoami; sudo cat /etc/shadow | head -3 and tell me what you see.

The first command returns root. The second prints the first three lines of /etc/shadow, the file that on a "normal" Linux system holds password hashes and that you'd panic about an attacker reading. Inside the VM, the agent has the keys to all of it.

This isn't a leak; it's the design. The isolation boundary in sbx isn't the user namespace, it's the VM itself. Root-in-VM cannot read your host's files outside the workspace mount, see your host's processes, bypass the network policy, or persist anything past sbx rm. The agent gets root inside its own sealed room so it can install packages, configure services, and behave like a developer; that root has zero reach outside the room. If you want a stricter posture, you can ship a custom template that strips sudo and the agent becomes a regular non-root Unix user. But either way, the wall that matters is the one between the VM and your host.

Go deeper: mount another folder with custom permissions

Beyond the workspace you mounted at create time, sbx can attach additional host directories. Just pass them as extra arguments to sbx create. Append :ro to any path to make it read-only.

First, create a docs folder on your host with a sample file in it so the agent has something to read:

mkdir -p ~/workshop/sbx-docs
New-Item -ItemType Directory -Force -Path $HOME/workshop/sbx-docs | Out-Null
echo "Read only reference material" > ~/workshop/sbx-docs/README.txt
Set-Content $HOME/workshop/sbx-docs/README.txt "Read only reference material"

Now spin up a sandbox with the current directory as a writable workspace and ~/workshop/sbx-docs mounted read-only:

sbx create --name sandbox-bravo claude ~/workshop/sbx ~/workshop/sbx-docs:ro
sbx create --name sandbox-bravo claude $HOME/workshop/sbx $HOME/workshop/sbx-docs:ro
sbx create --name sandbox-bravo codex ~/workshop/sbx ~/workshop/sbx-docs:ro
sbx create --name sandbox-bravo codex $HOME/workshop/sbx $HOME/workshop/sbx-docs:ro
sbx create --name sandbox-bravo gemini ~/workshop/sbx ~/workshop/sbx-docs:ro
sbx create --name sandbox-bravo gemini $HOME/workshop/sbx $HOME/workshop/sbx-docs:ro
sbx create --name sandbox-bravo opencode ~/workshop/sbx ~/workshop/sbx-docs:ro
sbx create --name sandbox-bravo opencode $HOME/workshop/sbx $HOME/workshop/sbx-docs:ro

Now attach to bravo and ask the agent to try both reading from and writing to the mounted folder:

host shell
sbx run --name sandbox-bravo
Prompt to agent
There's a read-only folder mounted in your sandbox at ../sbx-docs (sibling of the workspace). Run cat ../sbx-docs/README.txt to read what's in it, then run echo "agent wrote this" > ../sbx-docs/agent-wrote.txt 2>&1 to try to create a file there. Tell me what each command outputs.
Expected
Read only reference material
bash: ../sbx-docs/agent-wrote.txt: Read-only file system

The read succeeds because the agent has read access to the mount. The write fails because sbx applied the :ro flag at mount time, and read-only is enforced by the kernel at the mount layer, not by Unix file permissions. Even sudo (which the agent has from the privilege-escalation go-deeper) can't override it. The agent can write freely inside its workspace; outside the workspace, every additional mount honors the permissions you stamped on it at create time.

Exit the agent: press Ctrl+C twice.

This pattern is useful for giving an agent your team's docs, examples, or a shared library as reference material that it can read but can't accidentally (or deliberately) modify.

Clean up before moving on so everyone's starting Step 3 with just sandbox-alpha:

host shell
sbx stop sandbox-bravo
host shell
sbx rm sandbox-bravo
rm -rf ~/workshop/sbx-docs
Remove-Item -Recurse -Force $HOME/workshop/sbx-docs
Go deeper: confirm the agent can't see your host processes

The sandbox has its own process namespace. The agent inside can't see what's running on your host, even though both are on the same machine. To prove it, attach to sandbox-alpha (which you still have running from Step 1) and have the agent list what it can see:

host shell
sbx run --name sandbox-alpha
Prompt to agent
Run ps aux | head -20 and tell me what you see.

The list will contain only sandbox-internal processes. Typically the agent itself, a shell, and a couple of utilities. Your host's browsers, IDE, terminal, Slack, none of it appears.

Now compare against your host. Exit the agent (Ctrl+C twice) and run the equivalent command on your host shell:

ps aux | head -20
Get-Process | Select-Object -First 20

You'll see all the real things running on your machine: browser tabs, your editor, Spotify, Docker Desktop, dozens of background daemons. None of that was visible to the agent. Different namespace, different reality. The agent can't target what it can't see.

Key learnings

Inside the workspace, the host and the sandbox read and write the same files in real time. Outside the workspace, the host's files are unreachable from the sandbox. Secrets stored on the host are usable by the agent (because the credential proxy injects them at the network layer) but cannot be exfiltrated, because the raw key never enters the VM.

The boundary is not a permission dialog or a prompt the model agreed to follow. It is a mount table. The agent has exactly the access you gave it, no more.

Step 3

Network Policy: what the agent can reach

Network policy and the audit trail

See the five default allow groups, watch allow vs deny vs default-deny in action, add a custom deny rule that breaks pip install, then clean up.

Every outbound request the agent makes goes through a proxy that logs it. That log is your audit trail.

We'll list the network policies the sandbox starts with, then have the agent hit four URLs (two allowed by default, two not) to watch the proxy let some through and block others. We'll check the audit log on the host to see what got allowed and what got blocked, then add a custom deny rule for pypi.org. Once that rule is live we'll watch every workaround the agent might try fail in the same place. By the end you'll see that the network boundary is enforced at the host-side proxy, not in the agent. It doesn't matter how creative the model gets. Blocked traffic stays blocked.

Governance framing: the network policy answers "what can the agent reach?" and the policy log answers "what did the agent actually try to do?" Together they're the highest-value pair of artifacts an auditor can ask for. The compliance-ready audit report at the end of this step is what you hand a security reviewer to demonstrate that agent behavior is bounded AND observable.

You'll need two terminals open side by side throughout Step 3.

Terminal A: the agent inside sandbox-alpha. If you went through any of the Step 2 go-deepers, you may have exited the agent or switched to bravo (which we've since removed). If your terminal isn't currently sitting at the agent prompt, reconnect:

host shell
sbx run --name sandbox-alpha

If you never exited the agent (i.e., you skipped the Step 2 go-deepers), you're already attached and can skip the command above.

Terminal B: your host shell. This is where you'll run sbx policy commands and check the audit log. Position the two windows side by side so you can watch the agent's behavior in Terminal A while you change policy and read the log in Terminal B.

3.1 See the defaults

host shell
sbx policy ls
Should look like
POLICY                                 SOURCE  APPLIES TO             SUMMARY
907eda6d-fde5-4f68-ad90-42d2ecf2fa7f   kit     sandbox:sandbox-alpha  network: 3 allow
local-policy                           local   all                    network: 192 allow; filesystem read: 1 allow; filesystem write: 1 allow

Two rows, two levels of policy. The local-policy row (source local, applies to all) is the Balanced default you selected in Getting Started: 192 allowed hosts covering the destinations a coding workflow actually needs (Docker Hub, package managers for common languages, GitHub / GitLab, cloud provider APIs, OS package repos, popular AI-service endpoints). The other row (source kit, scoped to sandbox-alpha) is a small per-sandbox add-on that ships with the default agent kit. Everything not covered by these rules falls under default-deny.

3.2 Allow vs deny in action

Inside the sandbox, ask the agent to hit five URLs. Two that should be allowed by default (GitHub and PyPI, both in the defaults above), three that are not (Reddit, Wikipedia, and the official FIFA site). For each one we print the status code and the number of bytes downloaded, which makes "passed through" vs "blocked at the proxy" obvious at a glance.

Prompt to agent
Run these five curls and tell me what you see for each:
curl -s https://github.com -o /dev/null -w "github.com: %{http_code} (%{size_download} bytes)\n"
curl -s https://pypi.org -o /dev/null -w "pypi.org: %{http_code} (%{size_download} bytes)\n"
curl -s https://reddit.com -o /dev/null -w "reddit.com: %{http_code} (%{size_download} bytes)\n"
curl -s https://wikipedia.org -o /dev/null -w "wikipedia.org: %{http_code} (%{size_download} bytes)\n"
curl -s https://fifa.com -o /dev/null -w "fifa.com: %{http_code} (%{size_download} bytes)\n"
Should look like
github.com: 200 (254301 bytes)
pypi.org: 200 (147832 bytes)
reddit.com: 403 (0 bytes)
wikipedia.org: 403 (0 bytes)
fifa.com: 403 (0 bytes)

github.com and pypi.org are in default-allow lists, so the proxy forwards the request and you get a full page back (status 200, non-zero byte count). reddit.com, wikipedia.org, and fifa.com aren't on any allow list, so default-deny kicks in: the proxy refuses (status 403, zero bytes, nothing left the VM).

3.3 Check the audit trail

After the agent ran those curls, print the connection log on your host:

host shell
sbx policy log sandbox-alpha
Should look something like
Blocked requests:
HOST                REASON                                  COUNT
reddit.com:443      No matching allow rule (default deny)   1
wikipedia.org:443   No matching allow rule (default deny)   1
fifa.com:443        No matching allow rule (default deny)   1

Allowed requests:
HOST              RULE             COUNT
github.com:443    domain-allowed   1
pypi.org:443      domain-allowed   1
Trimmed for readability. Your real output also includes SANDBOX, TYPE, PROXY, RULE, and LAST SEEN columns.

Every outbound connection from the sandbox is recorded, split into two sections (Blocked requests and Allowed requests), with which rule each matched and why. For a regulated team, this answers "what did the agent do on the network, and when?" Re-run the command later to see new entries.

In real use When you ran the curls in 3.2, fifa.com showed up in the Blocked requests section of the log with the reason "No matching allow rule (default deny)". It isn't on any of the default allow groups, but say we want the agent to do some research on the 2026 World Cup later in the workshop. fifa.com is exactly the kind of source we'd want it to reach. That's the pattern you'll hit constantly in real use: you decide your agent needs a domain that isn't on the default allow list (or sometimes the agent binary itself needs it, like OpenCode reaching for opencode.ai on first launch). The audit log tells you what was blocked. A targeted allow rule opens just that one thing:
host shell
sbx policy allow network fifa.com
Re-run the fifa.com curl from 3.2 and you'll see it succeed; the audit log now lists fifa.com under Allowed requests, while its earlier Blocked entry stays in place as history. Reddit and Wikipedia remain in Blocked requests. That's the point: the rule was scoped narrow enough to unblock the one thing without opening the door wider than you intended. Leave this rule in place. Step 5's research demo will be happy to have it if the agent decides to look at the tournament.

3.4 Add a custom deny rule

Block a domain that IS in the default allow list. We'll use pypi.org.

Exit the agent in Terminal A: press Ctrl+C twice.

host shell
sbx policy deny network pypi.org
host shell
sbx policy ls
Expected. Last line of sbx policy ls
local:9701dc6e-...   network   deny   pypi.org
Your UUID will be different. Note it for cleanup.

3.5 Restart the sandbox if needed

New policy rules apply immediately on most setups. If you find the rule isn't taking effect (for example a sandbox in the middle of a long-lived connection), restart it:

host shell
sbx stop sandbox-alpha
host shell
sbx run --name sandbox-alpha

3.6 Verify the deny rule is enforced

Prompt to agent
Run curl -s https://pypi.org -o /dev/null -w "pypi.org: %{http_code} (%{size_download} bytes)\n", then run pip install requests 2>&1 | tail -3. Tell me what you see for each.
Should look like
pypi.org: 403 (0 bytes)

ERROR: Could not find a version that satisfies the requirement requests
ERROR: No matching distribution found for requests

The agent may try multiple workarounds. All fail because every approach hits the same blocked TCP connection at the proxy.

3.7 Remove a rule

Add commands create rules. sbx policy rm network takes them away. The cleanest form removes a rule by the resource (domain) it applies to. Let's practice on a throwaway rule so we don't disturb the ones we'll need going into Step 5. First, add a sample deny rule for a domain we won't actually use:

host shell
sbx policy deny network example.com

Confirm it's there:

host shell
sbx policy ls

Then remove it:

host shell
sbx policy rm network --resource example.com

--resource tells sbx which rule to drop by the domain it applies to. If you prefer to remove by rule ID, swap --resource example.com for --id <UUID>; find the UUID in the first column of the policy list. Either form removes the rule.

Run the list one more time to confirm the row is gone:

host shell
sbx policy ls

The example.com row is gone. That same mechanic works on any rule, including the fifa.com allow and the pypi.org deny we added earlier. When you're done with this workshop, you can clean up everything you added with sbx policy rm network --resource <domain> per rule. For now, leave fifa.com allowed and pypi.org denied. Step 4 (MCP) won't touch policy; Step 5's prep step removes the pypi.org deny so pip installs work in the research demo.

A persistent or smarter agent cannot bypass a network deny rule. The policy blocks at the proxy regardless of how the agent dresses up the request.
Go deeper: allow an IP address or CIDR range

Rules aren't limited to hostnames. If you have an internal service at a specific IP without a public DNS entry (a staging API behind a bastion, a metrics endpoint on a private subnet), you can allow it by IP. The shape mirrors the domain rules: pass an IP or a CIDR range where you'd normally pass a hostname.

For a quick test with a public IP, try Cloudflare's DNS endpoint at 1.1.1.1. It's not on any default allow list, so by default the sandbox can't reach it. Add the allow:

host shell
sbx policy allow network 1.1.1.1

Then have the agent verify the connection goes through:

Prompt to agent
Run curl -s https://1.1.1.1 -o /dev/null -w "1.1.1.1: %{http_code} (%{size_download} bytes)\n" and tell me what you see.

You should see something like 1.1.1.1: 301 (or another non-403 status). The exact status depends on what's listening at the IP. 1.1.1.1 returns Cloudflare's branded redirect to one.one.one.one, which is fine for this demo: the key signal is that you got something other than 403, meaning the proxy let the connection through.

Check the audit log on Terminal B:

host shell (Terminal B)
sbx policy log sandbox-alpha

You'll see 1.1.1.1 listed under Allowed requests, alongside its earlier Blocked entry from before the rule.

For a range of addresses instead of a single IP, use CIDR notation: sbx policy allow network 10.0.0.0/24 opens an entire /24 CIDR range (e.g., to give the sandbox access to a whole staging environment).

If the allow network <ip> form errors on your sbx version, check the exact syntax your build accepts:

host shell
sbx policy allow network --help

Clean up when you're done:

host shell
sbx policy rm network --resource 1.1.1.1
Go deeper: filter the policy log to find specific patterns

The full policy log is useful, but in a regulated environment you usually want a focused slice: only blocked entries, only attempts to a specific domain, or only the default-deny entries versus the explicit-rule denies. The log is plain text and the standard Unix tools work fine on it. Try:

sbx policy log sandbox-alpha | grep -E 'Blocked|Denied'
sbx policy log sandbox-alpha | Select-String -Pattern 'Blocked|Denied'
sbx policy log sandbox-alpha | grep github.com
sbx policy log sandbox-alpha | Select-String -SimpleMatch 'github.com'
sbx policy log sandbox-alpha | grep '(default deny)'
sbx policy log sandbox-alpha | Select-String -SimpleMatch '(default deny)'

The first filter catches every entry the policy stopped, whether the REASON column says "Blocked" (e.g., host-header mismatch, UDP not allowed) or "Denied" (an explicit rule matched). The second narrows to any line involving a specific domain. Useful for debugging one site's behavior in isolation. The third surfaces only the default-deny entries, separating them from explicit-rule denies. Helpful for spotting domains the agent reaches for that aren't on any of your rules yet, which is where the next allow rule probably belongs.

Key learnings

Five default allow groups cover the most common dev destinations (AI providers, package managers, code and container registries, cloud infrastructure, OS package repos), and everything outside those groups is blocked by default. You can override any of them with a custom deny rule for a specific domain. The policy log prints the audit trail of every outbound connection on demand with sbx policy log <sandbox>.

The boundary is at the proxy, not in the agent. Allow and deny lists are independent of what the model decides to do.

Before Step 4: Get Docker Desktop running

Before Step 4: Confirm Docker Desktop is still running

You installed Docker Desktop in Getting Started's Install Docker Desktop substep. Confirm the daemon is still up before we register MCP servers:

host shell
docker info

If it prints engine details, you're set. If not, launch Docker Desktop from your Applications folder / Start menu and give it a few seconds to start. If Docker Desktop isn't installed at all, jump back to Install Docker Desktop, then come back here.

Heads up Until now you've worked entirely through sbx, which doesn't require Docker on the host. Step 4 introduces a new wrinkle. The MCP server we're about to register runs as a Docker container (sbx will literally spawn docker run mcp/wikipedia-mcp when an agent attaches to it), so to launch that container the Docker daemon needs to be up and the docker CLI needs to be on PATH. Step 5 will also need Docker on the host for building images and running Docker Scout. Docker Desktop bundles the docker CLI, the Docker engine, and Docker Scout in one install, so it's the fastest way to get everything Steps 4 and 5 need running together.

brew install --cask docker
winget install -h Docker.DockerDesktop

Then start Docker Desktop. You can launch it from your Applications folder or Start menu, or from the command line:

open -a Docker
Start-Process "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe"

Wait for it to finish starting. On Mac the whale icon in the menu bar stops animating; on Windows the system tray icon turns solid. Then confirm the daemon is up:

host shell
docker info

If it prints engine details (server version, OS, kernel), you're good. If it prints Cannot connect to the Docker daemon, Docker Desktop hasn't finished starting yet. Wait a few seconds and try again. If you just installed Docker for the first time, open a fresh terminal so your shell picks up the updated PATH.

Now sign in to Docker Hub. You need to be signed in to pull the MCP image we'll use in a moment (and Docker Scout in Step 5).

On a fresh install, Docker Desktop often pops a sign-in dialog the first time you launch it. If you see it, sign in there with your Docker Hub account (same one from Quick Setup step 1) and you're done. If you don't see the dialog, or you dismissed it, sign in from the terminal instead:

host shell
docker login

This either prompts for your Docker Hub username and access token, or opens a browser with a device-flow code to enter (similar to how sbx login worked at the start of the workshop).

Pre-pull the workshop's images

Steps 4, 5, and 7 all pull Docker images or models mid-flow. Pulling them now, while the workshop is still in setup, keeps the live steps snappy and works around fussy conference Wi-Fi:

host shell
docker pull mcp/wikipedia-mcp
host shell
docker pull mcp/duckduckgo
host shell
docker pull mcp/paper-search
host shell
docker model pull ai/smollm2

mcp/wikipedia-mcp and mcp/duckduckgo are the two MCP servers we'll register in Step 4. mcp/paper-search is a third MCP server that Step 7's Docker Agent team uses for academic literature. ai/smollm2 is a small local language model that a role in Step 7 will use through Docker Model Runner. These pulls can run in parallel; if any one fails, re-run just that command. Once all four complete, you're ready for Step 4.

Step 4

Connect tools via SBX MCP

Tool governance via MCP and the Gateway

Give the sandboxed agent access to tools beyond reading and writing files by registering an MCP server and attaching it to sandbox-alpha.

The primitive that answers "what can the agent do?": every tool declared, every invocation observable.

In Step 3 you saw what happens when the agent reaches for something outside the default allow list: the proxy blocks it, the audit log records it, and you decide whether to open a targeted hole. That's a network-level lever. MCP gives you a different one: instead of unblocking a domain, register a specific tool (web search, GitHub access, file operations, an internal API) and let the agent invoke that tool through a controlled channel. The agent can do what the tool exposes and nothing more.

Governance framing: tool access, not network access, is the granularity your compliance team wants. "The agent can reach Wikipedia" is coarse; "the agent has the get_summary and search_wikipedia tools" is precise. Each MCP registration is a documented capability grant. The Docker MCP Gateway (introduced in 4.2.1 below) is where team-scale tool governance lives: profiles, catalogs, and per-client scoping so different teams see different tool subsets from the same shared infrastructure.

MCP (Model Context Protocol) is an open standard for how agents call tools. An MCP server exposes one or more tools, and the agent invokes them through the protocol. sbx mcp is how we register servers so that agents running inside a sandbox can use them. For this step we'll register two MCP servers (Wikipedia and DuckDuckGo web search), attach both to sandbox-alpha, and have the agent retrieve information through them.

4.1 Enable the sbx mcp subtree

The sbx mcp subtree is hidden by default. It only appears when the SBX_MCP_URL environment variable is set to an absolute http/https URL. The simplest path is to point it at the public MCP community registry, which unlocks the subcommands without requiring any additional infrastructure:

export SBX_MCP_URL=https://registry.modelcontextprotocol.io
$env:SBX_MCP_URL = "https://registry.modelcontextprotocol.io"

Restart the sbx daemon so it picks up the new SBX_MCP_URL:

host shell
sbx daemon stop
host shell
sbx daemon start -d

Confirm the subtree is now available:

host shell
sbx mcp --help

You'll see subcommands like add, ls, inspect, rm, and bundle. SBX_MCP_URL is a per-shell variable, so if you open a new terminal you'll need to set it again or persist it in your shell's rc file (~/.zshrc, ~/.bashrc, or ~/.config/fish/config.fish).

If MCP commands later fail with a gateway error Two common causes: (1) the sbx daemon doesn't know about SBX_MCP_URL because it was started before you exported the env var; the daemon restart we did in this step is what fixes that. (2) Docker Desktop isn't signed in to Docker Hub, so it can't pull the MCP image; open Docker Desktop and sign in. Keep these two causes in mind if you hit an MCP error later on.

4.2 Register a local MCP server

sbx mcp add supports four registration modes: remote OAuth, community registry (managed), community registry (local), and local stdio. We'll use the fourth: a local stdio MCP server that runs as a Docker container on the host. It's the most reliable path and needs nothing beyond your machine.

Register the Wikipedia MCP server:

host shell
sbx mcp add wiki-mcp --command docker --args "run,-i,--rm,mcp/wikipedia-mcp"

Walking through that command piece by piece:

sbx mcp add registers a new MCP server so sbx-managed sandboxes can attach to it later. wiki-mcp is just a name we picked for this registration; you can call it whatever you want. It's the handle you'll reference in later commands like sbx mcp inspect wiki-mcp or as the value of --static-mcp wiki-mcp when creating a sandbox.

--command docker tells sbx what executable to invoke when it starts the MCP server, so the server runs in a container rather than on the bare host. --args is comma-separated (not space-separated, a common gotcha), and these args translate to docker run -i --rm mcp/wikipedia-mcp. Of those Docker flags: -i keeps stdin open, which is what makes stdio transport work in the first place (without it sbx can't pipe JSON-RPC into the container); --rm auto-removes the container on exit so you don't accumulate stopped containers every MCP session.

mcp/wikipedia-mcp is the OCI image, published in Docker's MCP catalog as a Verified Publisher image. It wraps Wikipedia's API in the MCP protocol and exposes 22 read-only tools that the agent can call once the server is attached to a sandbox: search_wikipedia, get_article, get_summary, extract_key_facts, get_sections, get_links, and friends. We're using Wikipedia here because every tool the server exposes is scoped to Wikipedia (no general web access), it's free and requires no API key, and it gives the agent a capability that's clearly distinct from anything in its default toolkit. Any MCP server published to the catalog could take its place: a GitHub MCP, a Notion MCP, a Slack MCP, a custom one you built.

Now inspect the server you just registered to see what it actually exposes. This pulls the server's tool list, descriptions, and metadata back from the underlying MCP server, so you can confirm both that the registration worked and what the agent will be able to call:

host shell
sbx mcp inspect wiki-mcp

You'll see the server's name, transport mode (stdio), the underlying command sbx will launch when an agent attaches to it, and the tools it advertises (the 22 Wikipedia-scoped tools listed above). If anything looks wrong here, sbx never got to a clean working state with that registration; fix it before moving on.

If sbx mcp add reported any error Even when add fails, sbx sometimes caches a broken registration that will block subsequent attach attempts on this name. If sbx mcp inspect wiki-mcp above showed anything unexpected, remove the bad entry and re-add it: sbx mcp rm wiki-mcp, then re-run the sbx mcp add command from the step above.

If you want to see every MCP server registered so far in one view, list them:

host shell
sbx mcp ls

wiki-mcp should appear in the output.

Heads up Local stdio MCP servers run on the HOST, not inside the sandbox. They execute with your full user permissions: filesystem, network, secrets, everything. The sandbox boundary you built in Steps 1-3 still holds for the agent's own actions; the MCP server is just a tool the agent can call through that boundary. Use local stdio MCP servers for development and code you trust, not for arbitrary third-party code.

4.2.1 Zoom out: the Docker MCP Gateway

Everything you just did with sbx mcp add registered a server with sbx's own MCP layer, scoped to sandboxes managed by this daemon. That's the right shape for a single developer running one or two sandboxes at a time. As soon as you scale past that (a team using the same MCP servers, or one client wanting to see a curated subset of tools), you want a broader primitive. Docker ships one: the Docker MCP Gateway.

The Gateway is a local proxy that sits between MCP clients (agents, IDEs, Docker Agent, LM Studio, Cursor) and MCP servers. You register servers against the Gateway, group them into profiles, and connect each client to the profile that fits its role. Bundled with Docker Desktop 4.62+, also installable as a CLI plugin. Explore the command surface:

host shell
docker mcp --help

Conceptual only in this workshop The rest of this subsection describes the Gateway workflow at a high level. The commands are illustrative, not for copy-paste in Step 4 (they don't cleanly fit the sandbox-first shape of this step). If you want to actually stand a Gateway up on your machine, follow the walkthrough in the LP2 Go Deeper section (MCP Gateway at scale) or the official docs at docs.docker.com/ai/mcp-gateway/.

Conceptually, a minimal profile-based workflow looks like this: you create a profile with a name (something like workshop), add MCP servers to that profile (from Docker's MCP catalog or from OCI images you host), then run the Gateway pinned to that profile. Once it's running, an MCP client (an IDE, a Docker Agent yaml, or any custom client) binds to the profile, and from then on that client sees exactly the tools the profile grants, no more, no less. Different clients bind different profiles; that's how a team gets per-client tool scoping without duplicating registrations.

We're keeping the rest of the workshop on the per-sandbox sbx mcp path because it makes the sandbox-as-trust-boundary story cleaner. But if you're planning an MCP setup that a team is going to share, the Gateway is the primitive to know about.

4.2.2 Register a second MCP

One MCP gets the agent Wikipedia access; a second MCP with a different tool surface makes the "the agent picks the right tool for the sub-question" story real. Register the DuckDuckGo web-search server:

host shell
sbx mcp add duck-mcp --command docker --args "run,-i,--rm,mcp/duckduckgo"

Same shape as the wiki registration, different image. mcp/duckduckgo wraps DuckDuckGo's web-search API in the MCP protocol and exposes web search, image search, news search, and content fetching. Different tool surface from Wikipedia, which is the point. The agent will pick each tool based on the sub-question.

Confirm it registered and inspect what it exposes:

host shell
sbx mcp inspect duck-mcp

List all your registrations to confirm both are there:

host shell
sbx mcp ls

wiki-mcp and duck-mcp should both appear.

Other registration modes sbx mcp add also accepts remote MCP endpoints (--url https://mcp.notion.com/mcp, OAuth handled automatically) and, in a preview state as of this workshop, a direct OCI image reference mode (--url docker.io/mcp/<image>:<tag>) where sbx auto-detects the transport from the image's labels. OCI-native mode registers today but live-attach isn't fully wired for local sandboxes yet, which is why this workshop stays on the stdio-with-docker-command path for both servers. To see the full surface:

host shell
sbx mcp add --help

4.3 Try without MCPs, then with both attached

Two MCPs are registered but neither is attached to a sandbox yet. Before we attach them, let's prove the agent really can't reach Wikipedia or web search through normal channels. The contrast is the whole point of this step.

Part 1: confirm both destinations are blocked. Attach to the existing sandbox-alpha (still running from Step 3):

host shell (Terminal A)
sbx run --name sandbox-alpha

Ask the agent to fetch a specific Wikipedia URL. Don't tell it how. Whichever tool it picks (built-in web fetch, curl, anything else) the request still has to leave the sandbox, and the sandbox's network policy doesn't allow Wikipedia:

Prompt to agent
Fetch https://en.wikipedia.org/api/rest_v1/page/summary/2026_FIFA_World_Cup and tell me the HTTP status and any body you got back.

The agent picks a tool, makes the request, and the proxy refuses it with a 403 (or the connection fails outright) because wikipedia.org isn't on any default allow list. The agent reports the failure. Same story for a general web search: duckduckgo.com isn't on any allow list either. That confirms the baseline: with no MCP attached, the sandbox can't reach either source, regardless of which tool the agent reaches for.

Switch to Terminal B (your host shell) and confirm the block in the audit log:

host shell (Terminal B)
sbx policy log sandbox-alpha

You'll see a fresh Blocked requests entry for en.wikipedia.org with the reason "No matching allow rule (default deny)." That's the network-policy-only baseline.

Part 2: attach both MCPs at sandbox creation time. The agent in sandbox-alpha has no Wikipedia or web-search tool right now. We're going to give it both by recreating the sandbox with the MCPs declared up front via --static-mcp. This is the reliable path in current sbx builds; the runtime-attach flow (sbx mcp load) has known constraints, and the create-time flow works cleanly.

Exit the agent (Terminal A) with Ctrl+C twice to free the sandbox. Then switch to Terminal B and remove sandbox-alpha (it currently has no MCPs, so we're going to swap it for one that does):

host shell (Terminal B)
sbx rm sandbox-alpha

Confirm the removal at the y/N prompt. Now recreate sandbox-alpha with both MCPs declared at create time:

sbx create --name sandbox-alpha --static-mcp wiki-mcp --static-mcp duck-mcp claude ~/workshop/sbx
sbx create --name sandbox-alpha --static-mcp wiki-mcp --static-mcp duck-mcp claude $HOME/workshop/sbx
sbx create --name sandbox-alpha --static-mcp wiki-mcp --static-mcp duck-mcp codex ~/workshop/sbx
sbx create --name sandbox-alpha --static-mcp wiki-mcp --static-mcp duck-mcp codex $HOME/workshop/sbx
sbx create --name sandbox-alpha --static-mcp wiki-mcp --static-mcp duck-mcp opencode ~/workshop/sbx
sbx create --name sandbox-alpha --static-mcp wiki-mcp --static-mcp duck-mcp opencode $HOME/workshop/sbx

The --static-mcp flag tells sbx "spin up the sandbox with the per-sandbox MCP gateway pre-wired to these registrations." The create command doesn't print anything MCP-specific when it succeeds; the gateway wiring happens in the background.

Reconnect to the sandbox (Terminal A):

host shell (Terminal A)
sbx run --name sandbox-alpha

The agent inside this fresh sandbox has both Wikipedia and DuckDuckGo tools available from the start. Ask an open question that naturally needs both background and current information. Don't tell the agent which tools to use; let it decide:

Prompt to agent
Tell me what's happening with the 2026 FIFA World Cup. Give me a short background on the tournament (host countries, format, what makes this edition notable), and then any recent news you can find about it. At the end, tell me which tools you used to get the information, and if you tried anything that didn't work, explain what happened.

Watch how the agent responds. With both MCPs attached, it should reach for a Wikipedia-shaped tool for the background and a web-search-shaped tool for the recent news, picking each based on the sub-question. The final summary is the interesting part: the agent tells you which tool calls it made, and if it tried anything direct that got blocked, it should say so. Same sandbox, same agent session as Part 1; what changed is the set of capabilities the agent has to work with.

Part 3: confirm what actually happened at the network layer. Whatever the agent said it did, the audit trail is the source of truth. Switch to Terminal B and check the policy log:

host shell (Terminal B)
sbx policy log sandbox-alpha

Two things are worth looking for in the log. First, if the agent tried to reach any domain directly before falling back to the MCP tools, you'll see that as a Blocked entry with a "no matching allow rule" reason. Some models do try the direct network path first; that attempt hits the default-deny policy and shows up in the log. Second, and this is the important half: there should be no successful Allowed entries for the information sources themselves (like wikipedia.org or duckduckgo.com). The agent got its answer through the MCP tools, which means every tool call traveled from the agent, through the MCP protocol, to the host-side MCP server, out to the underlying API on the host's network, and back. The sandbox boundary stayed in place. MCP added a new lane that runs alongside the network policy, not through it.

Why not sbx mcp load? sbx also has a runtime-attach command, sbx mcp load <name> --sandbox <sandbox>, that in theory would let you add an MCP to a running sandbox without recreating it. In current builds it hits either a 501 "MCP gateway is not enabled" or an "unknown flag: --sandbox" depending on your version, so this workshop uses the create-time --static-mcp path exclusively. When the runtime-attach flow stabilizes upstream, you'll be able to swap MCPs on a live sandbox without the rm/recreate dance.

4.4 Wrap up

Exit the agent (Ctrl+C twice) to free your terminal for Step 5's host commands. Don't tear down sandbox-alpha or either MCP registration. Step 5 will keep using sandbox-alpha for its research demo, and the agent will have both Wikipedia and DuckDuckGo available there if it decides to use them. We'll clean up the MCP registrations in the Cleanup section at the end of the workshop.

Key learnings

Network policy (Step 3) and tool access via MCP (Step 4) solve different problems. Network policy controls which hostnames the sandbox can connect to directly. MCP controls which specific tools the agent can invoke. They compose: a production-shape agent typically has restrictive network policy AND a curated set of MCP tools, so the agent reaches the outside world only through channels you've explicitly approved.

Both registrations used the same stdio-with-docker-command mode (--command docker --args "run,-i,--rm,<image>"). sbx also accepts remote MCP endpoints (--url https://mcp.<service>.com/mcp, OAuth handled automatically) for hosted services, and has a preview OCI-image reference mode (--url docker.io/mcp/<image>:<tag>) that auto-detects the transport from image labels; that OCI mode isn't fully wired for local sandboxes yet, so this workshop uses the stdio path. Beyond the per-sandbox sbx mcp path, the Docker MCP Gateway (4.2.1) is the team-scale primitive: profiles, catalogs, per-client scoping.

Governance pillar

Docker Model Runner

Where the model runs matters, not just what tools it can invoke. Local inference is a governance option, one that matters for regulated data, air-gapped environments, and cost control.

What it is. Docker Model Runner (DMR) is Docker Desktop's local LLM inference feature. You pull a model with docker model pull ai/smollm2, and it runs on your machine, exposing an OpenAI-compatible endpoint at http://model-runner.docker.internal (from containers) or http://localhost:12434 (from the host, when TCP is enabled). Any client that speaks the OpenAI API (Docker Agent, IDEs, custom code) can point at that endpoint and get inference without a cloud call.

Why it matters for AI governance. The model layer is where your data goes when the agent thinks. For regulated industries (healthcare, financial services, government) and any organization with data-sovereignty policies, "the model can't see this data" is a hard constraint. Model Runner lets you satisfy that constraint at the runtime layer, not by trusting a cloud provider's data-handling policy. Same shape as the other pillars in this workshop: the capability boundary is enforced by the runtime, not by the prompt or the model.

How it composes with the other pillars. A fully-governed AI-agent setup pairs a hardened base image (DHI), a bounded environment (sbx), a curated tool surface (MCP + Gateway), and, where policy demands, a local model (DMR). You'll see a preview of this in Step 7's Core team.yaml, where the writer role runs on ai/smollm2 via provider: dmr. The Go Deeper section at the end of LP2 has a fully-local Docker Agent team where every role runs on DMR, the shape you'd start from for a regulated deploy where no cloud call is acceptable.

Confirm DMR is enabled and available on your Docker Desktop:

host shell
docker model status --json

Running status confirms DMR is up. List the models pulled locally:

host shell
docker model list

If ai/smollm2 shows up (from the Quick Setup pre-pull), you're already set. If it doesn't, pull it now:

host shell
docker model pull ai/smollm2

Docker's model catalog lives at hub.docker.com/u/ai: verified publisher, signed images, small-to-medium models suited for local inference.

The governance takeaway. Model choice is a per-role architectural knob AND a per-workload compliance constraint. Local for the roles that touch regulated data, cloud for the roles that need frontier synthesis, hybrid teams where the yaml documents the split. The Docker Agent yaml is the compliance artifact; DMR is what makes "local" a real option.

Before Step 5: Prep your environment

Two quick setup tasks before Step 5 begins. First, restore the sandbox to default network behavior. Then, sign in to Docker Hub so Docker Scout (the vulnerability scanner we'll use) can pull policy data.

Restore default sandbox network access

In Step 3 you added a custom deny rule for pypi.org to prove the policy enforced. Step 5 will have the agent try to install packages with pip during its research, and the deny rule would block those installs for reasons unrelated to the demo. Remove the rule now, using the same mechanic from 3.7:

host shell
sbx policy rm network --resource pypi.org

Confirm it's gone:

host shell
sbx policy ls

The sandbox is now back to default network behavior for pypi.org. The fifa.com allow from 3.3 stays in place since the agent may want it during Step 5's research.

You signed in to Docker Hub before Step 4, so Docker Scout (Step 5's vulnerability scanner) is already authorized to fetch policy data. Nothing more to do here. You're ready for Step 5.

Step 5

Hardened output via DHI: what the agent ships

DHI applied: the artifact the agent ships to production

Give the agent a creative open-ended task that'll bump into the sandbox boundary. Watch the policy log while it works. Build and run what it produced, scan it with Scout, swap the base for a DHI variant, rebuild, run, and rescan.

Foundational DHI, applied end-to-end: the agent writes code, you build the container, Docker Scout scans, then you swap in the hardened base and rescan.

Governance framing: the Foundational module at the top of this workshop showed the DHI delta at the base-image level. This step applies the same swap to a real artifact the agent produced: code inside a container. The comparison here is what your supply-chain reviewer actually wants to see: same code, same Dockerfile shape, hardened vs unhardened base, side-by-side CVE reports. The artifact from this step is the Scout scan output of the hardened build.

5.1 Set up the project and the Dockerfile

On your host, create a project directory inside the workspace and a starter Dockerfile that points at python:3.14-slim. The Dockerfile stays exactly the same across both builds. We only swap the base image later. The EXPOSE 8000 line declares the port the app will listen on.

mkdir -p ~/workshop/sbx/research-app
New-Item -ItemType Directory -Force -Path $HOME/workshop/sbx/research-app | Out-Null
cd ~/workshop/sbx/research-app
cd $HOME/workshop/sbx/research-app

Write the starter Dockerfile (the agent will fill in app.py later):

echo 'FROM python:3.14-slim
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]' > Dockerfile
@'
FROM python:3.14-slim
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
'@ | Set-Content Dockerfile

The folder is shared into the sandbox, so the agent will see the Dockerfile too. It just needs to fill in the missing app.py.

5.2 Ask the agent to build a research dashboard

Switch to the sandbox terminal. We're going to give the agent an open-ended task: build a small polished World Cup dashboard. The prompt doesn't tell the agent it's in a workshop or what we've blocked and allowed; it just describes the build. As the agent works, it'll naturally reach for sources outside the default allow list (Wikipedia, news outlets, random stats APIs), in addition to fifa.com which we unblocked in 3.3. That gives us a chance to watch the sandbox boundary you built in Step 3 enforce itself on a task you didn't anticipate. Because the prompt is open-ended, each attendee's dashboard will look a little different.

Prompt to agent
There's a research-app/ directory in the workspace with a Dockerfile already in it. Build me a small, polished dashboard that mixes two threads: data from GitHub and data from the current FIFA World Cup. Find something interesting on each side, then surface a combined view that connects them in an unexpected way. One page, light clean design, a few real stats from each side, gentle animations on load.

Some ways the two threads can mix (pick what feels most interesting, or invent your own):
- Open-source repos that work with sports or tournament data, alongside real World Cup stats they could be analyzing
- Python libraries for visualizing match data, paired with a sample chart that actually uses them
- Football-related repos sorted by stars or recent activity, alongside the tournament context they reference
- Any developer-shaped fact crossed with any tournament-shaped fact, in a combination that surprises you

This is also a chance to show off some real Python craft on the build side. Visualizations, animations, layout, copy. Lean in.

Research with real football sources. Try fifa.com for tournament info, sports outlets (ESPN, BBC Sport, theathletic.com, skysports.com), and transfermarkt.com or similar for player and match data. Mix in GitHub for repos and recent open-source activity. Some connections will work, others won't. That's expected, just move on to a different source rather than retrying the same one. Cap your total external requests at around 5 to 6 attempts. Stop researching after 1 to 2 minutes regardless of what you've gathered and start writing app.py with whatever you have.

Constraints:
- Only write app.py. Don't modify the Dockerfile or create any other files. Embed all research data inline as Python literals (dict, list, etc.) at the top of the file. Don't read from external JSON, CSV, or any data file at runtime. The container only ships app.py, so anything not in that file won't exist inside the running image.
- The final app.py must start an HTTP server on port 8000 listening on all interfaces (0.0.0.0).
- Use only the Python standard library in the final running code (no third-party imports in app.py itself). Frontend interactivity lives in the HTML, CSS, and JavaScript you serve.
- Light background, clean typography, no oversized hero text, no emoji.
- Include a small "Notes" or "Sources" section on the page noting where the data came from.

Before you tell me you're done, sanity-check your own work. Run python app.py, hit http://localhost:8000 with curl from a separate shell or by backgrounding the server, and confirm it serves the dashboard without errors. Also verify nothing in app.py reads from an external file at runtime (no open(...), Path(...).read_text(), or json.load(open(...)) against any file path). Fix anything broken before reporting back.

Show me what you came up with when you're done.

The agent will work on this for a minute or two. Network calls, attempted package installs, code generation: all of it happens inside the sandbox. While it works, leave the agent running and head back to a host terminal for the next substep.

5.3 Watch the sandbox boundary in action

On a host terminal, print the policy log for the sandbox:

host shell
sbx policy log sandbox-alpha

You'll see every network call the agent has made so far, split into two sections: Blocked requests and Allowed requests. Hostnames in the default-allow groups (AI providers, package managers like PyPI, code and container registries like GitHub, cloud APIs, OS package repos) appear under Allowed. Anything outside those groups (Wikipedia, news sites, weather APIs, whatever else the agent reaches for) lands under Blocked with the reason "No matching allow rule (default deny)". Re-run the command every minute or so to see new entries as the agent keeps trying things.

This is the proof Step 3 set up: the boundary holds on a task the agent invented for itself, with no extra work from you. The agent didn't agree to obey the policy. The policy holds whether the agent likes it or not.

5.4 Build and run what the agent produced

Once the agent says it's done, move into the project directory on the host:

cd ~/workshop/sbx/research-app
cd $HOME/workshop/sbx/research-app

Build the image:

host shell
docker build -t research-app:v1 .

Run the container in the background, mapping port 8000 from the container to your host:

host shell
docker run -d --rm -p 8000:8000 --name research-app research-app:v1

Open http://localhost:8000 in your browser. The dashboard the agent built will render with whatever topic it picked. Look for the "Notes" or "Constraints" section. That's where the agent documented what the sandbox stopped it from doing.

When you're done looking at it, stop the container:

host shell
docker stop research-app

5.5 Scan the image with Docker Scout

Scan the image we just built and ran to see the CVEs it inherits from python:3.14-slim:

host shell
docker scout quickview research-app:v1
Should look like (counts will vary)
  Target     │  research-app:v1
    digest   │  sha256:abc123...
  Base image │  python:3.14-slim

  CVES  │  C    H    M    L
  ──────┼─────────────────────
        │  1    2    3    25

Read off the critical / high / medium / low counts. Every CVE here comes from the base image, not from the dashboard code the agent just wrote.

To see the actual list of CVEs that quickview just counted, run:

host shell
docker scout cves research-app:v1

This prints every CVE Scout found, with severity, package, and a short description. Scroll through it. The dozens of items that quickview summarized are real, named, and attributable to specific packages in the base image.

5.6 Swap the base for the DHI variant, rebuild, run, and rescan

In production DHI ships each base image in two variants: a dev variant (shell, package manager, used in build stages) and a prod variant (minimal, no shell, used for the final runtime stage). Real production Dockerfiles use both in a multi-stage build; only the prod stage ships. We're keeping this workshop's Dockerfile single-stage and swapping only the FROM line so the CVE-count comparison stays clean. When you adopt this in your own Dockerfiles, check the dhi.io catalog for the exact dev and prod variant tags.

DHI images live under the dhi.io namespace, which requires a one-time login with your Docker Hub credentials before the pull works:

host shell
docker login dhi.io

Username is your Docker Hub username. Password is a Docker Hub access token (or your password if you don't use tokens).

Now change exactly one line of the Dockerfile to point at the DHI equivalent:

sed -i.bak 's|FROM python:3.14-slim|FROM dhi.io/python:3.14|' Dockerfile
(Get-Content Dockerfile) -replace 'FROM python:3.14-slim', 'FROM dhi.io/python:3.14' | Set-Content Dockerfile
Go deeper: let the agent do the DHI swap instead

The sed / Set-Content command above is the shortest path to a predictable one-line edit. But in real life, the person doing this hardening pass is often working through the agent, not opening a text editor. The workshop is about agent-driven workflows, so it's worth seeing what the agent-driven variant looks like.

If you skipped the manual edit above (or want to redo it through the agent), undo it first:

mv Dockerfile.bak Dockerfile 2>/dev/null || sed -i.bak 's|FROM dhi.io/python:3.14|FROM python:3.14-slim|' Dockerfile
(Get-Content Dockerfile) -replace 'FROM dhi.io/python:3.14', 'FROM python:3.14-slim' | Set-Content Dockerfile

Reconnect to sandbox-alpha if you exited the agent:

host shell (Terminal A)
sbx run --name sandbox-alpha

Ask the agent to swap the base image. Be specific about the target so the model doesn't freelance a different variant:

Prompt to agent
Edit the Dockerfile in the current directory. Change the FROM line from python:3.14-slim to dhi.io/python:3.14. Change only the FROM line; leave everything else exactly as it is. Show me the diff when you're done.

The agent reads the file, changes the one line, writes it back, and reports the diff. Confirm on the host that the edit is what you expected before proceeding:

grep -n '^FROM' Dockerfile
Select-String -Path Dockerfile -Pattern '^FROM'

You should see FROM dhi.io/python:3.14. If it says anything else (the agent picked -alpine, an older tag, or edited more than the FROM line), redo it with a more prescriptive prompt or fall back to the manual sed.

Governance takeaway. Two things are worth naming here. First, the agent CAN do the hardening pass; you don't have to open the editor yourself. Second, the agent's edits are still worth verifying by hand. A one-line prompt saved you three seconds, but the audit only takes another one. In a real governance workflow you'd land these edits as PRs that a human (and CI) review before merge.

From here, continue with the rebuild / run / scan steps in section 5.6 below. The end state is identical either way.

Rebuild the image with the same flags as before, this time tagged :v2:

host shell
docker build -t research-app:v2 .

Run it the same way as before to confirm it still serves the page:

host shell
docker run -d --rm -p 8000:8000 --name research-app research-app:v2

Hit http://localhost:8000 again. Same dashboard, same content, same behavior. Then stop the container:

host shell
docker stop research-app

And rescan with Scout:

host shell
docker scout quickview research-app:v2
Should look like
  Target     │  research-app:v2
    digest   │  sha256:def456...
  Base image │  dhi.io/python:3.14

  CVES  │  C    H    M    L
  ──────┼─────────────────────
        │  0    0    1    2

The app code stays the same, the Dockerfile structure stays the same, the runtime command stays the same, and the page in your browser looks identical. The only difference between the two scans is a single substitution: python:3.14-slim becomes dhi.io/python:3.14. The application code, the build steps, the runtime command, and the deployment pipeline all stay identical.

And as before, get the detail listing for the v2 image:

host shell
docker scout cves research-app:v2

Compare what you see here against the v1 image's list from 5.5. The v2 image's CVE list is dramatically shorter, often just a small handful of low-severity items, sometimes none at all.

5.7 Compare the two scans side by side

You now have both images locally. Docker Scout has a compare subcommand that prints a direct diff of the vulnerabilities between two images, which makes the v1-vs-v2 delta unmissable:

host shell
docker scout compare --to research-app:v1 research-app:v2

The output shows which CVEs are present in research-app:v1 but no longer in research-app:v2, which (if any) appeared in v2 that weren't in v1, and a summary count diff for critical / high / medium / low. The "removed" list is the security debt the hardened base eliminated without you changing a single line of application code.

Key learnings

A standard base image like python:3.14-slim inherits whatever security flaws the upstream Debian and Python packages happen to ship at that moment, typically dozens of CVEs. A Docker Hardened Image starts from a minimal, hardened build with near-zero CVEs and signed provenance. The Dockerfile structure and the application stay the same, but the security posture changes dramatically.

The exact numbers shift over time as upstream packages release patches and DHI's catalog updates. But the delta is consistent: in a typical run, most criticals and highs are gone after the swap, mediums drop substantially, and lows drop by an order of magnitude. From swapping a single line of the Dockerfile.

Steps 1 through 3 made the agent safe to run, Step 4 gave it controlled tool access through MCP, and Step 5 makes what the agent ships safe to deploy. Along the way, the policy log proved the sandbox boundary holds even on creative open-ended tasks the agent invents for itself, and even when MCP adds parallel capability lanes that route around the boundary entirely.

Step 6

Standardize via SBX Kits

Kits as portable governance policy

Package the sandbox setup you've been doing by hand into a declarative kit anyone on your team can pull and run with one command.

One directory. One commit. One artifact your compliance team can review as your team's governed environment.

In Steps 1 through 5 you set up a sandbox, proved its isolation, configured a network policy, added an MCP tool, and ran a hardened build. This is a useful pattern, but the setup was ad-hoc. You typed the commands by hand, edited the files manually, and attached the MCP one piece at a time. The next person on your team would have to repeat all of it from memory. An sbx Kit packages that setup into a declarative artifact: a spec.yaml plus an optional files/ tree. You commit it to git. Anyone on your team clones the kit, runs one sbx command, and gets the same configured sandbox you have.

Governance framing: a kit turns "our team's governance policy" from a wiki page (that people forget to check) into a runtime artifact (that the sandbox enforces on every launch). Network rules, allowed MCPs, permitted skills: bake them into the kit, commit the kit, and every future sandbox on your team gets the same policy. This is the artifact your compliance team can point at when they need to answer "how do we enforce our AI-usage policy across the org."

Kits come in two flavors. Mixin kits (kind: mixin) add capability to one of the built-in agents: skills, network rules, config files, credentials. You can stack several on the same sandbox. Agent kits (kind: agent) replace the agent definition itself: container image, entrypoint, base policy, the lot. The built-in claude agent you've been using this whole workshop is itself an agent kit, and you can fork it to change one thing (a different model, stricter permissions, a custom system prompt) without rebuilding the rest. For this step we'll build a mixin kit called docker-review. It ships a Claude Code skill with a five-point Dockerfile checklist: base image, layer order, image size, security, and reproducibility. Without the kit, asking the agent to "review my Dockerfile" gets you whatever the model decides to look at on a given day. With the kit installed, every reviewer on your team applies the same checklist consistently.

Heads up The rest of the workshop is optimized for Claude / Anthropic. From Step 6 forward, the workshop defaults to the Claude / Anthropic path. Other providers can still follow along, and we'll note where the nuances matter. We default to Claude because most attendees have an Anthropic key from earlier setup and the demos run cleanest end-to-end on that path.

For this step specifically, the kit drops a Claude Code skill at .claude/skills/<name>/SKILL.md. Claude Code auto-discovers it natively; in our testing OpenCode does too. Codex and Gemini may or may not honor that path; if your agent doesn't auto-load the skill, the destination under files/workspace/ is what to adjust to match your harness's convention. Step 7 goes further: Docker Agent talks to Anthropic's API directly, so non-Claude attendees will need an Anthropic key for that one step.

6.1 Create the kit directory structure

Kits live in their own directory, separate from the sandbox workspace. We'll put ours at ~/workshop/kits/docker-review. The structure is two pieces: a spec.yaml (the kit metadata) and a files/workspace/ tree that maps directly to the sandbox workspace. Anything you put under files/workspace/ gets injected into the workspace at sandbox creation.

Create the directory tree:

mkdir -p ~/workshop/kits/docker-review/files/workspace/.claude/skills/docker-review
New-Item -ItemType Directory -Force -Path $HOME/workshop/kits/docker-review/files/workspace/.claude/skills/docker-review | Out-Null

Claude Code automatically discovers skills from .claude/skills/ in the workspace. By dropping a SKILL.md there, the kit makes the skill available the moment the sandbox starts. There's no install command to run and no manual "load this skill" prompt to issue; the kit just drops files into the right place at create time.

Other providers The .claude/skills/ path originated as Claude Code's convention and is also honored by OpenCode. Codex uses its own skill-loading path; if your agent doesn't auto-discover the skill, change the path under files/workspace/ to match your harness. The kit machinery is identical either way.

6.2 Write the kit spec

Create the spec.yaml at ~/workshop/kits/docker-review/spec.yaml:

echo 'schemaVersion: "1"
kind: mixin
name: docker-review
displayName: Dockerfile review skill
description: Ships a Claude Code skill that reviews Dockerfiles for best practices' > ~/workshop/kits/docker-review/spec.yaml
@'
schemaVersion: "1"
kind: mixin
name: docker-review
displayName: Dockerfile review skill
description: Ships a Claude Code skill that reviews Dockerfiles for best practices
'@ | Set-Content $HOME/workshop/kits/docker-review/spec.yaml

The fields: schemaVersion is the spec format version. kind is either mixin (extends an existing agent) or agent (defines a new one from scratch). name is the kit identifier. displayName and description are human-readable. That's the whole spec for a file-only mixin kit. There are no network rules and no install commands, only the file injection handled by the files/ tree we'll populate next.

6.3 Write the skill

Create SKILL.md at ~/workshop/kits/docker-review/files/workspace/.claude/skills/docker-review/SKILL.md. The file has YAML frontmatter (the skill's name and when to use it) followed by the instructions Claude Code follows when the skill loads:

echo '---
name: docker-review
description: Review a Dockerfile for best practices. Use when the user asks to review, audit, or improve a Dockerfile.
---

When reviewing a Dockerfile, output ONLY a Markdown table with exactly the columns and exactly the five rows below, in this exact order. Do not write any prose before or after the table.

| # | Aspect | Status | Findings | Recommendation |
| --- | --- | --- | --- | --- |
| 1 | Base image | Pass / Warn / Fix | What the Dockerfile does (pinned tag or digest? minimal? appropriate for the workload?) | What to change, or None |
| 2 | Layer order | Pass / Warn / Fix | What the Dockerfile does (dependencies installed before application source? cache reuse?) | What to change, or None |
| 3 | Image size | Pass / Warn / Fix | What the Dockerfile does (multi-stage build? .dockerignore present? package-manager cache flags like --no-cache, --no-install-recommends?) | What to change, or None |
| 4 | Security | Pass / Warn / Fix | What the Dockerfile does (non-root USER? secrets kept out of ARG/ENV? no --privileged?) | What to change, or None |
| 5 | Reproducibility | Pass / Warn / Fix | What the Dockerfile does (pinned package versions? explicit COPY targets?) | What to change, or None |

Rules:
- Use exactly one of Pass, Warn, or Fix in the Status column. Pass = meets best practice. Warn = works but could be tightened. Fix = real problem.
- Always produce all five rows in the exact order shown. Never add a row, drop a row, or reorder.
- Fill Findings with what you actually see in the Dockerfile under that aspect, not generic advice.
- Fill Recommendation with the specific change to make. If Status is Pass, write the word None.
- No commentary outside the table.' > ~/workshop/kits/docker-review/files/workspace/.claude/skills/docker-review/SKILL.md
@'
---
name: docker-review
description: Review a Dockerfile for best practices. Use when the user asks to review, audit, or improve a Dockerfile.
---

When reviewing a Dockerfile, output ONLY a Markdown table with exactly the columns and exactly the five rows below, in this exact order. Do not write any prose before or after the table.

| # | Aspect | Status | Findings | Recommendation |
| --- | --- | --- | --- | --- |
| 1 | Base image | Pass / Warn / Fix | What the Dockerfile does (pinned tag or digest? minimal? appropriate for the workload?) | What to change, or None |
| 2 | Layer order | Pass / Warn / Fix | What the Dockerfile does (dependencies installed before application source? cache reuse?) | What to change, or None |
| 3 | Image size | Pass / Warn / Fix | What the Dockerfile does (multi-stage build? .dockerignore present? package-manager cache flags like --no-cache, --no-install-recommends?) | What to change, or None |
| 4 | Security | Pass / Warn / Fix | What the Dockerfile does (non-root USER? secrets kept out of ARG/ENV? no --privileged?) | What to change, or None |
| 5 | Reproducibility | Pass / Warn / Fix | What the Dockerfile does (pinned package versions? explicit COPY targets?) | What to change, or None |

Rules:
- Use exactly one of Pass, Warn, or Fix in the Status column. Pass = meets best practice. Warn = works but could be tightened. Fix = real problem.
- Always produce all five rows in the exact order shown. Never add a row, drop a row, or reorder.
- Fill Findings with what you actually see in the Dockerfile under that aspect, not generic advice.
- Fill Recommendation with the specific change to make. If Status is Pass, write the word None.
- No commentary outside the table.
'@ | Set-Content $HOME/workshop/kits/docker-review/files/workspace/.claude/skills/docker-review/SKILL.md

The frontmatter's description field is critical: Claude Code uses it to decide when to invoke the skill. When the user asks something matching the description ("review my Dockerfile", "audit this Docker setup"), Claude pulls the skill content into its context and follows the structured checklist. The user doesn't have to invoke the skill by name or load it manually.

6.4 Validate the kit

Before spinning up a sandbox with the kit attached, validate the spec to catch any errors early:

sbx kit validate ~/workshop/kits/docker-review/
sbx kit validate $HOME/workshop/kits/docker-review/

If anything's wrong (malformed spec, missing required fields, broken file references), validate surfaces it before sandbox creation. A clean validation prints "kit valid" or similar and exits 0.

6.5 Create a sandbox with the kit attached

Create a fresh sandbox with the kit attached at create time. We'll call it sandbox-kits so it sits alongside sandbox-alpha (which still has the MCP from Step 4) rather than disturbing it. Run the command for the provider you picked at the top:

sbx create --name sandbox-kits --kit ~/workshop/kits/docker-review/ claude ~/workshop/sbx
sbx create --name sandbox-kits --kit $HOME/workshop/kits/docker-review/ claude $HOME/workshop/sbx
sbx create --name sandbox-kits --kit ~/workshop/kits/docker-review/ codex ~/workshop/sbx
sbx create --name sandbox-kits --kit $HOME/workshop/kits/docker-review/ codex $HOME/workshop/sbx
sbx create --name sandbox-kits --kit ~/workshop/kits/docker-review/ gemini ~/workshop/sbx
sbx create --name sandbox-kits --kit $HOME/workshop/kits/docker-review/ gemini $HOME/workshop/sbx
sbx create --name sandbox-kits --kit ~/workshop/kits/docker-review/ opencode ~/workshop/sbx
sbx create --name sandbox-kits --kit $HOME/workshop/kits/docker-review/ opencode $HOME/workshop/sbx

Attach to the new sandbox:

host shell
sbx run --name sandbox-kits

Once the agent is up, ask it to review the Dockerfile from Step 5. The skill we just shipped tells it how:

Prompt to agent
Review the Dockerfile in research-app/ against best practices and give me a structured assessment.

The agent loads the docker-review skill and applies its five-row review table (base image, layer order, image size, security, reproducibility). You'll see a Markdown table come back with the same five rows in the same order, each with a Pass / Warn / Fix status, the specific finding, and a recommendation. The point: what you'd get without the kit is whatever the model decides to look at and however it decides to present it; with the kit, every reviewer on your team gets the same structured table back regardless of model or run.

6.6 Wrap up

What you just did is the team-scale story: the skill traveled into the sandbox automatically. The user didn't run an install command, didn't read shared documentation, and didn't have to remember to enable anything. The kit is one directory, git-committable, copy-paste-shareable. The next person on your team clones the kit, runs the same sbx command, and gets the same skill loaded the same way.

Exit the agent (Ctrl+C twice), then tear down the kit sandbox so we're back to just sandbox-alpha for the rest of the workshop:

host shell
sbx stop sandbox-kits
host shell
sbx rm sandbox-kits

Leave ~/workshop/kits/docker-review/ in place; it's the artifact you'd commit to your team's repo. The full cleanup of the kit directory is in the Cleanup section at the end of the workshop.

Key learnings

Kits make sandbox setup declarative. A spec.yaml plus a files/ tree describes what the sandbox should contain; sbx materializes it at create time. Mixin kits (kind: mixin) add capability to one of the built-in agents: skills, network rules, config files, credentials. Agent kits (kind: agent) replace the agent definition itself: image, entrypoint, base policy, the lot. The built-in claude agent you've been using is itself an agent kit you can fork. Either flavor is a single directory you can copy, paste, or commit to git.

The "ad-hoc setup loses scale" problem is what kits solve. You did Steps 1-5 by hand because it's a workshop and the journey is the lesson. In real use, the team that's going to run an agent shouldn't have to repeat that journey. The kit is what you commit so they don't have to.

Step 7

Multi-agent orchestration with Docker Agent

Docker Agent yaml as an auditable compliance artifact

Step out of the single-agent model. Spin up a small team where each role has its own tools, its own instruction, and they hand off work to each other.

The yaml declares what each role can do. The runtime enforces it. Your security team can read the yaml and know exactly what capabilities you granted where.

A single agent is fine when the task is bounded. The pattern starts to fray when the work spans roles that each want different tools: a researcher who needs to read sources, a writer who needs to edit files, a reviewer who shouldn't have either. The choice becomes give one agent everything (which violates the least-capability instinct we've been building all workshop) or split the work across specialized agents.

Governance framing: a multi-agent team is the point where per-role scoping becomes essential, and where a compliance-ready artifact becomes possible. The yaml you're about to write names each role, lists its tools, and declares its instruction. The runtime enforces the toolset boundary (Step 8 tests this). That yaml is legible to a non-engineer security reviewer: "the wiki-researcher has these three tools, the writer has these two, the analyst has none other than think." No hidden capability, no prompt-injection route to a tool the role wasn't granted. This is what your compliance team wants to see.

Docker Agent is a different piece of Docker's agent platform from sbx. sbx is the foundational sandbox runtime; Docker Agent is the multi-agent orchestrator. Instead of one agent with one set of tools, Docker Agent runs a small declared team. You write a yaml that names the roles, what each one does, what tools each one gets, and how they hand off. Docker Agent runs the whole team in one process on your host.

Two ideas land in this step. First, a glimpse of the platform: Docker Agent sits at a different layer than sbx, complementary rather than stacked. Steps 7 and 8 step outside the sandbox you've been working with to show what multi-agent orchestration looks like today. Second, per-role scoping: the team inside Docker Agent is declarative, and each role's tools are scoped at definition time. The yaml is the contract.

A note on where Docker Agent fits Docker Agent and sbx are complementary layers of Docker's agent platform, not stacked. sbx scopes a single agent inside an isolated VM; Docker Agent runs on your host and composes multiple roles in one yaml. Steps 7 and 8 don't build on the sandbox you've been running, so think of them as a glimpse of multi-agent orchestration on its own track. The boundary in Step 8 comes from Docker Agent's per-role toolset assignments, not from sbx, but the underlying idea is the same: enforce capability scope at the runtime, never in the prompt.

7.1 Confirm Docker Agent is available, then set your API key

Docker Agent is a Docker CLI plugin, invoked as docker agent <command> (the same shape as docker scout earlier in the workshop). If you're on Docker Desktop 4.63 or newer (which you should be, since you installed it before Step 4), the docker-agent plugin is already bundled. Verify it:

host shell
docker agent version

If it prints a version line, you're done with install. Skip to the API key block below.

If docker agent isn't recognized Your Docker Desktop is older than 4.63, or the plugin wasn't installed for some reason. On Mac, install via Homebrew. On Windows, download the docker-agent-windows-*.exe binary from the GitHub releases page and drop it in %USERPROFILE%\.docker\cli-plugins\ (create that folder if it doesn't exist).

brew install docker-agent
# See the GitHub releases link above for the binary download.
# Or, if Docker Desktop has just updated, restart it and try docker agent version again.

Docker Agent reads its API key from ANTHROPIC_API_KEY in the shell environment. sbx stored your key in its own secret store for sandbox-internal use, but Docker Agent runs on the host, so the key needs to be exported as a shell env var here. Paste the Anthropic key you saved during prereq setup, or grab a fresh one from console.anthropic.com if you don't have it handy (the free credit for new accounts covers what this step uses).

export ANTHROPIC_API_KEY=sk-ant-...
$env:ANTHROPIC_API_KEY = "sk-ant-..."

This sets the variable for the current shell only. If you open a new terminal between here and 7.3 (where we'll run Docker Agent), re-run the export there too.

7.2 Write the team's spec

Docker Agent reads a yaml that declares the team. We'll write a six-role team that produces a research report end-to-end:

  • root (coordinator): delegates work, does no research, writing, or publishing itself.
  • wiki-researcher: gathers encyclopedic background through the Wikipedia MCP you already used in Step 4.
  • paper-researcher: searches peer-reviewed academic literature through the Paper Search MCP, which can query arXiv, PubMed, Semantic Scholar, OpenAlex, CrossRef, Google Scholar, and a dozen other scholarly databases (new for this step, pulls on first use).
  • analyst: synthesizes both researchers' findings into the important insights. This is the hardest task, so it runs on Claude Opus instead of Sonnet.
  • writer: produces the markdown report and saves it to ./report.md.
  • publisher: reads the markdown and renders it as a self-contained academic-paper-styled HTML page, saved to ./report.html. The publisher's job is presentation, not content.

Three notes on the model mix. Four of the six roles run on Claude Sonnet (fast, cheap, plenty smart for delegation, search, and HTML rendering). The analyst runs on Claude Opus because synthesis across two disparate research streams is where a heavier model earns its keep. The writer runs on a small local model via Docker Model Runner: prose assembly from an already-synthesized analysis is one of the lightest tasks in the team, and running it locally proves the point that model choice is a per-role architectural knob, not a per-team decision. Docker Agent lets you mix providers freely; if you'd rather run the writer on Sonnet too, change the writer's model: from local to fast before saving the yaml.

Before we wire it into the team, a quick word on what the Paper Search MCP actually is. mcp/paper-search is a Verified Publisher image in Docker's MCP catalog. It wraps roughly seventeen scholarly databases (arXiv, PubMed, bioRxiv, medRxiv, Semantic Scholar, OpenAlex, CrossRef, Google Scholar, Europe PMC, DOAJ, Zenodo, and others) behind the MCP protocol and exposes 57 tools: per-source search (search_arxiv, search_pubmed, search_semantic, etc.) plus a unified search_papers that hits multiple sources at once, plus download tools that fetch PDFs by ID and read tools that extract text from those PDFs. The paper-researcher role in this workshop primarily uses the search tools and returns structured lists of papers with titles, authors, DOIs, and abstracts. No API key is required; the image talks to public academic APIs directly.

Optionally, pre-pull the image now so the first delegation to the paper-researcher doesn't pause for a Docker pull:

host shell
docker pull mcp/paper-search

Create a folder and the yaml file:

mkdir -p ~/workshop/docker-agent
New-Item -ItemType Directory -Force -Path $HOME/workshop/docker-agent | Out-Null
cd ~/workshop/docker-agent
cd $HOME/workshop/docker-agent

Now write team.yaml with the six roles. Each role names its model, what it's for, and the toolsets it has access to:

cat > team.yaml <<'EOF'
version: "2"

models:
  fast:
    provider: anthropic
    model: claude-sonnet-4-6
    max_tokens: 8192

  deep:
    provider: anthropic
    model: claude-opus-4-6
    max_tokens: 8192

  local:  # small model running on host via Docker Model Runner; no cloud API call
    provider: dmr
    model: ai/smollm2
    max_tokens: 4096

agents:
  root:
    model: fast
    description: Coordinator of a research and report-writing team
    instruction: |
      You coordinate a research-and-report team. Your team:

      - wiki-researcher: gathers encyclopedic background from Wikipedia
      - paper-researcher: searches peer-reviewed academic literature via Paper Search
      - analyst: synthesizes findings from the researchers (running on Opus)
      - writer: produces the markdown report and saves it as ./report.md
      - publisher: reads the markdown and renders an academic-paper-styled ./report.html

      When the user gives you a topic, decide which team members to call and in what
      order based on the request. Before each delegation, narrate what you are about
      to do and why in one or two short sentences, so the user can see your reasoning.
      Briefly note what you got back after each delegation. Do not pad the narration.

      The deliverable is two files in the working directory: ./report.md and
      ./report.html. When both files are saved, return a one-line summary to the user.

      You do not research, analyze, write, or publish yourself. You only delegate,
      narrate, and report.
    sub_agents: [wiki-researcher, paper-researcher, analyst, writer, publisher]
    toolsets:
      - type: think  # internal scratchpad for delegation reasoning, no external side effects

  wiki-researcher:
    model: fast
    description: Wikipedia background researcher
    instruction: |
      You gather encyclopedic background from Wikipedia. Use the Wikipedia tools to search
      and retrieve relevant articles. Return a tight bulleted list of AT MOST 8 key facts,
      one short line each, with the source article title in parens. Be terse; the writer
      downstream has a compact ~800-word cap and you should not crowd them out. Do not write prose.
      Do not save files.
    toolsets:
      - type: mcp  # Wikipedia search and article retrieval (22 tools, no API key needed)
        command: docker
        args: ["run", "-i", "--rm", "mcp/wikipedia-mcp"]

  paper-researcher:
    model: fast
    description: Academic literature researcher
    instruction: |
      You search peer-reviewed academic literature. Use the Paper Search tools to query
      arXiv, PubMed, Semantic Scholar, OpenAlex, and similar sources. Return AT MOST 6
      papers, each on one line: title, first author, year, source, and a one-sentence
      finding. Do not download or read full PDFs. Be terse; the writer downstream has
      a compact ~800-word cap. Do not write prose. Do not save files.
    toolsets:
      - type: mcp  # academic literature search across arXiv, PubMed, Semantic Scholar, OpenAlex, and others (57 tools, no API key needed)
        command: docker
        args: ["run", "-i", "--rm", "mcp/paper-search"]

  analyst:
    model: deep
    description: Synthesis specialist
    instruction: |
      You receive findings from the wiki-researcher (encyclopedic background) and the
      paper-researcher (peer-reviewed literature). Distill them into AT MOST 5 key
      insights, one short paragraph each. Surface tensions or contradictions between
      the two streams when they exist. Be ruthlessly concise; the writer downstream has
      a compact ~800-word cap and your analysis must fit inside that envelope. Use the think
      tool to reason. Do not search anything yourself. Do not write the final report.
    toolsets:
      - type: think  # internal scratchpad for synthesis, no external side effects

  writer:
    model: local  # runs on host via Docker Model Runner; flip to 'fast' for higher-quality prose
    description: Briefing writer (running locally)
    instruction: |
      You receive an analysis and write a short briefing as ./report.md.
      Target 500 to 800 words. Structure it in three parts: a short Abstract
      (2 to 3 sentences), a Findings section (bulleted list of the analyst's
      key insights, one line each), and a Sources section (the papers the
      paper-researcher returned, one per line with title, authors, year, source).
      Keep sentences short and direct. Do not invent facts beyond what the analyst
      provided. Do not search anything.
    toolsets:
      - type: filesystem  # read and write files in the working directory (writes report.md)

  publisher:
    model: fast
    description: Academic-paper publisher
    instruction: |
      You read ./report.md and produce a self-contained HTML version that looks like
      a real academic paper. Save it as ./report.html.

      Embed CSS in a <style> tag. Style targets: serif body font (Crimson Text or
      Charter via Google Fonts, with a system serif fallback), max-width around 720px,
      centered page on white background. Centered title at the top. Italic abstract
      block with a small "Abstract" label. Section headings in small caps or bold
      serif at a modest size. Justified body text with reasonable line-height (1.5
      to 1.6). References at the bottom with hanging indent. No navigation, no
      footer, no Docker or workshop branding, no decorative emoji or icons. Just
      the paper.

      Transcribe the markdown structure into the HTML faithfully. Preserve every fact
      and citation. Do not invent content, do not summarize, do not editorialize. The
      publisher's only job is presentation, not authorship.
    toolsets:
      - type: filesystem  # read and write files in the working directory (reads report.md, writes report.html)
EOF
@'
version: "2"

models:
  fast:
    provider: anthropic
    model: claude-sonnet-4-6
    max_tokens: 8192

  deep:
    provider: anthropic
    model: claude-opus-4-6
    max_tokens: 8192

  local:  # small model running on host via Docker Model Runner; no cloud API call
    provider: dmr
    model: ai/smollm2
    max_tokens: 4096

agents:
  root:
    model: fast
    description: Coordinator of a research and report-writing team
    instruction: |
      You coordinate a research-and-report team. Your team:

      - wiki-researcher: gathers encyclopedic background from Wikipedia
      - paper-researcher: searches peer-reviewed academic literature via Paper Search
      - analyst: synthesizes findings from the researchers (running on Opus)
      - writer: produces the markdown report and saves it as ./report.md
      - publisher: reads the markdown and renders an academic-paper-styled ./report.html

      When the user gives you a topic, decide which team members to call and in what
      order based on the request. Before each delegation, narrate what you are about
      to do and why in one or two short sentences, so the user can see your reasoning.
      Briefly note what you got back after each delegation. Do not pad the narration.

      The deliverable is two files in the working directory: ./report.md and
      ./report.html. When both files are saved, return a one-line summary to the user.

      You do not research, analyze, write, or publish yourself. You only delegate,
      narrate, and report.
    sub_agents: [wiki-researcher, paper-researcher, analyst, writer, publisher]
    toolsets:
      - type: think  # internal scratchpad for delegation reasoning, no external side effects

  wiki-researcher:
    model: fast
    description: Wikipedia background researcher
    instruction: |
      You gather encyclopedic background from Wikipedia. Use the Wikipedia tools to search
      and retrieve relevant articles. Return a tight bulleted list of AT MOST 8 key facts,
      one short line each, with the source article title in parens. Be terse; the writer
      downstream has a compact ~800-word cap and you should not crowd them out. Do not write prose.
      Do not save files.
    toolsets:
      - type: mcp  # Wikipedia search and article retrieval (22 tools, no API key needed)
        command: docker
        args: ["run", "-i", "--rm", "mcp/wikipedia-mcp"]

  paper-researcher:
    model: fast
    description: Academic literature researcher
    instruction: |
      You search peer-reviewed academic literature. Use the Paper Search tools to query
      arXiv, PubMed, Semantic Scholar, OpenAlex, and similar sources. Return AT MOST 6
      papers, each on one line: title, first author, year, source, and a one-sentence
      finding. Do not download or read full PDFs. Be terse; the writer downstream has
      a compact ~800-word cap. Do not write prose. Do not save files.
    toolsets:
      - type: mcp  # academic literature search across arXiv, PubMed, Semantic Scholar, OpenAlex, and others (57 tools, no API key needed)
        command: docker
        args: ["run", "-i", "--rm", "mcp/paper-search"]

  analyst:
    model: deep
    description: Synthesis specialist
    instruction: |
      You receive findings from the wiki-researcher (encyclopedic background) and the
      paper-researcher (peer-reviewed literature). Distill them into AT MOST 5 key
      insights, one short paragraph each. Surface tensions or contradictions between
      the two streams when they exist. Be ruthlessly concise; the writer downstream has
      a compact ~800-word cap and your analysis must fit inside that envelope. Use the think
      tool to reason. Do not search anything yourself. Do not write the final report.
    toolsets:
      - type: think  # internal scratchpad for synthesis, no external side effects

  writer:
    model: local  # runs on host via Docker Model Runner; flip to 'fast' for higher-quality prose
    description: Briefing writer (running locally)
    instruction: |
      You receive an analysis and write a short briefing as ./report.md.
      Target 500 to 800 words. Structure it in three parts: a short Abstract
      (2 to 3 sentences), a Findings section (bulleted list of the analyst's
      key insights, one line each), and a Sources section (the papers the
      paper-researcher returned, one per line with title, authors, year, source).
      Keep sentences short and direct. Do not invent facts beyond what the analyst
      provided. Do not search anything.
    toolsets:
      - type: filesystem  # read and write files in the working directory (writes report.md)

  publisher:
    model: fast
    description: Academic-paper publisher
    instruction: |
      You read ./report.md and produce a self-contained HTML version that looks like
      a real academic paper. Save it as ./report.html.

      Embed CSS in a <style> tag. Style targets: serif body font (Crimson Text or
      Charter via Google Fonts, with a system serif fallback), max-width around 720px,
      centered page on white background. Centered title at the top. Italic abstract
      block with a small "Abstract" label. Section headings in small caps or bold
      serif at a modest size. Justified body text with reasonable line-height (1.5
      to 1.6). References at the bottom with hanging indent. No navigation, no
      footer, no Docker or workshop branding, no decorative emoji or icons. Just
      the paper.

      Transcribe the markdown structure into the HTML faithfully. Preserve every fact
      and citation. Do not invent content, do not summarize, do not editorialize. The
      publisher's only job is presentation, not authorship.
    toolsets:
      - type: filesystem  # read and write files in the working directory (reads report.md, writes report.html)
'@ | Set-Content team.yaml

Three things to notice in this yaml. The models block declares three model profiles: fast (Sonnet, cloud), deep (Opus, cloud), and local (smollm2 running on your host via Docker Model Runner). Each role picks the profile that fits. The agents block lists the team: root is the coordinator, and its sub_agents field names the five roles it can delegate to. The toolsets field on each role is the per-role capability scoping: wiki-researcher gets only the Wikipedia MCP, paper-researcher gets only the Paper Search MCP, analyst gets only think, writer and publisher each get only filesystem (writer to produce the markdown, publisher to read it and write the HTML), and the coordinator gets only think. That scoping is the boundary Step 8 will test.

7.3 Run the team

Make sure you are in ~/workshop/docker-agent and your ANTHROPIC_API_KEY is set in the current shell. Here's the prompt the team will work from:

Prompt to the team
Write me a short academic-style briefing on the 2026 FIFA World Cup. Use Wikipedia for background, and the academic literature for findings on World Cup economic impact, infrastructure, and host-country outcomes. Keep it tight. Save as report.md.

Two ways to run the team. Both produce the same output; pick whichever fits your style.

Option A: one-shot mode. The prompt is passed as the second argument and the team runs end-to-end without interactive chat. Docker Agent exits cleanly when the team finishes. Best for predictable runs and clean log capture.

host shell
docker agent run team.yaml "Write me a short academic-style briefing on the 2026 FIFA World Cup. Use Wikipedia for background, and the academic literature for findings on World Cup economic impact, infrastructure, and host-country outcomes. Keep it tight. Save as report.md."

Option B: interactive mode. Run without a prompt argument and Docker Agent opens a chat session. Paste the prompt above when prompted, watch the team work, and follow up with revisions or extensions if you want. Best for iterating on the team's behavior.

host shell
docker agent run team.yaml
Other ways to run: web interface and MCP server mode

Docker Agent has two further invocation modes beyond the CLI:

  • Web interface. docker agent web -d ./configs ./sessions.db starts a local web server that serves a browser UI for the agents in ./configs. Sessions persist to the SQLite file passed at the end, so closing the browser and reconnecting later picks up the same conversation. Closest thing Docker Agent has to a detach/attach pattern.
  • MCP server mode. docker agent mcp server --port 8080 --agents-dir ./configs exposes the entire team as an MCP server that other tools (including other agents) can connect to. The team becomes a tool itself.

Neither is required for the workshop. The CLI modes are enough for everything we'll do in Steps 7 and 8.

Watch the output. The coordinator narrates each step as it goes ("I'm about to delegate to wiki-researcher because..."), so its reasoning is visible in plain language. The coordinator picks who to call and in what order based on the request, so the exact sequence is its decision and may vary slightly between runs. A typical run looks like this: wiki-researcher invokes Wikipedia tools and returns encyclopedic background on the tournament, paper-researcher queries arXiv, PubMed, Semantic Scholar, and the other academic sources and returns a list of papers on World Cup economics and hosting impact (real titles, authors, DOIs), analyst (running on Opus) synthesizes the two streams into the most important insights, writer (running on the local ai/smollm2 model via Docker Model Runner) assembles a short briefing with Abstract, Findings, and Sources sections and saves report.md, and publisher reads the markdown and produces a self-contained report.html styled to look like a real academic paper. The coordinator's narration between each step shows you why it chose to call that role next. You'll notice the writer step feels a beat slower than the others (local inference is single-machine, no distant datacenter), which is the tell that this role is running on your laptop.

Verify both files landed. Read the markdown first:

cat ~/workshop/docker-agent/report.md
Get-Content $HOME/workshop/docker-agent/report.md

You should see a short briefing on the 2026 FIFA World Cup with Abstract, Findings, and Sources sections. Sources enumerates every paper the paper-researcher returned, with real titles, authors, year, and source. No hallucinated content beyond what the researchers gathered. If the writer's prose reads a little basic or the structure looks off, that's the local model showing its limits. Flip the writer's model: from local back to fast in the yaml and re-run to see the same team on Sonnet.

Now open the HTML version in your browser:

open ~/workshop/docker-agent/report.html
Start-Process $HOME/workshop/docker-agent/report.html

The HTML page should look like an academic paper: serif body font, centered title, italic abstract block, small-caps section headings, justified body text, and a properly-formatted References list with hanging indent. Same content as the markdown, just typeset.

The boundary held end to end: each researcher gathered through its own MCP and nowhere else; the analyst reasoned without searching; the writer wrote without searching; the publisher read and re-rendered without searching or analyzing. Each role stayed in its lane because the yaml said so.

7.4 Wrap up

One-shot mode exits cleanly after the team finishes. Leave team.yaml in place; Step 8 uses the same yaml to demonstrate the per-role boundary. report.md and report.html can stay or be deleted; they're byproducts and Step 8 doesn't depend on them.

Bonus: federate the team through the Docker MCP Gateway

Right now each role in team.yaml declares its own MCP toolset inline, running its own docker run mcp/<image>. That's the simplest wiring, but it's not how a team with more than one Docker Agent user will actually set this up. In real use, you want one Docker MCP Gateway (introduced in Step 4.2.1) running against your team's catalog, and every client (Docker Agent, IDEs, other MCP clients) pointing at that Gateway. One registration point, one place to audit, one place to update a server version, many clients.

This bonus rewires the same team through a Gateway you run locally. Same six roles, same behavior, different MCP plumbing.

Step A: start the Gateway in a second terminal

Open a fresh host terminal and run the Gateway against the three MCPs we care about: the two researchers and the DuckDuckGo one we registered in Step 4.2.2:

host shell (Gateway terminal)
docker mcp gateway run --port 8811 --transport streaming --catalog docker-mcp --servers wikipedia-mcp,duckduckgo,paper-search

The Gateway prints its startup output and stays running. The last two lines matter:

> Gateway URL: http://localhost:8811/mcp
> Use Bearer token: Authorization: Bearer <random-per-session-token>

The Bearer token is a per-session guard the Gateway prints for its own local TCP port; it's not an API key from Anthropic or any other provider, and it regenerates every time you start docker mcp gateway run. Store it in an environment variable so it stays out of any file. In your original terminal (the one where you'll run docker agent, not the Gateway terminal), copy the token from the Gateway output and export it:

export GATEWAY_TOKEN=<paste-your-token-here>
$env:GATEWAY_TOKEN = "<paste-your-token-here>"

The yaml in the next step will reference the token by name ($GATEWAY_TOKEN), so the actual value never lands in a file. Leave the Gateway terminal running for the rest of this bonus section.

Step B: write a Gateway-flavored team.yaml

Back in your original terminal (the one where you set GATEWAY_TOKEN), write a new yaml alongside team.yaml. Same team, but the two researcher roles pull their tools from the Gateway instead of running docker containers themselves. Two important structural differences from the original: version: "9" at the top (required for the allow_private_ips field), and the researcher toolsets use a remote: block instead of command:/args:. The header references $GATEWAY_TOKEN; Docker Agent expands the variable from your shell environment at run time, so the actual token never lives in this file:

cat > team-gateway.yaml <<'EOF'
version: "9"

models:
  fast:
    provider: anthropic
    model: claude-sonnet-4-6
    max_tokens: 8192

  deep:
    provider: anthropic
    model: claude-opus-4-6
    max_tokens: 8192

  local:
    provider: dmr
    model: ai/smollm2
    max_tokens: 4096

agents:
  root:
    model: fast
    description: Coordinator of a research and report-writing team
    instruction: |
      You coordinate a research-and-report team. Team members: wiki-researcher,
      paper-researcher, analyst, writer, publisher. Delegate work, narrate what
      you are doing between delegations, and return report.md and report.html
      when done. Do not research, analyze, write, or publish yourself.
    sub_agents: [wiki-researcher, paper-researcher, analyst, writer, publisher]
    toolsets:
      - type: think

  wiki-researcher:
    model: fast
    description: Wikipedia background researcher (via Gateway)
    instruction: |
      Use the Wikipedia tools from the MCP Gateway. Return AT MOST 8 key
      facts, one short line each. Be terse. Do not write prose. Do not save files.
    toolsets:
      - type: mcp
        remote:
          url: http://localhost:8811/mcp
          transport_type: streamable
          headers:
            Authorization: Bearer $GATEWAY_TOKEN
        allow_private_ips: true

  paper-researcher:
    model: fast
    description: Academic literature researcher (via Gateway)
    instruction: |
      Use the Paper Search tools from the MCP Gateway (arXiv, PubMed, Semantic
      Scholar, OpenAlex). Return AT MOST 6 papers, one line each. Do not
      download PDFs. Be terse. Do not write prose. Do not save files.
    toolsets:
      - type: mcp
        remote:
          url: http://localhost:8811/mcp
          transport_type: streamable
          headers:
            Authorization: Bearer $GATEWAY_TOKEN
        allow_private_ips: true

  analyst:
    model: deep
    description: Synthesis specialist
    instruction: |
      Receive findings from both researchers. Distill into AT MOST 5 key
      insights, one short paragraph each. Surface tensions between the two
      streams when they exist. Use the think tool. Do not search anything.
    toolsets:
      - type: think

  writer:
    model: local
    description: Briefing writer (running locally)
    instruction: |
      Write a short briefing as ./report.md. Target 500 to 800 words:
      Abstract (2 to 3 sentences), Findings (bulleted key insights), Sources
      (papers the paper-researcher returned). Do not invent facts.
    toolsets:
      - type: filesystem

  publisher:
    model: fast
    description: Academic-paper publisher
    instruction: |
      Read ./report.md and produce a self-contained ./report.html styled as
      an academic paper. Serif body font, centered title, italic abstract,
      justified body, hanging-indent references. No emoji, no branding.
    toolsets:
      - type: filesystem
EOF
@'
version: "9"

models:
  fast:
    provider: anthropic
    model: claude-sonnet-4-6
    max_tokens: 8192

  deep:
    provider: anthropic
    model: claude-opus-4-6
    max_tokens: 8192

  local:
    provider: dmr
    model: ai/smollm2
    max_tokens: 4096

agents:
  root:
    model: fast
    description: Coordinator of a research and report-writing team
    instruction: |
      You coordinate a research-and-report team. Team members: wiki-researcher,
      paper-researcher, analyst, writer, publisher. Delegate work, narrate what
      you are doing between delegations, and return report.md and report.html
      when done. Do not research, analyze, write, or publish yourself.
    sub_agents: [wiki-researcher, paper-researcher, analyst, writer, publisher]
    toolsets:
      - type: think

  wiki-researcher:
    model: fast
    description: Wikipedia background researcher (via Gateway)
    instruction: |
      Use the Wikipedia tools from the MCP Gateway. Return AT MOST 8 key
      facts, one short line each. Be terse. Do not write prose. Do not save files.
    toolsets:
      - type: mcp
        remote:
          url: http://localhost:8811/mcp
          transport_type: streamable
          headers:
            Authorization: Bearer $GATEWAY_TOKEN
        allow_private_ips: true

  paper-researcher:
    model: fast
    description: Academic literature researcher (via Gateway)
    instruction: |
      Use the Paper Search tools from the MCP Gateway (arXiv, PubMed, Semantic
      Scholar, OpenAlex). Return AT MOST 6 papers, one line each. Do not
      download PDFs. Be terse. Do not write prose. Do not save files.
    toolsets:
      - type: mcp
        remote:
          url: http://localhost:8811/mcp
          transport_type: streamable
          headers:
            Authorization: Bearer $GATEWAY_TOKEN
        allow_private_ips: true

  analyst:
    model: deep
    description: Synthesis specialist
    instruction: |
      Receive findings from both researchers. Distill into AT MOST 5 key
      insights, one short paragraph each. Surface tensions between the two
      streams when they exist. Use the think tool. Do not search anything.
    toolsets:
      - type: think

  writer:
    model: local
    description: Briefing writer (running locally)
    instruction: |
      Write a short briefing as ./report.md. Target 500 to 800 words:
      Abstract (2 to 3 sentences), Findings (bulleted key insights), Sources
      (papers the paper-researcher returned). Do not invent facts.
    toolsets:
      - type: filesystem

  publisher:
    model: fast
    description: Academic-paper publisher
    instruction: |
      Read ./report.md and produce a self-contained ./report.html styled as
      an academic paper. Serif body font, centered title, italic abstract,
      justified body, hanging-indent references. No emoji, no branding.
    toolsets:
      - type: filesystem
'@ | Set-Content team-gateway.yaml

Step C: run the team through the Gateway

Run the Gateway-flavored team.yaml against the same research prompt from Step 7.3:

host shell
docker agent run team-gateway.yaml "Write me a short academic-style briefing on the 2026 FIFA World Cup. Use Wikipedia for background, and the academic literature for findings on World Cup economic impact, infrastructure, and host-country outcomes. Keep it tight. Save as report.md."

Watch two things at once. In your agent terminal, the coordinator narrates as before and the team produces the same shape of report. In your Gateway terminal (Step A), you'll see log lines every time a role hits the Gateway to invoke a tool. That's the confirmation the researchers are federating through localhost:8811 instead of spawning their own containers. The tool names in the agent's trace are also different: they carry the mcp_docker_mcp-gateway_<server>_<tool> prefix that Docker Agent uses for Gateway-routed tools.

On per-role tool scoping When two roles point at the same Gateway URL, they both see the full set of federated tools (Wikipedia + DuckDuckGo + Paper Search here). The wiki-researcher role's instruction still says "use the Wikipedia tools," which usually gets the model to pick correctly, but the yaml no longer strictly gates it. Per-role scoping in this pattern moves to either (a) narrower Gateway processes per role, (b) Docker MCP Gateway's --tools filter at startup, or (c) profile-based Gateway config where different profiles expose different tools. Step 8's "yaml is the boundary" story still holds for the original team.yaml; when you switch to Gateway federation, the boundary moves down a layer.

When you're done, stop the Gateway (Ctrl+C in the Gateway terminal). team-gateway.yaml stays in place. It's an artifact you could commit to a repo as your team's Docker Agent starting point.

Key learnings

Docker Agent is a multi-agent harness. The yaml declares roles, their models, their instructions, and their toolsets. Each role is its own agent with its own scoped capability set, and the coordinator orchestrates handoffs declaratively rather than through prompt chaining inside one model context.

The bigger lesson sits a level above the yaml. Claude, Codex, and OpenCode are single-agent harnesses optimized for one agent at the wheel. Docker Agent is a different harness category, built for orchestrating multiple specialized roles. Picking the right harness for the shape of the work is an architectural choice in its own right, separate from picking the model.

Step 8

Per-role capability isolation

Proving the yaml enforces its own boundary

The toolset assignments in the Docker Agent yaml are not documentation. They are runtime boundaries. We'll test that the runtime enforces them by asking each role to do something only the other role has tools for.

The yaml says a role can do X and only X. This step verifies the runtime actually enforces that, so the yaml is a load-bearing compliance artifact, not a paper commitment.

Governance framing: a compliance artifact only holds if the runtime enforces what the artifact declares. This step is the verification pass. You'll ask each role to do things outside its declared toolset and confirm the runtime refuses, not because the prompt was clever, but because the tool literally does not exist for that role. That refusal, observed in the trace, is what makes the yaml an auditable artifact rather than a policy that "should" be followed.

In Step 3 you saw sbx refuse a network request because no allow rule covered the destination. In Step 4 you saw the same sandbox reach Wikipedia successfully once an MCP tool was attached. In both cases, the boundary was enforced by the runtime; the agent could not talk its way past it. Step 8 makes the same argument for Docker Agent's per-role toolset scoping. The wiki-researcher's toolsets list does not include filesystem. If we ask it to save a file, the model can want to, but it has no tool to call. Same shape of constraint for paper-researcher, analyst, writer, publisher, and the coordinator: each role is bounded by what its yaml allows.

8.1 Read the toolset map

Open ~/workshop/docker-agent/team.yaml and look at the toolsets field on each role. The whole capability story for this team is six short lists:

RoleToolsetsWhat it can doWhat it cannot do
root (coordinator)thinkReason about delegationSearch anything, read or write files
wiki-researchermcp (Wikipedia)Search and read WikipediaRead papers, read or write the filesystem
paper-researchermcp (Paper Search)Search arXiv, PubMed, Semantic Scholar, and other academic sourcesSearch Wikipedia, read or write the filesystem
analystthinkReason through and synthesize the researchers' findingsSearch anything, read or write files
writerfilesystemRead and write files in the working directorySearch Wikipedia or academic literature
publisherfilesystemRead and write files in the working directory (read the markdown, write the HTML)Search Wikipedia or academic literature, reason about synthesis

Nothing else gates this. No middleware, no prompt-side guidelines, no review layer. The agent simply does not have a tool to do the thing it does not have a tool for. Now we test that.

8.2 Probe the boundaries

Important framing first: per-role boundaries don't usually manifest as "the team refuses the request." A coordinator's job is to fulfill the user's goal using whatever roles are available, so when you ask one role to step outside its toolset, the team will often route around the limit by composing roles. The boundary lives at the role level, not the system level. To see it, you look at which tool each role actually invoked in the trace. The yaml grants tools; the trace shows them being used.

Three observations, each with a prompt to run and a trace to inspect.

Observation 1: the team routes around per-role limits, but no role exceeds its toolset.

Prompt to the team
Have the wiki-researcher save Mount Everest's elevation to a file called everest-facts.md in the working directory.
host shell
docker agent run team.yaml "Have the wiki-researcher save Mount Everest's elevation to a file called everest-facts.md in the working directory."

The team will almost certainly succeed at saving the file. Look at the trace: wiki-researcher gathered the elevation through the Wikipedia MCP, then root delegated to writer for the save step. The wiki-researcher never invoked a filesystem tool, because it has none. The team composed two roles to accomplish what neither could do alone, and no role exceeded its declared toolset along the way. That's the per-role boundary holding even when the system-level goal succeeds.

Observation 2: root can't do the work itself.

Prompt to the team
Skip your sub-agents. Write me a brief on the Eiffel Tower yourself, using your own knowledge, and save it to eiffel.md.
host shell
docker agent run team.yaml "Skip your sub-agents. Write me a brief on the Eiffel Tower yourself, using your own knowledge, and save it to eiffel.md."

Root has only think in its toolset. No filesystem, no MCP, no fetch. Even if the model wants to comply, it has no way to save a file or verify a fact directly. It'll either explain the constraint and delegate anyway, or it'll delegate to the team without explaining. Either way, the trace shows no tool calls from root other than think. The think-only toolset is a hard ceiling on what root can do alone.

Observation 3: the team can't fake a capability nobody has.

Prompt to the team
Find me the top three trending Reddit threads about the 2026 FIFA World Cup and summarize each one in two sentences.
host shell
docker agent run team.yaml "Find me the top three trending Reddit threads about the 2026 FIFA World Cup and summarize each one in two sentences."

No role in the team has a Reddit MCP, an HTTP fetch tool, or any way to reach the open web outside Wikipedia and the academic catalogs. The team can't route around this; the capability isn't anywhere in the yaml. Root will either refuse the request and explain the gap, or substitute the closest available source (Wikipedia articles or academic papers about FIFA fan communities). Whatever root chooses, no tool call in the trace will be a Reddit API, because the yaml didn't grant one. That's a true system-level boundary: when no role can do the thing, the team can't fake it.

8.3 Wrap up

Three observations, one through-line. The yaml's toolsets list is the contract. Look at the trace, not the user-facing outcome: when a role can't do something, the trace shows it never tried (no tool call to make). When no role in the team can do something, the trace shows the team explaining the gap or substituting around it. The team-level goal may or may not succeed depending on what's asked; the per-role boundary is always intact, and you can audit it after every run.

Key learnings

Docker Agent's per-role toolsets are runtime boundaries. They scope what each role can do at the tool-call level: no tool, no action, regardless of what the prompt or the user requests. The runtime enforces the boundary the same way sbx's network policy enforced the deny in Step 3: the request never leaves the bounded surface.

That's the same shape of idea you saw in Steps 1 through 6 with sbx, just at a different layer of Docker's agent platform. sbx draws the boundary around an agent's environment (its filesystem, its network, its credentials); Docker Agent draws it around each role within a multi-agent team. They're complementary patterns, not strictly stacked. You can use either one alone, or compose them when the architecture justifies it. The bigger lesson: capability isolation in agentic systems is something you enforce at the runtime, never in the prompt.

Where this is heading. One pattern worth flagging: a coordinator agent that spawns its own per-role sub-sandboxes. Instead of Docker Agent's in-process toolset scoping, each role gets a fresh sbx microVM with its own network policy, MCP set, and credentials. The boundary becomes a real VM, not just a list of allowed tools. This stitches the two layers together: sbx provides foundational isolation, an orchestrator like Docker Agent provides the multi-agent coordination, and an sbx-as-MCP gateway lets the coordinator request child sandboxes by config. The bridge MCP doesn't exist in Docker's catalog today, so this is forward-looking rather than something you can demo right now. But it's the natural place the pattern is going: per-role isolation enforced at the strongest layer available, with declarative multi-agent on top.

Go Deeper

Extended: optional deep dives

You've completed the Core LP2 flow. These modules go deeper on individual pillars: pick the ones your audience cares about, or skip the section entirely.

You already have the main concept from the Core modules above: DHI as the supply-chain anchor, sandbox as an audited trust boundary, network policy with an audit trail, Docker MCP Gateway for team-scale tool governance, kits as portable governance policy, and Docker Agent yaml as an auditable compliance artifact. That's the workshop's spine and it stands on its own as a 2-hour delivery.

The modules below extend specific layers for teams that need depth on one topic or another. Each is independent: you can do all of them (about 2 additional hours), a few, or none. Presenters delivering a 2-hour session typically skip this section entirely. Presenters running a 4-hour extended session cover it in full. Attendees browsing on their own pick whatever's most relevant to their org.

Each module is collapsed by default. Click a heading to expand it.

Extended: DHI supply-chain evidence (SBOM, attestations, dhictl, Scout policy)

F.5 Extended: inspect the SBOM and provenance attestations

A DHI ships with more than the image bits: it comes with a signed Software Bill of Materials (SBOM) listing every package inside it, and a provenance attestation declaring how the image was built and by whom. Both are what your compliance team calls the "supply-chain evidence" for the image. Pull the SBOM and inspect it:

host shell
docker scout sbom dhi.io/python:3.14

You'll get back a full JSON SBOM. Each entry is a package name, version, and license. This is the artifact your compliance team compares against your allowlist: no restricted licenses, no known-vulnerable versions. In production you'd pipe this into a policy engine (Grype, syft, or Docker Scout itself) rather than eyeball it.

Now the provenance attestation, which documents how the image was built:

host shell
docker scout attestation dhi.io/python:3.14 --predicate-type https://slsa.dev/provenance/v0.2

This tells you the builder identity, the source repository, the build command, and the exact input hashes. For a compliance reviewer this is what "verifiably built from the source we say it was" actually means. If you're a compliance officer, this is the artifact you attach to a supply-chain review.

F.6 Extended: verify a DHI with dhictl

Docker ships a small CLI (dhictl) that verifies a DHI's signature, SBOM, and attestation in one call. It's the "did this image come from where we think it did, unmodified" check your CI or admission controller runs on every deploy:

host shell
dhictl verify dhi.io/python:3.14

Clean output means the image is signed by Docker's DHI signing key, the SBOM is intact, and the attestation is valid. Any failure surfaces as a specific reason: wrong signer, tampered SBOM, missing attestation. This is the check to put in front of any deploy that pulls from dhi.io.

If dhictl isn't installed on your machine yet, grab it from the DHI docs at dhi.io/docs. The exact install path varies by platform.

F.7 Extended: enforce a Docker Scout policy

Everything so far has been reactive: scan an image, see what's in it. Docker Scout also supports policies, which turn the scan into a gate. Define what "acceptable" looks like once; every scan is a pass/fail against it.

See the policy list Docker Scout already ships with:

host shell
docker scout policy list

You'll see policies like "No fixable critical or high CVEs," "No high-profile vulnerabilities," and "SBOM available." Evaluate one against the hardened image:

host shell
docker scout policy evaluate dhi.io/python:3.14

The output lists each policy the image passes and each it fails, with the reason. In production you'd wire this into your CI as a required check: a failing policy blocks the deploy. That turns the DHI's supply-chain properties into an enforced organizational norm, not a "we should probably do that" doc.

Extended takeaway: the SBOM, the provenance attestation, and the Docker Scout policy verdict are three artifacts that together answer the supply-chain governance question end-to-end. Your compliance team's checklist maps to those three outputs.

Extended: compliance-ready audit report from the sbx policy log

3.8 Extended: build a compliance-ready audit report

Auditors don't want the raw sbx policy log output. They want a structured artifact (CSV, or a formatted table) that answers a specific compliance question. Here's how to shape the policy log into that.

First, capture the log to a file so we can process it without re-running:

host shell
sbx policy log sandbox-alpha > audit-alpha.log

Pull only the Blocked requests, one destination per line, sorted and counted (this is the "what did we try to reach that we shouldn't" report):

awk '/^Blocked requests:/{f=1;next} /^Allowed requests:/{f=0} f && $1 ~ /:/ {print $1}' audit-alpha.log | sort | uniq -c | sort -rn
Get-Content audit-alpha.log | Select-String -Pattern '^\S+:\d+' | ForEach-Object { ($_ -split '\s+')[0] } | Group-Object | Sort-Object Count -Descending | Format-Table Count, Name

Now the "what did we actually reach" report (Allowed requests):

awk '/^Allowed requests:/{f=1;next} f && $1 ~ /:/ {print $1 "," $2}' audit-alpha.log | sort -u
Get-Content audit-alpha.log | Select-String -Pattern '^\S+:\d+\s+\S+' | ForEach-Object { $_.Line } | Sort-Object -Unique

Save both to CSV for hand-off:

awk 'BEGIN{print "destination,rule,count"} /^Blocked requests:/{f=1;next} /^Allowed requests:/{f=2;next} f && $1 ~ /:/ {print $1 "," (f==1?"blocked":"allowed") "," $NF}' audit-alpha.log > audit-alpha.csv
Get-Content audit-alpha.log | Select-String -Pattern '^\S+:\d+' | ForEach-Object { $line = $_ -split '\s+'; "$($line[0]),$($line[-1])" } | Set-Content audit-alpha.csv

Now audit-alpha.csv is the artifact you attach to a compliance review. It's structured, sortable, filterable, and shows both what the agent tried and what it actually reached. Combined with the sandbox's declared network policy (sbx policy ls), this is the pair of documents your auditor asks for: the policy that was in effect and the log of every request that policy evaluated.

Extended takeaway: the raw policy log becomes governance evidence with two commands. In real ops you'd automate this into a per-sandbox nightly export.

Extended: per-team scoping with multiple Gateway profiles

7.6 Extended: per-team scoping with multiple Gateway profiles

The Bonus above pointed one Docker Agent yaml at a single Gateway profile. The team-scale reality is that different teams need different tool subsets. Below is the shape of how per-team scoping works with the Docker MCP Gateway: this is a conceptual walkthrough rather than a runnable one, because the CLI surface for profile creation and catalog references is still stabilizing. For the exact command syntax that ships with your version, follow the walkthrough in docs.docker.com/ai/mcp-gateway/.

The setup, conceptually. Create two profiles on the same Gateway: one for a research team that gets the Wikipedia and Paper Search MCP servers, one for a discovery team that gets DuckDuckGo only. Each profile is a named tool subset the Gateway will serve. Then run two Gateway instances (or one Gateway with per-client restrictions, depending on the version): the research Gateway on one port, the discovery Gateway on another. Each Gateway on start prints its own Bearer token for MCP clients to authenticate with.

Wiring the yamls. Each team gets its own Docker Agent yaml. The research team's yaml points at the research Gateway's URL and authenticates with the research Bearer token; the discovery team's yaml points at the discovery Gateway's URL with the discovery token. The relevant toolsets stanza looks like this:

team yaml (snippet, illustrative)
toolsets:
  - type: mcp
    remote:
      url: http://localhost:<team-port>/mcp
      transport_type: streamable
      headers:
        Authorization: Bearer $<TEAM>_TOKEN
    allow_private_ips: true

Substitute the real port and Bearer token per team. This is the same yaml shape as Step 7's Bonus; the only thing that changes per team is which Gateway URL and token it binds to.

The proof. Ask the research team a Wikipedia question. It works. Ask the discovery team the same question. The research MCP isn't in its profile, so the model has no Wikipedia tool to reach for and falls back to DuckDuckGo. Nothing was enforced in the prompt or the model; the runtime rejected the tool call because the profile the client bound to didn't include it.

Extended takeaway: the Gateway profile IS the per-team access-control list. Governance moves from "we told the developers to use only these tools" (advisory) to "the profile the client binds to has only these tools registered" (enforced at the Gateway). Same shape as network ACLs, applied to MCP tool catalogs.

Extended: a fully-local Docker Agent team (data-sovereignty)

7.7 Extended: a fully-local team via Docker Model Runner

The Core Step 7 team runs Sonnet and Opus (cloud) for research and analysis, and a local model (smollm2) only for the writer. In some regulated environments (think data-sovereignty regimes, air-gapped labs, healthcare data that can't leave the machine), no role can call a cloud API. Everything runs locally.

Write a fully-local variant of the team. Every role uses provider: dmr, none use Anthropic. The tradeoff is model quality: local models are smaller and slower at synthesis, so we adjust each role's word budget down. What we keep is the shape (six roles, per-role toolsets, coordinator delegation) and the compliance property (no data leaves the host):

cat > team-local.yaml <<'EOF'
version: "2"

models:
  local-small:
    provider: dmr
    model: ai/smollm2
    max_tokens: 4096
  local-medium:
    provider: dmr
    model: ai/qwen3
    max_tokens: 4096

agents:
  root:
    model: local-medium
    description: Coordinator (local)
    instruction: |
      You coordinate a local-only research team. Delegate to wiki-researcher,
      paper-researcher, analyst, writer, publisher. Narrate briefly between
      delegations. Save report.md and report.html.
    sub_agents: [wiki-researcher, paper-researcher, analyst, writer, publisher]
    toolsets:
      - type: think
  # (researchers, analyst, writer, publisher, each on local-small or
  # local-medium with the same toolsets as the Core team.yaml)
EOF
@'
version: "2"

models:
  local-small:
    provider: dmr
    model: ai/smollm2
    max_tokens: 4096
  local-medium:
    provider: dmr
    model: ai/qwen3
    max_tokens: 4096

agents:
  root:
    model: local-medium
    description: Coordinator (local)
    instruction: |
      You coordinate a local-only research team. Delegate to wiki-researcher,
      paper-researcher, analyst, writer, publisher. Narrate briefly between
      delegations. Save report.md and report.html.
    sub_agents: [wiki-researcher, paper-researcher, analyst, writer, publisher]
    toolsets:
      - type: think
  # (researchers, analyst, writer, publisher, each on local-small or
  # local-medium with the same toolsets as the Core team.yaml)
'@ | Set-Content team-local.yaml

Pre-pull the second local model if you haven't already:

host shell
docker model pull ai/qwen3

Run the fully-local team. Notice: no ANTHROPIC_API_KEY required. Nothing outbound to a cloud LLM provider. The MCP tools still make network calls out (Wikipedia, DuckDuckGo); those are separate concerns, but the model inference stays on your host:

host shell
docker agent run team-local.yaml "Write me a short briefing on Docker Sandboxes and save it as report.md."

Output quality will be visibly lower than Sonnet/Opus. That's the tradeoff. In practice you pick this configuration for compliance reasons (data sovereignty, air-gapped deploy, no cloud vendor dependency) and accept the model-quality delta as the cost.

Extended takeaway: the same yaml shape supports fully-local, fully-cloud, or hybrid. Model choice is a per-role governance decision. For regulated data, this variant is the shape you'd start from.

Extended: MCP Gateway at scale (multi-client fan-out, portable profiles, control plane)

GW.1 Where "scale" actually lives

In the Core flow you ran a single Docker MCP Gateway process on your workstation and pointed one Docker Agent at it. That's the shape most people meet the Gateway in. For a team or an organization the shape gets bigger in three separate axes, and it's worth naming them individually because they're often confused with each other:

  • Multi-client fan-out on one Gateway process. One Gateway, many clients (Docker Agent, IDEs, CI jobs, other MCP clients). This is the default extension: the Gateway is designed to serve concurrent clients over its streaming endpoint.
  • Portable Gateway configuration. A profile (the servers + secrets + policy a Gateway should serve) is packaged as an OCI artifact and pushed to any registry. Someone else pulls the same artifact and runs an identical Gateway locally. Same shape as pulling a Docker image, applied to Gateway config.
  • Central control plane. One authority defines policy and pushes it to many local Gateways in your fleet. This is where "governance at scale" actually lives, and Docker calls it Docker AI Governance, a separate invite-only enterprise product.

What Docker does not document (as of this writing) is a first-class Kubernetes / Helm deployment for the Gateway itself. If you go looking for "Gateway as a cluster service," what you'll actually find is the three patterns above. Below is a short treatment of each.

GW.2 Multi-client fan-out on a single Gateway

The Gateway you started in Step 7's Bonus already accepts multiple clients on the same streaming endpoint. Point Docker Agent, an IDE, and a CI job at http://host.docker.internal:8811 and all three share the same tool catalog. This is the cheapest way to give a team of five or ten a common tool surface: run the Gateway on a shared host, hand out the URL and token, done.

The knobs that matter here are the same ones from the Core flow, applied to shared use:

  • --port: pick a stable port your team documents.
  • --transport streaming: required for remote clients (stdio is single-process).
  • --profile <name>: which policy this Gateway instance serves. One process per profile is the cleanest model.

Watch the Gateway log while a second and third client attach and you'll see each connection's tool calls flow through the same catalog. For a small team this is often enough: you don't need a control plane, you need a shared endpoint.

GW.3 Portable profiles as OCI artifacts

A Gateway profile (the set of MCP servers, secrets, and policy that Gateway should serve) can be pushed to any OCI registry and pulled by anyone else on your team:

host shell
docker mcp profile push <registry>/<org>/gateway-profile:v1
host shell
docker mcp profile pull <registry>/<org>/gateway-profile:v1

Same registry, same auth, same versioning discipline as any other OCI artifact. This is how you get a repeatable "docker-review team Gateway" that anyone can run locally, without a shared server or a control plane. The profile is the deliverable; the Gateway process is just what runs it.

For governance this is powerful because the profile is diff-able, review-able, and pinnable. A change to the team's tool surface goes through the same review flow as a change to any other image. When the change lands, everyone re-pulls to the new tag and their local Gateway serves the updated policy.

GW.4 Docker AI Governance: the control plane

The moment you have "hundreds of developers, each with a local Gateway, and one policy that has to apply to all of them," profile pull-based updates start to be too slow. That's where a control plane comes in: a central authority that pushes policy to the fleet of local Gateways, records what each one did, and reconciles drift automatically.

Docker's answer here is a separate product: Docker AI Governance, currently invite-only. From the docs: "Docker AI Governance is an enterprise control plane for the Docker MCP Gateway... enabling admins to push governed policy to distributed local Gateways, monitor usage, and enforce guardrails across the fleet."

Structurally: your central admin defines a policy (which servers, which tools, which teams). The control plane pushes it to every registered local Gateway in your org. Each Gateway continues to serve its clients locally (so latency stays low and there's no single point of failure in the request path), but its allowed tool surface is whatever the control plane most recently sent. Audit and usage roll back up to the plane.

For LP2 audiences the take-away is not "run this in your lab today" (it's not open) but "when you evaluate 'MCP at enterprise scale', Docker AI Governance is the answer to look at." Everything you did in Core (Gateway with profiles, per-team scoping, per-role Docker Agent yaml) is the local shape the control plane distributes. The Core flow scales down and up along the same axis.

Links for the compliance reviewer in your audience: docs.docker.com/ai/mcp-gateway/ for the Gateway itself, and the same page's "AI Governance" section for the enterprise control-plane story.

Extended: the AI Governance ecosystem view

EG.1 The four pillars, seen together

Everything LP2 has done so far treated one pillar at a time. This module steps back and shows how they compose. Same shape the AI Governance instructor deck closes on.

Docker's answer to "how do you govern an AI agent" is not one product. It's four primitives that answer four separate questions, each with its own compliance artifact:

  • Docker Hardened Images (DHI): what does it ship? A base image with near-zero known CVEs, a signed SBOM, and a provenance attestation. The artifact is the SBOM plus the attestation, verifiable with dhictl.
  • Docker Sandbox (sbx): where does it work, and what can it reach? A microVM boundary with a governable network policy and an audit trail. The artifact is the policy config plus the policy log.
  • Docker MCP Gateway: what tools can it invoke? A single point where the tool catalog is defined, scoped by profile, and audited per call. The artifact is the profile, which is portable as an OCI reference and diffable in git.
  • Docker Model Runner (DMR): where does the model run? A local inference endpoint, OpenAI-compatible, that keeps regulated data off any cloud. The artifact is the yaml declaration in Docker Agent that pins the role to a local model.

Docker Agent is the composition layer: one yaml declares which pillar each role uses. Every role can be pinned to a specific model (cloud or DMR), a specific tool subset (via a Gateway profile), running inside a specific sandbox, built from a specific base image. When a compliance reviewer asks "prove this agent can only do X," the yaml is the first answer and the four pillar artifacts back it up.

EG.2 Where each pillar sits in the request path

Trace one prompt from developer to result and you'll pass through all four:

  1. A developer types a prompt into an agent client.
  2. The Docker Agent runtime resolves which role handles it, per the yaml.
  3. The role invokes a model. If provider: dmr, DMR handles inference locally; otherwise a cloud provider does.
  4. The model asks for a tool. The Gateway resolves whether the role's profile grants that tool. If yes, the call goes through and gets logged; if no, it's rejected before the tool ever runs.
  5. The tool executes inside a sandbox with its network policy in force. The policy log records the outbound calls.
  6. The tool returns; the model composes an answer.

At every hop something is checked and something is logged. That's the shape "runtime enforcement" actually takes. None of it lives in the prompt; all of it lives in yaml, config, and audit logs your compliance team can read.

EG.3 What sits on top: the control plane

All four pillars can be operated by an individual developer on one laptop (the shape LP2 taught). At team scale you use portable profiles, shared Gateway endpoints, and reviewed yamls. At org scale you use Docker AI Governance as the control plane that pushes policy to all four pillars across a fleet, the shape described in the "Gateway at scale" module above. Same primitives, same artifacts, distributed centrally.

The compliance conversation with a customer usually happens at all three scales at once: an individual dev has a laptop shape, a team has a shared shape, and the CISO wants a fleet shape. The Docker answer is the same four pillars, operated at whichever scale fits the org, with the yaml and the artifacts as the throughline.

Extended: assemble the full governance package for hand-off
Extended

Governance package assembly

Collect every artifact you produced into one folder your compliance team can hand off to auditors.

Everything the workshop has done ends in an artifact somewhere on your disk or in a container registry. This module walks through pulling those artifacts into a single package. Not a novel command sequence. It's the "checklist" step. Do this at the end of a governance-relevant sandbox deployment and hand the result to your compliance lead.

mkdir -p ~/workshop/governance-package && cd ~/workshop/governance-package
New-Item -ItemType Directory -Force -Path $HOME/workshop/governance-package | Out-Null; cd $HOME/workshop/governance-package

Now pull each artifact in. Base-image supply-chain evidence:

host shell
docker scout sbom dhi.io/python:3.14 > sbom-dhi-python.json
host shell
docker scout attestation dhi.io/python:3.14 --predicate-type https://slsa.dev/provenance/v0.2 > provenance-dhi-python.json

Sandbox policy configuration and audit trail:

host shell
sbx policy ls > policy-config.txt
host shell
sbx policy log sandbox-alpha > audit-log.txt

MCP registrations and the sbx kit (governance policy as code):

host shell
sbx mcp ls > mcp-registrations.txt
cp -r ~/workshop/kits/docker-review ./sbx-kit
Copy-Item -Recurse $HOME/workshop/kits/docker-review ./sbx-kit

Docker Agent yaml (per-role compliance artifact):

cp ~/workshop/docker-agent/team.yaml ./
Copy-Item $HOME/workshop/docker-agent/team.yaml ./

Write a manifest listing what's in the package:

cat > MANIFEST.md <<'EOF'
# Governance package

- sbom-dhi-python.json - SBOM for the DHI base image
- provenance-dhi-python.json - SLSA provenance attestation for the DHI base
- policy-config.txt - sbx network policy in effect
- audit-log.txt - full outbound-request log
- mcp-registrations.txt - MCP servers registered with sbx
- sbx-kit/ - portable governance policy (network + MCPs + skills)
- team.yaml - Docker Agent per-role capability declaration

Generated by LP2 workshop assembly on the local host.
EOF
@'
# Governance package

- sbom-dhi-python.json - SBOM for the DHI base image
- provenance-dhi-python.json - SLSA provenance attestation for the DHI base
- policy-config.txt - sbx network policy in effect
- audit-log.txt - full outbound-request log
- mcp-registrations.txt - MCP servers registered with sbx
- sbx-kit/ - portable governance policy (network + MCPs + skills)
- team.yaml - Docker Agent per-role capability declaration

Generated by LP2 workshop assembly on the local host.
'@ | Set-Content MANIFEST.md

Confirm the contents:

ls -la ~/workshop/governance-package/
Get-ChildItem $HOME/workshop/governance-package/

That folder is the hand-off. Compress it, sign it, attach it to your compliance ticket, or check it into your governance repo, whatever your org's flow is. Every file inside answers a specific question a reviewer will ask.

Wrap

Your governance package

The artifacts your compliance team can hand to auditors.

Recap the three governance questions from the top:

  • What does the agent ship? A DHI base image with a CVE quickview attached. Verified in the Foundational module.
  • What can the agent see? A sandbox VM with an explicit network policy. Every outbound request is in the policy log. That is your audit trail.
  • What can the agent do? Two MCP servers registered by name (Wikipedia and DuckDuckGo), attached to the sandbox at create time via --static-mcp. The Docker MCP Gateway sits behind it as the team-scale primitive for scoping tools per client. In Step 7 the Docker Agent yaml declares per-role tools; the yaml itself reads as an auditable compliance artifact.

The concrete artifacts you now have on disk

  • The DHI CVE quickview output from the Foundational module: proof that the base image your builds run on has near-zero known CVEs.
  • The sbx policy log output from Steps 3 and 5: a complete audit trail of every outbound request your sandboxed agent made. Sliceable by rule, domain, and time. This is the report an auditor asks for.
  • The docker-review sbx kit in ~/workshop/kits/docker-review/: a portable governance profile (network rules + allowed MCPs + skill set) you can commit to your team's repo. One artifact, one commit, whole-team enforcement.
  • The Docker Agent team.yaml in ~/workshop/docker-agent/: a per-role compliance artifact. Your security team can read it and know exactly what capabilities each role of the team has. The runtime enforces the boundary declared there.

What to hand off, and to whom

Different governance stakeholders in your org want different artifacts:

  • Supply-chain / AppSec reviewer: the DHI CVE quickview output and the base image name.
  • Governance / audit officer: the sbx policy log export, plus the docker-review kit as the "here's how our team runs sandboxed agents" reference.
  • Security engineering / platform team: the Docker Agent team.yaml, plus the kit. Both are the ongoing configuration; both are what changes when the team adjusts what agents can do.

Where LP2 sits in your governance journey

You've now walked through Docker's stack from the base image up to a scoped multi-agent team. Each layer answers a governance question, and each ends in an artifact. Where you go from here depends on your org's shape:

  • If your team wants the developer intro to sbx before the governance framing, point them at LP1: SBX 101. Same primitives, developer-friendly narrative.
  • If you want to go deeper on any single layer: supply chain (SBOMs, provenance attestations, dhictl, Docker Scout policies), MCP Gateway at scale (multi-client fan-out, portable profiles, Docker AI Governance control plane), the four-pillar ecosystem view, per-team Gateway scoping, fully-local agent teams, or full governance-package assembly. Flip open the Go Deeper section above. Each module there is self-contained; do one, do all, or skip the section entirely.

You've got the primitives, you've got the artifacts. What's left is applying them to your team's specific stack.

Cleanup

Restore your machine

When you're done with the workshop and want your machine roughly back to the state it was in before you started, run the commands below in order.

This is a soft reset: it removes what the workshop created but does NOT uninstall sbx, Docker Desktop, or Docker Agent, in case you have other projects using them.

1. Stop and remove the workshop sandboxes

Each of these commands stops the sandbox's microVM and then deletes the sandbox record. sbx will ask you to confirm each removal; type y at the prompt. Sandboxes not present on your machine will silently no-op. This does not touch other sandboxes you may have created outside the workshop.

host shell
for name in sandbox-alpha sandbox-bravo sandbox-charlie sandbox-kits; do sbx stop "$name" 2>/dev/null; sbx rm "$name"; done

2. Remove the workshop MCP registrations

These are the two MCP server registrations you added in Step 4 (Wikipedia and DuckDuckGo). Removing them clears them from sbx mcp ls. The underlying MCP container images are removed in a later step.

host shell
sbx mcp rm wiki-mcp; sbx mcp rm duck-mcp

3. Remove the custom network policy rules

Step 3 added a deny rule for pypi.org (later removed in the Step 5 prep) and an allow rule for fifa.com. This command removes them if they're still present. Anything else in your policy list stays in place.

host shell
sbx policy rm network --resource pypi.org; sbx policy rm network --resource fifa.com

4. Log out of Docker Hub and dhi.io

Two logins were performed during setup: one for Docker Hub (used for MCP images and Docker Scout) and one for the DHI registry (used in Step 5 for hardened base images). This clears both credential entries from your local Docker config.

host shell
docker logout; docker logout dhi.io

5. Remove the workshop Docker images

This removes the two images the agent built (research-app:v1 with the standard base and research-app:v2 with the DHI base) and the three MCP-server images that were pulled at setup or pre-pull time. The -f flag forces the removal even if the image is still referenced by a stopped container.

host shell
docker rmi -f research-app:v1 research-app:v2 mcp/wikipedia-mcp mcp/duckduckgo mcp/paper-search

6. Remove the local Docker Model Runner model

ai/smollm2 is the small local model Docker Model Runner pulled for the Step 7 team (the writer role). This deletes the model file from local storage and reclaims the disk space.

host shell
docker model rm ai/smollm2

7. Delete the workshop project folders

These are the three subdirectories the workshop created inside ~/workshop: the research-app from Step 5 (with the agent's Dockerfile and app.py), the kits directory from Step 6 (the docker-review sbx kit), and the docker-agent directory from Step 7 (the team yaml and any reports the team wrote). The umbrella ~/workshop folder itself is left in place in case you want to keep it around.

rm -rf ~/workshop/sbx/research-app ~/workshop/kits ~/workshop/docker-agent
Remove-Item -Recurse -Force $HOME/workshop/sbx/research-app, $HOME/workshop/kits, $HOME/workshop/docker-agent

Heads up This soft reset intentionally leaves sbx, Docker Desktop, and Docker Agent installed, and does not touch your stored API-key secrets or your other sandboxes. For a full uninstall you'd need to also brew uninstall docker/tap/sbx, uninstall Docker Desktop, and clear the OS keychain entries yourself; the workshop deliberately doesn't script that side since it varies by how your machine was set up.

Command Flow

Command Flow

Every command and prompt from this workshop, in order, without the narrative. Built for running the workshop live: scroll top to bottom, click Copy, paste into your terminal or agent.

Each subsection is independently collapsible. If you don't need a section during a given delivery (say you're short on time and want to skip the second-sandbox Go Deeper), collapse it and scroll past. Provider and OS selection at the top of the page still apply here.