@silmaril-security/sdk 0.3.1 → 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 +85 -32
- 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 +154 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -146
- package/dist/index.d.ts +16 -146
- package/dist/index.js +150 -79
- package/dist/index.js.map +1 -1
- package/package.json +38 -10
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
declare const HookLabel: {
|
|
2
|
+
readonly USER_INPUT: "user_input";
|
|
3
|
+
readonly SYSTEM_PROMPT: "system_prompt";
|
|
4
|
+
readonly TOOL_CALL: "tool_call";
|
|
5
|
+
readonly TOOL_RESPONSE: "tool_response";
|
|
6
|
+
readonly LLM_OUTPUT: "llm_output";
|
|
7
|
+
readonly UNKNOWN: "unknown";
|
|
8
|
+
};
|
|
9
|
+
type HookLabel = (typeof HookLabel)[keyof typeof HookLabel];
|
|
10
|
+
declare const FirewallHook: {
|
|
11
|
+
readonly LLM_START: "on_llm_start";
|
|
12
|
+
readonly CHAT_MODEL_START: "on_chat_model_start";
|
|
13
|
+
readonly TOOL_START: "on_tool_start";
|
|
14
|
+
readonly RETRIEVER_START: "on_retriever_start";
|
|
15
|
+
readonly LLM_END: "on_llm_end";
|
|
16
|
+
readonly TOOL_END: "on_tool_end";
|
|
17
|
+
readonly RETRIEVER_END: "on_retriever_end";
|
|
18
|
+
};
|
|
19
|
+
type FirewallHook = (typeof FirewallHook)[keyof typeof FirewallHook];
|
|
20
|
+
declare const DEFAULT_HOOKS: ReadonlySet<FirewallHook>;
|
|
21
|
+
declare const INPUT_HOOKS: ReadonlySet<FirewallHook>;
|
|
22
|
+
declare const OUTPUT_HOOKS: ReadonlySet<FirewallHook>;
|
|
23
|
+
declare const ALL_HOOKS: ReadonlySet<FirewallHook>;
|
|
24
|
+
declare const FIREWALL_HOOK_TO_LABEL: Readonly<Record<FirewallHook, HookLabel>>;
|
|
25
|
+
declare function resolveHooks(hooks: Iterable<FirewallHook | string> | undefined): ReadonlySet<FirewallHook>;
|
|
26
|
+
declare function prependHook(text: string, hook: HookLabel | string | undefined | null): string;
|
|
27
|
+
declare function prependToolName(text: string, toolName: string | undefined | null): string;
|
|
28
|
+
|
|
29
|
+
type Prediction = "BENIGN" | "MALICIOUS";
|
|
30
|
+
interface BlockResult {
|
|
31
|
+
readonly prediction: Prediction;
|
|
32
|
+
readonly score: number;
|
|
33
|
+
readonly threshold: number;
|
|
34
|
+
readonly primaryOutcome?: string;
|
|
35
|
+
readonly outcomeScores?: Readonly<Record<string, number>>;
|
|
36
|
+
readonly detectorScores?: Readonly<Record<string, number>>;
|
|
37
|
+
readonly detectorCounts?: Readonly<Record<string, number>>;
|
|
38
|
+
}
|
|
39
|
+
interface FirewallOptions {
|
|
40
|
+
apiKey: string;
|
|
41
|
+
apiUrl: string;
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
chunkConcurrency?: number;
|
|
44
|
+
shadowMode?: boolean;
|
|
45
|
+
}
|
|
46
|
+
type ClassificationMetadata = Readonly<Record<string, unknown>>;
|
|
47
|
+
interface ClassifyOptions {
|
|
48
|
+
hook?: HookLabel;
|
|
49
|
+
toolName?: string;
|
|
50
|
+
metadata?: ClassificationMetadata;
|
|
51
|
+
requestId?: string;
|
|
52
|
+
}
|
|
53
|
+
interface ClassifyBatchOptions {
|
|
54
|
+
hooks?: readonly HookLabel[];
|
|
55
|
+
toolNames?: readonly (string | undefined)[];
|
|
56
|
+
metadata?: readonly (ClassificationMetadata | undefined)[];
|
|
57
|
+
requestId?: string;
|
|
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 LangChainFirewallHandler {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
readonly raiseError: boolean;
|
|
71
|
+
readonly awaitHandlers: boolean;
|
|
72
|
+
handleChatModelStart?: (...args: unknown[]) => unknown;
|
|
73
|
+
handleLLMStart?: (...args: unknown[]) => unknown;
|
|
74
|
+
handleToolStart?: (...args: unknown[]) => unknown;
|
|
75
|
+
handleRetrieverStart?: (...args: unknown[]) => unknown;
|
|
76
|
+
handleLLMEnd?: (...args: unknown[]) => unknown;
|
|
77
|
+
handleToolEnd?: (...args: unknown[]) => unknown;
|
|
78
|
+
handleRetrieverEnd?: (...args: unknown[]) => unknown;
|
|
79
|
+
}
|
|
80
|
+
interface ClassifyEvent {
|
|
81
|
+
readonly hook: HookLabel;
|
|
82
|
+
readonly toolName?: string;
|
|
83
|
+
readonly toolCallId?: string;
|
|
84
|
+
readonly runId?: string;
|
|
85
|
+
readonly text: string;
|
|
86
|
+
readonly result: BlockResult;
|
|
87
|
+
readonly blocked: boolean;
|
|
88
|
+
readonly shadowMode: boolean;
|
|
89
|
+
}
|
|
90
|
+
interface MiddlewareOptions {
|
|
91
|
+
scanInput?: boolean;
|
|
92
|
+
scanOutput?: boolean;
|
|
93
|
+
scanToolCalls?: boolean;
|
|
94
|
+
shadowMode?: boolean;
|
|
95
|
+
onBlocked?: (err: Error) => void;
|
|
96
|
+
onClassify?: (event: ClassifyEvent) => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface VercelContentPart {
|
|
100
|
+
type?: string;
|
|
101
|
+
text?: string;
|
|
102
|
+
toolCallId?: string;
|
|
103
|
+
toolName?: string;
|
|
104
|
+
args?: unknown;
|
|
105
|
+
output?: unknown;
|
|
106
|
+
result?: unknown;
|
|
107
|
+
}
|
|
108
|
+
interface VercelPromptMessage {
|
|
109
|
+
role: string;
|
|
110
|
+
content: string | ReadonlyArray<VercelContentPart | string>;
|
|
111
|
+
}
|
|
112
|
+
interface VercelToolCall {
|
|
113
|
+
toolCallType?: string;
|
|
114
|
+
toolCallId?: string;
|
|
115
|
+
toolName?: string;
|
|
116
|
+
args?: unknown;
|
|
117
|
+
input?: unknown;
|
|
118
|
+
}
|
|
119
|
+
interface VercelGenerateResult {
|
|
120
|
+
text?: string;
|
|
121
|
+
toolCalls?: ReadonlyArray<VercelToolCall>;
|
|
122
|
+
}
|
|
123
|
+
interface WrapGenerateArgs<TResult extends VercelGenerateResult & Record<string, unknown>> {
|
|
124
|
+
params: {
|
|
125
|
+
prompt?: ReadonlyArray<VercelPromptMessage>;
|
|
126
|
+
} & Record<string, unknown>;
|
|
127
|
+
doGenerate: () => PromiseLike<TResult>;
|
|
128
|
+
}
|
|
129
|
+
interface WrapStreamArgs<TResult extends {
|
|
130
|
+
stream: ReadableStream<unknown>;
|
|
131
|
+
} & Record<string, unknown>> {
|
|
132
|
+
params: {
|
|
133
|
+
prompt?: ReadonlyArray<VercelPromptMessage>;
|
|
134
|
+
} & Record<string, unknown>;
|
|
135
|
+
doStream: () => PromiseLike<TResult>;
|
|
136
|
+
}
|
|
137
|
+
interface FirewallMiddleware {
|
|
138
|
+
readonly specificationVersion: "v3";
|
|
139
|
+
readonly middlewareVersion: "v2";
|
|
140
|
+
wrapGenerate: <TResult extends VercelGenerateResult & Record<string, unknown>>(args: WrapGenerateArgs<TResult>) => Promise<TResult>;
|
|
141
|
+
wrapStream: <TResult extends {
|
|
142
|
+
stream: ReadableStream<unknown>;
|
|
143
|
+
} & Record<string, unknown>>(args: WrapStreamArgs<TResult>) => Promise<TResult>;
|
|
144
|
+
}
|
|
145
|
+
declare function createMiddleware(firewall: Firewall, options?: MiddlewareOptions): FirewallMiddleware;
|
|
146
|
+
|
|
147
|
+
declare const DEFAULT_TIMEOUT_MS = 10000;
|
|
148
|
+
declare const DEFAULT_CHUNK_CONCURRENCY = 8;
|
|
149
|
+
declare class Firewall {
|
|
150
|
+
readonly apiKey: string;
|
|
151
|
+
readonly apiUrl: string;
|
|
152
|
+
readonly timeoutMs: number;
|
|
153
|
+
readonly chunkConcurrency: number;
|
|
154
|
+
readonly shadowMode: boolean;
|
|
155
|
+
private readonly headers;
|
|
156
|
+
constructor(options: FirewallOptions);
|
|
157
|
+
classify(text: string, options?: ClassifyOptions): Promise<BlockResult>;
|
|
158
|
+
classifyBatch(texts: readonly string[], options?: ClassifyBatchOptions): Promise<BlockResult[]>;
|
|
159
|
+
asLangChainHandler<THandler = LangChainFirewallHandler>(options?: LangChainAdapterOptions): Promise<THandler>;
|
|
160
|
+
asMiddleware(options?: MiddlewareOptions): FirewallMiddleware;
|
|
161
|
+
private postWithRetry;
|
|
162
|
+
private classifySingleChunk;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { ALL_HOOKS as A, type BlockResult as B, type ClassificationMetadata as C, DEFAULT_CHUNK_CONCURRENCY as D, FIREWALL_HOOK_TO_LABEL as F, HookLabel as H, INPUT_HOOKS as I, type LangChainAdapterOptions as L, type MiddlewareOptions as M, OUTPUT_HOOKS as O, type Prediction as P, type ClassifyBatchOptions as a, type ClassifyEvent as b, type ClassifyOptions as c, DEFAULT_HOOKS as d, DEFAULT_TIMEOUT_MS as e, Firewall as f, FirewallHook as g, type FirewallMiddleware as h, type FirewallOptions as i, type LangChainFirewallHandler as j, prependToolName as k, createMiddleware as l, prependHook as p, resolveHooks as r };
|
package/dist/index.cjs
CHANGED
|
@@ -47,30 +47,39 @@ function parseErrorBody(body) {
|
|
|
47
47
|
return { details: void 0, error: void 0, apiMessage: void 0 };
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
var MAX_PROMPT_DISPLAY_LEN, PromptBlockedException, SilmarilApiError;
|
|
50
|
+
var MAX_PROMPT_DISPLAY_LEN, FirewallBlockedException, PromptBlockedException, SilmarilApiError;
|
|
51
51
|
var init_exceptions = __esm({
|
|
52
52
|
"src/exceptions.ts"() {
|
|
53
53
|
"use strict";
|
|
54
54
|
MAX_PROMPT_DISPLAY_LEN = 100;
|
|
55
|
-
|
|
55
|
+
FirewallBlockedException = class _FirewallBlockedException extends Error {
|
|
56
56
|
score;
|
|
57
57
|
threshold;
|
|
58
58
|
promptText;
|
|
59
59
|
runId;
|
|
60
|
+
hook;
|
|
61
|
+
toolName;
|
|
62
|
+
toolCallId;
|
|
63
|
+
result;
|
|
60
64
|
constructor(params) {
|
|
61
|
-
super(
|
|
62
|
-
this.name = "
|
|
65
|
+
super(_FirewallBlockedException.formatMessage(params));
|
|
66
|
+
this.name = "FirewallBlockedException";
|
|
63
67
|
this.score = params.score;
|
|
64
68
|
this.threshold = params.threshold;
|
|
65
69
|
this.promptText = params.promptText;
|
|
66
70
|
this.runId = params.runId;
|
|
67
|
-
|
|
71
|
+
this.hook = params.hook;
|
|
72
|
+
this.toolName = params.toolName;
|
|
73
|
+
this.toolCallId = params.toolCallId;
|
|
74
|
+
this.result = params.result;
|
|
75
|
+
Object.setPrototypeOf(this, _FirewallBlockedException.prototype);
|
|
68
76
|
}
|
|
69
77
|
static formatMessage(params) {
|
|
70
78
|
const truncated = params.promptText.length > MAX_PROMPT_DISPLAY_LEN ? `${params.promptText.slice(0, MAX_PROMPT_DISPLAY_LEN)}...` : params.promptText;
|
|
71
|
-
return `
|
|
79
|
+
return `Request blocked by Silmaril Firewall (score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): '${truncated}'`;
|
|
72
80
|
}
|
|
73
81
|
};
|
|
82
|
+
PromptBlockedException = FirewallBlockedException;
|
|
74
83
|
SilmarilApiError = class _SilmarilApiError extends Error {
|
|
75
84
|
status;
|
|
76
85
|
statusText;
|
|
@@ -261,19 +270,12 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
261
270
|
});
|
|
262
271
|
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
263
272
|
const onClassify = options.onClassify;
|
|
264
|
-
const fireOnClassify = (
|
|
273
|
+
const fireOnClassify = (event) => {
|
|
265
274
|
if (!onClassify) {
|
|
266
275
|
return;
|
|
267
276
|
}
|
|
268
277
|
try {
|
|
269
|
-
onClassify(
|
|
270
|
-
hook: hookLabel,
|
|
271
|
-
...toolName !== void 0 ? { toolName } : {},
|
|
272
|
-
text,
|
|
273
|
-
result,
|
|
274
|
-
blocked,
|
|
275
|
-
shadowMode
|
|
276
|
-
});
|
|
278
|
+
onClassify(event);
|
|
277
279
|
} catch (err) {
|
|
278
280
|
logger("onClassify callback threw", err);
|
|
279
281
|
}
|
|
@@ -294,15 +296,30 @@ async function createLangChainHandler(firewall, options = {}) {
|
|
|
294
296
|
}
|
|
295
297
|
const threshold = result.threshold;
|
|
296
298
|
const blocked = result.score >= threshold;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
const commonEventFields = {
|
|
300
|
+
hook: hookLabel,
|
|
301
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
302
|
+
runId,
|
|
303
|
+
text,
|
|
304
|
+
result
|
|
305
|
+
};
|
|
306
|
+
fireOnClassify({
|
|
307
|
+
...commonEventFields,
|
|
308
|
+
blocked,
|
|
309
|
+
shadowMode
|
|
310
|
+
});
|
|
311
|
+
if (!blocked || shadowMode) {
|
|
312
|
+
return;
|
|
305
313
|
}
|
|
314
|
+
throw new FirewallBlockedException({
|
|
315
|
+
score: result.score,
|
|
316
|
+
threshold,
|
|
317
|
+
promptText: text,
|
|
318
|
+
runId,
|
|
319
|
+
hook: hookLabel,
|
|
320
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
321
|
+
result
|
|
322
|
+
});
|
|
306
323
|
};
|
|
307
324
|
class SilmarilFirewallHandler extends BaseCallbackHandler {
|
|
308
325
|
name = "silmaril_firewall_handler";
|
|
@@ -406,8 +423,8 @@ var init_langchain = __esm({
|
|
|
406
423
|
});
|
|
407
424
|
|
|
408
425
|
// src/index.ts
|
|
409
|
-
var
|
|
410
|
-
__export(
|
|
426
|
+
var src_exports = {};
|
|
427
|
+
__export(src_exports, {
|
|
411
428
|
ALL_HOOKS: () => ALL_HOOKS,
|
|
412
429
|
CHUNK_OVERLAP: () => CHUNK_OVERLAP,
|
|
413
430
|
CHUNK_OVERLAP_CHARS: () => CHUNK_OVERLAP_CHARS,
|
|
@@ -418,6 +435,7 @@ __export(index_exports, {
|
|
|
418
435
|
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
419
436
|
FIREWALL_HOOK_TO_LABEL: () => FIREWALL_HOOK_TO_LABEL,
|
|
420
437
|
Firewall: () => Firewall,
|
|
438
|
+
FirewallBlockedException: () => FirewallBlockedException,
|
|
421
439
|
FirewallHook: () => FirewallHook,
|
|
422
440
|
HookLabel: () => HookLabel,
|
|
423
441
|
INPUT_HOOKS: () => INPUT_HOOKS,
|
|
@@ -432,7 +450,10 @@ __export(index_exports, {
|
|
|
432
450
|
resolveHooks: () => resolveHooks,
|
|
433
451
|
sanitizeText: () => sanitizeText
|
|
434
452
|
});
|
|
435
|
-
module.exports = __toCommonJS(
|
|
453
|
+
module.exports = __toCommonJS(src_exports);
|
|
454
|
+
|
|
455
|
+
// src/firewall.ts
|
|
456
|
+
var import_node_crypto = require("crypto");
|
|
436
457
|
|
|
437
458
|
// src/adapters/vercel.ts
|
|
438
459
|
init_exceptions();
|
|
@@ -450,6 +471,9 @@ function stringifyToolValue(value) {
|
|
|
450
471
|
return String(value);
|
|
451
472
|
}
|
|
452
473
|
}
|
|
474
|
+
function isRecord(value) {
|
|
475
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
476
|
+
}
|
|
453
477
|
function extractContentText(content) {
|
|
454
478
|
if (typeof content === "string") {
|
|
455
479
|
return content;
|
|
@@ -474,14 +498,29 @@ function iterateToolResultParts(message) {
|
|
|
474
498
|
continue;
|
|
475
499
|
}
|
|
476
500
|
if (part.type === "tool-result") {
|
|
477
|
-
const text =
|
|
501
|
+
const text = stringifyToolResult(part.result !== void 0 ? part.result : part.output);
|
|
478
502
|
if (text.trim()) {
|
|
479
|
-
out.push({ text, toolName: part.toolName });
|
|
503
|
+
out.push({ text, toolName: part.toolName, toolCallId: part.toolCallId });
|
|
480
504
|
}
|
|
481
505
|
}
|
|
482
506
|
}
|
|
483
507
|
return out;
|
|
484
508
|
}
|
|
509
|
+
function stringifyToolResult(value) {
|
|
510
|
+
if (!isRecord(value)) {
|
|
511
|
+
return stringifyToolValue(value);
|
|
512
|
+
}
|
|
513
|
+
if ((value.type === "text" || value.type === "error-text") && typeof value.value === "string") {
|
|
514
|
+
return value.value;
|
|
515
|
+
}
|
|
516
|
+
if ((value.type === "json" || value.type === "error-json") && value.value !== void 0) {
|
|
517
|
+
return stringifyToolValue(value.value);
|
|
518
|
+
}
|
|
519
|
+
if (value.type === "content" && Array.isArray(value.value)) {
|
|
520
|
+
return value.value.map((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "").filter((text) => text.length > 0).join(" ");
|
|
521
|
+
}
|
|
522
|
+
return stringifyToolValue(value);
|
|
523
|
+
}
|
|
485
524
|
function findLastUserMessage(messages) {
|
|
486
525
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
487
526
|
const msg = messages[i];
|
|
@@ -495,33 +534,43 @@ function createMiddleware(firewall, options = {}) {
|
|
|
495
534
|
const scanInput = options.scanInput ?? true;
|
|
496
535
|
const scanOutput = options.scanOutput ?? false;
|
|
497
536
|
const shadowMode = options.shadowMode ?? firewall.shadowMode;
|
|
498
|
-
const classifyOrBlock = async (text, hook, toolName) => {
|
|
537
|
+
const classifyOrBlock = async (text, hook, context = { toolName: void 0, toolCallId: void 0 }) => {
|
|
499
538
|
if (!text.trim()) {
|
|
500
539
|
return;
|
|
501
540
|
}
|
|
541
|
+
const { toolName, toolCallId } = context;
|
|
502
542
|
const result = await firewall.classify(
|
|
503
543
|
text,
|
|
504
544
|
toolName !== void 0 ? { hook, toolName } : { hook }
|
|
505
545
|
);
|
|
506
546
|
const threshold = result.threshold;
|
|
507
547
|
const blocked = result.score >= threshold;
|
|
508
|
-
|
|
548
|
+
const commonEventFields = {
|
|
509
549
|
hook,
|
|
510
550
|
...toolName !== void 0 ? { toolName } : {},
|
|
551
|
+
...toolCallId !== void 0 ? { toolCallId } : {},
|
|
511
552
|
text,
|
|
512
|
-
result
|
|
553
|
+
result
|
|
554
|
+
};
|
|
555
|
+
options.onClassify?.({
|
|
556
|
+
...commonEventFields,
|
|
513
557
|
blocked,
|
|
514
558
|
shadowMode
|
|
515
559
|
});
|
|
516
|
-
if (blocked
|
|
517
|
-
|
|
518
|
-
score: result.score,
|
|
519
|
-
threshold,
|
|
520
|
-
promptText: text
|
|
521
|
-
});
|
|
522
|
-
options.onBlocked?.(err);
|
|
523
|
-
throw err;
|
|
560
|
+
if (!blocked || shadowMode) {
|
|
561
|
+
return;
|
|
524
562
|
}
|
|
563
|
+
const err = new FirewallBlockedException({
|
|
564
|
+
score: result.score,
|
|
565
|
+
threshold,
|
|
566
|
+
promptText: text,
|
|
567
|
+
hook,
|
|
568
|
+
...toolName !== void 0 ? { toolName } : {},
|
|
569
|
+
...toolCallId !== void 0 ? { toolCallId } : {},
|
|
570
|
+
result
|
|
571
|
+
});
|
|
572
|
+
options.onBlocked?.(err);
|
|
573
|
+
throw err;
|
|
525
574
|
};
|
|
526
575
|
const scanPrompt = async (prompt) => {
|
|
527
576
|
if (prompt.length === 0) {
|
|
@@ -530,8 +579,8 @@ function createMiddleware(firewall, options = {}) {
|
|
|
530
579
|
const last = prompt[prompt.length - 1];
|
|
531
580
|
const role = last.role.toLowerCase();
|
|
532
581
|
if (role === "tool") {
|
|
533
|
-
for (const { text: text2, toolName } of iterateToolResultParts(last)) {
|
|
534
|
-
await classifyOrBlock(text2, HookLabel.TOOL_RESPONSE, toolName);
|
|
582
|
+
for (const { text: text2, toolName, toolCallId } of iterateToolResultParts(last)) {
|
|
583
|
+
await classifyOrBlock(text2, HookLabel.TOOL_RESPONSE, { toolName, toolCallId });
|
|
535
584
|
}
|
|
536
585
|
return;
|
|
537
586
|
}
|
|
@@ -554,21 +603,25 @@ function createMiddleware(firewall, options = {}) {
|
|
|
554
603
|
if (!call || typeof call !== "object") {
|
|
555
604
|
continue;
|
|
556
605
|
}
|
|
557
|
-
const
|
|
606
|
+
const toolInput = call.args !== void 0 ? call.args : call.input;
|
|
607
|
+
const args = typeof toolInput === "string" ? toolInput : stringifyToolValue(toolInput);
|
|
558
608
|
const toolName = typeof call.toolName === "string" ? call.toolName : void 0;
|
|
609
|
+
const toolCallId = typeof call.toolCallId === "string" ? call.toolCallId : void 0;
|
|
559
610
|
if (args.trim()) {
|
|
560
|
-
await classifyOrBlock(args, HookLabel.TOOL_CALL, toolName);
|
|
611
|
+
await classifyOrBlock(args, HookLabel.TOOL_CALL, { toolName, toolCallId });
|
|
561
612
|
}
|
|
562
613
|
}
|
|
563
614
|
}
|
|
564
615
|
};
|
|
565
616
|
return {
|
|
617
|
+
specificationVersion: "v3",
|
|
618
|
+
middlewareVersion: "v2",
|
|
566
619
|
async wrapGenerate({
|
|
567
620
|
params,
|
|
568
621
|
doGenerate
|
|
569
622
|
}) {
|
|
570
623
|
if (scanInput) {
|
|
571
|
-
await scanPrompt(params.prompt);
|
|
624
|
+
await scanPrompt(params.prompt ?? []);
|
|
572
625
|
}
|
|
573
626
|
const result = await doGenerate();
|
|
574
627
|
await scanGenerateResult(result);
|
|
@@ -579,7 +632,7 @@ function createMiddleware(firewall, options = {}) {
|
|
|
579
632
|
doStream
|
|
580
633
|
}) {
|
|
581
634
|
if (scanInput) {
|
|
582
|
-
await scanPrompt(params.prompt);
|
|
635
|
+
await scanPrompt(params.prompt ?? []);
|
|
583
636
|
}
|
|
584
637
|
const { stream, ...rest } = await doStream();
|
|
585
638
|
if (!scanOutput) {
|
|
@@ -603,7 +656,7 @@ function createMiddleware(firewall, options = {}) {
|
|
|
603
656
|
try {
|
|
604
657
|
await classifyOrBlock(buffered, HookLabel.LLM_OUTPUT);
|
|
605
658
|
} catch (err) {
|
|
606
|
-
if (err instanceof
|
|
659
|
+
if (err instanceof FirewallBlockedException) {
|
|
607
660
|
controller.enqueue({ type: "error", error: err });
|
|
608
661
|
return;
|
|
609
662
|
}
|
|
@@ -691,33 +744,17 @@ function chunkText(text) {
|
|
|
691
744
|
|
|
692
745
|
// src/firewall.ts
|
|
693
746
|
init_exceptions();
|
|
694
|
-
var
|
|
695
|
-
var TARGET_SEQUENCE_FPR = 0.01;
|
|
696
|
-
var MAX_ADAPTIVE_THRESHOLD = 0.9;
|
|
747
|
+
var SDK_VERSION = "0.4.1";
|
|
697
748
|
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
698
749
|
var DEFAULT_CHUNK_CONCURRENCY = 8;
|
|
699
750
|
var DEFAULT_MAX_RETRIES = 5;
|
|
700
751
|
var MAX_BACKOFF_SECONDS = 30;
|
|
701
752
|
var MAX_ERROR_BODY_BYTES = 1 << 16;
|
|
702
|
-
function
|
|
703
|
-
if (!Number.isInteger(scoringOpportunityCount) || scoringOpportunityCount < 1) {
|
|
704
|
-
throw new Error(
|
|
705
|
-
`Firewall: scoringOpportunityCount must be an integer >= 1, got ${scoringOpportunityCount}`
|
|
706
|
-
);
|
|
707
|
-
}
|
|
708
|
-
if (scoringOpportunityCount === 1) {
|
|
709
|
-
return BASE_THRESHOLD;
|
|
710
|
-
}
|
|
711
|
-
const targetChunkFpr = 1 - Math.pow(1 - TARGET_SEQUENCE_FPR, 1 / scoringOpportunityCount);
|
|
712
|
-
const oddsRatio = TARGET_SEQUENCE_FPR / targetChunkFpr;
|
|
713
|
-
const rawThreshold = oddsRatio / (1 + oddsRatio);
|
|
714
|
-
return Math.min(rawThreshold, MAX_ADAPTIVE_THRESHOLD);
|
|
715
|
-
}
|
|
716
|
-
function blockResultFromResponse(data, threshold) {
|
|
753
|
+
function blockResultFromResponse(data) {
|
|
717
754
|
const result = {
|
|
718
755
|
prediction: data.prediction,
|
|
719
756
|
score: Number(data.score),
|
|
720
|
-
threshold
|
|
757
|
+
threshold: Number(data.threshold)
|
|
721
758
|
};
|
|
722
759
|
if (data.primary_outcome !== void 0) {
|
|
723
760
|
result.primaryOutcome = data.primary_outcome;
|
|
@@ -739,6 +776,26 @@ function blockResultFromResponse(data, threshold) {
|
|
|
739
776
|
}
|
|
740
777
|
return Object.freeze(result);
|
|
741
778
|
}
|
|
779
|
+
function isRecord2(value) {
|
|
780
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
781
|
+
}
|
|
782
|
+
function withSdkMetadata(metadata, info) {
|
|
783
|
+
const payload = { ...metadata ?? {} };
|
|
784
|
+
const existing = payload.silmaril;
|
|
785
|
+
if (existing !== void 0 && !isRecord2(existing)) {
|
|
786
|
+
throw new Error("Firewall: metadata.silmaril must be an object when provided");
|
|
787
|
+
}
|
|
788
|
+
payload.silmaril = {
|
|
789
|
+
...isRecord2(existing) ? existing : {},
|
|
790
|
+
sdk_language: "typescript",
|
|
791
|
+
sdk_version: SDK_VERSION,
|
|
792
|
+
request_id: info.requestId,
|
|
793
|
+
input_index: info.inputIndex,
|
|
794
|
+
chunk_index: info.chunkIndex,
|
|
795
|
+
chunk_count: info.chunkCount
|
|
796
|
+
};
|
|
797
|
+
return payload;
|
|
798
|
+
}
|
|
742
799
|
async function readCappedErrorBody(response) {
|
|
743
800
|
if (!response.body) {
|
|
744
801
|
return response.text().then((body2) => body2.slice(0, MAX_ERROR_BODY_BYTES)).catch(() => "");
|
|
@@ -835,14 +892,24 @@ var Firewall = class {
|
|
|
835
892
|
}
|
|
836
893
|
async classify(text, options = {}) {
|
|
837
894
|
const chunks = chunkText(text);
|
|
838
|
-
const
|
|
895
|
+
const requestId = options.requestId ?? (0, import_node_crypto.randomUUID)();
|
|
839
896
|
if (chunks.length === 1) {
|
|
840
|
-
return this.classifySingleChunk(chunks[0], options,
|
|
897
|
+
return this.classifySingleChunk(chunks[0], options, {
|
|
898
|
+
requestId,
|
|
899
|
+
inputIndex: 0,
|
|
900
|
+
chunkIndex: 0,
|
|
901
|
+
chunkCount: 1
|
|
902
|
+
});
|
|
841
903
|
}
|
|
842
904
|
const results = await mapWithConcurrency(
|
|
843
905
|
chunks,
|
|
844
906
|
this.chunkConcurrency,
|
|
845
|
-
(chunk) => this.classifySingleChunk(chunk, options,
|
|
907
|
+
(chunk, index) => this.classifySingleChunk(chunk, options, {
|
|
908
|
+
requestId,
|
|
909
|
+
inputIndex: 0,
|
|
910
|
+
chunkIndex: index,
|
|
911
|
+
chunkCount: chunks.length
|
|
912
|
+
})
|
|
846
913
|
);
|
|
847
914
|
return results.reduce((best, r) => r.score > best.score ? r : best);
|
|
848
915
|
}
|
|
@@ -865,10 +932,9 @@ var Firewall = class {
|
|
|
865
932
|
`Firewall: metadata length ${options.metadata.length} does not match texts length ${texts.length}`
|
|
866
933
|
);
|
|
867
934
|
}
|
|
868
|
-
const
|
|
935
|
+
const requestId = options.requestId ?? (0, import_node_crypto.randomUUID)();
|
|
869
936
|
const payload = {
|
|
870
|
-
texts: texts.map((text) => sanitizeText(text))
|
|
871
|
-
threshold
|
|
937
|
+
texts: texts.map((text) => sanitizeText(text))
|
|
872
938
|
};
|
|
873
939
|
if (options.hooks && options.hooks.length > 0) {
|
|
874
940
|
payload.hooks = options.hooks.map((h) => String(h));
|
|
@@ -876,14 +942,21 @@ var Firewall = class {
|
|
|
876
942
|
if (options.toolNames && options.toolNames.length > 0) {
|
|
877
943
|
payload.tool_names = options.toolNames.map((t) => t === void 0 ? null : t);
|
|
878
944
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
945
|
+
payload.metadata = texts.map(
|
|
946
|
+
(_, index) => withSdkMetadata(options.metadata?.[index], {
|
|
947
|
+
requestId,
|
|
948
|
+
inputIndex: index,
|
|
949
|
+
chunkIndex: 0,
|
|
950
|
+
chunkCount: 1
|
|
951
|
+
})
|
|
952
|
+
);
|
|
882
953
|
const data = await this.postWithRetry(payload);
|
|
883
|
-
return data.predictions.map((p) => blockResultFromResponse(p
|
|
954
|
+
return data.predictions.map((p) => blockResultFromResponse(p));
|
|
884
955
|
}
|
|
885
956
|
asLangChainHandler(options = {}) {
|
|
886
|
-
return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
|
|
957
|
+
return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
|
|
958
|
+
(m) => m.createLangChainHandler(this, options)
|
|
959
|
+
);
|
|
887
960
|
}
|
|
888
961
|
asMiddleware(options = {}) {
|
|
889
962
|
return createMiddleware(this, options);
|
|
@@ -913,19 +986,17 @@ var Firewall = class {
|
|
|
913
986
|
}
|
|
914
987
|
throw new Error("Firewall: exhausted retries (unreachable)");
|
|
915
988
|
}
|
|
916
|
-
async classifySingleChunk(text, options,
|
|
917
|
-
const payload = { text
|
|
989
|
+
async classifySingleChunk(text, options, metadataInfo) {
|
|
990
|
+
const payload = { text };
|
|
918
991
|
if (options.hook !== void 0) {
|
|
919
992
|
payload.hook = options.hook;
|
|
920
993
|
}
|
|
921
994
|
if (options.toolName !== void 0) {
|
|
922
995
|
payload.tool_name = options.toolName;
|
|
923
996
|
}
|
|
924
|
-
|
|
925
|
-
payload.metadata = options.metadata;
|
|
926
|
-
}
|
|
997
|
+
payload.metadata = withSdkMetadata(options.metadata, metadataInfo);
|
|
927
998
|
const data = await this.postWithRetry(payload);
|
|
928
|
-
return blockResultFromResponse(data
|
|
999
|
+
return blockResultFromResponse(data);
|
|
929
1000
|
}
|
|
930
1001
|
};
|
|
931
1002
|
|
|
@@ -944,6 +1015,7 @@ init_hooks();
|
|
|
944
1015
|
DEFAULT_TIMEOUT_MS,
|
|
945
1016
|
FIREWALL_HOOK_TO_LABEL,
|
|
946
1017
|
Firewall,
|
|
1018
|
+
FirewallBlockedException,
|
|
947
1019
|
FirewallHook,
|
|
948
1020
|
HookLabel,
|
|
949
1021
|
INPUT_HOOKS,
|