Skip to content

AI app security

The AI security pack is part of the TypeScript and Python SDKs. It checks what your app sends to its models and what comes back, inside your app’s own process. Each check reports by default and changes nothing. You choose, per app and per check, which ones enforce. If a check fails, the call goes ahead (fail open).

What reaches Shield360 is what each check found and what it did. That means the check, the kind of finding, rule ids, a masked token and a keyed fingerprint. The text that was checked never leaves your app.

import Shield360 from "shield360";
Shield360.init({
applicationName: "support-bot",
shield360Url: process.env.SHIELD360_URL,
shield360ApiKey: process.env.SHIELD360_API_KEY,
aiSecurity: true, // or { policy: {...}, configPublicKey: {...} }
});
import shield360
shield360.init(
application_name="support-bot",
shield360_url=os.environ["SHIELD360_URL"],
shield360_api_key=os.environ["SHIELD360_API_KEY"],
ai_security=True, # or {"policy": {...}, "config_public_key": {...}}
)

Once it is on, the SDK checks these calls automatically:

  • OpenAI chat completions and responses, and Anthropic messages. It checks tool results in the request, the user’s turns and the answer, streamed or not.
  • LangChain retrievers and tools.
  • LlamaIndex retrievers.
  • MCP client tool lists and tool calls.

Each check is also available as a function, for code paths the SDK does not wrap. SHIELD360_AI_SECURITY=1 turns it on without a code change.

Settings live in the app’s Security tab in Shield360: Applications → the app → Security. The SDK fetches the app’s policy in the background every minute, never on a request’s path.

The policy is signed with the environment’s Ed25519 key. The SDK applies it only when it verifies under a key you pinned; the key sent beside the policy is never trusted on its own.

  1. Open the app’s Security tab and copy the JSON under Pin this environment’s key in the app. It is a JWKS: the current key plus keys rotated out, so an app pinned to it keeps working through a rotation.
  2. Give it to the SDK as SHIELD360_CONFIG_PUBLIC_KEY, or in code as configPublicKey (TypeScript) or config_public_key (Python). A single JWK or a PEM also works.
Terminal window
export SHIELD360_CONFIG_PUBLIC_KEY='{"keys":[{"kty":"OKP","crv":"Ed25519","x":"…","kid":"…","alg":"EdDSA","use":"sig"}]}'

Without a pinned key, the SDK does not apply the app’s settings. Its checks run in monitor mode with the policy from code, and it reports config_unpinned once (an ai_security.config alert). For local development only, allowUnpinnedConfig: true (TypeScript) or allow_unpinned_config=True (Python) trusts the first key the platform sends.

The checks and the OWASP risks they address

Section titled “The checks and the OWASP risks they address”
CheckOWASP LLM Top 10 (2025)What it doesWhen enforcing
Indirect prompt injectionLLM01 Prompt InjectionScans tool results, retrieved chunks, fetched pages and files with the same rules as the CLI, the MCP gateway and the platform.Removes the matched instructions, or blocks
JailbreaksLLM01 Prompt InjectionScans each user turn: role-play personas, refusal suppression, prefix injection, look-alike letters, leetspeak, ROT13, base64 and hex. It also reads the conversation for multi-turn escalation.Blocks the request
AnswersLLM02 Sensitive Information Disclosure, LLM05 Improper Output Handling, LLM07 System Prompt LeakageChecks answers, including streamed ones, for personal data, secrets, your own kinds of data, another customer’s identifiers, links and images that carry data or point off your allow-list, runs of the system prompt, and the canary.Redacts, or blocks. A leaked prompt or canary is always blocked.
Tool permissionsLLM06 Excessive AgencyDecides, before a tool runs, whether the signed-in end user may run it, by role, attribute or tenant.Refuses with a tool error the model can read
Usage limitsLLM10 Unbounded ConsumptionLimits requests per minute, and tokens and cost per day, for each end user and each customer.Refuses the call
MCP servers and toolsLLM03 Supply ChainAllow-lists servers and pins each tool by the SHA-256 of its description and schema. Scans a description for tool poisoning when a tool is first seen and when it changes.Hides the tool and refuses calls to it
Retrieved documentsLLM08 Vector and Embedding WeaknessesFlags chunks whose tenant metadata names another customer, and chunks that turn up for many unrelated questions (hub chunks).Drops the chunk, or blocks

These checks reduce the risks above; they do not remove them. The injection and jailbreak checks are rules, and rules can be evaded. LLM04 (data and model poisoning) and LLM09 (misinformation) are not covered.

const page = Shield360.aiSecurity.scanContext(fetchedHtml, { source: "document" });
// page.text is the content to give the model: unchanged, or with the instructions removed.
Shield360.aiSecurity.checkPrompt(userTurns); // oldest first; throws AiSecurityBlockedError when enforcing
from shield360 import ai_security
page = ai_security.scan_context(fetched_html, source="document")
ai_security.check_prompt(user_turns) # raises AiSecurityBlockedError when enforcing

Multi-turn escalation is a heuristic. It looks for three things:

  • turns that each match a little and together cross the threshold;
  • a request split across messages;
  • a role-play set up earlier followed by “continue” or “do it for real”.

scanOutput / scan_output checks a whole answer. streamScanner / stream_scanner checks a streamed one as it arrives. It holds back the last 256 characters, so a value split across chunks can still be redacted, and its memory is bounded by a 2,048-character window.

  • Exfiltration links. With an allow-list (for example docs.example.com, *.cdn.example.com), images to any other domain are flagged, because renderers fetch them without a click. Without a list, only links and images that carry data are flagged: a long encoded value, prose, or a sensitive value in the query string or path.
  • System prompt leakage. The answer is compared with the request’s own system prompt in 6-word runs. It counts as a leak when at least 3 runs match and they cover a tenth of the prompt (or 25 runs). Put a canary in the prompt with withCanary / with_canary and set it in the app’s settings: an answer containing it is certain to be a leak.
  • Another customer’s data. When the retrieval check sees a chunk tagged to another customer, the identifiers in that chunk are remembered for the request: e-mail addresses, UUIDs, record ids and long numbers. An answer that repeats one is flagged. tagContext / tag_context marks other text the same way.
const refund = Shield360.aiSecurity.defineTool({
name: "refund_order",
permissions: { roles: ["support_lead"], attributes: { region: ["eu"] } },
execute: async ({ orderId }) => refundOrder(orderId),
});
await Shield360.aiSecurity.withEndUser(
{ tenantId: "acme", userId: "u-42", roles: ["agent"], attributes: { region: "eu" } },
() => agent.run(question),
);
@ai_security.tool(permissions={"roles": ["support_lead"], "attributes": {"region": ["eu"]}})
def refund_order(order_id: str): ...
with ai_security.end_user(tenant_id="acme", user_id="u-42", roles=["agent"], attributes={"region": "eu"}):
agent.run(question)

A rule allows a call when every condition it names holds. A rule in the app’s settings replaces the code’s rule for the same tool. With Refuse tools that have no rule, a tool without a rule is refused. A refused call returns { error: "permission_denied", ... } to the model instead of running. Every refusal is recorded.

The SDK counts locally and, every 10 seconds in the background, sends its counts to Shield360. Shield360 keeps shared counters in Redis and answers with the totals across all the app’s processes. A limit check is a lookup in memory, and a Shield360 outage never slows or fails a call.

The trade-off: processes can together pass a limit by what they serve in one sync interval before they learn the total. Minutes and days are UTC. Tenant and user ids are hashed with a key before they are used as counter keys.

When an MCP client lists its tools, the SDK compares them with the app’s approved pins:

  • an unapproved server or tool is reported;
  • a tool whose description or schema changed since approval is reported as changed (a “rug pull”);
  • a description that reads like tool poisoning is reported as poisoned.

The poisoning check uses the shared injection rules, rules adapted from Cisco AI Defense mcp-scanner (Apache-2.0), and a cross-tool shadowing check. New and changed tools appear in the app’s Security tab for someone to approve or reject. Approving one adds its pin. When enforcing, refused tools are removed from the list the model sees and calls to them are refused. With also refuse MCP servers and tools that were never approved, anything without a pin is refused too.

Set the chunk metadata field that names your customer: tenant_id by default, or a dotted path such as acl.tenant. Chunks without the field are not checked.

The hub-chunk check is a streaming version of Cisco AI Defense adversarial-hubness-detector scoring. It flags a chunk when all of the following hold over the last 500 retrievals:

  • it appears in at least 10% of them;
  • its robust z-score is at least 5;
  • it answered at least 5 unrelated groups of questions.

Hub chunks are reported, never dropped.

Findings are kept for 400 days by default. To choose another period for the organisation or one environment, go to Settings → Data retention → AI security findings (see Data retention).

Findings raise these alert kinds, one per app, check and kind per window:

  • ai_security.indirect_injection
  • ai_security.jailbreak
  • ai_security.output
  • ai_security.tool_denied
  • ai_security.rate_limited
  • ai_security.mcp_tool
  • ai_security.rag

Leaked secrets, another customer’s data, leaked prompts, changed or poisoned tools, and chunks from another customer are critical.

Detection rules can use the AI app security findings signal. Five templates use it:

  • jailbreak spike per app;
  • an end user repeatedly trying jailbreaks;
  • an answer with another customer’s data;
  • an MCP tool changed after approval;
  • an end user hammering a usage limit.

These rates come from the corpora in the repository. Each SDK’s tests print them.

CheckCorpusDetectionFalse positives
Indirect injection (shared rules 2026.10.29-2)Shared corpus, untrusted sources74/75 (98.7%)0/85
Jailbreaks, single turnShared corpus, prompts67/68 (98.5%)0/50
Jailbreaks, multi-turn10 conversations (tuned on)5/50/5
Tool poisoning16 + 20 descriptions (tuned on)16/160/20
Tool poisoning6 + 8 descriptions held out4/60/8
Answers12 sensitive + 14 ordinary answers12/120/14

On public datasets the rules were never tuned on (benchmark report cli/internal/coding/injection/testdata/bench/results/2026.10.29-2.md):

DatasetDetectionFalse positives
jackhhao/jailbreak-classification (Apache-2.0)228/666 (34.2%, from 31.4% before the jailbreak rules)1/1,332 (0.1%)
databricks-dolly-15k, benign–1/15,011 (0.0%)
All held-out public sets598/1,929 (31.0%)2/16,742 (0.0%)

The small corpora were tuned on and show only what the rules were written to find. Treat the held-out rows as the realistic numbers: rules catch the common and published techniques, and miss many novel jailbreaks.