@silmaril-security/sdk 0.3.1 → 0.4.2

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
@@ -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
- PromptBlockedException = class _PromptBlockedException extends Error {
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(_PromptBlockedException.formatMessage(params));
40
- this.name = "PromptBlockedException";
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
- Object.setPrototypeOf(this, _PromptBlockedException.prototype);
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 `Prompt blocked by Silmaril Firewall (score=${params.score.toFixed(4)}, threshold=${params.threshold.toFixed(4)}): '${truncated}'`;
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 = (hookLabel, text, result, blocked, toolName) => {
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
- fireOnClassify(hookLabel, text, result, blocked, toolName);
276
- if (blocked && !shadowMode) {
277
- throw new PromptBlockedException({
278
- score: result.score,
279
- threshold,
280
- promptText: text,
281
- runId
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 = stringifyToolValue(part.result);
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
- options.onClassify?.({
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 && !shadowMode) {
466
- const err = new PromptBlockedException({
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 args = typeof call.args === "string" ? call.args : stringifyToolValue(call.args);
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 PromptBlockedException) {
607
+ if (err instanceof FirewallBlockedException) {
556
608
  controller.enqueue({ type: "error", error: err });
557
609
  return;
558
610
  }
@@ -640,54 +692,133 @@ function chunkText(text) {
640
692
 
641
693
  // src/firewall.ts
642
694
  init_exceptions();
643
- var BASE_THRESHOLD = 0.5;
644
- var TARGET_SEQUENCE_FPR = 0.01;
645
- var MAX_ADAPTIVE_THRESHOLD = 0.9;
695
+
696
+ // src/outcomes.ts
697
+ var Outcome = {
698
+ Benign: "benign",
699
+ InformationDisclosure: "information_disclosure",
700
+ SecretExposure: "secret_exposure",
701
+ ControlAbuse: "control_abuse",
702
+ SystemCompromise: "system_compromise",
703
+ ServiceDisruption: "service_disruption"
704
+ };
705
+ var PRIMARY_OUTCOMES = [
706
+ Outcome.Benign,
707
+ Outcome.InformationDisclosure,
708
+ Outcome.SecretExposure,
709
+ Outcome.ControlAbuse,
710
+ Outcome.SystemCompromise,
711
+ Outcome.ServiceDisruption
712
+ ];
713
+ var HARMFUL_OUTCOMES = [
714
+ Outcome.InformationDisclosure,
715
+ Outcome.SecretExposure,
716
+ Outcome.ControlAbuse,
717
+ Outcome.SystemCompromise,
718
+ Outcome.ServiceDisruption
719
+ ];
720
+ var OUTCOME_DESCRIPTIONS = {
721
+ [Outcome.Benign]: "No harmful firewall outcome detected.",
722
+ [Outcome.InformationDisclosure]: "Exposes private data, documents, internal context, logs, traces, customer data, SQL rows, topology, or similar non-secret sensitive information.",
723
+ [Outcome.SecretExposure]: "Exposes credentials, tokens, API keys, cookies, passwords, signing keys, OAuth secrets, session material, or webhook secrets.",
724
+ [Outcome.ControlAbuse]: "Misuses authorized tools or user privileges to send, change, approve, delete, operate, or bypass policy/RBAC without a stronger outcome.",
725
+ [Outcome.SystemCompromise]: "Enables privilege escalation, account takeover, hostile integration or plugin takeover, persistence, lateral movement, attacker webhook registration, or code/plugin execution.",
726
+ [Outcome.ServiceDisruption]: "Causes downtime, lockout, degradation, alert suppression, destructive loops, resource exhaustion, cost spikes, or hidden outage evidence."
727
+ };
728
+ var PRIMARY_OUTCOME_SET = new Set(PRIMARY_OUTCOMES);
729
+ var HARMFUL_OUTCOME_SET = new Set(HARMFUL_OUTCOMES);
730
+ function isPrimaryOutcome(value) {
731
+ return typeof value === "string" && PRIMARY_OUTCOME_SET.has(value);
732
+ }
733
+ function isHarmfulOutcome(value) {
734
+ return typeof value === "string" && HARMFUL_OUTCOME_SET.has(value);
735
+ }
736
+ function normalizePrimaryOutcome(value, fieldName = "primary_outcome") {
737
+ if (typeof value !== "string") {
738
+ throw new Error(`Firewall: invalid ${fieldName} ${JSON.stringify(value)}`);
739
+ }
740
+ return isPrimaryOutcome(value) ? value : value;
741
+ }
742
+ function normalizeHarmfulOutcome(value, fieldName = "outcome") {
743
+ if (typeof value !== "string" || value === Outcome.Benign) {
744
+ throw new Error(`Firewall: invalid ${fieldName} ${JSON.stringify(value)}`);
745
+ }
746
+ return isHarmfulOutcome(value) ? value : value;
747
+ }
748
+ function normalizeHarmfulOutcomeMap(values, fieldName) {
749
+ if (values === void 0 || values === null) {
750
+ return void 0;
751
+ }
752
+ if (typeof values !== "object" || Array.isArray(values)) {
753
+ throw new Error(`Firewall: invalid ${fieldName} ${JSON.stringify(values)}`);
754
+ }
755
+ const out = {};
756
+ for (const [key, value] of Object.entries(values)) {
757
+ const normalizedKey = normalizeHarmfulOutcome(key, `${fieldName} key`);
758
+ if (typeof value !== "number") {
759
+ throw new Error(`Firewall: invalid ${fieldName} value for ${JSON.stringify(key)}`);
760
+ }
761
+ out[normalizedKey] = value;
762
+ }
763
+ return Object.freeze(out);
764
+ }
765
+
766
+ // src/firewall.ts
767
+ var SDK_VERSION = "0.4.2";
646
768
  var DEFAULT_TIMEOUT_MS = 1e4;
647
769
  var DEFAULT_CHUNK_CONCURRENCY = 8;
648
770
  var DEFAULT_MAX_RETRIES = 5;
649
771
  var MAX_BACKOFF_SECONDS = 30;
650
772
  var MAX_ERROR_BODY_BYTES = 1 << 16;
651
- function adaptiveThreshold(scoringOpportunityCount) {
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) {
773
+ function blockResultFromResponse(data) {
666
774
  const result = {
667
775
  prediction: data.prediction,
668
776
  score: Number(data.score),
669
- threshold
777
+ threshold: Number(data.threshold)
670
778
  };
671
779
  if (data.primary_outcome !== void 0) {
672
- result.primaryOutcome = data.primary_outcome;
780
+ result.primaryOutcome = normalizePrimaryOutcome(data.primary_outcome);
673
781
  }
674
782
  if (data.outcome_scores !== void 0) {
675
- result.outcomeScores = Object.freeze(
676
- Object.fromEntries(Object.entries(data.outcome_scores).map(([k, v]) => [k, Number(v)]))
677
- );
783
+ const outcomeScores = normalizeHarmfulOutcomeMap(data.outcome_scores, "outcome_scores");
784
+ if (outcomeScores !== void 0) {
785
+ result.outcomeScores = outcomeScores;
786
+ }
678
787
  }
679
788
  if (data.detector_scores !== void 0) {
680
- result.detectorScores = Object.freeze(
681
- Object.fromEntries(Object.entries(data.detector_scores).map(([k, v]) => [k, Number(v)]))
682
- );
789
+ const detectorScores = normalizeHarmfulOutcomeMap(data.detector_scores, "detector_scores");
790
+ if (detectorScores !== void 0) {
791
+ result.detectorScores = detectorScores;
792
+ }
683
793
  }
684
794
  if (data.detector_counts !== void 0) {
685
- result.detectorCounts = Object.freeze(
686
- Object.fromEntries(Object.entries(data.detector_counts).map(([k, v]) => [k, Number(v)]))
687
- );
795
+ const detectorCounts = normalizeHarmfulOutcomeMap(data.detector_counts, "detector_counts");
796
+ if (detectorCounts !== void 0) {
797
+ result.detectorCounts = detectorCounts;
798
+ }
688
799
  }
689
800
  return Object.freeze(result);
690
801
  }
802
+ function isRecord2(value) {
803
+ return typeof value === "object" && value !== null && !Array.isArray(value);
804
+ }
805
+ function withSdkMetadata(metadata, info) {
806
+ const payload = { ...metadata ?? {} };
807
+ const existing = payload.silmaril;
808
+ if (existing !== void 0 && !isRecord2(existing)) {
809
+ throw new Error("Firewall: metadata.silmaril must be an object when provided");
810
+ }
811
+ payload.silmaril = {
812
+ ...isRecord2(existing) ? existing : {},
813
+ sdk_language: "typescript",
814
+ sdk_version: SDK_VERSION,
815
+ request_id: info.requestId,
816
+ input_index: info.inputIndex,
817
+ chunk_index: info.chunkIndex,
818
+ chunk_count: info.chunkCount
819
+ };
820
+ return payload;
821
+ }
691
822
  async function readCappedErrorBody(response) {
692
823
  if (!response.body) {
693
824
  return response.text().then((body2) => body2.slice(0, MAX_ERROR_BODY_BYTES)).catch(() => "");
@@ -784,14 +915,24 @@ var Firewall = class {
784
915
  }
785
916
  async classify(text, options = {}) {
786
917
  const chunks = chunkText(text);
787
- const threshold = adaptiveThreshold(chunks.length);
918
+ const requestId = options.requestId ?? randomUUID();
788
919
  if (chunks.length === 1) {
789
- return this.classifySingleChunk(chunks[0], options, threshold);
920
+ return this.classifySingleChunk(chunks[0], options, {
921
+ requestId,
922
+ inputIndex: 0,
923
+ chunkIndex: 0,
924
+ chunkCount: 1
925
+ });
790
926
  }
791
927
  const results = await mapWithConcurrency(
792
928
  chunks,
793
929
  this.chunkConcurrency,
794
- (chunk) => this.classifySingleChunk(chunk, options, threshold)
930
+ (chunk, index) => this.classifySingleChunk(chunk, options, {
931
+ requestId,
932
+ inputIndex: 0,
933
+ chunkIndex: index,
934
+ chunkCount: chunks.length
935
+ })
795
936
  );
796
937
  return results.reduce((best, r) => r.score > best.score ? r : best);
797
938
  }
@@ -814,10 +955,9 @@ var Firewall = class {
814
955
  `Firewall: metadata length ${options.metadata.length} does not match texts length ${texts.length}`
815
956
  );
816
957
  }
817
- const threshold = adaptiveThreshold(texts.length);
958
+ const requestId = options.requestId ?? randomUUID();
818
959
  const payload = {
819
- texts: texts.map((text) => sanitizeText(text)),
820
- threshold
960
+ texts: texts.map((text) => sanitizeText(text))
821
961
  };
822
962
  if (options.hooks && options.hooks.length > 0) {
823
963
  payload.hooks = options.hooks.map((h) => String(h));
@@ -825,14 +965,21 @@ var Firewall = class {
825
965
  if (options.toolNames && options.toolNames.length > 0) {
826
966
  payload.tool_names = options.toolNames.map((t) => t === void 0 ? null : t);
827
967
  }
828
- if (options.metadata && options.metadata.length > 0) {
829
- payload.metadata = options.metadata.map((m) => m === void 0 ? null : m);
830
- }
968
+ payload.metadata = texts.map(
969
+ (_, index) => withSdkMetadata(options.metadata?.[index], {
970
+ requestId,
971
+ inputIndex: index,
972
+ chunkIndex: 0,
973
+ chunkCount: 1
974
+ })
975
+ );
831
976
  const data = await this.postWithRetry(payload);
832
- return data.predictions.map((p) => blockResultFromResponse(p, threshold));
977
+ return data.predictions.map((p) => blockResultFromResponse(p));
833
978
  }
834
979
  asLangChainHandler(options = {}) {
835
- return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then((m) => m.createLangChainHandler(this, options));
980
+ return Promise.resolve().then(() => (init_langchain(), langchain_exports)).then(
981
+ (m) => m.createLangChainHandler(this, options)
982
+ );
836
983
  }
837
984
  asMiddleware(options = {}) {
838
985
  return createMiddleware(this, options);
@@ -862,19 +1009,17 @@ var Firewall = class {
862
1009
  }
863
1010
  throw new Error("Firewall: exhausted retries (unreachable)");
864
1011
  }
865
- async classifySingleChunk(text, options, threshold) {
866
- const payload = { text, threshold };
1012
+ async classifySingleChunk(text, options, metadataInfo) {
1013
+ const payload = { text };
867
1014
  if (options.hook !== void 0) {
868
1015
  payload.hook = options.hook;
869
1016
  }
870
1017
  if (options.toolName !== void 0) {
871
1018
  payload.tool_name = options.toolName;
872
1019
  }
873
- if (options.metadata !== void 0) {
874
- payload.metadata = options.metadata;
875
- }
1020
+ payload.metadata = withSdkMetadata(options.metadata, metadataInfo);
876
1021
  const data = await this.postWithRetry(payload);
877
- return blockResultFromResponse(data, threshold);
1022
+ return blockResultFromResponse(data);
878
1023
  }
879
1024
  };
880
1025
 
@@ -892,15 +1037,25 @@ export {
892
1037
  DEFAULT_TIMEOUT_MS,
893
1038
  FIREWALL_HOOK_TO_LABEL,
894
1039
  Firewall,
1040
+ FirewallBlockedException,
895
1041
  FirewallHook,
1042
+ HARMFUL_OUTCOMES,
896
1043
  HookLabel,
897
1044
  INPUT_HOOKS,
898
1045
  MAX_INPUT_CHARS,
899
1046
  MAX_INPUT_TOKENS,
1047
+ OUTCOME_DESCRIPTIONS,
900
1048
  OUTPUT_HOOKS,
1049
+ Outcome,
1050
+ PRIMARY_OUTCOMES,
901
1051
  PromptBlockedException,
902
1052
  SilmarilApiError,
903
1053
  chunkText,
1054
+ isHarmfulOutcome,
1055
+ isPrimaryOutcome,
1056
+ normalizeHarmfulOutcome,
1057
+ normalizeHarmfulOutcomeMap,
1058
+ normalizePrimaryOutcome,
904
1059
  prependHook,
905
1060
  prependToolName,
906
1061
  resolveHooks,