Skip to main content

Tool-calling agent harness with envelopes, staging gates, schedules, and post-run evaluation

Project description

Boundary

selftest

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: srt if installed (OS-enforced egress), otherwise fall back to seatbelt on 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.
  • srtAnthropic sandbox-runtime (Seatbelt/bubblewrap/WFP) adds an OS-enforced network egress allowlist over the whole process tree. Set --egress-allow <domain> (empty = no network); needs npm i -g @anthropic-ai/sandbox-runtime. Requesting srt explicitly 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-secrets also hides a built-in set of common credential locations (~/.aws, ~/.ssh, ~/.config/gh, …). In a schedule YAML: deny_read: [...] and deny_read_secrets: true. Reads are unrestricted on seatbelt/none, so the denylist is enforced only under srt (other drivers warn).
  • --require-srt-for-bash (envelope require_srt_for_bash: true) refuses the bash tool unless the driver is srt. seatbelt/none do 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.mdCost / 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 via pipx.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

boundary_envelope-0.12.0.tar.gz (179.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

boundary_envelope-0.12.0-py3-none-any.whl (152.3 kB view details)

Uploaded Python 3

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

Hashes for boundary_envelope-0.12.0.tar.gz
Algorithm Hash digest
SHA256 6e6fe2c1a0cb2e40b02a1f478e5f6382b8c4717035dd706e4c67fd9c7df86ea3
MD5 791e553212f5f22ccdd54a73d7c54cc0
BLAKE2b-256 8b304c2a64d67ba8c5d8d3e50375162250203f0a5d71f28fa7be1f48216d0993

See more details on using hashes here.

Provenance

The following attestation bundles were made for boundary_envelope-0.12.0.tar.gz:

Publisher: workflow.yml on mavaali/boundary

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file boundary_envelope-0.12.0-py3-none-any.whl.

File metadata

File hashes

Hashes for boundary_envelope-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b76ea99a0a3e60cb0b70eb13165ac295bcceaa0852e92818b1966da997f53062
MD5 80edccb304a35a8d7f82abde9b04d4f6
BLAKE2b-256 da0c32dbec5bc8fbb76a52e04c39d0d1371addf921ba21b16d513b1b781f4462

See more details on using hashes here.

Provenance

The following attestation bundles were made for boundary_envelope-0.12.0-py3-none-any.whl:

Publisher: workflow.yml on mavaali/boundary

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page