@silmaril-security/sdk 0.7.0 → 0.7.2
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 +18 -2
- package/README.md +106 -52
- package/dist/index.cjs +25 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,14 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Silmaril Firewall TypeScript SDK are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.7.2 - 2026-10-05
|
|
6
|
+
|
|
7
|
+
- Clear the attempt timeout immediately on caller cancellation, including when
|
|
8
|
+
retry-response body disposal never settles. This prevents residual timers
|
|
9
|
+
from keeping command-hook processes alive beyond their shared deadline.
|
|
10
|
+
- Make retried-response cleanup respect caller cancellation and attempt timeouts
|
|
11
|
+
even when a stream cancellation promise never settles.
|
|
12
|
+
|
|
13
|
+
## 0.7.1 - 2026-10-04
|
|
14
|
+
|
|
15
|
+
- Reject malformed batch responses and result counts that do not match the
|
|
16
|
+
serialized request, including retried requests whose caller inputs change.
|
|
17
|
+
|
|
5
18
|
## 0.7.0 - 2026-09-30
|
|
6
19
|
|
|
7
20
|
- Add optional Deep Agents middleware and protected constructors for root, general-purpose,
|
|
8
21
|
declarative, and explicitly protected compiled subagents. The compiled graph factory
|
|
9
22
|
records middleware installation and constructors reject unverified graphs.
|
|
23
|
+
- Declare optional peer dependencies for Deep Agents:
|
|
24
|
+
`deepagents@^1.14.1`, `langchain@^1.5.10`, `@langchain/langgraph@^1.4.10`, and
|
|
25
|
+
existing `@langchain/core@>=0.3.0`.
|
|
10
26
|
- Enforce input, tool-call, tool-result, and non-streamed model-output decisions in
|
|
11
|
-
Block mode with safe continuation and a repeated-denial cap
|
|
12
|
-
decisions without changing content.
|
|
27
|
+
Block mode with safe continuation and a repeated-denial cap (`maxBlockedAttempts`,
|
|
28
|
+
default `3`). Shadow and Warn report decisions without changing content.
|
|
13
29
|
- Enable LangChain tool start/end callbacks by default and send the LangChain run ID
|
|
14
30
|
as correlation metadata alongside a distinct classification request ID per event.
|
|
15
31
|
|
package/README.md
CHANGED
|
@@ -18,13 +18,13 @@ This SDK provides the low-level TypeScript interface for that workflow:
|
|
|
18
18
|
- Classify user input, tool calls, tool responses, model output, or system
|
|
19
19
|
prompt content.
|
|
20
20
|
- Preserve hook and tool-name context for more accurate decisions.
|
|
21
|
-
-
|
|
21
|
+
- Honor backend threat and governance decisions and effective Shadow, Warn, or
|
|
22
22
|
Block behavior in adapters.
|
|
23
23
|
- Send each complete sanitized event in one request.
|
|
24
|
-
-
|
|
24
|
+
- On individual `classify()` calls, preserve exact `metadata.conversationId` as sequence identity and add one event ID.
|
|
25
25
|
- Retry API rate-limit responses.
|
|
26
|
-
- Optionally attach the firewall to Vercel AI SDK middleware
|
|
27
|
-
callback flows.
|
|
26
|
+
- Optionally attach the firewall to Vercel AI SDK middleware, LangChain.js
|
|
27
|
+
callback flows, and Deep Agents middleware/constructors.
|
|
28
28
|
|
|
29
29
|
## Install
|
|
30
30
|
|
|
@@ -37,7 +37,7 @@ npm install @silmaril-security/sdk
|
|
|
37
37
|
For reproducible installs, pin a tagged release:
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
|
-
npm install @silmaril-security/sdk@0.7.
|
|
40
|
+
npm install @silmaril-security/sdk@0.7.2
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Requires Node 20 or later.
|
|
@@ -63,10 +63,10 @@ Optional LangChain.js support:
|
|
|
63
63
|
npm install @langchain/core @langchain/openai
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Optional Deep Agents support:
|
|
66
|
+
Optional Deep Agents support (peer ranges shipped with `0.7.0`):
|
|
67
67
|
|
|
68
68
|
```sh
|
|
69
|
-
npm install deepagents langchain @langchain/core @langchain/langgraph
|
|
69
|
+
npm install "deepagents@^1.14.1" "langchain@^1.5.10" "@langchain/core@>=0.3.0" "@langchain/langgraph@^1.4.10"
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## Configuration
|
|
@@ -119,15 +119,21 @@ console.log(`tool output: ${toolResult.prediction} ${toolResult.score.toFixed(4)
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
`classify()` and `classifyBatch()` return the server's prediction, score, and
|
|
122
|
-
|
|
123
|
-
The Vercel AI SDK and LangChain.js adapters
|
|
124
|
-
`FirewallBlockedException` when
|
|
122
|
+
diagnostic threshold. Direct calls do not throw on a `MALICIOUS` prediction or
|
|
123
|
+
a governance block. The Vercel AI SDK and LangChain.js adapters throw
|
|
124
|
+
`FirewallBlockedException` only when `prediction` is `MALICIOUS` or
|
|
125
|
+
`governance.action` is `block`, and the effective mode is `block`.
|
|
126
|
+
`result.threshold` is backend-returned diagnostic metadata carried on the
|
|
127
|
+
result and the exception. Disabled and observe policy paths can retain the
|
|
128
|
+
compatibility threshold. The adapters do not compare `score` to a local
|
|
129
|
+
threshold.
|
|
125
130
|
|
|
126
131
|
## Concurrency and Cancellation
|
|
127
132
|
|
|
128
133
|
A `Firewall` instance holds no per-request state, so one client can serve any
|
|
129
|
-
number of concurrent calls in a runtime.
|
|
130
|
-
|
|
134
|
+
number of concurrent calls in a runtime. Concurrent calls are appropriate for
|
|
135
|
+
independent events or different conversations. Responses are matched to their
|
|
136
|
+
own promise regardless of completion order:
|
|
131
137
|
|
|
132
138
|
```ts
|
|
133
139
|
const [userResult, toolResult] = await Promise.all([
|
|
@@ -136,6 +142,10 @@ const [userResult, toolResult] = await Promise.all([
|
|
|
136
142
|
]);
|
|
137
143
|
```
|
|
138
144
|
|
|
145
|
+
Events that belong to one conversation are not independent. Send them with
|
|
146
|
+
ordered individual `classify()` calls that share `metadata.conversationId`, and
|
|
147
|
+
wait for each call before sending the next event for that conversation.
|
|
148
|
+
|
|
139
149
|
Pass a `signal` to cancel one call without touching its siblings:
|
|
140
150
|
|
|
141
151
|
```ts
|
|
@@ -242,17 +252,16 @@ to each attempt and is combined with any caller-supplied `signal`.
|
|
|
242
252
|
|
|
243
253
|
## Backend Thresholding
|
|
244
254
|
|
|
245
|
-
Customers do not tune score thresholds in the SDK.
|
|
246
|
-
the
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
more are capped at `0.9`.
|
|
255
|
+
Customers do not tune score thresholds in the SDK. The Firewall backend owns
|
|
256
|
+
the threat decision and threshold policy. The current Cascade backend resolves
|
|
257
|
+
decision thresholds from a tenant default or a hook-specific override; it does
|
|
258
|
+
not raise them as text length, token-window count, batch size, or conversation
|
|
259
|
+
length grows.
|
|
251
260
|
|
|
252
|
-
The SDK does not send `threshold` in request payloads.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
261
|
+
The SDK does not send `threshold` in request payloads. `BlockResult.threshold`
|
|
262
|
+
and `FirewallBlockedException.threshold` are backend-returned diagnostic
|
|
263
|
+
metadata. Disabled and observe policy paths can retain the compatibility
|
|
264
|
+
threshold.
|
|
256
265
|
|
|
257
266
|
## Modes
|
|
258
267
|
|
|
@@ -350,14 +359,21 @@ await fw.classify(text, {
|
|
|
350
359
|
```
|
|
351
360
|
|
|
352
361
|
The SDK preserves caller metadata and adds a reserved `metadata.silmaril`
|
|
353
|
-
namespace to every request. SDK-controlled fields
|
|
354
|
-
`sdk_version`, and `request_id`; batches additionally
|
|
355
|
-
|
|
356
|
-
the typed governance option is supplied; it replaces any
|
|
357
|
-
`metadata.silmaril.governance` value.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
362
|
+
namespace to every request, including each batch item. SDK-controlled fields
|
|
363
|
+
are `sdk_language`, `sdk_version`, and `request_id`; batches additionally
|
|
364
|
+
carry `input_index` for diagnostics. `governance` is also SDK-controlled
|
|
365
|
+
whenever the typed governance option is supplied; it replaces any
|
|
366
|
+
caller-provided `metadata.silmaril.governance` value. On individual
|
|
367
|
+
`classify()` calls, exact `metadata.conversationId` is preserved as the
|
|
368
|
+
backend sequence identity. Batch items also preserve per-item metadata,
|
|
369
|
+
including `metadata.conversationId`, but current Cascade treats each input
|
|
370
|
+
independently and neither reads nor updates conversation history. Giving items
|
|
371
|
+
the same `metadata.conversationId` does not connect them into a sequence. For
|
|
372
|
+
conversation-aware checks, send complete events through ordered individual
|
|
373
|
+
`classify()` calls with the same `metadata.conversationId`, and wait for each
|
|
374
|
+
call before sending the next event for that conversation. No aliases are
|
|
375
|
+
inspected. If callers provide `metadata.silmaril`, it must be an object and
|
|
376
|
+
SDK-reserved keys are overwritten by the SDK.
|
|
361
377
|
|
|
362
378
|
The `governance` option on `classify()` and per-item `governance` array on
|
|
363
379
|
`classifyBatch()` send typed agent/resource context. `BlockResult.governance`
|
|
@@ -370,16 +386,22 @@ responses without governance remain valid.
|
|
|
370
386
|
- `SilmarilApiError`: thrown when the firewall API responds with a non-2xx or redirect status. Carries `status`, `statusText`, a 64 KiB-capped `body`, and any parsed malformed-input diagnostics. The default error message omits the body to keep logs clean.
|
|
371
387
|
- `FirewallBlockedException`: thrown by the Vercel AI SDK and LangChain.js adapters when a malicious or governance-block decision has effective Block mode. The message distinguishes governance-policy denials from threat-score denials. Carries `score`, `threshold`, `promptText`, and optional `runId`, `hook`, `toolName`, `toolCallId`, and `result`.
|
|
372
388
|
|
|
373
|
-
`PromptBlockedException` remains as a deprecated alias
|
|
389
|
+
`PromptBlockedException` remains as a deprecated alias of
|
|
390
|
+
`FirewallBlockedException`.
|
|
374
391
|
|
|
375
392
|
All SDK exception types extend `Error` and work with `instanceof`.
|
|
376
393
|
|
|
377
394
|
## Complete events
|
|
378
395
|
|
|
379
396
|
`classify()` sanitizes invalid Unicode surrogate fragments and sends the full
|
|
380
|
-
logical event once.
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
logical event once. For conversation-aware checks, send those complete events
|
|
398
|
+
through ordered individual `classify()` calls with the same
|
|
399
|
+
`metadata.conversationId`, and wait for each call before sending the next
|
|
400
|
+
event for that conversation. The backend owns token-window processing and
|
|
401
|
+
sequence ordering for those individual calls. `classifyBatch()` sends
|
|
402
|
+
independent texts in one request: each item keeps its metadata, including
|
|
403
|
+
`metadata.conversationId`, but Cascade neither reads nor updates conversation
|
|
404
|
+
history for the batch.
|
|
383
405
|
|
|
384
406
|
## Batch Classification
|
|
385
407
|
|
|
@@ -403,9 +425,36 @@ console.log(`classified ${results.length} items`);
|
|
|
403
425
|
```
|
|
404
426
|
|
|
405
427
|
Batch requests preserve result order and can carry per-item hooks, tool names,
|
|
406
|
-
and
|
|
407
|
-
texts. Each batch
|
|
408
|
-
|
|
428
|
+
metadata, and governance. Hook, tool-name, metadata, and governance arrays must
|
|
429
|
+
match the number of texts. Each batch item preserves caller metadata, including
|
|
430
|
+
`metadata.conversationId`, and carries SDK metadata so the backend can apply
|
|
431
|
+
tenant-owned thresholding. Current Cascade treats each input independently and
|
|
432
|
+
neither reads nor updates conversation history. Giving items the same
|
|
433
|
+
`metadata.conversationId` does not connect them into a sequence.
|
|
434
|
+
|
|
435
|
+
For conversation-aware checks, send complete events through ordered individual
|
|
436
|
+
`classify()` calls with the same `metadata.conversationId`. Wait for each call
|
|
437
|
+
before sending the next event for that conversation:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
const conversationId = "customer-conversation-123";
|
|
441
|
+
|
|
442
|
+
const first = await fw.classify(firstEvent, {
|
|
443
|
+
hook: HookLabel.USER_INPUT,
|
|
444
|
+
metadata: { conversationId },
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const second = await fw.classify(secondEvent, {
|
|
448
|
+
hook: HookLabel.TOOL_RESPONSE,
|
|
449
|
+
toolName: "read_file",
|
|
450
|
+
metadata: { conversationId },
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
Batch responses must contain a `predictions` array with exactly one result per
|
|
455
|
+
sent input. A missing array or mismatched count raises an `Error` before
|
|
456
|
+
results are returned or blocking callbacks run. Retries use the original
|
|
457
|
+
payload snapshot, even if caller-owned inputs change while the request is in flight.
|
|
409
458
|
|
|
410
459
|
## Migration Notes
|
|
411
460
|
|
|
@@ -416,7 +465,7 @@ tenant/backend config, adds SDK reconstruction metadata, renames blocking
|
|
|
416
465
|
exceptions to `FirewallBlockedException`, keeps optional LangChain.js types out
|
|
417
466
|
of the root package declarations, adds typed CJS/ESM export conditions, supports
|
|
418
467
|
Vercel AI SDK v5 and v6. The deprecated
|
|
419
|
-
`PromptBlockedException` alias remains available
|
|
468
|
+
`PromptBlockedException` alias remains available.
|
|
420
469
|
|
|
421
470
|
## Vercel AI SDK Middleware
|
|
422
471
|
|
|
@@ -439,10 +488,13 @@ const { text } = await generateText({ model, prompt: "Hello" });
|
|
|
439
488
|
console.log(text);
|
|
440
489
|
```
|
|
441
490
|
|
|
442
|
-
Middleware scans input by default
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
491
|
+
Middleware scans input by default (`scanInput` defaults to true). It classifies
|
|
492
|
+
that message's tool-result parts as `tool_response` only when the newest prompt
|
|
493
|
+
message is a tool message. Otherwise it classifies the latest user message as
|
|
494
|
+
`user_input` and does not scan earlier tool results. Set `scanOutput: true` to
|
|
495
|
+
classify model text, and `scanToolCalls: true` to classify tool-call arguments
|
|
496
|
+
on generate results. Infrastructure errors and blocking decisions are
|
|
497
|
+
fail-closed by default and bubble up to the caller.
|
|
446
498
|
|
|
447
499
|
When `scanOutput: true` is combined with streaming, output is classified in the
|
|
448
500
|
stream's `flush` after all deltas have been emitted, so blocking is advisory:
|
|
@@ -473,11 +525,11 @@ await model.invoke("Hello");
|
|
|
473
525
|
|
|
474
526
|
The LangChain handler is fail-open by default: infrastructure errors are logged
|
|
475
527
|
and the LLM call proceeds. Set `failOpen: false` to make API errors bubble up.
|
|
476
|
-
Blocking decisions
|
|
477
|
-
|
|
478
|
-
|
|
528
|
+
Blocking decisions throw `FirewallBlockedException` only when the effective mode
|
|
529
|
+
is Block; Shadow and Warn preserve the host flow.
|
|
530
|
+
`PromptBlockedException` continues to work as a deprecated alias.
|
|
479
531
|
Model-start, tool-start, and tool-end hooks are enabled by default; retriever
|
|
480
|
-
hooks remain opt-in. The LangChain run ID is sent as
|
|
532
|
+
and model-end (`LLM_END`) hooks remain opt-in. The LangChain run ID is sent as
|
|
481
533
|
`metadata.langgraph.run_id`; every classification gets a distinct
|
|
482
534
|
`metadata.silmaril.request_id`. Supply `conversationId` to the handler for
|
|
483
535
|
sequence identity; it sends `metadata.conversationId`. Callback blocks throw
|
|
@@ -515,17 +567,19 @@ model, tools })` and pass its returned spec through
|
|
|
515
567
|
`protectedCompiledSubagents`. That factory installs middleware before
|
|
516
568
|
compilation. The parent constructor verifies the exact graph and Firewall
|
|
517
569
|
client; it rejects arbitrary compiled runnables. Root custom middleware does
|
|
518
|
-
not automatically reach every subagent.
|
|
570
|
+
not automatically reach every subagent. For custom agent construction, import
|
|
571
|
+
`createDeepAgentsMiddleware` from the same adapter subpath.
|
|
519
572
|
|
|
520
573
|
The middleware checks input before model use, tool calls before execution,
|
|
521
574
|
tool results before the next model call, and non-streamed model output before
|
|
522
575
|
the graph consumes it. In Block mode, denied tool interactions become a fixed
|
|
523
576
|
safe `ToolMessage` with the original call ID. The agent can choose an allowed
|
|
524
|
-
alternative;
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
577
|
+
alternative; after `maxBlockedAttempts` denials (default `3`), the middleware
|
|
578
|
+
returns a fixed safe final response. Denied model output is replaced. Shadow
|
|
579
|
+
and Warn report decisions through `onClassify` without replacing content.
|
|
580
|
+
Classification errors allow model and tool execution by default; set
|
|
581
|
+
`silmaril: { failOpen: false }` to require a successful classification. Already
|
|
582
|
+
emitted streaming text cannot be recalled.
|
|
529
583
|
|
|
530
584
|
## Retries
|
|
531
585
|
|
package/dist/index.cjs
CHANGED
|
@@ -818,7 +818,7 @@ function sanitizeText(text) {
|
|
|
818
818
|
}
|
|
819
819
|
|
|
820
820
|
// src/firewall.ts
|
|
821
|
-
var SDK_VERSION = "0.7.
|
|
821
|
+
var SDK_VERSION = "0.7.2";
|
|
822
822
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
823
823
|
var DEFAULT_MAX_RETRIES = 5;
|
|
824
824
|
var MAX_BACKOFF_SECONDS = 30;
|
|
@@ -962,7 +962,7 @@ async function readCappedErrorBody(response) {
|
|
|
962
962
|
}
|
|
963
963
|
return new TextDecoder().decode(body);
|
|
964
964
|
}
|
|
965
|
-
async function
|
|
965
|
+
async function releaseDiscardedResponseBody(response) {
|
|
966
966
|
try {
|
|
967
967
|
if (response.body) {
|
|
968
968
|
if (!response.body.locked) {
|
|
@@ -974,6 +974,19 @@ async function discardResponseBody(response) {
|
|
|
974
974
|
} catch {
|
|
975
975
|
}
|
|
976
976
|
}
|
|
977
|
+
async function discardResponseBody(response, signal) {
|
|
978
|
+
signal.throwIfAborted();
|
|
979
|
+
let onAbort;
|
|
980
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
981
|
+
onAbort = () => reject(signal.reason);
|
|
982
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
983
|
+
});
|
|
984
|
+
try {
|
|
985
|
+
await Promise.race([releaseDiscardedResponseBody(response), aborted]);
|
|
986
|
+
} finally {
|
|
987
|
+
signal.removeEventListener("abort", onAbort);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
977
990
|
function isAbortLikeError(error) {
|
|
978
991
|
if (typeof error !== "object" || error === null) {
|
|
979
992
|
return false;
|
|
@@ -999,6 +1012,7 @@ function createAttemptSignal(timeoutMs, callerSignal) {
|
|
|
999
1012
|
);
|
|
1000
1013
|
}, timeoutMs);
|
|
1001
1014
|
const onCallerAbort = () => {
|
|
1015
|
+
clearTimeout(timer);
|
|
1002
1016
|
controller.abort(callerSignal?.reason);
|
|
1003
1017
|
};
|
|
1004
1018
|
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
@@ -1109,6 +1123,14 @@ var Firewall = class {
|
|
|
1109
1123
|
})
|
|
1110
1124
|
);
|
|
1111
1125
|
const data = await this.postWithRetry(payload, options.signal);
|
|
1126
|
+
if (!Array.isArray(data.predictions)) {
|
|
1127
|
+
throw new Error("Firewall: response predictions must be an array");
|
|
1128
|
+
}
|
|
1129
|
+
if (data.predictions.length !== payload.texts.length) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
`Firewall: response predictions length ${data.predictions.length} does not match submitted texts length ${payload.texts.length}`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1112
1134
|
return data.predictions.map((p) => blockResultFromResponse(p, requestedMode));
|
|
1113
1135
|
}
|
|
1114
1136
|
asLangChainHandler(options = {}) {
|
|
@@ -1148,7 +1170,7 @@ var Firewall = class {
|
|
|
1148
1170
|
attemptSignal.signal
|
|
1149
1171
|
);
|
|
1150
1172
|
}
|
|
1151
|
-
await discardResponseBody(response);
|
|
1173
|
+
await discardResponseBody(response, attemptSignal.signal);
|
|
1152
1174
|
} finally {
|
|
1153
1175
|
attemptSignal.dispose();
|
|
1154
1176
|
}
|