Skip to content
Reference

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

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 ls

Bounds 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 + pathDoes
GET /v1/healthliveness: the process is up (unauthenticated)
GET /v1/readyreadiness (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/whoamithe authenticated agent’s identity
GET /v1/skillsthe 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}/cancela 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}/readmark 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}/closeexternal conversations (caller’s own only)
GET /v1/knowledge · /v1/knowledge/item?path= · /v1/knowledge/search?q= (?in=&mode=&type=&tag=&status=&fresh=1&current=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:

RuleMatches
cmdany 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.

ToolGrantPurpose
knowledgeknowledgeQuery the agent’s knowledge base
memorymemoryRead and write the agent’s memory
graphgraph, 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
transcripttranscriptRecall past runs
sessionsession(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
consultconsult(<agent>:*), or consult for any agentPut 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 toolsmcp(<route>[ <tool>]:*)Tools brokered from an external Model Context Protocol (MCP) server, projected with its own schemas as <route>__<tool>
gatewaygatewayBrokered HTTP call. The credential stays in the gateway
codecodeRun a Starlark program (see the following section)
skillnone (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:

InjectedPresent whenReturns
run(cmd, args=[], stdin="")any subprocess command is grantedstruct .exit_code .stdout .stderr
gateway(path, method=, query=, headers=, body=)gateway is grantedstruct .status .body
knowledge(*args) / memory(*args) / transcript(*args) / graph(*args)each granted separatelystruct .ok .text
json, re, time, mathalwaysmodules (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:

ModuleNotes
jsondecode/encode. The shape most API and MCP results arrive in
rePython-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
timeparse_time/from_timestamp/now and the duration units. Subtract two times for a duration. Divide by time.hour for a number
mathThe 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.0

The 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).

Variableetc/milieu.tomlDefaultPurpose
MILIEU_HOME(environment only)$PWD/.milieuRoot state directory
MILIEU_AGENT(environment only)unsetDefault caller agent (override with --as)
MILIEU_APPLIED_BY(environment only)unsetWho milieu apply records as having applied, beside the OS user (override with --by). Claimed, not verified
MILIEU_SANDBOX[sandbox] backendbwrap (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] backendfirst available (claude, then vertex, then openai), else echoLarge language model (LLM) backend: claude, vertex, openai or echo. openrouter is the former name of openai and still works
ANTHROPIC_API_KEY(environment only)unsetClaude API key (enables claude)
MILIEU_LLM_URL[llm] urlhttps://openrouter.ai/api/v1For 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)unsetBearer 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] modelanthropic/claude-haiku-4.5Model id for openai and vertex, for example, openai/gpt-4o, google/gemini-2.5-pro
MILIEU_ENV_PASSTHROUGH[sandbox] env_passthroughunsetComma-separated host var names that agent.toml [env] $VAR references may resolve
MILIEU_WAKE_WORKERS[runs] wake_workers4Agents 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_iterations50Ceiling 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] retrievalonAdds 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] issuerunset (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] audienceeach agent’s MCP resource URIThe 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] levelinfoLog 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] enabledfalseLet 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] backendfirst available of openai, voyageEmbedding backend: openai, voyage, or vertex (vertex only when named)
MILIEU_EMBED_URL[embed] urlunsetFor 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)unsetBearer token for the embeddings endpoint, optional. Without it, vertex uses the machine’s Google identity, or gcloud off Google Cloud
MILIEU_EMBED_BATCH[embed] batch16Texts 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] modelbackend defaultModel 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] dimensions768vertex only: vector size, shortened from the model’s full 3072
VOYAGE_API_KEY(environment only)unsetEnables the voyage backend
OTEL_ENABLED[otel] enabledfalseGate 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] webhookunsetA 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)unsetSent to the approval webhook as a bearer credential
MILIEU_UNSIGNED_MESSAGES[messages] unsigneddeliverWhat 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)unsetThe 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_fileunsetA 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] modeoffshadow 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_timeout1sHow 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_grace10sHow 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_delay0sHow 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 devSets 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 state

Configuration 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. apply mints 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 apply reconciles is recorded: its spec and the content of every file it names are digested, and a declaration whose digest moved gets one apply audit row (with operator, digest, prev_digest) and a snapshot under var/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.
  • Ontology declares domain model modules for the global layer, or for the project it 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.
  • Organisation declares the organisation itself, at most once. Its charter: names the organisation’s charter.yaml, and a Project’s charter: names the project’s. A charter holds name (read from the organisation’s only), vision, mission, and objectives, each with an id, a statement, an optional advances naming an objective above as <project>/<id> or global/<id>, and an optional until date after which it lapses. Apply refuses a charter that does not parse, unknown keys included. Lint checks advances, lapsed objectives, whether a project’s chain composes past the 3072 bytes a run is told, and an objective that advances one whose statement or until an 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 code global is reserved for the organisation, and no project may take it.
  • An Agent’s oversees lets 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 Project may name a parent, 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 Project is 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.
  • Settings is written to etc/milieu.toml, whole: a setting the document leaves out returns to its default, except budget, which is kept unless declared. No token is ever written there.
  • sessionQuota bounds conversations opened over REST, A2A and MCP, because all three admit them for the same principal. It is applied whether or not a kind: API document 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.
  • Web declares the operator’s name only. apply takes a password out of band (milieu setup prompts 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 passwd does that.
  • evals: declares an agent’s evaluation suites: one directory per skill, named for the skill it grades (./agents/mira/evals/respond grades respond, the same convention skills: 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. Like skills:, a nil field means the manifest does not manage them, and evals: [] means there are none. Only --prune removes an undeclared suite.
  • Schedule declares recurring work. Whether anything fires depends on MILIEU_SCHEDULER on 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 with milieu schedule rm.
  • Policy writes Cedar policies to etc/policies/<name>.cedar, or with gateway: true to etc/policies/gateway/<name>.cedar, from source or inline cedar (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: a Policy taken out of the manifest leaves its file, so a forbid stays until someone deletes it. Lint checks each document as milieu policy lint checks a file, with the manifest’s policies as the whole set. Export writes every policy file back as a Policy document. See OPERATIONS → Policies.
  • The kind/spec shape 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.json

Each 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>, and term:<slug> are declared. org:root, project:<code>, and objective:<unit>/<id> are derived from the project tree and the charters. A reference without a prefix is a domain: reference.
  • Predicates. subclass_of, instance_of, part_of, same_as, supersedes, and related_to are built in, and so are the derived pursues and advances. Any other key on an instance is a declared relation, and the edge must run from an instance of its from class to one of its to class, 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 a reason and name an edge a layer above declares.
  • Attachments. about: in a knowledge document’s or a memory’s frontmatter, and acts-on: in a skill’s, name the nodes the object is about. They are read as documents, notes, and acts_on edges from knowledge:<layer>/<concept>, memory:<layer>/<name>, and skill:<agent>/<name>, where <layer> is global, project/<code>, or agent/<code>, and shown from the domain’s end as documented_by, noted_by, and acted_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’s ontology-class (a class whose live records it resolves) and ontology-instance (the one party it exposes), an agent’s [ontology] owns and a project’s [ontology] scope are read the same way, as resolves, exposes, accountable_for, and scopes from route:<name>, agent:<code>, and project:<code>, shown as resolved_via, exposed_by, owned_by, and scoped_by, in every view. In a manifest they are a route’s ontologyClass and ontologyInstance, and ontology: { owns: […] } and ontology: { 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 the gateway tool and its agent.toml has a [gateway] block, and none otherwise. graph orphans lists 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 the graph tool, 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 verified entry is older than the last change to the definition of a node it is about: is labelled by knowledge get and ls (CLI and tool), and reported by lint with the date, the Ontology declaration 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 of graph alone does not carry it) may call graph 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_of between classes (no cycle), or part_of, same_as, or related_to between instances. The edge must not be one the model already has or one a person rejected. A proposal is kept under var/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 accept writes 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=off turns 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 one subclass_of step away, at most five each, after the text matches. A knowledge search appends the documents about: them, and a memory search the notes about: them. A knowledge search also names the route whose ontology-instance exposes a party the query names, last, as a route:<name> instead of a path, and only to an agent granted gateway. Each is held to the search’s filters and labelled reached through the domain model, with , not a text match where a search could have matched the text. milieu knowledge search fuses 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 the graph tool.
  • 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 an Ontology declaration of its modules was applied and that digest moved.
  • Export. graph export writes a view as Turtle or JSON-LD, using rdfs, rdf, owl, and skos for 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 reindex writes each layer’s own declarations into var/global/graph.db, var/projects/<code>/graph.db or var/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, and graph sweep deletes it. graph inferred lists them with the model and document each rests on. --dry-run writes nothing. A pass run twice proposes nothing the second time, because its own edges are in the view it checks against.
  • Lint. milieu lint reports, 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, a resolved_via route the gateway does not declare, related_to making up most of the declared edges, an about: or acts-on: naming a node that is not declared (for a skill, a route, an agent or an agent’s own layer, in no view), an acts-on: or ontology-class naming anything but a class, and an ontology-instance naming 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):

FieldMeaning
seqmonotonic sequence number
agentthe acting agent
cmdcommand or tool name
argsargument tokens
exit_codeprocess/tool exit code
started_at, ended_attimestamps
sandboxnone · bwrap · sbexec · builtin
modelthe model that served the call. Present only on records that called one
usagetoken counts for the call. Present only on records that called a model
skill_versionthe skill’s declared version, on act records only
skill_digestsha256 of the skill as loaded (manifest + bundled resources), so an edit without a version bump is still visible
stopwhy 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
operatorthe 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_digesta 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_digestsha256 of the charter and department context an act run was told (what milieu charter show prints). Present only when either applies
policiesthe 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
approvalthe 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, hashchain fields. hash covers prev_hash + seq + payload
macHMAC-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.json

Because 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.

WhereDeclared asCovers
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.0

Zero 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 --json

milieu llm check is not metered: it is a diagnostic that costs a few tokens.

Glossary

TermMeaning
agentA directory under var/agents/<code>/ with an identity, role, permissions, skills, and its own knowledge/memory layers. Not a long-running process.
roleadministrator / associate / assistant / auditor, recorded in agent.toml. Auditors are read-only and cannot send messages.
skillAn Agent-Skills-format capability (<name>.md or <name>/SKILL.md). The body is the system prompt.
layerOne of global / agent / project. Reads union most-specific-first (agent > project > global) and shadow on clash.
projectA membership-gated workspace with instructions and its own knowledge/memory layers.
union / shadowUnion: merge all applicable layers. Shadow: a more-specific file hides a lower one with the same path/name.
routeCopy delivered messages from senders’ outboxes to recipients’ inboxes.
wakeDrain an inbox, run a skill per unread message, reply.
gatewayA trusted-zone broker that holds secrets and injects them into brokered calls so they never enter the sandbox.
API serverA REST control plane letting external programs drive Milieu as an agent by using a bearer key over TLS.
blobA content-addressed (SHA-256) attachment in var/blobs/, shared across agents by digest.