@silmaril-security/sdk 0.7.10 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/adapters/langchain.ts
31
31
  var langchain_exports = {};
32
32
  __export(langchain_exports, {
33
- createLangChainHandler: () => createLangChainHandler
33
+ createLangChainHandler: () => createLangChainHandler,
34
+ protectLangChainRunnable: () => protectLangChainRunnable
34
35
  });
35
36
  module.exports = __toCommonJS(langchain_exports);
36
37
 
@@ -184,7 +185,81 @@ function extractTextFromDocuments(documents) {
184
185
  return parts.join("\n");
185
186
  }
186
187
 
188
+ // src/adapters/thread-identity.ts
189
+ function record(value) {
190
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
191
+ }
192
+ function validThreadId(value) {
193
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
194
+ }
195
+ function nativeThreadId(runtime, metadata, inherited, currentConfig) {
196
+ const explicit = record(record(runtime)?.configurable);
197
+ const current = record(record(currentConfig)?.configurable);
198
+ for (const candidate of [explicit, current, record(metadata)]) {
199
+ if (candidate && Object.prototype.hasOwnProperty.call(candidate, "thread_id")) {
200
+ return validThreadId(candidate.thread_id);
201
+ }
202
+ }
203
+ return inherited;
204
+ }
205
+ function rejectConversationOptions(options) {
206
+ if ("conversationId" in options || "conversation_id" in options) {
207
+ throw new TypeError("conversationId/conversation_id was removed. Pass the native configurable.thread_id when invoking the agent or runnable instead.");
208
+ }
209
+ }
210
+ var RunThreadIdentity = class {
211
+ constructor(boundIdentity, currentConfig = () => void 0) {
212
+ this.boundIdentity = boundIdentity;
213
+ this.currentConfig = currentConfig;
214
+ }
215
+ boundIdentity;
216
+ currentConfig;
217
+ runs = /* @__PURE__ */ new Map();
218
+ children = /* @__PURE__ */ new Map();
219
+ start(runId, parentRunId, metadata) {
220
+ const discarded = this.finish(runId);
221
+ if (this.runs.size >= 4096) {
222
+ const oldestRoot = [...this.runs].find(([, run]) => run.parentRunId === void 0 || !this.runs.has(run.parentRunId))?.[0] ?? this.runs.keys().next().value;
223
+ if (oldestRoot !== void 0) discarded.push(...this.finish(oldestRoot));
224
+ }
225
+ const inherited = parentRunId === void 0 ? void 0 : this.runs.get(parentRunId)?.threadId;
226
+ const exposed = record(metadata);
227
+ const threadId = this.boundIdentity && (parentRunId === void 0 || !this.runs.has(parentRunId)) ? this.boundIdentity.threadId : exposed && Object.prototype.hasOwnProperty.call(exposed, "thread_id") ? typeof exposed.langgraph_node === "string" ? nativeThreadId(void 0, exposed, inherited, this.currentConfig()) : validThreadId(exposed.thread_id) : inherited;
228
+ this.runs.set(runId, { threadId, parentRunId });
229
+ if (parentRunId !== void 0 && parentRunId !== runId) {
230
+ const children = this.children.get(parentRunId) ?? /* @__PURE__ */ new Set();
231
+ children.add(runId);
232
+ this.children.set(parentRunId, children);
233
+ }
234
+ return discarded;
235
+ }
236
+ get(runId) {
237
+ const run = this.runs.get(runId);
238
+ return run ? run.threadId : this.boundIdentity?.threadId;
239
+ }
240
+ finish(runId) {
241
+ const pending = [runId];
242
+ const seen = /* @__PURE__ */ new Set();
243
+ while (pending.length) {
244
+ const id = pending.pop();
245
+ if (seen.has(id)) continue;
246
+ seen.add(id);
247
+ pending.push(...this.children.get(id) ?? []);
248
+ this.children.delete(id);
249
+ const parent = this.runs.get(id)?.parentRunId;
250
+ this.runs.delete(id);
251
+ if (parent !== void 0) {
252
+ const siblings = this.children.get(parent);
253
+ siblings?.delete(id);
254
+ if (siblings?.size === 0) this.children.delete(parent);
255
+ }
256
+ }
257
+ return [...seen];
258
+ }
259
+ };
260
+
187
261
  // src/adapters/langchain.ts
262
+ var handlerCleanups = /* @__PURE__ */ new WeakMap();
188
263
  var USER_ROLES = /* @__PURE__ */ new Set(["human", "user"]);
189
264
  function getMessageRole(message) {
190
265
  if (typeof message.role === "string") {
@@ -193,7 +268,8 @@ function getMessageRole(message) {
193
268
  if (typeof message.type === "string") {
194
269
  return message.type.toLowerCase();
195
270
  }
196
- return "";
271
+ const legacyType = message.getType?.() ?? message._getType?.();
272
+ return typeof legacyType === "string" ? legacyType.toLowerCase() : "";
197
273
  }
198
274
  function extractMessageText(content) {
199
275
  if (content === void 0) {
@@ -222,7 +298,16 @@ function findLastUserMessage(messages) {
222
298
  return void 0;
223
299
  }
224
300
  async function createLangChainHandler(firewall, options = {}) {
225
- const { BaseCallbackHandler } = await import("@langchain/core/callbacks/base");
301
+ return buildLangChainHandler(firewall, options);
302
+ }
303
+ async function buildLangChainHandler(firewall, options, boundIdentity) {
304
+ rejectConversationOptions(options);
305
+ const { ensureConfig } = await import("@langchain/core/runnables");
306
+ const threads = new RunThreadIdentity(boundIdentity, () => ensureConfig());
307
+ const { BaseTracer } = await import("@langchain/core/tracers/base");
308
+ let finishRun = (runId) => {
309
+ threads.finish(runId);
310
+ };
226
311
  const enabledHooks = resolveHooks(options.hooks);
227
312
  const includeTool = options.includeTool ?? true;
228
313
  const failOpen = options.failOpen ?? true;
@@ -241,14 +326,14 @@ async function createLangChainHandler(firewall, options = {}) {
241
326
  logger("onClassify callback threw", err);
242
327
  }
243
328
  };
244
- const classify = async (text, hookLabel, runId, toolName) => {
329
+ const classifyEvent = async (text, hookLabel, runId, toolName) => {
245
330
  let result;
246
331
  try {
247
332
  result = await firewall.classify(text, {
248
333
  hook: hookLabel,
249
334
  metadata: {
250
335
  langgraph: { run_id: runId },
251
- ...options.conversationId === void 0 ? {} : { conversationId: options.conversationId }
336
+ ...threads.get(runId) === void 0 ? {} : { conversationId: threads.get(runId) }
252
337
  },
253
338
  ...requestedMode !== void 0 ? { mode: requestedMode } : {},
254
339
  ...toolName !== void 0 ? { toolName } : {}
@@ -292,98 +377,327 @@ async function createLangChainHandler(firewall, options = {}) {
292
377
  result
293
378
  });
294
379
  };
295
- class SilmarilFirewallHandler extends BaseCallbackHandler {
380
+ const classify = async (text, hook, runId, toolName) => {
381
+ try {
382
+ await classifyEvent(text, hook, runId, toolName);
383
+ } catch (error) {
384
+ finishRun(runId);
385
+ throw error;
386
+ }
387
+ };
388
+ function serialized(value, name) {
389
+ if (value && typeof value === "object" && "id" in value && Array.isArray(value.id)) return value;
390
+ return { lc: 1, type: "not_implemented", id: [name] };
391
+ }
392
+ class SilmarilFirewallHandler extends BaseTracer {
296
393
  name = "silmaril_firewall_handler";
297
394
  raiseError = true;
298
395
  awaitHandlers = true;
299
- async handleChatModelStart(_llm, messages, runId) {
300
- if (!enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
301
- return;
396
+ // LangChain clears runnable context inside ordinary callbacks. Its synchronous
397
+ // tracer pre-start hooks capture native config before that boundary. Runs are
398
+ // memory-only and every terminal path discards both native tracer maps.
399
+ async persistRun() {
400
+ }
401
+ getRunById(runId) {
402
+ return runId === void 0 ? void 0 : this.runMap.get(runId);
403
+ }
404
+ // Only lifecycle identity is needed. Do not retain prompts, outputs, token
405
+ // events, or completed children when upstream omits a cancellation callback.
406
+ _addRunToRunMap(run) {
407
+ return super._addRunToRunMap({
408
+ ...run,
409
+ inputs: {},
410
+ extra: {},
411
+ serialized: {},
412
+ tags: [],
413
+ child_runs: [],
414
+ events: []
415
+ });
416
+ }
417
+ _addChildRun() {
418
+ }
419
+ async _endTrace(run) {
420
+ try {
421
+ await super._endTrace(run);
422
+ } finally {
423
+ run.inputs = {};
424
+ delete run.outputs;
425
+ delete run.error;
426
+ run.extra = {};
427
+ run.events = [];
428
+ run.child_runs = [];
302
429
  }
303
- const batches = messages;
304
- const flat = [];
305
- for (const batch of batches) {
306
- for (const m of batch) {
307
- flat.push(m);
308
- }
430
+ }
431
+ async handleText() {
432
+ }
433
+ async handleAgentAction() {
434
+ }
435
+ async handleAgentEnd() {
436
+ }
437
+ async handleLLMNewToken(_token, _idx, runId) {
438
+ return this.getRunById(runId) ?? this._createRunForLLMStart(serialized(void 0, "llm"), [], runId);
439
+ }
440
+ capture(runId, parentRunId, metadata) {
441
+ for (const id of threads.start(runId, parentRunId, metadata)) {
442
+ this.runMap.delete(id);
443
+ this.runTreeMap?.delete(id);
444
+ }
445
+ }
446
+ finishAll() {
447
+ for (const id of [...this.runMap.keys()]) this.finishRun(id);
448
+ this.runTreeMap?.clear();
449
+ }
450
+ finishRun(runId) {
451
+ for (const id of threads.finish(runId)) {
452
+ this.runMap.delete(id);
453
+ this.runTreeMap?.delete(id);
309
454
  }
310
- const lastUser = findLastUserMessage(flat);
311
- if (!lastUser) {
312
- return;
455
+ }
456
+ _createRunForLLMStart(...args) {
457
+ this.capture(args[2], args[3], args[6]);
458
+ return super._createRunForLLMStart(...args);
459
+ }
460
+ _createRunForChatModelStart(...args) {
461
+ this.capture(args[2], args[3], args[6]);
462
+ return super._createRunForChatModelStart(...args);
463
+ }
464
+ _createRunForChainStart(...args) {
465
+ this.capture(args[2], args[3], args[5]);
466
+ return super._createRunForChainStart(...args);
467
+ }
468
+ _createRunForToolStart(...args) {
469
+ this.capture(args[2], args[3], args[5]);
470
+ return super._createRunForToolStart(...args);
471
+ }
472
+ _createRunForRetrieverStart(...args) {
473
+ this.capture(args[2], args[3], args[5]);
474
+ return super._createRunForRetrieverStart(...args);
475
+ }
476
+ async handleChatModelStart(llm, messages, runId, parentRunId, extraParams, tags, metadata) {
477
+ const batches = messages;
478
+ const run = this.getRunById(runId) ?? this._createRunForChatModelStart(
479
+ serialized(llm, "chat_model"),
480
+ messages,
481
+ runId,
482
+ parentRunId,
483
+ extraParams,
484
+ tags,
485
+ metadata
486
+ );
487
+ if (enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
488
+ const lastUser = findLastUserMessage(batches.flat());
489
+ const text = lastUser ? extractMessageText(lastUser.content).trim() : "";
490
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
313
491
  }
314
- const text = extractMessageText(lastUser.content).trim();
315
- if (!text) {
316
- return;
492
+ return run;
493
+ }
494
+ async handleLLMStart(llm, prompts, runId, parentRunId, extraParams, tags, metadata) {
495
+ const run = this.getRunById(runId) ?? this._createRunForLLMStart(
496
+ serialized(llm, "llm"),
497
+ prompts,
498
+ runId,
499
+ parentRunId,
500
+ extraParams,
501
+ tags,
502
+ metadata
503
+ );
504
+ if (enabledHooks.has(FirewallHook.LLM_START)) {
505
+ const text = extractTextFromPrompts(prompts);
506
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
317
507
  }
318
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
508
+ return run;
319
509
  }
320
- async handleLLMStart(_llm, prompts, runId) {
321
- if (!enabledHooks.has(FirewallHook.LLM_START)) {
322
- return;
510
+ async handleToolStart(tool, inputStr, runId, parentRunId, tags, metadata) {
511
+ const run = this.getRunById(runId) ?? this._createRunForToolStart(
512
+ serialized(tool, tool?.name ?? "tool"),
513
+ inputStr,
514
+ runId,
515
+ parentRunId,
516
+ tags,
517
+ metadata
518
+ );
519
+ if (includeTool && enabledHooks.has(FirewallHook.TOOL_START)) {
520
+ const text = extractTextFromToolInput(inputStr);
521
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, tool?.name);
323
522
  }
324
- const text = extractTextFromPrompts(prompts);
325
- if (!text) {
326
- return;
523
+ return run;
524
+ }
525
+ async handleRetrieverStart(retriever, query, runId, parentRunId, tags, metadata) {
526
+ const run = this.getRunById(runId) ?? this._createRunForRetrieverStart(
527
+ serialized(retriever, "retriever"),
528
+ query,
529
+ runId,
530
+ parentRunId,
531
+ tags,
532
+ metadata
533
+ );
534
+ if (enabledHooks.has(FirewallHook.RETRIEVER_START) && query.trim()) {
535
+ await classify(query.trim(), FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
327
536
  }
328
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
537
+ return run;
329
538
  }
330
- async handleToolStart(tool, inputStr, runId) {
331
- if (!includeTool || !enabledHooks.has(FirewallHook.TOOL_START)) {
332
- return;
539
+ async handleLLMEnd(output, runId) {
540
+ try {
541
+ if (!this.getRunById(runId)) this._createRunForLLMStart(serialized(void 0, "llm"), [], runId);
542
+ if (enabledHooks.has(FirewallHook.LLM_END)) {
543
+ const text = extractTextFromLLMResult(output);
544
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
545
+ }
546
+ return await super.handleLLMEnd(output, runId);
547
+ } finally {
548
+ finishRun(runId);
333
549
  }
334
- const text = extractTextFromToolInput(inputStr);
335
- if (!text) {
336
- return;
550
+ }
551
+ async handleToolEnd(output, runId, _parentRunId, _tags, kwargs) {
552
+ try {
553
+ if (!this.getRunById(runId)) this._createRunForToolStart(serialized(void 0, "tool"), "", runId);
554
+ if (includeTool && enabledHooks.has(FirewallHook.TOOL_END)) {
555
+ const text = String(output).trim();
556
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, kwargs?.name);
557
+ }
558
+ return await super.handleToolEnd(output, runId);
559
+ } finally {
560
+ finishRun(runId);
337
561
  }
338
- const toolName = tool?.name;
339
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
340
562
  }
341
- async handleRetrieverStart(_retriever, query, runId) {
342
- if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
343
- return;
563
+ async handleRetrieverEnd(documents, runId) {
564
+ try {
565
+ if (!this.getRunById(runId)) this._createRunForRetrieverStart(serialized(void 0, "retriever"), "", runId);
566
+ if (enabledHooks.has(FirewallHook.RETRIEVER_END)) {
567
+ const text = extractTextFromDocuments(documents);
568
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
569
+ }
570
+ return await super.handleRetrieverEnd(documents, runId);
571
+ } finally {
572
+ finishRun(runId);
344
573
  }
345
- const text = query.trim();
346
- if (!text) {
347
- return;
574
+ }
575
+ async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, name) {
576
+ return super.handleChainStart(serialized(chain, "chain"), inputs, runId, parentRunId, tags, metadata, runType, name);
577
+ }
578
+ async handleChainEnd(...args) {
579
+ if (!this.getRunById(args[1])) this._createRunForChainStart(serialized(void 0, "chain"), {}, args[1]);
580
+ try {
581
+ return await super.handleChainEnd(...args);
582
+ } finally {
583
+ finishRun(args[1]);
348
584
  }
349
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
350
585
  }
351
- async handleLLMEnd(output, runId) {
352
- if (!enabledHooks.has(FirewallHook.LLM_END)) {
353
- return;
586
+ async handleChainError(...args) {
587
+ if (!this.getRunById(args[1])) this._createRunForChainStart(serialized(void 0, "chain"), {}, args[1]);
588
+ try {
589
+ return await super.handleChainError(...args);
590
+ } finally {
591
+ finishRun(args[1]);
354
592
  }
355
- const text = extractTextFromLLMResult(output);
356
- if (!text) {
357
- return;
593
+ }
594
+ async handleLLMError(...args) {
595
+ if (!this.getRunById(args[1])) this._createRunForLLMStart(serialized(void 0, "llm"), [], args[1]);
596
+ try {
597
+ return await super.handleLLMError(...args);
598
+ } finally {
599
+ finishRun(args[1]);
358
600
  }
359
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
360
601
  }
361
- async handleToolEnd(output, runId, _parentRunId, _tags, _kwargs) {
362
- if (!includeTool || !enabledHooks.has(FirewallHook.TOOL_END)) {
363
- return;
602
+ async handleToolError(...args) {
603
+ if (!this.getRunById(args[1])) this._createRunForToolStart(serialized(void 0, "tool"), "", args[1]);
604
+ try {
605
+ return await super.handleToolError(...args);
606
+ } finally {
607
+ finishRun(args[1]);
364
608
  }
365
- const text = String(output).trim();
366
- if (!text) {
367
- return;
609
+ }
610
+ async handleRetrieverError(...args) {
611
+ if (!this.getRunById(args[1])) this._createRunForRetrieverStart(serialized(void 0, "retriever"), "", args[1]);
612
+ try {
613
+ return await super.handleRetrieverError(...args);
614
+ } finally {
615
+ finishRun(args[1]);
368
616
  }
369
- const toolName = _kwargs?.name;
370
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
371
617
  }
372
- async handleRetrieverEnd(documents, runId) {
373
- if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
374
- return;
618
+ }
619
+ const handler = new SilmarilFirewallHandler();
620
+ finishRun = (runId) => {
621
+ handler.finishRun(runId);
622
+ };
623
+ handlerCleanups.set(handler, () => handler.finishAll());
624
+ return handler;
625
+ }
626
+ async function protectLangChainRunnable(firewall, runnable, options = {}) {
627
+ rejectConversationOptions(options);
628
+ const { RunnableBinding, RunnableRetry, ensureConfig, mergeConfigs } = await import("@langchain/core/runnables");
629
+ const { IterableReadableStream } = await import("@langchain/core/utils/stream");
630
+ const configFactories = [async (config) => {
631
+ const effective = ensureConfig(config);
632
+ const handler = await buildLangChainHandler(firewall, options, {
633
+ threadId: validThreadId(effective.configurable?.thread_id)
634
+ });
635
+ return { callbacks: [handler] };
636
+ }];
637
+ class ProtectedBinding extends RunnableBinding {
638
+ async scopedConfig(...configs) {
639
+ const created = [];
640
+ const scopedFactories = configFactories.map((factory) => async (config2) => {
641
+ const update = await factory(config2);
642
+ if (Array.isArray(update.callbacks)) created.push(...update.callbacks);
643
+ return update;
644
+ });
645
+ const scoped = new RunnableBinding({ bound: this.bound, config: this.config, configFactories: scopedFactories });
646
+ const config = await scoped._mergeConfig(...configs);
647
+ return { config, dispose: () => {
648
+ for (const handler of created) handlerCleanups.get(handler)?.();
649
+ } };
650
+ }
651
+ async invoke(input, options2) {
652
+ const scoped = await this.scopedConfig(options2, this.kwargs);
653
+ try {
654
+ return await this.bound.invoke(input, scoped.config);
655
+ } finally {
656
+ scoped.dispose();
375
657
  }
376
- const text = extractTextFromDocuments(documents);
377
- if (!text) {
378
- return;
658
+ }
659
+ async stream(input, options2) {
660
+ const scoped = await this.scopedConfig(ensureConfig(options2), this.kwargs);
661
+ let upstream;
662
+ try {
663
+ upstream = await this.bound.stream(input, scoped.config);
664
+ } catch (error) {
665
+ scoped.dispose();
666
+ throw error;
379
667
  }
380
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
668
+ const managed = async function* () {
669
+ try {
670
+ yield* upstream;
671
+ } finally {
672
+ scoped.dispose();
673
+ }
674
+ };
675
+ return IterableReadableStream.fromAsyncGenerator(managed());
676
+ }
677
+ async *_streamIterator(input, options2) {
678
+ const stream = await this.stream(input, options2);
679
+ yield* stream;
680
+ }
681
+ withConfig(config) {
682
+ return new ProtectedBinding({ bound: this.bound, config: mergeConfigs(this.config, config), configFactories });
683
+ }
684
+ withListeners(fields) {
685
+ return new RunnableBinding({ bound: this, config: {} }).withListeners(fields);
686
+ }
687
+ withRetry(fields) {
688
+ return new RunnableRetry({
689
+ bound: this,
690
+ config: {},
691
+ ...fields?.stopAfterAttempt === void 0 ? {} : { maxAttemptNumber: fields.stopAfterAttempt },
692
+ ...fields?.onFailedAttempt === void 0 ? {} : { onFailedAttempt: fields.onFailedAttempt }
693
+ });
381
694
  }
382
695
  }
383
- return new SilmarilFirewallHandler();
696
+ return new ProtectedBinding({ bound: runnable, config: {}, configFactories });
384
697
  }
385
698
  // Annotate the CommonJS export names for ESM import in node:
386
699
  0 && (module.exports = {
387
- createLangChainHandler
700
+ createLangChainHandler,
701
+ protectLangChainRunnable
388
702
  });
389
703
  //# sourceMappingURL=langchain.cjs.map