Skip to content

Reference

Permissions

Written by a human with AI assistance

How a call is decided, callers, actions, the refusals no rule changes, the capabilities and their fields, the errors when a Blueprint is read, and the denials at run time.

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.

blueprint.yaml (fragment)
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: allow

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

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 lint warns about an ask rule for one.
  • The file reads and writes of the submilli:code library. blueprint lint warns about an ask rule for an fs.* capability.
  • A redirect an HTTP request follows.
  • The server’s file tools, and session finalizers.
  • submilli run with 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.

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.

These are refused before the rules are read, whatever default says:

  • variables.get from main. Configuration is available only to Packages.
  • secrets.get from main. A secret’s value is available only to Packages, and submilli blueprint capability add refuses to write the rule.
  • A write to a volume the Blueprint mounts read-only.
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.

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
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)
  • Git: git.clone and git.fetch are checked with the branch the call names, then once for each branch fetched. A call that names none is checked with branch set to "", so a rule testing branch matches only calls that name it. git.clone needs no git.init or fs.* rule. Reading history, staging, and branching aren’t gated.
  • HTTP redirects: each redirect is checked before it is sent, with the new URL’s host and path, under the same caller. A redirect that turns the request into a GET is checked as http.get, or for request, as http.request with method "GET".
  • Models: prompt_count is 1 for call and the number of prompts for batch. The prompt text is never in the context. models() lists only the models a rule allows.
  • Session state: session.list is checked with prefix, and each key it would return as session.read. A refused key is left out.
  • MCP tools: the tool is a field, not part of the name. Write capability: mcp.linear with filter: 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.

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-*"
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/*"
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"
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
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-*"
Capability Fields Operation Example filter
secrets.get name: string Read a Blueprint-declared secret value name == "STRIPE_API_KEY"
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/"
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"
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"

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: allow
  • default: ask-human, a deprecated spelling of deny
  • an ask rule for an operation that can’t wait for its approver
  • a rule for secrets.get or variables.get under main
  • 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.

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.