{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://milieu.im/schema/v1/milieu.schema.json",
  "title": "Milieu manifest (milieu.dev/v1)",
  "description": "One document of a milieu.yaml manifest, the declarative input to `milieu apply`. A manifest is multi-document YAML and every document is validated independently against this schema. Reconcile is upsert, no prune: declared resources are set to match, resources absent from the manifest are never deleted. Secrets are never manifest fields — apply mints them and reports them once.",
  "type": ["object", "null"],
  "required": ["apiVersion", "kind", "metadata"],
  "additionalProperties": false,
  "properties": {
    "apiVersion": {
      "const": "milieu.dev/v1",
      "description": "The manifest schema version. Any other value is refused by the parser."
    },
    "kind": {
      "description": "The resource type this document declares.",
      "enum": ["Agent", "Project", "Knowledge", "Gateway", "API", "Web", "Settings", "Schedule", "Organisation", "Ontology", "Policy"]
    },
    "metadata": { "$ref": "#/definitions/metadata" },
    "spec": {
      "type": "object",
      "description": "The kind-specific body. Its shape is constrained once `kind` is known."
    }
  },

  "allOf": [
    {
      "if": { "properties": { "kind": { "const": "Agent" } }, "required": ["kind"] },
      "then": {
        "properties": {
          "metadata": { "$ref": "#/definitions/codeMetadata" },
          "spec": { "$ref": "#/definitions/agentSpec" }
        }
      }
    },
    {
      "if": { "properties": { "kind": { "const": "Project" } }, "required": ["kind"] },
      "then": {
        "properties": {
          "metadata": { "$ref": "#/definitions/codeMetadata" },
          "spec": { "$ref": "#/definitions/projectSpec" }
        }
      }
    },
    {
      "if": { "properties": { "kind": { "const": "Knowledge" } }, "required": ["kind"] },
      "then": { "properties": { "spec": { "$ref": "#/definitions/knowledgeSpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "Gateway" } }, "required": ["kind"] },
      "then": { "properties": { "spec": { "$ref": "#/definitions/gatewaySpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "API" } }, "required": ["kind"] },
      "then": { "properties": { "spec": { "$ref": "#/definitions/apiSpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "Web" } }, "required": ["kind"] },
      "then": { "properties": { "spec": { "$ref": "#/definitions/webSpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "Settings" } }, "required": ["kind"] },
      "then": { "properties": { "spec": { "$ref": "#/definitions/settingsSpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "Ontology" } }, "required": ["kind"] },
      "then": { "required": ["spec"], "properties": { "spec": { "$ref": "#/definitions/ontologySpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "Policy" } }, "required": ["kind"] },
      "then": {
        "required": ["spec"],
        "properties": {
          "metadata": { "$ref": "#/definitions/policyMetadata" },
          "spec": { "$ref": "#/definitions/policySpec" }
        }
      }
    },
    {
      "if": { "properties": { "kind": { "const": "Organisation" } }, "required": ["kind"] },
      "then": { "properties": { "spec": { "$ref": "#/definitions/organisationSpec" } } }
    },
    {
      "if": { "properties": { "kind": { "const": "Schedule" } }, "required": ["kind"] },
      "then": {
        "required": ["spec"],
        "properties": {
          "metadata": { "$ref": "#/definitions/scheduleMetadata" },
          "spec": { "$ref": "#/definitions/scheduleSpec" }
        }
      }
    }
  ],

  "definitions": {
    "metadata": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name"],
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "The resource's name. Required on every document."
        }
      }
    },

    "codeMetadata": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name"],
      "properties": {
        "name": {
          "$ref": "#/definitions/code",
          "description": "The agent or project code. It becomes a single path segment under var/, so it is constrained to lowercase letters, digits, underscore and hyphen, starting with a letter or digit."
        }
      }
    },

    "scheduleMetadata": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name"],
      "properties": {
        "name": {
          "type": "string",
          "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
          "description": "The schedule's name. It is also the topic its run transcripts are filed under.",
          "examples": ["nightly-audit"]
        }
      }
    },

    "policyMetadata": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name"],
      "properties": {
        "name": {
          "$ref": "#/definitions/code",
          "description": "The policy document's name. Apply writes it to etc/policies/<name>.cedar, or etc/policies/gateway/<name>.cedar for the gateway's.",
          "examples": ["no-push-on-friday"]
        }
      }
    },

    "code": {
      "type": "string",
      "pattern": "^[a-z0-9][a-z0-9_-]*$",
      "description": "A safe single path segment: lowercase letters, digits, underscore and hyphen, starting with a letter or digit."
    },

    "path": {
      "type": "string",
      "minLength": 1,
      "description": "A filesystem path, resolved relative to the manifest file."
    },

    "permissionRule": {
      "type": "string",
      "pattern": "^[A-Za-z0-9][A-Za-z0-9_.+-]*(\\([^)]*\\))?$",
      "description": "A permission rule, matched on argument tokens rather than a flattened string. `cmd` allows any invocation; `cmd(a b)` requires argv to equal those tokens exactly; `cmd(a b:*)` is a prefix match leaving later args unrestricted; `cmd(:*)` allows any argv; `cmd()` allows only an empty argv. The trailing `:*` constrains the leading tokens but not what follows, so use an exact rule to bound a whole command line.",
      "examples": ["gateway", "knowledge(search:*)", "curl(:*)", "jq(:*)"]
    },

    "agentSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "An agent's FULL desired config. Omitted fields are cleared rather than preserved, with the exception of role and the two briefs, which are left alone when omitted.",
      "properties": {
        "role": {
          "enum": ["administrator", "associate", "assistant", "auditor"],
          "description": "What the agent may do. administrator dispatches work to any agent; associate is the ordinary worker; assistant is narrower in scope; auditor is read-only and may not originate messages. Omitted defaults to associate."
        },
        "name": {
          "type": "string",
          "description": "Display name. Defaults to the metadata name."
        },
        "model": {
          "type": "string",
          "description": "This agent's model, over the installation's default (MILIEU_LLM_MODEL); a skill's own model still wins. A gateway-shaped backend (openai, vertex) takes a vendor/model id, e.g. google/gemini-2.5-flash. Omitted inherits the default."
        },
        "reasoning": {
          "type": "string",
          "enum": ["minimal", "low", "medium", "high", "max"],
          "description": "How much this agent's model reasons before answering, over the provider's default; a skill's own level still wins. Models differ in which levels they take. Backends without the control ignore it."
        },
        "modelOptions": {
          "type": "object",
          "description": "Provider-specific request keys for this agent's model calls, set as given over what milieu builds: temperature, top_p, or Vertex AI's extra_body (e.g. {extra_body: {google: {thinking_config: {thinking_budget: 0}}}} turns Gemini 2.5's thinking off). A skill's own model-options win key by key. model, messages, system, tools and stream are milieu's and refused.",
          "propertyNames": { "not": { "enum": ["model", "messages", "system", "tools", "stream"] } }
        },
        "oversees": {
          "type": "array",
          "description": "Projects this agent oversees. It may act in each, and in every project below it, without being on the roster, and reads their knowledge and memory; it writes nothing to their layers, and a memory it saves there lands in its own. One project at a time.",
          "items": { "$ref": "#/definitions/code" }
        },
        "ontology": { "$ref": "#/definitions/agentOntology" },
        "personality": {
          "$ref": "#/definitions/path",
          "description": "Path to a voice/tone brief. Copied in at apply time and prepended to the system prompt on every run."
        },
        "responsibility": {
          "$ref": "#/definitions/path",
          "description": "Path to a mandate/scope brief. Copied in at apply time and prepended to the system prompt on every run."
        },
        "permissions": {
          "type": "array",
          "description": "The allowlist of CLI patterns and built-in tools this agent may invoke. Anything not listed is refused.",
          "items": { "$ref": "#/definitions/permissionRule" }
        },
        "network": {
          "type": "array",
          "description": "Egress allow entries for the sandbox, as host:port. `[\"*\"]` drops every egress restriction. Per-host filtering is enforced on macOS (sbexec); on Linux (bwrap) egress is currently all-or-nothing.",
          "items": { "type": "string", "minLength": 1 },
          "examples": [["api.github.com:443"]]
        },
        "env": {
          "type": "object",
          "description": "Environment variables for the sandbox. A `$VAR` reference resolves only if the host var is named in MILIEU_ENV_PASSTHROUGH.",
          "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" },
          "additionalProperties": { "type": "string" }
        },
        "mounts": {
          "type": "array",
          "description": "Read-only host directories exposed inside the sandbox at /agent/<target>.",
          "items": { "$ref": "#/definitions/mount" }
        },
        "gateway": { "$ref": "#/definitions/agentGateway" },
        "apiKey": { "$ref": "#/definitions/apiKey" },
        "sessionQuota": { "$ref": "#/definitions/sessionQuota" },
        "budget": {
          "$ref": "#/definitions/budget",
          "description": "Caps what this agent's model calls may cost, including the calls of agents it consults: a front desk's budget bounds the whole of a request to it. Checked before each call, so a cap is overshot by at most one call. Omitted is unlimited."
        },
        "skills": {
          "type": "array",
          "description": "Skills copied into the agent, as either a directory (containing SKILL.md plus resources) or a flat .md file. A nil field means the manifest does not manage skills; `skills: []` means there are none. Only `apply --prune` removes an undeclared skill.",
          "items": { "$ref": "#/definitions/path" }
        },
        "evals": {
          "type": "array",
          "description": "Evaluation suites, one directory per skill and named for the skill it grades: ./agents/mira/evals/respond grades `respond`. Installing replaces rather than merges, and a directory with one unparseable case installs none of it.",
          "items": { "$ref": "#/definitions/path" }
        }
      }
    },

    "agentOntology": {
      "type": "object",
      "additionalProperties": false,
      "description": "What the agent answers for in the domain model. The graph reads it as accountable_for edges; it grants nothing.",
      "properties": {
        "owns": {
          "type": "array",
          "description": "Nodes of the domain model the agent is accountable for, as domain:refund or supplier/parcelco.",
          "items": { "type": "string", "minLength": 1 }
        }
      }
    },

    "mount": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source", "target"],
      "properties": {
        "source": {
          "type": "string",
          "minLength": 1,
          "description": "The host directory to expose. Mounted read-only."
        },
        "target": {
          "type": "string",
          "minLength": 1,
          "description": "The name it appears under inside the sandbox, at /agent/<target>."
        }
      }
    },

    "agentGateway": {
      "type": "object",
      "additionalProperties": false,
      "description": "Enrolls the agent with the trusted-zone broker. Apply issues it a proxy identity; existing identities are left in place so re-apply keeps certs stable.",
      "properties": {
        "endpoint": {
          "type": "string",
          "format": "uri",
          "description": "The gateway's URL.",
          "examples": ["https://127.0.0.1:8443"]
        },
        "identity": {
          "type": "string",
          "minLength": 1,
          "description": "The directory its proxy identity is written to, relative to the agent's host-only proxy dir. Defaults to gateway-id.",
          "examples": ["gateway-id"]
        }
      }
    },

    "apiKey": {
      "type": "object",
      "additionalProperties": false,
      "description": "Mints an API key for this agent. The key is displayed once and never again; a key is minted only if the agent has no active one.",
      "properties": {
        "create": { "type": "boolean" },
        "label": {
          "type": "string",
          "description": "What the key is for, recorded alongside it.",
          "examples": ["ci", "chat-widget"]
        }
      }
    },

    "budget": {
      "type": "object",
      "additionalProperties": false,
      "description": "A cap on model spend in US dollars, per UTC calendar day and month. Each call is priced from the model list (etc/models.json); while a budget applies, a model the list cannot price is refused rather than counted as free. Zero or omitted is unlimited on each.",
      "properties": {
        "daily": { "type": "number", "minimum": 0, "description": "Dollars per UTC day." },
        "monthly": { "type": "number", "minimum": 0, "description": "Dollars per UTC calendar month." }
      }
    },

    "sessionQuota": {
      "type": "object",
      "additionalProperties": false,
      "description": "Bounds the conversations this agent's principal may open over REST and A2A. Zero or omitted is unlimited on each knob independently. \"May open none\" is a revoked key, not a quota of zero.",
      "properties": {
        "maxOpen": {
          "type": "integer",
          "minimum": 0,
          "description": "How many conversations may be open at once."
        },
        "rate": {
          "type": "integer",
          "minimum": 0,
          "description": "Token-bucket arrival rate: how many may be opened per window."
        },
        "window": {
          "$ref": "#/definitions/duration",
          "description": "The window `rate` is measured over. Required when rate is set."
        }
      },
      "allOf": [
        {
          "if": {
            "properties": { "rate": { "type": "integer", "minimum": 1 } },
            "required": ["rate"]
          },
          "then": {
            "required": ["window"],
            "$comment": "apply refuses a rate without a window: `if s.Rate > 0 && s.Window == \"\"`."
          }
        }
      ]
    },

    "duration": {
      "type": "string",
      "pattern": "^[0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h)([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))*$",
      "description": "A Go duration.",
      "examples": ["1h", "30m", "1h30m"]
    },

    "projectSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "A project: a membership-gated scope with its own knowledge and memory layers.",
      "properties": {
        "name": {
          "type": "string",
          "description": "Display name. Defaults to the metadata name."
        },
        "description": { "type": "string" },
        "parent": {
          "$ref": "#/definitions/code",
          "description": "The project this one sits under. A project with children is a department: runs scoped to any project below it also read its knowledge and memory. Must be declared in the manifest or already installed."
        },
        "level": {
          "type": "string",
          "description": "A label for what kind of unit the project is: department, team, account. Nothing branches on it."
        },
        "members": {
          "type": "array",
          "description": "The agent codes that belong to this project. Membership gates the project's knowledge and memory layers.",
          "items": { "$ref": "#/definitions/code" }
        },
        "instructions": {
          "$ref": "#/definitions/path",
          "description": "Path to instructions prepended to runs scoped to this project."
        },
        "charter": {
          "$ref": "#/definitions/path",
          "description": "Path to the project's charter.yaml: its mission and objectives, each objective naming what it advances in a project above or the organisation. Runs scoped to the project are told its mission and objectives."
        },
        "ontology": { "$ref": "#/definitions/projectOntology" },
        "knowledge": {
          "type": "array",
          "description": "Directories seeded into the project's knowledge layer.",
          "items": { "$ref": "#/definitions/knowledgeSource" }
        }
      }
    },

    "projectOntology": {
      "type": "object",
      "additionalProperties": false,
      "description": "The part of the domain model the project works in. The graph reads it as scopes edges; it changes nothing a run reads.",
      "properties": {
        "scope": {
          "type": "array",
          "description": "Nodes of the domain model the project works in, as domain:claim or supplier/parcelco.",
          "items": { "type": "string", "minLength": 1 }
        }
      }
    },

    "organisationSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "The organisation the installation belongs to: global, the root every project sits under. A manifest holds at most one.",
      "properties": {
        "charter": {
          "$ref": "#/definitions/path",
          "description": "Path to the organisation's charter.yaml: its name, vision, mission and objectives. Every run is told the name, mission and vision."
        }
      }
    },

    "ontologySpec": {
      "type": "object",
      "additionalProperties": false,
      "required": ["sources"],
      "description": "Ontology modules: the house domain model, or one project's. The modules a manifest declares for a unit replace what was installed there; a unit the manifest declares none for is left alone.",
      "properties": {
        "project": {
          "$ref": "#/definitions/code",
          "description": "The project these modules hold within. Omitted, they are the house model, in the global layer."
        },
        "sources": {
          "type": "array",
          "description": "Module files, or directories whose .yaml and .yml files are modules (not recursively). Every module is parsed before any is installed.",
          "items": { "$ref": "#/definitions/path" }
        }
      }
    },

    "knowledgeSource": {
      "type": "object",
      "additionalProperties": false,
      "required": ["source"],
      "properties": {
        "source": {
          "$ref": "#/definitions/path",
          "description": "A directory of markdown, relative to the manifest."
        }
      }
    },

    "knowledgeSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "Seeds a knowledge layer from directories of markdown and reindexes it.",
      "properties": {
        "layer": {
          "enum": ["global", "agent"],
          "description": "Which layer to seed. global is shared and ungated; agent is one agent's own. A project's layer is declared on the Project instead. Omitted defaults to global."
        },
        "agent": {
          "$ref": "#/definitions/code",
          "description": "Whose layer to seed. Required when layer is agent."
        },
        "sources": {
          "type": "array",
          "description": "Directories of markdown, relative to the manifest.",
          "items": { "$ref": "#/definitions/path" }
        }
      },
      "allOf": [
        {
          "if": {
            "properties": { "layer": { "const": "agent" } },
            "required": ["layer"]
          },
          "then": {
            "required": ["agent"],
            "$comment": "apply refuses layer=agent without spec.agent."
          }
        }
      ]
    },

    "gatewaySpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "The trusted-zone broker's listener and routes. Apply writes this to etc/gateway.toml and issues a proxy identity for each gateway-enabled agent.",
      "properties": {
        "listen": {
          "type": "string",
          "minLength": 1,
          "description": "The address the gateway listens on.",
          "examples": ["127.0.0.1:8443", "0.0.0.0:8443"]
        },
        "routes": {
          "type": "array",
          "items": { "$ref": "#/definitions/gatewayRoute" }
        }
      }
    },

    "gatewayRoute": {
      "type": "object",
      "additionalProperties": false,
      "required": ["name", "prefix"],
      "description": "One brokered route. The gateway injects the credential, so the agent never holds it.",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "prefix": {
          "type": "string",
          "pattern": "^/",
          "description": "The path prefix agents address this route by.",
          "examples": ["/crm", "/github"]
        },
        "agents": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 },
          "uniqueItems": true,
          "description": "The agents this route admits, by name, or [\"*\"] for every agent with a gateway identity. A route that names none admits none: the gateway refuses every call to it with 403.",
          "examples": [["triage", "support"], ["*"]]
        },
        "backend": {
          "enum": ["rest", "a2a", "mcp"],
          "description": "What is on the other end. rest forwards HTTP; a2a brokers message/send to a remote Agent2Agent peer; mcp brokers a remote Model Context Protocol server. Omitted defaults to rest. gateway.toml also supports cli, sqlite and postgres backends, but those need [route.<backend>] blocks a manifest has no fields for, so a manifest may not declare them."
        },
        "upstream": {
          "type": "string",
          "format": "uri",
          "description": "The upstream URL. Required for rest, a2a and mcp."
        },
        "ontologyClass": {
          "type": "string",
          "minLength": 1,
          "description": "The domain class whose live records this route resolves, as customer or domain:customer. The graph reads it as a resolves edge.",
          "examples": ["customer"]
        },
        "ontologyInstance": {
          "type": "string",
          "minLength": 1,
          "description": "The one party this route exposes, as supplier/parcelco. The graph reads it as an exposes edge.",
          "examples": ["supplier/parcelco"]
        },
        "auth": { "$ref": "#/definitions/gatewayAuth" },
        "mcp": { "$ref": "#/definitions/gatewayMCP" },
        "limit": { "$ref": "#/definitions/gatewayLimit" }
      },
      "allOf": [
        {
          "if": {
            "properties": { "backend": { "const": "mcp" } },
            "required": ["backend"]
          },
          "then": {
            "required": ["mcp"],
            "$comment": "buildService: an mcp backend needs a [route.mcp] block with a tools allowlist."
          }
        },
        {
          "if": {
            "anyOf": [
              { "not": { "required": ["backend"] } },
              {
                "properties": { "backend": { "enum": ["rest", "a2a", "mcp"] } },
                "required": ["backend"]
              }
            ]
          },
          "then": { "required": ["upstream"] }
        }
      ]
    },

    "gatewayAuth": {
      "type": "object",
      "additionalProperties": false,
      "description": "The credential the gateway injects. The token itself is never here — only the name of the env var holding it.",
      "properties": {
        "kind": {
          "enum": ["bearer", "apikey"],
          "description": "How the credential is presented."
        },
        "tokenEnv": {
          "type": "string",
          "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
          "description": "The environment variable holding the secret, read by the gateway process in its own trust zone.",
          "examples": ["CRM_TOKEN", "GH_TOKEN"]
        },
        "header": {
          "type": "string",
          "description": "The header to present it in, when kind is apikey (default Authorization). Refused with bearer, which always sends Authorization."
        }
      },
      "if": {
        "properties": { "kind": { "const": "bearer" } },
        "required": ["kind"]
      },
      "then": {
        "not": { "required": ["header"] },
        "$comment": "auth.New: bearer always sends Authorization, so a header named for it is refused."
      }
    },

    "gatewayLimit": {
      "type": "object",
      "additionalProperties": false,
      "description": "How often the route may be called, counted by the gateway in var/gateway/state.db. agent is each agent's own allowance on the route, so one agent in a loop spends its own and not the others'; route is the total across every agent, which protects the partner behind it. A call over either is refused with 429 and Retry-After before its credential is injected. [route.limit] in gateway.toml.",
      "properties": {
        "agent": { "$ref": "#/definitions/callAllowance" },
        "route": { "$ref": "#/definitions/callAllowance" }
      }
    },

    "callAllowance": {
      "type": "object",
      "additionalProperties": false,
      "description": "Calls by the UTC minute and the UTC day. 0 or absent is unlimited on each; a minute's allowance may not be more than a day's.",
      "properties": {
        "minute": { "type": "integer", "minimum": 0 },
        "day": { "type": "integer", "minimum": 0 }
      }
    },

    "gatewayMCP": {
      "type": "object",
      "additionalProperties": false,
      "required": ["tools"],
      "description": "Configures a backend: mcp route.",
      "properties": {
        "tools": {
          "type": "array",
          "minItems": 1,
          "description": "The operator's allowlist. Required: an MCP server decides what it advertises, this decides what the instance exposes.",
          "items": { "type": "string", "minLength": 1 }
        },
        "pin": {
          "type": "string",
          "description": "The tool-set fingerprint the server must still match.",
          "examples": ["sha256:1f0c..."]
        }
      }
    },

    "apiSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "Enables the REST API control plane. Apply creates its CA and server cert.",
      "properties": {
        "host": {
          "type": "string",
          "description": "An extra SAN for the server certificate.",
          "examples": ["localhost", "api.example.com"]
        }
      }
    },

    "webSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "Enables the browser admin portal. Apply creates its CA, server cert and operator account.",
      "properties": {
        "host": {
          "type": "string",
          "description": "An extra SAN for the server certificate.",
          "examples": ["portal.example.com"]
        },
        "operator": {
          "type": "string",
          "minLength": 1,
          "description": "The account that may sign in. The portal refuses to start without one. The password is deliberately not a manifest field: apply takes it out of band or generates and reports it once, and an existing operator is never reset by a re-apply."
        }
      }
    },

    "settingsSpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "The installation's operator settings. Apply writes them to etc/milieu.toml, which every milieu process reads; an environment variable overrides one for a single process (milieu config shows which applies). The document is the settings whole: one it leaves out returns to its default, except budget, which is kept unless declared. Secrets are not settings and are never written there.",
      "properties": {
        "sandbox": {
          "enum": ["bwrap", "sbexec", "none"],
          "description": "Isolation backend for agent subprocesses. bwrap on Linux, sbexec on macOS. `none` is never auto-selected — set it explicitly to opt out of isolation."
        },
        "llm": {
          "enum": ["claude", "vertex", "openai", "openrouter", "echo"],
          "description": "Model backend. Omitted picks the first available: claude, then vertex (with a Vertex AI llmUrl), then openai, else echo. openrouter is the former name of openai."
        },
        "llmUrl": {
          "type": "string",
          "description": "Base URL. For openai, an OpenAI-compatible API: OpenRouter when omitted, or a gateway. For vertex, Vertex AI's endpoint, called as the machine's identity. [llm] url; MILIEU_LLM_URL."
        },
        "llmModel": {
          "type": "string",
          "description": "Default model id. [llm] model; MILIEU_LLM_MODEL.",
          "examples": ["anthropic/claude-haiku-4.5", "google/gemini-3.8-flash"]
        },
        "embed": {
          "enum": ["openai", "voyage", "vertex"],
          "description": "Embedding backend for vector indexes. Omitted picks the first available."
        },
        "embedUrl": {
          "type": "string",
          "description": "Embeddings base URL: an OpenAI-compatible server for openai, or Vertex AI's publisher-models URL for vertex. [embed] url; MILIEU_EMBED_URL."
        },
        "embedModel": {
          "type": "string",
          "description": "Model for the active embeddings backend.",
          "examples": ["text-embedding-3-small", "voyage-3-lite"]
        },
        "embedBatch": {
          "type": "integer", "minimum": 1,
          "description": "Texts per embeddings request. [embed] batch; MILIEU_EMBED_BATCH."
        },
        "embedDimensions": {
          "type": "integer", "minimum": 1,
          "description": "Vector size, where the model can shorten it (vertex). [embed] dimensions; MILIEU_EMBED_DIM."
        },
        "sandboxPath": {
          "type": "string",
          "description": "The PATH sandboxed commands resolve against, colon-separated. [sandbox] path; MILIEU_SANDBOX_PATH."
        },
        "envPassthrough": {
          "type": "array", "items": { "type": "string" },
          "description": "Host environment variables an agent.toml [env] value may reference; any other expands to empty. [sandbox] env_passthrough; MILIEU_ENV_PASSTHROUGH."
        },
        "maxIterations": {
          "type": "integer", "minimum": 1,
          "description": "Ceiling on a run's tool-use loop, over every skill (default 50). [runs] max_iterations; MILIEU_MAX_ITERATIONS."
        },
        "scheduler": {
          "type": "boolean",
          "description": "Whether declared schedules fire (default false). [scheduler] enabled; MILIEU_SCHEDULER."
        },
        "graphRetrieval": {
          "type": "boolean",
          "description": "Whether knowledge searches reach through the domain model (default true). [graph] retrieval; MILIEU_GRAPH_RETRIEVAL."
        },
        "oauthIssuer": {
          "type": "string",
          "format": "uri",
          "description": "Authorization server whose JWT access tokens the MCP endpoint accepts beside API keys; https unless on a loopback host. Unset: API keys only. [oauth] issuer; MILIEU_OAUTH_ISSUER."
        },
        "oauthAudience": {
          "type": "string",
          "description": "Audience every token must carry, for issuers that cannot bind a token to each agent's MCP endpoint (RFC 8707). Unset: the endpoint's own URI. [oauth] audience; MILIEU_OAUTH_AUDIENCE."
        },
        "logLevel": {
          "enum": ["debug", "info", "warn", "error"],
          "description": "Floor for the long-running services' logs (default info). [log] level; MILIEU_LOG_LEVEL."
        },
        "otel": {
          "type": "boolean",
          "description": "Whether OpenTelemetry is set up; the standard OTEL_* variables configure it. [otel] enabled; OTEL_ENABLED."
        },
        "policyMode": {
          "enum": ["off", "shadow", "enforce"],
          "description": "off (the default); shadow: command, built-in tool, consult, message, project access, MCP and A2A reach and session, gateway route, and admin portal checks are also put to the Cedar policies in etc/policies, and a disagreement is recorded in the audit log, while milieu's own checks still decide; or enforce: the policies decide every check, on the milieu host, in the admin portal and, with its own policies, on the gateway. [policy] mode; MILIEU_POLICY_MODE."
        },
        "approvalWebhook": {
          "type": "string",
          "format": "uri",
          "pattern": "^https?://",
          "description": "A URL told, by a JSON POST, of each request for a person's approval as it opens: its id, agent, call, policies, and the command that grants it. Its bearer token is environment only, MILIEU_APPROVAL_WEBHOOK_TOKEN. [approval] webhook; MILIEU_APPROVAL_WEBHOOK."
        },
        "otelFlushTimeout": {
          "type": "string",
          "pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$",
          "description": "How long milieu ask, act and wake wait at exit to send their telemetry, as a Go duration (default 1s). [otel] cli_flush_timeout; MILIEU_OTEL_FLUSH_TIMEOUT."
        },
        "shutdownGrace": {
          "type": "string",
          "pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$",
          "description": "How long a server (api, mcp, a2a, web, gateway, auth) waits on shutdown for in-flight requests, as a Go duration (default 10s); those still running are then answered 503. A serve command's --shutdown-grace overrides it. [server] shutdown_grace; MILIEU_SHUTDOWN_GRACE."
        },
        "drainDelay": {
          "type": "string",
          "pattern": "^([0-9]+(\\.[0-9]+)?(ns|us|µs|ms|s|m|h))+$",
          "description": "How long a server fails readiness on shutdown before it stops accepting connections, so a load balancer stops routing to it (default none). A serve command's --drain-delay overrides it. [server] drain_delay; MILIEU_DRAIN_DELAY."
        },
        "wakeWorkers": {
          "type": "integer", "minimum": 1,
          "description": "How many agents the daemon wakes at once (default 4). Concurrent runs can each pass a budget check before any is charged, so this also bounds how far a cap can be overshot. A daemon's --workers overrides it. [runs] wake_workers; MILIEU_WAKE_WORKERS."
        },
        "unsignedMessages": {
          "enum": ["deliver", "refuse"],
          "description": "What the router does with a message queued before signing: deliver it with a warning and an audit row (default), or refuse it and keep it in the sender's outbox. Set refuse after an upgrade's queues have drained. [messages] unsigned; MILIEU_UNSIGNED_MESSAGES."
        },
        "auditKeyFile": {
          "type": "string",
          "description": "A file outside MILIEU_HOME (mode 600) holding the audit HMAC key, hex or base64; the key itself never goes in a manifest. [audit] key_file; MILIEU_AUDIT_KEY_FILE."
        },
        "budget": {
          "$ref": "#/definitions/budget",
          "description": "Caps what every model call on the installation may cost, agents' and the rest (drafting, inference, compaction, the eval judge). Written to etc/milieu.toml, which every process reads on each call."
        }
      }
    },

    "scheduleSpec": {
      "type": "object",
      "additionalProperties": false,
      "required": ["agent", "input", "cron"],
      "description": "Recurring work. Declaring a schedule never switches it on: whether anything fires is 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, and nothing is pruned.",
      "properties": {
        "agent": {
          "$ref": "#/definitions/code",
          "description": "The agent that runs it. An auditor may not be scheduled to originate work."
        },
        "skill": {
          "type": "string",
          "description": "The skill to run. Omit to auto-select per run."
        },
        "project": {
          "$ref": "#/definitions/code",
          "description": "Optional scope. Membership is enforced at run time."
        },
        "input": {
          "type": "string",
          "minLength": 1,
          "description": "What to run with. Required: a scheduled run has no client to prompt it."
        },
        "cron": {
          "$ref": "#/definitions/cron",
          "description": "When it fires, in host local time."
        },
        "disabled": {
          "type": "boolean",
          "description": "Declare it without starting it. The field is negative so the common case is the YAML zero value, and a manifest cannot pause a task by omission."
        }
      }
    },

    "policySpec": {
      "type": "object",
      "additionalProperties": false,
      "description": "Cedar policies, written to etc/policies, which policy.mode shadow compares with milieu's checks and enforce lets decide. Exactly one of source and cedar. A policy that does not parse is refused before anything is written. Apply is upsert: a Policy taken out of the manifest leaves its file in place.",
      "oneOf": [
        { "required": ["source"] },
        { "required": ["cedar"] }
      ],
      "properties": {
        "source": {
          "$ref": "#/definitions/path",
          "description": "Path to a .cedar file holding the policies."
        },
        "cedar": {
          "type": "string",
          "minLength": 1,
          "description": "The policies, inline."
        },
        "gateway": {
          "type": "boolean",
          "default": false,
          "description": "The policies are the gateway's, in its own trust zone, written to etc/policies/gateway and checked against the gateway's schema (milieu policy schema --gateway)."
        }
      }
    },

    "cron": {
      "type": "string",
      "pattern": "^(@(hourly|daily|weekly|monthly|yearly)|\\S+(\\s+\\S+){4})$",
      "description": "Five fields — minute hour day-of-month month day-of-week — or a descriptor (@hourly, @daily, @weekly, @monthly, @yearly). Ranges are 0-59, 0-23, 1-31, 1-12, 0-6; month and day-of-week also accept three-letter names. Six-field (Quartz-style) expressions are refused.",
      "examples": ["0 3 * * *", "*/15 * * * *", "@daily"]
    }
  }
}
