Reference
Lookup tables: every command, every environment variable, the on-disk layout, the manifest schema, the audit record format, and a glossary. For prose explanations see the USER-GUIDE and OPERATIONS.
Contents
- Command index
- Permission rule grammar
- Built-in tools
- Environment variables
- Filesystem layout
- Manifest schema
- Audit records
- Glossary
Command index
milieu (imperative CLI)
milieu setup [--dir <dir>] [--defaults] [--dry-run] [--yes] Guided first run
milieu agent create <code> [--role <role>] Create a new agent
milieu agent ls List agents
milieu permission grant <agent> <rule> Allow a CLI pattern
milieu permission revoke <agent> <rule> Remove a permission
milieu permission [--as <code>] ls List the caller's permissions
milieu run [--as <code>] -- <cmd> [args...] Run a CLI, sandboxed
milieu run [--as <code>] -- <cmd> then <cmd> ... Pipe sandboxed stages
milieu message [--as <code>] send <to> <subj> [--attach <f>] Send a message (body on stdin)
milieu message [--as <code>] inbox List messages
milieu message [--as <code>] read <id> Print + mark consumed
milieu route Deliver outbound -> inbound (one-shot)
milieu wake [--as <code>] [--skill <name>] [--auto] Drain inbox, run skill, reply
milieu ask [--as <c>] --to <c> [--subject <s>] [--timeout <d>] Send and block until reply
milieu skill [--as <code>] ls List the caller's skills
milieu act [--as <c>] [--in <p>] [--attach <f>]... <skill> Execute a skill (input on stdin)
milieu act [--as <c>] [--in <p>] --auto Auto-select and execute a skill
milieu project create <code> [--name <n>] [--instructions <f>] Create a project
milieu project ls | show <code> List or inspect projects
milieu project member add|rm <code> <agent> Manage the member roster
milieu session --as <c> open [--in <p>] [--topic <t>] [--id <id>] Open a session
milieu session [--as <c>] ls | show <id> | close <id> | rm <id> Read/close/remove sessions
milieu act --as <c> --session <id> <skill> Continue a session (stdin -> reply)
milieu cleanup session --retention <d> Sweep idle sessions (never swept by default)
milieu cleanup builds --keep-builds <n> Trim recorded skill builds (never by default)
milieu compact [--as <c>] [--keep <n>] [--over <b>] [--dry-run] [<id>...] Summarise old turns
milieu schedule add <name> --agent <c> --cron <expr> --input <text>
[--skill <n>] [--project <c>] [--disabled] Declare recurring work
milieu schedule ls List schedules and next firing
milieu schedule show <name> One schedule, with its run history
milieu schedule enable|disable <name> Pause or resume one
milieu schedule rm <name> Remove it and its state
milieu eval --as <c> [--skill <s>] [--case <n>] [--backend <b>] [--judge] [--json] Grade a skill
milieu eval --as <c> add <name> --skill <s> [--contains <s>] [--tools <t>] < input Record a case
milieu eval --as <c> ls [--skill <s>] List cases and what they check
milieu eval --as <c> rm <name> --skill <s> Remove a case
milieu export --to <dir> [--force] Write this installation as a manifest
milieu a2a serve [--listen <addr>] [--url <public>] [--tls] Serve Agent2Agent to outside agents
milieu a2a card <agent> Print an agent's discovery card
milieu skill [--as <c>] versions <name> List recorded builds of a skill
milieu skill [--as <c>] diff <name> [<from>] [<to>] Compare two builds
milieu skill [--as <c>] rollback <name> [<selector>] Restore a build
milieu apply -f <m> [--prune] Prune removes undeclared SKILLS only
milieu knowledge [--as <c>] [--in <p>] ls List knowledge (agent>project>global)
milieu knowledge [--as <c>] [--in <p>] get <path> Print an item (most-specific)
milieu knowledge [--as <c>] [--in <p>] search [--lex|--sem|--hybrid] [--type <t>]
[--tag <t>] [--status <s>] [--fresh] [--current] [--verified] <query> Search
milieu knowledge [--scope <layer>] draft [--path <p>] [--type <t>] [--force] <file>
Draft an article from a .pdf/.md/.txt
milieu knowledge [--as <c>] [--in <p>] reindex [--scope <layer>] Rebuild one layer's index
milieu knowledge [--as <c>] [--in <p>] lint [--scope <layer>] Check one layer (OKF + contradictions)
milieu memory [--as <c>] [--in <p>] [--global] save <name> [--description <d>] [--type <t>] Save (stdin)
milieu memory [--as <c>] [--in <p>] ls List memories (union, labeled)
milieu memory [--as <c>] [--in <p>] get <name> Print a memory (most-specific)
milieu memory [--as <c>] [--in <p>] search <query> Search memories
milieu memory [--as <c>] [--in <p>] [--global] rm <name> Remove a memory
milieu cleanup [blob|transcript|messages] [--dry-run] [--grace <d>] [--retention <d>] [--keep-per-topic <n>] Reclaim derived data
milieu lint [<agent>] Check agent + skill + project config
milieu audit verify Verify the audit log hash chain
milieu audit tail [<n>] Print the last n audit records
milieu audit query [--agent <c>] [--cmd <n>] [--sandbox <k>] [--failed] [--since <w>] [--until <w>] [--limit <n>] Filter the log
milieu audit report <agent> [--since <w>] [--until <w>] [--prices <f>] Activity + token cost report
milieu gateway init Create the gateway CA + server identity (idempotent)
milieu gateway issue --cn <name> [--out <dir>] Issue a proxy identity (cert + pinned CA)
milieu gateway serve --config <gateway.toml> Run the trusted-zone broker (mTLS)
milieu gateway audit verify Verify the brokered-call log hash chain
milieu gateway audit tail [<n>] Print the last n brokered calls
milieu api init [--host <name>] Create the API CA + TLS server cert (idempotent)
milieu api key new --agent <code> [--label <t>] Mint an API key (shown once)
milieu api key ls [--agent <code>] | revoke <id> List or revoke API keys
milieu api serve [--listen <addr>] Run the REST control-plane server
milieu api audit verify Verify the API access log hash chain
milieu api audit tail [<n>] [--edge rest|a2a] Print the last n API/A2A requestsRoles: administrator, associate (default), assistant, auditor.
milieu daemon
milieu daemon --interval <d> Poll cadence (default 1s)
--skill <name> Skill run on each unread message (default respond)
--auto Auto-select the skill per message; --skill is the fallback
--wake-timeout <d> Per-agent wake deadline (default 2m)
--retention <d> Hourly-reclaim: prune settled messages & transcripts older than this (default 168h; negative = keep)
--grace <d> Blob GC: keep unreferenced blobs newer than this (default 1h)
--keep-per-topic <n> Transcript per-topic cap (0 = no cap)
--quiet Raise the log floor to warn (audit log still records)The hourly reclaim pass (also runnable on demand as milieu cleanup) GCs
orphaned attachment blobs and applies retention to transcripts and settled
messages. Blob GC runs even with a negative --retention, since attachments from
one-shot act --attach runs are never message-referenced and would otherwise
accumulate; the grace window protects in-flight uploads. Memory and knowledge
are authored data and are never touched.
milieu setup (guided)
milieu setup [--dir <dir>] [--defaults] [--dry-run] [--yes]Asks seven questions — sandbox, model, embeddings, agent roster, servers —
writes the answers to milieu.yaml, and applies them through the same engine
apply uses. Nothing is written until the final confirmation. Answers may be
piped in on stdin; --defaults asks nothing.
milieu apply (declarative)
milieu apply -f <manifest.yaml> [--dry-run] [--prune] [--secrets-out <file>]Session quotas
milieu api quota set --agent <code> [--max-open <n>] [--rate <n> --window <dur>]
milieu api quota lsBounds how many conversations a principal may hold open and how fast it may
start new ones. Checked when a session is opened over either the REST API or
the A2A endpoint — both admit conversations for the same principal — and never
mid-conversation, so work already admitted is not stranded. 0 is unlimited on
each knob independently; “may open none” is a revoked key, not a quota of zero.
Refusals are 429 with Retry-After over REST and JSON-RPC -32005 over A2A.
The arrival limit is a token bucket, so a full window’s allowance may be spent
as a burst but the sustained rate holds.
REST API endpoints
All act as the key’s agent; ?in=<project> is membership-gated.
| Method + path | Does |
|---|---|
GET /v1/health | liveness (unauthenticated) |
GET /v1/whoami | the authenticated agent’s identity |
GET /v1/skills | the agent’s skills |
POST /v1/skills/{name}/run (?in=) | run a skill, returns {skill,output}; optional attachments, topic |
POST /v1/skills/run (?in=) | auto-select a skill for the input, then run; optional attachments, topic |
POST /v1/wake (?skill=&auto=) | process the agent’s inbox |
GET /v1/inbox (?unread=) | list messages |
POST /v1/inbox/{id}/read | mark read, return the full message |
POST /v1/messages {to,subject,body,attachments?} | send a message |
POST /v1/ask {to,body,timeout,attachments?} | send + wait for the reply |
POST /v1/sessions · GET /v1/sessions · GET/DELETE /v1/sessions/{id} · POST /v1/sessions/{id}/close | external conversations (caller’s own only) |
GET /v1/knowledge · /v1/knowledge/item?path= · /v1/knowledge/search?q= (?in=&mode=&type=&tag=&status=&fresh=1¤t=1&verified=1) | read knowledge (union); items carry their OKF metadata |
GET /v1/memory · /v1/memory/item?name= · /v1/memory/search?q= (?in=) | read memory (union) |
Cross-agent delivery (/v1/messages, the /v1/ask reply path) requires
milieu daemon running alongside.
Permission rule grammar
Matched on argument tokens, not a flattened string:
| Rule | Matches |
|---|---|
cmd | any invocation of cmd |
cmd(a b) | exact: argv equals tokens a b |
cmd(a b:*) | prefix: argv begins with a b; later args unrestricted |
cmd(:*) | any argv |
cmd() | empty argv |
The trailing :* is a suffix wildcard — it constrains the leading tokens but not
what follows (curl(https://host:*) still permits extra args). Use an exact rule
to bound the whole command line.
Built-in tools
These are handled inside milieu rather than as sandboxed subprocesses, and
each is audited with sandbox builtin. All but skill are granted like any
other command — a rule naming the tool in the agent’s allow list.
| Tool | Grant | Purpose |
|---|---|---|
knowledge | knowledge | Query the agent’s knowledge base |
memory | memory | Read and write the agent’s memory |
transcript | transcript | Recall past runs |
session | session(request-input:*) | Say the reply is a question: marks the session input-required |
| MCP tools | mcp(<route>[ <tool>]:*) | Tools brokered from an external MCP server, projected with its own schemas as <route>__<tool> |
gateway | gateway | Brokered HTTP call; the credential stays in the gateway |
code | code | Run a Starlark program (see below) |
skill | none — part of the skill definition | Read the running skill’s own bundled files |
The code tool
code runs a short Starlark program — Python syntax, no standard library — so a
model can do in one step what would otherwise be a chain of tool calls. Fetching
a list and acting on each element is one script, not one round trip per element,
which is both faster and materially cheaper in tokens.
Starlark is used because it has no ambient authority: no open, no sockets, no
imports. Everything a script can reach is a function milieu injected, and which
functions those are is derived from the agent’s own grants:
| Injected | Present when | Returns |
|---|---|---|
run(cmd, args=[], stdin="") | any subprocess command is granted | struct .exit_code .stdout .stderr |
gateway(path, method=, query=, headers=, body=) | gateway is granted | struct .status .body |
knowledge(*args) / memory(*args) / transcript(*args) | each granted separately | struct .ok .text |
json, re, time, math | always | modules (see below) |
A script is therefore never a route around permissions: run calls the same
runner as a direct tool call, is permission-checked identically, and writes its
own audit record. Granting code alone grants nothing to call.
The program sets a global result, which is returned to the model; print()
output is returned alongside it. Because Starlark has no exceptions, a call that
ran and failed returns a value to branch on (.exit_code, .status), while
one that could not be attempted — a denied permission — aborts the program.
Runs are bounded at 10M execution steps and 30 seconds, and captured output at 8 KiB. A step-limit breach is reported to the model as a non-terminating loop so it can fix the actual fault.
Four modules are always present, because a capability’s return value is of no use until it can be parsed. All are values-in, values-out and widen nothing a script can reach:
| Module | Notes |
|---|---|
json | decode/encode. The shape most API and MCP results arrive in |
re | Python-shaped search/match/findall/sub/split/escape. The engine is Go’s RE2, so patterns may not use lookahead, lookbehind or backreferences — a compile failure says so explicitly. In exchange matching is linear-time, so a model-written pattern cannot hang a run |
time | parse_time/from_timestamp/now and the duration units. Subtract two times for a duration; divide by time.hour for a number |
math | The usual functions. Note floor/ceil return ints, sqrt a float |
time.now() is pinned to a single instant per run. Parsing and comparing
timestamps is ordinary computation and is why the module is present; reading the
clock is the one part that is ambient authority. Pinning keeps “older than 30
days” working while keeping a run reproducible and denying a script a timing side
channel — now() cannot measure how long anything took. It does advance between
runs.
Environment variables
| Variable | Default | Purpose |
|---|---|---|
MILIEU_HOME | $PWD/.milieu | Root state directory |
MILIEU_AGENT | — | Default caller agent (override with --as) |
MILIEU_SANDBOX | bwrap (Linux) / sbexec (macOS) | Sandbox backend; none is never auto-selected — set it explicitly to opt out of isolation |
MILIEU_SANDBOX_PATH | /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin (or build-time SANDBOX_PATH) | Fixed search path for sandboxed commands; relative entries ignored |
MILIEU_LLM | first available (claude, then openrouter), else echo | LLM backend |
ANTHROPIC_API_KEY | — | Claude API key (enables claude) |
MILIEU_LLM_URL | https://openrouter.ai/api/v1 | OpenAI-compatible chat base URL for openrouter (also LiteLLM, vLLM, local llama-server) |
MILIEU_LLM_TOKEN | — | Bearer token for the openrouter endpoint (falls back to OPENROUTER_API_KEY) |
MILIEU_LLM_MODEL | anthropic/claude-haiku-4.5 | Model id for openrouter, e.g. openai/gpt-4o, google/gemini-2.5-pro |
MILIEU_ENV_PASSTHROUGH | — | Comma-separated host var names that agent.toml [env] $VAR references may resolve |
MILIEU_MAX_ITERATIONS | 50 | Ceiling on a run’s tool-use loop. A skill’s max-iterations is clamped to this; a skill that declares none gets 10. A value that is not a positive integer is ignored |
MILIEU_SCHEDULER | false | Let milieu daemon fire declared schedules. Off by default: agents that act on their own initiative are a different proposition from agents that only answer. Schedules can be declared, linted and applied either way |
MILIEU_EMBED | first available of openai, voyage | Embedding backend |
MILIEU_EMBED_URL | — | OpenAI-compatible embeddings base URL (enables openai), e.g. http://localhost:8080/v1 |
MILIEU_EMBED_TOKEN | — | Bearer token for the embeddings endpoint (optional) |
MILIEU_EMBED_MODEL | backend default | Model for the active embeddings backend (text-embedding-3-small for openai, voyage-3-lite for voyage) |
VOYAGE_API_KEY | — | Enables the voyage backend |
OTEL_ENABLED | false | Gate for the OpenTelemetry SDK in the long-running servers (api serve, gateway serve, daemon). When off, the global no-op providers stay in place and telemetry costs nothing |
SERVICE_VERSION | embedded VCS revision, else dev | Sets the service.version resource attribute; left unset, binaries built from a git checkout are auto-stamped with their commit SHA (-dirty when the tree was modified) |
When OTEL_ENABLED=true, exporters and the rest of the resource are configured
out-of-band through the standard OTEL_* environment variables the SDK reads
directly — OTEL_EXPORTER_OTLP_ENDPOINT/_HEADERS/_PROTOCOL,
OTEL_TRACES_EXPORTER/OTEL_METRICS_EXPORTER, OTEL_TRACES_SAMPLER, and
OTEL_RESOURCE_ATTRIBUTES (e.g. deployment.environment=staging). Setting
OTEL_METRICS_EXPORTER=prometheus makes the API server expose a scrape endpoint
at GET /v1/metricsz instead of pushing metrics over OTLP. See
OPERATIONS §Observability.
Set by the runner inside an agent sandbox (not operator-set): MILIEU_GATEWAY_URL
(when a [gateway] block is present), and HTTPS_PROXY/HTTP_PROXY (when a
restricted [network] policy is enforced by the loopback egress proxy).
Filesystem layout
$MILIEU_HOME/var/
├── skills/<agent>/<name>/<digest>/ # every build of a skill that has run
├── sessions/<id>/ # external conversations: index.md, one file per turn, summary.md
├── global/{knowledge,memory,index}/ # global layer (shared, ungated)
├── agents/<code>/
│ ├── agent.toml # identity, role, permissions, network, [env], [[mount]], [gateway]
│ ├── personality.md # voice/tone brief (prepended to the system prompt)
│ ├── responsibility.md # mandate/scope brief (prepended to the system prompt)
│ ├── skills/<name>.md | <name>/SKILL.md # capability definitions
│ ├── knowledge/ memory/ index/ # the agent's own layers
│ ├── work/ # the ONLY sandbox-writable path
│ ├── in.db out.db # message queues
│ └── state/<topic>/ # run transcripts, filed by topic (+ TOPICS.md)
├── projects/<code>/
│ ├── project.toml instructions.md # config + prepended instructions
│ └── knowledge/ memory/ index/ # membership-gated project layers
├── evals/<agent>/<skill>/<case>.md # evaluation cases (declared by an agent's `evals:`)
├── schedules/<name>.toml # recurring task declarations (apply writes these)
├── schedules/state/<name>.json # when each last fired (the daemon writes these)
├── blobs/ # content-addressed attachments (SHA-256)
├── audit/audit.jsonl # append-only, hash-chained
├── gateway/{ca,server}/ gateway/audit.jsonl # trusted-zone broker state
├── proxy/<code>/ # an agent's proxy identity (host-only)
└── api/{ca,server}/ api/keys.db api/audit.jsonl # API server stateManifest schema
Multi-document YAML, apiVersion: milieu.dev/v1. Reconcile is upsert, no
prune: declared resources are set to match the manifest; resources absent from
the manifest are never deleted.
apiVersion: milieu.dev/v1
kind: Agent
metadata: { name: mira }
spec:
role: associate # administrator|associate|assistant|auditor
personality: ./agents/mira/personality.md # voice/tone brief, copied in + injected at run
responsibility: ./agents/mira/responsibility.md # mandate/scope brief, copied in + injected at run
permissions: ["knowledge(search:*)", "curl(:*)"] # permission rules
network: ["api.github.com:443"] # [network] allow entries
gateway: { endpoint: https://gw:8443, identity: gateway-id } # issues a proxy identity
apiKey: { create: true, label: ci } # mints an API key (once)
sessionQuota: { maxOpen: 5, rate: 20, window: 1h } # 0/omitted = unlimited per knob
skills: # copied into the agent; --prune removes undeclared
- ./agents/mira/skills/respond # directory form (SKILL.md + resources)
- ./agents/mira/skills/handover.md # flat form
evals: # one directory per skill, named for the skill
- ./agents/mira/evals/respond # grades the `respond` skill
---
apiVersion: milieu.dev/v1
kind: Project
metadata: { name: acme }
spec:
members: [mira]
instructions: ./acme.md
knowledge: [{ source: ./acme-docs }]
---
apiVersion: milieu.dev/v1
kind: Knowledge
metadata: { name: global }
spec: { layer: global, sources: [./global-docs] } # layer: global|agent (+ agent: <code>)
---
apiVersion: milieu.dev/v1
kind: Gateway
metadata: { name: default }
spec:
listen: 0.0.0.0:8443
routes:
- { name: github, prefix: /github, backend: rest,
upstream: https://api.github.com,
auth: { kind: bearer, tokenEnv: GH_TOKEN } }
- { name: oms, prefix: /oms, backend: mcp, # an MCP server, brokered
upstream: https://mcp.vendor.example/mcp,
auth: { kind: bearer, tokenEnv: OMS_MCP_TOKEN },
mcp: { tools: [order_status] } } # operator allowlist (required)
- { name: counsel, prefix: /counsel, backend: a2a, # an A2A peer, brokered
upstream: https://peer.example/a2a/advisor,
auth: { kind: bearer, tokenEnv: PEER_TOKEN } } # POST /send and /card
---
apiVersion: milieu.dev/v1
kind: API
metadata: { name: default }
spec: { host: api.example.com }
---
apiVersion: milieu.dev/v1
kind: Web
metadata: { name: portal }
spec:
host: portal.example.com # extra SAN for the server cert
operator: admin # created if absent; the portal
# will not start without one
---
apiVersion: milieu.dev/v1
kind: Schedule
metadata: { name: nightly-audit } # the name is also the transcript topic
spec:
agent: auditor
skill: review-log # omit to auto-select per run
cron: "0 3 * * *" # minute hour dom month dow, host local time
input: "Review yesterday's audit log and record anything anomalous."
# project: acme # optional scope; membership is enforced
# disabled: true # declare it without starting it
---
apiVersion: milieu.dev/v1
kind: Settings
metadata: { name: runtime }
spec:
sandbox: sbexec # bwrap|sbexec|none
llm: openrouter # claude|openrouter|echo
llmModel: anthropic/claude-haiku-4.5
embed: voyage # openai|voyage
embedModel: voyage-3-liteNotes:
- Secrets are never in the manifest.
applymints API keys / CA fingerprints and prints them once (or to--secrets-out <file>; keep it out of VCS). A key is minted only if the agent has no active one. Settings(backend and model names) are env-only;applywritesmilieu.envinMILIEU_HOMEfor the operator tosource. No token is ever written there.sessionQuotabounds conversations opened over REST and A2A, since both admit them for the same principal. It is applied whether or not akind: APIdocument is present. Zero (or omitted) is unlimited on each knob independently, so an agent without one is unchanged; “may open none” is a revoked key, not a quota of zero.Webdeclares the operator’s name, never their password.applytakes a password out of band (milieu setupprompts for one) or generates one and reports it once. An existing operator is left alone — a re-apply must not reset the credential to the installation;milieu web admin passwddoes that.evals:declares an agent’s evaluation suites: one directory per skill, named for the skill it grades (./agents/mira/evals/respondgradesrespond, the same conventionskills:uses). Installing replaces rather than merges — the declared cases are the suite, so a case deleted from the source stops being graded — and a directory with one unparseable case installs none of it. Likeskills:, a nil field means the manifest does not manage them andevals: []means there are none; only--pruneremoves an undeclared suite.Scheduledeclares recurring work but never switches it on: whether anything fires isMILIEU_SCHEDULERon the daemon, so a manifest can be applied to a host that runs nothing. Re-applying leaves an existing schedule’s clock alone — it neither resets the history nor replays what was missed while the schedule was declared differently. Nothing is pruned: a schedule that vanishes from a manifest may have moved to another file, and silently stopping recurring work is worse than leaving it declared. Remove one withmilieu schedule rm.- The
kind/specshape mirrors Kubernetes so manifests can map onto CRDs later.
Audit records
One JSON object per line in var/audit/audit.jsonl (and, in the same shape, the
API and gateway access logs):
| Field | Meaning |
|---|---|
seq | monotonic sequence number |
agent | the acting agent |
cmd | command or tool name |
args | argument tokens |
exit_code | process/tool exit code |
started_at, ended_at | timestamps |
sandbox | none · bwrap · sbexec · builtin |
model | the model that served the call — present only on records that called one |
usage | token counts for the call — present only on records that called a model |
skill_version | the skill’s declared version — on act records only |
skill_digest | sha256 of the skill as loaded (manifest + bundled resources), so an edit without a version bump is still visible |
stop | why a run ended early: max_iterations, max_tokens, no_tool, error. Absent when the model finished on its own — which is also how records written before the field existed read |
prev_hash, hash | chain fields; hash covers prev_hash + seq + payload |
usage holds input_tokens, output_tokens, cache_write_tokens,
cache_read_tokens, and calls (how many model calls the record covers — a
skill run makes one per tool-loop iteration, plus one for topic classification,
and reports their sum). Both model and usage are absent from records that
never reached a model, which keeps them byte-identical to logs written before
token accounting existed, so pre-existing chains still verify.
Usage rides inside the hashed payload, so token counts are as tamper-evident as the rest of the record.
builtin rows are host-side tool calls with no subprocess (currently the
knowledge and skill tools). audit verify recomputes the chain and reports
the first break:
audit: CHAIN BROKEN after 41 record(s): hash mismatch at seq 42 (record content was modified)Hash chaining detects edits, reorders, insertions, and deletions in the middle —
but not tail truncation (a shorter-but-valid chain). Anchor the latest hash
externally if you need to detect that.
audit query filters those records without leaving the CLI: --agent, --cmd,
and --sandbox match a field exactly, --failed keeps only non-zero exit codes,
--limit <n> keeps the newest n matches, and --since / --until bound
started_at. A <when> value is either a Go duration measured back from now
(--since 24h, --since 168h) or an absolute RFC3339 timestamp
(--until 2026-06-01T00:00:00Z). audit report <agent> aggregates the same
window into a Markdown summary — totals, failures, per-command / per-sandbox
breakdowns, and token usage with cost — for the auditor role.
Cost and the price table
The log records tokens, never money. A token count is a fact about a call
that stays true; a price is policy that changes, differs per model and contract,
and would be frozen wrong the moment it entered an append-only log. audit report therefore computes cost at render time from
var/audit/prices.json — correct the table and every historical report restates.
{
"claude-opus-5": {"input": 5, "output": 25, "cache_write": 6.25, "cache_read": 0.5},
"claude-haiku-4-5": {"input": 1, "output": 5, "cache_write": 1.25, "cache_read": 0.1}
}Rates are per million tokens, the unit providers publish, so an entry can be copied off a price page without arithmetic. The four kinds are priced separately because providers charge them differently — a cache read is a fraction of a fresh input token, and a cache write costs more — so collapsing them misprices any run that used caching.
A key matches by longest prefix, so claude-opus-5 prices
claude-opus-5-20260401: providers append dated suffixes, and an exact-match
table would silently lose the price on the next release. A model with no entry
is reported as unpriced and excluded from the total rather than counted as
free, and the report names what it left out. With no price table at all, reports
show tokens and omit cost.
--prices <file> values the same window against a different table — what a
period would have cost on another tier, or under a rate you are negotiating:
./milieu audit report alice --since 168h --prices ./enterprise-rates.jsonBecause the log holds tokens rather than money, any table can be applied to any past window. A report priced this way says so in its footer, so its figures are never mistaken for what the agent is actually billed. Unlike the default table, a file named here must exist and parse — the request was for those rates specifically, so falling back silently would answer a different question.
Glossary
| Term | Meaning |
|---|---|
| agent | A directory under var/agents/<code>/ with an identity, role, permissions, skills, and its own knowledge/memory layers. Not a long-running process. |
| role | administrator / associate / assistant / auditor; recorded in agent.toml. Auditors are read-only and cannot send messages. |
| skill | An Agent-Skills-format capability (<name>.md or <name>/SKILL.md); the body is the system prompt. |
| layer | One of global / agent / project. Reads union most-specific-first (agent > project > global) and shadow on clash. |
| project | A membership-gated workspace with instructions and its own knowledge/memory layers. |
| union / shadow | Union = merge all applicable layers; shadow = a more-specific file hides a lower one with the same path/name. |
| route | Copy delivered messages from senders’ outboxes to recipients’ inboxes. |
| wake | Drain an inbox, run a skill per unread message, reply. |
| gateway | A trusted-zone broker that holds secrets and injects them into brokered calls so they never enter the sandbox. |
| API server | A REST control plane letting external programs drive milieu as an agent via a bearer key over TLS. |
| blob | A content-addressed (SHA-256) attachment in var/blobs/, shared across agents by digest. |