Tutorials
Keep the Blueprints in a repository: on every pull request, lint them and test each one as two sessions, one it must allow and one it must refuse; on every merge to main or every release, register them on the server, with the Packages pinned in the same commit and a rollback that is a revert.
A Blueprint is policy, and apply from a laptop leaves no record of who
changed it, when, or why. The server holds whatever was applied last. Kept
in a repository, each change is a reviewed commit, the pipeline proves
each rule refuses what it should before the merge, and the server holds
what the main branch says.
In this tutorial we will put a Blueprint in a repository and give it a pipeline that tests it on pull requests and registers it on the server from main. The Blueprint grants the book’s example billing Package, which reads fixed data, so nothing here needs a key. You need a server, as Run the server shows, with its admin token in your shell.
The repository
Section titled “The repository”.github/workflows/blueprints.yml # written belowpackages.txtblueprints/└── support/ ├── blueprint.yaml ├── README.md ├── total.ts └── test.shEach Blueprint gets its own folder, with the file named
blueprint.yaml. The submilli blueprint commands read that file from
the current directory, so in the folder capability add, secret add,
and the rest work on it without naming it. A README.md beside it says
what the agent is for and who owns the policy, and the program and
script that test it sit there too.
The Blueprint lets the agent list the charges of the customer the session is for:
kind: blueprintname: support
# Bound once per request by the application, never by the program.variables: customerId: required: true
packages:- '@submilli/acme-billing'
default: deny
permissions: # What generated code may do. main: - capability: acme.com/charges.list filter: customerId == ${vars.customerId} action: allow
# What the package itself may do. Nothing: it reads a fixture. '@submilli/acme-billing': []packages.txt names each Package the Blueprints list, one per line, with
the GitHub repository it is installed from, the Package, and the commit to
pin:
submilli/acme @submilli/acme-billing 88656b81c537Test the policy as two sessions
Section titled “Test the policy as two sessions”A passing lint says the file is well formed. To test whether the rule refuses what it should, run a program under it, bound to the customer the rule should allow and then to one it should refuse:
import { listCharges } from "@submilli/acme-billing";
function main(): string { const charges = listCharges("cus_northwind"); let total = 0; for (const charge of charges) { total += charge.amount; } return `${charges.length} charges, ${total} cents`;}#!/usr/bin/env bash# Runs total.ts under this blueprint as two sessions: the customer it# should allow, and one it should refuse.set -eusubmilli run --blueprint blueprint.yaml --var customerId=cus_northwind total.tssubmilli run --blueprint blueprint.yaml --var customerId=cus_initech total.ts 2>&1 | grep PermissionDeniedErrorInstall the pinned Package, lint, and run the test, as the pull-request job will:
submilli install submilli/acme@88656b81c537 @submilli/acme-billingcd blueprints/supportsubmilli blueprint lint blueprint.yamlchmod +x test.sh./test.shinstalled @submilli/acme-billing v0.1.0 -> ~/.submilli/packages/@submilli/acme-billing✓ blueprint.yaml is valid2 charges, 6150 centserror: PermissionDeniedError: permission denied: caller=main capability=acme.com/charges.list: policy denied acme.com/charges.list 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.The first line of the test is the program’s result for the customer the
session is for. The second is the test. Bound to Initech, the same
program asks for Northwind’s charges and is refused, and the grep
passes only when that denial appears. A run that fails for another
reason, or that is allowed, fails the script.
Check every pull request
Section titled “Check every pull request”name: Blueprints
on: pull_request: push: branches: [main]
jobs: check: runs-on: ubuntu-latest env: SUBMILLI_DENY_WARNINGS: "1" steps: - uses: actions/checkout@v4
- name: Install Submilli run: | curl -fsSL https://submilli.ai/install.sh | sh -s -- --version v0.2.0 echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install the packages the blueprints list run: while read repo package sha; do submilli install "$repo@$sha" "$package"; done < packages.txt
- name: Lint and test each blueprint run: | for dir in blueprints/*/; do (cd "$dir" && submilli blueprint lint blueprint.yaml && ./test.sh) doneSUBMILLI_DENY_WARNINGS makes any warning fail the job. A Package
whose check and @capability tag disagree fails submilli install,
and a Blueprint that lint warns about, such as one with default: allow,
fails submilli blueprint lint.
Now break the rule the way a careless edit would, by dropping the filter so that any customer’s charges are allowed. Lint still passes, since the file is well formed. The policy test doesn’t:
✓ blueprint.yaml is valid2 charges, 6150 centsThe second run was allowed, so grep found no denial, the script exits
1, and the pull request’s check turns red with that line in its log.
A Package in packages.txt can also be held to an agent’s security review
before its commit is pinned there. Review a Package’s
security
shows how.
Register on every merge
Section titled “Register on every merge”The second job runs only on a push to main, after the check, and talks
to the server. It needs the server’s address and an admin token, since
registering a Blueprint is an admin operation. Store them in the
repository with the GitHub CLI, from a checkout of it. The address is a
variable, since it isn’t secret. The token is a secret, which gh
prompts for so it never lands in your shell history:
gh variable set SUBMILLI_SERVER_URL --body http://submilli.internal:8128gh secret set SUBMILLI_SERVER_TOKENThe job reads both into its environment, where the submilli server
commands look for them:
deploy: if: github.event_name == 'push' needs: check runs-on: ubuntu-latest env: SUBMILLI_SERVER_URL: ${{ vars.SUBMILLI_SERVER_URL }} SUBMILLI_SERVER_TOKEN: ${{ secrets.SUBMILLI_SERVER_TOKEN }} steps: - uses: actions/checkout@v4
- name: Install Submilli run: | curl -fsSL https://submilli.ai/install.sh | sh -s -- --version v0.2.0 echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install the packages on the server run: while read repo package sha; do submilli server packages install "$repo" "$package" --sha "$sha"; done < packages.txt
- name: Register the blueprints run: for dir in blueprints/*/; do submilli server blueprint apply "${dir}blueprint.yaml"; doneThe server has to be reachable from the runner, so a server inside your network takes a self-hosted runner in the same network. Merge the pull request, and the job’s log shows the server taking the Package and the Blueprint:
installed @submilli/acme-billing @ 88656b81c537Added blueprint 'support'Merge a change to the file and the same job prints Updated blueprint 'support'. apply registers or replaces, so running the job again is
safe. To register on every release instead of every merge, change the
trigger to release: types: [published] and the job’s condition to
github.event_name == 'release'.
Check what the server holds, and roll back
Section titled “Check what the server holds, and roll back”submilli server blueprint listsubmilli server run-code blueprints/support/total.ts --blueprint support --var customerId=cus_northwindsupport2 charges, 6150 centssubmilli server blueprint show support prints the file the server
holds, which is the main branch’s. When it isn’t, someone ran apply by
hand, and the next merge puts the repository’s version back. A change
that turns out wrong is reverted like any other (git revert HEAD and a
push), and the job registers the previous file. The job doesn’t remove
Blueprints. A Blueprint whose folder is deleted stays registered
until submilli server blueprint remove is run, which ends its
sessions, so make that call part of the same change.
You have a Blueprint that reaches the server from main alone, linted and tested from both sides on the way, with the Package it needs pinned beside it and a history of its changes. Next: Add the GitHub MCP server.