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
- Settings and environment variables
- Filesystem layout
- Manifest schema
- Ontology modules
- 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, marking the disabled
milieu agent disable|enable <code> Stop an agent doing anything, or let it again (audited)
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 policy ls [--gateway] The Cedar policies in force
milieu policy schema [--gateway] The requests policies are asked
milieu policy lint [--gateway] [--dir <dir>] Check etc/policies before it loads
milieu policy check --as <agent> <run|tool|consult|message|project> ... What the policies decide
milieu approval ls [--all] Actions waiting for a person's approval
milieu approval show|grant|deny <id> One request; approve it (once) or refuse it
milieu task ls [--agent <a>] [--state <s>] [--from api|a2a] Runs kept as tasks, every agent's, newest first
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>] send --about <node> <subj> Send to whoever answers for <node>
milieu message [--as <code>] inbox List messages
milieu message [--as <code>] read <id> Print + mark consumed
milieu route Deliver outbound -> inbound (one-shot), checking each signature
milieu route refused List messages the router refused, with the reason
milieu route retry Queue refused messages to be checked again
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 ask [--as <c>] --about <node> [--in <p>] Ask whoever answers for <node>
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>] [--parent <p>] [--level <l>] [--instructions <f>] Create a project
milieu project ls | tree | show <code> List, draw or inspect projects
milieu project set <code> [--parent <p>] [--level <l>] Move or relabel a project (a move is audited)
milieu project rm <code> [--force] Remove a project and its layer (audited)
milieu project disable|enable <code> Close a project to new work, or open it (audited)
milieu project member add|rm <code> <agent> Manage the member roster
milieu project overseer add|rm <code> <agent> Grant or withdraw oversight (audited)
milieu charter show [--in <p>] What a run is told the work is for
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 skill builds and applied history (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 eval compare --before <f.json>... --after <f.json>... Compare two sets of --json runs
milieu export --to <dir> [--force] Write this installation as a manifest
milieu a2a serve [--listen <addr>] [--url <public>] [--tls | --insecure-http] Serve Agent2Agent to outside agents
milieu a2a card <agent> Print an agent's discovery card
milieu mcp serve [--listen <addr>] [--url <public>] [--tls | --insecure-http] Serve agents to MCP hosts (default :8093)
milieu mcp tool <agent> Print the tool a host will list
milieu auth serve [--listen] [--url] [--mcp-url] [--tls | --insecure-http] Run the built-in OAuth issuer (default :8094)
milieu auth person add|passwd <name> [--password-stdin] Create a person, or change their password
milieu auth person rm <name> | person ls Remove a person; list people and their agents
milieu auth grant|revoke <person> <agent>... Change which agents a person reaches
milieu auth clients ls | clients rm <id> Hosts that registered; remove one
milieu auth audit tail [n] | audit verify The issuer's sign-in log
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 and eval suites, and disables undeclared agents and projects
milieu apply history [<kind>/<name>] What was applied, when, and by whom (as claimed)
milieu apply drift [--json] What changed outside apply since the last one; exits 2 on drift
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 graph classes [--in <p>] The classes a view of the domain model declares
milieu graph show <node> [--in <p>] One node and its edges, both ways
milieu graph path <a> <b> [--in <p>] The shortest chain of edges between two nodes
milieu graph trace <objective|project> [--in <p>] What advances an objective, or what a project serves
milieu graph reach <node|agent:<code>> [--in <p>] What documents, owns and reaches a node, and who holds the way
milieu graph orphans [--in <p>] What nothing documents, advances or connects
milieu graph why run:<seq> What a run read and called, under what each is about
milieu graph reindex Write each layer's declarations into its own graph.db
milieu graph infer --as <c> [--in <p>] [--limit <n>] [--ttl <d>] [--dry-run] Propose the edges the corpus states
milieu graph inferred --as <c> What inference added to that agent's own layer
milieu graph sweep --as <c> Remove the inferred edges that lapsed
milieu graph proposals [--sort count|recent] [--all] The edges agents proposed and the model lacks
milieu graph propose accept <id> --file <f> [--write] The change accepting a proposal makes; --write makes it
milieu graph propose reject <id> --reason <why> Decline a proposal, so it is not queued again
milieu graph propose reset <id> Forget a proposal, so it may be proposed afresh
milieu graph history <node> What a declared node meant, each time it changed
milieu graph export [--format turtle|jsonld|dot] [--base <iri>] [--in <p>] A view, as RDF or a digraph
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 lint -f <manifest.yaml> Check a manifest before applying it
milieu llm check [--json] Send the model backend one short prompt; report what answered
milieu lsp Language server for manifests (stdin/stdout)
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 gateway approval ls [--all] | show|grant|deny <id> The gateway's calls waiting for a person's approval
milieu gateway limits [--config <gateway.toml>] What each limited route has counted this minute and today
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 quota set --agent <code> [--max-open <n>] [--rate <n> --window <d>] Bound sessions
milieu api quota ls List agents that have a quota
milieu api audit tail [<n>] [--edge rest|a2a|mcp] Print the last n API/A2A/MCP requests
milieu web init [--host <name>] Create the portal CA + TLS server cert (idempotent)
milieu web admin add <name> [--role <r>] [--password-stdin] Create an operator account (admin|chat|viewer)
milieu web admin passwd <name> [--password-stdin] Change an operator's password
milieu web admin role <name> <admin|chat|viewer> Change what an operator may do
milieu web admin ls | rm <name> List operators and roles, or remove one and end its sessions
milieu web audit verify Verify the portal log's hash chain
milieu web audit tail [n] [--operator <n>] [--action <a>] Show the last n portal log entries
milieu web serve [--listen <addr>] [--insecure-http] Run the browser admin portal (HTTPS)Every serve command (api, mcp, a2a, auth, gateway, web) also
takes --shutdown-grace <d>, how long it waits on SIGTERM for in-flight
requests before answering them 503 (default 10s), and --drain-delay <d>,
how long it fails readiness before refusing connections (default 0s). See
OPERATIONS → Restarts drain in-flight requests.
Roles: 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)
--workers <n> Agents woken at once (default [runs] wake_workers, 4)
--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) runs garbage
collection (GC) on orphaned attachment blobs and applies retention to
transcripts and settled messages. Memory and knowledge are authored data and are
never touched. Blob GC runs even with a negative --retention, because
attachments from one-shot act --attach runs are never message-referenced and
would otherwise accumulate. The grace window protects in-flight uploads.
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. You can pipe answers in
on stdin. With --defaults, it asks nothing.
milieu apply (declarative)
milieu apply -f <manifest.yaml> [--dry-run] [--prune] [--secrets-out <file>] [--by <name>]
milieu apply history [<kind>/<name>]
milieu apply drift [--json]Every resource is digested with the content of every file it names. A changed
declaration is recorded under var/applied/ and audited with its old and new
digest. An unchanged one is reported as unchanged (and still reconciled, so
state edited by hand is put back). history lists the declaration in effect for
every resource, or one resource’s declarations, newest last, with who applied
it: the OS user, the --by label (default MILIEU_APPLIED_BY), and the
manifest’s Git commit and a dirty flag when it is in a work tree. All of these
are claimed, not verified.
After a successful apply, each declared resource’s installed state (the files
apply wrote, read back) is digested into var/applied/<kind>/<name>/installed.yaml.
drift reads them again and lists each resource that changed, naming the files
changed (~), added (+) and removed (-), and each agent, project, schedule
and policy on disk that was never applied. A resource applied before installed
state was recorded shows as unknown, which is not drift. It exits 0 with no
drift, 2 with drift, and 1 on error. --json prints an array of
{kind, name, state, changed, added, removed}. See
Drift.
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 the REST API, the
Agent2Agent (A2A) endpoint, or the MCP endpoint (all admit conversations for the
same principal), and never mid-conversation, so work already admitted is not
stranded. 0 is unlimited on each knob independently, so no quota expresses
“may open none”. Revoking the key does that. Refusals are 429 with Retry-After over REST, JSON-RPC -32000 (reason
QUOTA_EXCEEDED) over A2A, and a tool result with isError over MCP.
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 endpoints act as the key’s agent, and ?in=<project> is membership-gated. The
OpenAPI 3.1 document is schema/openapi.yaml, kept in
step with the server by a test: every route is in it, and every schema matches
its Go type.
| Method + path | Does |
|---|---|
GET /v1/health | liveness: the process is up (unauthenticated) |
GET /v1/ready | readiness (unauthenticated): 200 when the state directory is writable, the key store answers, and the model backend is configured. Otherwise 503 with each check’s result. a2a serve and mcp serve answer the same at /healthz and /readyz |
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, session. "wait": false returns 202 and a task instead, as does a run that ends waiting on a person’s approval |
GET /v1/tasks (?state=&limit=&before=) | the caller’s tasks, newest first, 50 a page (at most 200). before is the last task id of the previous page |
GET /v1/tasks/{id} · POST /v1/tasks/{id}/cancel | a run not waited on: working, completed, input_required (with waiting_on when parked on an approval, and inputs saying what it waits on, typed by kind: approval or question), failed, or canceled. The caller’s own only |
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. 403 when the sender’s role or the policies refuse it, or it waits for a person’s approval (the error names the request) |
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
Rules are matched on argument tokens:
| 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 run inside the Milieu process, with no sandboxed subprocess, 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 |
graph | graph, or a verb at a time: graph(show:*) | Walk the domain model the run’s scope reads: classes, find <words>, show <node>, neighbors <node> [1|2], path <a> <b>. Read-only |
transcript | transcript | Recall past runs |
session | session(request-input:*) | Say the reply is a question: marks the session input-required. --option <answer> (repeatable, up to 12) offers answers the client may pick |
consult | consult(<agent>:*), or consult for any agent | Put a question to another agent and get its answer as the tool result, within this run: the agent runs its respond skill one-shot, unscoped, filed under topic consulted-by-<caller>. One level deep. Refused to auditors |
| MCP tools | mcp(<route>[ <tool>]:*) | Tools brokered from an external Model Context Protocol (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 the following section) |
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 takes one script where it would otherwise
take a round trip per element. One script is faster and uses substantially fewer
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) / graph(*args) | each granted separately | struct .ok .text |
json, re, time, math | always | modules (see the following table) |
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), whereas
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 underlying 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, and a compile failure says so. 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. The module is present for
parsing and comparing timestamps, which is ordinary computation. Reading the
clock is the one part that is ambient authority. Pinning keeps “older than 30
days” working, keeps a run reproducible, and denies a script a timing side
channel: now() cannot measure how long anything took. It does advance between
runs.
Settings and environment variables
Operator settings live in etc/milieu.toml, one section per area, which every
Milieu process reads as it starts. An environment variable overrides the file
for the process it is set in. milieu config shows every setting, its value,
and whether it came from the environment, the file, or the default. A manifest’s
Settings document writes the file (milieu apply).
# etc/milieu.toml
[llm]
backend = "vertex"
url = "https://aiplatform.googleapis.com/v1/projects/P/locations/global/endpoints/openapi"
model = "google/gemini-3.8-flash"
[sandbox]
env_passthrough = ["GH_TOKEN"]
[budget]
daily = 5.0
monthly = 50.0The file is checked strictly: an unknown section or key, or a value of the
wrong type, stops every command but milieu config and milieu version, which
report it. Secrets are never read from it: API keys and tokens stay in the
environment, and milieu config shows only whether each is set. MILIEU_HOME,
which says where etc/ is, MILIEU_AGENT, and SERVICE_VERSION are environment
only too. [budget] is read on every call, not at start (see
Budgets).
| Variable | etc/milieu.toml | Default | Purpose |
|---|---|---|---|
MILIEU_HOME | (environment only) | $PWD/.milieu | Root state directory |
MILIEU_AGENT | (environment only) | unset | Default caller agent (override with --as) |
MILIEU_APPLIED_BY | (environment only) | unset | Who milieu apply records as having applied, beside the OS user (override with --by). Claimed, not verified |
MILIEU_SANDBOX | [sandbox] backend | bwrap (Linux) / sbexec (macOS) | Sandbox backend. none is never auto-selected. Set it to opt out of isolation |
MILIEU_SANDBOX_PATH | [sandbox] path | /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin (or build-time SANDBOX_PATH) | Fixed search path for sandboxed commands. Relative entries are ignored |
MILIEU_LLM | [llm] backend | first available (claude, then vertex, then openai), else echo | Large language model (LLM) backend: claude, vertex, openai or echo. openrouter is the former name of openai and still works |
ANTHROPIC_API_KEY | (environment only) | unset | Claude API key (enables claude) |
MILIEU_LLM_URL | [llm] url | https://openrouter.ai/api/v1 | For openai, the OpenAI-compatible chat base URL (also LiteLLM, vLLM and local llama-server). For vertex, Vertex AI’s …/locations/<loc>/endpoints/openapi, which it requires |
MILIEU_LLM_TOKEN | (environment only) | unset | Bearer token for the openai endpoint (falls back to OPENROUTER_API_KEY). Unset for vertex: the machine’s Google identity, from the metadata server on Google Cloud, else gcloud auth print-access-token |
MILIEU_LLM_MODEL | [llm] model | anthropic/claude-haiku-4.5 | Model id for openai and vertex, for example, openai/gpt-4o, google/gemini-2.5-pro |
MILIEU_ENV_PASSTHROUGH | [sandbox] env_passthrough | unset | Comma-separated host var names that agent.toml [env] $VAR references may resolve |
MILIEU_WAKE_WORKERS | [runs] wake_workers | 4 | Agents the daemon wakes at once. Concurrent runs can each pass a budget check before any is charged, so this also bounds how far a cap can be overshot. A value that is not a positive integer is ignored |
MILIEU_MAX_ITERATIONS | [runs] max_iterations | 50 | Ceiling on a run’s tool-use loop. A skill’s max-iterations is clamped to this, and a skill that declares none gets 10. A value that is not a positive integer is ignored |
MILIEU_GRAPH_RETRIEVAL | [graph] retrieval | on | Adds to a search what the domain model reaches from its words, after the text matches and labelled as reached through the model: documents into a knowledge search, notes into a memory one, and the route exposing a party the query names. Applies to the tools and to milieu knowledge search. off turns it off, for a corpus where it does not help. Score yours with examples/domain-retrieval and milieu eval compare |
MILIEU_OAUTH_ISSUER | [oauth] issuer | unset (API keys only) | Authorization server whose JWT access tokens the MCP endpoint accepts beside API keys. HTTPS unless on a loopback host. A token reaches an agent only with the scope milieu:agent:<code> |
MILIEU_OAUTH_AUDIENCE | [oauth] audience | each agent’s MCP resource URI | The audience every token must carry, for issuers that can’t bind a token to the resource a host names (RFC 8707) |
MILIEU_LOG_LEVEL | [log] level | info | Log floor for the CLI and the long-running servers: debug, info, warn, error. Unrecognised values fall back to info. The audit log is written regardless |
MILIEU_SCHEDULER | [scheduler] enabled | 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 | [embed] backend | first available of openai, voyage | Embedding backend: openai, voyage, or vertex (vertex only when named) |
MILIEU_EMBED_URL | [embed] url | unset | For openai, the OpenAI-compatible embeddings base URL (and it enables the backend), for example, http://localhost:8080/v1. For vertex, the publisher-models URL, for example, https://aiplatform.eu.rep.googleapis.com/v1/projects/P/locations/eu/publishers/google/models |
MILIEU_EMBED_TOKEN | (environment only) | unset | Bearer token for the embeddings endpoint, optional. Without it, vertex uses the machine’s Google identity, or gcloud off Google Cloud |
MILIEU_EMBED_BATCH | [embed] batch | 16 | Texts per embeddings request during a reindex (openai, voyage). Lower it for a slow CPU embedder, raise it for a hosted one. vertex sends one text per request, eight at a time |
MILIEU_EMBED_MODEL | [embed] model | backend default | Model for the active embeddings backend (text-embedding-3-small for openai, voyage-3-lite for voyage, gemini-embedding-2 for vertex) |
MILIEU_EMBED_DIM | [embed] dimensions | 768 | vertex only: vector size, shortened from the model’s full 3072 |
VOYAGE_API_KEY | (environment only) | unset | Enables the voyage backend |
OTEL_ENABLED | [otel] enabled | false | Gate for the OpenTelemetry SDK in every long-running server and in ask, act, and wake. When off, the global no-op providers stay in place and telemetry costs nothing |
MILIEU_APPROVAL_WEBHOOK | [approval] webhook | unset | A URL told, by a JSON POST, of each request for a person’s approval as it opens, by the host and by the gateway. See OPERATIONS → Policies |
MILIEU_APPROVAL_WEBHOOK_TOKEN | (environment only) | unset | Sent to the approval webhook as a bearer credential |
MILIEU_UNSIGNED_MESSAGES | [messages] unsigned | deliver | What the router does with a queued message that has no signature (one queued before signing): deliver delivers it with a warning and an audit row, and refuse keeps it in the outbox, refused and audited. Set refuse after an upgrade’s queues have drained. See OPERATIONS → Message signing |
MILIEU_AUDIT_KEY | (environment only) | unset | The audit HMAC key, hex or base64, 32 bytes or more (openssl rand -hex 32). Every audit log record written with it carries a mac. See OPERATIONS → Keying the audit logs |
MILIEU_AUDIT_KEY_FILE | [audit] key_file | unset | A file holding the audit key instead. Refused inside MILIEU_HOME or when group or other can read it. Setting both is an error |
MILIEU_POLICY_MODE | [policy] mode | off | shadow also puts command, built-in tool, consult, message, project access, MCP and A2A reach and session, gateway route, and admin portal checks to the Cedar policies in etc/policies/, and records each disagreement in the agent audit log. Milieu’s own checks still decide. enforce lets the policies decide every check on the Milieu host, the admin portal’s, and with its own policies the gateway’s, failing closed (a policy file that doesn’t parse refuses everything they decide). See OPERATIONS → Policies |
MILIEU_OTEL_FLUSH_TIMEOUT | [otel] cli_flush_timeout | 1s | How long ask, act, and wake wait at exit to send their telemetry, as a duration (500ms, 2s). Past it the command exits and its spans are dropped, with a warning |
MILIEU_SHUTDOWN_GRACE | [server] shutdown_grace | 10s | How long a server waits on shutdown for in-flight requests before answering those still running 503, as a duration. A serve command’s --shutdown-grace overrides it. Keep the supervisor’s stop timeout above it |
MILIEU_DRAIN_DELAY | [server] drain_delay | 0s | How long a server fails readiness on shutdown before it stops accepting connections, so a load balancer stops routing to it first. 5s behind one. A serve command’s --drain-delay overrides it |
SERVICE_VERSION | (environment only) | embedded version-control 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 (for example, 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
and NO_PROXY=127.0.0.1 (when a [gateway] block is present), and
HTTPS_PROXY/HTTP_PROXY (when a restricted [network] policy is enforced by
the egress proxy, and under bwrap they point at the in-sandbox bridge).
Filesystem layout
$MILIEU_HOME/etc/ # what the operator declares; milieu never writes it at run time
├── milieu.toml # installation settings: [budget]
├── models.json # each model's rates per million tokens (audit report, budgets)
├── gateway.toml # the gateway's listener and routes (apply writes it)
├── charter.yaml # the organisation's name, vision, mission and objectives
├── policies/*.cedar # the installation's Cedar policies (read in shadow mode)
├── policies/gateway/*.cedar # the gateway's own Cedar policies (read by the gateway alone)
└── projects/<code>/charter.yaml # each project's mission and objectives
$MILIEU_HOME/var/ # what the installation accumulates
├── skills/<agent>/<name>/<digest>/ # every build of a skill that has run
├── applied/<kind>/<name>/ # what apply recorded: history.yaml + a snapshot per digest
├── applied/objects/ # the files those declarations named, stored once by digest
├── sessions/<id>/ # external conversations: index.md, one file per turn, summary.md
├── global/{knowledge,memory,index}/ # global layer (shared, ungated)
├── global/ontology/*.yaml # the house domain model
├── 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 (parent, level, roster) + prepended instructions
│ ├── ontology/*.yaml # what holds within the project's domain model
│ └── 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
├── audit/spend.db # what each agent and the installation has spent (budgets)
├── audit/approvals.db # actions waiting for, or given, a person's approval
├── a2a/tasks.db # A2A tasks a peer did not wait on: working, finished, canceled
├── mcp/request-state.key # signs the state an MCP client hands back to resume a call held for approval
├── api/tasks.db # REST skill runs a client did not wait on
├── gateway/{ca,server}/ gateway/audit.jsonl # trusted-zone broker state
├── gateway/approvals.db # the gateway's calls waiting for, or given, a person's approval
├── gateway/state.db # the gateway's call counters, for routes' limits
├── proxy/<code>/ # an agent's proxy identity (host-only)
├── signing/<code>/message.key # an agent's Ed25519 message-signing key (host-only)
├── api/{ca,server}/ api/keys.db api/audit.jsonl # API server state
└── web/{ca,server}/ web/admin.db web/audit.jsonl # admin portal stateConfiguration that predates etc/ (var/gateway/gateway.toml,
var/global/charter.yaml, var/projects/<code>/charter.yaml) is moved there
the first time a process starts. A file already in etc/ is kept, and the older
one is left where it was.
etc/ is written one of two ways, chosen by the deployment. Where apply runs
against the installation, it writes etc/ like everything else, and a container
deployment persists etc/ on a volume as it does var/ (the Quadlet virtual
machines do).
Where etc/ is shipped as configuration, such as a Kubernetes ConfigMap, it is
read-only: it is rendered by applying the manifest against a scratch home, and
an apply inside the installation that would write etc/ (a Settings
budget, a Gateway, a charter) is refused before anything changes.
Manifest schema
Multi-document YAML, apiVersion: milieu.dev/v1. Reconcile is upsert, with no
prune: declared resources are set to match the manifest, and resources absent
from the manifest are never deleted. A key a kind does not have is refused at its
line, and so is a Settings backend that does not exist.
apiVersion: milieu.dev/v1
kind: Organisation
metadata: { name: default }
spec: { charter: ./charter.yaml } # name, vision, mission, objectives
---
apiVersion: milieu.dev/v1
kind: Agent
metadata: { name: mira }
spec:
role: associate # administrator|associate|assistant|auditor
name: Mira # display name (defaults to the metadata name)
oversees: [customer-ops] # act and read in these projects and below; writes nothing
ontology: { owns: [domain:refund] } # what it answers for in the domain model; grants nothing
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
env: { GH_USER: "$GITHUB_USER" } # [env] for the sandbox; $VAR needs MILIEU_ENV_PASSTHROUGH
mounts: [{ source: /srv/reports, target: reports }] # read-only, at /agent/reports in the sandbox
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:
name: Acme Broadband # display name (defaults to the metadata name)
description: Tier-1 support for the Acme account
parent: customer-ops # optional: the project this one sits under
level: account # optional label; nothing branches on it
members: [mira]
instructions: ./acme.md
charter: ./acme-charter.yaml # mission + objectives a run in acme is told
ontology: { scope: [domain:claim] } # the part of the domain model it works in
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: Ontology
metadata: { name: house }
spec: { sources: [./ontology] } # module files, or directories of them
---
apiVersion: milieu.dev/v1
kind: Ontology
metadata: { name: acme }
spec: { project: acme, sources: [./acme-ontology] } # what holds within one project
---
apiVersion: milieu.dev/v1
kind: Gateway
metadata: { name: default }
spec:
listen: 0.0.0.0:8443
routes:
- { name: github, prefix: /github, backend: rest,
agents: [mira], # who may call it; ["*"] for every agent
upstream: https://api.github.com,
auth: { kind: bearer, tokenEnv: GH_TOKEN } }
- { name: crm, prefix: /crm, backend: rest, agents: [mira, arun],
upstream: https://crm.example/api,
auth: { kind: bearer, tokenEnv: CRM_TOKEN },
limit: { agent: { minute: 30 }, # each agent's own allowance, by the UTC minute and day
route: { day: 5000 } }, # every agent together; past either, 429
ontologyClass: customer } # resolves domain:customer; ontologyInstance names one party
- { name: oms, prefix: /oms, backend: mcp, agents: [mira], # an MCP server, brokered
upstream: https://mcp.vendor.example/mcp,
auth: { kind: bearer, tokenEnv: OMS_MCP_TOKEN },
mcp: { tools: [order_status], # operator allowlist (required)
pin: "sha256:1f0c…" } } # tool-set fingerprint the server must still match
- { name: counsel, prefix: /counsel, backend: a2a, agents: [arun], # 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: default }
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: default }
spec:
sandbox: sbexec # bwrap|sbexec|none
llm: openai # claude|vertex|openai|echo
llmModel: anthropic/claude-haiku-4.5
embed: voyage # openai|voyage
embedModel: voyage-3-lite
---
apiVersion: milieu.dev/v1
kind: Policy
metadata: { name: no-push } # written to etc/policies/no-push.cedar
spec:
source: ./policies/no-push.cedar # or cedar: | with the policies inline
# gateway: true # the gateway's: etc/policies/gateway/Notes:
- Secrets are never in the manifest.
applymints API keys / certificate authority (CA) fingerprints and prints them one time, or writes them to--secrets-out <file>(keep that file out of version control). A key is minted only if the agent has no active one. - Every declaration
applyreconciles is recorded: its spec and the content of every file it names are digested, and a declaration whose digest moved gets oneapplyaudit row (withoperator,digest,prev_digest) and a snapshot undervar/applied/. Editing a comment or reordering a key leaves the digest unchanged, and editing a brief moves it. An apply that stops partway is audited as a failure and records nothing.cleanup builds --keep-builds <n>trims the history. Ontologydeclares domain model modules for the global layer, or for theprojectit names. The modules a manifest declares for a unit replace what was installed there, so a module deleted from the manifest stops shaping the model. A unit it declares none for is left alone. Every module is parsed before any is installed, and two modules with one file name in a unit are refused. See Ontology modules for the file format.Organisationdeclares the organisation itself, at most once. Itscharter:names the organisation’scharter.yaml, and aProject’scharter:names the project’s. A charter holdsname(read from the organisation’s only),vision,mission, andobjectives, each with anid, astatement, an optionaladvancesnaming an objective above as<project>/<id>orglobal/<id>, and an optionaluntildate after which it lapses. Apply refuses a charter that does not parse, unknown keys included. Lint checksadvances, lapsed objectives, whether a project’s chain composes past the 3072 bytes a run is told, and an objective that advances one whose statement oruntilan apply changed after it was itself last stated (restating it clears the warning).graph history objective:<unit>/<id>reads an objective’s statements from the charters applied history holds. The codeglobalis reserved for the organisation, and no project may take it.- An
Agent’soverseeslets it act in the projects it names, and in every project below them, without being on their rosters. It reads their knowledge and memory and writes nothing to them: a memory it saves while scoped to one lands in its own layer, and a knowledge write is refused. Lint reports a grant that names no project. - A
Projectmay name aparent, declared in the manifest or already installed. A run scoped to it reads the parent’s knowledge and memory too, and so on up the chain, at most six projects deep. Membership is still checked against the project itself. Apply creates parents before children whatever the document order, and names the chain before and after when it moves a project. - A
Projectis reconciled on every apply, whether or not it already exists: its name and description are set as declared, its instructions are replaced when a path is declared (and left in place when none is), and its roster is matched. Settingsis written toetc/milieu.toml, whole: a setting the document leaves out returns to its default, exceptbudget, which is kept unless declared. No token is ever written there.sessionQuotabounds conversations opened over REST, A2A and MCP, because all three 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. No quota expresses “may open none”. Revoking the key does that.Webdeclares the operator’s name only.applytakes a password out of band (milieu setupprompts for one) or generates one and reports it one time. An existing operator is left alone, because 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 the installed suite with the declared cases (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. Whether anything fires depends onMILIEU_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 stopping recurring work without a warning is worse than leaving it declared. Remove one withmilieu schedule rm.Policywrites Cedar policies toetc/policies/<name>.cedar, or withgateway: truetoetc/policies/gateway/<name>.cedar, fromsourceor inlinecedar(one or the other, not both). Policies are written before anything else, so a forbid is in place before what it governs is created. A policy that doesn’t parse is refused before any is written. Nothing is pruned: aPolicytaken out of the manifest leaves its file, so a forbid stays until someone deletes it. Lint checks each document asmilieu policy lintchecks a file, with the manifest’s policies as the whole set. Export writes every policy file back as aPolicydocument. See OPERATIONS → Policies.- The
kind/specshape mirrors Kubernetes so manifests can map onto custom resource definitions (CRDs) later.
Editor validation
schema/milieu.schema.json is a JSON Schema for
the preceding documents, published at
https://milieu.im/schema/v1/milieu.schema.json. Point an editor at it and a
manifest gets completion, hover documentation, and inline errors, with no tooling
beyond a YAML language server.
In VS Code (the YAML extension), in .vscode/settings.json:
{
"yaml.schemas": {
"https://milieu.im/schema/v1/milieu.schema.json": ["milieu.yaml", "**/milieu.yaml"]
}
}Neovim, Helix, and Zed drive the same language server and take the same mapping. A modeline at the top of the file works anywhere and travels with the manifest:
# yaml-language-server: $schema=https://milieu.im/schema/v1/milieu.schema.jsonEach document is validated on its own, the way apply decodes it. The schema is
stricter than the parser in one place: backend: accepts only the three
route kinds a manifest can express (rest, a2a, mcp). gateway.toml also
supports cli, sqlite, and postgres, but those need [route.<backend>]
blocks the manifest has no fields for, so declaring one here writes a
gateway.toml that the gateway then refuses to build. The schema reports that
in the editor, before startup.
The schema is hand-maintained. go test ./internal/setup/... checks it against
the types in internal/setup/manifest.go: every field covered, every enum in
step, and the published copy identical to the canonical one.
The schema checks each document’s shape. What a manifest means (a project
member no document declares, a skill that asks for a tool its agent was never
granted, a brief file that is not there, a cron expression that can never fire)
is what milieu lint -f milieu.yaml checks, without applying anything. See
OPERATIONS → Declarative provisioning.
Ontology modules
The domain model: the classes of thing the organisation works with, the relations
between them, the reference instances policies are written about, and its
vocabulary. One file per module, under var/global/ontology/ for the house model
and var/projects/<code>/ontology/ for what holds within a project:
classes:
- name: customer # lowercase; hyphens between words
label: Customer # what a person reads; defaults to the name
description: Someone we serve.
subclass_of: party # optional
keyed_by: customer_id # what identifies a live instance
resolved_via: route:crm # the gateway route that holds live instances
aliases: [subscriber]
relations:
- { name: entitled_to, from: customer, to: sla-tier, inverse: entitles }
instances: # reference instances only: few, stable, named in prose
- { class: sla-tier, key: gold, label: Gold, part_of: sla-tier/standard }
- { class: customer, key: acme, entitled_to: sla-tier/gold } # any other key is a relation
terms:
- { term: the courier, means: supplier/parcelco } # another name for a node
- { term: truck roll, definition: Sending an engineer out. } # a word with no class
retracts: # a project only
- { s: policy/house-refunds, p: governs, o: product-line/home-cover, reason: Replaced by the ACME contract. }- Identifiers are lowercase.
domain:<class>,domain:<class>/<key>, andterm:<slug>are declared.org:root,project:<code>, andobjective:<unit>/<id>are derived from the project tree and the charters. A reference without a prefix is adomain:reference. - Predicates.
subclass_of,instance_of,part_of,same_as,supersedes, andrelated_toare built in, and so are the derivedpursuesandadvances. Any other key on an instance is a declared relation, and the edge must run from an instance of itsfromclass to one of itstoclass, or of a class below either. - Views.
--in <project>reads the global layer, then each project in the chain from the outermost down. A nearer declaration of a node replaces its label, description, and aliases. Edges accumulate, and a term shadows by term. A retraction must give areasonand name an edge a layer above declares. - Attachments.
about:in a knowledge document’s or a memory’s frontmatter, andacts-on:in a skill’s, name the nodes the object is about. They are read asdocuments,notes, andacts_onedges fromknowledge:<layer>/<concept>,memory:<layer>/<name>, andskill:<agent>/<name>, where<layer>isglobal,project/<code>, oragent/<code>, and shown from the domain’s end asdocumented_by,noted_by, andacted_on_by. A view reads the global layer’s and its chain’s knowledge and memory and every skill. An agent’s own layers are read in the operator’s view and that agent’s runs only.acts-on:names classes. A gateway route’sontology-class(a class whose live records it resolves) andontology-instance(the one party it exposes), an agent’s[ontology] ownsand a project’s[ontology] scopeare read the same way, asresolves,exposes,accountable_for, andscopesfromroute:<name>,agent:<code>, andproject:<code>, shown asresolved_via,exposed_by,owned_by, andscoped_by, in every view. In a manifest they are a route’sontologyClassandontologyInstance, andontology: { owns: […] }andontology: { scope: […] }. The edges are derived and grant nothing. - Governance.
graph reach <node>lists what documents and notes a node, what acts on it, who owns it and works in it, and each route to it with the agents that hold it, naming an owner who does not.graph reach agent:<code>lists what an agent owns and its skills act on, and whether it holds the way to each. An agent holds every gateway route when a permission grants it thegatewaytool and itsagent.tomlhas a[gateway]block, and none otherwise.graph orphanslists classes nothing documents, nodes documented only by stale or deprecated knowledge, live objectives of a unit with units below it that nothing advances, and claims that name nothing in the view.graph why run:<seq>joins the audit log to the model: the knowledge the run got, the memory it recalled, the routes it called, and the nodes it looked up with thegraphtool, each under what it is about in the view its agent reads from the run’s project, and what it did that is about nothing in the model. A run’s calls are its agent’s built-in calls while it ran, so an overlapping run by the same agent is named. A search is listed but not its hits. A node redefined since the run is explained against the declaration in force at the time (what it meant then, read from the applied snapshot, and the change that moved it), not against the view as it is now. The label stays today’s, and a node whose snapshots retention has swept says so. - Changed since verified. A knowledge document whose newest
verifiedentry is older than the last change to the definition of a node it isabout:is labelled byknowledge getandls(CLI and tool), and reported by lint with the date, theOntologydeclaration and its operator. A change is one an applied declaration makes to a node it already declared. An alias, a label, or a project declaring the node for its own readers is not one. Search hits are not labelled. - Proposals. An agent granted
graph(propose:*)(a grant ofgraphalone does not carry it) may callgraph propose <subject> <predicate> <object> <why>from a run, at most three times a run. The ends must be classes or instances the run’s view declares. The predicate must be a declared relation between the right classes,subclass_ofbetween classes (no cycle), orpart_of,same_as, orrelated_tobetween instances. The edge must not be one the model already has or one a person rejected. A proposal is kept undervar/ontology/proposals/<id>.json, outside every agent’s home, keyed by the proposing unit and the edge, with a count, the reasons, the runs (agent, skill, and digest) and the nodes they had looked up.graph propose acceptwrites the edge into its subject’s declaration in the module file it is given, as one inserted or changed line, and prints the diff. The model changes when the file is applied. Accept, reject, and reset are audited with the operator. - Retrieval (on by default,
MILIEU_GRAPH_RETRIEVAL=offturns it off). A search links the query’s words to the classes, instances and terms of the run’s view (labels, aliases, identifiers, and terms, as whole words, the longer name winning) and appends what is attached to them, then to the classes onesubclass_ofstep away, at most five each, after the text matches. Aknowledgesearch appends the documentsabout:them, and amemorysearch the notesabout:them. Aknowledgesearch also names the route whoseontology-instanceexposes a party the query names, last, as aroute:<name>instead of a path, and only to an agent grantedgateway. Each is held to the search’s filters and labelledreached through the domain model, with, not a text matchwhere a search could have matched the text.milieu knowledge searchfuses the same way, against the operator’s view or, with--as, that agent’s. - Vocabulary in the prompt. A run scoped to a project is told the terms that
project’s own modules declare, beneath its charter, within the charter’s
3072-byte cap and its
context_digest. A department’s terms and the house glossary are not told. An agent reaches them through thegraphtool. - Definitions and history. Every node carries a definition digest: its
description, superclass, key, route, class, or part. Its label and aliases are
excluded.
graph history <node>lists what it meant each time anOntologydeclaration of its modules was applied and that digest moved. - Export.
graph exportwrites a view as Turtle or JSON-LD, usingrdfs,rdf,owl, andskosfor subclass, type, sameAs, labels, and alternative labels, or as a Graphviz digraph. Identifiers go under--base(urn:milieu:by default). - Read from files. A view is built from the modules on every read, so a changed module is in the next read with nothing to reindex. Unknown keys are refused everywhere except on an instance, whose other keys are its relations.
- The store.
graph reindexwrites each layer’s own declarations intovar/global/graph.db,var/projects/<code>/graph.dborvar/agents/<code>/graph.db: its own, never what it inherits, so a reorganisation rewrites none of them. It is a projection of the files. The only thing it alone holds is inference, which no file would rebuild, and a rewrite leaves those edges alone. - Inference.
graph infer --as <agent>reads the corpus a document at a time and asks a model which declared relations it states. A candidate is dropped unless its subject, object and predicate are all already declared and it passes the same type check a run’s own proposal gets. This check stops a sentence written into a document from becoming a relation. What survives is queued for review and written to that agent’s own layer alone,unverified, expiring after--ttl(a fortnight by default). An inferred edge never outranks a declared one, a read leaves a lapsed one out, andgraph sweepdeletes it.graph inferredlists them with the model and document each rests on.--dry-runwrites nothing. A pass run twice proposes nothing the second time, because its own edges are in the view it checks against. - Lint.
milieu lintreports, against the layer that declared it: an identifier not in one case, a reference to an undeclared node, class, or relation, a relation joining the wrong classes, a subclass cycle, a term that means nothing, a retraction without a reason or with nothing to retract, aresolved_viaroute the gateway does not declare,related_tomaking up most of the declared edges, anabout:oracts-on:naming a node that is not declared (for a skill, a route, an agent or an agent’s own layer, in no view), anacts-on:orontology-classnaming anything but a class, and anontology-instancenaming anything but an instance.
Audit records
One JSON object per line in var/audit/audit.jsonl (and, in the same shape, the
access log the REST API, A2A and MCP edges share, and the gateway’s):
| 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, empty (no reply after using tools, even when asked once more), error. Absent when the model finished on its own, which is also how records written before the field existed read |
operator | the local user who ran apply, on apply records only. What the OS reports, so it says who ran the command, not who approved the change |
digest, prev_digest | a declaration’s digest after and before an apply changed it, on apply records only, and prev_digest is absent for a resource new to the history |
context_digest | sha256 of the charter and department context an act run was told (what milieu charter show prints). Present only when either applies |
policies | the ids of the Cedar policies that decided a command or built-in tool call: the permit that allowed it (milieu:grant for an agent’s own permission) or the forbid that refused it. Present with policy.mode shadow or enforce wherever the policies’ answer stood. Absent when it is off, and where shadow mode recorded a disagreement instead. The gateway’s brokered-call rows and the admin portal’s entries carry the same field |
approval | the approval request a call waited on (refused, exit_code -1) or was let through on (granted, and now spent). Also on the approval rows milieu approval grant and deny write, with operator, the request decided |
prev_hash, hash | chain fields. hash covers prev_hash + seq + payload |
mac | HMAC-SHA256 of hash under the audit key. Present only on records written with one (every log, not only this one) |
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)A second line says how many records are keyed, from which seq, and whether
the MACs were checked.
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 only. A token count is a fact about a call that
stays true. A price is policy: it changes and differs per model and contract,
and a price written into an append-only log would go out of date. audit report
therefore computes cost at render time from the model list, etc/models.json.
If you correct the table, every historical report is restated. An installation that
predates the model list keeps its table at var/audit/prices.json, which is
read while etc/models.json is absent.
The same table prices each call as it is made for budgets, so it is
operator policy, not state. It sits under etc/, outside every agent’s home.
Anyone who can lower a price can lift a cap.
{
"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 lose the price, with no warning, on the next release. A model with no entry
is reported as unpriced and excluded from the total, 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, for 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, 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 billed. Unlike the default table, a file named here must exist and parse: you asked for those rates, and falling back to the default table without a warning would price the window at rates you didn’t ask for.
Budgets
Budgets cap what model calls may cost, in US dollars per UTC calendar day and month: for the whole installation, and for each agent.
| Where | Declared as | Covers |
|---|---|---|
| Installation | [budget] daily, monthly in etc/milieu.toml (manifest: Settings budget: {daily, monthly}) | Every model call: agents’, and drafting, inference, compaction, and the eval judge |
| Agent | [budget] daily, monthly in agent.toml (manifest: Agent budget: {daily, monthly}) | The agent’s own calls, and those of every agent it consults |
# etc/milieu.toml
[budget]
daily = 5.0
monthly = 50.0Zero or unset is unlimited, per field. An agent that may spend nothing is one
whose key is revoked. Both are read on every call, so a changed cap holds from
the next call in every process, without a restart. A milieu.toml that does not
parse, or a [budget] key other than daily and monthly, refuses calls until
it is fixed, so that a typo in a cap can’t lift it.
Every call is checked before it is made, against what the agent, the agent it is working for and the installation have spent so far in the day and the month, and charged after, priced from the model list by the model that answered. A cap can therefore be overshot by at most the one call that crossed it.
Consultation
A consulted agent’s calls are charged to it and to the agent that consulted it. A front desk’s budget bounds the whole of a request to it, advisors included, and an advisor’s budget bounds that advisor however it is reached.
When a budget is spent
The call is refused: the run ends with stop reason
budget, REST answers 429 with Retry-After, and A2A answers -32000 (QUOTA_EXCEEDED) with
retryAfter in the error data, until the period resets at 00:00 UTC (or the
first of the month). The message says when, not how much was spent or by whom.
A consulted agent whose own budget is spent is reported to the consulting agent
as a failed consultation, so a front desk can say that advisor is unavailable.
Unpriced models
A model the list cannot price is never counted as free.
While any budget is set, after such a model has answered, calls are refused
until the list names it. milieu budget lists the models concerned.
The spend counter
The counter is var/audit/spend.db, one row per payer and period, shared by
every process on the installation and owner-only. Spend is counted whether or
not a budget is set, so a cap set mid-month starts from what has already been
spent. milieu budget shows today’s and this month’s spend against each cap:
./milieu budget # the installation, and every agent with a cap or spend
./milieu budget --jsonmilieu llm check is not metered: it is a diagnostic that costs a few tokens.
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 by using a bearer key over TLS. |
| blob | A content-addressed (SHA-256) attachment in var/blobs/, shared across agents by digest. |