Skip to content

Blueprints

Start a Blueprint

AI-assistedThis page includes both human and AI contributions.

How to create a Blueprint with the CLI: start from nothing allowed, check the file, add a Package, grant operations, declare secrets and variables, test it, and register it on a server.

This guide shows you how to build a Blueprint block by block with the submilli blueprint commands, test it, and register it on a server. The examples use the billing Package from Packages. Substitute your own Package, operation, and fields.

The commands edit blueprint.yaml in the current directory and rewrite it each time, so comments you add by hand don’t survive them. Refer to the Blueprint file reference for every block and field.

A Blueprint can only list a Package that is in your local store. If the Package is your own, submilli build publish-local from its project puts it there. If someone else published it, install it from its repository:

Terminal window
submilli install acme/billing-package @acme/billing

The first argument is the GitHub repository, owner/repo. The second is the Package to build from it, since one repository can hold several. Leave the Package out to install all the Packages the repository declares. install fetches the repository, builds the Package, and puts it in the local store, pinned to the commit it resolved. To pin a branch, tag, or commit yourself, append @<ref> to the repository name. This installs the curated Jina Package at the runtime’s v0.2.0 tag:

Terminal window
submilli install submilli/submilli-runtime@v0.2.0 @submilli/jina

Add --upgrade to replace a Package already installed at another commit. The curated Packages all come from that repository.

A private repository needs a GitHub token that can read it. Without one, install reports that it found no public repository by that name. Store yours once, and install and build send it from then on:

Terminal window
submilli github authenticate
✓ stored a GitHub token for octocat (never expires) in ~/.submilli/github_token

authenticate prompts for the token, or reads it from piped standard input, and checks it with GitHub. In CI, put the token in GH_TOKEN. submilli github auth-status says which token applies. Refer to Install private Packages on a server for the token’s settings, and to the CLI reference for the errors an install can give.

A server has its own Package store, so a Blueprint that will run there needs the Package installed there too. The submilli server commands talk to a running server, and Connect the CLI shows how they reach it:

Terminal window
submilli server packages install acme/billing-package @acme/billing
installed @acme/billing @ 3f9c2a1b7e40

The server fetches, builds, and pins the Package the same way. --sha <ref> pins a commit, tag, or branch, and --upgrade replaces an installed one.

Read the Package’s capabilities and docs

Section titled “Read the Package’s capabilities and docs”

Before granting anything, read what the Package provides:

Terminal window
submilli blueprint capability list @acme/billing
@acme/billing
acme.com/credits.apply — Add a goodwill credit to a customer's account.
fields: amount: number, customerClass: string, customerId: string

submilli docs @acme/billing prints the declarations the model will read.

Terminal window
submilli blueprint init support
✓ created blueprint.yaml (name: support)

The file it writes, minus its comments, permits nothing:

blueprint.yaml
kind: blueprint
name: support
default: deny
permissions:
main: []

The application and the server refer to the Blueprint by name. default: deny means anything without a rule is refused. Leaving default out means the same.

Terminal window
submilli blueprint lint blueprint.yaml
✓ blueprint.yaml is valid

lint validates the file against the installed Packages and exits 1 on an error and 0 on warnings, so it can gate a commit. lint --fix adds missing Package rules. Run it after each step below, because its warnings say what the file still lacks.

Terminal window
submilli blueprint add-package @acme/billing --no-capabilities
warning: blueprint.yaml: package `@acme/billing` requires secret `BILLING_API_KEY`, but `secrets:` does not declare it
✓ added @acme/billing to blueprint.yaml
1 provided capabilities not selected; `default: deny` denies calls to them
added 2 rules to caller `@acme/billing` (default allow):
allow http.post (filter: host == "billing.internal.example.com")
allow secrets.get (filter: name == "BILLING_API_KEY")
blueprint.yaml (fragment)
packages:
- '@acme/billing'
permissions:
'@acme/billing':
- capability: http.post
filter: host == "billing.internal.example.com"
action: allow
- capability: secrets.get
filter: name == "BILLING_API_KEY"
action: allow
main: []

The Package is listed, so the import resolves. It also got its own caller list, written from what it declares it requires. --no-capabilities leaves main empty so that you grant operations one by one below. --all-capabilities or --capabilities a,b grants the Package’s operations in the same command. The warning is about the key, declared in Declare the secret.

main is still empty, so a program that calls applyCredit is refused. Grant the operation, narrowed by a filter over the fields it reports:

Terminal window
submilli blueprint capability add acme.com/credits.apply --filter 'customerClass == "premium"'
✓ added allow acme.com/credits.apply (filter: customerClass == "premium") to caller 'main' in blueprint.yaml
blueprint.yaml (fragment)
permissions:
main:
- capability: acme.com/credits.apply
filter: customerClass == "premium"
action: allow

A rule names a capability, an optional filter over the fields the operation reports, and an action. Rules are read top to bottom and the first match wins. Names match exactly, so allowing fs.write doesn’t allow fs.mkdir. capability add refuses a name nothing provides. Refer to the filter language reference for what a filter can test.

The Package reads BILLING_API_KEY by name, and add-package warned that the Blueprint doesn’t declare it. When a Package reads a secret, the Blueprint declares it by that name and says where its value comes from. The value itself never enters the file:

Terminal window
submilli blueprint secret add BILLING_API_KEY --store billing_api_key
✓ declared secret 'BILLING_API_KEY' (store: billing_api_key) in blueprint.yaml
blueprint.yaml (fragment)
secrets:
BILLING_API_KEY:
store: billing_api_key

Lint again, and the warning is gone:

Terminal window
submilli blueprint lint blueprint.yaml
✓ blueprint.yaml is valid

--store names a key in a secret store, which keeps your credentials outside the Blueprint. There are two. The local store is a directory under ~/.submilli, readable only by your user, and submilli run reads it. A server has its own store, encrypted at rest, and the Blueprints registered on that server read it. Put the value in the local store:

Terminal window
submilli secret put billing_api_key
Value for 'billing_api_key': [hidden]
Stored secret 'billing_api_key'

The command prompts with echo off. In a script, pipe the value in with submilli secret put billing_api_key < key.txt. On a server, submilli server secret put does the same.

If the credential changes with the context, such as the access token a user granted your application for their Google Calendar, declare it with --harness. The application supplies the value when it opens the session, and --required refuses a session that doesn’t:

Terminal window
submilli blueprint secret add GOOGLE_ACCESS_TOKEN --harness --required
✓ declared secret 'GOOGLE_ACCESS_TOKEN' (harness, required: true) in blueprint.yaml
blueprint.yaml (fragment)
secrets:
GOOGLE_ACCESS_TOKEN:
harness:
required: true

Connect a harness shows how each harness supplies it.

To limit a permission by the session’s context, such as the customer the agent is serving, declare a variable for that context. The application binds its value when it opens the session:

Terminal window
submilli blueprint variable add customerId --required
✓ declared variable 'customerId' (required) in blueprint.yaml
blueprint.yaml (fragment)
variables:
customerId:
required: true

A variable is a string, either --required or with a --default. A session that omits a required variable is refused before any program runs:

error: invalid variables: required variable 'customerId' was not supplied

A filter refers to the variable as ${vars.customerId}. Replace the grant above with one that also requires the customer to be the session’s, so one Blueprint serves all customers:

Terminal window
submilli blueprint capability remove acme.com/credits.apply
submilli blueprint capability add acme.com/credits.apply \
--filter 'customerId == ${vars.customerId} and customerClass == "premium"'
✓ removed 1 rule(s) for 'acme.com/credits.apply' from caller 'main' in blueprint.yaml
✓ added allow acme.com/credits.apply (filter: customerId == ${vars.customerId} and customerClass == "premium") to caller 'main' in blueprint.yaml
blueprint.yaml (fragment)
permissions:
main:
- capability: acme.com/credits.apply
filter: customerId == ${vars.customerId} and customerClass == "premium"
action: allow

Create credit.ts, a program like one a model would write under this Blueprint. It imports the Package and calls its operation for the premium customer:

credit.ts
import { applyCredit } from "@acme/billing";
function main(): string {
const credit = applyCredit("cus_northwind", 1500);
return `credited ${credit.amount} cents`;
}

submilli run --blueprint runs it the way a session would, with --var binding the variable the way the application does. Binding the premium customer lets the call through, and binding another customer produces the denial a session would see:

Terminal window
submilli run --blueprint blueprint.yaml --var customerId=cus_northwind credit.ts
credited 1500 cents
Terminal window
submilli run --blueprint blueprint.yaml --var customerId=cus_initech credit.ts
error: PermissionDeniedError: permission denied: caller=main capability=acme.com/credits.apply: policy denied acme.com/credits.apply for main. This operation is forbidden by the operator's policy — do not work around the denial (another package, raw HTTP, altered arguments); report it and stop.
fields: caller = "main", capability = "acme.com/credits.apply", reason = "policy denied acme.com/credits.apply for main"
at applyCredit (lib:28:66) [thrown here]

Each time you change a rule, test the case it should allow and the case it should refuse.

blueprint.yaml
kind: blueprint
name: support
secrets:
BILLING_API_KEY:
store: billing_api_key
variables:
customerId:
required: true
packages:
- '@acme/billing'
default: deny
permissions:
'@acme/billing':
- capability: http.post
filter: host == "billing.internal.example.com"
action: allow
- capability: secrets.get
filter: name == "BILLING_API_KEY"
action: allow
main:
- capability: acme.com/credits.apply
filter: customerId == ${vars.customerId} and customerClass == "premium"
action: allow

The Blueprint also shapes what the model is told. The description of the execute tool is assembled from it. It says which modules a program may import, what its filesystem is, and which hosts it may reach. Print it as the model receives it:

Terminal window
submilli blueprint prompt
…
You do NOT have access to Node.js APIs, browser globals, or NPM
packages. Submilli ships its own standard library — modules are:
`submilli:url`, `submilli:crypto`, `submilli:uuid`, `submilli:session`. Submilli native
packages and discovered `@mcp/<server>` packages may also be available;
…

Nothing here grants fs.read or http.get, so submilli:fs and submilli:http are not listed. A Blueprint that grants them gets the modules, a Sandbox: line naming its filesystem, and a Network: line listing its hosts. The rest of the text, on how to write a program and what to do with a denial, is the same for all Blueprints.

You register a Blueprint file on a server. The server keeps its own copy and never reads the file again. Put the secret’s value in the server’s store first, since registration checks that every store: secret exists there:

Terminal window
submilli server secret put billing_api_key
submilli server blueprint apply blueprint.yaml
Value for 'billing_api_key': [hidden]
Stored secret 'billing_api_key'
Added blueprint 'support'

Run apply again after an edit, and the answer is Updated blueprint 'support'. Then run the program the way an application would, naming the Blueprint by its name:

Terminal window
submilli server run-code credit.ts --blueprint support --var customerId=cus_northwind
credited 1500 cents

Applications name the Blueprint the same way when they open a session. Refer to Register a Blueprint for applying Blueprints, replacing them, and removing them.