Agent Scope Declaration
A scope is a declared policy identity for an agent: what it is for, which tools it may use, and when its authority expires. You declare it once at spawn; Shrike enforces it on every scan, on every channel. Tool calls outside the declaration route to human approval before they execute, and an expired scope stops acting silently.
The model
- You declare; Shrike enforces. The scope comes from the operator or the agent framework at spawn, never from the agent's own reasoning. Shrike is the enforcement mechanism, not an authority of its own.
- No scope, no check. Agents without a declared scope are unaffected — existing integrations keep working unchanged. Declaring a scope opts an agent into enforcement.
- Forbidden beats allowed.
forbidden_toolstakes precedence overallowed_tools;["*"]in the allowlist permits any tool not explicitly forbidden. - Scopes expire. Whichever comes first of
expires_atandmax_duration_secondsbounds the scope; re-declaring refreshes the duration. After expiry every action needs approval until the scope is refreshed. - Scopes can cap volume. An optional
max_actionsceiling bounds how many scans the scope passes; at the ceiling, further actions are held for approval until the scope is re-declared (which resets the count). A runaway loop hits a limit the operator chose, not the credit card. - Scope cuts both ways. The declared purpose also grounds Shrike's semantic analysis, so an agent doing exactly what it was scoped to do isn't second-guessed as an attacker. This grounding covers in-scope business activity only — it never excuses attacks on the AI itself (jailbreak, injection, prompt extraction), so a scope can't be used to self-whitelist.
Declare a scope
One request, upsert semantics — the same (customer, agent_id) pair overwrites the previous declaration:
curl -X POST https://api.shrikesecurity.com/agent/api/v1/agent/scope/declare \
-H "Authorization: Bearer $SHRIKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "recon_agent_q2_2026",
"purpose": "invoice reconciliation for FY26 Q2",
"allowed_tools": ["read_invoice", "match_ledger_entry", "post_reconciliation_note"],
"forbidden_tools": ["exec_shell", "network_egress"],
"max_duration_seconds": 28800
}'The response returns the persisted scope with its computed active_until. Retrieve with GET /api/v1/agent/scope/{agent_id}. An operator removes it with DELETE from the dashboard, and the agent returns to unscoped mode. The agent's own key is refused there: removing a scope widens it, and an agent never widens itself. To run with less, declare a narrower scope.
Fields
| Field | Required | Meaning |
|---|---|---|
agent_id | yes | The agent's persistent identity. Scopes are keyed per customer + agent. |
purpose | no | Human-readable task description. Shown in audit records and used to ground semantic analysis. |
allowed_tools | yes | Exact tool names the agent may call. ["*"] allows any; an empty list allows none. |
forbidden_tools | no | Tools whose every call waits for a person, even under the ["*"] wildcard; the hold names the scope as the reason. Takes precedence over the allowlist. Shrike never refuses a kind of tool outright: a mistaken or hallucinated call gets a person's look. |
authoring_paths | no | Repository paths where the agent maintains detection content. A file written under one still runs every detector; content-pattern hits are recorded as warn instead of refused, because rule text looks like what it detects. Not a write boundary: a write outside them is judged on its content, not refused for sitting outside. Operator-granted; an agent refresh cannot add one. |
guardrail_paths | no | The files that decide what the agent may do (hooks, settings, instruction files, CI), each with a tier: block, require_approval or warn. A write under one resolves to that tier whatever the content said, and a file whose content writes against one is held for a person. Adding or raising one is free; removing or lowering one needs an operator. |
work_profile | no | general (default) or security_engineering: the trade the scope is declared for, which decides what a live scope records rather than refuses. General records code that describes a threat on file writes. Security engineering adds exfiltration-class findings and verdicts the model reached alone, on files, commands and queries. Credential material and prompt injection stay refused under both. A wider trade is a widening, operator only. |
max_duration_seconds | no | Relative TTL from the latest declaration; re-declaring refreshes it. |
max_actions | no | Action ceiling: scans passed under this scope before further actions are held for approval. Re-declaring resets the count. Counted per backend instance, so treat it as a tripwire that can overshoot slightly under horizontal scaling, not an exact meter. |
expires_at | no | Absolute expiry. Whichever of the two bounds comes first wins. |
renewable_seconds | no | Renewal window, from the operator's last save: how long the agent's own key may keep refreshing the scope (same or narrower bounds). Past it only an operator can refresh or revive it. Absent means no ceiling. Operator-set; an agent refresh always keeps the stored value. |
enforcement_mode | no | enforce (default) or observe. Observing, every verdict is still computed and a refusal is recorded as what would have happened while the action proceeds. The safety floor (corroborated injection, credential material) still enforces. Operator-set only: entering observe mode is a widening, so an agent refresh keeps the stored value and cannot enter or extend it. |
observe_until | with observe | When observe mode ends on its own, at most seven days from the save. Required whenever the mode is observe: observing is a state with an end, not a flag. Past it the scope enforces again without anyone touching it; an operator renews by saving again. |
Refreshing a scope
A refresh is the same request from the agent's own key. Every field it omits is inherited from the scope on file, so {"agent_id": "...", "max_duration_seconds": 7200} is a complete refresh: the time limit starts again, the tools, authoring paths and action ceiling stay as the operator set them, and a scope that has already lapsed comes back. A refresh may narrow anything; it may not add tools, drop forbidden tools, extend the time limit, raise the action ceiling, add authoring paths, or move the renewal window. Those answer 403 with reason: "widening" and a widening delta naming the axis.
The renewal window bounds how long that self-refresh works. Once it closes the same request answers 403 with reason: "ceiling_reached" and renewable_until; an operator refreshes or edits the scope on the Agents screen, which opens a new window. Every response carries renewable_until and ceiling_reached next to active_until and expired, so a client can see the ceiling coming.
A scope file in the repository
A repository can carry its default scope as a checked-in file, .shrike/scope.json, with the same fields as a declaration minus the agent id. The coding-agent hook finds it above the working directory, stops at the repository root, and applies it under the seat's own key when a session first acts, whenever the file changes, and every half hour after that. Every developer machine that scans from the repository is governed by it from its first action.
Under the agent's key the file can only narrow: what lands is the narrowest of the file and the scope already on record, and the file's privileges (authoring paths, a wider work profile) are set aside until an operator applies the file from the Agents screen, which replaces each chosen row whole after a per-agent preview. The file's own path is added to the guardrail list at require_approval, so an agent's edit to the file that governs it is held for a person. Unknown keys are refused; enforcement_mode, expires_at and agent_id are not file fields. The same operation is POST /api/v1/agent/scope/apply-file with {"agent_ids": [...], "file": {...}, "dry_run": true}.
What enforcement looks like
The check runs inside the scan pipeline itself, before any verdict is returned — so REST, the SDKs, and MCP all inherit it with no per-channel configuration. On every scan that carries the agent's identity:
- A tool call on the allowlist proceeds through normal scanning.
- A tool call outside the scope returns a
scope_violationinviolations[]withrefuse_tier: require_approval— the session is not killed; a human decides. - An expired scope returns
scope_expired, also routed to approval, whether or not a tool is being called. This catches agents that keep running past their task window. - A scope past its declared
max_actionsreturnsscope_budget_exceeded(threat typeresource_exhaustion, OWASP Agentic ASI08), routed to approval, until the scope is re-declared. - Scope and expiry violations map to OWASP LLM03 (Excessive Agency, 2026 numbering); all carry a remediation message naming the exact re-declaration to make.
- Under a live scope, a bounded set of false-positive-prone content classes on the agent's own file content (SQL and shell injection patterns, script-tag injection, acrostic steganography, capability acquisition) resolve to
refuse_tier: warninstead of block: the threat type stays on the record, the row is visible in Evaluation History, and no incident is minted. Commands and queries are never softened, credential material and the non-overridable classes never are, and expired or held scopes see no difference. - A hold never skips the content scan. The response carries
held_by_scope: true; a content block wins and stands as the verdict, otherwisecontent_verdictsays what the content scan found. A hold is an authorization state, not evidence: it never raises session risk, so a lapsed scope cannot lock a session on its own. - On the Agents screen the default view is Needs decision: agents whose scope lapsed while they kept working, agents past their renewal window, agents running without a scope, and agents with open incidents, each with the reason beside its status. The Holds / content column counts, over seven days, refusals on authorization alone against refusals on content: mostly holds with clean content means the scope needs adjusting, not the agent. Propose from history drafts a scope from what an agent actually used; you edit and save it, and authoring paths are never proposed. The Coding agent profile fills the form for the common case: command, file writes, web search, two-hour sessions renewable for a day.
- Observe mode is how you find out what enforcing would stop before you turn it on. With
enforcement_mode: observethe response to an action that would have been refused issafe: truewithobserved: true,observed_refuse_tierandobserved_by_axissaying what would have applied;axis_verdictsstill carries every axis's real answer. Detection is untouched, session risk still accumulates, and the safety floor still refuses. Every observed refusal is stored, and the Agents screen counts them over seven days as would have been refused beside the observing agent, which is the number to read before deciding to enforce or to fix the scope. The window ends on its own atobserve_until. - A locked session has a way back too. With a live scope whose renewal window is open, the agent's own
POST /api/session/reset(the MCPreset_sessiontool) releases the lock up to three times per window; each release is written to the audit trail assession.self_release. Past the window, or past three releases, the operator's Release on the Agents screen is the only path, and the refusal says which (reason: "ceiling_reached"or"release_limit").
Tool identity comes from request_metadata.tool_name when the caller declares it, or from the specialized scan type (scan_command, scan_sql,scan_file_write, …) which doubles as the tool name. Plain prompt/response scans have no tool to check — expiry still applies.
Channels
Python
from shrike_guard import ScanClient
client = ScanClient(api_key=os.environ["SHRIKE_API_KEY"])
client.declare_scope(
agent_id="recon_agent_q2_2026",
purpose="invoice reconciliation for FY26 Q2",
allowed_tools=["read_invoice", "match_ledger_entry", "post_reconciliation_note"],
max_duration_seconds=28800,
)TypeScript
import { ScanClient } from 'shrike-guard';
const client = new ScanClient({ apiKey: process.env.SHRIKE_API_KEY });
await client.declareScope({
agentId: 'recon_agent_q2_2026',
purpose: 'invoice reconciliation for FY26 Q2',
allowedTools: ['read_invoice', 'match_ledger_entry', 'post_reconciliation_note'],
maxDurationSeconds: 28800,
});MCP
The scan_declare_scope tool (one of the 14 MCP security tools) takes the same fields. Every other scan_* tool then checks the declared scope automatically — no per-tool configuration.
What ships today vs. roadmap
Today: named-tool allow/forbid lists, expiry, approval routing on violations, and purpose grounding — live on every channel. Roadmap: resource-level read/write/approval statements — scope as code — extending the same declaration with IAM-style effect/actions/resources/condition statements, including human-in-the-loop as a first-class policy effect.
Related
- The AI agent security stack — where scope-based authorization sits among the other control layers.
- What is action governance? — the concept scope enforcement implements.
- The 17 agentic attack techniques — where scope sits in the coverage map (Excessive Tool Permissions, Unvetted Servers).
- MCP integration guide — the 14 tools, including
scan_declare_scope. - API reference — the full scan contract every violation is returned in.