# Blueprint file

Source: https://submilli.ai/docs/reference/blueprint-file.md

Authorship: AI-assisted.

A Blueprint file is one YAML document. This page describes each of its
top-level keys and their fields, in the order the CLI writes them, and the
errors that refuse a file when it is linted or registered.

## Top-level keys

| Key | Type | Required | Default |
| --- | --- | --- | --- |
| [`kind`](https://submilli.ai/docs/reference/blueprint-file.md#kind) | string | no | absent |
| [`name`](https://submilli.ai/docs/reference/blueprint-file.md#name) | string | yes | |
| [`allow_insecure_http`](https://submilli.ai/docs/reference/blueprint-file.md#allow_insecure_http) | boolean | no | `false` |
| [`idle_timeout`](https://submilli.ai/docs/reference/blueprint-file.md#idle_timeout) | duration | no | `24h` |
| [`vfs`](https://submilli.ai/docs/reference/blueprint-file.md#vfs) | mode name or map | no | `ephemeral` |
| [`secrets`](https://submilli.ai/docs/reference/blueprint-file.md#secrets) | map | no | empty |
| [`variables`](https://submilli.ai/docs/reference/blueprint-file.md#variables) | map | no | empty |
| [`packages`](https://submilli.ai/docs/reference/blueprint-file.md#packages) | list | no | empty |
| [`auth_proxy`](https://submilli.ai/docs/reference/blueprint-file.md#auth_proxy) | list | no | empty |
| [`git`](https://submilli.ai/docs/reference/blueprint-file.md#git) | map | no | absent: `submilli:git` disabled |
| [`default`](https://submilli.ai/docs/reference/blueprint-file.md#default) | `deny`, `allow` | no | `deny` |
| [`permissions`](https://submilli.ai/docs/reference/blueprint-file.md#permissions) | map | no | empty |
| [`mcp`](https://submilli.ai/docs/reference/blueprint-file.md#mcp) | map | no | empty |
| [`llm`](https://submilli.ai/docs/reference/blueprint-file.md#llm) | map | no | empty |
| [`embedding`](https://submilli.ai/docs/reference/blueprint-file.md#embedding) | map | no | empty |

Any other top-level key is a parse error, and so is an unknown field in any
block. A key repeated at the top level or within one block's fields is
refused. Repeated names in `secrets`, `variables`, `permissions`, `mcp`,
`llm.providers`, `llm.models`, `embedding.providers`,
`embedding.models`, and the map form of `packages` are also
refused. The same rule applies to MCP and auth-proxy header maps,
auth-proxy query maps, and paths in `vfs.mounts`.

```text
error: case.yaml: blueprint parse error: unknown field `permision`, expected one of `kind`, `name`, `allow_insecure_http`, `idle_timeout`, `vfs`, `secrets`, `variables`, `packages`, `auth_proxy`, `git`, `default`, `permissions`, `mcp`, `llm`, `embedding` at line 3 column 1
```

```text
error: case.yaml: blueprint parse error: duplicate field `name`
```

The CLI's editing commands (`submilli blueprint secret`, `variable`,
`auth-proxy`, `add-package`, `git`, `capability`, `add-mcp`) rewrite the
whole file: keys in the order of the table, map entries sorted by key,
durations in seconds (`'3600s'`), sizes in bytes, and comments removed.

### Example

Every key except `allow_insecure_http` and `packages`, as the CLI writes it:

```yaml title="blueprint.yaml"
kind: blueprint
name: support
idle_timeout: '3600s'
vfs:
  mode: per_session
  size_limit: 104857600
  cwd: /notes
  mounts:
    /notes:
      mode: named
      volume: notes
      subPath: users/${vars.customerId}
secrets:
  ANTHROPIC_API_KEY:
    store: anthropic_api_key
  LINEAR_API_KEY:
    harness:
      required: true
  STATUS_TOKEN:
    store: status_token
variables:
  customerId:
    required: true
  region:
    default: us
auth_proxy:
- host: status.acme.com
  auth:
    bearer: STATUS_TOKEN
git:
  identity:
    name: Support Agent (${vars.customerId})
    email: agent@acme.example
default: deny
permissions:
  main:
  - capability: http.get
    filter: host == "status.acme.com"
    action: allow
mcp:
  linear:
    url: https://mcp.linear.app/mcp
    headers:
      Authorization: Bearer ${secrets.LINEAR_API_KEY}
llm:
  providers:
    anthropic:
      type: anthropic
      api_key: ${secrets.ANTHROPIC_API_KEY}
  models:
    claude-haiku-4-5:
      provider: anthropic
      context_window: 200000
      output_reserve: 4000
      description: Cheap and fast.
embedding:
  providers:
    voyage:
      type: voyage
      api_key: ${secrets.VOYAGE_API_KEY}
  models:
    notes-embedding:
      provider: voyage
      model: voyage-3.5
      dimensions: 1024
```

### References to variables and secrets

`${vars.NAME}` is replaced by the session's value of a declared
[variable](https://submilli.ai/docs/reference/blueprint-file.md#variables). `${secrets.NAME}` is replaced, outside the program,
by the value of a declared [secret](https://submilli.ai/docs/reference/blueprint-file.md#secrets). Each may appear only in these
fields. Elsewhere the text is taken literally. A reference to a name the
file doesn't declare is an error.

| Reference | Fields |
| --- | --- |
| `${vars.NAME}` | `permissions` rule `filter`; `git.identity.name`, `git.identity.email`, `git.username`; `vfs.subPath`, `vfs.cwd`, and a mount's `subPath`, as a whole path component |
| `${secrets.NAME}` | `auth_proxy` `headers` and `query` values; `mcp` `headers` values; `mcp` `auth` `client_id`, `authorization_endpoint`, `token_endpoint`, `scopes`; `llm.providers` `api_key` and `base_url`; `embedding.providers` `api_key` and `base_url` |
| A secret name, bare | `auth_proxy` `auth.bearer` and `auth.basic.password` |

## kind

| | |
| --- | --- |
| Type | string |
| Required | no |
| Allowed values | `blueprint` |

```text
error: case.yaml: invalid blueprint kind: unknown kind 'policy': expected `blueprint` (or omit `kind`)
```

## name

| | |
| --- | --- |
| Type | string |
| Required | yes |
| Constraints | non-empty, with only ASCII letters, digits, `_` and `-` |

The name a server registers the Blueprint under and an application names
when it opens a session. `submilli server blueprint apply` registers the
file under this name, replacing a Blueprint registered with it.

```text
error: case.yaml: blueprint parse error: missing field `name`
```

```text
error: case.yaml: invalid blueprint name: blueprint name 'my agent' contains characters outside [A-Za-z0-9_-]
```

## allow_insecure_http

| | |
| --- | --- |
| Type | boolean |
| Default | `false` |

`true` permits `http://` requests through `submilli:http`, including
Package calls and downloads. With `false`, only `https://` is permitted. An
`http://` request to a host that has an [`auth_proxy`](https://submilli.ai/docs/reference/blueprint-file.md#auth_proxy) rule is
permitted only when that rule's own `allow_insecure_http` is `true` as well.
The check applies to every redirect too.

## idle_timeout

| | |
| --- | --- |
| Type | duration, a whole number followed by `s`, `m` or `h` |
| Default | `24h` |

How long a session may go unused before the server closes it. Closing a
session deletes its `submilli:session` state and its `per_session` files.
The CLI writes the value back in seconds, so `1h` becomes `'3600s'`.

```text
error: case.yaml: blueprint parse error: idle_timeout: duration '10' needs a unit (s, m, or h)
```

```text
error: case.yaml: blueprint parse error: idle_timeout: unknown duration unit 'd' in '1d' (use s, m, or h)
```

## vfs

The program's filesystem. Either a mode name, `vfs: per_session`, or a map
with `mode` and that mode's fields.

| `mode` | The program's `/` |
| --- | --- |
| `none` | No filesystem. Every `submilli:fs` call fails |
| `ephemeral` (default) | A directory created for the run and deleted when it returns |
| `per_session` | A directory that lasts as long as the session |
| `named` | A named volume declared in the server's config. It is kept across sessions and restarts, and shared with every Blueprint that names it |

| Field | Type | Modes | Default | Constraints |
| --- | --- | --- | --- | --- |
| `mode` | `none`, `ephemeral`, `per_session`, `named` | all | `ephemeral` | |
| `size_limit` | size | `ephemeral`, `per_session` | no limit | A named volume's limit is set in the server's config |
| `volume` | string | `named` | | Required under `named`. The non-empty name of a volume declared on the server |
| `subPath` | string | `named` | the volume's root | A relative, normalized path inside the volume |
| `access` | `read_only`, `read_write` | `named` | the server's declaration | Can only narrow the server's declaration |
| `cwd` | string | `ephemeral`, `per_session`, `named` | `/` | An absolute, normalized guest path |
| `mounts` | map | `ephemeral`, `per_session`, `named` | none | See [Mounts](https://submilli.ai/docs/reference/blueprint-file.md#mounts) |
| `grace_period` | duration | `per_session` | | Accepted and ignored |
| `path_limit` | number | `ephemeral`, `per_session` | | Accepted and ignored |

A field that doesn't belong to the block's mode is refused, with a message
naming the field and the mode:

```text
error: case.yaml: blueprint parse error: vfs.size_limit: invalid vfs config: 'size_limit' is not valid for vfs mode 'named'; a named volume's size limit is set by the operator where the server declares it at line 5 column 15
```

```text
error: case.yaml: blueprint parse error: vfs: invalid vfs config: `volume` is only valid under `mode: named`, and this vfs block has no `mode:` (it defaults to `ephemeral`); add `mode: named` to this vfs block, or move the volume under `mounts:` to keep an ephemeral root at line 3 column 3
```

```text
error: case.yaml: blueprint parse error: vfs: invalid vfs config: vfs mode 'named' requires a 'volume': the name of a volume the operator declared in the server config at line 3 column 3
```

Two retired forms are refused with the replacement: mode `persistent`, and
the `path` key.

```text
error: case.yaml: blueprint parse error: vfs: vfs mode `persistent` was removed: write `mode: named` and keep the `volume:` line (`vfs: {mode: named, volume: <name>}`). A named volume keeps its files across calls, sessions and restarts as before; leave `access:` out to keep the access the server declares for it at line 2 column 6
```

```text
error: case.yaml: blueprint parse error: vfs.path: the `path` key is retired: a blueprint can no longer name a host directory. Use `volume: <name>` under `mode: named` — the operator declares each volume by name in the server config at line 4 column 9
```

`submilli run` refuses a Blueprint that names a volume, as the root or as a
mount, because volumes are declared only in a server's config.

### Sizes

A size is a byte count, `104857600`, or a whole number followed by a unit:
`B`, `KB`, `MB`, `GB`, `TB`, with `K`, `M`, `G`, `T` and `KiB`, `MiB`,
`GiB`, `TiB` accepted as the same units. Units are 1024-based and
case-insensitive, so `100MB` is 104,857,600 bytes.

```text
error: case.yaml: blueprint parse error: vfs: invalid vfs config: unknown size unit 'XB' in '10XB' (use B, KB, MB, GB, TB) at line 3 column 3
```

Under `per_session`, `size_limit` covers all of the session's files. A write
that would pass it throws `QuotaExceededError`.

### Mounts

`mounts` maps an absolute guest path to a named volume mounted there, below
the root.

```yaml title="blueprint.yaml (fragment)"
vfs:
  mode: per_session
  mounts:
    /memory:
      mode: named
      volume: project-memory
      access: read_write
    /handbook:
      mode: named
      volume: company-handbook
      access: read_only
```

| Field | Type | Required | Default | Constraints |
| --- | --- | --- | --- | --- |
| `mode` | `named` | yes | | The only mount mode |
| `volume` | string | yes | | The non-empty name of a volume declared on the server |
| `subPath` | string | no | the volume's root | A relative, normalized path inside the volume |
| `access` | `read_only`, `read_write` | no | the server's declaration | Can only narrow the server's declaration |

A mount path:

- is absolute and is not `/`
- has at most 4,096 bytes and 64 components
- contains only ASCII letters, digits, `.`, `_`, `-` and `/`
- has no empty, `.` or `..` component, no trailing `/`, and no component ending in `.`
- names no `.git` component
- is not inside another mount, does not contain one, and differs from every
  other mount by more than letter case

A Blueprint has at most 16 mounts. The same volume may be mounted at several
paths. `mounts` under `mode: none` is refused.

```text
error: case.yaml: blueprint parse error: vfs.mounts./a/b: mount `/a/b` is inside mount `/a`; mounts may not nest — mount the volumes side by side, such as `/a` and `/b` at line 5 column 11
```

```text
error: case.yaml: blueprint parse error: vfs.mounts./a: a mount needs `mode: named`: mounts are named volumes the operator declares in the server config at line 4 column 9
```

```text
error: case.yaml: blueprint parse error: vfs.mounts./a.size_limit: unknown field `size_limit` in a mount, expected one of `mode`, `volume`, `access`, `subPath` at line 4 column 46
```

### subPath and cwd

`subPath` (on a named root or a mount) is relative to the volume's root. The
program sees the selected directory as the root of that volume. `cwd` is an
absolute guest path, and the directory relative paths resolve against for
`submilli:fs`, Packages, and `http.download` destinations. `fs.cwd()`
returns it. In both, `${vars.NAME}` may stand for one whole component, and
must resolve to a non-empty name containing no `/`, `\` or NUL that is not
`.` or `..`. A path is at most 4,096 bytes, before and after substitution.

```yaml title="blueprint.yaml (fragment)"
variables:
  userId:
    required: true
vfs:
  mode: ephemeral
  cwd: /notes
  mounts:
    /notes:
      mode: named
      volume: notes
      subPath: users/${vars.userId}
```

```text
error: case.yaml: invalid vfs config: must be a relative volume path of at most 4096 bytes
```

```text
error: case.yaml: invalid vfs config: use ${vars.NAME} as a whole path component
```

```text
error: case.yaml: invalid vfs config: must be an absolute guest path of at most 4096 bytes
```

## secrets

A map from secret name to the source its value comes from.
`${secrets.NAME}` and `auth_proxy` `auth` fields reference a name declared
here. The file holds names, never values.

| Source | YAML | The value comes from |
| --- | --- | --- |
| `store` | `store: <key>` | The secret store under `<key>`. On a server, that is the server's encrypted store (`submilli server secret put`), and in `submilli run`, the local store (`submilli secret put`). Read on each use. |
| `harness` | `harness: {}` or `harness: {required: true}` | The application, when it opens or rebinds a session |

| Field | Type | Default | |
| --- | --- | --- | --- |
| `harness.required` | boolean | `false` | `true` refuses a session that doesn't supply a non-empty value |

```yaml title="blueprint.yaml (fragment)"
secrets:
  BILLING_API_KEY:
    store: billing_api_key
  LINEAR_API_KEY:
    harness:
      required: true
```

A session that supplies a value for a name the file doesn't declare, or for
a `store` secret, is refused. An empty supplied value counts as absent.

```text
error: case.yaml: blueprint parse error: secrets: a secret needs exactly one source: store / harness at line 3 column 3
```

```text
error: case.yaml: blueprint parse error: secrets.A: unknown field `env`, expected `store` or `harness` at line 4 column 5
```

Registration on a server checks that every `store` secret has a value in
the server's store. See [Registration](https://submilli.ai/docs/reference/blueprint-file.md#registration).

## variables

A map from variable name to its rule. An application supplies variable
values, as strings, when it opens a session. The program can't read or
change them.

| Field | Type | Default | Constraints |
| --- | --- | --- | --- |
| `required` | boolean | `false` | `true` refuses a session that doesn't supply a non-empty value |
| `default` | string | none | The value bound when the session supplies none. Can't be combined with `required: true` |

An optional variable with no value and no default is unbound, and a filter
comparing against it doesn't match. A session that supplies a name the file
doesn't declare is refused.

```text
error: case.yaml: invalid variables config: variable 'a': `required: true` and `default:` are mutually exclusive
```

```text
error: case.yaml: invalid variables config: permissions for 'main': filter references undeclared variable '${vars.p}'
```

## Packages

A list of Package names a program may import. A map whose keys are the
names is accepted as well, and its values are ignored.

Each name:

- has the scoped form `@org/name`, with exactly one `/`
- has an `org` of 1 to 39 ASCII letters, digits, and single inner hyphens,
  with no leading or trailing hyphen
- has a `name` of ASCII letters, digits, `.`, `_` and `-`, other than `.`
  and `..`
- is not a `submilli:*` module and not an `@mcp/*` Package
- is listed once

```text
error: case.yaml: invalid packages config: package `@acme.co/billing` scope `@acme.co` must be a GitHub org: ASCII letters, digits, and single internal hyphens only (no dots), 1–39 characters
```

```text
error: case.yaml: invalid packages config: `@mcp/linear` is an MCP virtual package; declare the server in `mcp:` instead
```

```text
error: case.yaml: blueprint parse error: duplicate package `@acme/a` in packages:
```

`submilli blueprint lint` checks each Package against the local Package
store, and registration against the server's:

```text
error: case.yaml: cannot validate package `@acme/billing` capabilities: package `@acme/billing` was not found in /…/packages; no packages are available
```

## auth_proxy

A list of rules, each adding credentials to outbound `submilli:http`
requests to one host. For a request, the first rule whose `host` equals the
request's host applies. The permission check runs before it. A request that
received credentials follows a redirect only to the same scheme, host, and
port.

| Field | Type | Required | Default | Constraints |
| --- | --- | --- | --- | --- |
| `host` | string | yes | | Matched exactly |
| `allow_insecure_http` | boolean | no | `false` | Also needs the top-level [`allow_insecure_http`](https://submilli.ai/docs/reference/blueprint-file.md#allow_insecure_http) |
| `auth.bearer` | secret name | one of `auth`, `headers`, `query` | | Sends `Authorization: Bearer <value>` |
| `auth.basic.username` | string | with `auth.basic` | | A literal |
| `auth.basic.password` | secret name | with `auth.basic` | | Sends `Authorization: Basic <base64(username:value)>` |
| `headers` | map of name to string | one of `auth`, `headers`, `query` | | Values may hold `${secrets.NAME}`. No `Authorization` header alongside `auth` |
| `query` | map of name to string | one of `auth`, `headers`, `query` | | Values may hold `${secrets.NAME}` |

`auth` sets exactly one of `bearer` and `basic`.

```yaml title="blueprint.yaml (fragment)"
auth_proxy:
- host: status.acme.com
  auth:
    bearer: STATUS_TOKEN
- host: legacy.acme.com
  headers:
    X-Api-Key: ${secrets.LEGACY_KEY}
```

```text
error: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' must set auth, headers, and/or query
```

```text
error: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' must set exactly one of `auth.bearer` or `auth.basic`
```

```text
error: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' sets both `auth:` and an explicit `Authorization` header — use one or the other
```

```text
error: case.yaml: invalid auth_proxy config: auth_proxy rule for host 'a.com' references undeclared secret 'K'
```

## git

The commit identity for `submilli:git`. Without a `git` key, programs can't
import `submilli:git`.

| Field | Type | Required | Constraints |
| --- | --- | --- | --- |
| `identity.name` | string | yes | Non-empty, with no control characters, `<` or `>` |
| `identity.email` | string | yes | Non-empty, with no control characters, `<` or `>` |
| `username` | string | no | Non-empty, with no control characters or `:`. The HTTPS username for private repositories, sent with the secret named `GIT_TOKEN` |

Each value may hold `${vars.NAME}`, resolved when the session opens. No
other `${…}` reference is accepted.

```yaml title="blueprint.yaml (fragment)"
git:
  identity:
    name: Support Agent (${vars.customerId})
    email: agent@acme.example
  username: agent
```

```text
error: case.yaml: blueprint parse error: git: missing field `identity` at line 3 column 3
```

```text
error: case.yaml: invalid git config: only ${vars.NAME} references are supported
```

```text
error: case.yaml: invalid git config: must be nonempty and contain no control characters or identity delimiters
```

## default

| | |
| --- | --- |
| Type | string |
| Allowed values | `deny`, `allow` |
| Default | `deny` |

The action for a capability check that no [`permissions`](https://submilli.ai/docs/reference/blueprint-file.md#permissions)
rule matches.

```text
error: case.yaml: blueprint parse error: default: unknown variant `block`, expected one of … at line 2 column 10
```

## permissions

A map from caller to an ordered list of rules. The caller is `main` for the
program, or a Package name for that Package's own calls.

| Field | Type | Required |
| --- | --- | --- |
| `name` | string, non-empty | no |
| `capability` | string, non-empty | yes |
| `filter` | filter expression | no |
| `action` | `allow`, `deny` | yes |

`name` is an optional label for the rule. It must be unique within a caller's
list (`blueprint lint` reports a repeat as an error). It gives the rule a
stable name to refer to instead of its position in the list.

```yaml title="blueprint.yaml (fragment)"
permissions:
  main:
  - capability: acme.com/credits.apply
    filter: customerId == ${vars.customerId} and customerClass == "premium"
    action: allow
```

How rules are matched, and every capability and its fields, are in
[Permissions](https://submilli.ai/docs/reference/permissions.md). The filter grammar is in
[Filter language](https://submilli.ai/docs/reference/filter-language.md). A filter is
parsed with the file, so a malformed one is a parse error:

```text
error: case.yaml: blueprint parse error: permissions.main[0]: invalid filter `path ===`: expected `==`; a single `=` is not an operator
  path ===
         ^ at line 4 column 5
```

## mcp

A map from a server name, chosen by the file, to an outbound MCP server.
The name becomes the Package `@mcp/<name>` and the capability
`mcp.<name>`.

| Field | Type | Required | Default |
| --- | --- | --- | --- |
| `url` | string | yes | |
| `transport` | `streamable_http`, `sse` | no | `streamable_http` |
| `headers` | map of name to string | no | none |
| `auth` | map with `type: oauth2` | no | none |

```yaml title="blueprint.yaml (fragment)"
mcp:
  linear:
    url: https://mcp.linear.app/mcp
    auth:
      type: oauth2
```

Every field, the OAuth fields, and the errors are in
[MCP servers](https://submilli.ai/docs/reference/mcp-servers.md).

## llm

The model providers `submilli:llm` reaches and the models a program may
name. A model this block doesn't declare can't be called.

| Field | Type | Default |
| --- | --- | --- |
| `providers` | map from provider name to [provider](https://submilli.ai/docs/reference/blueprint-file.md#providers) | empty |
| `models` | map from model name to [model](https://submilli.ai/docs/reference/blueprint-file.md#models) | empty |

```yaml title="blueprint.yaml (fragment)"
llm:
  providers:
    anthropic:
      type: anthropic
      api_key: ${secrets.ANTHROPIC_API_KEY}
  models:
    claude-haiku-4-5:
      provider: anthropic
      output_reserve: 4000
      description: "Cheap and fast; use for bulk per-item classification."
    claude-sonnet-5:
      provider: anthropic
```

The block has no budget fields. Token budgets are server settings. See
[Server settings](https://submilli.ai/docs/reference/server-settings.md).

### providers

| Field | Type | Required | Default | Constraints |
| --- | --- | --- | --- | --- |
| `type` | `anthropic`, `google`, `openai`, `openai-compatible` | yes | | |
| `base_url` | string | for `openai-compatible` | the provider's own endpoint | An absolute `https://` URL that is not `localhost` or a loopback, unspecified, link-local, private, broadcast, or carrier-grade NAT address literal. May hold `${secrets.NAME}` |
| `api_key` | string | no | none | Normally `${secrets.NAME}` |
| `supports_structured_outputs` | boolean | no | `true` | `false` sends no JSON Schema with typed calls |

```text
error: case.yaml: invalid llm config: llm provider 'p': unknown type 'mistral' (use one of: anthropic, google, openai, openai-compatible)
```

```text
error: case.yaml: invalid llm config: llm provider 'p': type 'openai-compatible' has no default endpoint; set 'base_url' to the https:// URL of the endpoint
```

```text
error: case.yaml: invalid llm config: llm provider 'p': base_url 'http://api.x.com/v1' uses the 'http' scheme; the API key travels in the Authorization header, so the endpoint must be https://
```

```text
error: case.yaml: invalid llm config: llm provider 'p': base_url 'https://10.0.0.5/v1' resolves to loopback, link-local, or private address space, which a credentialed client must not be pointed at; use the endpoint's public https:// hostname
```

```text
error: case.yaml: invalid llm config: llm provider 'p' references undeclared secret 'K'; add it under 'secrets:'
```

### models

The key is the name a program passes to `llm.call`, `llm.batch`, and the
name `llm.models()` returns.

| Field | Type | Required | Default | Constraints |
| --- | --- | --- | --- | --- |
| `provider` | string | yes | | A key of `llm.providers` |
| `context_window` | non-negative integer | no | unknown | Returned by `models()` as `contextWindow` |
| `output_reserve` | non-negative integer | no | 64,000 reserved, no output cap sent | Output tokens per prompt |
| `description` | string | no | none | One line of at most 512 characters, with no control or invisible formatting characters. Returned by `models()` |

`output_reserve` is counted against the run's and the server's token budgets
for each prompt, with the prompt's estimated size, before the prompt is
sent. A prompt that doesn't fit is refused with `QuotaExceededError`
without being sent. When set, it is also sent as the request's output cap.
When absent, 64,000 tokens are reserved and the request sets no cap, except
Anthropic requests, which always carry one and use 4,096.

```text
error: case.yaml: invalid llm config: llm model 'm' names undeclared provider 'p'; declare it under 'llm.providers:' or point the model at one of: (none declared)
```

```text
error: case.yaml: invalid llm config: llm model 'm': description contains a newline; it reaches a model's selection reasoning verbatim, so keep it to a single line of printable text
```

```text
error: case.yaml: blueprint parse error: llm.models.m.output_reserve: invalid type: string "4k", expected u64 at line 6 column 38
```

An `llm.call` rule whose filter tests `model == "…"` must name a declared
model:

```text
error: case.yaml: invalid llm config: caller 'main': permission filter names undeclared llm model 'gpt-9'; declare it under 'llm.models:' or filter on one of: m
```

## embedding

The embedding providers `submilli:embedding` reaches and the model aliases a
program may name. An alias this block doesn't declare can't be used.

| Field | Type | Default |
| --- | --- | --- |
| `providers` | map from provider name to [provider](https://submilli.ai/docs/reference/blueprint-file.md#embedding-providers) | empty |
| `models` | map from alias to [alias](https://submilli.ai/docs/reference/blueprint-file.md#embedding-models) | empty |

```yaml title="blueprint.yaml (fragment)"
embedding:
  providers:
    voyage:
      type: voyage
      api_key: ${secrets.VOYAGE_API_KEY}
  models:
    notes-embedding:
      provider: voyage
      model: voyage-3.5
      dimensions: 1024
      description: "Embeds notes for semantic search."
```

The block has no budget fields. Embedding budgets are server settings; see
[Server settings](https://submilli.ai/docs/reference/server-settings.md).

### Embedding providers

| Field | Type | Required | Default | Constraints |
| --- | --- | --- | --- | --- |
| `type` | `voyage`, `openai`, `google`, `jina`, `huggingface` | yes | | |
| `base_url` | string | for a Hugging Face dedicated endpoint | the provider's own endpoint | An absolute `https://` URL; not `localhost`, nor a loopback, unspecified, link-local, private, broadcast, or carrier-grade NAT address literal. May hold `${secrets.NAME}` |
| `api_key` | string | yes, except for `huggingface` with a `base_url` | none | `${secrets.NAME}`; a literal key is refused |

A `huggingface` provider without `base_url` calls the shared Inference
Providers router. With `base_url` it calls the dedicated Inference Endpoint at
that address.

```text
error: bad.yaml: invalid embedding config: embedding provider 'p': unknown type 'cohere' (use one of: voyage, openai, google, jina, huggingface)
```

```text
error: bad.yaml: invalid embedding config: embedding provider 'p': base_url 'http://api.x.com/v1' uses the 'http' scheme; the API key travels in the Authorization header, so the endpoint must be https://
```

```text
error: bad.yaml: invalid embedding config: embedding provider 'p': 'api_key' must be a "${secrets.NAME}" reference, not a literal key; declare the key under 'secrets:'
```

```text
error: bad.yaml: invalid embedding config: embedding provider 'p': 'api_key' is required; set it to "${secrets.NAME}" naming a declared secret (only a huggingface provider with a 'base_url' may omit it)
```

```text
error: case.yaml: invalid embedding config: embedding provider 'p' references undeclared secret 'Q'; add it under 'secrets:'
```

### Embedding models

The key is the name a program passes to `embed` and the name `models()`
returns. Every field except `provider`, `model`, and `dimensions` is optional.

| Field | Type | Required | Default | Constraints |
| --- | --- | --- | --- | --- |
| `provider` | string | yes | | A key of `embedding.providers` |
| `model` | string | yes | | The provider's own model id, including any version suffix |
| `dimensions` | integer | yes | | 1 to 8,192. Sent to the provider where the model accepts it, and checked against every response either way. `text-embedding-ada-002` must be 1,536 |
| `max_input_tokens` | positive integer | no | Per provider, below | At most half the provider's per-request token cap, below |
| `description` | string | no | none | One line; at most 512 characters; no control or invisible formatting characters. Returned by `models()` |
| `query_prompt_name` | string | no | none | `huggingface` with a `base_url` only; 1 to 128 printable characters. The prompt applied to queries |
| `document_prompt_name` | string | no | none | `huggingface` with a `base_url` only; same limits. The prompt applied to documents |

Prompt names are refused on Hugging Face shared hosting (a provider without
a `base_url`), because the shared router might truncate the text it prefixes
with the prompt.

The alias's embedding-space identity changes with the provider's `type`,
`model`, `dimensions`, the prompt names, how the alias applies purpose
(`input_type` for Voyage, `task` for Jina, `taskType` or a text template for
Google, the prompt names for Hugging Face, nothing for OpenAI), and, for a
Hugging Face dedicated endpoint, the host of its `base_url`. It doesn't change
with `max_input_tokens`, `description`, or the provider's key.

Limits by provider. `maxInputBytes`, which `models()` reports, is three bytes
per token of the alias's input limit and, for Google, no more than the byte
bound. On Hugging Face shared hosting it is the input limit minus 16 bytes
instead (496 by default), because the shared router ignores a request not to
truncate and cuts long input silently with a success response; an alias there
must declare a `max_input_tokens` above 16, and it must not exceed the
model's real maximum, or the router truncates silently. A dedicated endpoint
keeps three bytes per token. For longer chunks, use a dedicated endpoint or another
provider. Submilli splits a batch into requests that fit the request caps.

| Provider | Default `max_input_tokens` | Byte bound (Google, shared Hugging Face) | Texts per request | Other request cap | Ceiling for `max_input_tokens` |
| --- | --- | --- | --- | --- | --- |
| Voyage | 32,000 | | 1,000 | Tokens per request: 1,000,000 for `voyage-4-lite` and `voyage-3.5-lite`, 320,000 for `voyage-4` and `voyage-3.5`, 120,000 for other models | Half the token cap |
| OpenAI | 8,192 | | 2,048 | 300,000 tokens | 150,000 |
| Google | 2,048; 8,192 for `gemini-embedding-2` | 2,032 bytes; 8,147 for `gemini-embedding-2` | 100 | None | None |
| Jina | 8,192 | | 512 | None | None |
| Hugging Face | 512 | 496 bytes on shared hosting (input limit minus 16) | 32 | None | None |

A Google model other than `gemini-embedding-2` takes the `gemini-embedding-001`
row. `gemini-embedding-001` reports no usage, so its estimates stay held against
the server-wide budget like Hugging Face's; `gemini-embedding-2` reports usage.
The token counts behind budgets are an estimate of three bytes per token, not
the provider's tokenizer. The input limit uses the same three bytes per token,
except for Google and shared Hugging Face hosting, whose byte bounds above
assume one byte per token so that nothing is silently truncated.

```text
error: bad.yaml: invalid embedding config: embedding model 'm': dimensions 0 is out of range; use a whole number from 1 to 8192
```

```text
error: bad.yaml: invalid embedding config: embedding model 'm': text-embedding-ada-002 always returns 1536 dimensions and cannot be shortened; set 'dimensions: 1536'
```

```text
error: bad.yaml: invalid embedding config: embedding model 'm': max_input_tokens 200000 is over 150000, half of the openai provider's per-request token cap; lower it
```

```text
error: bad.yaml: invalid embedding config: embedding model 'm': 'query_prompt_name' applies only to huggingface providers; remove it
```

```text
error: bad.yaml: invalid embedding config: embedding model 'm' names undeclared provider 'q'; declare it under 'embedding.providers:' or point the model at one of: p
```

An `embedding.embed` rule whose filter tests `model == "…"` must name a declared
alias:

```text
error: bad.yaml: invalid embedding config: caller 'main': permission filter names undeclared embedding model 'gpt'; declare it under 'embedding.models:' or filter on one of: (none declared)
```

## Errors

`submilli blueprint lint`, `submilli run --blueprint`, and registration
parse the file the same way, and parsing stops at the first error. The CLI prints it
as `error: <file>: <prefix>: <message>`, with the YAML path and the line and
column when they are known. Over HTTP, the same error is a `400` response
whose `error` field names the class. See [HTTP API](https://submilli.ai/docs/reference/http-api.md).

| Prefix | `error` over HTTP | Raised by |
| --- | --- | --- |
| `blueprint YAML is empty` | `parse_error` | An empty file |
| `blueprint parse error` | `parse_error` | YAML syntax, unknown or repeated keys, wrong types, unknown values, `vfs` fields and mounts, filters, `idle_timeout` |
| `invalid blueprint kind` | `invalid_kind` | [`kind`](https://submilli.ai/docs/reference/blueprint-file.md#kind) |
| `invalid blueprint name` | `invalid_name` | [`name`](https://submilli.ai/docs/reference/blueprint-file.md#name) |
| `invalid vfs config` | `invalid_vfs` | `subPath` and `cwd` |
| `invalid packages config` | `invalid_packages` | [`packages`](https://submilli.ai/docs/reference/blueprint-file.md#packages) names |
| `invalid variables config` | `invalid_variables` | [`variables`](https://submilli.ai/docs/reference/blueprint-file.md#variables), undeclared `${vars.NAME}` in filters |
| `invalid auth_proxy config` | `invalid_auth_proxy` | [`auth_proxy`](https://submilli.ai/docs/reference/blueprint-file.md#auth_proxy) |
| `invalid permissions config` | `invalid_permissions` | Empty caller or capability names |
| `invalid git config` | `invalid_git` | [`git`](https://submilli.ai/docs/reference/blueprint-file.md#git) |
| `invalid mcp config` | `invalid_mcp` | [`mcp`](https://submilli.ai/docs/reference/blueprint-file.md#mcp), `mcp.<name>` rules |
| `invalid llm config` | `invalid_llm` | [`llm`](https://submilli.ai/docs/reference/blueprint-file.md#llm), `llm.call` model filters |
| `invalid embedding config` | `invalid_embedding` | [`embedding`](https://submilli.ai/docs/reference/blueprint-file.md#embedding), `embedding.embed` model filters |

### Registration

`submilli server blueprint apply` sends the file to the server, which parses
it and then checks it against what the server holds. A file that fails a
check isn't registered.

| Check | Message |
| --- | --- |
| Every volume, as the root or a mount, is declared on the server | `volume 'team' is not declared on this server; declared volumes: handbook, notes` |
| `access` doesn't exceed the server's declaration | ``volume 'handbook' is read_only on this server; drop `access: read_write` (or write `access: read_only`), or ask the operator to declare it read_write`` |
| Every `store` secret has a value in the server's store | `secret check failed: missing secret 'K'` |
| Every Package, and every Package it depends on, is installed on the server | ``package check failed: package `@acme/billing` is not installed; install it with `submilli server packages install <org/repo> @acme/billing` `` |
| Each capability a Package requires for its own calls has a rule under that Package's caller | `package check failed:` followed by the Package, the capability, and the missing rule |
| A filter tests only fields its capability reports, for standard-library, Package, and `mcp.<name>` capabilities | The rule, the field, and the fields the capability reports |

`submilli blueprint lint` makes the Package and filter checks against the
local Package store, and reports every filter field it finds:

```text
error: case.yaml: `permissions.main` rule 2 for `fs.read` tests `host`, which the operation doesn't report, so a condition on it is false for every call, and true under `not`; its fields are: chunkSize, length, path, recursive
```

`harness` secrets aren't checked at registration. The secret check is made
once, so a value removed from the store later fails the call that needs it.
