End-User Suspension Policy
The end-user-suspension policy category blocks execution when the current run's sub-user has WaxellUser.status = 'suspended'. It's the per-end-user analogue of the tenant Kill Switch -- you can suspend a single bad actor without affecting the rest of the tenant.
Runs at before_workflow, mid_execution, and before_domain_call so a suspension flip takes effect within the next turn for long-running workflows.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Toggle enforcement on/off |
grace_seconds | integer | 0 | Reserved -- tolerate recent status changes by N seconds |
The handler reads the user's status directly from the WaxellUser row. There is no policy rule to tune which users are suspended -- that's controlled by the user record itself.
How It Works
The handler runs at:
| Phase | Effect when suspended |
|---|---|
before_workflow | BLOCK -- run never starts a turn |
mid_execution | BLOCK -- halts at next turn boundary |
before_domain_call | BLOCK -- no outbound call fires |
after_workflow | ALLOW (no-op -- run already halted) |
When no sub-user is on the context, the handler returns ALLOW. This category is scoped to sub-user identity only; for tenant-wide bans use the Kill Switch Policy.
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.sub_user_identity | all | Resolve sub_user_id |
context.user_id | all | Fallback resolver |
context.metadata["tenant_id"] / context.tenant_id | all | Scope the user lookup |
Data Sources
WaxellUser.status-- compared againstWaxellUserStatus.SUSPENDED
Example Policy
{
"name": "Block suspended sub-users",
"category": "end-user-suspension",
"rules": {
"enabled": true,
"grace_seconds": 0
},
"scope": {
"agents": ["*"]
},
"enabled": true
}
A single policy like this on agents: ["*"] covers your whole tenant. You don't need to author per-user policies -- suspension state lives on the user row, not the policy.
SDK Integration
import waxell_observe as waxell
waxell.init()
@waxell.observe(
agent_name="customer-support",
user_id="cust-9912",
enforce_policy=True,
)
async def handle(text: str) -> str:
return await respond(text)
To suspend a user:
wax end-users suspend cust-9912
# or via API:
curl -X POST /api/v1/end-users/cust-9912/suspend/
To unsuspend:
wax end-users unsuspend cust-9912
Observability
| Field | Example (BLOCK) |
|---|---|
| Category | end-user-suspension |
| Action | block |
| Reason | End-user 'cust-9912' is suspended on this tenant. Unsuspend via /api/v1/end-users/<id>/unsuspend/ or wax end-users unsuspend <id>. |
| Metadata | {"sub_user_id": "cust-9912", "tenant_id": "acme"} |
Common Gotchas
- No sub-user on context = ALLOW. Tenant-wide bans must use the Kill Switch Policy.
- Suspension flip takes effect at next turn boundary, not instantly. A long-running step in flight will finish before the next BLOCK fires.
- Lookup errors fail-open. If the user lookup raises, the handler returns ALLOW with a warning log. Better to let a real run proceed than to take everyone down on a transient DB error.
- No rule to "suspend user X". Suspension state is on the
WaxellUserrow; the policy just decides whether to enforce it. Tenants who want suspension behavior on always can leave this category enabled tenant-wide. grace_secondsis reserved. Currently not honored by the handler; future phase will let you tolerate very recent status flips to avoid race conditions on suspend/unsuspend.- Observe plane uses Django ORM. Runtime plane wires a cached / Redis lookup via
_status_query_fn.
Next Steps
- Kill Switch Policy -- Tenant-wide emergency stop
- End-User Budget -- Per-user monthly spend cap
- End-User Rate Limit -- Per-user request-rate cap
- User Tracking -- How sub-user identity flows
- Policy Categories & Templates -- All categories