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