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​