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.
Objective
Section titled “Objective”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.
How the pieces fit
Section titled “How the pieces fit”The workstation, from the agent you type at down to the weights
- AgentsTwo or more, each with its own configuration file and its own permission mode. Terminal, editor, or bothyou use these
- The sandboxOne project directory, one network route, no credentials. Everything above this line runs inside itthe boundary
- MCP serversFrom Part 24. Tools the agent can call, each one schemas in the prompt on every turnkeep the list short
- Role aliaseslocal/agent, local/completion, local/judge. One virtual key per agent, so usage is attributable
- The Part 9 gatewayLiteLLM in front of llama-swap: retries, fallbacks, health checks, usage records, both API shapesunchanged from Part 9
- Engines and weightsllama-server, vLLM or an MLX server, loading the models the aliases point atnever leaves the machine
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.
Requirements
Section titled “Requirements”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
export LABS_ROOT="${LABS_ROOT:-$HOME/llm-course/labs}"export LAB_DIR="$LABS_ROOT/part-25-coding-agents"cd "$LAB_DIR"pwdtest -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.
2. Publish the three aliases
Section titled “2. Publish the three aliases”RunnableAll tracks
# 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"]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
curl --silent http://127.0.0.1:4000/v1/models \ --header "Authorization: Bearer ${GATEWAY_KEY}" \ | python3 -m json.toolOutput — 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.
3. Generate one virtual key per agent
Section titled “3. Generate one virtual key per agent”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
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.
4. Run the setup script
Section titled “4. Run the setup script”RunnableAll tracks
#!/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 jsonimport 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."fisay "all three role aliases are published"
# ------------------------------------------------------------------ 2. the directory layoutsay "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 agentscopy_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' -deletefi
# ------------------------------------------------------------------------- 4. the sandboxcopy_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 runbookif [ ! -f "$WORKSTATION/workstation-runbook.md" ]; then copy_template "workstation-runbook.md" "$WORKSTATION/workstation-runbook.md"fi
# ------------------------------------------------------------------------ 7. what to do nextcat <<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 makerather than inherit.EOFIt 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
cd ~/agent-labGATEWAY_KEY="${GATEWAY_KEY}" bash setup-workstation.shOutput — 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 listIt 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
cd ~/agentic-workstation/sandboxdocker compose -f compose-sandbox.yaml run --rm \ -e LABBOOK=/work/../evidence/labbook.md \ agent bash /task/boundary-test.shNine 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.
6. Wire in the MCP servers
Section titled “6. Wire in the MCP servers”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.
7. Configure and test two agents
Section titled “7. Configure and test two agents”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
cd ~/agentic-workstation/sandboxdocker 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.
8. Check the whole thing
Section titled “8. Check the whole thing”RunnableAll tracks
#!/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=0failures=0results=""
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 layoutecho "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" fidoneecho
# ------------------------------------------------------------------- 2. the role aliasesecho "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"fiecho
# ------------------------------------------------------------------------- 3. the agentsecho "Agents"configured=0for 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" fidone
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"fiecho
# --------------------------------------------------------------------- 4. the MCP serversecho "MCP"if [ -f "$WORKSTATION/mcp/mcp-servers.json" ]; then count="$(WORKSTATION="$WORKSTATION" python3 - <<'PY'import jsonimport 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" fielse record "mcp-list-valid" "fail" "mcp/mcp-servers.json is missing"fiecho
# ------------------------------------------------------------------------- 5. the sandboxecho "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/" fifiecho
# -------------------------------------------------------------------------- 6. the runbookecho "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" fielse record "runbook-filled-in" "fail" "workstation-runbook.md is missing"fi
echoecho "$passes passed, $failures failed."
mkdir -p "$(dirname "$LABBOOK")" 2>/dev/null || trueRESULTS="$results" PASSES="$passes" FAILURES="$failures" LABBOOK="$LABBOOK" \WORKSTATION="$WORKSTATION" python3 - <<'PY'import datetimeimport jsonimport 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 1fiRunnableAll tracks
cd ~/agentic-workstationGATEWAY_KEY="${GATEWAY_KEY}" bash ~/agent-lab/check-workstation.shOutput — 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.
9. Write the runbook
Section titled “9. Write the runbook”RunnableAll tracks
# Agentic coding workstation — runbook
Purpose: the template you fill in for your own workstation, so that a machine rebuilt insix months comes back the same and so that somebody else, including you, can tell what wasdecided 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`. Replaceevery angle-bracketed placeholder and every empty table cell. A cell left empty is adecision 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, thecompletion role fires as you type, and the judge role scores agent output in the evaluationsuite. 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 atthe 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 agentcan 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 hasentries here. A blank section means the risks were not examined rather than that there arenone.
| 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 || --- | --- | --- | --- || | | | |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.
10. Rebuild it once
Section titled “10. Rebuild it once”The last step, and the one that proves the script rather than the workstation.
RunnableAll tracks
mv ~/agentic-workstation ~/agentic-workstation-firstcd ~/agent-labGATEWAY_KEY="${GATEWAY_KEY}" bash setup-workstation.shThen 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.
Validation
Section titled “Validation”| Role | Alias | Model and quantisation | Context configured | Tool-call pass rate | Date measured |
|---|---|---|---|---|---|
| Agent | local/agent | — | — | — | — |
| Completion | local/completion | — | — | — | — |
| Judge | local/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.shreports 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 -rbetween the two trees shows nothing that matters.
Expected outcome
Section titled “Expected outcome”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.
Troubleshooting
Section titled “Troubleshooting”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.
Cleanup
Section titled “Cleanup”Nothing here is meant to be cleaned up; the point is that it persists. Two things to tidy:
RunnableAll tracks
rm -rf ~/agentic-workstation-firstRevoke 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.
What you learned
Section titled “What you learned”- 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
Sources for this lesson
9 verified · checked 2026-09-09
- 01LiteLLM — Proxy config.yaml§ model_list; OpenAI-compatible endpointsdocs.litellm.ai/docs/proxy/configs2026-09-09
- 02LiteLLM — Virtual keys§ Key generation; per-key usagedocs.litellm.ai/docs/proxy/virtual_keys2026-09-09
- 03LiteLLM — Reliability and fallbacks§ fallbacksdocs.litellm.ai/docs/proxy/reliability2026-09-09
- 04OpenCode — configuration§ model; small_model; instructionsopencode.ai/docs/config2026-09-09
- 05Codex — configuration reference§ mcp_servers; sandbox_mode; approval_policylearn.chatgpt.com/docs/config-file/config-reference2026-09-09
- 06Claude Code — MCP§ .mcp.json; scopescode.claude.com/docs/en/mcp2026-09-09
- 07Goose — using extensions§ Adding an MCP servergoose-docs.ai/docs/getting-started/using-extensions2026-09-09
- 08Compose file reference — networks§ internal; externaldocs.docker.com/reference/compose-file/networks2026-09-09
- 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.