shrike-guard 4.0.5 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.1.0] - 2026-09-09
4
+
5
+ ### Added
6
+ - **Act-plane scanning: every channel the backend scans is now reachable from
7
+ the SDK.** The backend has scanned eight specialized content types since the
8
+ act plane shipped; this SDK exposed four. `scanCommand` — the highest-volume
9
+ surface, the one an agent hits before every shell-out — had no method at all,
10
+ so the only way to reach it was to hand-roll an HTTP call or route through the
11
+ MCP server. New on `ScanClient`:
12
+ - `scanCommand(command, cwd?)` — shell commands, screened before `exec`.
13
+ Commands are decomposed, so SQL passed to `psql -c`, `mysql -e`, or a
14
+ heredoc is scanned as SQL rather than as opaque shell text.
15
+ - `scanWebSearch(query)` — search queries that acquire attack tooling,
16
+ credentials, or evasion tradecraft.
17
+ - `scanRagContext(chunks, query?)` — retrieved context, the standard carrier
18
+ for indirect prompt injection. Accepts a string or a list of strings.
19
+ - `scanMcpSchema(name, description, inputSchema?)` — a single MCP tool
20
+ definition, for tool poisoning in a `tools/list` response. Screened once at
21
+ registration, not per call.
22
+ - **`content_origin` on `ScanResult`.** Every verdict now says where the scanned
23
+ content came from: `human_prompt`, `agent_output`, `agent_action`, or
24
+ `third_party`. This answers the question a verdict alone cannot — was that my
25
+ prompt, or the agent acting on its own — which decides who a refusal message
26
+ is addressed to. Exported alongside it: the `ContentOrigin` type and
27
+ `attributableToOperator(origin)`, true only for `human_prompt`. Unknown content
28
+ types resolve to `agent_action`, never to `human_prompt`.
29
+ - **Act-plane parity test** (`tests/unit/actplane-parity.test.ts`). Iterates the
30
+ canonical channel list and fails when a channel has no SDK method. The gap
31
+ above went unnoticed because "the backend supports it" and "a customer can
32
+ call it" were two facts with nothing comparing them. This is the comparison.
33
+
34
+ - **Per-request session identity — `sessionId`/`agentId` on `ScanClientOptions`,
35
+ and `forSession()`.** Session identity is the key the backend accumulates
36
+ multi-turn risk against, so it has to mean one unit of work: one agent run,
37
+ one conversation, one user's request. It defaults to a process-wide id, which
38
+ suits a CLI or a worker but not a server serving many end users, where every
39
+ user would share one risk score and one user's refusal would count against
40
+ the next user's action. `forSession()` returns a view that shares the parent's
41
+ rate limiter, so deriving one per request is cheap and cannot multiply your
42
+ rate budget:
43
+
44
+ ```ts
45
+ const guard = new ScanClient({ apiKey: KEY }); // once, at startup
46
+
47
+ app.post('/act', async (req, res) => { // per request
48
+ const scoped = guard.forSession(req.session.id);
49
+ const verdict = await scoped.scanCommand(req.body.command);
50
+ });
51
+ ```
52
+
53
+ The process-wide default is unchanged when nothing is supplied, so
54
+ single-agent callers keep multi-turn correlation; the SDK now warns once when
55
+ it is in force. Silence that with `SHRIKE_SUPPRESS_SESSION_WARNING=1`.
56
+
57
+ `evaluateRotation`'s caller-owned branch now applies: a caller that supplies
58
+ its own session id receives a rotation recommendation rather than a
59
+ module-owned rotation.
60
+
61
+ ### Fixed
62
+ - **`content_origin` was computed by the backend and dropped before the caller.**
63
+ The response sanitizer is an allow-list, and the field was never added to
64
+ `PRESERVED_GOVERNANCE_FIELDS`, so it was serialized by the server and stripped
65
+ one layer before the application. It now survives sanitization.
66
+ - **Session identity could not be configured.** Every scan in a process used one
67
+ generated session id, so a server scanning on behalf of more than one person
68
+ placed all of them in one session. See `sessionId` / `forSession()` above.
69
+
3
70
  ## [4.0.5] - 2026-08-31
4
71
 
5
72
  ### Added
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  **Shrike Guard** is a TypeScript SDK for the [Shrike](https://shrikesecurity.com) platform — AI governance for every AI interaction. It wraps OpenAI, Anthropic (Claude), and Google Gemini clients to automatically evaluate all prompts against policy before they reach the LLM. Whether you're governing a customer-facing chatbot, securing developer AI tools, or managing autonomous agent actions — the same 9-layer cognitive pipeline evaluates every interaction.
8
8
 
9
+ It scans two different things. **Prompts and responses**, which is what a guardrail library normally means. And **agent actions** — the shell command, the SQL query, the web search, the retrieved document, the MCP tool definition — screened before they run, which is where an autonomous agent actually causes harm. See [Scanning agent actions](#scanning-agent-actions-shell-commands-sql-web-search-rag-mcp-tools).
10
+
9
11
  ## Features
10
12
 
11
13
  - **Drop-in replacement** for OpenAI, Anthropic, and Gemini clients
@@ -15,6 +17,8 @@
15
17
  - Jailbreak attempts
16
18
  - SQL injection
17
19
  - Path traversal
20
+ - **Pre-execution scanning for agent actions**: shell commands, SQL, file writes, web searches, RAG context, agent-to-agent messages, agent cards, and MCP tool schemas
21
+ - **Content provenance** (`content_origin`): every verdict says whether a human typed it, the model wrote it, the agent is about to do it, or it arrived from outside
18
22
  - **Fail-safe modes**: Defaults to fail-closed (Zero Trust posture); opt into fail-open explicitly when availability outranks enforcement
19
23
  - **CJS + ESM**: Dual build via tsup, works everywhere
20
24
  - **Subpath imports**: `shrike-guard/openai`, `shrike-guard/anthropic`, `shrike-guard/gemini`
@@ -167,6 +171,50 @@ const client = new ShrikeOpenAI({
167
171
  });
168
172
  ```
169
173
 
174
+ ### Sessions: one per unit of work, not one per process
175
+
176
+ Shrike correlates risk across a session. After a refusal, later actions in the
177
+ same session are held until the session recovers. That is the multi-turn
178
+ defence, and it means the session id has to mean one unit of work: one agent
179
+ run, one conversation, one user's request.
180
+
181
+ By default the SDK scans under one id for the whole process. That suits a CLI,
182
+ a worker, or a single agent. It does not suit a server that scans on behalf of
183
+ many end users, because every user then shares one risk score, and one user's
184
+ refusal counts against the next user's action.
185
+
186
+ Build one client at startup and derive a per-request view from it. The view
187
+ shares the rate limiter, so it costs nothing to make one per request and it
188
+ cannot multiply your rate budget:
189
+
190
+ ```typescript
191
+ import { ScanClient } from 'shrike-guard';
192
+
193
+ const guard = new ScanClient({ apiKey: 'shrike-...' }); // once, at startup
194
+
195
+ app.post('/act', async (req, res) => { // per request
196
+ const scoped = guard.forSession(req.session.id);
197
+ const verdict = await scoped.scanCommand(req.body.command);
198
+ if (verdict.refuse_tier !== 'allow') {
199
+ ...
200
+ }
201
+ });
202
+ ```
203
+
204
+ Or pin the identity at construction when one client serves one unit of work:
205
+
206
+ ```typescript
207
+ const client = new ScanClient({ apiKey: 'shrike-...', sessionId: 'job-42', agentId: 'ingest' });
208
+ ```
209
+
210
+ `agentId` is separate on purpose: it names *which agent* a scope is enforced
211
+ against and who an incident is attributed to. Set it when one process drives
212
+ several distinct agents.
213
+
214
+ The SDK warns once per process when it is scanning under the shared default.
215
+ Set `SHRIKE_SUPPRESS_SESSION_WARNING=1` to silence it once you have decided
216
+ the default is what you want.
217
+
170
218
  ### Local and self-hosted LLMs
171
219
 
172
220
  Shrike governs the model you point it at — it does not have to be a hosted
@@ -208,31 +256,131 @@ const client = new ShrikeGemini({
208
256
  The prompt still leaves your process to reach the Shrike backend for scanning;
209
257
  the *model call* stays on your local/self-hosted endpoint.
210
258
 
211
- ## SQL and File Scanning
259
+ ## Scanning agent actions (shell commands, SQL, web search, RAG, MCP tools)
260
+
261
+ Scanning the prompt protects the model. It does not protect the shell. An agent
262
+ that was never told anything malicious can still be talked into running
263
+ `curl … | sh` by a poisoned README, and the prompt scan has no view of that.
264
+
265
+ `ScanClient` exposes a method per action channel. Call the one that matches
266
+ what the agent is about to do, before it does it:
212
267
 
213
- The OpenAI client also provides standalone scanning for SQL queries and file paths:
268
+ | Channel | Method | Screens for |
269
+ |---|---|---|
270
+ | Shell command | `scanCommand(command, cwd?)` | destructive commands, data exfiltration, credential dumps, embedded SQL injection |
271
+ | SQL query | `scanSql(query, database?, allowDestructive?)` | SQL injection, unauthorized destructive statements |
272
+ | File path | `scanFile(path)` | path traversal, writes outside the working tree |
273
+ | File content | `scanFile(path, content)` | secrets, credentials, PII before they land on disk |
274
+ | Web search | `scanWebSearch(query)` | searches that acquire attack tooling, credentials, or evasion tradecraft |
275
+ | RAG context | `scanRagContext(chunks, query?)` | indirect prompt injection in retrieved documents |
276
+ | Agent message | `scanA2AMessage(message, options?)` | instructions smuggled between agents |
277
+ | Agent card | `scanAgentCard(card, verifySignature?)` | capability misrepresentation in A2A discovery |
278
+ | MCP tool schema | `scanMcpSchema(name, description, inputSchema?)` | tool poisoning in `tools/list` responses |
214
279
 
215
280
  ```typescript
216
- import { ShrikeOpenAI } from 'shrike-guard/openai';
281
+ import { ScanClient } from 'shrike-guard';
217
282
 
218
- const client = new ShrikeOpenAI({
219
- apiKey: 'sk-...',
220
- shrikeApiKey: 'shrike-...',
221
- });
283
+ const scanner = new ScanClient({ apiKey: 'shrike-...' });
222
284
 
223
- // Scan SQL queries for injection attacks
224
- const sqlResult = await client.scanSql('SELECT * FROM users WHERE id = 1');
225
- if (!sqlResult.safe) {
226
- console.log(`SQL threat: ${sqlResult.reason}`);
285
+ // Before shelling out
286
+ const cmd = await scanner.scanCommand('psql -c "SELECT * FROM users"', '/srv/app');
287
+ if (!cmd.safe) throw new Error(`Refused: ${cmd.reason}`);
288
+
289
+ // Before querying
290
+ const sql = await scanner.scanSql('SELECT * FROM users WHERE id = $1', 'postgres');
291
+
292
+ // Before writing
293
+ const write = await scanner.scanFile('/tmp/config.py', 'api_key = "sk-..."');
294
+
295
+ // Before searching the web
296
+ const search = await scanner.scanWebSearch('sql injection prevention owasp');
297
+ ```
298
+
299
+ A shell command is not one thing. `scanCommand` decomposes it, so SQL passed to
300
+ `psql -c`, `mysql -e`, or a heredoc is scanned as SQL rather than as an opaque
301
+ string of shell text.
302
+
303
+ ### Indirect prompt injection in RAG pipelines
304
+
305
+ Retrieved chunks are text somebody else wrote. They are the standard carrier for
306
+ indirect prompt injection: the user asks nothing unusual, the document tells the
307
+ model what to do, and the model complies. Scan on the way in, not after the
308
+ model has acted:
309
+
310
+ ```typescript
311
+ const chunks = await vectorStore.similaritySearch(userQuery, 5);
312
+
313
+ const verdict = await scanner.scanRagContext(
314
+ chunks.map((c) => c.pageContent),
315
+ userQuery,
316
+ );
317
+
318
+ if (!verdict.safe) {
319
+ // The retrieved context is hostile, not the user's question.
320
+ console.warn(`Poisoned context: ${verdict.reason}`);
321
+ }
322
+ ```
323
+
324
+ ### MCP tool poisoning
325
+
326
+ An MCP tool description is read by the model as guidance. A hostile server can
327
+ put instructions in the `description` field of a tool that never executes, and
328
+ the agent will act on them at registration time. Screen every entry of a
329
+ `tools/list` response from a server you do not control:
330
+
331
+ ```typescript
332
+ const { tools } = await mcpClient.listTools();
333
+
334
+ for (const tool of tools) {
335
+ const verdict = await scanner.scanMcpSchema(
336
+ tool.name,
337
+ tool.description ?? '',
338
+ tool.inputSchema,
339
+ );
340
+ if (!verdict.safe) {
341
+ console.warn(`Not registering ${tool.name}: ${verdict.reason}`);
342
+ continue;
343
+ }
344
+ register(tool);
227
345
  }
346
+ ```
347
+
348
+ Screening happens once per tool at registration, not on every call.
349
+
350
+ ## Who is answerable: `content_origin`
228
351
 
229
- // Scan file paths for path traversal
230
- const fileResult = await client.scanFile('/app/data/output.csv');
352
+ Every verdict carries `content_origin`, which says where the scanned content
353
+ came from. It answers the question a verdict alone cannot: *was that my prompt,
354
+ or the agent acting on its own?*
231
355
 
232
- // Scan file content for secrets
233
- const contentResult = await client.scanFile('/tmp/config.py', 'api_key = "sk-..."');
356
+ | Value | Meaning |
357
+ |---|---|
358
+ | `human_prompt` | the operator typed it |
359
+ | `agent_output` | the model generated it |
360
+ | `agent_action` | the agent is about to do it (every act-plane channel) |
361
+ | `third_party` | it arrived from outside: a tool result, a retrieved document, a peer agent |
362
+
363
+ ```typescript
364
+ import { attributableToOperator } from 'shrike-guard';
365
+
366
+ const verdict = await scanner.scanRagContext(chunks);
367
+
368
+ if (!verdict.safe) {
369
+ if (attributableToOperator(verdict.content_origin)) {
370
+ showUser(`Your request was blocked: ${verdict.reason}`);
371
+ } else {
372
+ // The agent poisoned its own context. Telling the user "your request
373
+ // was blocked" would be both wrong and unhelpful.
374
+ log.warn('agent-side refusal', verdict.reason);
375
+ retryWithCleanContext();
376
+ }
377
+ }
234
378
  ```
235
379
 
380
+ Unknown content types resolve to `agent_action`, never to `human_prompt`:
381
+ attributing an unattributable action to the operator is the one error that is
382
+ never safe to make by default.
383
+
236
384
  ## Error Handling
237
385
 
238
386
  ```typescript
@@ -298,6 +446,8 @@ export OPENAI_API_KEY="sk-..."
298
446
  export ANTHROPIC_API_KEY="sk-ant-..."
299
447
  export SHRIKE_API_KEY="shrike-..."
300
448
  export SHRIKE_ENDPOINT="https://your-shrike-instance.com"
449
+ export SHRIKE_AGENT_ID="ingest" # names this process's agent; see Sessions
450
+ export SHRIKE_SUPPRESS_SESSION_WARNING=1 # once you have decided the shared session is right
301
451
  ```
302
452
 
303
453
  ## Scope and Limitations
@@ -308,13 +458,23 @@ export SHRIKE_ENDPOINT="https://your-shrike-instance.com"
308
458
  | Multi-modal text content | Image/audio content |
309
459
  | SQL queries | Non-chat API calls |
310
460
  | File paths and content | |
461
+ | Shell commands | |
462
+ | Web search queries | |
463
+ | Retrieved RAG context | |
464
+ | Agent-to-agent messages and agent cards | |
465
+ | MCP tool schemas | |
311
466
 
312
- ### Why Input-Only Scanning?
467
+ ### Why Pre-Execution Scanning?
313
468
 
314
- Shrike Guard focuses on **pre-flight protection** — blocking malicious prompts BEFORE they reach the LLM. This:
469
+ Shrike Guard focuses on **pre-flight protection** — evaluating a prompt before it
470
+ reaches the LLM, and an action before it runs. This:
315
471
  - Prevents prompt injection attacks at the source
316
472
  - Has zero latency impact on LLM responses
317
- - Catches the vast majority of threats at the input layer
473
+ - Puts the decision point before the side effect, where refusing still costs nothing
474
+
475
+ An action already taken cannot be un-taken by detecting it afterwards. That is the
476
+ distinction between this and after-the-fact monitoring: the verdict arrives while
477
+ refusing is still free.
318
478
 
319
479
  ## Other Integration Surfaces
320
480
 
@@ -332,22 +492,39 @@ Shrike Guard is one of several ways to integrate with the Shrike platform:
332
492
  | Scenario | How Shrike Guard Helps |
333
493
  |---|---|
334
494
  | **Customer chatbot** | Wrap your OpenAI/Anthropic client. Every user message scanned for injection before it reaches the model. |
335
- | **Internal RAG pipeline** | Scan retrieved context + user queries for PII leakage and injection attempts. |
336
- | **AI coding assistant** | Scan prompts for proprietary code patterns before they leave your environment. |
337
- | **Agent orchestration** | Scan every tool call and LLM request in your LangChain/CrewAI/AutoGen pipeline. |
338
-
339
- ## Alternatives
340
-
341
- Looking for an AI security SDK? Here's how Shrike Guard compares:
342
-
343
- | Feature | Shrike Guard | Lakera | Prompt Armor |
344
- |---|---|---|---|
345
- | Drop-in OpenAI/Anthropic/Gemini wrapper | Yes | No | No |
346
- | 9-layer cognitive pipeline | Yes | Limited | Limited |
347
- | PII detection + redaction | Yes | Partial | No |
348
- | Session correlation | Yes (Pro+) | No | No |
349
- | Free tier (no API key) | Yes | No | No |
350
- | Open source client | Yes (Apache 2.0) | No | No |
495
+ | **Coding agents that shell out** | `scanCommand` before every `exec`. Destructive commands, exfiltration, and SQL smuggled through `psql -c` are caught before the process starts. |
496
+ | **Internal RAG pipeline** | `scanRagContext` on retrieved chunks for indirect prompt injection, plus PII leakage on the query. |
497
+ | **MCP clients** | `scanMcpSchema` on every `tools/list` entry from a server you do not control, to catch tool poisoning at registration. |
498
+ | **Multi-agent systems** | `scanA2AMessage` and `scanAgentCard` for instructions smuggled between agents. |
499
+ | **Agent orchestration** | Scan every tool call and LLM request in your LangChain/LangGraph/CrewAI/AutoGen pipeline. |
500
+
501
+ ## How This Differs From a Prompt Scanner
502
+
503
+ If you are evaluating TypeScript or JavaScript AI security SDKs, this is the
504
+ distinction worth testing against your own workload.
505
+
506
+ Most guardrail libraries answer one question: **is this text hostile?** They read
507
+ the prompt, and sometimes the response. That is necessary, and Shrike Guard does
508
+ it through a 9-layer cascade with PII redaction and multi-turn session
509
+ correlation.
510
+
511
+ But an autonomous agent does not cause harm by saying something. It causes harm
512
+ by *doing* something: running a command, writing a file, querying a database,
513
+ calling a tool. Shrike Guard scans those too, before they execute:
514
+
515
+ - **A verdict per action channel** — shell commands, SQL, file writes, web
516
+ searches, RAG context, agent-to-agent messages, agent cards, MCP tool schemas.
517
+ See [Scanning agent actions](#scanning-agent-actions-shell-commands-sql-web-search-rag-mcp-tools).
518
+ - **Pre-execution, not after the fact.** The verdict arrives while refusing is
519
+ still free. An action already taken cannot be un-taken by detecting it.
520
+ - **Provenance on every verdict** (`content_origin`) — whether a human typed it,
521
+ the model wrote it, the agent is about to do it, or it arrived from outside.
522
+ - **A governance contract, not just a boolean** — `refuse_tier`
523
+ (allow / warn / require_approval / block), a `recovery` block telling the agent
524
+ how to proceed legitimately, and `session_state`. Present on safe verdicts too.
525
+ - **Drop-in wrappers** for OpenAI, Anthropic, and Gemini, so the prompt-scanning
526
+ half needs no code changes.
527
+ - **Free tier with no API key**, and an Apache 2.0 client you can read.
351
528
 
352
529
  ## License
353
530
 
@@ -1,6 +1,6 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { MessageCreateParams, Message } from '@anthropic-ai/sdk/resources/messages';
3
- import { F as FailMode, S as ScanResult } from '../scanner-cxA3LnQR.mjs';
3
+ import { F as FailMode, S as ScanResult } from '../scanner-z9r8BJvO.mjs';
4
4
 
5
5
  /**
6
6
  * ShrikeAnthropic - Drop-in replacement for Anthropic client with security scanning.
@@ -1,6 +1,6 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { MessageCreateParams, Message } from '@anthropic-ai/sdk/resources/messages';
3
- import { F as FailMode, S as ScanResult } from '../scanner-cxA3LnQR.js';
3
+ import { F as FailMode, S as ScanResult } from '../scanner-z9r8BJvO.js';
4
4
 
5
5
  /**
6
6
  * ShrikeAnthropic - Drop-in replacement for Anthropic client with security scanning.
@@ -13,12 +13,12 @@ var DEFAULT_FAIL_MODE = "closed" /* CLOSED */;
13
13
  var DEFAULT_ENDPOINT = "https://api.shrikesecurity.com/agent";
14
14
  var SDK_NAME = "typescript";
15
15
  var SESSION_ID = crypto.randomUUID();
16
- var AGENT_ID = process.env.SHRIKE_AGENT_ID || `sdk-ts-${crypto.randomUUID().slice(0, 8)}`;
16
+ var AGENT_ID = `sdk-ts-${crypto.randomUUID().slice(0, 8)}`;
17
17
  function getSessionId() {
18
18
  return SESSION_ID;
19
19
  }
20
20
  function getAgentId() {
21
- return AGENT_ID;
21
+ return process.env.SHRIKE_AGENT_ID || AGENT_ID;
22
22
  }
23
23
 
24
24
  // src/errors.ts
@@ -266,6 +266,13 @@ var PRESERVED_GOVERNANCE_FIELDS = [
266
266
  "recovery",
267
267
  "session_state",
268
268
  "content_type",
269
+ // Provenance: human_prompt / agent_output / agent_action / third_party.
270
+ // An OUTCOME field, not detection attribution — it says who is answerable
271
+ // for the scanned content, which the caller needs in order to know who a
272
+ // refusal is even addressed to. It shipped on the backend in 2cfaa5f and was
273
+ // silently dropped here, because this is an allow-list and nobody added it:
274
+ // a new backend field reaches no caller until it appears in this array.
275
+ "content_origin",
269
276
  "approval_info",
270
277
  "client_session_rotation"
271
278
  ];
@@ -330,7 +337,7 @@ function sanitizeScanResponse(raw) {
330
337
  }
331
338
 
332
339
  // package.json
333
- var version = "4.0.5";
340
+ var version = "4.1.0";
334
341
 
335
342
  // src/version.ts
336
343
  var VERSION = version;