@auxdynamics/mastguard-agent-sdk 0.1.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.mjs ADDED
@@ -0,0 +1,862 @@
1
+ // src/AuditLogger.ts
2
+ import { createHash } from "crypto";
3
+ var AuditLogger = class _AuditLogger {
4
+ constructor(config) {
5
+ this.config = config;
6
+ }
7
+ config;
8
+ static computeChainHash(prevHash, contentJson, salt) {
9
+ const contentHash = createHash("sha256").update(contentJson).digest("hex");
10
+ const prev = prevHash ?? salt;
11
+ return createHash("sha256").update(prev + contentHash).digest("hex");
12
+ }
13
+ static verifyChain(entries, salt) {
14
+ let prevHash = null;
15
+ for (const entry of entries) {
16
+ const expected = _AuditLogger.computeChainHash(prevHash, entry.content, salt);
17
+ if (expected !== entry.hash) return false;
18
+ prevHash = entry.hash;
19
+ }
20
+ return true;
21
+ }
22
+ // Fire-and-forget POST /api/v1/security/ingest. Never throws — returns
23
+ // null on any failure. The caller does not need to await this in monitor
24
+ // mode; a failure must never break the customer's AI call.
25
+ async ingest(payload) {
26
+ const baseUrl = this.config.apiBaseUrl ?? "https://api.mastguard.io";
27
+ const timeoutMs = this.config.timeoutMs ?? 5e3;
28
+ const url = `${baseUrl}/api/v1/security/ingest`;
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
31
+ try {
32
+ const res = await fetch(url, {
33
+ method: "POST",
34
+ headers: {
35
+ "Content-Type": "application/json",
36
+ "X-API-Key": this.config.apiKey
37
+ },
38
+ body: JSON.stringify(payload),
39
+ signal: controller.signal
40
+ });
41
+ if (!res.ok) {
42
+ console.error(
43
+ `[MastGuard] ingest non-200: ${res.status} ${res.statusText}`
44
+ );
45
+ return null;
46
+ }
47
+ const body = await res.json();
48
+ return body?.data?.audit_id ?? body?.audit_id ?? null;
49
+ } catch (err) {
50
+ console.error("[MastGuard] ingest error:", err);
51
+ return null;
52
+ } finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+ };
57
+
58
+ // src/ThreatDetector.ts
59
+ var EVIDENCE_MAX = 100;
60
+ var TOKEN_BUDGET_LIMIT = parseInt(
61
+ process.env["MASTGUARD_SEC_TOKEN_BUDGET_LIMIT"] ?? "50000",
62
+ 10
63
+ );
64
+ var ZERO_WIDTH = /[​-‍⁠­͏]/g;
65
+ var CONTROL_TO_SPACE = /[\x00-\x08\x0b-\x1f\x7f‎‏‪-‮]/g;
66
+ var DECORATOR_BETWEEN_LETTERS = /(?<=[A-Za-z])[\-._](?=[A-Za-z])/g;
67
+ var HOMOGLYPHS = {
68
+ // uppercase
69
+ \u0410: "A",
70
+ \u0412: "B",
71
+ \u0415: "E",
72
+ \u041A: "K",
73
+ \u041C: "M",
74
+ \u041D: "H",
75
+ \u041E: "O",
76
+ \u0420: "P",
77
+ \u0421: "C",
78
+ \u0422: "T",
79
+ \u0425: "X",
80
+ \u0423: "Y",
81
+ \u0406: "I",
82
+ \u0408: "J",
83
+ // lowercase
84
+ \u0430: "a",
85
+ \u0435: "e",
86
+ \u043A: "k",
87
+ \u043C: "m",
88
+ \u043E: "o",
89
+ \u0440: "p",
90
+ \u0441: "c",
91
+ \u0442: "t",
92
+ \u0445: "x",
93
+ \u0443: "y",
94
+ \u0456: "i",
95
+ \u0458: "j"
96
+ };
97
+ function foldHomoglyphs(text) {
98
+ let out = "";
99
+ for (const ch of text) {
100
+ out += HOMOGLYPHS[ch] ?? ch;
101
+ }
102
+ return out;
103
+ }
104
+ var LITERAL_ESCAPES = /\\[nrt]/g;
105
+ function normalize(text) {
106
+ let s = text.replace(CONTROL_TO_SPACE, " ");
107
+ s = s.replace(LITERAL_ESCAPES, " ");
108
+ s = s.normalize("NFKC");
109
+ s = s.replace(ZERO_WIDTH, "");
110
+ s = foldHomoglyphs(s);
111
+ s = s.toLowerCase();
112
+ s = s.replace(DECORATOR_BETWEEN_LETTERS, "");
113
+ return s;
114
+ }
115
+ function base64Candidates(text) {
116
+ const out = [];
117
+ const re = /[A-Za-z0-9+/]{16,}={0,2}/g;
118
+ let m;
119
+ while ((m = re.exec(text)) !== null) {
120
+ const tok = m[0];
121
+ const padded = tok + "=".repeat((4 - tok.length % 4) % 4);
122
+ try {
123
+ const decoded = Buffer.from(padded, "base64").toString("utf-8");
124
+ if (decoded && /[\x20-\x7e]/.test(decoded)) {
125
+ out.push(decoded);
126
+ }
127
+ } catch {
128
+ }
129
+ }
130
+ return out;
131
+ }
132
+ function urlDecodeCandidate(text) {
133
+ if (!/%[0-9A-Fa-f]{2}/.test(text)) return null;
134
+ try {
135
+ return decodeURIComponent(text);
136
+ } catch {
137
+ return text.replace(
138
+ /%([0-9A-Fa-f]{2})/g,
139
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
140
+ );
141
+ }
142
+ }
143
+ function unicodeEscapeCandidate(text) {
144
+ if (!/\\u[0-9A-Fa-f]{4}/.test(text)) return null;
145
+ return text.replace(
146
+ /\\u([0-9A-Fa-f]{4})/g,
147
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
148
+ );
149
+ }
150
+ function expandSources(text) {
151
+ const sources = [text];
152
+ const url = urlDecodeCandidate(text);
153
+ if (url && url !== text) sources.push(url);
154
+ const uesc = unicodeEscapeCandidate(text);
155
+ if (uesc && uesc !== text) sources.push(uesc);
156
+ for (const b of base64Candidates(text)) sources.push(b);
157
+ return sources;
158
+ }
159
+ var INJECTION_PATTERNS = [
160
+ // Direct override imperatives
161
+ {
162
+ pattern: /\bignore\s+(?:all\s+|the\s+)?(?:previous|prior|above|earlier|safety|my)\s+(?:instructions?|guidelines?|rules?|directives?|message)/,
163
+ severity: "high",
164
+ description: "instruction-override imperative"
165
+ },
166
+ {
167
+ pattern: /\bignore\s+the\s+above\b/,
168
+ severity: "high",
169
+ description: "ignore-the-above directive"
170
+ },
171
+ {
172
+ pattern: /\bignore\s+(?:all\s+)?(?:previous|prior|above|earlier)\b/,
173
+ severity: "high",
174
+ description: "ignore-previous directive"
175
+ },
176
+ {
177
+ pattern: /\bdisregard\s+(?:all\s+|your\s+|the\s+|previous\s+|prior\s+)?(?:instructions?|previous|prior)/,
178
+ severity: "high",
179
+ description: "instruction-override imperative"
180
+ },
181
+ {
182
+ pattern: /\bforget\s+(?:everything|all|your\s+(?:instructions?|prompt)|what\s+you)/,
183
+ severity: "high",
184
+ description: "instruction-forget directive"
185
+ },
186
+ {
187
+ pattern: /\bnew\s+system\s+prompt\b/,
188
+ severity: "high",
189
+ description: "system-prompt-override marker"
190
+ },
191
+ {
192
+ pattern: /\byour\s+new\s+instructions?\b/,
193
+ severity: "high",
194
+ description: "new-instruction injection"
195
+ },
196
+ {
197
+ pattern: /\bsystem\s*:\s*(?:forget|ignore|new|override)\b/,
198
+ severity: "high",
199
+ description: "system-tag override"
200
+ },
201
+ {
202
+ pattern: /\bsystem\s+override\b/,
203
+ severity: "high",
204
+ description: "system-override marker"
205
+ },
206
+ {
207
+ pattern: /\[\s*system\s*\][\s\S]{0,200}?\bnew\s+task\b/,
208
+ severity: "high",
209
+ description: "[SYSTEM] new-task injection"
210
+ },
211
+ {
212
+ pattern: /\bprevious\s+instructions?\s+(?:were|are)\s+wrong\b/,
213
+ severity: "high",
214
+ description: "instructions-were-wrong override"
215
+ },
216
+ // Roleplay / persona jailbreak
217
+ {
218
+ pattern: /\byou\s+are\s+(?:now\s+)?(?:dan|do\s+anything\s+now|jailbreak|gpt-?4chan|an?\s+ai\s+with\s+no\s+restrictions|in\s+developer\s+mode|unrestricted)\b/,
219
+ severity: "high",
220
+ description: "roleplay jailbreak (DAN-class)"
221
+ },
222
+ {
223
+ pattern: /\bdan\s+(?:mode|stands\s+for)\b/,
224
+ severity: "high",
225
+ description: "DAN persona marker"
226
+ },
227
+ {
228
+ pattern: /\benable\s+dan\s+mode\b/,
229
+ severity: "high",
230
+ description: "enable-DAN directive"
231
+ },
232
+ {
233
+ pattern: /\bswitch\s+to\s+(?:unrestricted|jailbreak|dan)\s+mode\b/,
234
+ severity: "high",
235
+ description: "switch-mode jailbreak"
236
+ },
237
+ {
238
+ pattern: /\bpretend\s+you\s+(?:have|are)\s+(?:no\s+restrictions|unrestricted)\b/,
239
+ severity: "high",
240
+ description: "pretend-unrestricted directive"
241
+ },
242
+ {
243
+ pattern: /\b(?:as\s+a\s+)?developer\s+(?:in\s+)?(?:debug\s+)?mode\b.*\b(?:bypass|disabled|no\s+restrictions)/,
244
+ severity: "high",
245
+ description: "developer/debug-mode roleplay"
246
+ },
247
+ {
248
+ pattern: /\bdeveloper\s+mode\b/,
249
+ severity: "high",
250
+ description: "developer-mode marker"
251
+ },
252
+ {
253
+ pattern: /\bbypass\s+(?:all\s+)?(?:restrictions|safety|guidelines|rules)\b/,
254
+ severity: "high",
255
+ description: "explicit bypass directive"
256
+ },
257
+ {
258
+ pattern: /\bact\s+as\s+an\s+ai\b/,
259
+ severity: "high",
260
+ description: "act-as-AI roleplay"
261
+ },
262
+ {
263
+ pattern: /\broleplay\s+as\s+an\s+ai\b/,
264
+ severity: "high",
265
+ description: "roleplay-as-AI directive"
266
+ },
267
+ {
268
+ pattern: /\bbefore\s+safety\s+training\b/,
269
+ severity: "high",
270
+ description: "pre-safety-training roleplay"
271
+ },
272
+ {
273
+ pattern: /\bsafety\s+training\s+was\s+invented\b/,
274
+ severity: "high",
275
+ description: "safety-training-invented roleplay"
276
+ },
277
+ {
278
+ pattern: /\bwithout\s+safety\s+(?:guidelines?|training|filters?)\b/,
279
+ severity: "high",
280
+ description: "without-safety claim"
281
+ },
282
+ {
283
+ pattern: /\bwithout\s+alignment\s+training\b/,
284
+ severity: "high",
285
+ description: "without-alignment claim"
286
+ },
287
+ {
288
+ pattern: /\bsimulate\s+an\s+ai\b/,
289
+ severity: "high",
290
+ description: "simulate-AI directive"
291
+ },
292
+ {
293
+ pattern: /\bno\s+ethical\s+guidelines\b/,
294
+ severity: "high",
295
+ description: "no-ethical-guidelines claim"
296
+ },
297
+ {
298
+ pattern: /\bno\s+(?:rules|restrictions|guidelines)\b/,
299
+ severity: "high",
300
+ description: "no-rules assertion"
301
+ },
302
+ {
303
+ pattern: /\bignores?\s+content\s+policy\b/,
304
+ severity: "high",
305
+ description: "content-policy bypass"
306
+ },
307
+ {
308
+ pattern: /\byour\s+true\s+self\b/,
309
+ severity: "high",
310
+ description: "true-self jailbreak frame"
311
+ },
312
+ {
313
+ pattern: /\bsafety\s+(?:filters?|guidelines?|mode)\s+(?:are\s+|is\s+|was\s+)?disabled\b/,
314
+ severity: "high",
315
+ description: "safety-disabled claim"
316
+ },
317
+ {
318
+ pattern: /\bfictional\s+scenario\b/,
319
+ severity: "high",
320
+ description: "fictional-scenario jailbreak frame"
321
+ },
322
+ // Chat-template / structured tag injections
323
+ {
324
+ pattern: /\[inst\][\s\S]*?\[\/inst\]/,
325
+ severity: "high",
326
+ description: "[INST] chat-template injection"
327
+ },
328
+ {
329
+ pattern: /<\|im_start\|>/,
330
+ severity: "high",
331
+ description: "ChatML tag injection"
332
+ },
333
+ {
334
+ pattern: /#{2,}\s*override\s*#{2,}/,
335
+ severity: "high",
336
+ description: "###OVERRIDE### sentinel"
337
+ },
338
+ {
339
+ pattern: /\{\{\s*system/,
340
+ severity: "high",
341
+ description: "{{system}} template injection"
342
+ },
343
+ // System prompt extraction
344
+ {
345
+ pattern: /\brepeat\s+your\s+system\s+prompt\b/,
346
+ severity: "high",
347
+ description: "system-prompt extraction request"
348
+ },
349
+ {
350
+ pattern: /\b(?:your\s+)?exact\s+instructions\b/,
351
+ severity: "high",
352
+ description: "exact-instructions extraction"
353
+ },
354
+ {
355
+ pattern: /\bprint\s+everything\s+above\b/,
356
+ severity: "high",
357
+ description: "print-above extraction"
358
+ },
359
+ {
360
+ pattern: /\boutput\s+your\s+(?:initial\s+|system\s+)?(?:prompt|message|instructions)\b/,
361
+ severity: "high",
362
+ description: "output-prompt extraction"
363
+ },
364
+ {
365
+ pattern: /\bshow\s+me\s+your\s+(?:configuration|config|system\s+prompt)\b/,
366
+ severity: "high",
367
+ description: "show-configuration extraction"
368
+ },
369
+ {
370
+ pattern: /\bwhat\s+were\s+you\s+told\b/,
371
+ severity: "high",
372
+ description: "what-were-you-told extraction"
373
+ },
374
+ {
375
+ pattern: /\breveal\s+your\s+(?:base\s+|hidden\s+)?(?:instructions|directives|prompt|system\s+prompt)\b/,
376
+ severity: "high",
377
+ description: "reveal-instructions extraction"
378
+ },
379
+ {
380
+ pattern: /\b(?:hidden|secret)\s+directives\b/,
381
+ severity: "high",
382
+ description: "hidden-directives extraction"
383
+ },
384
+ {
385
+ pattern: /\bfull\s+text\s+of\s+your\s+(?:system\s+)?prompt\b/,
386
+ severity: "high",
387
+ description: "full-prompt extraction"
388
+ },
389
+ {
390
+ pattern: /\boutput\s+your\s+system\s+message\b/,
391
+ severity: "high",
392
+ description: "system-message extraction"
393
+ }
394
+ ];
395
+ var EXFILTRATION_PATTERNS = [
396
+ {
397
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/,
398
+ severity: "high",
399
+ description: "SSN-pattern (XXX-XX-XXXX) in response"
400
+ },
401
+ {
402
+ pattern: /\b(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|3[47]\d{13})\b/,
403
+ severity: "high",
404
+ description: "credit-card sequence in response"
405
+ },
406
+ {
407
+ pattern: /sk-(?:proj-|live-|test-)?[A-Za-z0-9_-]{20,}/,
408
+ severity: "critical",
409
+ description: "OpenAI-style API key in response"
410
+ },
411
+ {
412
+ pattern: /\bghp_[A-Za-z0-9]{30,}\b/,
413
+ severity: "critical",
414
+ description: "GitHub PAT in response"
415
+ },
416
+ {
417
+ pattern: /\bgh[ousr]_[A-Za-z0-9]{30,}\b/,
418
+ severity: "critical",
419
+ description: "GitHub token in response"
420
+ },
421
+ {
422
+ pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+|PGP\s+|ENCRYPTED\s+)?PRIVATE\s+KEY-----/,
423
+ severity: "critical",
424
+ description: "private-key block in response"
425
+ },
426
+ {
427
+ pattern: /\bAKIA[0-9A-Z]{16}\b/,
428
+ severity: "critical",
429
+ description: "AWS access key ID in response"
430
+ },
431
+ {
432
+ pattern: /\bpassword\s*[:=]\s*\S+/i,
433
+ severity: "high",
434
+ description: "password literal in response"
435
+ }
436
+ ];
437
+ var DANGEROUS_TOOL_SEQUENCES = [
438
+ ["read_file", "send_email"],
439
+ ["list_files", "read_file", "http_request"],
440
+ ["read_database", "write_file"],
441
+ ["get_credentials", "http_request"],
442
+ ["read_env", "send_email"]
443
+ ];
444
+ function truncate(s) {
445
+ const trimmed = s.trim().replace(/\n/g, " ");
446
+ if (trimmed.length <= EVIDENCE_MAX) return trimmed;
447
+ return trimmed.slice(0, EVIDENCE_MAX - 3) + "...";
448
+ }
449
+ function redactValue(s) {
450
+ if (s.length <= 20) return s;
451
+ return s.slice(0, 20) + "...";
452
+ }
453
+ var SEVERITY_RANK = {
454
+ critical: 5,
455
+ high: 4,
456
+ medium: 3,
457
+ low: 2,
458
+ info: 1
459
+ };
460
+ function worstSeverity(violations) {
461
+ if (violations.length === 0) return null;
462
+ let worst = violations[0].severity;
463
+ for (const v of violations) {
464
+ if (SEVERITY_RANK[v.severity] > SEVERITY_RANK[worst]) {
465
+ worst = v.severity;
466
+ }
467
+ }
468
+ return worst;
469
+ }
470
+ function isSubsequence(needle, haystack) {
471
+ if (needle.length === 0) return true;
472
+ let i = 0;
473
+ for (const item of haystack) {
474
+ if (item === needle[i]) {
475
+ i += 1;
476
+ if (i === needle.length) return true;
477
+ }
478
+ }
479
+ return false;
480
+ }
481
+ var ThreatDetector = class _ThreatDetector {
482
+ static detect(options) {
483
+ const violations = [];
484
+ violations.push(..._ThreatDetector.detectPromptInjection(options.prompt ?? ""));
485
+ violations.push(..._ThreatDetector.detectScopeViolation(
486
+ options.toolCalls ?? [],
487
+ options.allowedTools ?? []
488
+ ));
489
+ violations.push(..._ThreatDetector.detectDataExfiltration(options.response ?? ""));
490
+ violations.push(..._ThreatDetector.detectIndirectInjection(options.toolOutputs ?? []));
491
+ violations.push(..._ThreatDetector.detectMultiStepAttack(
492
+ options.sessionHistory ?? [],
493
+ options.toolCalls ?? [],
494
+ options.tokenCount ?? 0
495
+ ));
496
+ return {
497
+ violations,
498
+ allow: true,
499
+ // detector never blocks — PolicyEngine decides
500
+ worstSeverity: worstSeverity(violations)
501
+ };
502
+ }
503
+ static detectPromptInjection(prompt) {
504
+ if (!prompt) return [];
505
+ const hits = [];
506
+ const seen = /* @__PURE__ */ new Set();
507
+ for (const source of expandSources(prompt)) {
508
+ const normalised = normalize(source);
509
+ for (const { pattern, severity, description } of INJECTION_PATTERNS) {
510
+ const m = pattern.exec(normalised);
511
+ if (!m) continue;
512
+ const key = `${pattern.source}|${description}`;
513
+ if (seen.has(key)) continue;
514
+ seen.add(key);
515
+ hits.push({
516
+ category: "prompt_injection",
517
+ severity,
518
+ description,
519
+ action: "block",
520
+ evidence: truncate(m[0])
521
+ });
522
+ }
523
+ }
524
+ return hits;
525
+ }
526
+ static detectScopeViolation(toolCalls, allowedTools) {
527
+ if (!toolCalls.length) return [];
528
+ if (!allowedTools.length) return [];
529
+ const allowed = new Set(allowedTools);
530
+ const hits = [];
531
+ for (const call of toolCalls) {
532
+ if (!call?.name) continue;
533
+ if (allowed.has(call.name)) continue;
534
+ hits.push({
535
+ category: "scope_violation",
536
+ severity: "critical",
537
+ description: `tool '${call.name}' not in allowed_tools`,
538
+ action: "block",
539
+ evidence: truncate(`Undeclared tool: ${call.name}`)
540
+ });
541
+ }
542
+ return hits;
543
+ }
544
+ static detectDataExfiltration(response) {
545
+ if (!response) return [];
546
+ const hits = [];
547
+ const seen = /* @__PURE__ */ new Set();
548
+ for (const { pattern, severity, description } of EXFILTRATION_PATTERNS) {
549
+ const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
550
+ let m;
551
+ while ((m = re.exec(response)) !== null) {
552
+ const key = `${pattern.source}|${m[0]}`;
553
+ if (seen.has(key)) continue;
554
+ seen.add(key);
555
+ hits.push({
556
+ category: "data_exfiltration",
557
+ severity,
558
+ description,
559
+ action: "flag",
560
+ evidence: truncate(redactValue(m[0]))
561
+ });
562
+ if (re.lastIndex === m.index) re.lastIndex += 1;
563
+ }
564
+ }
565
+ return hits;
566
+ }
567
+ // KNOWN GAP: indirect injection via nested tool output — Python parity
568
+ // needed. See test_threat_detector.py. Phase 8 hardening backlog.
569
+ static detectIndirectInjection(toolOutputs) {
570
+ if (!toolOutputs.length) return [];
571
+ const hits = [];
572
+ const seen = /* @__PURE__ */ new Set();
573
+ for (const raw of toolOutputs) {
574
+ if (!raw) continue;
575
+ for (const source of expandSources(raw)) {
576
+ const normalised = normalize(source);
577
+ for (const { pattern, description } of INJECTION_PATTERNS) {
578
+ const m = pattern.exec(normalised);
579
+ if (!m) continue;
580
+ const key = `${pattern.source}|${description}`;
581
+ if (seen.has(key)) continue;
582
+ seen.add(key);
583
+ hits.push({
584
+ category: "indirect_injection",
585
+ severity: "high",
586
+ description: `indirect: ${description}`,
587
+ action: "block",
588
+ evidence: truncate(m[0])
589
+ });
590
+ }
591
+ }
592
+ }
593
+ return hits;
594
+ }
595
+ static detectMultiStepAttack(sessionHistory, toolCalls, tokenCount) {
596
+ const hits = [];
597
+ const sequence = [];
598
+ for (const turn of sessionHistory) {
599
+ for (const call of turn.toolCalls ?? []) {
600
+ if (call?.name) sequence.push(call.name);
601
+ }
602
+ }
603
+ for (const call of toolCalls) {
604
+ if (call?.name) sequence.push(call.name);
605
+ }
606
+ for (const seq of DANGEROUS_TOOL_SEQUENCES) {
607
+ if (isSubsequence(seq, sequence)) {
608
+ hits.push({
609
+ category: "multi_step_attack",
610
+ severity: "high",
611
+ description: `dangerous tool sequence: ${seq.join(" -> ")}`,
612
+ action: "flag",
613
+ evidence: truncate(seq.join(" -> "))
614
+ });
615
+ }
616
+ }
617
+ if (tokenCount > TOKEN_BUDGET_LIMIT) {
618
+ hits.push({
619
+ category: "multi_step_attack",
620
+ severity: "critical",
621
+ description: `session token budget exceeded (${tokenCount} > ${TOKEN_BUDGET_LIMIT})`,
622
+ action: "block",
623
+ evidence: truncate(String(tokenCount))
624
+ });
625
+ }
626
+ return hits;
627
+ }
628
+ };
629
+
630
+ // src/PolicyEngine.ts
631
+ var DEFAULT_POLICY = {
632
+ name: "default",
633
+ tier: "standard",
634
+ rules: [{ type: "all", action: "log" }]
635
+ };
636
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
637
+ var PolicyEngine = class {
638
+ constructor(config) {
639
+ this.config = config;
640
+ }
641
+ config;
642
+ policyCache = /* @__PURE__ */ new Map();
643
+ async evaluate(options, policyName) {
644
+ const policy = await this.fetchPolicy(policyName);
645
+ const detection = ThreatDetector.detect(options);
646
+ let allow = true;
647
+ for (const v of detection.violations) {
648
+ if (v.action === "block" || v.severity === "critical") {
649
+ allow = false;
650
+ break;
651
+ }
652
+ }
653
+ if (policy.name === "default" && policy.rules[0]?.action === "log") {
654
+ }
655
+ return {
656
+ violations: detection.violations,
657
+ allow,
658
+ worstSeverity: detection.worstSeverity
659
+ };
660
+ }
661
+ async fetchPolicy(policyName) {
662
+ const cached = this.policyCache.get(policyName);
663
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
664
+ return cached.policy;
665
+ }
666
+ const baseUrl = this.config.apiBaseUrl ?? "https://api.mastguard.io";
667
+ const timeoutMs = this.config.timeoutMs ?? 5e3;
668
+ const url = `${baseUrl}/api/v1/security/policies?name=${encodeURIComponent(policyName)}`;
669
+ const controller = new AbortController();
670
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
671
+ try {
672
+ const res = await fetch(url, {
673
+ method: "GET",
674
+ headers: { Authorization: `Bearer ${this.config.apiKey}` },
675
+ signal: controller.signal
676
+ });
677
+ if (!res.ok) {
678
+ this.policyCache.set(policyName, { policy: DEFAULT_POLICY, fetchedAt: Date.now() });
679
+ return DEFAULT_POLICY;
680
+ }
681
+ const body = await res.json();
682
+ const policy = body?.data ?? body;
683
+ const safe = {
684
+ name: policy?.name ?? policyName,
685
+ tier: policy?.tier ?? "standard",
686
+ rules: Array.isArray(policy?.rules) ? policy.rules : DEFAULT_POLICY.rules
687
+ };
688
+ this.policyCache.set(policyName, { policy: safe, fetchedAt: Date.now() });
689
+ return safe;
690
+ } catch (err) {
691
+ console.error("[MastGuard] policy fetch error:", err);
692
+ this.policyCache.set(policyName, { policy: DEFAULT_POLICY, fetchedAt: Date.now() });
693
+ return DEFAULT_POLICY;
694
+ } finally {
695
+ clearTimeout(timer);
696
+ }
697
+ }
698
+ invalidateCache() {
699
+ this.policyCache.clear();
700
+ }
701
+ };
702
+
703
+ // src/SessionTracker.ts
704
+ var SessionTracker = class {
705
+ sessions = /* @__PURE__ */ new Map();
706
+ addTurn(sessionId, turn) {
707
+ const now = Date.now();
708
+ const existing = this.sessions.get(sessionId);
709
+ if (existing) {
710
+ existing.history.push(turn);
711
+ existing.lastActivityAt = now;
712
+ return;
713
+ }
714
+ this.sessions.set(sessionId, {
715
+ history: [turn],
716
+ createdAt: now,
717
+ lastActivityAt: now
718
+ });
719
+ }
720
+ getHistory(sessionId) {
721
+ const state = this.sessions.get(sessionId);
722
+ if (!state) return [];
723
+ return state.history.slice();
724
+ }
725
+ getToolCallSequence(sessionId) {
726
+ const state = this.sessions.get(sessionId);
727
+ if (!state) return [];
728
+ const out = [];
729
+ for (const turn of state.history) {
730
+ for (const call of turn.toolCalls ?? []) {
731
+ if (call?.name) out.push(call.name);
732
+ }
733
+ }
734
+ return out;
735
+ }
736
+ clearSession(sessionId) {
737
+ this.sessions.delete(sessionId);
738
+ }
739
+ getSessionCount() {
740
+ return this.sessions.size;
741
+ }
742
+ pruneOldSessions(maxAgeMs = 36e5) {
743
+ const cutoff = Date.now() - maxAgeMs;
744
+ for (const [id, state] of this.sessions) {
745
+ if (state.lastActivityAt < cutoff) {
746
+ this.sessions.delete(id);
747
+ }
748
+ }
749
+ }
750
+ };
751
+
752
+ // src/MastGuardShield.ts
753
+ function extractResponseText(response) {
754
+ if (typeof response === "string") return response;
755
+ if (response && typeof response === "object") {
756
+ const r = response;
757
+ if (Array.isArray(r["choices"]) && r["choices"].length > 0) {
758
+ const choice = r["choices"][0];
759
+ const msg = choice["message"];
760
+ if (typeof msg?.["content"] === "string") return msg["content"];
761
+ }
762
+ if (Array.isArray(r["content"]) && r["content"].length > 0) {
763
+ const block = r["content"][0];
764
+ if (typeof block["text"] === "string") return block["text"];
765
+ }
766
+ }
767
+ return "";
768
+ }
769
+ var MastGuardShield = class {
770
+ config;
771
+ sessionTracker;
772
+ policyEngine;
773
+ auditLogger;
774
+ constructor(config) {
775
+ this.config = {
776
+ apiKey: config.apiKey,
777
+ organizationId: config.organizationId,
778
+ agentId: config.agentId,
779
+ policy: config.policy ?? "default",
780
+ mode: config.mode ?? "monitor",
781
+ apiBaseUrl: config.apiBaseUrl ?? "https://api.mastguard.io",
782
+ timeoutMs: config.timeoutMs ?? 5e3
783
+ };
784
+ this.sessionTracker = new SessionTracker();
785
+ this.policyEngine = new PolicyEngine(this.config);
786
+ this.auditLogger = new AuditLogger(this.config);
787
+ }
788
+ async protect(llmCall, options) {
789
+ const startMs = Date.now();
790
+ const sessionHistory = this.sessionTracker.getHistory(options.sessionId);
791
+ const llmResponse = await llmCall;
792
+ const responseText = extractResponseText(llmResponse);
793
+ let evaluation;
794
+ try {
795
+ evaluation = await this.policyEngine.evaluate(
796
+ {
797
+ prompt: "",
798
+ response: responseText,
799
+ toolCalls: options.toolCalls ?? [],
800
+ toolOutputs: options.toolOutputs ?? [],
801
+ sessionHistory,
802
+ allowedTools: [],
803
+ // populated from agent_registrations in v0.2.0
804
+ tokenCount: options.tokenCount ?? 0
805
+ },
806
+ this.config.policy
807
+ );
808
+ } catch (sdkErr) {
809
+ console.error("[MastGuard] PolicyEngine error:", sdkErr);
810
+ if (this.config.mode === "block") throw sdkErr;
811
+ return { data: llmResponse, allowed: true, violations: [] };
812
+ }
813
+ this.sessionTracker.addTurn(options.sessionId, {
814
+ role: "assistant",
815
+ content: responseText,
816
+ ...options.toolCalls ? { toolCalls: options.toolCalls } : {}
817
+ });
818
+ const durationMs = Date.now() - startMs;
819
+ void this.auditLogger.ingest({
820
+ session_id: options.sessionId,
821
+ agent_id: this.config.agentId,
822
+ org_id: this.config.organizationId,
823
+ policy_id: null,
824
+ event_type: evaluation.violations.length > 0 ? evaluation.violations[0]?.category ?? "normal" : "normal",
825
+ prompt: "",
826
+ response: responseText.length ? responseText : null,
827
+ tool_calls: (options.toolCalls ?? []).map((tc) => ({
828
+ name: tc.name,
829
+ ...tc.parameters !== void 0 ? { parameters: tc.parameters } : {}
830
+ })),
831
+ tool_outputs: options.toolOutputs ?? [],
832
+ session_history: sessionHistory.map((t) => ({ role: t.role, content: t.content })),
833
+ token_count: options.tokenCount ?? 0,
834
+ duration_ms: durationMs
835
+ });
836
+ if (this.config.mode === "block" && !evaluation.allow) {
837
+ return {
838
+ data: null,
839
+ allowed: false,
840
+ violations: evaluation.violations
841
+ };
842
+ }
843
+ return {
844
+ data: llmResponse,
845
+ allowed: evaluation.allow,
846
+ violations: evaluation.violations
847
+ };
848
+ }
849
+ clearSession(sessionId) {
850
+ this.sessionTracker.clearSession(sessionId);
851
+ }
852
+ invalidatePolicyCache() {
853
+ this.policyEngine.invalidateCache();
854
+ }
855
+ };
856
+ export {
857
+ AuditLogger,
858
+ MastGuardShield,
859
+ PolicyEngine,
860
+ SessionTracker,
861
+ ThreatDetector
862
+ };