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