This page describes how the permissions and default blocks of a
Blueprint decide a gated operation, the capabilities and their fields, and
the errors and denials a rule produces. A rule’s filter is written in the
filter language.
How a call is decided
Section titled “How a call is decided”default: deny
permissions: main: - capability: fs.write filter: path glob "/notes/*" action: allow - capability: fs.read action: allow
'@pied-piper/billing': - capability: http.get filter: host == "billing.internal.example.com" action: allowWhen code performs a gated operation, the runtime names the
caller, applies the refusals no rule can
change, then reads the caller’s list from the
top. The first rule whose capability equals the operation’s and whose
filter, if any, is true decides. If none matches, or the caller has no
list, default decides.
Names are matched exactly, with no wildcards, so a rule for fs.write
doesn’t match fs.mkdir. A rule without a filter matches every use of its
capability.
| Field | Type | Required | Meaning |
|---|---|---|---|
name |
non-empty string | No | A label for the rule, unique within the caller’s list (blueprint lint errors on a repeat). Names the rule instead of its position |
capability |
string | Yes | The operation’s name, such as fs.write, pied-piper.example/credits.apply, or mcp.linear |
filter |
string | No | A condition on the operation’s fields, in the filter language |
action |
allow, deny, ask-human, or ask-admin |
Yes | What happens when the rule matches |
Actions and the default
Section titled “Actions and the default”| Action | Effect |
|---|---|
allow |
The operation proceeds |
deny |
The operation throws PermissionDeniedError |
ask-human |
The operation waits for the person in the conversation to approve it, through the harness. See Approvals |
ask-admin |
The operation waits for an administrator to approve it, with an admin token. See Approvals |
default takes allow or deny, and is deny when the Blueprint doesn’t
set it. Under default: allow, every operation no rule matches is permitted,
including ones a Package added later provides. default: ask-human is a
deprecated spelling of deny, and blueprint lint warns about it.
default: ask-admin is refused.
Approvals
Section titled “Approvals”A rule with ask-human or ask-admin lets the operation run once its
approver approves it. The run stops at the call, and the HTTP API shows the
call as kind: "approval", with the values the filter tested
(Application tools and approvals).
ask-human |
ask-admin |
|
|---|---|---|
| Who decides | The harness, with a user or an admin token, after asking the person |
Only an admin token: submilli server approval, /v1/approvals, or POST /v1/runs/{run_id}/resume |
| How long it waits | The Blueprint’s approval_timeout, else the server’s, 24 hours by default |
The same |
| A refusal or no decision in time | PermissionDeniedError, source human |
PermissionDeniedError, source admin |
An approval covers the call it was asked for, once. The same operation run
again asks again. After a restart, a durable run waits on the same call
under the same id, and a decision already made is not asked again. Without
durable_execution, a restart ends the waiting run, as it ends any run.
Some calls can’t wait for an approver. A call one of these makes, and an
ask rule matches, is denied as a deny rule would deny it: the same
reason, with source policy. The server logs a warning naming the rule:
- A run over MCP, or through
POST /v1/execute. - A
git.*operation.blueprint lintwarns about an ask rule for one. - The file reads and writes of the
submilli:codelibrary.blueprint lintwarns about an ask rule for anfs.*capability. - A redirect an HTTP request follows.
- The server’s file tools, and session finalizers.
submilli runwith no terminal to ask on. At a terminal, it asks there.
A listing keeps an item whose use needs approval, such as a model in
llm.models(), when the run can ask, because using it asks. Where it
can’t, the listing leaves the item out, as it leaves out a denied one. A library an ask rule names is
visible to the program, as for any rule that isn’t deny.
Callers
Section titled “Callers”Each list under permissions belongs to a caller:
| Caller | Its rules apply to |
|---|---|
main |
The program |
A Package’s name, such as '@pied-piper/billing' |
That Package’s own code, whether listed under packages: or a dependency of one |
The caller is the code that is running, not anything the program passes. A
standard-library operation is attributed to the code that calls it, so a
Package’s HTTP request is judged under the Package’s list. An operation a
Package checks with check is attributed to the code that called the
Package. A program calling listCharges is judged under main for
pied-piper.example/charges.list, and the request the Package then sends under
'@pied-piper/billing'. A Package with no list may do only what default allows.
Refusals no rule can change
Section titled “Refusals no rule can change”These are refused before the rules are read, whatever default says:
variables.getfrommain. Configuration is available only to Packages.secrets.getfrommain. A secret’s value is available only to Packages, andsubmilli blueprint capability addrefuses to write the rule.- A write to a volume the Blueprint mounts read-only.
Capabilities
Section titled “Capabilities”| Source | Names | Fields |
|---|---|---|
| The standard library | embedding.embed, fs.*, git.*, http.*, llm.call, secrets.get, variables.get, session.* |
In the tables below |
| A Package | Chosen by its author, such as pied-piper.example/credits.apply |
Declared by its @capability tags (Package manifest) |
| An MCP server the Blueprint declares | mcp.<server>, such as mcp.linear |
tool, transport (MCP servers) |
| The application’s tools a harness declares for a session | tools.call |
tool (Application tools) |
submilli blueprint capability list prints every capability a Blueprint can
use, with its fields and the rules written for it.
A field is a string, a number, or a boolean. A Package field may also
be an object, whose members a filter names with a dot.
Fields only some calls report
Section titled “Fields only some calls report”A call that doesn’t supply a field, or passes it as undefined, leaves it
out, and a condition on a missing field is false.
| Capability | Field | Reported by |
|---|---|---|
fs.read |
length |
readBytes |
fs.read |
chunkSize |
bytes |
fs.read, fs.stat |
recursive |
The submilli:code workspace tools, always true |
fs.write |
length |
write, writeText, append, appendText, and code edits |
fs.write |
max_bytes |
http.download, and Packages that stream a download to a file |
fs.write |
diff |
Code edits |
http.* |
body_size, timeout_ms |
Every request, except a redirect turned into a GET |
Normalized fields
Section titled “Normalized fields”| Field | Capabilities | A rule sees |
|---|---|---|
path, from, to, vfs_path |
fs.*, git.*, http.download |
The absolute path, resolved against the working directory, with . and .. resolved (/notes/../secrets.txt is /secrets.txt) |
host |
http.* |
The host without port or trailing dot (https://api.example.com./ is api.example.com) |
remote |
git.clone, git.fetch |
The full HTTPS URL, host in lowercase, default port removed (https://GitHub.com:443/pied-piper/./project.git is https://github.com/pied-piper/project.git) |
Checks some operations make
Section titled “Checks some operations make”- Git:
git.cloneandgit.fetchare checked with the branch the call names, then once for each branch fetched. A call that names none is checked withbranchset to"", so a rule testingbranchmatches only calls that name it.git.cloneneeds nogit.initorfs.*rule. Reading history, staging, and branching aren’t gated. - HTTP redirects: each redirect is checked before it is sent, with the
new URL’s
hostandpath, under the same caller. A redirect that turns the request into aGETis checked ashttp.get, or forrequest, ashttp.requestwithmethod"GET". - Models:
prompt_countis1forcalland the number of prompts forbatch. The prompt text is never in the context.models()lists only the models a rule allows. - Session state:
session.listis checked withprefix, and each key it would return assession.read. A refused key is left out. - MCP tools: the tool is a field, not part of the name. Write
capability: mcp.linearwithfilter: tool == "save_issue".
variables.get is available only to Packages, regardless of policy. An alias
read checks the requested alias name and grants no access to its target name.
submilli:embedding
Section titled “submilli:embedding”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
embedding.embed |
model: string, input_count: number |
Embed text through a declared embedding alias (embed) and enumerate the aliases it may use (models). Narrowing model also narrows what models() reveals: every candidate is filtered through this same rule, so a listing never offers an alias the caller would be denied at call time. A policy allowing no candidates returns an empty listing |
model glob "memory-*" |
submilli:fs
Section titled “submilli:fs”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
fs.read |
path: string, length: number, chunkSize: number, recursive: boolean |
Read files and code workspace content (including search and ignore rules) | path glob "*.csv" |
fs.write |
path: string, length: number, max_bytes: number, diff: string |
Create, write, append, or apply code edits to files | path glob "/out/*" |
fs.stat |
path: string, recursive: boolean |
Inspect metadata (including code workspace discovery) | path glob "/data/*" |
fs.list |
path: string, recursive: boolean |
List directory entries (including code search, glob and tree) | path glob "/data/*" |
fs.mkdir |
path: string, recursive: boolean |
Create directories | path glob "/tmp/*" |
fs.remove |
path: string, recursive: boolean |
Delete files or directories | path glob "/tmp/*" |
fs.move |
from: string, to: string |
Move or rename a path | to glob "/archive/*" |
fs.copy |
from: string, to: string, recursive: boolean |
Copy a path | to glob "/backup/*" |
submilli:git
Section titled “submilli:git”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
git.init |
path: string |
Create a local repository and its VFS directory | path == "/repo" |
git.clone |
path: string, remoteName: string, remote: string, branch: string |
Clone an HTTPS repository into a VFS directory | path == "/repo" and remote == "https://github.com/pied-piper/project.git" |
git.fetch |
path: string, remoteName: string, remote: string, branch: string |
Fetch or pull HTTPS remote branches into an existing repository | path == "/repo" and remote == "https://github.com/pied-piper/project.git" |
git.commit |
path: string, branch: string |
Commit staged changes with Blueprint identity | path == "/repo" and branch == "main" |
submilli:http
Section titled “submilli:http”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
http.get |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP GET | host == "api.example.com" |
http.post |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP POST | host == "api.example.com" |
http.put |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP PUT | host == "api.example.com" |
http.patch |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP PATCH | host == "api.example.com" |
http.delete |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP DELETE | host == "api.example.com" |
http.head |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP HEAD | host == "api.example.com" |
http.options |
host: string, path: string, body_size: number, timeout_ms: number |
HTTP OPTIONS | host == "api.example.com" |
http.request |
method: string, host: string, path: string, body_size: number, timeout_ms: number |
HTTP request with a method chosen at the call, through http.request. The per-method functions (http.get, …) check their own capability instead |
method == "GET" and host == "api.example.com" |
http.download |
host: string, url_path: string, vfs_path: string, max_bytes: number, overwrite: boolean, decompress: boolean |
Download a URL straight to the VFS | host == "cdn.example.com" and overwrite == false |
submilli:llm
Section titled “submilli:llm”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
llm.call |
model: string, prompt_count: number |
Call a model (text, texts, call, batch) and enumerate the models it may call (models). Narrowing model also narrows what models() reveals: every candidate is filtered through this same rule, so a listing never offers a model the caller would be denied at call time. A policy allowing no candidates returns an empty listing |
model glob "claude-*" |
submilli:secrets
Section titled “submilli:secrets”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
secrets.get |
name: string |
Read a Blueprint-declared secret value | name == "STRIPE_API_KEY" |
submilli:session
Section titled “submilli:session”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
session.finalize |
key: string |
Register or unregister durable package-owned session cleanup | key glob "browser/*" |
session.read |
key: string |
Read session state (get, has), and decide which keys a list may reveal | key glob "triage/*" |
session.write |
key: string |
Store or overwrite a session value (set) | key glob "triage/*" |
session.remove |
key: string |
Delete a session key | key glob "triage/*" |
session.list |
prefix: string |
Enumerate session keys under a prefix | prefix == "triage/" |
@tools
Section titled “@tools”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
tools.call |
tool: string |
Call an application tool the harness declared for the session (@tools). The harness answers each call, and may refuse it |
tool == "query_orders" |
submilli:variables
Section titled “submilli:variables”| Capability | Fields | Operation | Example filter |
|---|---|---|---|
variables.get |
name: string |
Read an immutable Blueprint/session configuration variable | name == "BROWSER_STORE_NAME" |
| Capability | Fields | Operation | Example filter |
|---|---|---|---|
mcp.<server> |
tool: string, transport: string |
Call tools on a declared outbound MCP server (streamable_http) (declare a server with blueprint add-mcp to concretize) |
tool == "create_issue" |
Errors when the Blueprint is read
Section titled “Errors when the Blueprint is read”submilli blueprint lint, the server’s registration, and
submilli run --blueprint stop on these:
| Mistake | Error |
|---|---|
| A filter that doesn’t parse, or names an undeclared variable | See Filter language |
An action other than allow, deny, ask-human, or ask-admin |
unknown variant `maybe`, expected one of `allow`, `deny`, `ask-human`, `ask-admin` |
A default other than deny or allow |
unknown variant `block`, expected `deny` or `allow` |
default: ask-admin |
`default: ask-admin` is not accepted: an administrator would be asked about every call no rule matches. … |
A key in a rule other than capability, filter, action |
unknown field `extra`, expected one of `capability`, `filter`, `action` |
| An MCP capability with the tool in its name | permission rule 'mcp.linear/save_issue': use capability 'mcp.linear' with a filter such as 'tool == "name"' instead of '/tool' |
A rule for an undeclared MCP server, llm model, or embedding alias |
permission rule 'mcp.x' references undeclared mcp server 'x' |
| A Package requires an operation its own list has no rule for (lint and registration) | package `@pied-piper/billing` requires `http.get` with filter `…`, but `permissions.@pied-piper/billing` has no matching rule (submilli blueprint lint --fix adds it) |
| A filter tests a field the capability doesn’t report (lint and registration) | rule 1 for `fs.read` tests `owner`, which the operation doesn't report, … |
submilli blueprint lint also warns, without stopping, about:
default: allowdefault: ask-human, a deprecated spelling ofdeny- an ask rule for an operation that can’t wait for its approver
- a rule for
secrets.getorvariables.getundermain - a rule that an earlier unfiltered rule always decides first
- a capability name nothing provides, with a suggestion
- a Package rule that differs from what the Package requires
- a Package list for a Package the Blueprint doesn’t use
With --deny-warnings, or SUBMILLI_DENY_WARNINGS=1, any of these fails
the lint.
Denials at run time
Section titled “Denials at run time”A refused operation throws PermissionDeniedError, which a program can
catch. e.caller, e.capability, and e.reason hold its fields.
error: PermissionDeniedError: permission denied: caller=main capability=fs.write: policy denied fs.write on /secrets.txt for main. This request is forbidden by the operator's policy — do not work around the denial (another package, raw HTTP, or other arguments to reach the same thing); report it and stop. fields: caller = "main", capability = "fs.write", reason = "policy denied fs.write on /secrets.txt for main" at main (w.ts:4:43) [thrown here]The message is permission denied: caller=<caller> capability=<capability>: <reason>.
and a closing sentence addressed to the model. It doesn’t name the rule
that refused.
| Reason | Cause |
|---|---|
policy denied <capability><target> for <caller> |
A deny rule, the default, or an ask rule for a call that can’t be put to its approver |
the person asked declined the call (ask-human) |
The person refused it, followed by : <reason> when they gave one. source is human |
an administrator declined the call (ask-admin) |
An administrator refused it, followed by : <reason> when they gave one. source is admin |
the person asked did not decide in time (ask-human), an administrator did not decide in time (ask-admin) |
No decision before the timeout |
secret values are never available to main-module code, … |
secrets.get from main |
<path> is in the volume mounted read-only at <mount> |
A write to a read-only volume |
<target> is on <path> for the fs.*, git.*, and http.download
capabilities ( from <path> to <path> for fs.copy and fs.move). For a
capability a Package declares, it is with and the values the call asked
with: each text, number, or true/false field, up to four, text cut at 64
characters. It is empty otherwise. Diagnose a denial
traces a denial back to the rule that decided it.