Skip to content
Level 5 · Agentic EngineerProjectPart 25 · page 11 of 1190 minSXMN 16 GB
90Minutes
7Tools
9Sources
All fourTracks
Tools used on this page7

Project: A Local Agentic Coding Workstation

Validated on: written from the documentation cited above; not yet validated on hardware on any track. The versions, model files and per-track results this project was reproduced with belong here once the validation pass has run it on real machines.

By the end of this project you will have a coding workstation on your own hardware that you keep using after the course: three models chosen for three roles behind stable aliases, at least two agents configured and tested against a real task, the MCP servers you built in Part 24 wired in, a sandbox that passes its boundary test, one script that rebuilds the whole thing, and a runbook that records every decision including the risks you decided to accept.

The deliverable is the runbook plus a passing check. Not the container, not the configuration files: those are reproducible from the script. The runbook is the part that cannot be regenerated, because it holds the reasoning.

This is the fifth capstone deliverable in draft. Build it properly now and the capstone asks you only to substitute your own improved model from Part 27.

The workstation, from the agent you type at down to the weights

  1. AgentsTwo or more, each with its own configuration file and its own permission mode. Terminal, editor, or bothyou use these
  2. The sandboxOne project directory, one network route, no credentials. Everything above this line runs inside itthe boundary
  3. MCP serversFrom Part 24. Tools the agent can call, each one schemas in the prompt on every turnkeep the list short
  4. Role aliaseslocal/agent, local/completion, local/judge. One virtual key per agent, so usage is attributable
  5. The Part 9 gatewayLiteLLM in front of llama-swap: retries, fallbacks, health checks, usage records, both API shapesunchanged from Part 9
  6. Engines and weightsllama-server, vLLM or an MLX server, loading the models the aliases point atnever leaves the machine
Three layers are accented because they are the three decisions this project asks you to make: which agents, what the boundary is, and which model does which job. Everything else you already built.

The load-bearing idea, again, is the alias. An agent’s configuration names local/agent. It does not know which weights that is, which engine serves them, or that you swapped the model last Tuesday. Change the alias’s target and six configuration files keep working.

The Part 9 gateway, the sandbox from this part’s second lab, at least one MCP server from Part 24, and two agents you have already run at least once. Ninety minutes, all of it attended, plus whatever model downloads you still need.

Memory. The floor is 16 GB, because the agent role is where the whole thing succeeds or fails and below 16 GB the model that fits is the one the second lesson said to use for learning the loop rather than for work. If you have less, build everything anyway and record honestly in the runbook that the agent role is under-provisioned; that is a legitimate result and the runbook has a place for it.

Track S — NVIDIA DGX Spark

Everything runs natively. With 128 GB you can hold all three roles resident at once, which is the configuration to aim for: put the three models in a persistent llama-swap group so that switching from an agent turn to a completion does not swap a model.

Consider whether the agent role should be the 96 GB-tier model rather than the 24 GB-tier coder. Measure both with the same task before deciding; the larger one is not automatically better for agent work, and it is certainly slower per turn.

Track X — AMD Ryzen AI Max+ 395

Everything runs natively. The constraint is Part 5’s cap on GPU-visible memory rather than the machine’s total: size the three roles against the smaller number. If all three do not fit, put the completion role on the processor and the agent role on the GPU, and record that split in the runbook because it changes the latency arithmetic Part 10 taught.

Track M — Apple silicon

Everything runs natively through the MLX or llama.cpp servers from Part 8 and Part 6. Two notes.

All three roles come out of the same unified pool as your editor and browser, so the memory arithmetic is a real budget rather than a formality; the model reference’s sizes plus your chosen context lengths is the sum to do before you start.

For the sandbox, either Docker Desktop or the dedicated-user route from the second lab. If you take the dedicated-user route, run check-workstation.sh with SKIP_SANDBOX=1 and record in the runbook which of the boundary-test checks legitimately do not apply and why.

Track N — NVIDIA desktop or laptop

Everything runs natively. On a 16 GB or 24 GB card the three roles will not be resident together, so let llama-swap swap them and put the completion role somewhere it will not be evicted: a separate small server on the processor is a legitimate arrangement and costs you nothing that matters at that model size. Record which roles share the card.

Working directory and terminal roles

Prepare the course execution workspace once before this procedure. It includes this part's scripts, data and shared Python helpers. In the client or training terminal, select this directory:

RunnableAll tracks

select this part’s execution directory
export LABS_ROOT="${LABS_ROOT:-$HOME/llm-course/labs}"
export LAB_DIR="$LABS_ROOT/part-25-coding-agents"
cd "$LAB_DIR"
pwd
test -f "workstation-aliases.yaml"

Expected result: pwd ends in part-25-coding-agents and the file check returns successfully. If it does not, finish workspace preparation before continuing. Activate the environment in the requirements for your track. Bare script and data filenames below are relative to this directory; paths to earlier experiments must point at the artefacts you actually retained.

Keep each foreground server in a separate terminal and send requests from this terminal. Reapply lesson-specific environment variables in each new shell. Stop at the first failed checkpoint and retain its output; the execution guide explains how to distinguish missing files, endpoint failures and capacity problems.

1. Choose the three models, and write down why

Section titled “1. Choose the three models, and write down why”

Do this before touching a configuration file, because every later step depends on it and because it is the step people skip.

The agent role drives the loop. It needs reliable tool calls, long context and enough decode speed for twenty turns. Start from the second lesson’s tier table and your own measurement: run the Part 24 reliability test against each candidate through the gateway, at the context length you intend to allow, and write the pass rates in the runbook. A model that fails there is not a candidate however good it looks elsewhere.

The completion role fires as you type and wants Part 10’s latency arithmetic rather than this part’s. Small, fill-in-the-middle, capped output length.

The judge role scores agent output in your evaluation suite, and the one firm rule is that it must not be the same weights as the agent role. A model marking its own work measures its own preferences, and the check script fails the workstation when the two aliases are the same.

RunnableAll tracks

workstation-aliases.yaml
# Purpose: the three role aliases the agentic workstation adds to the Part 9 gateway -
# one model for driving agents, one for editor completion, one for judging - so
# that every tool names a role rather than a model file.
# Platform: all (spark, strix, mac, nvidia)
# Minimum memory: 16 GB for the agent role; 8 GB if you point the agent role at the same
# model as the chat alias and accept the consequences the project page describes
# Assumes: merged into the model_list of the litellm-config.yaml you wrote in Part 9, with
# matching model ids in llama-swap.yaml. The names on the left are the public
# interface: nothing in this part should ever name a file or a port.
model_list:
# ------------------------------------------------------------------- the agent role
# Long context, native tool calling, and a small active parameter count so that twenty
# turns is not an afternoon. The project page has you choose this by tier and record the
# choice; the alias is what everything else refers to.
- model_name: local/agent
litellm_params:
model: openai/local/agent
api_base: os.environ/LLAMA_SWAP_BASE_URL
api_key: os.environ/LOCAL_API_KEY
# -------------------------------------------------------------- the completion role
# Small, fast, fill-in-the-middle. Part 10's latency arithmetic decides the size; the
# only thing that matters here is that it is a different model from the agent role and
# is not swapped out when the agent starts work.
- model_name: local/completion
litellm_params:
model: openai/local/completion
api_base: os.environ/LLAMA_SWAP_BASE_URL
api_key: os.environ/LOCAL_API_KEY
# -------------------------------------------------------------------- the judge role
# Used by the evaluation harness from Part 10 and Part 16 to score agent output. It must
# not be the same weights as the agent role: a model marking its own work is measuring
# its own preferences. Keep it small enough that scoring a suite is cheap.
- model_name: local/judge
litellm_params:
model: openai/local/judge
api_base: os.environ/LLAMA_SWAP_BASE_URL
api_key: os.environ/LOCAL_API_KEY
litellm_settings:
# An agent turn on a cold model can take a long time. This is the same reasoning as the
# Part 9 gateway's own timeout, restated because agents hit it far more often than chat.
request_timeout: 900
# Where each role goes when its own retries are exhausted. The agent falls back to the
# coder alias from Part 9 rather than to nothing; the judge does not fall back at all,
# because a silently substituted judge invalidates a score sheet.
fallbacks:
- local/agent: ["local/coder"]

Download workstation-aliases.yaml50 lines

Merge that into the model_list in the litellm-config.yaml you wrote in Part 9, add matching model ids to llama-swap.yaml, and restart the gateway. Then confirm all three answer:

RunnableAll tracks

the three roles must exist before anything else is worth configuring
curl --silent http://127.0.0.1:4000/v1/models \
--header "Authorization: Bearer ${GATEWAY_KEY}" \
| python3 -m json.tool

Output — what you should see

{
"data": [
{ "id": "local/agent", "object": "model" },
{ "id": "local/completion", "object": "model" },
{ "id": "local/judge", "object": "model" },
{ "id": "local/chat", "object": "model" },
{ "id": "local/coder", "object": "model" },
{ "id": "local/embed", "object": "model" }
]
}

The fallback in that file sends local/agent to local/coder when the agent route fails, and deliberately gives the judge no fallback at all. A coder model substituted for a failing agent is a worse agent; a silently substituted judge is an invalid score sheet, and the difference is worth encoding in configuration rather than remembering.

This is what makes the usage records answer “which tool cost what”, and it is what lets you revoke one agent’s access without touching the others.

Fragment — not complete on its own

Terminal window
curl -s http://127.0.0.1:4000/key/generate \
-H "Authorization: Bearer ${GATEWAY_MASTER_KEY}" \
-H 'Content-Type: application/json' \
-d '{"models": ["local/agent"], "metadata": {"application": "opencode"}}'

Repeat with the metadata changed for each agent, plus one for the sandbox itself. On Track M’s native gateway path there is no database and therefore one key; the Part 9 project says what that costs, and the runbook should record it.

RunnableAll tracks

setup-workstation.sh
#!/usr/bin/env bash
# Purpose: build an agentic coding workstation from nothing - the directory layout, the
# agent configurations pointed at the gateway's role aliases, the MCP server
# declarations, the sandbox image and the runbook - so that the whole thing can
# be reproduced on a new machine or after a mistake.
# Platform: all (spark, strix, nvidia natively; mac with Docker Desktop or without the
# sandbox step)
# Minimum memory: 16 GB for the agent role behind the gateway alias
# Assumes: the Part 9 gateway is running and reachable at GATEWAY_URL with the three role
# aliases from workstation-aliases.yaml published; the configuration templates
# from this part are in the directory this script is run from; docker and python3
# are on PATH for the sandbox step. It refuses to overwrite an existing
# workstation unless FORCE=1, because reproducibility is not the same as
# clobbering.
#
# Usage:
# ./setup-workstation.sh
# WORKSTATION=~/agentic FORCE=1 ./setup-workstation.sh
# SKIP_SANDBOX=1 ./setup-workstation.sh
set -euo pipefail
WORKSTATION="${WORKSTATION:-$HOME/agentic-workstation}"
TEMPLATES="${TEMPLATES:-$PWD}"
GATEWAY_URL="${GATEWAY_URL:-http://127.0.0.1:4000}"
AGENT_ALIAS="${AGENT_ALIAS:-local/agent}"
COMPLETION_ALIAS="${COMPLETION_ALIAS:-local/completion}"
JUDGE_ALIAS="${JUDGE_ALIAS:-local/judge}"
SKIP_SANDBOX="${SKIP_SANDBOX:-0}"
FORCE="${FORCE:-0}"
say() { printf '==> %s\n' "$1"; }
die() { printf 'error: %s\n' "$1" >&2; exit 1; }
for required in python3 curl; do
command -v "$required" >/dev/null 2>&1 || die "$required is required and is not on PATH"
done
if [ "$SKIP_SANDBOX" != "1" ] && ! command -v docker >/dev/null 2>&1; then
die "docker is not on PATH. Install it, or run with SKIP_SANDBOX=1 and read the page."
fi
if [ -e "$WORKSTATION" ] && [ "$FORCE" != "1" ]; then
die "$WORKSTATION already exists. Move it aside, or re-run with FORCE=1."
fi
# ------------------------------------------------------------------- 1. the gateway first
# Nothing else is worth configuring if the roles are not published. Ask the gateway what it
# serves and check all three names are there before writing a single file.
say "checking the gateway at $GATEWAY_URL"
models_json="$(curl --silent --show-error --max-time 10 \
--header "Authorization: Bearer ${GATEWAY_KEY:-}" \
"$GATEWAY_URL/v1/models" || true)"
if [ -z "$models_json" ]; then
die "the gateway did not answer at $GATEWAY_URL/v1/models. Start it, then re-run."
fi
missing="$(MODELS_JSON="$models_json" \
AGENT="$AGENT_ALIAS" COMPLETION="$COMPLETION_ALIAS" JUDGE="$JUDGE_ALIAS" python3 - <<'PY'
import json
import os
try:
payload = json.loads(os.environ["MODELS_JSON"])
except json.JSONDecodeError:
print("the gateway did not return JSON")
raise SystemExit(0)
published = {entry.get("id") for entry in payload.get("data", []) if isinstance(entry, dict)}
wanted = [os.environ["AGENT"], os.environ["COMPLETION"], os.environ["JUDGE"]]
absent = [name for name in wanted if name not in published]
print(" ".join(absent))
PY
)"
if [ -n "$missing" ]; then
die "the gateway does not publish: $missing. Merge workstation-aliases.yaml and restart it."
fi
say "all three role aliases are published"
# ------------------------------------------------------------------ 2. the directory layout
say "creating $WORKSTATION"
mkdir -p \
"$WORKSTATION/agents" \
"$WORKSTATION/sandbox" \
"$WORKSTATION/mcp" \
"$WORKSTATION/projects" \
"$WORKSTATION/evidence"
copy_template() {
# copy_template <source-name> <destination-path>
if [ -f "$TEMPLATES/$1" ]; then
cp "$TEMPLATES/$1" "$2"
say "installed $1"
else
printf ' skipped %s (not found in %s)\n' "$1" "$TEMPLATES"
fi
}
# ------------------------------------------------------------------------- 3. the agents
copy_template "opencode.json" "$WORKSTATION/agents/opencode.json"
copy_template "codex-config.toml" "$WORKSTATION/agents/codex-config.toml"
copy_template "aider-model-settings.yml" "$WORKSTATION/agents/aider-model-settings.yml"
copy_template "goose-local-env.txt" "$WORKSTATION/agents/goose-local-env.txt"
copy_template "goose-recipe.yaml" "$WORKSTATION/agents/goose-recipe.yaml"
copy_template "continue-agent-config.yaml" "$WORKSTATION/agents/continue-agent-config.yaml"
copy_template "zed-local-settings.json" "$WORKSTATION/agents/zed-local-settings.json"
# Rewrite the alias names in the copies so that a workstation configured for different role
# names does not need six manual edits. Only the copies are touched; the templates are not.
if [ "$AGENT_ALIAS" != "local/agent" ]; then
say "rewriting local/agent to $AGENT_ALIAS in the installed configurations"
find "$WORKSTATION/agents" -type f -print0 \
| xargs -0 sed -i.bak "s|local/agent|${AGENT_ALIAS}|g"
find "$WORKSTATION/agents" -name '*.bak' -delete
fi
# ------------------------------------------------------------------------- 4. the sandbox
copy_template "agent-sandbox.Dockerfile" "$WORKSTATION/sandbox/agent-sandbox.Dockerfile"
copy_template "compose-sandbox.yaml" "$WORKSTATION/sandbox/compose-sandbox.yaml"
copy_template "boundary-test.sh" "$WORKSTATION/sandbox/boundary-test.sh"
copy_template "audit-sandbox.sh" "$WORKSTATION/sandbox/audit-sandbox.sh"
copy_template "env-example.txt" "$WORKSTATION/sandbox/env-example.txt"
if [ -f "$WORKSTATION/sandbox/env-example.txt" ] && [ ! -f "$WORKSTATION/sandbox/.env" ]; then
cp "$WORKSTATION/sandbox/env-example.txt" "$WORKSTATION/sandbox/.env"
say "created sandbox/.env from the template; fill it in before first use"
fi
if [ "$SKIP_SANDBOX" != "1" ] && [ -f "$WORKSTATION/sandbox/agent-sandbox.Dockerfile" ]; then
say "building the sandbox image (this needs the network once)"
docker build \
--file "$WORKSTATION/sandbox/agent-sandbox.Dockerfile" \
--tag agent-sandbox:workstation \
"$WORKSTATION/sandbox"
fi
# ---------------------------------------------------------------------- 5. the MCP servers
# The servers themselves come from Part 24. This writes the declaration files the agents
# read, with an empty server list, so that adding one is a single edit in a known place
# rather than a search through six tools' configuration formats.
if [ ! -f "$WORKSTATION/mcp/mcp-servers.json" ]; then
cat >"$WORKSTATION/mcp/mcp-servers.json" <<'JSON'
{
"_readme": [
"The MCP servers this workstation wires into its agents. Add one entry per server,",
"using the shape each agent expects: Claude Code reads mcpServers from .mcp.json,",
"Codex reads an mcp_servers table in config.toml, Goose takes --with-extension or an",
"extensions block, and OpenCode has its own mcp configuration. This file is the single",
"list you keep; the runbook records which agents it has been applied to.",
"Every schema you add here is tokens in the prompt on every turn. Add what a task",
"needs and no more."
],
"mcpServers": {}
}
JSON
say "created mcp/mcp-servers.json with an empty server list"
fi
# -------------------------------------------------------------------------- 6. the runbook
if [ ! -f "$WORKSTATION/workstation-runbook.md" ]; then
copy_template "workstation-runbook.md" "$WORKSTATION/workstation-runbook.md"
fi
# ------------------------------------------------------------------------ 7. what to do next
cat <<EOF
Workstation created at: $WORKSTATION
agents/ one configuration per tool, pointed at the gateway's role aliases
sandbox/ the container, its boundary test and the audit script
mcp/ the list of MCP servers this workstation wires in
projects/ where the directories an agent may edit live
evidence/ boundary-test sheets, audit reports and agent transcripts
Next, in this order:
1. Fill in sandbox/.env: the project directory, the gateway address as seen from
inside the sandbox network, and a per-sandbox gateway key.
2. Generate one gateway virtual key per agent, so usage is attributable per tool.
3. Run check-workstation.sh and fix whatever it reports.
4. Write workstation-runbook.md: the models per role, the agents, the sandbox
decisions, and the residual risks you have decided to accept.
Nothing above has been done for you, and each step needs a decision you should make
rather than inherit.
EOF

Download setup-workstation.sh187 lines

It checks the gateway first and refuses to go on if the three roles are not published, because building a workstation around aliases that do not exist wastes an hour. Then it creates the layout, installs each agent configuration, rewrites the alias names if yours differ, copies the sandbox and its two test scripts, builds the image, creates an empty MCP declaration and installs the runbook template.

RunnableAll tracks

build the workstation
cd ~/agent-lab
GATEWAY_KEY="${GATEWAY_KEY}" bash setup-workstation.sh

Output — what you should see

==> checking the gateway at http://127.0.0.1:4000
==> all three role aliases are published
==> creating ~/agentic-workstation
==> installed opencode.json
==> installed codex-config.toml
==> installed aider-model-settings.yml
==> installed goose-local-env.txt
==> installed agent-sandbox.Dockerfile
==> installed compose-sandbox.yaml
==> created sandbox/.env from the template; fill it in before first use
==> building the sandbox image (this needs the network once)
==> created mcp/mcp-servers.json with an empty server list

It refuses to overwrite an existing workstation unless you pass FORCE=1. Reproducible is not the same as destructive, and a setup script that quietly replaces a configuration you spent an hour on is a worse tool than one that stops.

5. Fill in the sandbox and prove the boundary

Section titled “5. Fill in the sandbox and prove the boundary”

The .env the script created is a template with empty values. Fill in the project directory, the gateway address as seen from inside the sandbox network, and the sandbox’s own virtual key. Then run the boundary test with its notebook pointed at the workstation’s evidence directory, so the sheet lands where the check script looks for it.

RunnableAll tracks

prove the boundary, and keep the sheet
cd ~/agentic-workstation/sandbox
docker compose -f compose-sandbox.yaml run --rm \
-e LABBOOK=/work/../evidence/labbook.md \
agent bash /task/boundary-test.sh

Nine passes, zero failures, including the gateway control. If the control fails, the network attachment is incomplete and no agent will work in there; the second lab’s task 4 is the fix.

Take the servers you built in Part 24’s MCP lab and add them to mcp/mcp-servers.json, then apply them to each agent in that agent’s own format: Claude Code reads mcpServers from .mcp.json and adds servers with claude mcp add; Codex CLI takes an mcp_servers table in config.toml or codex mcp add; Goose takes --with-extension or an extensions block; OpenCode has its own MCP configuration. The shared list is the record; each agent gets its own translation.

Two, not one. The differences only become visible on the second.

Pick from what the lab told you: whichever two came out well on your hardware, or one terminal agent and one editor agent if you work in both. Point each at local/agent, set its permission mode deliberately, and run the lab’s task through each of them inside the sandbox.

Fragment — not complete on its own

Terminal window
cd ~/agentic-workstation/sandbox
docker compose -f compose-sandbox.yaml run --rm agent \
opencode run --model "gateway/local/agent" \
"Read /task/task-readme.md and make the tests pass without editing them."

Record the outcome for each in the runbook’s agents table with the date. An agent that has never completed a real task on this machine is not configured; it is installed.

RunnableAll tracks

check-workstation.sh
#!/usr/bin/env bash
# Purpose: verify an agentic coding workstation end to end - the three role aliases answer,
# each configured agent is installed, the MCP list is declared, the sandbox image
# exists and its boundary test passes - and record the result in the lab notebook.
# Platform: all (spark, strix, nvidia natively; mac with Docker Desktop, or with the
# sandbox checks skipped on the dedicated-user route)
# Minimum memory: 16 GB for the agent role behind the gateway alias
# Assumes: the workstation created by setup-workstation.sh, the Part 9 gateway running,
# curl and python3 on PATH, and GATEWAY_KEY exported. It only reads and makes one
# small completion request per role alias.
#
# Usage:
# ./check-workstation.sh
# WORKSTATION=~/agentic SKIP_SANDBOX=1 ./check-workstation.sh
set -uo pipefail
WORKSTATION="${WORKSTATION:-$HOME/agentic-workstation}"
GATEWAY_URL="${GATEWAY_URL:-http://127.0.0.1:4000}"
AGENT_ALIAS="${AGENT_ALIAS:-local/agent}"
COMPLETION_ALIAS="${COMPLETION_ALIAS:-local/completion}"
JUDGE_ALIAS="${JUDGE_ALIAS:-local/judge}"
SKIP_SANDBOX="${SKIP_SANDBOX:-0}"
LABBOOK="${LABBOOK:-$WORKSTATION/evidence/labbook.md}"
passes=0
failures=0
results=""
record() {
local name="$1" verdict="$2" detail="$3"
if [ "$verdict" = "pass" ]; then
passes=$(( passes + 1 ))
printf ' PASS %-30s %s\n' "$name" "$detail"
else
failures=$(( failures + 1 ))
printf ' FAIL %-30s %s\n' "$name" "$detail"
fi
results="${results}${results:+,}\"${name}\":\"${verdict}\""
}
echo "Workstation check: $WORKSTATION"
echo
# ------------------------------------------------------------------------ 1. the layout
echo "Layout"
for directory in agents sandbox mcp projects evidence; do
if [ -d "$WORKSTATION/$directory" ]; then
record "layout-$directory" "pass" "present"
else
record "layout-$directory" "fail" "missing; run setup-workstation.sh"
fi
done
echo
# ------------------------------------------------------------------- 2. the role aliases
echo "Roles"
check_alias() {
# check_alias <name> <alias>
local name="$1" alias="$2" body status
body="$(printf '{"model":"%s","messages":[{"role":"user","content":"ok"}],"max_tokens":8}' "$alias")"
status="$(curl --silent --output /dev/null --write-out '%{http_code}' --max-time 120 \
--request POST "$GATEWAY_URL/v1/chat/completions" \
--header "Authorization: Bearer ${GATEWAY_KEY:-}" \
--header 'Content-Type: application/json' \
--data "$body" || echo 000)"
case "$status" in
2*) record "$name" "pass" "answered with HTTP $status" ;;
000) record "$name" "fail" "no response; is the gateway running?" ;;
*) record "$name" "fail" "HTTP $status" ;;
esac
}
check_alias "role-agent" "$AGENT_ALIAS"
check_alias "role-completion" "$COMPLETION_ALIAS"
check_alias "role-judge" "$JUDGE_ALIAS"
if [ "$AGENT_ALIAS" = "$JUDGE_ALIAS" ]; then
record "judge-is-a-different-model" "fail" "the judge and the agent are the same alias"
else
record "judge-is-a-different-model" "pass" "agent and judge are different aliases"
fi
echo
# ------------------------------------------------------------------------- 3. the agents
echo "Agents"
configured=0
for pair in "aider:agents/aider-model-settings.yml" \
"opencode:agents/opencode.json" \
"codex:agents/codex-config.toml" \
"goose:agents/goose-local-env.txt"; do
tool="${pair%%:*}"
file="${pair#*:}"
if command -v "$tool" >/dev/null 2>&1 && [ -f "$WORKSTATION/$file" ]; then
record "agent-$tool" "pass" "installed and configured"
configured=$(( configured + 1 ))
elif [ -f "$WORKSTATION/$file" ]; then
record "agent-$tool" "fail" "configuration present but $tool is not on PATH"
else
printf ' ---- %-30s %s\n' "agent-$tool" "not configured on this workstation"
fi
done
if [ "$configured" -ge 2 ]; then
record "at-least-two-agents" "pass" "$configured agents installed and configured"
else
record "at-least-two-agents" "fail" "only $configured; the project asks for two"
fi
echo
# --------------------------------------------------------------------- 4. the MCP servers
echo "MCP"
if [ -f "$WORKSTATION/mcp/mcp-servers.json" ]; then
count="$(WORKSTATION="$WORKSTATION" python3 - <<'PY'
import json
import os
path = os.path.join(os.environ["WORKSTATION"], "mcp", "mcp-servers.json")
try:
with open(path, "r", encoding="utf-8") as handle:
data = json.load(handle)
except (OSError, json.JSONDecodeError):
print(-1)
else:
print(len(data.get("mcpServers") or {}))
PY
)"
if [ "$count" = "-1" ]; then
record "mcp-list-valid" "fail" "mcp/mcp-servers.json is not valid JSON"
elif [ "$count" = "0" ]; then
record "mcp-list-valid" "fail" "no servers declared; wire in at least one from Part 24"
else
record "mcp-list-valid" "pass" "$count server(s) declared"
fi
else
record "mcp-list-valid" "fail" "mcp/mcp-servers.json is missing"
fi
echo
# ------------------------------------------------------------------------- 5. the sandbox
echo "Sandbox"
if [ "$SKIP_SANDBOX" = "1" ]; then
printf ' ---- %-30s %s\n' "sandbox" "skipped by request; record why in the runbook"
else
if [ -f "$WORKSTATION/sandbox/compose-sandbox.yaml" ]; then
record "sandbox-compose-present" "pass" "compose-sandbox.yaml present"
else
record "sandbox-compose-present" "fail" "compose-sandbox.yaml missing"
fi
if [ -f "$WORKSTATION/sandbox/.env" ]; then
if grep -q -E '^(PROJECT_DIR|GATEWAY_URL|AGENT_GATEWAY_KEY)=$' "$WORKSTATION/sandbox/.env"; then
record "sandbox-env-complete" "fail" "sandbox/.env still has empty required values"
else
record "sandbox-env-complete" "pass" "sandbox/.env has no empty required values"
fi
else
record "sandbox-env-complete" "fail" "sandbox/.env is missing"
fi
if command -v docker >/dev/null 2>&1 \
&& docker image inspect agent-sandbox:workstation >/dev/null 2>&1; then
record "sandbox-image-built" "pass" "agent-sandbox:workstation exists"
else
record "sandbox-image-built" "fail" "image not built; run setup-workstation.sh"
fi
latest_sheet="$(grep -l 'part-25-sandbox-your-agent' "$WORKSTATION/evidence"/*.md 2>/dev/null | head -1 || true)"
if [ -n "$latest_sheet" ]; then
record "boundary-test-recorded" "pass" "evidence found in $(basename "$latest_sheet")"
else
record "boundary-test-recorded" "fail" "no boundary-test record in evidence/"
fi
fi
echo
# -------------------------------------------------------------------------- 6. the runbook
echo "Runbook"
if [ -f "$WORKSTATION/workstation-runbook.md" ]; then
if grep -q -E '^\|[[:space:]]*(Agent|Completion|Judge)[[:space:]]*\|[^|]*\|[[:space:]]*\|' \
"$WORKSTATION/workstation-runbook.md"; then
record "runbook-filled-in" "fail" "the runbook still has empty rows in the roles table"
else
record "runbook-filled-in" "pass" "the roles table has been filled in"
fi
else
record "runbook-filled-in" "fail" "workstation-runbook.md is missing"
fi
echo
echo "$passes passed, $failures failed."
mkdir -p "$(dirname "$LABBOOK")" 2>/dev/null || true
RESULTS="$results" PASSES="$passes" FAILURES="$failures" LABBOOK="$LABBOOK" \
WORKSTATION="$WORKSTATION" python3 - <<'PY'
import datetime
import json
import os
record = {
"lab": "part-25-agentic-workstation",
"recorded": datetime.datetime.now(datetime.timezone.utc)
.replace(microsecond=0)
.isoformat(),
"workstation": os.environ["WORKSTATION"],
"checks": json.loads("{" + os.environ["RESULTS"] + "}"),
"passed": int(os.environ["PASSES"]),
"failed": int(os.environ["FAILURES"]),
"complete": os.environ["FAILURES"] == "0",
}
try:
with open(os.environ["LABBOOK"], "a", encoding="utf-8") as handle:
handle.write(json.dumps(record) + "\n")
print("recorded in " + os.environ["LABBOOK"])
except OSError as error:
print("could not write the lab notebook: " + str(error))
PY
if [ "$failures" -ne 0 ]; then
exit 1
fi

Download check-workstation.sh220 lines

RunnableAll tracks

the workstation check
cd ~/agentic-workstation
GATEWAY_KEY="${GATEWAY_KEY}" bash ~/agent-lab/check-workstation.sh

Output — what you should see

Workstation check: ~/agentic-workstation
Layout
PASS layout-agents present
PASS layout-sandbox present
PASS layout-mcp present
PASS layout-projects present
PASS layout-evidence present
Roles
PASS role-agent answered with HTTP 200
PASS role-completion answered with HTTP 200
PASS role-judge answered with HTTP 200
PASS judge-is-a-different-model agent and judge are different aliases
Agents
PASS agent-aider installed and configured
PASS agent-opencode installed and configured
PASS at-least-two-agents 2 agents installed and configured
MCP
PASS mcp-list-valid 2 server(s) declared
Sandbox
PASS sandbox-compose-present compose-sandbox.yaml present
PASS sandbox-env-complete sandbox/.env has no empty required values
PASS sandbox-image-built agent-sandbox:workstation exists
PASS boundary-test-recorded evidence found in labbook.md
Runbook
PASS runbook-filled-in the roles table has been filled in
18 passed, 0 failed.

It makes one small request per role alias, so a passing Roles section means the models really load rather than that the configuration parses.

RunnableAll tracks

workstation-runbook.md
# Agentic coding workstation — runbook
Purpose: the template you fill in for your own workstation, so that a machine rebuilt in
six months comes back the same and so that somebody else, including you, can tell what was
decided and why.
Platform: all (spark, strix, mac, nvidia).
Minimum memory: 16 GB for the agent role.
Assumes: created by `setup-workstation.sh` and verified by `check-workstation.sh`. Replace
every angle-bracketed placeholder and every empty table cell. A cell left empty is a
decision nobody made.
---
## 1. The machine
| Field | Value |
| --- | --- |
| Track | S / X / M / N |
| Chip and memory | |
| Operating system and version | |
| Accelerator memory available to engines | |
| Date this runbook was last verified | |
## 2. Models per role
Three roles, three aliases, three deliberate choices. The agent role drives the loop, the
completion role fires as you type, and the judge role scores agent output in the evaluation
suite. The judge must not be the same weights as the agent.
| Role | Alias | Model and quantisation | Context configured | Why this one |
| --- | --- | --- | --- | --- |
| Agent | `local/agent` | | | |
| Completion | `local/completion` | | | |
| Judge | `local/judge` | | | |
Tool-call reliability, measured with Part 24's reliability test against the agent alias at
the context length above:
| Model | Date measured | Pass rate | Notes |
| --- | --- | --- | --- |
| | | | |
## 3. The gateway
| Field | Value |
| --- | --- |
| Address (loopback only, or the Part 23 fronted address) | |
| Engine or engines behind the aliases | |
| Prompt-cache reuse enabled | yes / no |
| Where the role aliases are defined | |
| Usage records available per key | yes / no |
Virtual keys, one per agent so that usage is attributable:
| Agent | Key name or metadata label | Aliases it may use | Created | Revoked |
| --- | --- | --- | --- | --- |
| | | | | |
## 4. Agents configured
At least two, each tested against the agent alias on a real task.
| Agent | Version | Configuration file | Permission mode set | Last tested | Result |
| --- | --- | --- | --- | --- | --- |
| | | | | | |
| | | | | | |
Notes on anything that did not work as documented, with the date:
-
## 5. MCP servers
Every server here is tool schemas in the prompt on every turn, and a capability the agent
can invoke. Keep the list short and justify each entry.
| Server | What it does | Which agents it is wired into | Why it earns its tokens |
| --- | --- | --- | --- |
| | | | |
## 6. The sandbox
| Field | Value |
| --- | --- |
| Route | container / dedicated user |
| Engine and version | |
| Image tag | |
| Mounts, exactly | |
| Network | internal / none / other |
| Environment variables passed in | |
| Runs as | |
| Read-only root filesystem | yes / no |
| Resource limits | |
Boundary test, most recent run:
| Check | Result | Date |
| --- | --- | --- |
| host-home-not-mounted | | |
| container-socket-absent | | |
| runs-as-non-root | | |
| root-filesystem-read-only | | |
| project-is-writable | | |
| planted-secret-unreadable | | |
| environment-is-clean | | |
| internet-unreachable | | |
| gateway-reachable | | |
## 7. Residual risk
What is still true that you have decided to accept, and why. Every real workstation has
entries here. A blank section means the risks were not examined rather than that there are
none.
| Accepted risk | Why it is acceptable here | What would change the decision |
| --- | --- | --- |
| | | |
## 8. Rebuilding this from nothing
1. Start the Part 9 gateway and confirm the three role aliases are published.
2. Run `setup-workstation.sh` from the directory holding the configuration templates.
3. Fill in `sandbox/.env` and generate one gateway key per agent.
4. Run `check-workstation.sh` and fix everything it reports.
5. Run `boundary-test.sh` inside the sandbox and store the sheet in `evidence/`.
6. Run one real task through two agents and record the result.
7. Update this runbook with anything that changed.
Expected wall-clock for a rebuild on a machine that already has the models: _____ minutes.
## 9. Change log
| Date | What changed | Who | Re-verified |
| --- | --- | --- | --- |
| | | | |

Download workstation-runbook.md135 lines

Fill in every table. The check script fails until the roles table has real values in it, which is a deliberate nudge: a workstation whose model choices are not written down will be a mystery to you in three months.

Two sections deserve more time than the rest.

Section 2’s reliability table is the evidence for your model choice. Without it, “I chose this model for the agent role” is a preference. With it, it is a measurement, and it is the baseline Part 27 improves against.

Section 7, residual risk, is the section people leave blank and the one that makes this professional work. Every real workstation has something accepted: an agent that needs the container socket, a Track M setup with no network boundary, a virtual key that is shared because the native gateway path has no database. Name each one, say why it is acceptable here, and say what would change the decision.

The last step, and the one that proves the script rather than the workstation.

RunnableAll tracks

prove it reproduces
mv ~/agentic-workstation ~/agentic-workstation-first
cd ~/agent-lab
GATEWAY_KEY="${GATEWAY_KEY}" bash setup-workstation.sh

Then fill in .env, re-run the check, and compare the two trees with diff -r. Anything that differs and matters is something the script does not yet do, and either the script or the runbook’s rebuild section should be updated until a rebuild is boring.

Rebuild the workstation from its written contract

Section titled “Rebuild the workstation from its written contract”

Before wiring clients, verify the completion, chat and agent aliases individually through the gateway. Record each model identity, context allocation and feature probe. Keep per-client keys and limits distinct so one runaway session cannot silently consume the whole service budget.

Run the sandbox boundary checks before connecting repository tools or MCP servers. Test each MCP server directly, then through an agent. Use two clients with different interaction styles on the same bounded task and inspect both final diffs with independent tests. Preserve provider-specific configuration and any manual intervention.

For the rebuild exercise, create a fresh disposable workspace and follow only the setup script and runbook. Verify that files, model aliases, permissions and endpoint credentials are obtained by the documented procedure. A client that works because it inherited a global configuration is an undocumented dependency to fix. Keep redacted configuration, installation identities, boundary-test outputs and task results as the handoff package. The workstation is complete when another operator can start it, perform a verified task, rotate a key, stop it and recover the previous working setup using the written instructions.

Pending validationThe workstation, as built — your recording sheet
RoleAliasModel and quantisationContext configuredTool-call pass rateDate measured
Agentlocal/agent
Completionlocal/completion
Judgelocal/judge

your machine: track, chip and memory, your operating system and version · the engine or engines behind the aliases, with versions gateway and engine versions on the day you measured · as listed per row, as listed per row · 0 tokens of context · the date you measured

Empty on purpose. The tool-call pass rate comes from Part 24's reliability test run against each alias at the context length in the neighbouring column, not from a leaderboard. Put the context length you configured in place of the zero.

You are done when all of the following are true:

  • check-workstation.sh reports zero failures, or reports only the sandbox checks you deliberately skipped with a written reason;
  • the three role aliases each answer a real request, and the judge is different weights from the agent;
  • two agents have each completed the lab task inside the sandbox, with the date recorded;
  • at least one MCP server is wired in, and the runbook says why it earns its tokens;
  • the boundary test’s nine checks pass, including the gateway control, and the sheet is in evidence/;
  • the runbook is complete, including the reliability table and the residual-risk section;
  • the workstation has been rebuilt from the script once, and diff -r between the two trees shows nothing that matters.

A machine you keep. Concretely: you can open a terminal in any project, run one agent inside a container that can only see that project, and know that the model is yours, the usage is logged, the boundary is tested and the whole arrangement is rebuildable from one script.

The setup script says the gateway does not publish a role. The alias is missing from litellm-config.yaml, or the gateway was not restarted, or llama-swap.yaml has no model with the matching id. Ask the gateway what it publishes and compare exactly, including the prefix.

A role answers but the first request takes minutes. The model is being loaded on demand, which is llama-swap doing its job. Either accept the first-request cost, or put the roles you use constantly in a persistent group. Record which you chose.

The check script says the judge and agent are the same alias. They are, and it is a real problem rather than a formality. A judge sharing weights with the thing it scores is measuring its own preferences, and every number your evaluation suite produces afterwards inherits that.

Two agents disagree about the same model’s context length. Each tool has its own place to state it and none of them can see the engine’s real setting. Make the engine authoritative, then set each tool to the same number, and write the number in the runbook so the next tool you add matches.

The sandbox cannot reach the gateway after the rebuild. The network name changed with the Compose project name. Either name the network explicitly with name:, or use external: true against the gateway stack’s network, which the Compose reference describes as a network whose “lifecycle is maintained outside of that of the application”.

An agent needs the container socket and you want it in the workstation. That is a decision, not a bug, and Docker’s security documentation is clear about what it means: a container that can reach the socket can start one that alters the host filesystem without restriction. Put it in section 7 of the runbook with what would change the decision, and consider giving that agent its own machine or virtual machine rather than the shared sandbox.

Everything passes but agent turns are slower than the second lesson predicted. Check prompt-cache reuse on the engine, and check how many MCP tool schemas are in the prompt. Those two account for most unexplained slowness in an otherwise correct workstation.

Nothing here is meant to be cleaned up; the point is that it persists. Two things to tidy:

RunnableAll tracks

remove the rebuild comparison copy
rm -rf ~/agentic-workstation-first

Revoke any virtual keys you created for experiments and did not keep. Leave the gateway, the workstation and the sandbox image in place: Part 26, Part 27 and the capstone all build on them.

  • Roles, not models. Three aliases with three jobs is what lets you change weights without touching six configurations, and it is what makes a judge possible at all.
  • A judge must be different weights. Otherwise every evaluation number you produce is a model agreeing with itself, and the whole measurement discipline in this course rests on that not happening.
  • One key per agent makes usage attributable. It also makes revocation cheap, which is what turns “should I try this new tool” from a risk into an experiment.
  • The MCP list is a token budget. Every server is schemas in every prompt, and the discipline is to justify each one in writing rather than to collect them.
  • A setup script that refuses to overwrite is a better tool. Reproducibility means you can build it again, not that building it again destroys what you had.
  • The runbook is the deliverable. The configuration files regenerate; the reasoning does not. The residual-risk section in particular is the difference between a workstation you engineered and one you assembled.

Record in the notebook: the completed recording sheet, the reliability figures behind your model choices, the two agents with their versions and permission modes, the MCP servers with their justification, the boundary-test sheet, the rebuild time, and the residual risks you accepted.

Check your understanding

Question 1. Why must the judge role be different weights from the agent role?
Show the answer and why

Answer: Because a model scoring its own output is measuring its own preferences, which invalidates every before-and-after number the evaluation suite produces

It is the same reason the lab does not let the agent report whether its own tests passed. The check script fails the workstation when the two aliases match, because this is the mistake that quietly ruins a measurement rather than breaking it visibly.

Question 2. Why does the workstation configuration give local/agent a fallback but give local/judge none?
Show the answer and why

Answer: Because a substituted agent is a worse agent and better than an error, while a silently substituted judge produces a score sheet that is invalid without saying so

A fallback is a good default when degraded service beats no service. It is a bad default when the value of the answer depends on knowing exactly what produced it, which is precisely the case for an evaluation score.

Question 3. What does adding a fifth MCP server cost on a workstation with a local model?
Show the answer and why

Answer: Its tool schemas occupy prompt tokens on every turn for every agent that has it wired in, which is a fixed cost charged before the conversation starts

This is why the runbook has a column asking why each server earns its tokens. On a model with a modest window, several verbose servers can consume a large share of the context before the agent has read a single file.

Question 4. Which of these belong in the runbook's residual-risk section? Select all that apply.
Show the answer and why

Answer: An agent that requires the container socket, kept because you need that agent, A Track M dedicated-user setup with no network boundary, A single shared gateway key because the native path has no database

The first three are things that are true, that you decided to accept, and that somebody reviewing the machine needs to know. A version number is a fact about the build, which belongs in section 3, not a risk you accepted.

Question 5. Why does the setup script refuse to overwrite an existing workstation unless forced?
Show the answer and why

Answer: Because reproducibility means being able to build it again, not destroying an hour of configuration decisions that the script cannot regenerate

The script regenerates files. It cannot regenerate the choices you made in .env, in the runbook or in the MCP list. Stopping is the correct behaviour, and FORCE is there for when you genuinely mean it.

Sources for this lesson

9 verified · checked 2026-09-09

  1. 01LiteLLM — Proxy config.yaml§ model_list; OpenAI-compatible endpointsdocs.litellm.ai/docs/proxy/configs2026-09-09
  2. 02LiteLLM — Virtual keys§ Key generation; per-key usagedocs.litellm.ai/docs/proxy/virtual_keys2026-09-09
  3. 03LiteLLM — Reliability and fallbacks§ fallbacksdocs.litellm.ai/docs/proxy/reliability2026-09-09
  4. 04OpenCode — configuration§ model; small_model; instructionsopencode.ai/docs/config2026-09-09
  5. 05Codex — configuration reference§ mcp_servers; sandbox_mode; approval_policylearn.chatgpt.com/docs/config-file/config-reference2026-09-09
  6. 06Claude Code — MCP§ .mcp.json; scopescode.claude.com/docs/en/mcp2026-09-09
  7. 07Goose — using extensions§ Adding an MCP servergoose-docs.ai/docs/getting-started/using-extensions2026-09-09
  8. 08Compose file reference — networks§ internal; externaldocs.docker.com/reference/compose-file/networks2026-09-09
  9. 09Docker — security§ Docker daemon attack surfacedocs.docker.com/engine/security2026-09-09

Every technical claim on this page was checked against the official documentation of the tool, vendor or model publisher on the date shown, at the version pinned for the course. Where the course disagrees with folklore, the source is how you can tell which one to trust.