Shield Protect: mandatory authorization before execution
The claim, precisely
Section titled “The claim, precisely”In an explicitly supported, locked-down deployment, no protected action executes without a valid Shield360 authorization decision, and an authorization failure stops execution.
That is a guarantee about enforcement of configured policies. It is not a promise to detect every harmful intent, and it is not something an SDK can give on its own: code that can disable an SDK can bypass it. The claim holds at the MCP gateway in enforced mode, inside the reference deployment or one shaped like it. Everywhere else, Shield360 says monitored or partially protected, and its Protect tab says which is which.
Coverage matrix
Section titled “Coverage matrix”| Path | Observation or prevention | Where enforced | Configuration | On failure | Known bypasses | Tests |
|---|---|---|---|---|---|---|
| MCP gateway tool calls (MCP servers, wrapped REST APIs, documents, contexts, data tools) | Prevention | The gateway boundary, before any downstream call | Enforcement mode per environment (observe, warn, enforced); grants; action policies; approval rules | Enforced: a schema violation, a policy denial, a limit, a missing or stale approval, an unreadable policy set, or evidence that cannot be written refuses the call. Observe and warn: recorded, and the call runs | None inside the reference deployment. Outside it: an agent with its own credentials or network path | __tests__/lib/platform/agent-gateway/protect/boundary.test.ts (36 cases, zero downstream execution asserted on every refusal) |
| Data gateway reads | Prevention | The data gateway, per table and class | Data authorizations | Refuses | As above | __tests__/lib/platform/data-gateway |
| Model calls through the AI proxy | Prevention at the proxy | The proxy asks the check API per call | Agent profile: models, spend limits | Deny refuses; an unrecorded allow is denied | A process that reaches a provider directly | agent-authorization/enforcement.test.ts |
| TypeScript SDK provider guards (openai, @anthropic-ai/sdk, groq-sdk, together-ai, @mistralai/mistralai, cohere-ai, @aws-sdk/client-bedrock-runtime, @google/generative-ai) | Prevention of the call leaving the process | In-process wrapper on the provider method | protect: { mode, required } on init; SHIELD360_PROTECT_MODE raises, never lowers | Enforced: a guard that throws, input past the scan bound, or a stream postflight guards cannot inspect refuses; a required provider not wrapped stops startup | Code that bypasses the SDK; an ESM-only resolution that loads a class the wrapper never sees (name it in required so enforced mode fails rather than silently not wrapping) | sdk/typescript/src/protect/__tests__ |
TypeScript SDK protectTool | Prevention | The wrapper around your function | Explicit | Enforced: a guard finding, a failed guard, a refused or failed authorize step refuses | None for calls that go through it; calls that do not are unseen | As above |
TypeScript SDK MCP client callTool | Prevention | Before the request leaves | Process mode + guards from init | Enforced refuses on a finding | A client built without the SDK | As above |
Python SDK (openai, anthropic, groq, mistralai, cohere, together; protect_tool; MCP call_tool) | As the TypeScript SDK | As above | protect={"mode": ..., "required": [...]} | As above | As above | sdk/python/tests/test_protect.py |
| Coding-agent hooks (Claude Code, Codex, Cursor, Copilot, Gemini, Windsurf, OpenCode) | Prevention of what the vendor lets a hook stop; detection of content, not business authorization | The vendor’s pre-execution hook | Organisation enforcement policy; fail closed makes it managed with a 72-hour freshness window | Fail closed: an unreadable, missing or stale policy refuses guarded events. Managed: local overrides may raise a level, never lower one | A vendor without a pre-execution hook for an event (Cursor edits, after-the-fact results); a user who removes the hook | cli/internal/coding/guard/managed_test.go, failclosed_test.go, sequence/hook_test.go |
| Browser, shell, direct network and credential use by an agent | Prevention only by isolation | The deployment, not Shield360 code | The reference deployment’s networks and container settings | The agent has no route and no credential, so there is nothing to fail | Any deployment that gives the agent a credential or a route | deploy/protect-reference/verify.sh |
The boundary
Section titled “The boundary”Every gateway tool call, whether by a direct tool name or through call_tool, crosses the same steps:
__proto__ and friends refused, a size limit). A schema keyword the gateway cannot check is a readiness failure in enforced mode, never a silent pass. Text past the injection scan bound is refused rather than scanned in part.Idempotency-Key and X-Shield360-Action-Id on a wrapped API and _meta.shield360/actionId on an MCP call, so a downstream that honours one never acts twice on a retried action. Shield360 does not promise exactly-once execution downstream.Enforcement modes
Section titled “Enforcement modes”| Mode | What a Protect finding does | What keeps refusing regardless |
|---|---|---|
| Observe (default for existing environments) | Written to the action ledger; the call runs | Grants and the kill switch, approval rules, prompt injection in block mode, sequence policies in block mode |
| Warn | Written, and the agent is told in the result what enforced mode would have refused | As above |
| Enforced | Refuses the call | As above |
Protect findings are: a schema violation, an unsupported schema, oversized arguments, an action-policy denial, an invalid policy, an unreadable policy set, a require-approval policy, a cumulative limit, a stale approval, and evidence that cannot be written. The mode is read every 15 seconds per pod; a pod that cannot read it and has nothing cached decides as enforced, because the open mode is never guessed.
Set the Enforcement mode under Govern → Agent access → Agent authorization, on the Protect tab. The readiness panel names what blocks enforcement: tools with schemas the gateway cannot check, policies that cannot be evaluated, and actions waiting to be settled.
Action policies
Section titled “Action policies”A policy names the tools (patterns) and agents it covers, conditions that must all hold on the call’s canonical arguments, an effect, and limits:
{ "name": "Payments to approved recipients", "toolPatterns": ["payments.send"], "effect": "allow", "conditions": [{ "path": "recipient", "op": "in", "value": ["acct_1", "acct_2"] }, { "path": "amount", "op": "lte", "value": 500 }], "limits": [{ "path": "amount", "window": "day", "max": 2000, "scope": "agent" }] }{ "name": "No other payments", "toolPatterns": ["payments.send"], "effect": "deny", "priority": 200 }{ "name": "Sending mail needs a person", "toolPatterns": ["mail.send"], "effect": "require_approval" }{ "name": "Never delete repositories", "toolPatterns": ["github.delete_*"], "effect": "deny" }{ "name": "At most 50 approved deletes an hour", "toolPatterns": ["*"], "effect": "require_approval", "conditions": [{ "path": "$action", "op": "eq", "value": "delete" }], "limits": [{ "kind": "count", "window": "hour", "max": 50, "scope": "agent" }] }Conditions are data, never code: a path (body.amount, recipients[0]), one operator from eq, neq, in, not_in, any_in, lt, lte, gt, gte, prefix, suffix, contains, matches, exists, absent, len_lte, len_gte, and a literal. any_in holds when any element of a list is one of the values. A comparison on a value of the wrong type is false: a missing amount is not “below 500”. An operator the gateway does not know is refused when the policy is saved; a stored policy that still has one denies the calls it covers in enforced mode rather than falling through to a looser one.
A path that starts with $ reads a fact about the call instead of its arguments (an argument path cannot start with $, so arguments cannot pose as one):
| Fact | Value |
|---|---|
$action | The action scope the call needs, as the tool risk classifier decides it: read, write or delete (the tool’s MCP annotations, then its HTTP method, then the words of its name) |
$words | The words of the tool’s own name, lower case: sendPayment is ["send", "payment"] |
With them, one policy covers a class of tools (every delete, anything whose name mentions a payment) without listing each tool.
Limits reserve before the call under the agent’s lock: two calls at once cannot both fit under a limit only one fits under. A reservation counts until a person settles the action as never ran.
How policies are chosen
Section titled “How policies are chosen”Policies apply to the environment you are in. For each call, the gateway takes the enabled policies in order of priority (lower first; the default is 100, and the oldest goes first among equals) and uses the first one that covers the agent and the tool and whose conditions all hold. Its effect decides the call. A policy whose conditions do not all hold is skipped, so end a set with a catch-all (as No other payments above, at a higher priority number). When no policy matches, grants alone decide.
- A tool pattern matches a tool name exactly, with
*standing for any run of characters (github.delete_*). Matching is case-sensitive. A pattern matches a tool by its gateway name (github.delete_repo) and by its Copilot name (github__delete_repo); a pattern written in the Copilot form is saved in the gateway form. agentIdslimits a policy to those agents of the environment; with none, it covers every agent.- A condition can add
"ci": trueto compare strings without regard to case. - A limit caps the total of a numeric argument (
path) atmaxperwindow(hour,dayormonth, counted in UTC from the start of the hour, day or month), either per agent ("scope": "agent", the default) or across every agent the policy covers ("scope": "policy"). With"kind": "count"and nopath, it caps the number of calls instead (a whole-numbermax): each call reserves one. Limits are allowed on allow and require approval policies; on a require-approval policy they are reserved when the approved call runs. A limit the gateway cannot read (an unknownkind, for example) makes the policy invalid, and in enforced mode it denies the calls it covers. - For a require approval policy,
approvalChannelIdsnames up to 5 alert channels to ask in, andapprovalExpiresMinutes(5 minutes to 7 days, default 60) is how long an approval waits.
| Limit | Value |
|---|---|
| Policies per environment | 200 |
| Tool patterns per policy | 1 to 20, each up to 128 characters of letters, digits and _ . * : - |
| Agents per policy | 50 |
| Conditions per policy | 20 |
Values in an in / not_in list | 200 |
| Limits per policy | 5 |
| Name | 100 characters (description 500) |
| Priority | 0 to 10,000 |
Add an action policy
Section titled “Add an action policy”You need Grant agent access (agent_access.write).
Go to Govern → Agent access → Agent authorization and open the Protect tab. Action policies lists the environment’s policies, with their tool patterns, effect, conditions and limits.
In the form below the list, enter a Name, and the Tool patterns it covers, separated by commas (for example payments.send, github.delete_*).
Pick the Effect: Allow, Deny (the default) or Require approval.
Write Conditions (JSON) as a JSON list, for example [{"path":"recipient","op":"in","value":["acct_1"]},{"path":"amount","op":"lte","value":500}]. Leave [] for a policy that applies to every call to the tools. On an allow or require-approval policy, write Limits (JSON, not on deny policies) the same way, for example [{"path":"amount","window":"day","max":2000,"scope":"agent"}], or [{"kind":"count","window":"hour","max":50,"scope":"agent"}] to count calls. If either is not a valid JSON list you are told “Conditions and limits must be valid JSON lists.”
Select Add policy. It is saved enabled, at priority 100, for every agent, and applies to the next call. Delete beside a policy removes it; calls it covered are then decided by the remaining policies and grants.
High-risk actions baseline
Section titled “High-risk actions baseline”A new environment has no action policies, so nothing high-risk is gated beyond grants; the readiness panel says so and recommends the baseline. Apply baseline, under Action policies on the Protect tab, adds these policies in one step:
| Policy | Covers | Effect |
|---|---|---|
| Destructive actions need approval | $action is delete | Require approval; at most 50 approved an hour per agent |
| Money movement needs approval | Not a read, and the name says pay, payment, payout, transfer, refund, charge, withdraw, wire or remit | Require approval |
| Permission changes need approval | Not a read, and the name says grant, invite, share, role, permission, collaborator, member, acl, owner or admin | Require approval |
| Bulk export needs approval | Not a read, and the name says export, dump, backup or bulk | Require approval |
| Outbound sending needs approval | Not a read, and the name says send, email, post, publish, upload, reply, forward, broadcast, tweet or sms | Require approval |
| Cap writes per agent | $action is write | Allow; at most 200 an hour per agent |
The baseline sits at priorities 9000 and up, after your own policies (default 100): yours decide first, and the baseline catches what they do not. Applying it again adds only what is missing; a baseline policy you edited or switched off is left as it is, and one you deleted comes back. It never changes or removes another policy.
Action policy API
Section titled “Action policy API”The routes act on the environment of your signed-in session, as the page does.
| Method and path | Permission | Does |
|---|---|---|
GET /api/agent-access/protect/policies | agent_access.read | Lists the environment’s policies, by priority |
POST /api/agent-access/protect/policies | agent_access.write | Adds a policy; 201 with it |
PUT /api/agent-access/protect/policies/<id> | agent_access.write | Replaces a policy with the body given (send every field you want to keep) |
DELETE /api/agent-access/protect/policies/<id> | agent_access.write | Removes it |
POST /api/agent-access/protect/baseline | agent_access.write | Adds the baseline policies the environment does not hold (body {}); 201 with those added, or 200 when all were in place |
The body takes name and toolPatterns (required), effect (allow, deny or require_approval, required), and optionally description, agentIds, conditions, limits, approvalChannelIds, approvalExpiresMinutes, priority and enabled (default true). A malformed body or an invalid field answers 400 with the reason; the 201st policy answers 409. Every change is recorded in the audit log.
curl -X POST https://<shield360>/api/agent-access/protect/policies \ -H 'Content-Type: application/json' --cookie '<session>' \ -d '{ "name": "Refunds over 100 need a person", "toolPatterns": ["payments.refund"], "agentIds": ["<agent id>"], "effect": "require_approval", "conditions": [{ "path": "amount", "op": "gt", "value": 100 }], "approvalChannelIds": ["<alert channel id>"], "approvalExpiresMinutes": 120, "priority": 50 }'Approvals
Section titled “Approvals”An approval binds to the environment, the agent, the tool, a keyed hash of the canonical arguments, the requesting person, and the agent’s policy version (a fingerprint of its grants, profile and elevations). It is used up atomically before the call runs; two retries racing get one run. Changed arguments are another call. A grant, profile or elevation change after approval makes it stale: the call does not run under the new policy, and a fresh request is filed. The requesting person, the agent’s creator and the person it acts for cannot approve.
Evidence
Section titled “Evidence”The action ledger (gateway_action) holds one row per decided call: action id, agent, tool, action scope, a keyed digest of the arguments, mode, status (denied, awaiting_approval, executing, succeeded, failed, unknown), whether it was enforced, reason codes, the decision id and policy version, the approval id, a classified error code and a masked one-line error summary, timings, and who settled an unknown outcome and when. Never arguments, results or a raw error. The ClickHouse activity log and provenance delivery remain optional telemetry; the ledger is written synchronously before the call.
Grants: every tool, said explicitly
Section titled “Grants: every tool, said explicitly”A grant that named no tools used to mean every tool. It now says so: allTools: true. The migration set it on every existing grant that named none, so nothing changed for them; a new grant must name tools or say allTools explicitly, and an empty list with it false allows nothing.
The SDKs
Section titled “The SDKs”The SDKs’ tracing APIs (trace, startTrace, the instrumentations) record. Their Protect APIs refuse:
init({ protect: { mode: "enforced", required: ["openai"] } })/init(protect={"mode": "enforced", "required": ["openai"]}).SHIELD360_PROTECT_MODEcan raise the mode and never lower it;SHIELD360_PROTECT_REQUIREDadds providers. In enforced mode a guard that throws refuses (fail closed, whateverguardFailOpensays), input past the guards’ scan bound is refused rather than scanned in part, a streamed completion is refused when postflight guards exist, andinitthrowsProtectReadinessErrorinstead of starting with a required provider unguarded.protectTool(name, fn, { authorize })/@protect_tool(name, authorize=...): arguments checked before the call, an optional authorization step (for exampleauthorizeWithShield360({ kind: "destination", host }), which asks the check API as the agent), the result checked after. In enforced mode a refusal, a failed check or an answer that did not arrive refuses.- The MCP client’s
callTool/call_toolis checked before the request leaves.
Verified against the package versions the SDKs’ tests exercise: the TypeScript tests use fake provider modules and the real @modelcontextprotocol/sdk patch points; the Python tests use fake provider modules. Unverified: enforced mode against live openai and anthropic SDK calls (the SDK test suites that need credentials are held), Bedrock, Google and Mistral argument extraction, and ESM-only resolution of a provider package.
Coding-agent hooks
Section titled “Coding-agent hooks”A fail-closed guard policy is managed enforcement on the machine: SHIELD360_AGENT_GUARD, SHIELD360_AGENT_INJECTION, SHIELD360_AGENT_SEQUENCE and config.env may raise a level and never lower one, and never clear an unavailable policy. A system policy that exists but cannot be read is never replaced by the user’s own file. A fail-closed policy the collector has not refreshed in 72 hours is stale and fails closed. What a vendor lets a hook see and stop is listed in cli/internal/coding/sequence/hook.go and injection/hook.go; events a vendor reports only after the fact are recorded, not refused. Under a managed policy the local budget switch SHIELD360_BUDGET_ENFORCEMENT=off does not apply either (collector 0.1.0 and later).
The reference deployment
Section titled “The reference deployment”deploy/protect-reference/ is a compose bundle in which the claim holds: the agent on an internal network with an egress proxy whose allow list names only the gateway; the gateway on the tool network with the downstream credential in its vault; a counting fake downstream; the agent read-only, non-root, without capabilities, host mounts or secrets. verify.sh proves, from inside the agent container, that it cannot reach the downstream, the host or the Internet, that a refused call leaves the downstream untouched, and that an allowed call reaches it once with the gateway’s credential and an idempotency key. See its README for what is and is not covered.
Migration and rollback
Section titled “Migration and rollback”- The migration
20261112090000_agent_gateway_protectis additive: new tables, new nullable or defaulted columns, and one update that keeps every existing grant’s meaning. Every environment starts in observe. - To roll out: run in observe, read the ledger for
enforced: falserows (what enforced mode would have refused), fix schemas and policies the readiness panel names, then switch to enforced. - To roll back: switch the mode to observe. Nothing else needs undoing; grants, approval rules, injection and sequence policies behave as before Protect in every mode.
- Approvals given before the upgrade have an empty policy version and are treated as stale on first use: the agent asks again.