Skip to main content

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:

  1. Identity check — context must carry a service-account marker (metadata.service_account, _sub_user_identity, or context.service_account). No marker = raw-user run = block.
  2. Operation scope — even when an SA is configured, sensitive operations (admin/destructive/cross-tenant) require the SA to declare allowed-scope membership.

Rules

RuleTypeDefaultDescription
require_service_accountbooleantrueWhen true, the run must carry an SA marker
service_account_fieldstring"service_account"Key in context.metadata carrying the SA id
restricted_operationsstring[][]Operation-name globs (fnmatch) that require an SA
restricted_workflow_typesstring[][]Workflow types where an SA is mandatory regardless of require_service_account
allowed_service_account_patternstring""Optional regex the SA id must match (e.g., "^sa_.*")
action_on_violationstring"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.

PhaseWhat it checksAction
before_workflowrequire_service_account true or workflow_type in restricted_workflow_types, and no SA resolvedBLOCK or WARN
before_workflowSA id doesn't match allowed_service_account_pattern regexBLOCK or WARN
mid_executionEach context.pending_actions entry matching restricted_operations (fnmatch) while no SA is setBLOCK or WARN
after_workflow(no-op)ALLOW

Context Attributes Read

AttributePhasePurpose
context.metadata[service_account_field]before, midPrimary SA marker
context.service_accountbefore, midDirect attribute fallback
context._sub_user_identitybefore, midPhase B sub-user marker (string or dict with sub_user_id/id)
context.workflow_typebefore, midMatched against restricted_workflow_types
context.pending_actionsmidList 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:

FieldExample
Categoryagent-service-account-scope
Actionblock
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:

FieldExample
Actionblock
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_operations uses fnmatch globs, not regex. Use admin_* to match admin_create, admin_delete — not ^admin_.*.
  • SA resolution falls through three sources. metadata[service_account_field]context.service_accountcontext._sub_user_identity. Setting any of them satisfies the identity check.
  • _sub_user_identity accepts a dict OR a string. When it's a dict, the handler looks at sub_user_id then id. Other keys are ignored.
  • Empty restricted_operations skips 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_account block, not a pattern mismatch.

Next Steps