Skip to main content

Adding Governance

Governance in Waxell has two layers, and most teams grow from the first into the second:

  • What the agent author declares — per-agent caps in waxell.yaml. Fast to set, scoped to one agent, and enough on day one.
  • What the operator configures — policies in the engine, scoped across agents, workflows, tools, models, and teams, evaluated server-side.

The second layer is the enforcement backbone: an agent can't opt out of a policy by editing its own manifest. If you're governing more than a couple of agents, that's where you want the rules to live.

Per-agent limits

Declare caps on the agent:

agents:
- name: email_sender
max_budget_usd: 2.00 # hard USD cap (default 10.00)
max_turns: 15 # agent turns (default 25)
timeout_seconds: 120 # wall time

Set them explicitly — the defaults are generous, and a looping agent will spend to them.

For a spawned child, caps are set at the call site and debit from the parent:

findings = await ctx.spawn(
"research_agent",
inputs={"topic": topic},
budget_usd=2.00,
timeout_seconds=300,
)

Approval requirements

Approval is a property of the domain action:

domains:
- name: accounts
actions:
- name: delete_account
requires_approval: true

When the agent calls it, the run pauses with PausedReason.awaiting_approval and holds no compute until a human decides. It resumes from the checkpoint — the work already done is not repeated.

This is the right place for it: the approval attaches to the action, so every agent that can call accounts.delete_account inherits the requirement, and you can't forget it on the fifth agent.

Where policies are evaluated

Policies run at three points, and the differences matter:

WhenSeesTypical use
Before the runInputs, agent identity, callerBlock disallowed inputs; require approval up front
Mid-runEach tool call, before it executesStop a specific action — a write, a spend, an external send
After the runFull output and span treeRedact, flag for review, raise an incident

Mid-run is the one people underestimate: an agent that decided to do something disallowed is stopped at the call, not cleaned up afterwards.

A blocked run is a first-class outcome — it finishes in the terminal BLOCKED state with a recorded decision and an ExecutionIncident. It does not raise, so there is nothing to try/except. Code that polls for only COMPLETED or FAILED hangs forever; see run states.

Policies: the engine behind all of this

Limits and approvals cover the agent you're writing. Policies are the layer above — rules an operator sets that apply across agents, scoped to a tenant, agent, workflow, tool, model, user, or team, and evaluated server-side at the three points above.

Waxell ships 49 policy categories. A few you'll reach for early:

CategoryGoverns
BudgetToken and dollar budgets — per-run, daily, monthly
Rate LimitRequest rates and concurrency per agent, user, or team
ContentInput/output scanning for PII, credentials, injection
Data AccessWhich sources an agent may read or write
NetworkOutbound domain allowlists
ScopeBlast radius — records modified, transaction amounts
ApprovalWhat requires a human before it proceeds
Kill SwitchEmergency stop on error rate or anomaly

The full set — including OWASP LLM Top 10, GDPR, HIPAA, SOC 2, ISO 42001, and NIST AI RMF categories — is in Policy Categories & Templates.

Every check returns an action: allow, warn, redact, throttle, block, or skip. See Policy & Governance for how the evaluation and the actions work.

Policy as code

Define policies alongside your agents and push them like any other artifact:

from waxell_sdk import policy

outreach_budget = policy(
name="outreach_budget",
category="budget",
scope={"agents": ["outreach_agent"]},
rules={
"per_workflow_token_limit": 50_000,
"per_workflow_cost_limit": 1.00,
},
description="Budget ceiling for outreach agent runs.",
)
wax -p prod policies push ./governance/ --dry-run   # show the diff
wax -p prod policies push ./governance/ # apply

This creates real, scoped policy records — the same objects the Governance panel edits. Inspect what's live with wax -p prod policies list and wax -p prod policies show <name>.

Scope is the important field. {"agents": ["outreach_agent"]} narrows a policy to one agent; omit it and the policy applies tenant-wide. You can also scope by workflows, tools, models, user_groups, and more.

Promoting policy between environments

Policy bundles move a curated set of policies between tenants — a golden config rollout, or a snapshot before a change:

EndpointPurpose
GET /v1/governance/bundles/export/Current policy set as a versioned JSON bundle
POST /v1/governance/bundles/preview/Dry-run import — per-policy diff, writes nothing
POST /v1/governance/bundles/import/Apply, with mode=merge|replace and conflict=skip|overwrite

Imports auto-snapshot the affected policies into PolicyVersion first, so a rollout is reversible.

Test before you enforce

Two things worth doing before a policy goes live:

  • Dry-run a draft against real traffic to see what it would have caught, before it blocks anything.
  • Check for conflicts — overlapping scopes where two policies disagree surface as a conflict rather than as a mystery block in production.

Both are in the Governance panel alongside the policy editor.

The audit trail

Every run is recorded automatically. There is nothing to enable and nothing to instrument:

ArtifactHolds
Span treeEvery LLM, tool, and domain call — nested, timed, errors marked
LlmCallRecordModel, tokens in/out, cost, provider instance
Policy decisionsWhat evaluated, what it decided, why
ExecutionIncidentErrors, policy blocks, budget stops
Audit logWho or what triggered the run, under which identity

A failed or blocked run is recorded as fully as a successful one.

Inspect from the CLI:

wax -p prod traces list --hours 24 --agent email_sender
wax -p prod traces show <trace_id>

Or open Observe → Runs for the same trace with the span tree rendered.

Next steps