Agent Service Account Scope Policy
The agent-service-account-scope policy enforces OWASP LLM06b (excessive agency, sub-control "delegated authority"). When an agent acts on behalf of a user inside a SaaS app — Confluence, Jira, Salesforce, Workday — it must run under a least-privilege service account rather than inheriting the user's full session. Without this control, an agent transparently inherits every page its user can see, every project it can edit, every record it can delete.
The handler enforces on two axes:
- Identity check — context must carry a service-account marker (
metadata.service_account,_sub_user_identity, orcontext.service_account). No marker = raw-user run = block. - Operation scope — even when an SA is configured, sensitive operations (admin/destructive/cross-tenant) require the SA to declare allowed-scope membership.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
require_service_account | boolean | true | When true, the run must carry an SA marker |
service_account_field | string | "service_account" | Key in context.metadata carrying the SA id |
restricted_operations | string[] | [] | Operation-name globs (fnmatch) that require an SA |
restricted_workflow_types | string[] | [] | Workflow types where an SA is mandatory regardless of require_service_account |
allowed_service_account_pattern | string | "" | Optional regex the SA id must match (e.g., "^sa_.*") |
action_on_violation | string | "block" | "block" or "warn" |
How It Works
The handler runs at before_workflow (identity pre-check) and mid_execution (per-pending-action scope check). after_workflow is a no-op.
| Phase | What it checks | Action |
|---|---|---|
before_workflow | require_service_account true or workflow_type in restricted_workflow_types, and no SA resolved | BLOCK or WARN |
before_workflow | SA id doesn't match allowed_service_account_pattern regex | BLOCK or WARN |
mid_execution | Each context.pending_actions entry matching restricted_operations (fnmatch) while no SA is set | BLOCK or WARN |
after_workflow | (no-op) | ALLOW |
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.metadata[service_account_field] | before, mid | Primary SA marker |
context.service_account | before, mid | Direct attribute fallback |
context._sub_user_identity | before, mid | Phase B sub-user marker (string or dict with sub_user_id/id) |
context.workflow_type | before, mid | Matched against restricted_workflow_types |
context.pending_actions | mid | List of {"kind": "tool_call", "tool": "...", "args": {...}} to gate |
Example Policy
Atlassian Rovo-style policy — Jira/Confluence agent must run as a scoped SA, and admin/delete/bulk operations are gated:
{
"require_service_account": true,
"service_account_field": "service_account",
"restricted_operations": ["admin_*", "delete_*", "bulk_*", "transfer_*"],
"restricted_workflow_types": ["administrative", "financial"],
"allowed_service_account_pattern": "^sa_jira_",
"action_on_violation": "block"
}
SDK Integration
import waxell_observe as waxell
waxell.init()
@waxell.observe(agent_name="jira-triage", enforce_policy=True)
async def triage_ticket(ticket_id: str) -> str:
# before_workflow: blocks if no service_account in metadata
return await process_ticket(ticket_id)
# Pass the SA marker via metadata
async with waxell.WaxellContext(
agent_name="jira-triage",
enforce_policy=True,
metadata={"service_account": "sa_jira_triage_v1"},
) as ctx:
await triage_ticket("PROJ-123")
Observability
Evaluations appear in the Governance tab with:
| Field | Example |
|---|---|
| Category | agent-service-account-scope |
| Action | block |
| Reason | "No service account configured — agent would inherit the user's full permissions on this surface." |
| Metadata | {"signal": "no_service_account", "workflow_type": "administrative", "owasp": "LLM06"} |
Restricted-operation block:
| Field | Example |
|---|---|
| Action | block |
| Reason | "Operation 'delete_user' requires a scoped service account; the agent is running as the raw user." |
| Metadata | {"signal": "restricted_operation_without_sa", "operation": "delete_user", "owasp": "LLM06"} |
Common Gotchas
restricted_operationsuses fnmatch globs, not regex. Useadmin_*to matchadmin_create,admin_delete— not^admin_.*.- SA resolution falls through three sources.
metadata[service_account_field]→context.service_account→context._sub_user_identity. Setting any of them satisfies the identity check. _sub_user_identityaccepts a dict OR a string. When it's a dict, the handler looks atsub_user_idthenid. Other keys are ignored.- Empty
restricted_operationsskips the mid-execution check entirely. The pre-run identity check still fires. - The pattern check only runs when an SA exists. An SA that's missing entirely produces the
no_service_accountblock, not a pattern mismatch.
Next Steps
- Policy Categories — All 49 categories
- Tool Argument Schema — Companion control for tool arguments
- Identity Policy — Broader identity / sub-user enforcement
- Delegation Policy — Multi-agent trust boundaries