Skip to content

Shield Protect: mandatory authorization before execution

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.

PathObservation or preventionWhere enforcedConfigurationOn failureKnown bypassesTests
MCP gateway tool calls (MCP servers, wrapped REST APIs, documents, contexts, data tools)PreventionThe gateway boundary, before any downstream callEnforcement mode per environment (observe, warn, enforced); grants; action policies; approval rulesEnforced: 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 runsNone 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 readsPreventionThe data gateway, per table and classData authorizationsRefusesAs above__tests__/lib/platform/data-gateway
Model calls through the AI proxyPrevention at the proxyThe proxy asks the check API per callAgent profile: models, spend limitsDeny refuses; an unrecorded allow is deniedA process that reaches a provider directlyagent-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 processIn-process wrapper on the provider methodprotect: { mode, required } on init; SHIELD360_PROTECT_MODE raises, never lowersEnforced: a guard that throws, input past the scan bound, or a stream postflight guards cannot inspect refuses; a required provider not wrapped stops startupCode 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 protectToolPreventionThe wrapper around your functionExplicitEnforced: a guard finding, a failed guard, a refused or failed authorize step refusesNone for calls that go through it; calls that do not are unseenAs above
TypeScript SDK MCP client callToolPreventionBefore the request leavesProcess mode + guards from initEnforced refuses on a findingA client built without the SDKAs above
Python SDK (openai, anthropic, groq, mistralai, cohere, together; protect_tool; MCP call_tool)As the TypeScript SDKAs aboveprotect={"mode": ..., "required": [...]}As aboveAs abovesdk/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 authorizationThe vendor’s pre-execution hookOrganisation enforcement policy; fail closed makes it managed with a 72-hour freshness windowFail closed: an unreadable, missing or stale policy refuses guarded events. Managed: local overrides may raise a level, never lower oneA vendor without a pre-execution hook for an event (Cursor edits, after-the-fact results); a user who removes the hookcli/internal/coding/guard/managed_test.go, failclosed_test.go, sequence/hook_test.go
Browser, shell, direct network and credential use by an agentPrevention only by isolationThe deployment, not Shield360 codeThe reference deployment’s networks and container settingsThe agent has no route and no credential, so there is nothing to failAny deployment that gives the agent a credential or a routedeploy/protect-reference/verify.sh

Every gateway tool call, whether by a direct tool name or through call_tool, crosses the same steps:

Authenticate
The agent and its environment come from its token or OAuth access token. Nothing in the call can name another.
Resolve
The tool, from the agent’s own catalog. A name it was not granted stops here.
Validate
The arguments against the tool’s input schema, canonicalised (keys sorted, __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.
Authorize
The grant and action scope, the kill switch, the run context and the person acted for (the decision log row is the evidence; in enforced mode a call whose decision could not be recorded does not run); prompt injection in the arguments; action policies on the arguments; sequence policies on the session.
Reserve
In one transaction under the agent’s lock: cumulative limits are checked and this call’s share reserved; the approval, if one is needed, is used up, only while it is bound to the same policy version; the action is written as executing. Any failure means nothing runs.
Execute
The downstream call, with the action id as 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.
Record
Succeeded, failed, or unknown when the service did not answer in time: the agent is told the call may or may not have taken effect and not to retry another way (guidance, not a control), and a person settles the outcome in the ledger. A failure to record an outcome never re-runs anything.
ModeWhat a Protect finding doesWhat keeps refusing regardless
Observe (default for existing environments)Written to the action ledger; the call runsGrants and the kill switch, approval rules, prompt injection in block mode, sequence policies in block mode
WarnWritten, and the agent is told in the result what enforced mode would have refusedAs above
EnforcedRefuses the callAs 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.

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):

FactValue
$actionThe 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)
$wordsThe 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.

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.
  • agentIds limits a policy to those agents of the environment; with none, it covers every agent.
  • A condition can add "ci": true to compare strings without regard to case.
  • A limit caps the total of a numeric argument (path) at max per window (hour, day or month, 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 no path, it caps the number of calls instead (a whole-number max): 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 unknown kind, for example) makes the policy invalid, and in enforced mode it denies the calls it covers.
  • For a require approval policy, approvalChannelIds names up to 5 alert channels to ask in, and approvalExpiresMinutes (5 minutes to 7 days, default 60) is how long an approval waits.
LimitValue
Policies per environment200
Tool patterns per policy1 to 20, each up to 128 characters of letters, digits and _ . * : -
Agents per policy50
Conditions per policy20
Values in an in / not_in list200
Limits per policy5
Name100 characters (description 500)
Priority0 to 10,000

You need Grant agent access (agent_access.write).

Open the Protect tab

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.

Name it and choose the tools

In the form below the list, enter a Name, and the Tool patterns it covers, separated by commas (for example payments.send, github.delete_*).

Choose the effect

Pick the Effect: Allow, Deny (the default) or Require approval.

Add conditions and limits

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.”

Add it

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.

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:

PolicyCoversEffect
Destructive actions need approval$action is deleteRequire approval; at most 50 approved an hour per agent
Money movement needs approvalNot a read, and the name says pay, payment, payout, transfer, refund, charge, withdraw, wire or remitRequire approval
Permission changes need approvalNot a read, and the name says grant, invite, share, role, permission, collaborator, member, acl, owner or adminRequire approval
Bulk export needs approvalNot a read, and the name says export, dump, backup or bulkRequire approval
Outbound sending needs approvalNot a read, and the name says send, email, post, publish, upload, reply, forward, broadcast, tweet or smsRequire approval
Cap writes per agent$action is writeAllow; 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.

The routes act on the environment of your signed-in session, as the page does.

Method and pathPermissionDoes
GET /api/agent-access/protect/policiesagent_access.readLists the environment’s policies, by priority
POST /api/agent-access/protect/policiesagent_access.writeAdds a policy; 201 with it
PUT /api/agent-access/protect/policies/<id>agent_access.writeReplaces a policy with the body given (send every field you want to keep)
DELETE /api/agent-access/protect/policies/<id>agent_access.writeRemoves it
POST /api/agent-access/protect/baselineagent_access.writeAdds 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.

Terminal window
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
}'

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.

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.

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’ 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_MODE can raise the mode and never lower it; SHIELD360_PROTECT_REQUIRED adds providers. In enforced mode a guard that throws refuses (fail closed, whatever guardFailOpen says), input past the guards’ scan bound is refused rather than scanned in part, a streamed completion is refused when postflight guards exist, and init throws ProtectReadinessError instead 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 example authorizeWithShield360({ 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_tool is 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.

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).

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.

  • The migration 20261112090000_agent_gateway_protect is 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: false rows (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.