@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/dist/index.d.ts
CHANGED
|
@@ -1,147 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
declare const HookLabel: {
|
|
4
|
-
readonly USER_INPUT: "user_input";
|
|
5
|
-
readonly SYSTEM_PROMPT: "system_prompt";
|
|
6
|
-
readonly TOOL_CALL: "tool_call";
|
|
7
|
-
readonly TOOL_RESPONSE: "tool_response";
|
|
8
|
-
readonly LLM_OUTPUT: "llm_output";
|
|
9
|
-
readonly UNKNOWN: "unknown";
|
|
10
|
-
};
|
|
11
|
-
type HookLabel = (typeof HookLabel)[keyof typeof HookLabel];
|
|
12
|
-
declare const FirewallHook: {
|
|
13
|
-
readonly LLM_START: "on_llm_start";
|
|
14
|
-
readonly CHAT_MODEL_START: "on_chat_model_start";
|
|
15
|
-
readonly TOOL_START: "on_tool_start";
|
|
16
|
-
readonly RETRIEVER_START: "on_retriever_start";
|
|
17
|
-
readonly LLM_END: "on_llm_end";
|
|
18
|
-
readonly TOOL_END: "on_tool_end";
|
|
19
|
-
readonly RETRIEVER_END: "on_retriever_end";
|
|
20
|
-
};
|
|
21
|
-
type FirewallHook = (typeof FirewallHook)[keyof typeof FirewallHook];
|
|
22
|
-
declare const DEFAULT_HOOKS: ReadonlySet<FirewallHook>;
|
|
23
|
-
declare const INPUT_HOOKS: ReadonlySet<FirewallHook>;
|
|
24
|
-
declare const OUTPUT_HOOKS: ReadonlySet<FirewallHook>;
|
|
25
|
-
declare const ALL_HOOKS: ReadonlySet<FirewallHook>;
|
|
26
|
-
declare const FIREWALL_HOOK_TO_LABEL: Readonly<Record<FirewallHook, HookLabel>>;
|
|
27
|
-
declare function resolveHooks(hooks: Iterable<FirewallHook | string> | undefined): ReadonlySet<FirewallHook>;
|
|
28
|
-
declare function prependHook(text: string, hook: HookLabel | string | undefined | null): string;
|
|
29
|
-
declare function prependToolName(text: string, toolName: string | undefined | null): string;
|
|
30
|
-
|
|
31
|
-
type Prediction = "BENIGN" | "MALICIOUS";
|
|
32
|
-
interface BlockResult {
|
|
33
|
-
readonly prediction: Prediction;
|
|
34
|
-
readonly score: number;
|
|
35
|
-
readonly threshold: number;
|
|
36
|
-
readonly primaryOutcome?: string;
|
|
37
|
-
readonly outcomeScores?: Readonly<Record<string, number>>;
|
|
38
|
-
readonly detectorScores?: Readonly<Record<string, number>>;
|
|
39
|
-
readonly detectorCounts?: Readonly<Record<string, number>>;
|
|
40
|
-
}
|
|
41
|
-
interface FirewallOptions {
|
|
42
|
-
apiKey: string;
|
|
43
|
-
apiUrl: string;
|
|
44
|
-
timeoutMs?: number;
|
|
45
|
-
chunkConcurrency?: number;
|
|
46
|
-
shadowMode?: boolean;
|
|
47
|
-
}
|
|
48
|
-
type ClassificationMetadata = Readonly<Record<string, unknown>>;
|
|
49
|
-
interface ClassifyOptions {
|
|
50
|
-
hook?: HookLabel;
|
|
51
|
-
toolName?: string;
|
|
52
|
-
metadata?: ClassificationMetadata;
|
|
53
|
-
}
|
|
54
|
-
interface ClassifyBatchOptions {
|
|
55
|
-
hooks?: readonly HookLabel[];
|
|
56
|
-
toolNames?: readonly (string | undefined)[];
|
|
57
|
-
metadata?: readonly (ClassificationMetadata | undefined)[];
|
|
58
|
-
}
|
|
59
|
-
interface LangChainAdapterOptions {
|
|
60
|
-
hooks?: ReadonlySet<FirewallHook>;
|
|
61
|
-
includeSystem?: boolean;
|
|
62
|
-
includeTool?: boolean;
|
|
63
|
-
failOpen?: boolean;
|
|
64
|
-
logger?: (message: string, error: unknown) => void;
|
|
65
|
-
shadowMode?: boolean;
|
|
66
|
-
onClassify?: (event: ClassifyEvent) => void;
|
|
67
|
-
}
|
|
68
|
-
interface ClassifyEvent {
|
|
69
|
-
readonly hook: HookLabel;
|
|
70
|
-
readonly toolName?: string;
|
|
71
|
-
readonly text: string;
|
|
72
|
-
readonly result: BlockResult;
|
|
73
|
-
readonly blocked: boolean;
|
|
74
|
-
readonly shadowMode: boolean;
|
|
75
|
-
}
|
|
76
|
-
interface MiddlewareOptions {
|
|
77
|
-
scanInput?: boolean;
|
|
78
|
-
scanOutput?: boolean;
|
|
79
|
-
scanToolCalls?: boolean;
|
|
80
|
-
shadowMode?: boolean;
|
|
81
|
-
onBlocked?: (err: Error) => void;
|
|
82
|
-
onClassify?: (event: ClassifyEvent) => void;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
interface VercelContentPart {
|
|
86
|
-
type?: string;
|
|
87
|
-
text?: string;
|
|
88
|
-
toolCallId?: string;
|
|
89
|
-
toolName?: string;
|
|
90
|
-
args?: unknown;
|
|
91
|
-
result?: unknown;
|
|
92
|
-
}
|
|
93
|
-
interface VercelPromptMessage {
|
|
94
|
-
role: string;
|
|
95
|
-
content: string | ReadonlyArray<VercelContentPart | string>;
|
|
96
|
-
}
|
|
97
|
-
interface VercelToolCall {
|
|
98
|
-
toolCallType?: string;
|
|
99
|
-
toolCallId?: string;
|
|
100
|
-
toolName?: string;
|
|
101
|
-
args?: unknown;
|
|
102
|
-
}
|
|
103
|
-
interface VercelGenerateResult {
|
|
104
|
-
text?: string;
|
|
105
|
-
toolCalls?: ReadonlyArray<VercelToolCall>;
|
|
106
|
-
}
|
|
107
|
-
interface WrapGenerateArgs<TResult extends VercelGenerateResult & Record<string, unknown>> {
|
|
108
|
-
params: {
|
|
109
|
-
prompt: ReadonlyArray<VercelPromptMessage>;
|
|
110
|
-
} & Record<string, unknown>;
|
|
111
|
-
doGenerate: () => PromiseLike<TResult>;
|
|
112
|
-
}
|
|
113
|
-
interface WrapStreamArgs<TResult extends {
|
|
114
|
-
stream: ReadableStream<unknown>;
|
|
115
|
-
} & Record<string, unknown>> {
|
|
116
|
-
params: {
|
|
117
|
-
prompt: ReadonlyArray<VercelPromptMessage>;
|
|
118
|
-
} & Record<string, unknown>;
|
|
119
|
-
doStream: () => PromiseLike<TResult>;
|
|
120
|
-
}
|
|
121
|
-
interface FirewallMiddleware {
|
|
122
|
-
wrapGenerate: <TResult extends VercelGenerateResult & Record<string, unknown>>(args: WrapGenerateArgs<TResult>) => Promise<TResult>;
|
|
123
|
-
wrapStream: <TResult extends {
|
|
124
|
-
stream: ReadableStream<unknown>;
|
|
125
|
-
} & Record<string, unknown>>(args: WrapStreamArgs<TResult>) => Promise<TResult>;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
129
|
-
declare const DEFAULT_CHUNK_CONCURRENCY = 8;
|
|
130
|
-
declare class Firewall {
|
|
131
|
-
readonly apiKey: string;
|
|
132
|
-
readonly apiUrl: string;
|
|
133
|
-
readonly timeoutMs: number;
|
|
134
|
-
readonly chunkConcurrency: number;
|
|
135
|
-
readonly shadowMode: boolean;
|
|
136
|
-
private readonly headers;
|
|
137
|
-
constructor(options: FirewallOptions);
|
|
138
|
-
classify(text: string, options?: ClassifyOptions): Promise<BlockResult>;
|
|
139
|
-
classifyBatch(texts: readonly string[], options?: ClassifyBatchOptions): Promise<BlockResult[]>;
|
|
140
|
-
asLangChainHandler(options?: LangChainAdapterOptions): Promise<BaseCallbackHandler>;
|
|
141
|
-
asMiddleware(options?: MiddlewareOptions): FirewallMiddleware;
|
|
142
|
-
private postWithRetry;
|
|
143
|
-
private classifySingleChunk;
|
|
144
|
-
}
|
|
1
|
+
import { H as HookLabel, B as BlockResult } from './firewall-CEV5hItA.js';
|
|
2
|
+
export { A as ALL_HOOKS, C as ClassificationMetadata, a as ClassifyBatchOptions, b as ClassifyEvent, c as ClassifyOptions, D as DEFAULT_CHUNK_CONCURRENCY, d as DEFAULT_HOOKS, e as DEFAULT_TIMEOUT_MS, F as FIREWALL_HOOK_TO_LABEL, f as Firewall, g as FirewallHook, h as FirewallMiddleware, i as FirewallOptions, I as INPUT_HOOKS, L as LangChainAdapterOptions, j as LangChainFirewallHandler, M as MiddlewareOptions, O as OUTPUT_HOOKS, P as Prediction, p as prependHook, k as prependToolName, r as resolveHooks } from './firewall-CEV5hItA.js';
|
|
145
3
|
|
|
146
4
|
interface MalformedInputDetails {
|
|
147
5
|
field?: string;
|
|
@@ -151,19 +9,31 @@ interface MalformedInputDetails {
|
|
|
151
9
|
codePoint?: string;
|
|
152
10
|
reason?: string;
|
|
153
11
|
}
|
|
154
|
-
declare class
|
|
12
|
+
declare class FirewallBlockedException extends Error {
|
|
155
13
|
readonly score: number;
|
|
156
14
|
readonly threshold: number;
|
|
157
15
|
readonly promptText: string;
|
|
158
16
|
readonly runId: string | undefined;
|
|
17
|
+
readonly hook: HookLabel | undefined;
|
|
18
|
+
readonly toolName: string | undefined;
|
|
19
|
+
readonly toolCallId: string | undefined;
|
|
20
|
+
readonly result: BlockResult | undefined;
|
|
159
21
|
constructor(params: {
|
|
160
22
|
score: number;
|
|
161
23
|
threshold: number;
|
|
162
24
|
promptText: string;
|
|
163
25
|
runId?: string;
|
|
26
|
+
hook?: HookLabel;
|
|
27
|
+
toolName?: string;
|
|
28
|
+
toolCallId?: string;
|
|
29
|
+
result?: BlockResult;
|
|
164
30
|
});
|
|
165
31
|
private static formatMessage;
|
|
166
32
|
}
|
|
33
|
+
/** @deprecated Use FirewallBlockedException instead. */
|
|
34
|
+
declare const PromptBlockedException: typeof FirewallBlockedException;
|
|
35
|
+
/** @deprecated Use FirewallBlockedException instead. */
|
|
36
|
+
type PromptBlockedException = FirewallBlockedException;
|
|
167
37
|
declare class SilmarilApiError extends Error {
|
|
168
38
|
readonly status: number;
|
|
169
39
|
readonly statusText: string;
|
|
@@ -181,7 +51,7 @@ declare class SilmarilApiError extends Error {
|
|
|
181
51
|
});
|
|
182
52
|
}
|
|
183
53
|
|
|
184
|
-
declare const MAX_INPUT_TOKENS =
|
|
54
|
+
declare const MAX_INPUT_TOKENS = 81920;
|
|
185
55
|
declare const CHUNK_WINDOW = 400;
|
|
186
56
|
declare const CHUNK_OVERLAP = 64;
|
|
187
57
|
declare const MAX_INPUT_CHARS: number;
|
|
@@ -190,4 +60,4 @@ declare const CHUNK_OVERLAP_CHARS: number;
|
|
|
190
60
|
declare function sanitizeText(text: string): string;
|
|
191
61
|
declare function chunkText(text: string): string[];
|
|
192
62
|
|
|
193
|
-
export {
|
|
63
|
+
export { BlockResult, CHUNK_OVERLAP, CHUNK_OVERLAP_CHARS, CHUNK_WINDOW, CHUNK_WINDOW_CHARS, FirewallBlockedException, HookLabel, MAX_INPUT_CHARS, MAX_INPUT_TOKENS, type MalformedInputDetails, PromptBlockedException, SilmarilApiError, chunkText, sanitizeText };
|
package/dist/index.js
CHANGED
|
@@ -25,30 +25,39 @@ function parseErrorBody(body) {
|
|
|
25
25
|
return { details: void 0, error: void 0, apiMessage: void 0 };
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
var MAX_PROMPT_DISPLAY_LEN, PromptBlockedException, SilmarilApiError;
|
|
28
|
+
var MAX_PROMPT_DISPLAY_LEN, FirewallBlockedException, PromptBlockedException, SilmarilApiError;
|
|
29
29
|
var init_exceptions = __esm({
|
|
30
30
|
"src/exceptions.ts"() {
|
|
31
31
|
"use strict";
|
|
32
32
|
MAX_PROMPT_DISPLAY_LEN = 100;
|
|
33
|
-
|
|
33
|
+
FirewallBlockedException = class _FirewallBlockedException extends Error {
|
|
34
34
|
score;
|
|
35
35
|
threshold;
|
|
36
36
|
promptText;
|
|
37
37
|
runId;
|
|
38
|
+
hook;
|
|
39
|
+
toolName;
|
|
40
|
+
toolCallId;
|
|
41
|
+
result;
|
|
38
42
|
constructor(params) {
|
|
39
|
-
super(
|
|
40
|
-
this.name = "
|
|
43
|
+
super(_FirewallBlockedException.formatMessage(params));
|
|
44
|
+
this.name = "FirewallBlockedException";
|
|
41
45
|
this.score = params.score;
|
|
42
46
|
this.threshold = params.threshold;
|
|
43
47
|
this.promptText = params.promptText;
|
|
44
48
|
this.runId = params.runId;
|
|
45
|
-
|
|
49
|
+
this.hook = params.hook;
|
|
50
|
+
this.toolName = params.toolName;
|
|
51
|
+
this.toolCallId = params.toolCallId;
|
|
52
|
+
this.result = params.result;
|
|
53
|
+
Object.setPrototypeOf(this, _FirewallBlockedException.prototype);
|
|
46
54
|
}
|
|
47
55
|
static formatMessage(params) {
|
|
48
56
|
const truncated = params.promptText.length > MAX_PROMPT_DISPLAY_LEN ? `${params.promptText.slice(0, MAX_PROMPT_DISPLAY_LEN)}...` : params.promptText;
|
|
49
|
-
return `
|
|
57
|
+
return `Request blocked by Silmaril Firewall (score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): '${truncated}'`;
|
|
50
58
|
}
|
|
51
59
|
};
|
|
60
|
+
PromptBlockedException = FirewallBlockedException;
|
|
52
61
|
SilmarilApiError = class _SilmarilApiError extends Error {
|
|
53
62
|
status;
|
|
54
63
|
statusText;
|
|
@@ -239,19 +248,12 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
239
248
|
});
|
|
240
249
|
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
241
250
|
const onClassify = options.onClassify;
|
|
242
|
-
const fireOnClassify = (
|
|
251
|
+
const fireOnClassify = (event) => {
|
|
243
252
|
if (!onClassify) {
|
|
244
253
|
return;
|
|
245
254
|
}
|
|
246
255
|
try {
|
|
247
|
-
onClassify(
|
|
248
|
-
hook: hookLabel,
|
|
249
|
-
...toolName !== void 0 ? { toolName } : {},
|
|
250
|
-
text,
|
|
251
|
-
result,
|
|
252
|
-
blocked,
|
|
253
|
-
shadowMode
|
|
254
|
-
});
|
|
256
|
+
onClassify(event);
|
|
255
257
|
} catch (err) {
|
|
256
258
|
logger("onClassify callback threw", err);
|
|
257
259
|
}
|
|
@@ -272,15 +274,30 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
272
274
|
}
|
|
273
275
|
const threshold = result.threshold;
|
|
274
276
|
const blocked = result.score >= threshold;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
const commonEventFields = {
|
|
278
|
+
hook: hookLabel,
|
|
279
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
280
|
+
runId,
|
|
281
|
+
text,
|
|
282
|
+
result
|
|
283
|
+
};
|
|
284
|
+
fireOnClassify({
|
|
285
|
+
...commonEventFields,
|
|
286
|
+
blocked,
|
|
287
|
+
shadowMode
|
|
288
|
+
});
|
|
289
|
+
if (!blocked || shadowMode) {
|
|
290
|
+
return;
|
|
283
291
|
}
|
|
292
|
+
throw new FirewallBlockedException({
|
|
293
|
+
score: result.score,
|
|
294
|
+
threshold,
|
|
295
|
+
promptText: text,
|
|
296
|
+
runId,
|
|
297
|
+
hook: hookLabel,
|
|
298
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
299
|
+
result
|
|
300
|
+
});
|
|
284
301
|
};
|
|
285
302
|
class SilmarilFirewallHandler extends BaseCallbackHandler {
|
|
286
303
|
name = "silmaril_firewall_handler";
|
|
@@ -383,6 +400,9 @@ var init_langchain = __esm({
|
|
|
383
400
|
}
|
|
384
401
|
});
|
|
385
402
|
|
|
403
|
+
// src/firewall.ts
|
|
404
|
+
import { randomUUID } from "crypto";
|
|
405
|
+
|
|
386
406
|
// src/adapters/vercel.ts
|
|
387
407
|
init_exceptions();
|
|
388
408
|
init_hooks();
|
|
@@ -399,6 +419,9 @@ function stringifyToolValue(value) {
|
|
|
399
419
|
return String(value);
|
|
400
420
|
}
|
|
401
421
|
}
|
|
422
|
+
function isRecord(value) {
|
|
423
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
424
|
+
}
|
|
402
425
|
function extractContentText(content) {
|
|
403
426
|
if (typeof content === "string") {
|
|
404
427
|
return content;
|
|
@@ -423,14 +446,29 @@ function iterateToolResultParts(message) {
|
|
|
423
446
|
continue;
|
|
424
447
|
}
|
|
425
448
|
if (part.type === "tool-result") {
|
|
426
|
-
const text =
|
|
449
|
+
const text = stringifyToolResult(part.result !== void 0 ? part.result : part.output);
|
|
427
450
|
if (text.trim()) {
|
|
428
|
-
out.push({ text, toolName: part.toolName });
|
|
451
|
+
out.push({ text, toolName: part.toolName, toolCallId: part.toolCallId });
|
|
429
452
|
}
|
|
430
453
|
}
|
|
431
454
|
}
|
|
432
455
|
return out;
|
|
433
456
|
}
|
|
457
|
+
function stringifyToolResult(value) {
|
|
458
|
+
if (!isRecord(value)) {
|
|
459
|
+
return stringifyToolValue(value);
|
|
460
|
+
}
|
|
461
|
+
if ((value.type === "text" || value.type === "error-text") && typeof value.value === "string") {
|
|
462
|
+
return value.value;
|
|
463
|
+
}
|
|
464
|
+
if ((value.type === "json" || value.type === "error-json") && value.value !== void 0) {
|
|
465
|
+
return stringifyToolValue(value.value);
|
|
466
|
+
}
|
|
467
|
+
if (value.type === "content" && Array.isArray(value.value)) {
|
|
468
|
+
return value.value.map((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "").filter((text) => text.length > 0).join(" ");
|
|
469
|
+
}
|
|
470
|
+
return stringifyToolValue(value);
|
|
471
|
+
}
|
|
434
472
|
function findLastUserMessage(messages) {
|
|
435
473
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
436
474
|
const msg = messages[i];
|
|
@@ -444,33 +482,43 @@ function createMiddleware(firewall, options = {}) {
|
|
|
444
482
|
const scanInput = options.scanInput ?? true;
|
|
445
483
|
const scanOutput = options.scanOutput ?? false;
|
|
446
484
|
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
447
|
-
const classifyOrBlock = async (text, hook, toolName) => {
|
|
485
|
+
const classifyOrBlock = async (text, hook, context = { toolName: void 0, toolCallId: void 0 }) => {
|
|
448
486
|
if (!text.trim()) {
|
|
449
487
|
return;
|
|
450
488
|
}
|
|
489
|
+
const { toolName, toolCallId } = context;
|
|
451
490
|
const result = await firewall.classify(
|
|
452
491
|
text,
|
|
453
492
|
toolName !== void 0 ? { hook, toolName } : { hook }
|
|
454
493
|
);
|
|
455
494
|
const threshold = result.threshold;
|
|
456
495
|
const blocked = result.score >= threshold;
|
|
457
|
-
|
|
496
|
+
const commonEventFields = {
|
|
458
497
|
hook,
|
|
459
498
|
...toolName !== void 0 ? { toolName } : {},
|
|
499
|
+
...toolCallId !== void 0 ? { toolCallId } : {},
|
|
460
500
|
text,
|
|
461
|
-
result
|
|
501
|
+
result
|
|
502
|
+
};
|
|
503
|
+
options.onClassify?.({
|
|
504
|
+
...commonEventFields,
|
|
462
505
|
blocked,
|
|
463
506
|
shadowMode
|
|
464
507
|
});
|
|
465
|
-
if (blocked
|
|
466
|
-
|
|
467
|
-
score: result.score,
|
|
468
|
-
threshold,
|
|
469
|
-
promptText: text
|
|
470
|
-
});
|
|
471
|
-
options.onBlocked?.(err);
|
|
472
|
-
throw err;
|
|
508
|
+
if (!blocked || shadowMode) {
|
|
509
|
+
return;
|
|
473
510
|
}
|
|
511
|
+
const err = new FirewallBlockedException({
|
|
512
|
+
score: result.score,
|
|
513
|
+
threshold,
|
|
514
|
+
promptText: text,
|
|
515
|
+
hook,
|
|
516
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
517
|
+
...toolCallId !== void 0 ? { toolCallId } : {},
|
|
518
|
+
result
|
|
519
|
+
});
|
|
520
|
+
options.onBlocked?.(err);
|
|
521
|
+
throw err;
|
|
474
522
|
};
|
|
475
523
|
const scanPrompt = async (prompt) => {
|
|
476
524
|
if (prompt.length === 0) {
|
|
@@ -479,8 +527,8 @@ function createMiddleware(firewall, options = {}) {
|
|
|
479
527
|
const last = prompt[prompt.length - 1];
|
|
480
528
|
const role = last.role.toLowerCase();
|
|
481
529
|
if (role === "tool") {
|
|
482
|
-
for (const { text: text2, toolName } of iterateToolResultParts(last)) {
|
|
483
|
-
await classifyOrBlock(text2, HookLabel.TOOL_RESPONSE, toolName);
|
|
530
|
+
for (const { text: text2, toolName, toolCallId } of iterateToolResultParts(last)) {
|
|
531
|
+
await classifyOrBlock(text2, HookLabel.TOOL_RESPONSE, { toolName, toolCallId });
|
|
484
532
|
}
|
|
485
533
|
return;
|
|
486
534
|
}
|
|
@@ -503,21 +551,25 @@ function createMiddleware(firewall, options = {}) {
|
|
|
503
551
|
if (!call || typeof call !== "object") {
|
|
504
552
|
continue;
|
|
505
553
|
}
|
|
506
|
-
const
|
|
554
|
+
const toolInput = call.args !== void 0 ? call.args : call.input;
|
|
555
|
+
const args = typeof toolInput === "string" ? toolInput : stringifyToolValue(toolInput);
|
|
507
556
|
const toolName = typeof call.toolName === "string" ? call.toolName : void 0;
|
|
557
|
+
const toolCallId = typeof call.toolCallId === "string" ? call.toolCallId : void 0;
|
|
508
558
|
if (args.trim()) {
|
|
509
|
-
await classifyOrBlock(args, HookLabel.TOOL_CALL, toolName);
|
|
559
|
+
await classifyOrBlock(args, HookLabel.TOOL_CALL, { toolName, toolCallId });
|
|
510
560
|
}
|
|
511
561
|
}
|
|
512
562
|
}
|
|
513
563
|
};
|
|
514
564
|
return {
|
|
565
|
+
specificationVersion: "v3",
|
|
566
|
+
middlewareVersion: "v2",
|
|
515
567
|
async wrapGenerate({
|
|
516
568
|
params,
|
|
517
569
|
doGenerate
|
|
518
570
|
}) {
|
|
519
571
|
if (scanInput) {
|
|
520
|
-
await scanPrompt(params.prompt);
|
|
572
|
+
await scanPrompt(params.prompt ?? []);
|
|
521
573
|
}
|
|
522
574
|
const result = await doGenerate();
|
|
523
575
|
await scanGenerateResult(result);
|
|
@@ -528,7 +580,7 @@ function createMiddleware(firewall, options = {}) {
|
|
|
528
580
|
doStream
|
|
529
581
|
}) {
|
|
530
582
|
if (scanInput) {
|
|
531
|
-
await scanPrompt(params.prompt);
|
|
583
|
+
await scanPrompt(params.prompt ?? []);
|
|
532
584
|
}
|
|
533
585
|
const { stream, ...rest } = await doStream();
|
|
534
586
|
if (!scanOutput) {
|
|
@@ -552,7 +604,7 @@ function createMiddleware(firewall, options = {}) {
|
|
|
552
604
|
try {
|
|
553
605
|
await classifyOrBlock(buffered, HookLabel.LLM_OUTPUT);
|
|
554
606
|
} catch (err) {
|
|
555
|
-
if (err instanceof
|
|
607
|
+
if (err instanceof FirewallBlockedException) {
|
|
556
608
|
controller.enqueue({ type: "error", error: err });
|
|
557
609
|
return;
|
|
558
610
|
}
|
|
@@ -568,7 +620,7 @@ function createMiddleware(firewall, options = {}) {
|
|
|
568
620
|
|
|
569
621
|
// src/chunking.ts
|
|
570
622
|
var CHARS_PER_TOKEN = 4;
|
|
571
|
-
var MAX_INPUT_TOKENS =
|
|
623
|
+
var MAX_INPUT_TOKENS = 81920;
|
|
572
624
|
var CHUNK_WINDOW = 400;
|
|
573
625
|
var CHUNK_OVERLAP = 64;
|
|
574
626
|
var MAX_INPUT_CHARS = MAX_INPUT_TOKENS * CHARS_PER_TOKEN;
|
|
@@ -640,33 +692,17 @@ function chunkText(text) {
|
|
|
640
692
|
|
|
641
693
|
// src/firewall.ts
|
|
642
694
|
init_exceptions();
|
|
643
|
-
var
|
|
644
|
-
var TARGET_SEQUENCE_FPR = 0.01;
|
|
645
|
-
var MAX_ADAPTIVE_THRESHOLD = 0.9;
|
|
695
|
+
var SDK_VERSION = "0.4.1";
|
|
646
696
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
647
697
|
var DEFAULT_CHUNK_CONCURRENCY = 8;
|
|
648
698
|
var DEFAULT_MAX_RETRIES = 5;
|
|
649
699
|
var MAX_BACKOFF_SECONDS = 30;
|
|
650
700
|
var MAX_ERROR_BODY_BYTES = 1 << 16;
|
|
651
|
-
function
|
|
652
|
-
if (!Number.isInteger(scoringOpportunityCount) || scoringOpportunityCount < 1) {
|
|
653
|
-
throw new Error(
|
|
654
|
-
`Firewall: scoringOpportunityCount must be an integer >= 1, got ${scoringOpportunityCount}`
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
if (scoringOpportunityCount === 1) {
|
|
658
|
-
return BASE_THRESHOLD;
|
|
659
|
-
}
|
|
660
|
-
const targetChunkFpr = 1 - Math.pow(1 - TARGET_SEQUENCE_FPR, 1 / scoringOpportunityCount);
|
|
661
|
-
const oddsRatio = TARGET_SEQUENCE_FPR / targetChunkFpr;
|
|
662
|
-
const rawThreshold = oddsRatio / (1 + oddsRatio);
|
|
663
|
-
return Math.min(rawThreshold, MAX_ADAPTIVE_THRESHOLD);
|
|
664
|
-
}
|
|
665
|
-
function blockResultFromResponse(data, threshold) {
|
|
701
|
+
function blockResultFromResponse(data) {
|
|
666
702
|
const result = {
|
|
667
703
|
prediction: data.prediction,
|
|
668
704
|
score: Number(data.score),
|
|
669
|
-
threshold
|
|
705
|
+
threshold: Number(data.threshold)
|
|
670
706
|
};
|
|
671
707
|
if (data.primary_outcome !== void 0) {
|
|
672
708
|
result.primaryOutcome = data.primary_outcome;
|
|
@@ -688,6 +724,26 @@ function blockResultFromResponse(data, threshold) {
|
|
|
688
724
|
}
|
|
689
725
|
return Object.freeze(result);
|
|
690
726
|
}
|
|
727
|
+
function isRecord2(value) {
|
|
728
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
729
|
+
}
|
|
730
|
+
function withSdkMetadata(metadata, info) {
|
|
731
|
+
const payload = { ...metadata ?? {} };
|
|
732
|
+
const existing = payload.silmaril;
|
|
733
|
+
if (existing !== void 0 && !isRecord2(existing)) {
|
|
734
|
+
throw new Error("Firewall: metadata.silmaril must be an object when provided");
|
|
735
|
+
}
|
|
736
|
+
payload.silmaril = {
|
|
737
|
+
...isRecord2(existing) ? existing : {},
|
|
738
|
+
sdk_language: "typescript",
|
|
739
|
+
sdk_version: SDK_VERSION,
|
|
740
|
+
request_id: info.requestId,
|
|
741
|
+
input_index: info.inputIndex,
|
|
742
|
+
chunk_index: info.chunkIndex,
|
|
743
|
+
chunk_count: info.chunkCount
|
|
744
|
+
};
|
|
745
|
+
return payload;
|
|
746
|
+
}
|
|
691
747
|
async function readCappedErrorBody(response) {
|
|
692
748
|
if (!response.body) {
|
|
693
749
|
return response.text().then((body2) => body2.slice(0, MAX_ERROR_BODY_BYTES)).catch(() => "");
|
|
@@ -784,14 +840,24 @@ var Firewall = class {
|
|
|
784
840
|
}
|
|
785
841
|
async classify(text, options = {}) {
|
|
786
842
|
const chunks = chunkText(text);
|
|
787
|
-
const
|
|
843
|
+
const requestId = options.requestId ?? randomUUID();
|
|
788
844
|
if (chunks.length === 1) {
|
|
789
|
-
return this.classifySingleChunk(chunks[0], options,
|
|
845
|
+
return this.classifySingleChunk(chunks[0], options, {
|
|
846
|
+
requestId,
|
|
847
|
+
inputIndex: 0,
|
|
848
|
+
chunkIndex: 0,
|
|
849
|
+
chunkCount: 1
|
|
850
|
+
});
|
|
790
851
|
}
|
|
791
852
|
const results = await mapWithConcurrency(
|
|
792
853
|
chunks,
|
|
793
854
|
this.chunkConcurrency,
|
|
794
|
-
(chunk) => this.classifySingleChunk(chunk, options,
|
|
855
|
+
(chunk, index) => this.classifySingleChunk(chunk, options, {
|
|
856
|
+
requestId,
|
|
857
|
+
inputIndex: 0,
|
|
858
|
+
chunkIndex: index,
|
|
859
|
+
chunkCount: chunks.length
|
|
860
|
+
})
|
|
795
861
|
);
|
|
796
862
|
return results.reduce((best, r) => r.score > best.score ? r : best);
|
|
797
863
|
}
|
|
@@ -814,10 +880,9 @@ var Firewall = class {
|
|
|
814
880
|
`Firewall: metadata length ${options.metadata.length} does not match texts length ${texts.length}`
|
|
815
881
|
);
|
|
816
882
|
}
|
|
817
|
-
const
|
|
883
|
+
const requestId = options.requestId ?? randomUUID();
|
|
818
884
|
const payload = {
|
|
819
|
-
texts: texts.map((text) => sanitizeText(text))
|
|
820
|
-
threshold
|
|
885
|
+
texts: texts.map((text) => sanitizeText(text))
|
|
821
886
|
};
|
|
822
887
|
if (options.hooks && options.hooks.length > 0) {
|
|
823
888
|
payload.hooks = options.hooks.map((h) => String(h));
|
|
@@ -825,14 +890,21 @@ var Firewall = class {
|
|
|
825
890
|
if (options.toolNames && options.toolNames.length > 0) {
|
|
826
891
|
payload.tool_names = options.toolNames.map((t) => t === void 0 ? null : t);
|
|
827
892
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
893
|
+
payload.metadata = texts.map(
|
|
894
|
+
(_, index) => withSdkMetadata(options.metadata?.[index], {
|
|
895
|
+
requestId,
|
|
896
|
+
inputIndex: index,
|
|
897
|
+
chunkIndex: 0,
|
|
898
|
+
chunkCount: 1
|
|
899
|
+
})
|
|
900
|
+
);
|
|
831
901
|
const data = await this.postWithRetry(payload);
|
|
832
|
-
return data.predictions.map((p) => blockResultFromResponse(p
|
|
902
|
+
return data.predictions.map((p) => blockResultFromResponse(p));
|
|
833
903
|
}
|
|
834
904
|
asLangChainHandler(options = {}) {
|
|
835
|
-
return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
|
|
905
|
+
return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
|
|
906
|
+
(m) => m.createLangChainHandler(this, options)
|
|
907
|
+
);
|
|
836
908
|
}
|
|
837
909
|
asMiddleware(options = {}) {
|
|
838
910
|
return createMiddleware(this, options);
|
|
@@ -862,19 +934,17 @@ var Firewall = class {
|
|
|
862
934
|
}
|
|
863
935
|
throw new Error("Firewall: exhausted retries (unreachable)");
|
|
864
936
|
}
|
|
865
|
-
async classifySingleChunk(text, options,
|
|
866
|
-
const payload = { text
|
|
937
|
+
async classifySingleChunk(text, options, metadataInfo) {
|
|
938
|
+
const payload = { text };
|
|
867
939
|
if (options.hook !== void 0) {
|
|
868
940
|
payload.hook = options.hook;
|
|
869
941
|
}
|
|
870
942
|
if (options.toolName !== void 0) {
|
|
871
943
|
payload.tool_name = options.toolName;
|
|
872
944
|
}
|
|
873
|
-
|
|
874
|
-
payload.metadata = options.metadata;
|
|
875
|
-
}
|
|
945
|
+
payload.metadata = withSdkMetadata(options.metadata, metadataInfo);
|
|
876
946
|
const data = await this.postWithRetry(payload);
|
|
877
|
-
return blockResultFromResponse(data
|
|
947
|
+
return blockResultFromResponse(data);
|
|
878
948
|
}
|
|
879
949
|
};
|
|
880
950
|
|
|
@@ -892,6 +962,7 @@ export {
|
|
|
892
962
|
DEFAULT_TIMEOUT_MS,
|
|
893
963
|
FIREWALL_HOOK_TO_LABEL,
|
|
894
964
|
Firewall,
|
|
965
|
+
FirewallBlockedException,
|
|
895
966
|
FirewallHook,
|
|
896
967
|
HookLabel,
|
|
897
968
|
INPUT_HOOKS,
|