@i4ctime/q-ring 0.3.2 → 0.9.1

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/mcp.js CHANGED
@@ -1,22 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  checkDecay,
4
+ checkExecPolicy,
5
+ checkKeyReadPolicy,
6
+ checkToolPolicy,
4
7
  collapseEnvironment,
5
8
  deleteSecret,
6
9
  detectAnomalies,
10
+ disentangleSecrets,
7
11
  entangleSecrets,
12
+ exportAudit,
13
+ exportSecrets,
14
+ fireHooks,
8
15
  getEnvelope,
16
+ getExecMaxRuntime,
17
+ getPolicySummary,
9
18
  getSecret,
10
19
  hasSecret,
20
+ httpRequest_,
21
+ listHooks,
11
22
  listSecrets,
12
23
  logAudit,
13
24
  queryAudit,
25
+ readProjectConfig,
26
+ registerHook,
27
+ registry,
28
+ removeHook,
14
29
  setSecret,
15
30
  tunnelCreate,
16
31
  tunnelDestroy,
17
32
  tunnelList,
18
- tunnelRead
19
- } from "./chunk-3WTTWJYU.js";
33
+ tunnelRead,
34
+ verifyAuditChain
35
+ } from "./chunk-5JBU7TWN.js";
20
36
 
21
37
  // src/mcp.ts
22
38
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -147,7 +163,9 @@ function runHealthScan(config = {}) {
147
163
  `EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
148
164
  );
149
165
  if (cfg.autoRotate) {
150
- const newValue = generateSecret({ format: "api-key" });
166
+ const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
167
+ const prefix = entry.envelope?.meta.rotationPrefix;
168
+ const newValue = generateSecret({ format: fmt, prefix });
151
169
  setSecret(entry.key, newValue, {
152
170
  scope: entry.scope,
153
171
  projectPath: cfg.projectPaths[0],
@@ -161,6 +179,14 @@ function runHealthScan(config = {}) {
161
179
  source: "agent",
162
180
  detail: "auto-rotated by agent (expired)"
163
181
  });
182
+ fireHooks({
183
+ action: "rotate",
184
+ key: entry.key,
185
+ scope: entry.scope,
186
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
187
+ source: "agent"
188
+ }, entry.envelope?.meta.tags).catch(() => {
189
+ });
164
190
  }
165
191
  } else if (decay.isStale) {
166
192
  report.stale++;
@@ -240,6 +266,858 @@ function teleportUnpack(encoded, passphrase) {
240
266
  return JSON.parse(decrypted.toString("utf8"));
241
267
  }
242
268
 
269
+ // src/core/import.ts
270
+ import { readFileSync } from "fs";
271
+ function parseDotenv(content) {
272
+ const result = /* @__PURE__ */ new Map();
273
+ const lines = content.split(/\r?\n/);
274
+ for (let i = 0; i < lines.length; i++) {
275
+ const line = lines[i].trim();
276
+ if (!line || line.startsWith("#")) continue;
277
+ const eqIdx = line.indexOf("=");
278
+ if (eqIdx === -1) continue;
279
+ const key = line.slice(0, eqIdx).trim();
280
+ let value = line.slice(eqIdx + 1).trim();
281
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
282
+ value = value.slice(1, -1);
283
+ }
284
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\\\/g, "\\").replace(/\\"/g, '"');
285
+ if (value.includes("#") && !line.includes('"') && !line.includes("'")) {
286
+ value = value.split("#")[0].trim();
287
+ }
288
+ if (key) result.set(key, value);
289
+ }
290
+ return result;
291
+ }
292
+ function importDotenv(filePathOrContent, options = {}) {
293
+ let content;
294
+ try {
295
+ content = readFileSync(filePathOrContent, "utf8");
296
+ } catch {
297
+ content = filePathOrContent;
298
+ }
299
+ const pairs = parseDotenv(content);
300
+ const result = {
301
+ imported: [],
302
+ skipped: [],
303
+ total: pairs.size
304
+ };
305
+ for (const [key, value] of pairs) {
306
+ if (options.skipExisting && hasSecret(key, {
307
+ scope: options.scope,
308
+ projectPath: options.projectPath,
309
+ source: options.source ?? "cli"
310
+ })) {
311
+ result.skipped.push(key);
312
+ continue;
313
+ }
314
+ if (options.dryRun) {
315
+ result.imported.push(key);
316
+ continue;
317
+ }
318
+ const setOpts = {
319
+ scope: options.scope ?? "global",
320
+ projectPath: options.projectPath ?? process.cwd(),
321
+ source: options.source ?? "cli"
322
+ };
323
+ setSecret(key, value, setOpts);
324
+ result.imported.push(key);
325
+ }
326
+ return result;
327
+ }
328
+
329
+ // src/core/exec.ts
330
+ import { spawn } from "child_process";
331
+ import { Transform } from "stream";
332
+ var BUILTIN_PROFILES = {
333
+ unrestricted: { name: "unrestricted" },
334
+ restricted: {
335
+ name: "restricted",
336
+ denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
337
+ maxRuntimeSeconds: 30,
338
+ allowNetwork: false,
339
+ stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
340
+ },
341
+ ci: {
342
+ name: "ci",
343
+ maxRuntimeSeconds: 300,
344
+ allowNetwork: true,
345
+ denyCommands: ["rm -rf /", "mkfs", "dd if="]
346
+ }
347
+ };
348
+ function getProfile(name) {
349
+ if (!name) return BUILTIN_PROFILES.unrestricted;
350
+ return BUILTIN_PROFILES[name] ?? { name };
351
+ }
352
+ var RedactionTransform = class extends Transform {
353
+ patterns = [];
354
+ tail = "";
355
+ maxLen = 0;
356
+ constructor(secretsToRedact) {
357
+ super();
358
+ const validSecrets = secretsToRedact.filter((s) => s.length > 5);
359
+ validSecrets.sort((a, b) => b.length - a.length);
360
+ this.patterns = validSecrets.map((s) => ({
361
+ value: s,
362
+ replacement: "[QRING:REDACTED]"
363
+ }));
364
+ if (validSecrets.length > 0) {
365
+ this.maxLen = validSecrets[0].length;
366
+ }
367
+ }
368
+ _transform(chunk, encoding, callback) {
369
+ if (this.patterns.length === 0) {
370
+ this.push(chunk);
371
+ return callback();
372
+ }
373
+ const text2 = this.tail + chunk.toString();
374
+ let redacted = text2;
375
+ for (const { value, replacement } of this.patterns) {
376
+ redacted = redacted.split(value).join(replacement);
377
+ }
378
+ if (redacted.length < this.maxLen) {
379
+ this.tail = redacted;
380
+ return callback();
381
+ }
382
+ const outputLen = redacted.length - this.maxLen + 1;
383
+ const output = redacted.slice(0, outputLen);
384
+ this.tail = redacted.slice(outputLen);
385
+ this.push(output);
386
+ callback();
387
+ }
388
+ _flush(callback) {
389
+ if (this.tail) {
390
+ let final = this.tail;
391
+ for (const { value, replacement } of this.patterns) {
392
+ final = final.split(value).join(replacement);
393
+ }
394
+ this.push(final);
395
+ }
396
+ callback();
397
+ }
398
+ };
399
+ async function execCommand(opts2) {
400
+ const profile = getProfile(opts2.profile);
401
+ const fullCommand = [opts2.command, ...opts2.args].join(" ");
402
+ const policyDecision = checkExecPolicy(fullCommand, opts2.projectPath);
403
+ if (!policyDecision.allowed) {
404
+ throw new Error(`Policy Denied: ${policyDecision.reason}`);
405
+ }
406
+ if (profile.denyCommands) {
407
+ const denied = profile.denyCommands.find((d) => fullCommand.includes(d));
408
+ if (denied) {
409
+ throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
410
+ }
411
+ }
412
+ if (profile.allowCommands) {
413
+ const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
414
+ if (!allowed) {
415
+ throw new Error(`Exec profile "${profile.name}" does not allow command "${opts2.command}"`);
416
+ }
417
+ }
418
+ const envMap = {};
419
+ for (const [k, v] of Object.entries(process.env)) {
420
+ if (v !== void 0) envMap[k] = v;
421
+ }
422
+ if (profile.stripEnvVars) {
423
+ for (const key of profile.stripEnvVars) {
424
+ delete envMap[key];
425
+ }
426
+ }
427
+ const secretsToRedact = /* @__PURE__ */ new Set();
428
+ let entries = listSecrets({
429
+ scope: opts2.scope,
430
+ projectPath: opts2.projectPath,
431
+ source: opts2.source ?? "cli",
432
+ silent: true
433
+ // list silently
434
+ });
435
+ if (opts2.keys?.length) {
436
+ const keySet = new Set(opts2.keys);
437
+ entries = entries.filter((e) => keySet.has(e.key));
438
+ }
439
+ if (opts2.tags?.length) {
440
+ entries = entries.filter(
441
+ (e) => opts2.tags.some((t) => e.envelope?.meta.tags?.includes(t))
442
+ );
443
+ }
444
+ for (const entry of entries) {
445
+ if (entry.envelope) {
446
+ const decay = checkDecay(entry.envelope);
447
+ if (decay.isExpired) continue;
448
+ }
449
+ const val = getSecret(entry.key, {
450
+ scope: entry.scope,
451
+ projectPath: opts2.projectPath,
452
+ env: opts2.env,
453
+ source: opts2.source ?? "cli",
454
+ silent: false
455
+ // Log access for execution
456
+ });
457
+ if (val !== null) {
458
+ envMap[entry.key] = val;
459
+ if (val.length > 5) {
460
+ secretsToRedact.add(val);
461
+ }
462
+ }
463
+ }
464
+ const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts2.projectPath);
465
+ return new Promise((resolve, reject) => {
466
+ const networkTools = /* @__PURE__ */ new Set([
467
+ "curl",
468
+ "wget",
469
+ "ping",
470
+ "nc",
471
+ "netcat",
472
+ "ssh",
473
+ "telnet",
474
+ "ftp",
475
+ "dig",
476
+ "nslookup"
477
+ ]);
478
+ if (profile.allowNetwork === false && networkTools.has(opts2.command)) {
479
+ const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts2.command}" is considered network-related`;
480
+ if (opts2.captureOutput) {
481
+ return resolve({ code: 126, stdout: "", stderr: msg });
482
+ }
483
+ process.stderr.write(msg + "\n");
484
+ return resolve({ code: 126, stdout: "", stderr: "" });
485
+ }
486
+ const child = spawn(opts2.command, opts2.args, {
487
+ env: envMap,
488
+ stdio: ["inherit", "pipe", "pipe"],
489
+ shell: false
490
+ });
491
+ let timedOut = false;
492
+ let timer;
493
+ if (maxRuntime) {
494
+ timer = setTimeout(() => {
495
+ timedOut = true;
496
+ child.kill("SIGKILL");
497
+ }, maxRuntime * 1e3);
498
+ }
499
+ const stdoutRedact = new RedactionTransform([...secretsToRedact]);
500
+ const stderrRedact = new RedactionTransform([...secretsToRedact]);
501
+ if (child.stdout) child.stdout.pipe(stdoutRedact);
502
+ if (child.stderr) child.stderr.pipe(stderrRedact);
503
+ let stdoutStr = "";
504
+ let stderrStr = "";
505
+ if (opts2.captureOutput) {
506
+ stdoutRedact.on("data", (d) => stdoutStr += d.toString());
507
+ stderrRedact.on("data", (d) => stderrStr += d.toString());
508
+ } else {
509
+ stdoutRedact.pipe(process.stdout);
510
+ stderrRedact.pipe(process.stderr);
511
+ }
512
+ child.on("close", (code) => {
513
+ if (timer) clearTimeout(timer);
514
+ if (timedOut) {
515
+ resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
516
+ [QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
517
+ } else {
518
+ resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
519
+ }
520
+ });
521
+ child.on("error", (err) => {
522
+ if (timer) clearTimeout(timer);
523
+ reject(err);
524
+ });
525
+ });
526
+ }
527
+
528
+ // src/core/scan.ts
529
+ import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
530
+ import { join } from "path";
531
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
532
+ "node_modules",
533
+ ".git",
534
+ ".next",
535
+ "dist",
536
+ "build",
537
+ "coverage",
538
+ ".cursor",
539
+ "venv",
540
+ "__pycache__"
541
+ ]);
542
+ var IGNORE_EXTS = /* @__PURE__ */ new Set([
543
+ ".png",
544
+ ".jpg",
545
+ ".jpeg",
546
+ ".gif",
547
+ ".ico",
548
+ ".svg",
549
+ ".webp",
550
+ ".mp4",
551
+ ".mp3",
552
+ ".wav",
553
+ ".ogg",
554
+ ".pdf",
555
+ ".zip",
556
+ ".tar",
557
+ ".gz",
558
+ ".xz",
559
+ ".ttf",
560
+ ".woff",
561
+ ".woff2",
562
+ ".eot",
563
+ ".exe",
564
+ ".dll",
565
+ ".so",
566
+ ".dylib",
567
+ ".lock"
568
+ ]);
569
+ var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
570
+ function calculateEntropy(str) {
571
+ if (!str) return 0;
572
+ const len = str.length;
573
+ const frequencies = /* @__PURE__ */ new Map();
574
+ for (let i = 0; i < len; i++) {
575
+ const char = str[i];
576
+ frequencies.set(char, (frequencies.get(char) || 0) + 1);
577
+ }
578
+ let entropy = 0;
579
+ for (const count of frequencies.values()) {
580
+ const p = count / len;
581
+ entropy -= p * Math.log2(p);
582
+ }
583
+ return entropy;
584
+ }
585
+ function scanCodebase(dir) {
586
+ const results = [];
587
+ function walk(currentDir) {
588
+ let entries;
589
+ try {
590
+ entries = readdirSync(currentDir);
591
+ } catch {
592
+ return;
593
+ }
594
+ for (const entry of entries) {
595
+ if (IGNORE_DIRS.has(entry)) continue;
596
+ const fullPath = join(currentDir, entry);
597
+ let stat;
598
+ try {
599
+ stat = statSync(fullPath);
600
+ } catch {
601
+ continue;
602
+ }
603
+ if (stat.isDirectory()) {
604
+ walk(fullPath);
605
+ } else if (stat.isFile()) {
606
+ const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
607
+ if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
608
+ let content;
609
+ try {
610
+ content = readFileSync2(fullPath, "utf8");
611
+ } catch {
612
+ continue;
613
+ }
614
+ if (content.includes("\0")) continue;
615
+ const lines = content.split(/\r?\n/);
616
+ for (let i = 0; i < lines.length; i++) {
617
+ const line = lines[i];
618
+ if (line.length > 500) continue;
619
+ const match = line.match(SECRET_KEYWORDS);
620
+ if (match) {
621
+ const varName = match[1];
622
+ const value = match[3];
623
+ if (value.length < 8) continue;
624
+ const lowerValue = value.toLowerCase();
625
+ if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
626
+ continue;
627
+ }
628
+ const entropy = calculateEntropy(value);
629
+ if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
630
+ const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
631
+ results.push({
632
+ file: relPath || fullPath,
633
+ line: i + 1,
634
+ keyName: varName,
635
+ match: value,
636
+ context: line.trim(),
637
+ entropy: parseFloat(entropy.toFixed(2))
638
+ });
639
+ }
640
+ }
641
+ }
642
+ }
643
+ }
644
+ }
645
+ walk(dir);
646
+ return results;
647
+ }
648
+
649
+ // src/core/linter.ts
650
+ import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
651
+ import { basename, extname } from "path";
652
+ var ENV_REF_BY_EXT = {
653
+ ".ts": (k) => `process.env.${k}`,
654
+ ".tsx": (k) => `process.env.${k}`,
655
+ ".js": (k) => `process.env.${k}`,
656
+ ".jsx": (k) => `process.env.${k}`,
657
+ ".mjs": (k) => `process.env.${k}`,
658
+ ".cjs": (k) => `process.env.${k}`,
659
+ ".py": (k) => `os.environ["${k}"]`,
660
+ ".rb": (k) => `ENV["${k}"]`,
661
+ ".go": (k) => `os.Getenv("${k}")`,
662
+ ".rs": (k) => `std::env::var("${k}")`,
663
+ ".java": (k) => `System.getenv("${k}")`,
664
+ ".kt": (k) => `System.getenv("${k}")`,
665
+ ".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
666
+ ".php": (k) => `getenv('${k}')`,
667
+ ".sh": (k) => `\${${k}}`,
668
+ ".bash": (k) => `\${${k}}`
669
+ };
670
+ function getEnvRef(filePath, keyName) {
671
+ const ext = extname(filePath).toLowerCase();
672
+ const formatter = ENV_REF_BY_EXT[ext];
673
+ return formatter ? formatter(keyName) : `process.env.${keyName}`;
674
+ }
675
+ function lintFiles(files, opts2 = {}) {
676
+ const results = [];
677
+ for (const file of files) {
678
+ if (!existsSync(file)) continue;
679
+ let content;
680
+ try {
681
+ content = readFileSync3(file, "utf8");
682
+ } catch {
683
+ continue;
684
+ }
685
+ if (content.includes("\0")) continue;
686
+ const SECRET_KEYWORDS2 = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
687
+ const lines = content.split(/\r?\n/);
688
+ const fixes = [];
689
+ for (let i = 0; i < lines.length; i++) {
690
+ const line = lines[i];
691
+ if (line.length > 500) continue;
692
+ let match;
693
+ SECRET_KEYWORDS2.lastIndex = 0;
694
+ while ((match = SECRET_KEYWORDS2.exec(line)) !== null) {
695
+ const varName = match[1].toUpperCase();
696
+ const quote = match[2];
697
+ const value = match[3];
698
+ if (value.length < 8) continue;
699
+ const lv = value.toLowerCase();
700
+ if (lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx")) continue;
701
+ const entropy = calculateEntropy2(value);
702
+ if (entropy <= 3.5 && !value.startsWith("sk-") && !value.startsWith("ghp_")) continue;
703
+ const shouldFix = opts2.fix === true;
704
+ if (shouldFix) {
705
+ const envRef = getEnvRef(file, varName);
706
+ fixes.push({
707
+ line: i,
708
+ original: `${quote}${value}${quote}`,
709
+ replacement: envRef,
710
+ keyName: varName,
711
+ value
712
+ });
713
+ }
714
+ results.push({
715
+ file,
716
+ line: i + 1,
717
+ keyName: varName,
718
+ match: value,
719
+ context: line.trim(),
720
+ entropy: parseFloat(entropy.toFixed(2)),
721
+ fixed: shouldFix
722
+ });
723
+ }
724
+ }
725
+ if (opts2.fix && fixes.length > 0) {
726
+ const fixLines = content.split(/\r?\n/);
727
+ for (const fix of fixes.reverse()) {
728
+ const lineIdx = fix.line;
729
+ if (lineIdx >= 0 && lineIdx < fixLines.length) {
730
+ fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
731
+ }
732
+ if (!hasSecret(fix.keyName, { scope: opts2.scope, projectPath: opts2.projectPath })) {
733
+ setSecret(fix.keyName, fix.value, {
734
+ scope: opts2.scope ?? "global",
735
+ projectPath: opts2.projectPath,
736
+ source: "cli",
737
+ description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
738
+ });
739
+ }
740
+ }
741
+ writeFileSync(file, fixLines.join("\n"), "utf8");
742
+ }
743
+ }
744
+ return results;
745
+ }
746
+ function calculateEntropy2(str) {
747
+ if (!str) return 0;
748
+ const len = str.length;
749
+ const frequencies = /* @__PURE__ */ new Map();
750
+ for (let i = 0; i < len; i++) {
751
+ const ch = str[i];
752
+ frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
753
+ }
754
+ let entropy = 0;
755
+ for (const count of frequencies.values()) {
756
+ const p = count / len;
757
+ entropy -= p * Math.log2(p);
758
+ }
759
+ return entropy;
760
+ }
761
+
762
+ // src/core/validate.ts
763
+ function makeRequest(url, headers, timeoutMs = 1e4) {
764
+ return httpRequest_({ url, method: "GET", headers, timeoutMs });
765
+ }
766
+ var ProviderRegistry = class {
767
+ providers = /* @__PURE__ */ new Map();
768
+ register(provider) {
769
+ this.providers.set(provider.name, provider);
770
+ }
771
+ get(name) {
772
+ return this.providers.get(name);
773
+ }
774
+ detectProvider(value, hints) {
775
+ if (hints?.provider) {
776
+ return this.providers.get(hints.provider);
777
+ }
778
+ for (const provider of this.providers.values()) {
779
+ if (provider.prefixes) {
780
+ for (const pfx of provider.prefixes) {
781
+ if (value.startsWith(pfx)) return provider;
782
+ }
783
+ }
784
+ }
785
+ return void 0;
786
+ }
787
+ listProviders() {
788
+ return [...this.providers.values()];
789
+ }
790
+ };
791
+ var openaiProvider = {
792
+ name: "openai",
793
+ description: "OpenAI API key validation",
794
+ prefixes: ["sk-"],
795
+ async validate(value) {
796
+ const start = Date.now();
797
+ try {
798
+ const { statusCode } = await makeRequest(
799
+ "https://api.openai.com/v1/models?limit=1",
800
+ {
801
+ Authorization: `Bearer ${value}`,
802
+ "User-Agent": "q-ring-validator/1.0"
803
+ }
804
+ );
805
+ const latencyMs = Date.now() - start;
806
+ if (statusCode === 200)
807
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "openai" };
808
+ if (statusCode === 401)
809
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "openai" };
810
+ if (statusCode === 429)
811
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "openai" };
812
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "openai" };
813
+ } catch (err) {
814
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "openai" };
815
+ }
816
+ }
817
+ };
818
+ var stripeProvider = {
819
+ name: "stripe",
820
+ description: "Stripe API key validation",
821
+ prefixes: ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "pk_live_", "pk_test_"],
822
+ async validate(value) {
823
+ const start = Date.now();
824
+ try {
825
+ const { statusCode } = await makeRequest(
826
+ "https://api.stripe.com/v1/balance",
827
+ {
828
+ Authorization: `Bearer ${value}`,
829
+ "User-Agent": "q-ring-validator/1.0"
830
+ }
831
+ );
832
+ const latencyMs = Date.now() - start;
833
+ if (statusCode === 200)
834
+ return { valid: true, status: "valid", message: "API key is valid", latencyMs, provider: "stripe" };
835
+ if (statusCode === 401)
836
+ return { valid: false, status: "invalid", message: "Invalid or revoked API key", latencyMs, provider: "stripe" };
837
+ if (statusCode === 429)
838
+ return { valid: true, status: "error", message: "Rate limited \u2014 key may be valid", latencyMs, provider: "stripe" };
839
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "stripe" };
840
+ } catch (err) {
841
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "stripe" };
842
+ }
843
+ }
844
+ };
845
+ var githubProvider = {
846
+ name: "github",
847
+ description: "GitHub token validation",
848
+ prefixes: ["ghp_", "gho_", "ghu_", "ghs_", "ghr_", "github_pat_"],
849
+ async validate(value) {
850
+ const start = Date.now();
851
+ try {
852
+ const { statusCode } = await makeRequest(
853
+ "https://api.github.com/user",
854
+ {
855
+ Authorization: `token ${value}`,
856
+ "User-Agent": "q-ring-validator/1.0",
857
+ Accept: "application/vnd.github+json"
858
+ }
859
+ );
860
+ const latencyMs = Date.now() - start;
861
+ if (statusCode === 200)
862
+ return { valid: true, status: "valid", message: "Token is valid", latencyMs, provider: "github" };
863
+ if (statusCode === 401)
864
+ return { valid: false, status: "invalid", message: "Invalid or expired token", latencyMs, provider: "github" };
865
+ if (statusCode === 403)
866
+ return { valid: false, status: "invalid", message: "Token lacks required permissions", latencyMs, provider: "github" };
867
+ if (statusCode === 429)
868
+ return { valid: true, status: "error", message: "Rate limited \u2014 token may be valid", latencyMs, provider: "github" };
869
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "github" };
870
+ } catch (err) {
871
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "github" };
872
+ }
873
+ }
874
+ };
875
+ var awsProvider = {
876
+ name: "aws",
877
+ description: "AWS access key validation (checks key format only \u2014 full STS validation requires secret key + region)",
878
+ prefixes: ["AKIA", "ASIA"],
879
+ async validate(value) {
880
+ const start = Date.now();
881
+ const latencyMs = Date.now() - start;
882
+ if (/^(AKIA|ASIA)[A-Z0-9]{16}$/.test(value)) {
883
+ return { valid: true, status: "unknown", message: "Valid AWS access key format (STS validation requires secret key)", latencyMs, provider: "aws" };
884
+ }
885
+ return { valid: false, status: "invalid", message: "Invalid AWS access key format", latencyMs, provider: "aws" };
886
+ }
887
+ };
888
+ var httpProvider = {
889
+ name: "http",
890
+ description: "Generic HTTP endpoint validation",
891
+ async validate(value, url) {
892
+ const start = Date.now();
893
+ if (!url) {
894
+ return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
895
+ }
896
+ try {
897
+ const { statusCode } = await makeRequest(url, {
898
+ Authorization: `Bearer ${value}`,
899
+ "User-Agent": "q-ring-validator/1.0"
900
+ });
901
+ const latencyMs = Date.now() - start;
902
+ if (statusCode >= 200 && statusCode < 300)
903
+ return { valid: true, status: "valid", message: `Endpoint returned ${statusCode}`, latencyMs, provider: "http" };
904
+ if (statusCode === 401 || statusCode === 403)
905
+ return { valid: false, status: "invalid", message: `Authentication failed (${statusCode})`, latencyMs, provider: "http" };
906
+ return { valid: false, status: "error", message: `Unexpected status ${statusCode}`, latencyMs, provider: "http" };
907
+ } catch (err) {
908
+ return { valid: false, status: "error", message: `${err instanceof Error ? err.message : "Network error"}`, latencyMs: Date.now() - start, provider: "http" };
909
+ }
910
+ }
911
+ };
912
+ var registry2 = new ProviderRegistry();
913
+ registry2.register(openaiProvider);
914
+ registry2.register(stripeProvider);
915
+ registry2.register(githubProvider);
916
+ registry2.register(awsProvider);
917
+ registry2.register(httpProvider);
918
+ async function validateSecret(value, opts2) {
919
+ const provider = opts2?.provider ? registry2.get(opts2.provider) : registry2.detectProvider(value);
920
+ if (!provider) {
921
+ return {
922
+ valid: false,
923
+ status: "unknown",
924
+ message: "No provider detected \u2014 set a provider in the manifest or secret metadata",
925
+ latencyMs: 0,
926
+ provider: "none"
927
+ };
928
+ }
929
+ if (provider.name === "http" && opts2?.validationUrl) {
930
+ return provider.validate(value, opts2.validationUrl);
931
+ }
932
+ return provider.validate(value);
933
+ }
934
+ async function rotateWithProvider(value, providerName) {
935
+ const provider = providerName ? registry2.get(providerName) : registry2.detectProvider(value);
936
+ if (!provider) {
937
+ return { rotated: false, provider: "none", message: "No provider detected for rotation" };
938
+ }
939
+ const rotatable = provider;
940
+ if (rotatable.supportsRotation && rotatable.rotate) {
941
+ return rotatable.rotate(value);
942
+ }
943
+ const format = "api-key";
944
+ const newValue = generateSecret({ format, length: 48 });
945
+ return {
946
+ rotated: true,
947
+ provider: provider.name,
948
+ message: `Provider "${provider.name}" does not support native rotation \u2014 generated new value locally`,
949
+ newValue
950
+ };
951
+ }
952
+ async function ciValidateBatch(secrets) {
953
+ const results = [];
954
+ for (const s of secrets) {
955
+ const validation = await validateSecret(s.value, {
956
+ provider: s.provider,
957
+ validationUrl: s.validationUrl
958
+ });
959
+ results.push({
960
+ key: s.key,
961
+ validation,
962
+ requiresRotation: validation.status === "invalid"
963
+ });
964
+ }
965
+ const failCount = results.filter((r) => !r.validation.valid).length;
966
+ return { results, allValid: failCount === 0, failCount };
967
+ }
968
+
969
+ // src/core/context.ts
970
+ function getProjectContext(opts2 = {}) {
971
+ const projectPath = opts2.projectPath ?? process.cwd();
972
+ const envResult = collapseEnvironment({ projectPath });
973
+ const secretsList = listSecrets({
974
+ ...opts2,
975
+ projectPath,
976
+ silent: true
977
+ });
978
+ let expiredCount = 0;
979
+ let staleCount = 0;
980
+ let protectedCount = 0;
981
+ const secrets = secretsList.map((entry) => {
982
+ const meta = entry.envelope?.meta;
983
+ const decay = entry.decay;
984
+ if (decay?.isExpired) expiredCount++;
985
+ if (decay?.isStale) staleCount++;
986
+ if (meta?.requiresApproval) protectedCount++;
987
+ return {
988
+ key: entry.key,
989
+ scope: entry.scope,
990
+ tags: meta?.tags,
991
+ description: meta?.description,
992
+ provider: meta?.provider,
993
+ requiresApproval: meta?.requiresApproval,
994
+ jitProvider: meta?.jitProvider,
995
+ hasStates: !!(entry.envelope?.states && Object.keys(entry.envelope.states).length > 0),
996
+ isExpired: decay?.isExpired ?? false,
997
+ isStale: decay?.isStale ?? false,
998
+ timeRemaining: decay?.timeRemaining ?? null,
999
+ accessCount: meta?.accessCount ?? 0,
1000
+ lastAccessed: meta?.lastAccessedAt ?? null,
1001
+ rotationFormat: meta?.rotationFormat
1002
+ };
1003
+ });
1004
+ let manifest = null;
1005
+ const config = readProjectConfig(projectPath);
1006
+ if (config?.secrets) {
1007
+ const declaredKeys = Object.keys(config.secrets);
1008
+ const existingKeys = new Set(secrets.map((s) => s.key));
1009
+ const missing = declaredKeys.filter((k) => !existingKeys.has(k));
1010
+ manifest = { declared: declaredKeys.length, missing };
1011
+ }
1012
+ const recentEvents = queryAudit({ limit: 20 });
1013
+ const recentActions = recentEvents.map((e) => ({
1014
+ action: e.action,
1015
+ key: e.key,
1016
+ source: e.source,
1017
+ timestamp: e.timestamp
1018
+ }));
1019
+ return {
1020
+ projectPath,
1021
+ environment: envResult ? { env: envResult.env, source: envResult.source } : null,
1022
+ secrets,
1023
+ totalSecrets: secrets.length,
1024
+ expiredCount,
1025
+ staleCount,
1026
+ protectedCount,
1027
+ manifest,
1028
+ validationProviders: registry2.listProviders().map((p) => p.name),
1029
+ jitProviders: registry.listProviders().map((p) => p.name),
1030
+ hooksCount: listHooks().length,
1031
+ recentActions
1032
+ };
1033
+ }
1034
+
1035
+ // src/core/memory.ts
1036
+ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
1037
+ import { join as join2 } from "path";
1038
+ import { homedir, hostname, userInfo } from "os";
1039
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
1040
+ var MEMORY_FILE = "agent-memory.enc";
1041
+ function getMemoryDir() {
1042
+ const dir = join2(homedir(), ".config", "q-ring");
1043
+ if (!existsSync2(dir)) {
1044
+ mkdirSync(dir, { recursive: true });
1045
+ }
1046
+ return dir;
1047
+ }
1048
+ function getMemoryPath() {
1049
+ return join2(getMemoryDir(), MEMORY_FILE);
1050
+ }
1051
+ function deriveKey2() {
1052
+ const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
1053
+ return createHash("sha256").update(fingerprint).digest();
1054
+ }
1055
+ function encrypt(data) {
1056
+ const key = deriveKey2();
1057
+ const iv = randomBytes3(12);
1058
+ const cipher = createCipheriv2("aes-256-gcm", key, iv);
1059
+ const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
1060
+ const tag = cipher.getAuthTag();
1061
+ return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
1062
+ }
1063
+ function decrypt(blob) {
1064
+ const parts = blob.split(":");
1065
+ if (parts.length !== 3) throw new Error("Invalid encrypted format");
1066
+ const iv = Buffer.from(parts[0], "base64");
1067
+ const tag = Buffer.from(parts[1], "base64");
1068
+ const encrypted = Buffer.from(parts[2], "base64");
1069
+ const key = deriveKey2();
1070
+ const decipher = createDecipheriv2("aes-256-gcm", key, iv);
1071
+ decipher.setAuthTag(tag);
1072
+ return decipher.update(encrypted) + decipher.final("utf8");
1073
+ }
1074
+ function loadStore() {
1075
+ const path = getMemoryPath();
1076
+ if (!existsSync2(path)) {
1077
+ return { entries: {} };
1078
+ }
1079
+ try {
1080
+ const raw = readFileSync4(path, "utf8");
1081
+ const decrypted = decrypt(raw);
1082
+ return JSON.parse(decrypted);
1083
+ } catch {
1084
+ return { entries: {} };
1085
+ }
1086
+ }
1087
+ function saveStore(store) {
1088
+ const json = JSON.stringify(store);
1089
+ const encrypted = encrypt(json);
1090
+ writeFileSync2(getMemoryPath(), encrypted, "utf8");
1091
+ }
1092
+ function remember(key, value) {
1093
+ const store = loadStore();
1094
+ store.entries[key] = {
1095
+ value,
1096
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1097
+ };
1098
+ saveStore(store);
1099
+ }
1100
+ function recall(key) {
1101
+ const store = loadStore();
1102
+ return store.entries[key]?.value ?? null;
1103
+ }
1104
+ function listMemory() {
1105
+ const store = loadStore();
1106
+ return Object.entries(store.entries).map(([key, entry]) => ({
1107
+ key,
1108
+ updatedAt: entry.updatedAt
1109
+ }));
1110
+ }
1111
+ function forget(key) {
1112
+ const store = loadStore();
1113
+ if (key in store.entries) {
1114
+ delete store.entries[key];
1115
+ saveStore(store);
1116
+ return true;
1117
+ }
1118
+ return false;
1119
+ }
1120
+
243
1121
  // src/mcp/server.ts
244
1122
  function text(t, isError = false) {
245
1123
  return {
@@ -251,16 +1129,27 @@ function opts(params) {
251
1129
  return {
252
1130
  scope: params.scope,
253
1131
  projectPath: params.projectPath ?? process.cwd(),
1132
+ teamId: params.teamId,
1133
+ orgId: params.orgId,
254
1134
  env: params.env,
255
1135
  source: "mcp"
256
1136
  };
257
1137
  }
1138
+ function enforceToolPolicy(toolName, projectPath) {
1139
+ const decision = checkToolPolicy(toolName, projectPath);
1140
+ if (!decision.allowed) {
1141
+ return text(`Policy Denied: ${decision.reason} (source: ${decision.policySource})`, true);
1142
+ }
1143
+ return null;
1144
+ }
258
1145
  function createMcpServer() {
259
1146
  const server2 = new McpServer({
260
1147
  name: "q-ring",
261
1148
  version: "0.2.0"
262
1149
  });
263
- const scopeSchema = z.enum(["global", "project"]).optional().describe("Scope: global or project");
1150
+ const teamIdSchema = z.string().optional().describe("Team identifier for team-scoped secrets");
1151
+ const orgIdSchema = z.string().optional().describe("Org identifier for org-scoped secrets");
1152
+ const scopeSchema = z.enum(["global", "project", "team", "org"]).optional().describe("Scope: global, project, team, or org");
264
1153
  const projectPathSchema = z.string().optional().describe("Project root path for project-scoped secrets");
265
1154
  const envSchema = z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)");
266
1155
  server2.tool(
@@ -270,23 +1159,63 @@ function createMcpServer() {
270
1159
  key: z.string().describe("The secret key name"),
271
1160
  scope: scopeSchema,
272
1161
  projectPath: projectPathSchema,
273
- env: envSchema
1162
+ env: envSchema,
1163
+ teamId: teamIdSchema,
1164
+ orgId: orgIdSchema
274
1165
  },
275
1166
  async (params) => {
276
- const value = getSecret(params.key, opts(params));
277
- if (value === null) return text(`Secret "${params.key}" not found`, true);
278
- return text(value);
1167
+ const toolBlock = enforceToolPolicy("get_secret", params.projectPath);
1168
+ if (toolBlock) return toolBlock;
1169
+ try {
1170
+ const keyBlock = checkKeyReadPolicy(params.key, void 0, params.projectPath);
1171
+ if (!keyBlock.allowed) {
1172
+ return text(`Policy Denied: ${keyBlock.reason}`, true);
1173
+ }
1174
+ const value = getSecret(params.key, opts(params));
1175
+ if (value === null) return text(`Secret "${params.key}" not found`, true);
1176
+ return text(value);
1177
+ } catch (err) {
1178
+ return text(err instanceof Error ? err.message : String(err), true);
1179
+ }
279
1180
  }
280
1181
  );
281
1182
  server2.tool(
282
1183
  "list_secrets",
283
- "List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed.",
1184
+ "List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed. Supports filtering by tag, expiry state, and key pattern.",
284
1185
  {
285
1186
  scope: scopeSchema,
286
- projectPath: projectPathSchema
1187
+ projectPath: projectPathSchema,
1188
+ tag: z.string().optional().describe("Filter by tag"),
1189
+ expired: z.boolean().optional().describe("Show only expired secrets"),
1190
+ stale: z.boolean().optional().describe("Show only stale secrets (75%+ decay)"),
1191
+ filter: z.string().optional().describe("Glob pattern on key name (e.g., 'API_*')"),
1192
+ teamId: teamIdSchema,
1193
+ orgId: orgIdSchema
287
1194
  },
288
1195
  async (params) => {
289
- const entries = listSecrets(opts(params));
1196
+ const toolBlock = enforceToolPolicy("list_secrets", params.projectPath);
1197
+ if (toolBlock) return toolBlock;
1198
+ let entries = listSecrets(opts(params));
1199
+ if (params.tag) {
1200
+ entries = entries.filter(
1201
+ (e) => e.envelope?.meta.tags?.includes(params.tag)
1202
+ );
1203
+ }
1204
+ if (params.expired) {
1205
+ entries = entries.filter((e) => e.decay?.isExpired);
1206
+ }
1207
+ if (params.stale) {
1208
+ entries = entries.filter(
1209
+ (e) => e.decay?.isStale && !e.decay?.isExpired
1210
+ );
1211
+ }
1212
+ if (params.filter) {
1213
+ const regex = new RegExp(
1214
+ "^" + params.filter.replace(/\*/g, ".*") + "$",
1215
+ "i"
1216
+ );
1217
+ entries = entries.filter((e) => regex.test(e.key));
1218
+ }
290
1219
  if (entries.length === 0) return text("No secrets found");
291
1220
  const lines = entries.map((e) => {
292
1221
  const parts = [`[${e.scope}] ${e.key}`];
@@ -323,9 +1252,15 @@ function createMcpServer() {
323
1252
  env: z.string().optional().describe("If provided, sets the value for this specific environment (superposition)"),
324
1253
  ttlSeconds: z.number().optional().describe("Time-to-live in seconds (quantum decay)"),
325
1254
  description: z.string().optional().describe("Human-readable description"),
326
- tags: z.array(z.string()).optional().describe("Tags for organization")
1255
+ tags: z.array(z.string()).optional().describe("Tags for organization"),
1256
+ rotationFormat: z.enum(["hex", "base64", "alphanumeric", "uuid", "api-key", "token", "password"]).optional().describe("Format for auto-rotation when this secret expires"),
1257
+ rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')"),
1258
+ teamId: teamIdSchema,
1259
+ orgId: orgIdSchema
327
1260
  },
328
1261
  async (params) => {
1262
+ const toolBlock = enforceToolPolicy("set_secret", params.projectPath);
1263
+ if (toolBlock) return toolBlock;
329
1264
  const o = opts(params);
330
1265
  if (params.env) {
331
1266
  const existing = getEnvelope(params.key, o);
@@ -340,7 +1275,9 @@ function createMcpServer() {
340
1275
  defaultEnv: existing?.envelope?.defaultEnv ?? params.env,
341
1276
  ttlSeconds: params.ttlSeconds,
342
1277
  description: params.description,
343
- tags: params.tags
1278
+ tags: params.tags,
1279
+ rotationFormat: params.rotationFormat,
1280
+ rotationPrefix: params.rotationPrefix
344
1281
  });
345
1282
  return text(`[${params.scope ?? "global"}] ${params.key} set for env:${params.env}`);
346
1283
  }
@@ -348,7 +1285,9 @@ function createMcpServer() {
348
1285
  ...o,
349
1286
  ttlSeconds: params.ttlSeconds,
350
1287
  description: params.description,
351
- tags: params.tags
1288
+ tags: params.tags,
1289
+ rotationFormat: params.rotationFormat,
1290
+ rotationPrefix: params.rotationPrefix
352
1291
  });
353
1292
  return text(`[${params.scope ?? "global"}] ${params.key} saved`);
354
1293
  }
@@ -359,9 +1298,13 @@ function createMcpServer() {
359
1298
  {
360
1299
  key: z.string().describe("The secret key name"),
361
1300
  scope: scopeSchema,
362
- projectPath: projectPathSchema
1301
+ projectPath: projectPathSchema,
1302
+ teamId: teamIdSchema,
1303
+ orgId: orgIdSchema
363
1304
  },
364
1305
  async (params) => {
1306
+ const toolBlock = enforceToolPolicy("delete_secret", params.projectPath);
1307
+ if (toolBlock) return toolBlock;
365
1308
  const deleted = deleteSecret(params.key, opts(params));
366
1309
  return text(
367
1310
  deleted ? `Deleted "${params.key}"` : `Secret "${params.key}" not found`,
@@ -375,21 +1318,185 @@ function createMcpServer() {
375
1318
  {
376
1319
  key: z.string().describe("The secret key name"),
377
1320
  scope: scopeSchema,
378
- projectPath: projectPathSchema
1321
+ projectPath: projectPathSchema,
1322
+ teamId: teamIdSchema,
1323
+ orgId: orgIdSchema
379
1324
  },
380
1325
  async (params) => {
1326
+ const toolBlock = enforceToolPolicy("has_secret", params.projectPath);
1327
+ if (toolBlock) return toolBlock;
381
1328
  return text(hasSecret(params.key, opts(params)) ? "true" : "false");
382
1329
  }
383
1330
  );
1331
+ server2.tool(
1332
+ "export_secrets",
1333
+ "Export secrets as .env or JSON format. Collapses superposition. Supports filtering by specific keys or tags.",
1334
+ {
1335
+ format: z.enum(["env", "json"]).optional().default("env").describe("Output format"),
1336
+ keys: z.array(z.string()).optional().describe("Only export these specific key names"),
1337
+ tags: z.array(z.string()).optional().describe("Only export secrets with any of these tags"),
1338
+ scope: scopeSchema,
1339
+ projectPath: projectPathSchema,
1340
+ env: envSchema,
1341
+ teamId: teamIdSchema,
1342
+ orgId: orgIdSchema
1343
+ },
1344
+ async (params) => {
1345
+ const toolBlock = enforceToolPolicy("export_secrets", params.projectPath);
1346
+ if (toolBlock) return toolBlock;
1347
+ const output = exportSecrets({
1348
+ ...opts(params),
1349
+ format: params.format,
1350
+ keys: params.keys,
1351
+ tags: params.tags
1352
+ });
1353
+ if (!output.trim()) return text("No secrets matched the filters", true);
1354
+ return text(output);
1355
+ }
1356
+ );
1357
+ server2.tool(
1358
+ "import_dotenv",
1359
+ "Import secrets from .env file content. Parses standard dotenv syntax (comments, quotes, multiline escapes) and stores each key/value pair in q-ring.",
1360
+ {
1361
+ content: z.string().describe("The .env file content to parse and import"),
1362
+ scope: scopeSchema.default("global"),
1363
+ projectPath: projectPathSchema,
1364
+ skipExisting: z.boolean().optional().default(false).describe("Skip keys that already exist in q-ring"),
1365
+ dryRun: z.boolean().optional().default(false).describe("Preview what would be imported without saving")
1366
+ },
1367
+ async (params) => {
1368
+ const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath);
1369
+ if (toolBlock) return toolBlock;
1370
+ const result = importDotenv(params.content, {
1371
+ scope: params.scope,
1372
+ projectPath: params.projectPath ?? process.cwd(),
1373
+ source: "mcp",
1374
+ skipExisting: params.skipExisting,
1375
+ dryRun: params.dryRun
1376
+ });
1377
+ const lines = [
1378
+ params.dryRun ? "Dry run \u2014 no changes made" : `Imported ${result.imported.length} secret(s)`
1379
+ ];
1380
+ if (result.imported.length > 0) {
1381
+ lines.push(`Keys: ${result.imported.join(", ")}`);
1382
+ }
1383
+ if (result.skipped.length > 0) {
1384
+ lines.push(`Skipped (existing): ${result.skipped.join(", ")}`);
1385
+ }
1386
+ return text(lines.join("\n"));
1387
+ }
1388
+ );
1389
+ server2.tool(
1390
+ "check_project",
1391
+ "Validate project secrets against the .q-ring.json manifest. Returns which required secrets are present, missing, expired, or stale. Use this to verify project readiness.",
1392
+ {
1393
+ projectPath: projectPathSchema
1394
+ },
1395
+ async (params) => {
1396
+ const toolBlock = enforceToolPolicy("check_project", params.projectPath);
1397
+ if (toolBlock) return toolBlock;
1398
+ const projectPath = params.projectPath ?? process.cwd();
1399
+ const config = readProjectConfig(projectPath);
1400
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1401
+ return text("No secrets manifest found in .q-ring.json", true);
1402
+ }
1403
+ const results = [];
1404
+ let presentCount = 0;
1405
+ let missingCount = 0;
1406
+ let expiredCount = 0;
1407
+ let staleCount = 0;
1408
+ for (const [key, manifest] of Object.entries(config.secrets)) {
1409
+ const result = getEnvelope(key, { projectPath, source: "mcp" });
1410
+ if (!result) {
1411
+ const status = manifest.required !== false ? "missing" : "optional_missing";
1412
+ if (manifest.required !== false) missingCount++;
1413
+ results.push({ key, status, required: manifest.required !== false, description: manifest.description });
1414
+ continue;
1415
+ }
1416
+ const decay = checkDecay(result.envelope);
1417
+ if (decay.isExpired) {
1418
+ expiredCount++;
1419
+ results.push({ key, status: "expired", timeRemaining: decay.timeRemaining, description: manifest.description });
1420
+ } else if (decay.isStale) {
1421
+ staleCount++;
1422
+ results.push({ key, status: "stale", lifetimePercent: decay.lifetimePercent, timeRemaining: decay.timeRemaining, description: manifest.description });
1423
+ } else {
1424
+ presentCount++;
1425
+ results.push({ key, status: "ok", description: manifest.description });
1426
+ }
1427
+ }
1428
+ const summary = {
1429
+ total: Object.keys(config.secrets).length,
1430
+ present: presentCount,
1431
+ missing: missingCount,
1432
+ expired: expiredCount,
1433
+ stale: staleCount,
1434
+ ready: missingCount === 0 && expiredCount === 0,
1435
+ secrets: results
1436
+ };
1437
+ return text(JSON.stringify(summary, null, 2));
1438
+ }
1439
+ );
1440
+ server2.tool(
1441
+ "env_generate",
1442
+ "Generate .env file content from the project manifest (.q-ring.json). Resolves each declared secret from q-ring, collapses superposition, and returns .env formatted output. Warns about missing or expired secrets.",
1443
+ {
1444
+ projectPath: projectPathSchema,
1445
+ env: envSchema
1446
+ },
1447
+ async (params) => {
1448
+ const toolBlock = enforceToolPolicy("env_generate", params.projectPath);
1449
+ if (toolBlock) return toolBlock;
1450
+ const projectPath = params.projectPath ?? process.cwd();
1451
+ const config = readProjectConfig(projectPath);
1452
+ if (!config?.secrets || Object.keys(config.secrets).length === 0) {
1453
+ return text("No secrets manifest found in .q-ring.json", true);
1454
+ }
1455
+ const lines = [];
1456
+ const warnings = [];
1457
+ for (const [key, manifest] of Object.entries(config.secrets)) {
1458
+ const value = getSecret(key, {
1459
+ projectPath,
1460
+ env: params.env,
1461
+ source: "mcp"
1462
+ });
1463
+ if (value === null) {
1464
+ if (manifest.required !== false) {
1465
+ warnings.push(`MISSING (required): ${key}`);
1466
+ }
1467
+ lines.push(`# ${key}=`);
1468
+ continue;
1469
+ }
1470
+ const result2 = getEnvelope(key, { projectPath, source: "mcp" });
1471
+ if (result2) {
1472
+ const decay = checkDecay(result2.envelope);
1473
+ if (decay.isExpired) warnings.push(`EXPIRED: ${key}`);
1474
+ else if (decay.isStale) warnings.push(`STALE: ${key}`);
1475
+ }
1476
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
1477
+ lines.push(`${key}="${escaped}"`);
1478
+ }
1479
+ const output = lines.join("\n");
1480
+ const result = warnings.length > 0 ? `${output}
1481
+
1482
+ # Warnings:
1483
+ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1484
+ return text(result);
1485
+ }
1486
+ );
384
1487
  server2.tool(
385
1488
  "inspect_secret",
386
1489
  "Show full quantum state of a secret: superposition states, decay status, entanglement links, access history. Never reveals the actual value.",
387
1490
  {
388
1491
  key: z.string().describe("The secret key name"),
389
1492
  scope: scopeSchema,
390
- projectPath: projectPathSchema
1493
+ projectPath: projectPathSchema,
1494
+ teamId: teamIdSchema,
1495
+ orgId: orgIdSchema
391
1496
  },
392
1497
  async (params) => {
1498
+ const toolBlock = enforceToolPolicy("inspect_secret", params.projectPath);
1499
+ if (toolBlock) return toolBlock;
393
1500
  const result = getEnvelope(params.key, opts(params));
394
1501
  if (!result) return text(`Secret "${params.key}" not found`, true);
395
1502
  const { envelope, scope } = result;
@@ -430,6 +1537,8 @@ function createMcpServer() {
430
1537
  projectPath: projectPathSchema
431
1538
  },
432
1539
  async (params) => {
1540
+ const toolBlock = enforceToolPolicy("detect_environment", params.projectPath);
1541
+ if (toolBlock) return toolBlock;
433
1542
  const result = collapseEnvironment({
434
1543
  projectPath: params.projectPath ?? process.cwd()
435
1544
  });
@@ -450,9 +1559,13 @@ function createMcpServer() {
450
1559
  prefix: z.string().optional().describe("Prefix for api-key/token format"),
451
1560
  saveAs: z.string().optional().describe("If provided, save the generated secret with this key name"),
452
1561
  scope: scopeSchema.default("global"),
453
- projectPath: projectPathSchema
1562
+ projectPath: projectPathSchema,
1563
+ teamId: teamIdSchema,
1564
+ orgId: orgIdSchema
454
1565
  },
455
1566
  async (params) => {
1567
+ const toolBlock = enforceToolPolicy("generate_secret", params.projectPath);
1568
+ if (toolBlock) return toolBlock;
456
1569
  const secret = generateSecret({
457
1570
  format: params.format,
458
1571
  length: params.length,
@@ -483,6 +1596,8 @@ function createMcpServer() {
483
1596
  targetProjectPath: z.string().optional()
484
1597
  },
485
1598
  async (params) => {
1599
+ const toolBlock = enforceToolPolicy("entangle_secrets", params.sourceProjectPath);
1600
+ if (toolBlock) return toolBlock;
486
1601
  entangleSecrets(
487
1602
  params.sourceKey,
488
1603
  {
@@ -500,6 +1615,37 @@ function createMcpServer() {
500
1615
  return text(`Entangled: ${params.sourceKey} <-> ${params.targetKey}`);
501
1616
  }
502
1617
  );
1618
+ server2.tool(
1619
+ "disentangle_secrets",
1620
+ "Remove a quantum entanglement between two secrets. They will no longer synchronize on rotation.",
1621
+ {
1622
+ sourceKey: z.string().describe("Source secret key"),
1623
+ targetKey: z.string().describe("Target secret key"),
1624
+ sourceScope: scopeSchema.default("global"),
1625
+ targetScope: scopeSchema.default("global"),
1626
+ sourceProjectPath: z.string().optional(),
1627
+ targetProjectPath: z.string().optional()
1628
+ },
1629
+ async (params) => {
1630
+ const toolBlock = enforceToolPolicy("disentangle_secrets", params.sourceProjectPath);
1631
+ if (toolBlock) return toolBlock;
1632
+ disentangleSecrets(
1633
+ params.sourceKey,
1634
+ {
1635
+ scope: params.sourceScope,
1636
+ projectPath: params.sourceProjectPath ?? process.cwd(),
1637
+ source: "mcp"
1638
+ },
1639
+ params.targetKey,
1640
+ {
1641
+ scope: params.targetScope,
1642
+ projectPath: params.targetProjectPath ?? process.cwd(),
1643
+ source: "mcp"
1644
+ }
1645
+ );
1646
+ return text(`Disentangled: ${params.sourceKey} </> ${params.targetKey}`);
1647
+ }
1648
+ );
503
1649
  server2.tool(
504
1650
  "tunnel_create",
505
1651
  "Create an ephemeral secret that exists only in memory (quantum tunneling). Never persisted to disk. Optional TTL and max-reads for self-destruction.",
@@ -509,6 +1655,8 @@ function createMcpServer() {
509
1655
  maxReads: z.number().optional().describe("Self-destruct after N reads")
510
1656
  },
511
1657
  async (params) => {
1658
+ const toolBlock = enforceToolPolicy("tunnel_create");
1659
+ if (toolBlock) return toolBlock;
512
1660
  const id = tunnelCreate(params.value, {
513
1661
  ttlSeconds: params.ttlSeconds,
514
1662
  maxReads: params.maxReads
@@ -523,6 +1671,8 @@ function createMcpServer() {
523
1671
  id: z.string().describe("Tunnel ID")
524
1672
  },
525
1673
  async (params) => {
1674
+ const toolBlock = enforceToolPolicy("tunnel_read");
1675
+ if (toolBlock) return toolBlock;
526
1676
  const value = tunnelRead(params.id);
527
1677
  if (value === null) {
528
1678
  return text(`Tunnel "${params.id}" not found or expired`, true);
@@ -535,6 +1685,8 @@ function createMcpServer() {
535
1685
  "List active tunneled secrets (IDs and metadata only, never values).",
536
1686
  {},
537
1687
  async () => {
1688
+ const toolBlock = enforceToolPolicy("tunnel_list");
1689
+ if (toolBlock) return toolBlock;
538
1690
  const tunnels = tunnelList();
539
1691
  if (tunnels.length === 0) return text("No active tunnels");
540
1692
  const lines = tunnels.map((t) => {
@@ -557,6 +1709,8 @@ function createMcpServer() {
557
1709
  id: z.string().describe("Tunnel ID")
558
1710
  },
559
1711
  async (params) => {
1712
+ const toolBlock = enforceToolPolicy("tunnel_destroy");
1713
+ if (toolBlock) return toolBlock;
560
1714
  const destroyed = tunnelDestroy(params.id);
561
1715
  return text(
562
1716
  destroyed ? `Destroyed ${params.id}` : `Tunnel "${params.id}" not found`,
@@ -571,9 +1725,13 @@ function createMcpServer() {
571
1725
  keys: z.array(z.string()).optional().describe("Specific keys to pack (all if omitted)"),
572
1726
  passphrase: z.string().describe("Encryption passphrase"),
573
1727
  scope: scopeSchema,
574
- projectPath: projectPathSchema
1728
+ projectPath: projectPathSchema,
1729
+ teamId: teamIdSchema,
1730
+ orgId: orgIdSchema
575
1731
  },
576
1732
  async (params) => {
1733
+ const toolBlock = enforceToolPolicy("teleport_pack", params.projectPath);
1734
+ if (toolBlock) return toolBlock;
577
1735
  const o = opts(params);
578
1736
  const entries = listSecrets(o);
579
1737
  const secrets = [];
@@ -597,9 +1755,13 @@ function createMcpServer() {
597
1755
  passphrase: z.string().describe("Decryption passphrase"),
598
1756
  scope: scopeSchema.default("global"),
599
1757
  projectPath: projectPathSchema,
1758
+ teamId: teamIdSchema,
1759
+ orgId: orgIdSchema,
600
1760
  dryRun: z.boolean().optional().default(false).describe("Preview without importing")
601
1761
  },
602
1762
  async (params) => {
1763
+ const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
1764
+ if (toolBlock) return toolBlock;
603
1765
  try {
604
1766
  const payload = teleportUnpack(params.bundle, params.passphrase);
605
1767
  if (params.dryRun) {
@@ -626,6 +1788,8 @@ ${preview}`);
626
1788
  limit: z.number().optional().default(20).describe("Max events to return")
627
1789
  },
628
1790
  async (params) => {
1791
+ const toolBlock = enforceToolPolicy("audit_log");
1792
+ if (toolBlock) return toolBlock;
629
1793
  const events = queryAudit({
630
1794
  key: params.key,
631
1795
  action: params.action,
@@ -650,6 +1814,8 @@ ${preview}`);
650
1814
  key: z.string().optional().describe("Check anomalies for a specific key")
651
1815
  },
652
1816
  async (params) => {
1817
+ const toolBlock = enforceToolPolicy("detect_anomalies");
1818
+ if (toolBlock) return toolBlock;
653
1819
  const anomalies = detectAnomalies(params.key);
654
1820
  if (anomalies.length === 0) return text("No anomalies detected");
655
1821
  const lines = anomalies.map(
@@ -663,9 +1829,13 @@ ${preview}`);
663
1829
  "Run a comprehensive health check on all secrets: decay status, staleness, anomalies, entropy assessment.",
664
1830
  {
665
1831
  scope: scopeSchema,
666
- projectPath: projectPathSchema
1832
+ projectPath: projectPathSchema,
1833
+ teamId: teamIdSchema,
1834
+ orgId: orgIdSchema
667
1835
  },
668
1836
  async (params) => {
1837
+ const toolBlock = enforceToolPolicy("health_check", params.projectPath);
1838
+ if (toolBlock) return toolBlock;
669
1839
  const entries = listSecrets(opts(params));
670
1840
  const anomalies = detectAnomalies();
671
1841
  let healthy = 0;
@@ -708,6 +1878,299 @@ ${preview}`);
708
1878
  return text(summary.join("\n"));
709
1879
  }
710
1880
  );
1881
+ server2.tool(
1882
+ "validate_secret",
1883
+ "Test if a secret is actually valid with its target service (e.g., OpenAI, Stripe, GitHub). Uses provider auto-detection based on key prefixes, or accepts an explicit provider name. Never logs the secret value.",
1884
+ {
1885
+ key: z.string().describe("The secret key name"),
1886
+ provider: z.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
1887
+ scope: scopeSchema,
1888
+ projectPath: projectPathSchema,
1889
+ teamId: teamIdSchema,
1890
+ orgId: orgIdSchema
1891
+ },
1892
+ async (params) => {
1893
+ const toolBlock = enforceToolPolicy("validate_secret", params.projectPath);
1894
+ if (toolBlock) return toolBlock;
1895
+ const value = getSecret(params.key, opts(params));
1896
+ if (value === null) return text(`Secret "${params.key}" not found`, true);
1897
+ const envelope = getEnvelope(params.key, opts(params));
1898
+ const provHint = params.provider ?? envelope?.envelope.meta.provider;
1899
+ const result = await validateSecret(value, { provider: provHint });
1900
+ return text(JSON.stringify(result, null, 2));
1901
+ }
1902
+ );
1903
+ server2.tool(
1904
+ "list_providers",
1905
+ "List all available validation providers for secret liveness testing.",
1906
+ {},
1907
+ async () => {
1908
+ const toolBlock = enforceToolPolicy("list_providers");
1909
+ if (toolBlock) return toolBlock;
1910
+ const providers = registry2.listProviders().map((p) => ({
1911
+ name: p.name,
1912
+ description: p.description,
1913
+ prefixes: p.prefixes ?? []
1914
+ }));
1915
+ return text(JSON.stringify(providers, null, 2));
1916
+ }
1917
+ );
1918
+ server2.tool(
1919
+ "register_hook",
1920
+ "Register a webhook/callback that fires when a secret is updated, deleted, or rotated. Supports shell commands, HTTP webhooks, and process signals.",
1921
+ {
1922
+ type: z.enum(["shell", "http", "signal"]).describe("Hook type"),
1923
+ key: z.string().optional().describe("Trigger on exact key match"),
1924
+ keyPattern: z.string().optional().describe("Trigger on key glob pattern (e.g. DB_*)"),
1925
+ tag: z.string().optional().describe("Trigger on secrets with this tag"),
1926
+ scope: z.enum(["global", "project"]).optional().describe("Trigger only for this scope"),
1927
+ actions: z.array(z.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe("Which actions trigger this hook"),
1928
+ command: z.string().optional().describe("Shell command to execute (for shell type)"),
1929
+ url: z.string().optional().describe("URL to POST to (for http type)"),
1930
+ signalTarget: z.string().optional().describe("Process name or PID (for signal type)"),
1931
+ signalName: z.string().optional().default("SIGHUP").describe("Signal to send (for signal type)"),
1932
+ description: z.string().optional().describe("Human-readable description")
1933
+ },
1934
+ async (params) => {
1935
+ const toolBlock = enforceToolPolicy("register_hook");
1936
+ if (toolBlock) return toolBlock;
1937
+ if (!params.key && !params.keyPattern && !params.tag) {
1938
+ return text("At least one match criterion required: key, keyPattern, or tag", true);
1939
+ }
1940
+ const entry = registerHook({
1941
+ type: params.type,
1942
+ match: {
1943
+ key: params.key,
1944
+ keyPattern: params.keyPattern,
1945
+ tag: params.tag,
1946
+ scope: params.scope,
1947
+ action: params.actions
1948
+ },
1949
+ command: params.command,
1950
+ url: params.url,
1951
+ signal: params.signalTarget ? { target: params.signalTarget, signal: params.signalName } : void 0,
1952
+ description: params.description,
1953
+ enabled: true
1954
+ });
1955
+ return text(JSON.stringify(entry, null, 2));
1956
+ }
1957
+ );
1958
+ server2.tool(
1959
+ "list_hooks",
1960
+ "List all registered secret change hooks with their match criteria, type, and status.",
1961
+ {},
1962
+ async () => {
1963
+ const toolBlock = enforceToolPolicy("list_hooks");
1964
+ if (toolBlock) return toolBlock;
1965
+ const hooks = listHooks();
1966
+ if (hooks.length === 0) return text("No hooks registered");
1967
+ return text(JSON.stringify(hooks, null, 2));
1968
+ }
1969
+ );
1970
+ server2.tool(
1971
+ "remove_hook",
1972
+ "Remove a registered hook by ID.",
1973
+ {
1974
+ id: z.string().describe("Hook ID to remove")
1975
+ },
1976
+ async (params) => {
1977
+ const toolBlock = enforceToolPolicy("remove_hook");
1978
+ if (toolBlock) return toolBlock;
1979
+ const removed = removeHook(params.id);
1980
+ return text(
1981
+ removed ? `Removed hook ${params.id}` : `Hook "${params.id}" not found`,
1982
+ !removed
1983
+ );
1984
+ }
1985
+ );
1986
+ server2.tool(
1987
+ "exec_with_secrets",
1988
+ "Run a shell command securely. Project secrets are injected into the environment, and any secret values in the output are automatically redacted to prevent leaking into transcripts.",
1989
+ {
1990
+ command: z.string().describe("Command to run"),
1991
+ args: z.array(z.string()).optional().describe("Command arguments"),
1992
+ keys: z.array(z.string()).optional().describe("Only inject these specific keys"),
1993
+ tags: z.array(z.string()).optional().describe("Only inject secrets with these tags"),
1994
+ profile: z.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe("Exec profile: unrestricted, restricted, or ci"),
1995
+ scope: scopeSchema,
1996
+ projectPath: projectPathSchema,
1997
+ teamId: teamIdSchema,
1998
+ orgId: orgIdSchema
1999
+ },
2000
+ async (params) => {
2001
+ const toolBlock = enforceToolPolicy("exec_with_secrets", params.projectPath);
2002
+ if (toolBlock) return toolBlock;
2003
+ const execBlock = checkExecPolicy(params.command, params.projectPath);
2004
+ if (!execBlock.allowed) {
2005
+ return text(`Policy Denied: ${execBlock.reason}`, true);
2006
+ }
2007
+ try {
2008
+ const result = await execCommand({
2009
+ command: params.command,
2010
+ args: params.args ?? [],
2011
+ keys: params.keys,
2012
+ tags: params.tags,
2013
+ profile: params.profile,
2014
+ scope: params.scope,
2015
+ projectPath: params.projectPath,
2016
+ source: "mcp",
2017
+ captureOutput: true
2018
+ });
2019
+ const output = [];
2020
+ output.push(`Exit code: ${result.code}`);
2021
+ if (result.stdout) output.push(`STDOUT:
2022
+ ${result.stdout}`);
2023
+ if (result.stderr) output.push(`STDERR:
2024
+ ${result.stderr}`);
2025
+ return text(output.join("\n\n"));
2026
+ } catch (err) {
2027
+ return text(`Execution failed: ${err instanceof Error ? err.message : String(err)}`, true);
2028
+ }
2029
+ }
2030
+ );
2031
+ server2.tool(
2032
+ "scan_codebase_for_secrets",
2033
+ "Scan a directory for hardcoded secrets using regex heuristics and Shannon entropy analysis. Returns file paths, line numbers, and the matched key/value to help migrate legacy codebases into q-ring.",
2034
+ {
2035
+ dirPath: z.string().describe("Absolute or relative path to the directory to scan")
2036
+ },
2037
+ async (params) => {
2038
+ const toolBlock = enforceToolPolicy("scan_codebase_for_secrets");
2039
+ if (toolBlock) return toolBlock;
2040
+ try {
2041
+ const results = scanCodebase(params.dirPath);
2042
+ if (results.length === 0) {
2043
+ return text("No hardcoded secrets found in the specified directory.");
2044
+ }
2045
+ return text(JSON.stringify(results, null, 2));
2046
+ } catch (err) {
2047
+ return text(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
2048
+ }
2049
+ }
2050
+ );
2051
+ server2.tool(
2052
+ "get_project_context",
2053
+ "Get a safe, redacted overview of the project's secrets, environment, manifest, providers, hooks, and recent audit activity. No secret values are ever exposed. Use this to understand what secrets exist before asking to read them.",
2054
+ {
2055
+ scope: scopeSchema,
2056
+ projectPath: projectPathSchema,
2057
+ teamId: teamIdSchema,
2058
+ orgId: orgIdSchema
2059
+ },
2060
+ async (params) => {
2061
+ const toolBlock = enforceToolPolicy("get_project_context", params.projectPath);
2062
+ if (toolBlock) return toolBlock;
2063
+ const context = getProjectContext(opts(params));
2064
+ return text(JSON.stringify(context, null, 2));
2065
+ }
2066
+ );
2067
+ server2.tool(
2068
+ "agent_remember",
2069
+ "Store a key-value pair in encrypted agent memory that persists across sessions. Use this to remember decisions, rotation history, or project-specific context.",
2070
+ {
2071
+ key: z.string().describe("Memory key"),
2072
+ value: z.string().describe("Value to store")
2073
+ },
2074
+ async (params) => {
2075
+ const toolBlock = enforceToolPolicy("agent_remember");
2076
+ if (toolBlock) return toolBlock;
2077
+ remember(params.key, params.value);
2078
+ return text(`Remembered "${params.key}"`);
2079
+ }
2080
+ );
2081
+ server2.tool(
2082
+ "agent_recall",
2083
+ "Retrieve a value from agent memory, or list all stored keys if no key is provided.",
2084
+ {
2085
+ key: z.string().optional().describe("Memory key to recall (omit to list all)")
2086
+ },
2087
+ async (params) => {
2088
+ const toolBlock = enforceToolPolicy("agent_recall");
2089
+ if (toolBlock) return toolBlock;
2090
+ if (!params.key) {
2091
+ const entries = listMemory();
2092
+ if (entries.length === 0) return text("Agent memory is empty");
2093
+ return text(JSON.stringify(entries, null, 2));
2094
+ }
2095
+ const value = recall(params.key);
2096
+ if (value === null) return text(`No memory found for "${params.key}"`, true);
2097
+ return text(value);
2098
+ }
2099
+ );
2100
+ server2.tool(
2101
+ "agent_forget",
2102
+ "Delete a key from agent memory.",
2103
+ {
2104
+ key: z.string().describe("Memory key to forget")
2105
+ },
2106
+ async (params) => {
2107
+ const toolBlock = enforceToolPolicy("agent_forget");
2108
+ if (toolBlock) return toolBlock;
2109
+ const removed = forget(params.key);
2110
+ return text(removed ? `Forgot "${params.key}"` : `No memory found for "${params.key}"`, !removed);
2111
+ }
2112
+ );
2113
+ server2.tool(
2114
+ "lint_files",
2115
+ "Scan specific files for hardcoded secrets. Optionally auto-fix by replacing them with process.env references and storing the values in q-ring.",
2116
+ {
2117
+ files: z.array(z.string()).describe("File paths to lint"),
2118
+ fix: z.boolean().optional().default(false).describe("Auto-replace and store secrets"),
2119
+ scope: scopeSchema,
2120
+ projectPath: projectPathSchema,
2121
+ teamId: teamIdSchema,
2122
+ orgId: orgIdSchema
2123
+ },
2124
+ async (params) => {
2125
+ const toolBlock = enforceToolPolicy("lint_files", params.projectPath);
2126
+ if (toolBlock) return toolBlock;
2127
+ try {
2128
+ const results = lintFiles(params.files, {
2129
+ fix: params.fix,
2130
+ scope: params.scope,
2131
+ projectPath: params.projectPath
2132
+ });
2133
+ if (results.length === 0) {
2134
+ return text("No hardcoded secrets found in the specified files.");
2135
+ }
2136
+ return text(JSON.stringify(results, null, 2));
2137
+ } catch (err) {
2138
+ return text(`Lint failed: ${err instanceof Error ? err.message : String(err)}`, true);
2139
+ }
2140
+ }
2141
+ );
2142
+ server2.tool(
2143
+ "analyze_secrets",
2144
+ "Analyze secret usage patterns and provide optimization suggestions including most accessed, stale, unused, and rotation recommendations.",
2145
+ {
2146
+ scope: scopeSchema,
2147
+ projectPath: projectPathSchema,
2148
+ teamId: teamIdSchema,
2149
+ orgId: orgIdSchema
2150
+ },
2151
+ async (params) => {
2152
+ const toolBlock = enforceToolPolicy("analyze_secrets", params.projectPath);
2153
+ if (toolBlock) return toolBlock;
2154
+ const o = opts(params);
2155
+ const entries = listSecrets({ ...o, silent: true });
2156
+ const audit = queryAudit({ limit: 500 });
2157
+ const accessMap = /* @__PURE__ */ new Map();
2158
+ for (const e of audit) {
2159
+ if (e.action === "read" && e.key) {
2160
+ accessMap.set(e.key, (accessMap.get(e.key) || 0) + 1);
2161
+ }
2162
+ }
2163
+ const analysis = {
2164
+ total: entries.length,
2165
+ expired: entries.filter((e) => e.decay?.isExpired).length,
2166
+ stale: entries.filter((e) => e.decay?.isStale && !e.decay?.isExpired).length,
2167
+ neverAccessed: entries.filter((e) => (e.envelope?.meta.accessCount ?? 0) === 0).map((e) => e.key),
2168
+ noRotationFormat: entries.filter((e) => !e.envelope?.meta.rotationFormat).map((e) => e.key),
2169
+ mostAccessed: [...accessMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([key, count]) => ({ key, reads: count }))
2170
+ };
2171
+ return text(JSON.stringify(analysis, null, 2));
2172
+ }
2173
+ );
711
2174
  let dashboardInstance = null;
712
2175
  server2.tool(
713
2176
  "status_dashboard",
@@ -716,10 +2179,12 @@ ${preview}`);
716
2179
  port: z.number().optional().default(9876).describe("Port to serve on")
717
2180
  },
718
2181
  async (params) => {
2182
+ const toolBlock = enforceToolPolicy("status_dashboard");
2183
+ if (toolBlock) return toolBlock;
719
2184
  if (dashboardInstance) {
720
2185
  return text(`Dashboard already running at http://127.0.0.1:${dashboardInstance.port}`);
721
2186
  }
722
- const { startDashboardServer } = await import("./dashboard-QQWKOOI5.js");
2187
+ const { startDashboardServer } = await import("./dashboard-Q5OQRQCX.js");
723
2188
  dashboardInstance = startDashboardServer({ port: params.port });
724
2189
  return text(`Dashboard started at http://127.0.0.1:${dashboardInstance.port}
725
2190
  Open this URL in a browser to see live quantum status.`);
@@ -733,6 +2198,8 @@ Open this URL in a browser to see live quantum status.`);
733
2198
  projectPaths: z.array(z.string()).optional().describe("Project paths to monitor")
734
2199
  },
735
2200
  async (params) => {
2201
+ const toolBlock = enforceToolPolicy("agent_scan");
2202
+ if (toolBlock) return toolBlock;
736
2203
  const report = runHealthScan({
737
2204
  autoRotate: params.autoRotate,
738
2205
  projectPaths: params.projectPaths ?? [process.cwd()]
@@ -740,6 +2207,128 @@ Open this URL in a browser to see live quantum status.`);
740
2207
  return text(JSON.stringify(report, null, 2));
741
2208
  }
742
2209
  );
2210
+ server2.tool(
2211
+ "verify_audit_chain",
2212
+ "Verify the tamper-evident hash chain of the audit log. Returns integrity status and the first break point if tampered.",
2213
+ {},
2214
+ async () => {
2215
+ const toolBlock = enforceToolPolicy("verify_audit_chain");
2216
+ if (toolBlock) return toolBlock;
2217
+ const result = verifyAuditChain();
2218
+ return text(JSON.stringify(result, null, 2));
2219
+ }
2220
+ );
2221
+ server2.tool(
2222
+ "export_audit",
2223
+ "Export audit events in a portable format (jsonl, json, or csv) with optional time range filtering.",
2224
+ {
2225
+ since: z.string().optional().describe("Start date (ISO 8601)"),
2226
+ until: z.string().optional().describe("End date (ISO 8601)"),
2227
+ format: z.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe("Output format")
2228
+ },
2229
+ async (params) => {
2230
+ const toolBlock = enforceToolPolicy("export_audit");
2231
+ if (toolBlock) return toolBlock;
2232
+ const output = exportAudit({
2233
+ since: params.since,
2234
+ until: params.until,
2235
+ format: params.format
2236
+ });
2237
+ return text(output);
2238
+ }
2239
+ );
2240
+ server2.tool(
2241
+ "rotate_secret",
2242
+ "Attempt issuer-native rotation of a secret via its detected or specified provider. Returns rotation result.",
2243
+ {
2244
+ key: z.string().describe("The secret key to rotate"),
2245
+ provider: z.string().optional().describe("Force a specific provider"),
2246
+ scope: scopeSchema,
2247
+ projectPath: projectPathSchema,
2248
+ teamId: teamIdSchema,
2249
+ orgId: orgIdSchema
2250
+ },
2251
+ async (params) => {
2252
+ const toolBlock = enforceToolPolicy("rotate_secret", params.projectPath);
2253
+ if (toolBlock) return toolBlock;
2254
+ const value = getSecret(params.key, opts(params));
2255
+ if (!value) return text(`Secret "${params.key}" not found`, true);
2256
+ const result = await rotateWithProvider(value, params.provider);
2257
+ if (result.rotated && result.newValue) {
2258
+ setSecret(params.key, result.newValue, {
2259
+ scope: params.scope ?? "global",
2260
+ projectPath: params.projectPath,
2261
+ source: "mcp"
2262
+ });
2263
+ }
2264
+ return text(JSON.stringify(result, null, 2));
2265
+ }
2266
+ );
2267
+ server2.tool(
2268
+ "ci_validate_secrets",
2269
+ "CI-oriented batch validation: validates all accessible secrets against their providers and returns a structured pass/fail report.",
2270
+ {
2271
+ scope: scopeSchema,
2272
+ projectPath: projectPathSchema,
2273
+ teamId: teamIdSchema,
2274
+ orgId: orgIdSchema
2275
+ },
2276
+ async (params) => {
2277
+ const toolBlock = enforceToolPolicy("ci_validate_secrets", params.projectPath);
2278
+ if (toolBlock) return toolBlock;
2279
+ const entries = listSecrets(opts(params));
2280
+ const secrets = entries.map((e) => {
2281
+ const val = getSecret(e.key, { ...opts(params), scope: e.scope, silent: true });
2282
+ if (!val) return null;
2283
+ return {
2284
+ key: e.key,
2285
+ value: val,
2286
+ provider: e.envelope?.meta.provider,
2287
+ validationUrl: e.envelope?.meta.validationUrl
2288
+ };
2289
+ }).filter((s) => s !== null);
2290
+ if (secrets.length === 0) return text("No secrets to validate");
2291
+ const report = await ciValidateBatch(secrets);
2292
+ return text(JSON.stringify(report, null, 2));
2293
+ }
2294
+ );
2295
+ server2.tool(
2296
+ "check_policy",
2297
+ "Check if an action is allowed by the project's governance policy. Returns the policy decision and source.",
2298
+ {
2299
+ action: z.enum(["tool", "key_read", "exec"]).describe("Type of policy check"),
2300
+ toolName: z.string().optional().describe("Tool name to check (for action=tool)"),
2301
+ key: z.string().optional().describe("Secret key to check (for action=key_read)"),
2302
+ command: z.string().optional().describe("Command to check (for action=exec)"),
2303
+ projectPath: projectPathSchema
2304
+ },
2305
+ async (params) => {
2306
+ if (params.action === "tool" && params.toolName) {
2307
+ const d = checkToolPolicy(params.toolName, params.projectPath);
2308
+ return text(JSON.stringify(d, null, 2));
2309
+ }
2310
+ if (params.action === "key_read" && params.key) {
2311
+ const d = checkKeyReadPolicy(params.key, void 0, params.projectPath);
2312
+ return text(JSON.stringify(d, null, 2));
2313
+ }
2314
+ if (params.action === "exec" && params.command) {
2315
+ const d = checkExecPolicy(params.command, params.projectPath);
2316
+ return text(JSON.stringify(d, null, 2));
2317
+ }
2318
+ return text("Missing required parameter for the selected action type", true);
2319
+ }
2320
+ );
2321
+ server2.tool(
2322
+ "get_policy_summary",
2323
+ "Get a summary of the project's governance policy configuration.",
2324
+ {
2325
+ projectPath: projectPathSchema
2326
+ },
2327
+ async (params) => {
2328
+ const summary = getPolicySummary(params.projectPath);
2329
+ return text(JSON.stringify(summary, null, 2));
2330
+ }
2331
+ );
743
2332
  return server2;
744
2333
  }
745
2334