@silmaril-security/sdk 0.3.0 → 0.4.1
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 +22 -0
- package/README.md +86 -33
- package/dist/adapters/langchain.cjs +349 -0
- package/dist/adapters/langchain.cjs.map +1 -0
- package/dist/adapters/langchain.d.cts +6 -0
- package/dist/adapters/langchain.d.ts +6 -0
- package/dist/adapters/langchain.js +312 -0
- package/dist/adapters/langchain.js.map +1 -0
- package/dist/adapters/vercel.cjs +321 -0
- package/dist/adapters/vercel.cjs.map +1 -0
- package/dist/adapters/vercel.d.cts +1 -0
- package/dist/adapters/vercel.d.ts +1 -0
- package/dist/adapters/vercel.js +294 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/firewall-CEV5hItA.d.cts +165 -0
- package/dist/firewall-CEV5hItA.d.ts +165 -0
- package/dist/index.cjs +155 -83
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -147
- package/dist/index.d.ts +17 -147
- package/dist/index.js +151 -80
- package/dist/index.js.map +1 -1
- package/package.json +38 -10
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Silmaril Firewall TypeScript SDK are documented here.
|
|
4
|
+
|
|
5
|
+
## 0.4.1 - 2026-05-24
|
|
6
|
+
|
|
7
|
+
- Recover the `0.4.x` npm release by moving to `0.4.1`. The `v0.4.0` Git tag
|
|
8
|
+
exists, but `@silmaril-security/sdk@0.4.0` was never published to npm.
|
|
9
|
+
- Support Vercel AI SDK v5 and v6 with the `ai` peer range
|
|
10
|
+
`^5.0.0 || ^6.0.0`.
|
|
11
|
+
- Keep optional LangChain.js types out of the root package declarations so core
|
|
12
|
+
consumers can typecheck without installing `@langchain/core`.
|
|
13
|
+
- Add typed ESM and CommonJS export conditions, plus explicit adapter subpath
|
|
14
|
+
exports for `@silmaril-security/sdk/adapters/vercel` and
|
|
15
|
+
`@silmaril-security/sdk/adapters/langchain`.
|
|
16
|
+
- Add public repository hygiene docs for contribution and vulnerability
|
|
17
|
+
reporting.
|
|
18
|
+
|
|
19
|
+
## 0.4.0 - Skipped on npm
|
|
20
|
+
|
|
21
|
+
- Do not install this version. The Git tag exists, but the npm package was not
|
|
22
|
+
published.
|
package/README.md
CHANGED
|
@@ -18,9 +18,10 @@ 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
|
-
- Enforce
|
|
21
|
+
- Enforce backend-owned adaptive thresholds in adapters, with shadow mode for
|
|
22
22
|
observation-only rollout.
|
|
23
23
|
- Chunk long inputs consistently before they reach the API.
|
|
24
|
+
- Send SDK metadata that lets the Firewall reconstruct chunked payloads.
|
|
24
25
|
- Retry API rate-limit responses.
|
|
25
26
|
- Optionally attach the firewall to Vercel AI SDK middleware and LangChain.js
|
|
26
27
|
callback flows.
|
|
@@ -36,16 +37,21 @@ npm install @silmaril-security/sdk
|
|
|
36
37
|
For reproducible installs, pin a tagged release:
|
|
37
38
|
|
|
38
39
|
```sh
|
|
39
|
-
npm install @silmaril-security/sdk@0.
|
|
40
|
+
npm install @silmaril-security/sdk@0.4.1
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
Requires Node 18 or later.
|
|
43
44
|
|
|
44
45
|
The package name and SDK import path are both `@silmaril-security/sdk`, so call
|
|
45
|
-
sites use `Firewall`, `HookLabel`, and `
|
|
46
|
-
package.
|
|
46
|
+
sites use `Firewall`, `HookLabel`, and `FirewallBlockedException` from that
|
|
47
|
+
package. `PromptBlockedException` is a deprecated alias.
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
This repository is public and source-available for integration transparency,
|
|
50
|
+
but the SDK is not permissive open source. See [LICENSE](LICENSE) for the
|
|
51
|
+
governing terms.
|
|
52
|
+
|
|
53
|
+
The core client does not require optional framework peer dependencies. Optional
|
|
54
|
+
Vercel AI SDK middleware support is compatible with Vercel AI SDK v5 and v6:
|
|
49
55
|
|
|
50
56
|
```sh
|
|
51
57
|
npm install ai @ai-sdk/openai
|
|
@@ -109,7 +115,7 @@ console.log(`tool output: ${toolResult.prediction} ${toolResult.score.toFixed(4)
|
|
|
109
115
|
`classify()` and `classifyBatch()` return the server's prediction, score, and
|
|
110
116
|
internally applied threshold. Direct calls do not throw on malicious verdicts.
|
|
111
117
|
The Vercel AI SDK and LangChain.js adapters use `result.threshold` and throw
|
|
112
|
-
`
|
|
118
|
+
`FirewallBlockedException` when enforcement is enabled.
|
|
113
119
|
|
|
114
120
|
## Options
|
|
115
121
|
|
|
@@ -126,24 +132,26 @@ interface FirewallOptions {
|
|
|
126
132
|
The SDK uses native `fetch`, `AbortSignal.timeout`, and JSON request bodies with
|
|
127
133
|
`x-api-key` and `content-type` headers.
|
|
128
134
|
|
|
129
|
-
##
|
|
135
|
+
## Backend Thresholding
|
|
130
136
|
|
|
131
|
-
Customers do not tune score thresholds
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
5
|
|
137
|
+
Customers do not tune score thresholds in the SDK. Tenant Firewall config owns
|
|
138
|
+
the adaptive threshold schedule. The default backend config is
|
|
139
|
+
`base_threshold=0.5`, `target_sequence_fpr=0.01`, and
|
|
140
|
+
`max_adaptive_threshold=0.9`, which keeps the current schedule: 1 scoring
|
|
141
|
+
opportunity uses `0.5`, 2 use about `0.6661`, 5 use about `0.8328`, and 10 or
|
|
142
|
+
more are capped at `0.9`.
|
|
136
143
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
applied value remains available on
|
|
140
|
-
`
|
|
144
|
+
The SDK no longer sends `threshold` in request payloads. It sends chunk
|
|
145
|
+
metadata instead, and the backend combines tenant config, active batch size,
|
|
146
|
+
and chunk count to decide the threshold. The applied value remains available on
|
|
147
|
+
`BlockResult.threshold` and `FirewallBlockedException.threshold` as diagnostic
|
|
148
|
+
metadata.
|
|
141
149
|
|
|
142
150
|
## Shadow Mode
|
|
143
151
|
|
|
144
152
|
The Vercel AI SDK and LangChain.js adapters enforce thresholds by default.
|
|
145
153
|
Shadow mode keeps the same classification and threshold logic but suppresses
|
|
146
|
-
`
|
|
154
|
+
`FirewallBlockedException`, so live traffic can continue while telemetry records
|
|
147
155
|
what would have blocked:
|
|
148
156
|
|
|
149
157
|
```ts
|
|
@@ -175,8 +183,8 @@ const model = wrapLanguageModel({
|
|
|
175
183
|
await generateText({ model, prompt: "Hello" });
|
|
176
184
|
```
|
|
177
185
|
|
|
178
|
-
Per-adapter
|
|
179
|
-
client default:
|
|
186
|
+
Per-adapter `shadowMode` options let you enforce or shadow one surface without
|
|
187
|
+
changing the client default:
|
|
180
188
|
|
|
181
189
|
```ts
|
|
182
190
|
fw.asMiddleware({
|
|
@@ -188,10 +196,10 @@ await fw.asLangChainHandler({
|
|
|
188
196
|
});
|
|
189
197
|
```
|
|
190
198
|
|
|
191
|
-
`ClassifyEvent` includes `hook`, `toolName`, `
|
|
192
|
-
`shadowMode`. `blocked` is computed from
|
|
193
|
-
Direct `classify()` and `classifyBatch()`
|
|
194
|
-
unaffected by shadow mode.
|
|
199
|
+
`ClassifyEvent` includes `hook`, `toolName`, `toolCallId`, `runId`, `text`,
|
|
200
|
+
`result`, `blocked`, and `shadowMode`. `blocked` is computed from
|
|
201
|
+
`result.score >= result.threshold`. Direct `classify()` and `classifyBatch()`
|
|
202
|
+
calls never throw on verdicts and are unaffected by shadow mode.
|
|
195
203
|
|
|
196
204
|
## Hook Labels
|
|
197
205
|
|
|
@@ -209,17 +217,46 @@ text-prefix integrations. `classify()` and `classifyBatch()` send hook and tool
|
|
|
209
217
|
metadata as structured JSON fields, so normal callers should use the `hook`,
|
|
210
218
|
`toolName`, `hooks`, and `toolNames` options.
|
|
211
219
|
|
|
220
|
+
## Request Metadata
|
|
221
|
+
|
|
222
|
+
Use `metadata` to forward application or integration identifiers to the
|
|
223
|
+
classification API without embedding them in the classified text:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
await fw.classify(text, {
|
|
227
|
+
hook: HookLabel.USER_INPUT,
|
|
228
|
+
metadata: {
|
|
229
|
+
langgraph: {
|
|
230
|
+
thread_id: "customer-thread-123",
|
|
231
|
+
run_id: "langgraph-run-456",
|
|
232
|
+
message_id: "message-789",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The SDK preserves caller metadata and adds a reserved `metadata.silmaril`
|
|
239
|
+
namespace to every request. SDK-controlled fields are `sdk_language`,
|
|
240
|
+
`sdk_version`, `request_id`, `input_index`, `chunk_index`, and `chunk_count`.
|
|
241
|
+
Single unchunked requests use `input_index=0`, `chunk_index=0`, and
|
|
242
|
+
`chunk_count=1`; batches use one metadata object per input; chunked requests
|
|
243
|
+
reuse a single request id across all chunks. If callers provide
|
|
244
|
+
`metadata.silmaril`, it must be an object and SDK-reserved keys are overwritten
|
|
245
|
+
by the SDK.
|
|
246
|
+
|
|
212
247
|
## Errors
|
|
213
248
|
|
|
214
249
|
- `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.
|
|
215
|
-
- `
|
|
250
|
+
- `FirewallBlockedException`: thrown by the Vercel AI SDK and LangChain.js adapters in enforcement mode when the backend blocks the request. Carries `score`, `threshold`, `promptText`, and optional `runId`, `hook`, `toolName`, `toolCallId`, and `result`.
|
|
251
|
+
|
|
252
|
+
`PromptBlockedException` remains as a deprecated alias for one release.
|
|
216
253
|
|
|
217
254
|
All SDK exception types extend `Error` and work with `instanceof`.
|
|
218
255
|
|
|
219
256
|
## Chunking
|
|
220
257
|
|
|
221
258
|
Long inputs are chunked client-side into 400-token overlapping windows
|
|
222
|
-
(64-token overlap). The maximum input is
|
|
259
|
+
(64-token overlap). The maximum input is 81,920 tokens. For `classify()`, chunks
|
|
223
260
|
are sent as bounded parallel single-text requests using `chunkConcurrency`
|
|
224
261
|
(default: 8), letting API Gateway and SageMaker distribute work across serving
|
|
225
262
|
instances. The highest score is returned.
|
|
@@ -252,14 +289,19 @@ console.log(`classified ${results.length} items`);
|
|
|
252
289
|
|
|
253
290
|
Batch requests preserve result order and can carry per-item hooks, tool names,
|
|
254
291
|
and metadata. Hook, tool-name, and metadata arrays must match the number of
|
|
255
|
-
texts. Each batch carries
|
|
292
|
+
texts. Each batch carries SDK metadata per item so the backend can apply
|
|
293
|
+
tenant-owned thresholding.
|
|
256
294
|
|
|
257
295
|
## Migration Notes
|
|
258
296
|
|
|
259
|
-
Version `0.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
297
|
+
Version `0.4.1` is the public npm recovery release for `0.4.x`: the `v0.4.0`
|
|
298
|
+
Git tag exists, but `@silmaril-security/sdk@0.4.0` was not published to npm.
|
|
299
|
+
Use `0.4.1` or later. This release moves all threshold decisions to Firewall
|
|
300
|
+
tenant/backend config, adds SDK reconstruction metadata, renames blocking
|
|
301
|
+
exceptions to `FirewallBlockedException`, keeps optional LangChain.js types out
|
|
302
|
+
of the root package declarations, adds typed CJS/ESM export conditions, supports
|
|
303
|
+
Vercel AI SDK v5 and v6. The deprecated
|
|
304
|
+
`PromptBlockedException` alias remains available for one release.
|
|
263
305
|
|
|
264
306
|
## Vercel AI SDK Middleware
|
|
265
307
|
|
|
@@ -316,11 +358,22 @@ await model.invoke("Hello");
|
|
|
316
358
|
|
|
317
359
|
The LangChain handler is fail-open by default: infrastructure errors are logged
|
|
318
360
|
and the LLM call proceeds. Set `failOpen: false` to make API errors bubble up.
|
|
319
|
-
Blocking decisions still throw `
|
|
320
|
-
enabled.
|
|
361
|
+
Blocking decisions still throw `FirewallBlockedException` unless shadow mode is
|
|
362
|
+
enabled. `PromptBlockedException` continues to work as a deprecated alias for
|
|
363
|
+
one release.
|
|
321
364
|
|
|
322
365
|
`asLangChainHandler()` is async because it lazy-loads `@langchain/core` so core
|
|
323
|
-
users do not pay for it.
|
|
366
|
+
users do not pay for it. The root package intentionally exposes a structural
|
|
367
|
+
handler type so projects that do not install LangChain.js can still typecheck
|
|
368
|
+
with `skipLibCheck: false`. If your project needs the exact LangChain
|
|
369
|
+
`BaseCallbackHandler` return type, import the adapter helper from the optional
|
|
370
|
+
subpath:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
import { createLangChainHandler } from "@silmaril-security/sdk/adapters/langchain";
|
|
374
|
+
|
|
375
|
+
const handler = await createLangChainHandler(fw);
|
|
376
|
+
```
|
|
324
377
|
|
|
325
378
|
## Retries
|
|
326
379
|
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/adapters/langchain.ts
|
|
31
|
+
var langchain_exports = {};
|
|
32
|
+
__export(langchain_exports, {
|
|
33
|
+
createLangChainHandler: () => createLangChainHandler
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(langchain_exports);
|
|
36
|
+
|
|
37
|
+
// src/exceptions.ts
|
|
38
|
+
var MAX_PROMPT_DISPLAY_LEN = 100;
|
|
39
|
+
var FirewallBlockedException = class _FirewallBlockedException extends Error {
|
|
40
|
+
score;
|
|
41
|
+
threshold;
|
|
42
|
+
promptText;
|
|
43
|
+
runId;
|
|
44
|
+
hook;
|
|
45
|
+
toolName;
|
|
46
|
+
toolCallId;
|
|
47
|
+
result;
|
|
48
|
+
constructor(params) {
|
|
49
|
+
super(_FirewallBlockedException.formatMessage(params));
|
|
50
|
+
this.name = "FirewallBlockedException";
|
|
51
|
+
this.score = params.score;
|
|
52
|
+
this.threshold = params.threshold;
|
|
53
|
+
this.promptText = params.promptText;
|
|
54
|
+
this.runId = params.runId;
|
|
55
|
+
this.hook = params.hook;
|
|
56
|
+
this.toolName = params.toolName;
|
|
57
|
+
this.toolCallId = params.toolCallId;
|
|
58
|
+
this.result = params.result;
|
|
59
|
+
Object.setPrototypeOf(this, _FirewallBlockedException.prototype);
|
|
60
|
+
}
|
|
61
|
+
static formatMessage(params) {
|
|
62
|
+
const truncated = params.promptText.length > MAX_PROMPT_DISPLAY_LEN ? `${params.promptText.slice(0, MAX_PROMPT_DISPLAY_LEN)}...` : params.promptText;
|
|
63
|
+
return `Request blocked by Silmaril Firewall (score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): '${truncated}'`;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/hooks.ts
|
|
68
|
+
var HookLabel = {
|
|
69
|
+
USER_INPUT: "user_input",
|
|
70
|
+
SYSTEM_PROMPT: "system_prompt",
|
|
71
|
+
TOOL_CALL: "tool_call",
|
|
72
|
+
TOOL_RESPONSE: "tool_response",
|
|
73
|
+
LLM_OUTPUT: "llm_output",
|
|
74
|
+
UNKNOWN: "unknown"
|
|
75
|
+
};
|
|
76
|
+
var FirewallHook = {
|
|
77
|
+
LLM_START: "on_llm_start",
|
|
78
|
+
CHAT_MODEL_START: "on_chat_model_start",
|
|
79
|
+
TOOL_START: "on_tool_start",
|
|
80
|
+
RETRIEVER_START: "on_retriever_start",
|
|
81
|
+
LLM_END: "on_llm_end",
|
|
82
|
+
TOOL_END: "on_tool_end",
|
|
83
|
+
RETRIEVER_END: "on_retriever_end"
|
|
84
|
+
};
|
|
85
|
+
var DEFAULT_HOOKS = /* @__PURE__ */ new Set([
|
|
86
|
+
FirewallHook.LLM_START,
|
|
87
|
+
FirewallHook.CHAT_MODEL_START
|
|
88
|
+
]);
|
|
89
|
+
var INPUT_HOOKS = /* @__PURE__ */ new Set([
|
|
90
|
+
FirewallHook.LLM_START,
|
|
91
|
+
FirewallHook.CHAT_MODEL_START,
|
|
92
|
+
FirewallHook.TOOL_START,
|
|
93
|
+
FirewallHook.RETRIEVER_START
|
|
94
|
+
]);
|
|
95
|
+
var OUTPUT_HOOKS = /* @__PURE__ */ new Set([
|
|
96
|
+
FirewallHook.LLM_END,
|
|
97
|
+
FirewallHook.TOOL_END,
|
|
98
|
+
FirewallHook.RETRIEVER_END
|
|
99
|
+
]);
|
|
100
|
+
var ALL_HOOKS = /* @__PURE__ */ new Set([
|
|
101
|
+
...INPUT_HOOKS,
|
|
102
|
+
...OUTPUT_HOOKS
|
|
103
|
+
]);
|
|
104
|
+
var FIREWALL_HOOK_TO_LABEL = {
|
|
105
|
+
[FirewallHook.CHAT_MODEL_START]: HookLabel.USER_INPUT,
|
|
106
|
+
[FirewallHook.LLM_START]: HookLabel.USER_INPUT,
|
|
107
|
+
[FirewallHook.TOOL_START]: HookLabel.TOOL_CALL,
|
|
108
|
+
[FirewallHook.TOOL_END]: HookLabel.TOOL_RESPONSE,
|
|
109
|
+
[FirewallHook.RETRIEVER_START]: HookLabel.TOOL_CALL,
|
|
110
|
+
[FirewallHook.RETRIEVER_END]: HookLabel.TOOL_RESPONSE,
|
|
111
|
+
[FirewallHook.LLM_END]: HookLabel.LLM_OUTPUT
|
|
112
|
+
};
|
|
113
|
+
var FIREWALL_HOOK_VALUES = new Set(Object.values(FirewallHook));
|
|
114
|
+
function resolveHooks(hooks) {
|
|
115
|
+
if (hooks === void 0) {
|
|
116
|
+
return DEFAULT_HOOKS;
|
|
117
|
+
}
|
|
118
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
119
|
+
for (const h of hooks) {
|
|
120
|
+
if (!FIREWALL_HOOK_VALUES.has(h)) {
|
|
121
|
+
throw new Error(`Invalid FirewallHook value: ${String(h)}`);
|
|
122
|
+
}
|
|
123
|
+
resolved.add(h);
|
|
124
|
+
}
|
|
125
|
+
return resolved;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/utils/extract.ts
|
|
129
|
+
function extractTextFromPrompts(prompts) {
|
|
130
|
+
return prompts.map((p) => p.trim()).filter((p) => p.length > 0).join("\n");
|
|
131
|
+
}
|
|
132
|
+
function extractTextFromToolInput(inputStr) {
|
|
133
|
+
return inputStr.trim();
|
|
134
|
+
}
|
|
135
|
+
function extractTextFromLLMResult(response) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
for (const genList of response.generations ?? []) {
|
|
138
|
+
for (const gen of genList) {
|
|
139
|
+
const text = (gen.text ?? "").trim();
|
|
140
|
+
if (text) {
|
|
141
|
+
parts.push(text);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return parts.join("\n");
|
|
146
|
+
}
|
|
147
|
+
function extractTextFromDocuments(documents) {
|
|
148
|
+
const parts = [];
|
|
149
|
+
for (const doc of documents) {
|
|
150
|
+
const text = (doc.pageContent ?? "").trim();
|
|
151
|
+
if (text) {
|
|
152
|
+
parts.push(text);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return parts.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/adapters/langchain.ts
|
|
159
|
+
var USER_ROLES = /* @__PURE__ */ new Set(["human", "user"]);
|
|
160
|
+
function getMessageRole(message) {
|
|
161
|
+
if (typeof message.role === "string") {
|
|
162
|
+
return message.role.toLowerCase();
|
|
163
|
+
}
|
|
164
|
+
if (typeof message.type === "string") {
|
|
165
|
+
return message.type.toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
function extractMessageText(content) {
|
|
170
|
+
if (content === void 0) {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
if (typeof content === "string") {
|
|
174
|
+
return content;
|
|
175
|
+
}
|
|
176
|
+
const parts = [];
|
|
177
|
+
for (const block of content) {
|
|
178
|
+
if (typeof block === "string") {
|
|
179
|
+
parts.push(block);
|
|
180
|
+
} else if (block && block.type === "text") {
|
|
181
|
+
parts.push(block.text ?? "");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return parts.join(" ");
|
|
185
|
+
}
|
|
186
|
+
function findLastUserMessage(messages) {
|
|
187
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
188
|
+
const msg = messages[i];
|
|
189
|
+
if (USER_ROLES.has(getMessageRole(msg))) {
|
|
190
|
+
return msg;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return void 0;
|
|
194
|
+
}
|
|
195
|
+
async function createLangChainHandler(firewall, options = {}) {
|
|
196
|
+
const { BaseCallbackHandler } = await import("@langchain/core/callbacks/base");
|
|
197
|
+
const enabledHooks = resolveHooks(options.hooks);
|
|
198
|
+
const failOpen = options.failOpen ?? true;
|
|
199
|
+
const logger = options.logger ?? ((message, error) => {
|
|
200
|
+
console.warn(`silmaril.firewall: ${message}`, error);
|
|
201
|
+
});
|
|
202
|
+
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
203
|
+
const onClassify = options.onClassify;
|
|
204
|
+
const fireOnClassify = (event) => {
|
|
205
|
+
if (!onClassify) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
onClassify(event);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logger("onClassify callback threw", err);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const classify = async (text, hookLabel, runId, toolName) => {
|
|
215
|
+
let result;
|
|
216
|
+
try {
|
|
217
|
+
result = await firewall.classify(text, {
|
|
218
|
+
hook: hookLabel,
|
|
219
|
+
...toolName !== void 0 ? { toolName } : {}
|
|
220
|
+
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (!failOpen) {
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
logger("classification failed, allowing prompt through", err);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const threshold = result.threshold;
|
|
229
|
+
const blocked = result.score >= threshold;
|
|
230
|
+
const commonEventFields = {
|
|
231
|
+
hook: hookLabel,
|
|
232
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
233
|
+
runId,
|
|
234
|
+
text,
|
|
235
|
+
result
|
|
236
|
+
};
|
|
237
|
+
fireOnClassify({
|
|
238
|
+
...commonEventFields,
|
|
239
|
+
blocked,
|
|
240
|
+
shadowMode
|
|
241
|
+
});
|
|
242
|
+
if (!blocked || shadowMode) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
throw new FirewallBlockedException({
|
|
246
|
+
score: result.score,
|
|
247
|
+
threshold,
|
|
248
|
+
promptText: text,
|
|
249
|
+
runId,
|
|
250
|
+
hook: hookLabel,
|
|
251
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
252
|
+
result
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
class SilmarilFirewallHandler extends BaseCallbackHandler {
|
|
256
|
+
name = "silmaril_firewall_handler";
|
|
257
|
+
raiseError = true;
|
|
258
|
+
awaitHandlers = true;
|
|
259
|
+
async handleChatModelStart(_llm, messages, runId) {
|
|
260
|
+
if (!enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const batches = messages;
|
|
264
|
+
const flat = [];
|
|
265
|
+
for (const batch of batches) {
|
|
266
|
+
for (const m of batch) {
|
|
267
|
+
flat.push(m);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const lastUser = findLastUserMessage(flat);
|
|
271
|
+
if (!lastUser) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const text = extractMessageText(lastUser.content).trim();
|
|
275
|
+
if (!text) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
|
|
279
|
+
}
|
|
280
|
+
async handleLLMStart(_llm, prompts, runId) {
|
|
281
|
+
if (!enabledHooks.has(FirewallHook.LLM_START)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const text = extractTextFromPrompts(prompts);
|
|
285
|
+
if (!text) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
|
|
289
|
+
}
|
|
290
|
+
async handleToolStart(tool, inputStr, runId) {
|
|
291
|
+
if (!enabledHooks.has(FirewallHook.TOOL_START)) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const text = extractTextFromToolInput(inputStr);
|
|
295
|
+
if (!text) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const toolName = tool?.name;
|
|
299
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
|
|
300
|
+
}
|
|
301
|
+
async handleRetrieverStart(_retriever, query, runId) {
|
|
302
|
+
if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const text = query.trim();
|
|
306
|
+
if (!text) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
|
|
310
|
+
}
|
|
311
|
+
async handleLLMEnd(output, runId) {
|
|
312
|
+
if (!enabledHooks.has(FirewallHook.LLM_END)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const text = extractTextFromLLMResult(output);
|
|
316
|
+
if (!text) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
|
|
320
|
+
}
|
|
321
|
+
async handleToolEnd(output, runId, _parentRunId, _tags, _kwargs) {
|
|
322
|
+
if (!enabledHooks.has(FirewallHook.TOOL_END)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const text = String(output).trim();
|
|
326
|
+
if (!text) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const toolName = _kwargs?.name;
|
|
330
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
|
|
331
|
+
}
|
|
332
|
+
async handleRetrieverEnd(documents, runId) {
|
|
333
|
+
if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const text = extractTextFromDocuments(documents);
|
|
337
|
+
if (!text) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return new SilmarilFirewallHandler();
|
|
344
|
+
}
|
|
345
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
346
|
+
0 && (module.exports = {
|
|
347
|
+
createLangChainHandler
|
|
348
|
+
});
|
|
349
|
+
//# sourceMappingURL=langchain.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/langchain.ts","../../src/exceptions.ts","../../src/hooks.ts","../../src/utils/extract.ts"],"sourcesContent":["// Copyright (c) 2024-2025 Silmaril Security Inc. All rights reserved.\n// PROPRIETARY AND CONFIDENTIAL\n\nimport type { BaseCallbackHandler } from \"@langchain/core/callbacks/base\";\n\nimport type { Firewall } from \"../firewall.js\";\nimport { FirewallBlockedException } from \"../exceptions.js\";\nimport {\n FIREWALL_HOOK_TO_LABEL,\n FirewallHook,\n HookLabel,\n resolveHooks,\n} from \"../hooks.js\";\nimport type { BlockResult, ClassifyEvent, LangChainAdapterOptions } from \"../types.js\";\nimport {\n extractTextFromDocuments,\n extractTextFromLLMResult,\n extractTextFromPrompts,\n extractTextFromToolInput,\n} from \"../utils/extract.js\";\n\nconst USER_ROLES: ReadonlySet<string> = new Set([\"human\", \"user\"]);\n\ninterface LangChainDucktypedMessage {\n role?: string;\n type?: string;\n content?: string | ReadonlyArray<string | { type?: string; text?: string }>;\n}\n\nfunction getMessageRole(message: LangChainDucktypedMessage): string {\n if (typeof message.role === \"string\") {\n return message.role.toLowerCase();\n }\n if (typeof message.type === \"string\") {\n return message.type.toLowerCase();\n }\n return \"\";\n}\n\nfunction extractMessageText(content: LangChainDucktypedMessage[\"content\"]): string {\n if (content === undefined) {\n return \"\";\n }\n if (typeof content === \"string\") {\n return content;\n }\n const parts: string[] = [];\n for (const block of content) {\n if (typeof block === \"string\") {\n parts.push(block);\n } else if (block && block.type === \"text\") {\n parts.push(block.text ?? \"\");\n }\n }\n return parts.join(\" \");\n}\n\nfunction findLastUserMessage(\n messages: ReadonlyArray<LangChainDucktypedMessage>,\n): LangChainDucktypedMessage | undefined {\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i]!;\n if (USER_ROLES.has(getMessageRole(msg))) {\n return msg;\n }\n }\n return undefined;\n}\n\nexport async function createLangChainHandler(\n firewall: Firewall,\n options: LangChainAdapterOptions = {},\n): Promise<BaseCallbackHandler> {\n const { BaseCallbackHandler } = await import(\"@langchain/core/callbacks/base\");\n\n const enabledHooks = resolveHooks(options.hooks);\n // includeSystem / includeTool are retained in LangChainAdapterOptions for\n // backwards compat but are inert under the per-message hook-routing algorithm:\n // handleChatModelStart only scans the last user-role message; system and tool\n // messages are either trusted or already classified by their own hooks.\n const failOpen = options.failOpen ?? true;\n const logger =\n options.logger ??\n ((message: string, error: unknown): void => {\n // eslint-disable-next-line no-console\n console.warn(`silmaril.firewall: ${message}`, error);\n });\n const shadowMode = options.shadowMode ?? firewall.shadowMode;\n const onClassify = options.onClassify;\n\n const fireOnClassify = (event: ClassifyEvent): void => {\n if (!onClassify) {\n return;\n }\n try {\n onClassify(event);\n } catch (err) {\n logger(\"onClassify callback threw\", err);\n }\n };\n\n const classify = async (\n text: string,\n hookLabel: HookLabel,\n runId: string,\n toolName?: string,\n ): Promise<void> => {\n let result: BlockResult;\n try {\n result = await firewall.classify(text, {\n hook: hookLabel,\n ...(toolName !== undefined ? { toolName } : {}),\n });\n } catch (err) {\n if (!failOpen) {\n throw err;\n }\n logger(\"classification failed, allowing prompt through\", err);\n return;\n }\n const threshold = result.threshold;\n const blocked = result.score >= threshold;\n const commonEventFields = {\n hook: hookLabel,\n ...(toolName !== undefined ? { toolName } : {}),\n runId,\n text,\n result,\n };\n fireOnClassify({\n ...commonEventFields,\n blocked,\n shadowMode,\n });\n if (!blocked || shadowMode) {\n return;\n }\n\n throw new FirewallBlockedException({\n score: result.score,\n threshold,\n promptText: text,\n runId,\n hook: hookLabel,\n ...(toolName !== undefined ? { toolName } : {}),\n result,\n });\n };\n\n class SilmarilFirewallHandler extends BaseCallbackHandler {\n override name = \"silmaril_firewall_handler\";\n override raiseError = true;\n override awaitHandlers = true;\n\n override async handleChatModelStart(\n _llm: unknown,\n messages: unknown,\n runId: string,\n ): Promise<void> {\n if (!enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {\n return;\n }\n const batches = messages as ReadonlyArray<ReadonlyArray<LangChainDucktypedMessage>>;\n const flat: LangChainDucktypedMessage[] = [];\n for (const batch of batches) {\n for (const m of batch) {\n flat.push(m);\n }\n }\n const lastUser = findLastUserMessage(flat);\n if (!lastUser) {\n return;\n }\n const text = extractMessageText(lastUser.content).trim();\n if (!text) {\n return;\n }\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);\n }\n\n override async handleLLMStart(\n _llm: unknown,\n prompts: string[],\n runId: string,\n ): Promise<void> {\n if (!enabledHooks.has(FirewallHook.LLM_START)) {\n return;\n }\n const text = extractTextFromPrompts(prompts);\n if (!text) {\n return;\n }\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);\n }\n\n override async handleToolStart(\n tool: { name?: string } | undefined,\n inputStr: string,\n runId: string,\n ): Promise<void> {\n if (!enabledHooks.has(FirewallHook.TOOL_START)) {\n return;\n }\n const text = extractTextFromToolInput(inputStr);\n if (!text) {\n return;\n }\n const toolName = tool?.name;\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);\n }\n\n override async handleRetrieverStart(\n _retriever: unknown,\n query: string,\n runId: string,\n ): Promise<void> {\n if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {\n return;\n }\n const text = query.trim();\n if (!text) {\n return;\n }\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);\n }\n\n override async handleLLMEnd(output: unknown, runId: string): Promise<void> {\n if (!enabledHooks.has(FirewallHook.LLM_END)) {\n return;\n }\n const text = extractTextFromLLMResult(output as { generations?: ReadonlyArray<ReadonlyArray<{ text?: string }>> });\n if (!text) {\n return;\n }\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);\n }\n\n override async handleToolEnd(\n output: unknown,\n runId: string,\n _parentRunId?: string,\n _tags?: string[],\n _kwargs?: { name?: string },\n ): Promise<void> {\n if (!enabledHooks.has(FirewallHook.TOOL_END)) {\n return;\n }\n const text = String(output).trim();\n if (!text) {\n return;\n }\n const toolName = _kwargs?.name;\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);\n }\n\n override async handleRetrieverEnd(\n documents: ReadonlyArray<{ pageContent?: string }>,\n runId: string,\n ): Promise<void> {\n if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {\n return;\n }\n const text = extractTextFromDocuments(documents);\n if (!text) {\n return;\n }\n await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);\n }\n }\n\n return new SilmarilFirewallHandler();\n}\n","// Copyright (c) 2024-2025 Silmaril Security Inc. All rights reserved.\n// PROPRIETARY AND CONFIDENTIAL\n\nimport type { HookLabel } from \"./hooks.js\";\nimport type { BlockResult } from \"./types.js\";\n\nconst MAX_PROMPT_DISPLAY_LEN = 100;\n\nexport interface MalformedInputDetails {\n field?: string;\n inputIndex?: number;\n charOffset?: number;\n malformedToken?: string;\n codePoint?: string;\n reason?: string;\n}\n\nexport class FirewallBlockedException extends Error {\n readonly score: number;\n readonly threshold: number;\n readonly promptText: string;\n readonly runId: string | undefined;\n readonly hook: HookLabel | undefined;\n readonly toolName: string | undefined;\n readonly toolCallId: string | undefined;\n readonly result: BlockResult | undefined;\n\n constructor(params: {\n score: number;\n threshold: number;\n promptText: string;\n runId?: string;\n hook?: HookLabel;\n toolName?: string;\n toolCallId?: string;\n result?: BlockResult;\n }) {\n super(FirewallBlockedException.formatMessage(params));\n this.name = \"FirewallBlockedException\";\n this.score = params.score;\n this.threshold = params.threshold;\n this.promptText = params.promptText;\n this.runId = params.runId;\n this.hook = params.hook;\n this.toolName = params.toolName;\n this.toolCallId = params.toolCallId;\n this.result = params.result;\n Object.setPrototypeOf(this, FirewallBlockedException.prototype);\n }\n\n private static formatMessage(params: {\n score: number;\n threshold: number;\n promptText: string;\n }): string {\n const truncated =\n params.promptText.length > MAX_PROMPT_DISPLAY_LEN\n ? `${params.promptText.slice(0, MAX_PROMPT_DISPLAY_LEN)}...`\n : params.promptText;\n return (\n `Request blocked by Silmaril Firewall ` +\n `(score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): ` +\n `'${truncated}'`\n );\n }\n}\n\n/** @deprecated Use FirewallBlockedException instead. */\nexport const PromptBlockedException = FirewallBlockedException;\n/** @deprecated Use FirewallBlockedException instead. */\nexport type PromptBlockedException = FirewallBlockedException;\n\nexport class SilmarilApiError extends Error {\n readonly status: number;\n readonly statusText: string;\n readonly body: string;\n readonly details: MalformedInputDetails | undefined;\n readonly error: string | undefined;\n readonly apiMessage: string | undefined;\n\n constructor(params: {\n status: number;\n statusText: string;\n body: string;\n details?: MalformedInputDetails;\n error?: string;\n apiMessage?: string;\n }) {\n const statusText = params.statusText ? ` ${params.statusText}` : \"\";\n super(`Silmaril API error ${params.status}${statusText}`);\n this.name = \"SilmarilApiError\";\n this.status = params.status;\n this.statusText = params.statusText;\n this.body = params.body;\n const parsed = parseErrorBody(params.body);\n this.details = params.details ?? parsed.details;\n this.error = params.error ?? parsed.error;\n this.apiMessage = params.apiMessage ?? parsed.apiMessage;\n Object.setPrototypeOf(this, SilmarilApiError.prototype);\n }\n}\n\nfunction parseErrorBody(body: string): {\n details: MalformedInputDetails | undefined;\n error: string | undefined;\n apiMessage: string | undefined;\n} {\n try {\n const parsed = JSON.parse(body) as unknown;\n if (!parsed || typeof parsed !== \"object\") {\n return { details: undefined, error: undefined, apiMessage: undefined };\n }\n const data = parsed as Record<string, unknown>;\n return {\n details:\n data.details && typeof data.details === \"object\"\n ? (data.details as MalformedInputDetails)\n : undefined,\n error: typeof data.error === \"string\" ? data.error : undefined,\n apiMessage: typeof data.message === \"string\" ? data.message : undefined,\n };\n } catch {\n return { details: undefined, error: undefined, apiMessage: undefined };\n }\n}\n","// Copyright (c) 2024-2025 Silmaril Security Inc. All rights reserved.\n// PROPRIETARY AND CONFIDENTIAL\n\nexport const HookLabel = {\n USER_INPUT: \"user_input\",\n SYSTEM_PROMPT: \"system_prompt\",\n TOOL_CALL: \"tool_call\",\n TOOL_RESPONSE: \"tool_response\",\n LLM_OUTPUT: \"llm_output\",\n UNKNOWN: \"unknown\",\n} as const;\n\nexport type HookLabel = (typeof HookLabel)[keyof typeof HookLabel];\n\nexport const FirewallHook = {\n LLM_START: \"on_llm_start\",\n CHAT_MODEL_START: \"on_chat_model_start\",\n TOOL_START: \"on_tool_start\",\n RETRIEVER_START: \"on_retriever_start\",\n LLM_END: \"on_llm_end\",\n TOOL_END: \"on_tool_end\",\n RETRIEVER_END: \"on_retriever_end\",\n} as const;\n\nexport type FirewallHook = (typeof FirewallHook)[keyof typeof FirewallHook];\n\nexport const DEFAULT_HOOKS: ReadonlySet<FirewallHook> = new Set<FirewallHook>([\n FirewallHook.LLM_START,\n FirewallHook.CHAT_MODEL_START,\n]);\n\nexport const INPUT_HOOKS: ReadonlySet<FirewallHook> = new Set<FirewallHook>([\n FirewallHook.LLM_START,\n FirewallHook.CHAT_MODEL_START,\n FirewallHook.TOOL_START,\n FirewallHook.RETRIEVER_START,\n]);\n\nexport const OUTPUT_HOOKS: ReadonlySet<FirewallHook> = new Set<FirewallHook>([\n FirewallHook.LLM_END,\n FirewallHook.TOOL_END,\n FirewallHook.RETRIEVER_END,\n]);\n\nexport const ALL_HOOKS: ReadonlySet<FirewallHook> = new Set<FirewallHook>([\n ...INPUT_HOOKS,\n ...OUTPUT_HOOKS,\n]);\n\nexport const FIREWALL_HOOK_TO_LABEL: Readonly<Record<FirewallHook, HookLabel>> = {\n [FirewallHook.CHAT_MODEL_START]: HookLabel.USER_INPUT,\n [FirewallHook.LLM_START]: HookLabel.USER_INPUT,\n [FirewallHook.TOOL_START]: HookLabel.TOOL_CALL,\n [FirewallHook.TOOL_END]: HookLabel.TOOL_RESPONSE,\n [FirewallHook.RETRIEVER_START]: HookLabel.TOOL_CALL,\n [FirewallHook.RETRIEVER_END]: HookLabel.TOOL_RESPONSE,\n [FirewallHook.LLM_END]: HookLabel.LLM_OUTPUT,\n};\n\nconst FIREWALL_HOOK_VALUES: ReadonlySet<string> = new Set(Object.values(FirewallHook));\n\nexport function resolveHooks(\n hooks: Iterable<FirewallHook | string> | undefined,\n): ReadonlySet<FirewallHook> {\n if (hooks === undefined) {\n return DEFAULT_HOOKS;\n }\n const resolved = new Set<FirewallHook>();\n for (const h of hooks) {\n if (!FIREWALL_HOOK_VALUES.has(h)) {\n throw new Error(`Invalid FirewallHook value: ${String(h)}`);\n }\n resolved.add(h as FirewallHook);\n }\n return resolved;\n}\n\nexport function prependHook(text: string, hook: HookLabel | string | undefined | null): string {\n if (hook === undefined || hook === null) {\n return text;\n }\n if (hook === HookLabel.UNKNOWN) {\n return text;\n }\n return `[HOOK:${hook}] ${text}`;\n}\n\nexport function prependToolName(text: string, toolName: string | undefined | null): string {\n if (toolName === undefined || toolName === null) {\n return text;\n }\n return `[TOOL:${toolName}] ${text}`;\n}\n","// Copyright (c) 2024-2025 Silmaril Security Inc. All rights reserved.\n// PROPRIETARY AND CONFIDENTIAL\n\nconst SKIP_ROLES: ReadonlySet<string> = new Set([\"ai\", \"assistant\"]);\nconst SYSTEM_ROLES: ReadonlySet<string> = new Set([\"system\"]);\nconst TOOL_ROLES: ReadonlySet<string> = new Set([\"tool\", \"function\"]);\n\ntype MessageContent = string | ReadonlyArray<string | { type?: string; text?: string }>;\n\ninterface DucktypedMessage {\n role?: string;\n type?: string;\n content?: MessageContent;\n}\n\ninterface DucktypedGeneration {\n text?: string;\n}\n\ninterface DucktypedLLMResult {\n generations?: ReadonlyArray<ReadonlyArray<DucktypedGeneration>>;\n}\n\ninterface DucktypedDocument {\n pageContent?: string;\n}\n\nfunction extractContentText(content: MessageContent | undefined): string {\n if (typeof content === \"string\") {\n return content;\n }\n if (Array.isArray(content)) {\n const parts: string[] = [];\n for (const block of content) {\n if (typeof block === \"string\") {\n parts.push(block);\n } else if (block && typeof block === \"object\" && block.type === \"text\") {\n parts.push(block.text ?? \"\");\n }\n }\n return parts.join(\" \");\n }\n return \"\";\n}\n\nfunction getRole(message: DucktypedMessage): string {\n if (message.role !== undefined) {\n return message.role.toLowerCase();\n }\n if (typeof message.type === \"string\") {\n return message.type.toLowerCase();\n }\n return \"\";\n}\n\nexport function extractTextFromMessages(\n messages: ReadonlyArray<DucktypedMessage>,\n options: { includeSystem?: boolean; includeTool?: boolean } = {},\n): string {\n const includeSystem = options.includeSystem ?? true;\n const includeTool = options.includeTool ?? true;\n const parts: string[] = [];\n for (const message of messages) {\n const role = getRole(message);\n if (SKIP_ROLES.has(role)) {\n continue;\n }\n if (!includeSystem && SYSTEM_ROLES.has(role)) {\n continue;\n }\n if (!includeTool && TOOL_ROLES.has(role)) {\n continue;\n }\n const text = extractContentText(message.content);\n const stripped = text.trim();\n if (stripped) {\n parts.push(stripped);\n }\n }\n return parts.join(\"\\n\");\n}\n\nexport function extractTextFromPrompts(prompts: readonly string[]): string {\n return prompts\n .map((p) => p.trim())\n .filter((p) => p.length > 0)\n .join(\"\\n\");\n}\n\nexport function extractTextFromToolInput(inputStr: string): string {\n return inputStr.trim();\n}\n\nexport function extractTextFromLLMResult(response: DucktypedLLMResult): string {\n const parts: string[] = [];\n for (const genList of response.generations ?? []) {\n for (const gen of genList) {\n const text = (gen.text ?? \"\").trim();\n if (text) {\n parts.push(text);\n }\n }\n }\n return parts.join(\"\\n\");\n}\n\nexport function extractTextFromDocuments(documents: ReadonlyArray<DucktypedDocument>): string {\n const parts: string[] = [];\n for (const doc of documents) {\n const text = (doc.pageContent ?? \"\").trim();\n if (text) {\n parts.push(text);\n }\n }\n return parts.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
2
|
+
import { f as Firewall, L as LangChainAdapterOptions } from '../firewall-CEV5hItA.cjs';
|
|
3
|
+
|
|
4
|
+
declare function createLangChainHandler(firewall: Firewall, options?: LangChainAdapterOptions): Promise<BaseCallbackHandler>;
|
|
5
|
+
|
|
6
|
+
export { createLangChainHandler };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
|
|
2
|
+
import { f as Firewall, L as LangChainAdapterOptions } from '../firewall-CEV5hItA.js';
|
|
3
|
+
|
|
4
|
+
declare function createLangChainHandler(firewall: Firewall, options?: LangChainAdapterOptions): Promise<BaseCallbackHandler>;
|
|
5
|
+
|
|
6
|
+
export { createLangChainHandler };
|