Security / DevSecOps • Authorization & AI Safety
Scope-Gated Autonomous Agent: Permission-Bounded Tool Execution
Quick Links
The interesting security idea here is that the agent’s safety doesn’t rest on the model behaving. Capability is decided below the model: a tool only exists in the agent’s toolset if the bot’s JWT carries the scope for it, every mutation is written to an audit trail, and the rules that actually matter — compliance, suppression, validation — are enforced by the backend the agent has to go through. The model is free to plan; what it can do is bounded by least privilege.
Summary
- Problem: An autonomous agent that can email customers and modify a CRM is dangerous if its permissions live in the prompt — models can be coaxed into doing things they shouldn't.
- Solution: Bound capability at the tool layer — register a tool only when the bot's JWT carries the matching scope — and enforce compliance server-side, outside the agent's reach.
- Impact: The agent literally cannot call a tool it isn't scoped for; every action it takes is recorded; and regulatory compliance doesn't depend on the model behaving.
Context
The agent ("reason→tool loop" built on the Vercel AI SDK with Anthropic Claude) acts on a real CRM — it can search and create contacts, send single emails, run campaigns, and build automations. The security question isn't "is the model smart enough" but "what can it do at all, and who said so." The answer is enforced below the model, in how tools are wired and what the backend will accept.
Symptoms / Failure Modes
- Prompt-based "you may not do X" guardrails are bypassable and don't constrain real capability
- An over-permissioned agent could email unsubscribed contacts or mass-mutate the CRM
- Without an audit trail, autonomous actions are impossible to review after the fact
Goals, Requirements, Constraints
Goals
- Bound the agent's real capabilities by least-privilege scopes, not by trust in the model
- Make it impossible for the agent to invoke a tool it isn't authorized for
- Record every state-changing action for review
- Keep regulatory compliance (CAN-SPAM, suppression) outside the agent's control
Constraints
- The agent authenticates to the backend with a scoped service-account (bot) JWT
- All writes go through the backend API; the agent has no direct DB connection
Non-Goals
- Relying on prompt instructions as a security control
- Giving the agent direct database access
Approach
At startup the agent builds its toolset from the scopes present in its bot JWT. Read-only tools (knowledge retrieval, contact search) are always available; each write-capable tool is conditionally registered only when the corresponding scope is held — crm:write, email:send, email:blast, automation:fire. A tool that isn't registered simply doesn't exist for the model to call. Every mutating tool records an entry in an audit table, and compliance (unsubscribe footer, suppression of unsubscribed contacts) is enforced by the backend, which the agent must go through.
Key Design Decisions
- Decision: Gate capability at tool registration, keyed on JWT scopes
Why: The most robust control is making an unauthorized action unrepresentable. buildTools() reads the bot's scopes and only assigns a write tool into the toolset when the scope is present, so the model can't call what was never registered.
Alternatives: System-prompt restrictions — convenient but bypassable; they constrain phrasing, not capability. - Decision: No direct database access for the agent
Why: All mutations flow through the backend API, so server-side rules (validation, compliance, suppression) apply uniformly and the agent can't sidestep them.
Alternatives: Direct DB writes would be faster but would move enforcement into the least-trusted component. - Decision: Server-side compliance enforcement
Why: The backend attaches the unsubscribe footer and blocks unsubscribed contacts on every send. Compliance therefore holds regardless of what the agent decides — it's a property of the system, not the prompt.
Alternatives: Asking the agent to remember the rules — unacceptable for a legal requirement. - Decision: Contract tests pinning every backend call
Why: A silent key-casing mismatch (PascalCase vs snake_case) can make the backend ignore a field and behave wrongly. Tests assert the exact method, path, and JSON shape of each tool call so binding bugs fail loudly.
Alternatives: Manual testing — too easy to miss a quietly-ignored field.
Implementation
Components / Modules
- Scope-gated toolset (buildTools): Reads the bot's scopes; always registers read tools (kb_retrieve, crm_search_contacts, …); conditionally registers write tools (email_send, campaign_*, automation_*, crm write ops) only when the matching scope is held.
- Reason→tool loop: Vercel AI SDK + Anthropic Claude; the model plans and calls tools from whatever toolset it was given — which is exactly the set its scopes permit.
- Audit log (agent_actions): Every mutating tool records conversation_id, action, target, result, optional approver, and detail — a reviewable trail of what the agent actually did.
- Backend compliance layer: Attaches the CAN-SPAM unsubscribe footer and refuses sends to unsubscribed contacts; the agent has no path around it.
// buildTools() — a write tool is only added to the set when the bot holds the scope.
export function buildTools(ctx: ToolContext) {
const canSend = ctx.scopes.includes("email:send");
const canFire = ctx.scopes.includes("automation:fire");
const tools: ToolSet = { kb_retrieve, crm_search_contacts /* read-only, always present */ };
if (canSend) Object.assign(tools, { email_send }); // real single send
if (canFire) Object.assign(tools, { automation_create, automation_enroll });
// ...crm:write and email:blast gated the same way...
return tools; // the model can only call what's registered
}
Security
- Prompt injection coaxing a forbidden action: the tool isn't registered, so there's nothing to call
- Over-permissioned bot token: scopes are minimal and explicit; capability tracks the token, not the prompt
- Emailing unsubscribed contacts: blocked server-side regardless of agent intent
- Silent backend-binding bug: contract tests pin exact request shapes to catch ignored fields
Controls Implemented
- Least-privilege tool registration keyed on the bot JWT's scopes (crm:read/write, email:send/blast, automation:fire)
- No direct database access — all writes go through the backend API
- Server-side compliance (unsubscribe footer + suppression of unsubscribed contacts)
- Audit logging of every mutating action to the agent_actions table
- Contract tests pinning method/path/JSON shape for each tool call
Verification
- Unit/contract tests assert each tool's exact backend request and reject wrong key casing
- Read vs write tools verified to register only under the appropriate scopes
Operations
Observability
- agent_actions provides a queryable record of every action, target, and result
- Action results (success/failed) and error details captured for review
Incident Response
- Suspected misuse: review agent_actions for the conversation; revoke or narrow the bot's scopes to immediately cut capability
- Compromised bot token: rotate the service-account credentials; scopes bound the blast radius in the meantime
Cost Controls
- Runs as a lightweight Node service under systemd on existing Hetzner infrastructure
- Scheduled work (knowledge ingestion, daily planning, due-task runs) driven by systemd timers rather than a standing scheduler
Results
Outcomes
- Authorization: The agent's real capabilities are bounded by least-privilege scopes — an unauthorized tool is unrepresentable, not just discouraged.
- Accountability: Every state-changing action is recorded with its target and result, making autonomous behavior reviewable.
- Compliance: CAN-SPAM footer and suppression are enforced server-side, so legal compliance doesn't depend on the model.
- Reliability: Contract tests catch silent backend-binding regressions before they ship.
Tradeoffs
- Routing everything through the backend (no direct DB) is slightly slower but keeps enforcement in the trusted layer
- Scope-gating per tool means new capabilities require an explicit scope + registration step — friction by design
Next Steps
- Optional human-approval gating for the highest-impact tools (mass sends)
- Per-action rate limits on top of scope gating