@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.
package/dist/index.js CHANGED
@@ -256,10 +256,90 @@ var init_extract = __esm({
256
256
  }
257
257
  });
258
258
 
259
+ // src/adapters/thread-identity.ts
260
+ function record(value) {
261
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
262
+ }
263
+ function validThreadId(value) {
264
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
265
+ }
266
+ function nativeThreadId(runtime, metadata, inherited, currentConfig) {
267
+ const explicit = record(record(runtime)?.configurable);
268
+ const current = record(record(currentConfig)?.configurable);
269
+ for (const candidate of [explicit, current, record(metadata)]) {
270
+ if (candidate && Object.prototype.hasOwnProperty.call(candidate, "thread_id")) {
271
+ return validThreadId(candidate.thread_id);
272
+ }
273
+ }
274
+ return inherited;
275
+ }
276
+ function rejectConversationOptions(options) {
277
+ if ("conversationId" in options || "conversation_id" in options) {
278
+ throw new TypeError("conversationId/conversation_id was removed. Pass the native configurable.thread_id when invoking the agent or runnable instead.");
279
+ }
280
+ }
281
+ var RunThreadIdentity;
282
+ var init_thread_identity = __esm({
283
+ "src/adapters/thread-identity.ts"() {
284
+ "use strict";
285
+ RunThreadIdentity = class {
286
+ constructor(boundIdentity, currentConfig = () => void 0) {
287
+ this.boundIdentity = boundIdentity;
288
+ this.currentConfig = currentConfig;
289
+ }
290
+ boundIdentity;
291
+ currentConfig;
292
+ runs = /* @__PURE__ */ new Map();
293
+ children = /* @__PURE__ */ new Map();
294
+ start(runId, parentRunId, metadata) {
295
+ const discarded = this.finish(runId);
296
+ if (this.runs.size >= 4096) {
297
+ const oldestRoot = [...this.runs].find(([, run]) => run.parentRunId === void 0 || !this.runs.has(run.parentRunId))?.[0] ?? this.runs.keys().next().value;
298
+ if (oldestRoot !== void 0) discarded.push(...this.finish(oldestRoot));
299
+ }
300
+ const inherited = parentRunId === void 0 ? void 0 : this.runs.get(parentRunId)?.threadId;
301
+ const exposed = record(metadata);
302
+ 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;
303
+ this.runs.set(runId, { threadId, parentRunId });
304
+ if (parentRunId !== void 0 && parentRunId !== runId) {
305
+ const children = this.children.get(parentRunId) ?? /* @__PURE__ */ new Set();
306
+ children.add(runId);
307
+ this.children.set(parentRunId, children);
308
+ }
309
+ return discarded;
310
+ }
311
+ get(runId) {
312
+ const run = this.runs.get(runId);
313
+ return run ? run.threadId : this.boundIdentity?.threadId;
314
+ }
315
+ finish(runId) {
316
+ const pending = [runId];
317
+ const seen = /* @__PURE__ */ new Set();
318
+ while (pending.length) {
319
+ const id = pending.pop();
320
+ if (seen.has(id)) continue;
321
+ seen.add(id);
322
+ pending.push(...this.children.get(id) ?? []);
323
+ this.children.delete(id);
324
+ const parent = this.runs.get(id)?.parentRunId;
325
+ this.runs.delete(id);
326
+ if (parent !== void 0) {
327
+ const siblings = this.children.get(parent);
328
+ siblings?.delete(id);
329
+ if (siblings?.size === 0) this.children.delete(parent);
330
+ }
331
+ }
332
+ return [...seen];
333
+ }
334
+ };
335
+ }
336
+ });
337
+
259
338
  // src/adapters/langchain.ts
260
339
  var langchain_exports = {};
261
340
  __export(langchain_exports, {
262
- createLangChainHandler: () => createLangChainHandler
341
+ createLangChainHandler: () => createLangChainHandler,
342
+ protectLangChainRunnable: () => protectLangChainRunnable
263
343
  });
264
344
  function getMessageRole(message) {
265
345
  if (typeof message.role === "string") {
@@ -268,7 +348,8 @@ function getMessageRole(message) {
268
348
  if (typeof message.type === "string") {
269
349
  return message.type.toLowerCase();
270
350
  }
271
- return "";
351
+ const legacyType = message.getType?.() ?? message._getType?.();
352
+ return typeof legacyType === "string" ? legacyType.toLowerCase() : "";
272
353
  }
273
354
  function extractMessageText(content) {
274
355
  if (content === void 0) {
@@ -297,7 +378,16 @@ function findLastUserMessage2(messages) {
297
378
  return void 0;
298
379
  }
299
380
  async function createLangChainHandler(firewall, options = {}) {
300
- const { BaseCallbackHandler } = await import("@langchain/core/callbacks/base");
381
+ return buildLangChainHandler(firewall, options);
382
+ }
383
+ async function buildLangChainHandler(firewall, options, boundIdentity) {
384
+ rejectConversationOptions(options);
385
+ const { ensureConfig } = await import("@langchain/core/runnables");
386
+ const threads = new RunThreadIdentity(boundIdentity, () => ensureConfig());
387
+ const { BaseTracer } = await import("@langchain/core/tracers/base");
388
+ let finishRun = (runId) => {
389
+ threads.finish(runId);
390
+ };
301
391
  const enabledHooks = resolveHooks(options.hooks);
302
392
  const includeTool = options.includeTool ?? true;
303
393
  const failOpen = options.failOpen ?? true;
@@ -316,14 +406,14 @@ async function createLangChainHandler(firewall, options = {}) {
316
406
  logger("onClassify callback threw", err);
317
407
  }
318
408
  };
319
- const classify = async (text, hookLabel, runId, toolName) => {
409
+ const classifyEvent = async (text, hookLabel, runId, toolName) => {
320
410
  let result;
321
411
  try {
322
412
  result = await firewall.classify(text, {
323
413
  hook: hookLabel,
324
414
  metadata: {
325
415
  langgraph: { run_id: runId },
326
- ...options.conversationId === void 0 ? {} : { conversationId: options.conversationId }
416
+ ...threads.get(runId) === void 0 ? {} : { conversationId: threads.get(runId) }
327
417
  },
328
418
  ...requestedMode !== void 0 ? { mode: requestedMode } : {},
329
419
  ...toolName !== void 0 ? { toolName } : {}
@@ -367,97 +457,325 @@ async function createLangChainHandler(firewall, options = {}) {
367
457
  result
368
458
  });
369
459
  };
370
- class SilmarilFirewallHandler extends BaseCallbackHandler {
460
+ const classify = async (text, hook, runId, toolName) => {
461
+ try {
462
+ await classifyEvent(text, hook, runId, toolName);
463
+ } catch (error) {
464
+ finishRun(runId);
465
+ throw error;
466
+ }
467
+ };
468
+ function serialized(value, name) {
469
+ if (value && typeof value === "object" && "id" in value && Array.isArray(value.id)) return value;
470
+ return { lc: 1, type: "not_implemented", id: [name] };
471
+ }
472
+ class SilmarilFirewallHandler extends BaseTracer {
371
473
  name = "silmaril_firewall_handler";
372
474
  raiseError = true;
373
475
  awaitHandlers = true;
374
- async handleChatModelStart(_llm, messages, runId) {
375
- if (!enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
376
- return;
377
- }
378
- const batches = messages;
379
- const flat = [];
380
- for (const batch of batches) {
381
- for (const m of batch) {
382
- flat.push(m);
383
- }
476
+ // LangChain clears runnable context inside ordinary callbacks. Its synchronous
477
+ // tracer pre-start hooks capture native config before that boundary. Runs are
478
+ // memory-only and every terminal path discards both native tracer maps.
479
+ async persistRun() {
480
+ }
481
+ getRunById(runId) {
482
+ return runId === void 0 ? void 0 : this.runMap.get(runId);
483
+ }
484
+ // Only lifecycle identity is needed. Do not retain prompts, outputs, token
485
+ // events, or completed children when upstream omits a cancellation callback.
486
+ _addRunToRunMap(run) {
487
+ return super._addRunToRunMap({
488
+ ...run,
489
+ inputs: {},
490
+ extra: {},
491
+ serialized: {},
492
+ tags: [],
493
+ child_runs: [],
494
+ events: []
495
+ });
496
+ }
497
+ _addChildRun() {
498
+ }
499
+ async _endTrace(run) {
500
+ try {
501
+ await super._endTrace(run);
502
+ } finally {
503
+ run.inputs = {};
504
+ delete run.outputs;
505
+ delete run.error;
506
+ run.extra = {};
507
+ run.events = [];
508
+ run.child_runs = [];
384
509
  }
385
- const lastUser = findLastUserMessage2(flat);
386
- if (!lastUser) {
387
- return;
510
+ }
511
+ async handleText() {
512
+ }
513
+ async handleAgentAction() {
514
+ }
515
+ async handleAgentEnd() {
516
+ }
517
+ async handleLLMNewToken(_token, _idx, runId) {
518
+ return this.getRunById(runId) ?? this._createRunForLLMStart(serialized(void 0, "llm"), [], runId);
519
+ }
520
+ capture(runId, parentRunId, metadata) {
521
+ for (const id of threads.start(runId, parentRunId, metadata)) {
522
+ this.runMap.delete(id);
523
+ this.runTreeMap?.delete(id);
388
524
  }
389
- const text = extractMessageText(lastUser.content).trim();
390
- if (!text) {
391
- return;
525
+ }
526
+ finishAll() {
527
+ for (const id of [...this.runMap.keys()]) this.finishRun(id);
528
+ this.runTreeMap?.clear();
529
+ }
530
+ finishRun(runId) {
531
+ for (const id of threads.finish(runId)) {
532
+ this.runMap.delete(id);
533
+ this.runTreeMap?.delete(id);
392
534
  }
393
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
394
535
  }
395
- async handleLLMStart(_llm, prompts, runId) {
396
- if (!enabledHooks.has(FirewallHook.LLM_START)) {
397
- return;
536
+ _createRunForLLMStart(...args) {
537
+ this.capture(args[2], args[3], args[6]);
538
+ return super._createRunForLLMStart(...args);
539
+ }
540
+ _createRunForChatModelStart(...args) {
541
+ this.capture(args[2], args[3], args[6]);
542
+ return super._createRunForChatModelStart(...args);
543
+ }
544
+ _createRunForChainStart(...args) {
545
+ this.capture(args[2], args[3], args[5]);
546
+ return super._createRunForChainStart(...args);
547
+ }
548
+ _createRunForToolStart(...args) {
549
+ this.capture(args[2], args[3], args[5]);
550
+ return super._createRunForToolStart(...args);
551
+ }
552
+ _createRunForRetrieverStart(...args) {
553
+ this.capture(args[2], args[3], args[5]);
554
+ return super._createRunForRetrieverStart(...args);
555
+ }
556
+ async handleChatModelStart(llm, messages, runId, parentRunId, extraParams, tags, metadata) {
557
+ const batches = messages;
558
+ const run = this.getRunById(runId) ?? this._createRunForChatModelStart(
559
+ serialized(llm, "chat_model"),
560
+ messages,
561
+ runId,
562
+ parentRunId,
563
+ extraParams,
564
+ tags,
565
+ metadata
566
+ );
567
+ if (enabledHooks.has(FirewallHook.CHAT_MODEL_START)) {
568
+ const lastUser = findLastUserMessage2(batches.flat());
569
+ const text = lastUser ? extractMessageText(lastUser.content).trim() : "";
570
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.CHAT_MODEL_START], runId);
398
571
  }
399
- const text = extractTextFromPrompts(prompts);
400
- if (!text) {
401
- return;
572
+ return run;
573
+ }
574
+ async handleLLMStart(llm, prompts, runId, parentRunId, extraParams, tags, metadata) {
575
+ const run = this.getRunById(runId) ?? this._createRunForLLMStart(
576
+ serialized(llm, "llm"),
577
+ prompts,
578
+ runId,
579
+ parentRunId,
580
+ extraParams,
581
+ tags,
582
+ metadata
583
+ );
584
+ if (enabledHooks.has(FirewallHook.LLM_START)) {
585
+ const text = extractTextFromPrompts(prompts);
586
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
587
+ }
588
+ return run;
589
+ }
590
+ async handleToolStart(tool, inputStr, runId, parentRunId, tags, metadata) {
591
+ const run = this.getRunById(runId) ?? this._createRunForToolStart(
592
+ serialized(tool, tool?.name ?? "tool"),
593
+ inputStr,
594
+ runId,
595
+ parentRunId,
596
+ tags,
597
+ metadata
598
+ );
599
+ if (includeTool && enabledHooks.has(FirewallHook.TOOL_START)) {
600
+ const text = extractTextFromToolInput(inputStr);
601
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, tool?.name);
402
602
  }
403
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_START], runId);
603
+ return run;
604
+ }
605
+ async handleRetrieverStart(retriever, query, runId, parentRunId, tags, metadata) {
606
+ const run = this.getRunById(runId) ?? this._createRunForRetrieverStart(
607
+ serialized(retriever, "retriever"),
608
+ query,
609
+ runId,
610
+ parentRunId,
611
+ tags,
612
+ metadata
613
+ );
614
+ if (enabledHooks.has(FirewallHook.RETRIEVER_START) && query.trim()) {
615
+ await classify(query.trim(), FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
616
+ }
617
+ return run;
404
618
  }
405
- async handleToolStart(tool, inputStr, runId) {
406
- if (!includeTool || !enabledHooks.has(FirewallHook.TOOL_START)) {
407
- return;
619
+ async handleLLMEnd(output, runId) {
620
+ try {
621
+ if (!this.getRunById(runId)) this._createRunForLLMStart(serialized(void 0, "llm"), [], runId);
622
+ if (enabledHooks.has(FirewallHook.LLM_END)) {
623
+ const text = extractTextFromLLMResult(output);
624
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
625
+ }
626
+ return await super.handleLLMEnd(output, runId);
627
+ } finally {
628
+ finishRun(runId);
408
629
  }
409
- const text = extractTextFromToolInput(inputStr);
410
- if (!text) {
411
- return;
630
+ }
631
+ async handleToolEnd(output, runId, _parentRunId, _tags, kwargs) {
632
+ try {
633
+ if (!this.getRunById(runId)) this._createRunForToolStart(serialized(void 0, "tool"), "", runId);
634
+ if (includeTool && enabledHooks.has(FirewallHook.TOOL_END)) {
635
+ const text = String(output).trim();
636
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, kwargs?.name);
637
+ }
638
+ return await super.handleToolEnd(output, runId);
639
+ } finally {
640
+ finishRun(runId);
412
641
  }
413
- const toolName = tool?.name;
414
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_START], runId, toolName);
415
642
  }
416
- async handleRetrieverStart(_retriever, query, runId) {
417
- if (!enabledHooks.has(FirewallHook.RETRIEVER_START)) {
418
- return;
643
+ async handleRetrieverEnd(documents, runId) {
644
+ try {
645
+ if (!this.getRunById(runId)) this._createRunForRetrieverStart(serialized(void 0, "retriever"), "", runId);
646
+ if (enabledHooks.has(FirewallHook.RETRIEVER_END)) {
647
+ const text = extractTextFromDocuments(documents);
648
+ if (text) await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
649
+ }
650
+ return await super.handleRetrieverEnd(documents, runId);
651
+ } finally {
652
+ finishRun(runId);
419
653
  }
420
- const text = query.trim();
421
- if (!text) {
422
- return;
654
+ }
655
+ async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, name) {
656
+ return super.handleChainStart(serialized(chain, "chain"), inputs, runId, parentRunId, tags, metadata, runType, name);
657
+ }
658
+ async handleChainEnd(...args) {
659
+ if (!this.getRunById(args[1])) this._createRunForChainStart(serialized(void 0, "chain"), {}, args[1]);
660
+ try {
661
+ return await super.handleChainEnd(...args);
662
+ } finally {
663
+ finishRun(args[1]);
423
664
  }
424
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_START], runId);
425
665
  }
426
- async handleLLMEnd(output, runId) {
427
- if (!enabledHooks.has(FirewallHook.LLM_END)) {
428
- return;
666
+ async handleChainError(...args) {
667
+ if (!this.getRunById(args[1])) this._createRunForChainStart(serialized(void 0, "chain"), {}, args[1]);
668
+ try {
669
+ return await super.handleChainError(...args);
670
+ } finally {
671
+ finishRun(args[1]);
429
672
  }
430
- const text = extractTextFromLLMResult(output);
431
- if (!text) {
432
- return;
673
+ }
674
+ async handleLLMError(...args) {
675
+ if (!this.getRunById(args[1])) this._createRunForLLMStart(serialized(void 0, "llm"), [], args[1]);
676
+ try {
677
+ return await super.handleLLMError(...args);
678
+ } finally {
679
+ finishRun(args[1]);
433
680
  }
434
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.LLM_END], runId);
435
681
  }
436
- async handleToolEnd(output, runId, _parentRunId, _tags, _kwargs) {
437
- if (!includeTool || !enabledHooks.has(FirewallHook.TOOL_END)) {
438
- return;
682
+ async handleToolError(...args) {
683
+ if (!this.getRunById(args[1])) this._createRunForToolStart(serialized(void 0, "tool"), "", args[1]);
684
+ try {
685
+ return await super.handleToolError(...args);
686
+ } finally {
687
+ finishRun(args[1]);
439
688
  }
440
- const text = String(output).trim();
441
- if (!text) {
442
- return;
689
+ }
690
+ async handleRetrieverError(...args) {
691
+ if (!this.getRunById(args[1])) this._createRunForRetrieverStart(serialized(void 0, "retriever"), "", args[1]);
692
+ try {
693
+ return await super.handleRetrieverError(...args);
694
+ } finally {
695
+ finishRun(args[1]);
443
696
  }
444
- const toolName = _kwargs?.name;
445
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.TOOL_END], runId, toolName);
446
697
  }
447
- async handleRetrieverEnd(documents, runId) {
448
- if (!enabledHooks.has(FirewallHook.RETRIEVER_END)) {
449
- return;
698
+ }
699
+ const handler = new SilmarilFirewallHandler();
700
+ finishRun = (runId) => {
701
+ handler.finishRun(runId);
702
+ };
703
+ handlerCleanups.set(handler, () => handler.finishAll());
704
+ return handler;
705
+ }
706
+ async function protectLangChainRunnable(firewall, runnable, options = {}) {
707
+ rejectConversationOptions(options);
708
+ const { RunnableBinding, RunnableRetry, ensureConfig, mergeConfigs } = await import("@langchain/core/runnables");
709
+ const { IterableReadableStream } = await import("@langchain/core/utils/stream");
710
+ const configFactories = [async (config) => {
711
+ const effective = ensureConfig(config);
712
+ const handler = await buildLangChainHandler(firewall, options, {
713
+ threadId: validThreadId(effective.configurable?.thread_id)
714
+ });
715
+ return { callbacks: [handler] };
716
+ }];
717
+ class ProtectedBinding extends RunnableBinding {
718
+ async scopedConfig(...configs) {
719
+ const created = [];
720
+ const scopedFactories = configFactories.map((factory) => async (config2) => {
721
+ const update = await factory(config2);
722
+ if (Array.isArray(update.callbacks)) created.push(...update.callbacks);
723
+ return update;
724
+ });
725
+ const scoped = new RunnableBinding({ bound: this.bound, config: this.config, configFactories: scopedFactories });
726
+ const config = await scoped._mergeConfig(...configs);
727
+ return { config, dispose: () => {
728
+ for (const handler of created) handlerCleanups.get(handler)?.();
729
+ } };
730
+ }
731
+ async invoke(input, options2) {
732
+ const scoped = await this.scopedConfig(options2, this.kwargs);
733
+ try {
734
+ return await this.bound.invoke(input, scoped.config);
735
+ } finally {
736
+ scoped.dispose();
450
737
  }
451
- const text = extractTextFromDocuments(documents);
452
- if (!text) {
453
- return;
738
+ }
739
+ async stream(input, options2) {
740
+ const scoped = await this.scopedConfig(ensureConfig(options2), this.kwargs);
741
+ let upstream;
742
+ try {
743
+ upstream = await this.bound.stream(input, scoped.config);
744
+ } catch (error) {
745
+ scoped.dispose();
746
+ throw error;
454
747
  }
455
- await classify(text, FIREWALL_HOOK_TO_LABEL[FirewallHook.RETRIEVER_END], runId);
748
+ const managed = async function* () {
749
+ try {
750
+ yield* upstream;
751
+ } finally {
752
+ scoped.dispose();
753
+ }
754
+ };
755
+ return IterableReadableStream.fromAsyncGenerator(managed());
756
+ }
757
+ async *_streamIterator(input, options2) {
758
+ const stream = await this.stream(input, options2);
759
+ yield* stream;
760
+ }
761
+ withConfig(config) {
762
+ return new ProtectedBinding({ bound: this.bound, config: mergeConfigs(this.config, config), configFactories });
763
+ }
764
+ withListeners(fields) {
765
+ return new RunnableBinding({ bound: this, config: {} }).withListeners(fields);
766
+ }
767
+ withRetry(fields) {
768
+ return new RunnableRetry({
769
+ bound: this,
770
+ config: {},
771
+ ...fields?.stopAfterAttempt === void 0 ? {} : { maxAttemptNumber: fields.stopAfterAttempt },
772
+ ...fields?.onFailedAttempt === void 0 ? {} : { onFailedAttempt: fields.onFailedAttempt }
773
+ });
456
774
  }
457
775
  }
458
- return new SilmarilFirewallHandler();
776
+ return new ProtectedBinding({ bound: runnable, config: {}, configFactories });
459
777
  }
460
- var USER_ROLES;
778
+ var handlerCleanups, USER_ROLES;
461
779
  var init_langchain = __esm({
462
780
  "src/adapters/langchain.ts"() {
463
781
  "use strict";
@@ -465,6 +783,8 @@ var init_langchain = __esm({
465
783
  init_hooks();
466
784
  init_modes();
467
785
  init_extract();
786
+ init_thread_identity();
787
+ handlerCleanups = /* @__PURE__ */ new WeakMap();
468
788
  USER_ROLES = /* @__PURE__ */ new Set(["human", "user"]);
469
789
  }
470
790
  });
@@ -831,7 +1151,7 @@ function sanitizeText(text) {
831
1151
  }
832
1152
 
833
1153
  // src/firewall.ts
834
- var SDK_VERSION = "0.7.10";
1154
+ var SDK_VERSION = "0.8.0";
835
1155
  var DEFAULT_TIMEOUT_MS = 1e4;
836
1156
  var DEFAULT_MAX_RETRIES = 5;
837
1157
  var MAX_BACKOFF_SECONDS = 30;