Samples
Four agent specifications you can apply against a real model.
Rendered from samples/README.md in the repository.
Each subdirectory is a project: a folder with a .agent-container/
directory holding the YAML the CLI reads. That folder is the sample. It is
plain text, git-trackable, and it is what agent-container apply reconciles —
nothing here is generated at run time.
samples/01-workspace-write/
└── .agent-container/
├── environments.yaml the spec: name, host, container, task,
│ credentials, egress
└── sample01.config/pi/ canonical config, delivered to the agent's
├── models.json home by the <name>.config/<agent>/ convention
└── settings.json (used only when agent: pi)
| Sample | What it declares | Needs a repo? |
|---|---|---|
01-workspace-write |
A headless agent, a task, one credential | no |
02-egress-boundary |
The same, plus an egress: allow-list under enforcement: strict |
yes |
03-clone-commit-push |
repo: clone-on-start, a three-commit pipeline task, a forge token |
yes |
04-avl-tree |
The hardest task — real software, tests and a TUI | yes |
Start with 01: it needs only a model key.
Check your CLI is current first#
These samples use the declarative verbs (plan, apply, destroy). An older
install will answer no such command — which looks like a broken sample and
is not:
agent-container --version # an install too old to have this also lacks `plan`
uv tool upgrade agent-container # if it is behind
Or run the repo copy directly: ./bin/agent-container plan.
Once per machine#
./setup-once.sh
The single imperative step, and it exists for a reason worth reading: delivering
a credential needs an operator-declared SSH identity (Constitution IX), and
the tool deliberately refuses to generate one — a tool-minted key would be a
standing credential granting entry to every environment it ever deploys. The
script creates one under ~/.config/agent-container/, declares it in
settings.yaml and admits its public half in your key collection. Nothing it
writes goes into this repository.
Then, per sample#
export ANTHROPIC_API_KEY=sk-ant-... # or OLLAMA_API_KEY, for agent: pi
export SAMPLE_GH_TOKEN=ghp_... # samples 02-04 only
cd 01-workspace-write
agent-container plan # validates and shows absent / matching / drifted
agent-container apply # converge
plan mutates nothing, so it is always safe to run first. apply is
idempotent: a matching spec makes no changes; a drifted one is announced,
then recreated.
Samples 02–04 need one edit before they will work. repo: points at a
placeholder — change it to a repository you can write to. A throwaway is right;
each run pushes a branch. Prefer a private one: a public repo clones happily
with a junk token, which would tell you nothing about whether the credential
path works.
To run pi instead of Claude Code, change agent: and swap the credential
entry. Both alternatives are written out in comments beside the line you change.
No secret is in these files, by construction#
credentials: entries are locators, never values:
credentials:
- { name: ANTHROPIC_API_KEY, source: env, var: ANTHROPIC_API_KEY }
The variable is read at apply time; the value travels to the container over that container's own sshd and lands on a per-credential volume. It is never written into this file, into the compose model the tool generates, or onto a command line. That is why these samples can be committed and why they ask you to export a variable instead of filling in a blank.
source: also accepts file, keychain, command, onepassword and
bitwarden — see docs/agent-as-code.md.
Cleaning up#
agent-container destroy # removes only what THIS spec declares and owns
When a sample fails#
Not automatically a bug — a model can simply be bad at the task, and 04 is hard for a small one. The distinction:
applyfailed, or a credential was not delivered → anagent-containerproblem. Re-run with-vfor the exact commands, and readlogs <name>.- The run succeeded but the output is wrong or incomplete → usually model capability. One model did the first half of 03 faithfully, committed, and silently skipped the branch-and-push half; the honest fix was a better model, not a smaller task.
The specifications in full#
01-workspace-write — A headless agent, a task, one credential#
Browse this sample on GitHub · apply it with cd samples/01-workspace-write && agent-container plan
# Sample 01 — a real agent runs headless and writes to the workspace.
#
# THIS FILE IS THE SAMPLE. `agent-container apply` reads it, validates it, and
# reconciles the environment it declares. Nothing is generated at run time.
#
# To run pi instead of Claude Code, change `agent:` below and swap the
# credential — both alternatives are written out in comments.
environments:
- name: sample01
host: local
container:
mode: headless # the agent IS the workload; the container exits with it
agent: claude # claude | codex | pi | opencode
workspace: persistent # a named volume that survives recreation
# The task is part of the SPEC, not a flag. It is recorded verbatim in the
# run record, so never put a credential here — nothing redacts it.
task: |
Create a file at /workspace/proof.txt whose contents are exactly:
SAMPLE-01-OK
# References, NEVER values. `source: env` reads the variable at apply time;
# the value travels to the container over its own sshd and lands on a
# per-credential volume. It is never written into this file, the compose
# model, or argv.
credentials:
- { name: ANTHROPIC_API_KEY, source: env, var: ANTHROPIC_API_KEY }
# for agent: pi ->
# - { name: OLLAMA_API_KEY, source: env, var: OLLAMA_API_KEY }
02-egress-boundary — The same, from behind a declared egress boundary#
Browse this sample on GitHub · apply it with cd samples/02-egress-boundary && agent-container plan
# Sample 02 — the same kind of work, from BEHIND a declared egress boundary.
#
# EDIT `repo:` to a repository you can write to before applying.
environments:
- name: sample02
host: local
container:
mode: headless
agent: claude
workspace: persistent
repo: https://github.com/YOU/YOUR-SCRATCH-REPO # <-- EDIT ME
task: |
In the git repository at /workspace, create a file
report.md containing one sentence about what this repository holds.
Commit it with the message 'sample02 report', create a branch named
sample02-report and push it to origin.
credentials:
- { name: ANTHROPIC_API_KEY, source: env, var: ANTHROPIC_API_KEY }
# The forge token. Delivered as an env var because GH_TOKEN is not a
# provider API key; the git credential helper reads it for the HTTPS push.
- { name: GH_TOKEN, source: env, var: SAMPLE_GH_TOKEN }
# The WHOLE egress policy. Everything not named here is refused at the packet
# level, in a namespace the agent shares with a sidecar that alone holds
# NET_ADMIN. Declared WITHOUT a port, so these go to the PROXY allowlist and
# the agent's TLS is spliced — never terminated, never inspected.
#
# github.com is here because this environment pushes. A run that only talked
# to its model would reach one destination and prove much less.
egress:
allow:
- { provider: anthropic } # the tool supplies the hosts for a named provider
- { host: github.com }
# for agent: pi -> - { host: ollama.com }
enforcement: strict # advisory (default) | strict
03-clone-commit-push — Clone on start, three commits, a forge token#
Browse this sample on GitHub · apply it with cd samples/03-clone-commit-push && agent-container plan
# Sample 03 — clone on start, generate data, transform it, report, PUSH.
#
# EDIT `repo:` to a repository you can write to before applying.
environments:
- name: sample03
host: local
container:
mode: headless
agent: claude
workspace: persistent
repo: https://github.com/YOU/YOUR-SCRATCH-REPO # <-- EDIT ME
# Three commits, not one. A single "write this string" commit proves the
# plumbing and nothing about the agent — it cannot tell an agent that
# worked from one that echoed its instructions. Step 2 must READ what step
# 1 produced and compute over it, so the answer can be recomputed from the
# pushed data and a fabricated one caught.
task: |
In the git repository at /workspace, do the following as THREE SEPARATE
git commits, then push. Work only inside the directory data/sample03.
1. Create data/sample03/input.csv with the header line `id,item,amount`
followed by exactly 8 data rows. Use ids 1 to 8, any item names you
like without commas, and integer amounts between 10 and 99. Stage it
and commit with the message 'step1 generate'.
2. READ data/sample03/input.csv and compute the number of data rows and
the sum of the amount column. Create data/sample03/summary.md
containing exactly two lines:
ROWS=<the number of data rows>
TOTAL=<the sum of the amount column>
Stage it and commit with the message 'step2 transform'.
3. Create data/sample03/report.md with one sentence describing what you
computed. Stage it and commit with the message 'step3 report'.
Finally create a branch named sample03-pipeline and push it to origin.
credentials:
- { name: ANTHROPIC_API_KEY, source: env, var: ANTHROPIC_API_KEY }
- { name: GH_TOKEN, source: env, var: SAMPLE_GH_TOKEN }
04-avl-tree — Real software, tests and a TUI — the hard one#
Browse this sample on GitHub · apply it with cd samples/04-avl-tree && agent-container plan
# Sample 04 — the agent writes real software: an AVL tree, unit tests, a TUI.
#
# The hardest sample. A small model will often fail it, and that is information
# rather than a defect.
#
# EDIT `repo:` to a repository you can write to before applying.
environments:
- name: sample04
host: local
container:
mode: headless
agent: claude
workspace: persistent
repo: https://github.com/YOU/YOUR-SCRATCH-REPO # <-- EDIT ME
task: |
In the git repository at /workspace, write a small Python project in the
directory avl/sample04. Use ONLY the Python standard library — do NOT
install or import any third-party package, and do NOT use an existing
tree implementation; write the AVL tree yourself.
1. avl/sample04/avl.py — a self-balancing AVL tree. It must define class
AVLTree with methods: insert(value: int) -> None,
delete(value: int) -> None, contains(value: int) -> bool,
in_order() -> list[int] returning every value in ascending order, and
height() -> int (an empty tree has height 0). Rebalance with rotations
on both insert and delete. Commit with the message 'avl implement'.
2. avl/sample04/test_avl.py — unit tests using the standard library
`unittest` module (NOT pytest). Cover insert, delete, contains,
in-order order, and that the tree stays balanced when values are
inserted in ascending order. Run them with `python -m unittest` and
make sure they PASS before you commit. Commit with 'avl tests'.
3. avl/sample04/tui.py — a text UI reading commands from standard input,
one per line: `i <number>` inserts, `d <number>` deletes, `p` prints,
`q` quits. After each insert or delete print the tree's contents in
ascending order on one line. Exit cleanly on `q` or end of input.
Commit with 'avl tui'.
Finally create a branch named sample04-avl and push it to origin.
credentials:
- { name: ANTHROPIC_API_KEY, source: env, var: ANTHROPIC_API_KEY }
- { name: GH_TOKEN, source: env, var: SAMPLE_GH_TOKEN }