Tool-calling agent harness with envelopes, staging gates, schedules, and post-run evaluation
Project description
Boundary
Agents do not need more trust. They need a boundary.
Boundary runs tool-calling agents inside an explicit envelope: what they may read, what they may write — and how much (a write floor and ceiling, not just an allowlist) — what they may spend, when they must stage a thesis, and how the run gets graded afterward. It is for the moment when a coding agent is useful enough to delegate to, but not safe enough to leave unsupervised.
Most guards enforce safety — bad things don't happen. Boundary also enforces liveness (enough must happen: the write ceiling is refused at the tool layer, the floor is graded in review) and treats spend as a first-class boundary that fails closed (an unpriced model is priced at the most expensive rate, so a dollar cap always binds). Both track state across the whole run — the thing a per-call guard structurally cannot see.
Four modes:
- Interactive:
boundary run --system-file <prompt.md> --task "..." - Fielding Coach:
boundary fielding-coach "loose prompt" --workspace <dir> - Scheduled:
boundary schedule install <yaml>(macOS launchd / Windows Task Scheduler) - Pipelines:
boundary pipeline-run <yaml>for squad-planned multi-persona jobs
Every envelope run can be reviewed by the Third Umpire: property checks against the envelope spec, not against the agent's prose quality.
Overlays
Keep the core generic and put local skins in overlays:
boundary overlays list
boundary overlays show sample
boundary run --overlay sample --role repo-reviewer --task "Review this repo"
An overlay can provide role names, default workspace, optional bridge tools, and extra system guidance without changing the generic engine.
Security boundary
Boundary enforces a workspace write boundary and envelope write allowlist via a
pluggable sandbox driver (--sandbox-driver):
auto(default) — prefer the strongest sandbox available:srtif installed (OS-enforced egress), otherwise fall back toseatbelton macOS with a loud warning that egress is uncontained, and refuse outright where neither is available rather than silently dropping the jail. The secure path is the default, not opt-in.seatbelt— macOS write-jail; blocks local writes outside the workspace, but does not bound network egress and reads are unrestricted.srt— Anthropic sandbox-runtime (Seatbelt/bubblewrap/WFP) adds an OS-enforced network egress allowlist over the whole process tree. Set--egress-allow <domain>(empty = no network); needsnpm i -g @anthropic-ai/sandbox-runtime. Requestingsrtexplicitly is strict — it fails loudly if srt is absent rather than degrading.none— no sandbox.
Two opt-in controls harden the posture further (both enforced only under srt
for reads / require srt for bash respectively):
--deny-read <path>(repeatable) hides paths from the jailed bash process, and--deny-read-secretsalso hides a built-in set of common credential locations (~/.aws,~/.ssh,~/.config/gh, …). In a schedule YAML:deny_read: [...]anddeny_read_secrets: true. Reads are unrestricted onseatbelt/none, so the denylist is enforced only undersrt(other drivers warn).--require-srt-for-bash(enveloperequire_srt_for_bash: true) refuses thebashtool unless the driver issrt.seatbelt/nonedo not bound egress, and the commit-class bash denylist is a nudge, not a boundary — this makes a run fail closed rather than shell out with uncontained egress.
For sensitive work, prefer --sandbox-driver srt with a tight egress allowlist
(or run as a dedicated OS user / inside a container), add --deny-read-secrets
and --require-srt-for-bash, and disable shell or web tools when not needed.
Spend control
An agent you leave running is an agent spending money. Boundary treats spend as a first-class boundary, not a footnote — the same "bound it, then verify it" posture it applies to writes and egress. Six composable primitives: five that take a run from one-shot to fleet-wide, plus attribution to slice the bill.
per-run caps ─▶ fail-closed pricing ─▶ spend gradient ─▶ degrade-to-cheaper ─▶ cross-run budgets
one run honest cost soft landing cheaper tail many runs
+ cost attribution: slice any of it by project / tenant
1. Per-run caps
Every run is bounded by input/output tokens, an optional dollar ceiling, and a wall-clock kill switch. On the CLI:
boundary run ... \
--envelope-max-input-tokens 500000 \
--envelope-max-output-tokens 50000 \
--envelope-max-dollars 0.25 \
--envelope-max-wall-seconds 900
or in a schedule/pipeline YAML:
envelope:
max_input_tokens: 500000
max_output_tokens: 50000
max_dollars: 0.25
2. Fail-closed pricing
The dollar cap can only bind for a model the rate card knows. An unlisted
model used to estimate at $0.00 and sail past max_dollars entirely — an
unpriced model was an uncapped run. Unknown models are now priced at a
conservative upper bound so the cap still bites; the live banner shows
rate=fallback.
envelope:
on_unpriced_model: max_rate # default; "zero" restores fail-open, "<model-id>" borrows a rate
The same honesty applies to caching: cache reads are cheap (~0.1× input) but cache writes carry a ~1.25× premium, and each is priced on its own axis — billing a write as fresh input undercounts cached workloads.
3. Spend gradient — a soft landing, not a wall
Hitting a cap mid-write wastes the tokens already spent on an unfinished artifact. Before the hard halt at 100%, the agent is nudged to converge at fractions of whichever cap is closest to breach:
envelope:
spend_pressure_at: [0.75, 0.9] # nudge at 75% and 90%; [] disables
At 90% the agent sees "spend at 90% of cap … converge now: finish your current
write and stop" and a spend_pressure event is logged.
4. Degrade-to-cheaper-model
Instead of only nudging, swap the run onto a cheaper model once spend crosses a threshold — the expensive model does the early, high-leverage reasoning; the cheap one finishes under budget pressure. Spend is accounted per response, so the post-swap tail is priced at the cheaper rate.
envelope:
max_dollars: 1.00
degrade_to: claude-haiku-4.5 # a model in the rate card
degrade_at: 0.6 # swap at 60% of the cap
5. Cross-run budgets
Per-run caps bound one run. A schedule firing hourly at $0.50/run has no
ceiling on the sum. A budget: block bounds spend across runs over calendar
windows (daily/weekly/monthly, calendar-reset) and a trailing rolling window,
aggregated over the run-history ledger — the same data boundary history reads,
so there's no second store to drift:
budget:
daily: 5.00
weekly: 20.00
monthly: 60.00
rolling: 3.00
rolling_hours: 6
scope: workspace # or "global"
At run time a run whose window is already spent out is skipped before any
cost (stop_reason: skipped_budget); otherwise its per-run max_dollars is
clamped to the tightest remaining headroom, so primitives 3–4 enforce the
cross-run ceiling from inside the run.
6. Cost attribution
Stamp arbitrary tags on every run so the ledger can be sliced — and budgets scoped — per project, purpose, or tenant:
attribution:
tenant: acme
project: pricing
budget:
monthly: 50.00
scope: "tag:tenant" # one $50/mo budget PER tenant, summed across workspaces
Interactive runs can be tagged too — envelope-mode boundary run records an
(adhoc) ledger row carrying --attribution key=value tags, so ad-hoc spend is
sliceable alongside scheduled runs:
boundary run ... --envelope-writable scratch/note.md \
--attribution tenant=acme --attribution project=pricing --task "..."
Inspect any config's live status (exit code 3 when a window is exhausted, so
cron/CI can branch on it):
boundary budget path/to/schedule.yaml
# budget scope=tag:tenant=acme workspace=/work/acme
# monthly $3.5000 / $50.00 $46.5000 left
# -> ok; binding=monthly; next run capped at $0.2800
The budget bounds one tenant; the chargeback rollup reports every tenant — the total spend grouped by any attribution tag ("the bill"):
boundary history --by tenant # all time; --since 30 windows to a period
# spend by tenant (all time):
# acme $ 0.5500 2 run(s)
# globex $ 0.2800 1 run(s)
# total $ 0.8300 3 run(s)
Full reference and run-cost ballparks: GUIDE.md → Cost / budget knobs.
Where Boundary sits
Boundary's category is authorization + post-run verification for tool-calling agents. Here is what the envelope defends and what it doesn't — stated plainly, because differentiation by silence reads as ignorance of the field.
The lethal trifecta
The lethal trifecta — private-data access + exposure to untrusted content + external communication — is what turns a prompt injection into an exfiltration. Boundary now touches all three legs:
| Trifecta leg | Boundary today |
|---|---|
| Private-data access | Partially — reads are unbounded, but once a run reads untrusted external content, the taint gate (--on-taint) treats any subsequent write as a potential exfil channel |
| Untrusted content drives action | Bounded — the staging pivot forces a committed thesis before deep reads/writes, and the taint gate refuses/warns when untrusted content flows into a writable sink (the write-as-exfil channel) |
| External communication | Bounded — commit-tool gating + write allowlist bound irreversible/outbound actions; with --sandbox-driver srt an OS-enforced egress allowlist bounds network exfiltration across the whole process tree (default seatbelt driver does not) |
The taint dimension is coarse and file-granular — it tracks which files are
untrusted, not which bytes. A run is tainted when it fetches external content,
reads a file a prior run marked tainted, or runs bash without OS-bounded egress
(--sandbox-driver srt); a write made while tainted marks its output tainted
too. The ledger persists per workspace (outside it, so the sandboxed agent can't
clear it), so taint carries across pipeline stages and separate scheduled
runs — the stage that finally commits is no longer blind to what an earlier
stage fetched. Default is warn (a verdict line, not a block); refuse blocks
the write in any run that became tainted. Inspect/clear with boundary taint.
The honest gaps: (1) it is file-granular, not per-value — reading a tainted file
taints the whole run even if no tainted byte reaches the output (an
over-approximation, in the safe direction); (2) it catches untrusted-content →
write/commit sinks, not exfil through a second fetch_url (data in a URL is
an external read, not a gated sink) — that channel is closed only by srt's OS
egress allowlist, and a tainted run without srt is flagged egress_uncontained
(a Third Umpire failure). Per-value information-flow tracking remains future work.
The six secure-agent design patterns
Against the six design patterns for securing LLM agents, Boundary is mainly a Plan-Then-Execute system — the staging pivot is its "commit a plan before acting" — with Action-Selector typed commit tools for irreversible actions. The twist those patterns don't have: a post-run check (the Third Umpire) that the plan actually held. Boundary does not implement Dual-LLM or Context-Minimization isolation; those stay available as overlays if coarse controls prove insufficient. A fixed plan protects action choice, not action parameters — Boundary inherits that limit and names it.
How this differs from neighbors
Neighbor characterizations are from the coding-agent sandbox census.
| Project | Category | Boundary's difference |
|---|---|---|
| predicate-secure | Policy authz + post-run verification (closest sibling) | Same shape, plus the staging pivot: a mid-run gate that makes a refused write resume from a staged thesis instead of restarting research |
| Cupcake | OPA/Rego policy hooks on tool calls | Boundary's authz is a typed envelope (write allowlist + commit policy), not a general policy engine — narrower, but the Third Umpire grades whether the envelope held, which a hook layer doesn't |
| nah | allow / ask / block guard on commands | Same allow/ask/refuse verbs, but attached to typed tool kinds and a write-count budget, with post-run grading on top |
The primitive none of them have is the staging pivot — forcing the agent to stage a provisional answer mid-run, then resuming a refused write from that stage rather than from scratch. That is Boundary's differentiator.
Read the guide
GUIDE.md — operational manual. Setup, all three modes, schedule syntax, cost knobs, troubleshooting. Read this first.
Examples
examples/README.md has runnable starter recipes:
- Prompt files for research, repo review, doc maintenance, and release notes
- A tiny sample workspace you can safely let agents inspect and write into
- Schedule YAMLs for daily/weekly headless runs
- Pipeline YAMLs for multi-persona runs
- A sample overlay that maps role names to local prompt files
Install
Public alpha — pin to a tag once you have a workflow you like.
# Recommended: isolated install via pipx
pipx install git+https://github.com/mavaali/boundary.git
# Or in a venv
python3 -m venv .venv && source .venv/bin/activate
pip install git+https://github.com/mavaali/boundary.git
Requires Python 3.10+. After install, authenticate the Copilot backend once:
boundary copilot login # device-code flow, opens https://github.com/login/device
boundary copilot status # should say "oauth token: present"
For local development (clone + editable install), see GUIDE.md.
Backends
Copilot is the default, not a requirement. Pick a backend with --client
(or client: in a schedule/pipeline YAML) and the model with --model:
--client |
Auth | Default model | Billing |
|---|---|---|---|
copilot (default) |
boundary copilot login (OAuth device flow) |
claude-sonnet-4.5 |
your GitHub Copilot subscription |
anthropic |
ANTHROPIC_API_KEY |
claude-sonnet-4-5 |
Anthropic API (metered, per-token) |
openrouter |
OPENROUTER_API_KEY |
anthropic/claude-haiku-4.5 |
OpenRouter (metered) |
together |
TOGETHER_API_KEY |
Qwen/Qwen2.5-Coder-32B-Instruct |
Together (metered) |
export ANTHROPIC_API_KEY=sk-ant-...
boundary run --client anthropic --model claude-sonnet-4.6 --task "..."
On Claude subscriptions: the anthropic client uses an API key (metered
API billing), which is separate from a Claude.ai Pro/Max plan. There's no
"log in with your Claude subscription" client — unlike GitHub Copilot, Anthropic
doesn't expose a third-party API for Pro/Max quota (it's consumed through
claude.ai, the apps, and Claude Code). If you want subscription economics, that's
Claude Code's lane, not Boundary's.
Quick start
boundary copilot login # first-time only
boundary run \
--system-file examples/prompts/researcher.md \
--workspace examples/workspaces/sample-repo \
--envelope-writable "scratch/snapshot-$(date +%F).md" \
--envelope-min-writes 1 --envelope-max-writes 3 \
--max-iters 25 \
--task "Summarize the repo and identify one improvement." --verbose
The
examples/tree ships with the package. Copy it next to your working directory or pass absolute paths if you installed viapipx.
Doctrine
Built on the principles in Hallucinated Intent and the Envelope Problem. The agent is not an employee. The envelope is the game plan. The Fielding Coach sets it before play; the Third Umpire checks whether it held afterward.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file boundary_envelope-0.12.0.tar.gz.
File metadata
- Download URL: boundary_envelope-0.12.0.tar.gz
- Upload date:
- Size: 179.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e6fe2c1a0cb2e40b02a1f478e5f6382b8c4717035dd706e4c67fd9c7df86ea3
|
|
| MD5 |
791e553212f5f22ccdd54a73d7c54cc0
|
|
| BLAKE2b-256 |
8b304c2a64d67ba8c5d8d3e50375162250203f0a5d71f28fa7be1f48216d0993
|
Provenance
The following attestation bundles were made for boundary_envelope-0.12.0.tar.gz:
Publisher:
workflow.yml on mavaali/boundary
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boundary_envelope-0.12.0.tar.gz -
Subject digest:
6e6fe2c1a0cb2e40b02a1f478e5f6382b8c4717035dd706e4c67fd9c7df86ea3 - Sigstore transparency entry: 2083890499
- Sigstore integration time:
-
Permalink:
mavaali/boundary@8a742fc74ecd3f29e5749aa9370a6acdfeab1fde -
Branch / Tag:
refs/tags/v0.12.0 - Owner: https://github.com/mavaali
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@8a742fc74ecd3f29e5749aa9370a6acdfeab1fde -
Trigger Event:
push
-
Statement type:
File details
Details for the file boundary_envelope-0.12.0-py3-none-any.whl.
File metadata
- Download URL: boundary_envelope-0.12.0-py3-none-any.whl
- Upload date:
- Size: 152.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b76ea99a0a3e60cb0b70eb13165ac295bcceaa0852e92818b1966da997f53062
|
|
| MD5 |
80edccb304a35a8d7f82abde9b04d4f6
|
|
| BLAKE2b-256 |
da0c32dbec5bc8fbb76a52e04c39d0d1371addf921ba21b16d513b1b781f4462
|
Provenance
The following attestation bundles were made for boundary_envelope-0.12.0-py3-none-any.whl:
Publisher:
workflow.yml on mavaali/boundary
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boundary_envelope-0.12.0-py3-none-any.whl -
Subject digest:
b76ea99a0a3e60cb0b70eb13165ac295bcceaa0852e92818b1966da997f53062 - Sigstore transparency entry: 2083890531
- Sigstore integration time:
-
Permalink:
mavaali/boundary@8a742fc74ecd3f29e5749aa9370a6acdfeab1fde -
Branch / Tag:
refs/tags/v0.12.0 - Owner: https://github.com/mavaali
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@8a742fc74ecd3f29e5749aa9370a6acdfeab1fde -
Trigger Event:
push
-
Statement type: