@i4ctime/q-ring 0.4.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -1,29 +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,
7
10
  disentangleSecrets,
8
11
  entangleSecrets,
12
+ exportAudit,
9
13
  exportSecrets,
10
14
  fireHooks,
11
15
  getEnvelope,
16
+ getExecMaxRuntime,
17
+ getPolicySummary,
12
18
  getSecret,
13
19
  hasSecret,
20
+ httpRequest_,
14
21
  listHooks,
15
22
  listSecrets,
16
23
  logAudit,
17
24
  queryAudit,
18
25
  readProjectConfig,
19
26
  registerHook,
27
+ registry,
20
28
  removeHook,
21
29
  setSecret,
22
30
  tunnelCreate,
23
31
  tunnelDestroy,
24
32
  tunnelList,
25
- tunnelRead
26
- } from "./chunk-6IQ5SFLI.js";
33
+ tunnelRead,
34
+ verifyAuditChain
35
+ } from "./chunk-5JBU7TWN.js";
27
36
 
28
37
  // src/mcp.ts
29
38
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -317,33 +326,443 @@ function importDotenv(filePathOrContent, options = {}) {
317
326
  return result;
318
327
  }
319
328
 
320
- // src/core/validate.ts
321
- import { request as httpsRequest } from "https";
322
- import { request as httpRequest } from "http";
323
- function makeRequest(url, headers, timeoutMs = 1e4) {
324
- return new Promise((resolve, reject) => {
325
- const parsedUrl = new URL(url);
326
- const reqFn = parsedUrl.protocol === "https:" ? httpsRequest : httpRequest;
327
- const req = reqFn(
328
- url,
329
- { method: "GET", headers, timeout: timeoutMs },
330
- (res) => {
331
- let body = "";
332
- res.on("data", (chunk) => body += chunk);
333
- res.on(
334
- "end",
335
- () => resolve({ statusCode: res.statusCode ?? 0, body })
336
- );
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);
337
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))
338
442
  );
339
- req.on("error", reject);
340
- req.on("timeout", () => {
341
- req.destroy();
342
- reject(new Error("Request timed out"));
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);
343
524
  });
344
- req.end();
345
525
  });
346
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
+ }
347
766
  var ProviderRegistry = class {
348
767
  providers = /* @__PURE__ */ new Map();
349
768
  register(provider) {
@@ -490,14 +909,14 @@ var httpProvider = {
490
909
  }
491
910
  }
492
911
  };
493
- var registry = new ProviderRegistry();
494
- registry.register(openaiProvider);
495
- registry.register(stripeProvider);
496
- registry.register(githubProvider);
497
- registry.register(awsProvider);
498
- registry.register(httpProvider);
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);
499
918
  async function validateSecret(value, opts2) {
500
- const provider = opts2?.provider ? registry.get(opts2.provider) : registry.detectProvider(value);
919
+ const provider = opts2?.provider ? registry2.get(opts2.provider) : registry2.detectProvider(value);
501
920
  if (!provider) {
502
921
  return {
503
922
  valid: false,
@@ -512,6 +931,192 @@ async function validateSecret(value, opts2) {
512
931
  }
513
932
  return provider.validate(value);
514
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
+ }
515
1120
 
516
1121
  // src/mcp/server.ts
517
1122
  function text(t, isError = false) {
@@ -524,16 +1129,27 @@ function opts(params) {
524
1129
  return {
525
1130
  scope: params.scope,
526
1131
  projectPath: params.projectPath ?? process.cwd(),
1132
+ teamId: params.teamId,
1133
+ orgId: params.orgId,
527
1134
  env: params.env,
528
1135
  source: "mcp"
529
1136
  };
530
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
+ }
531
1145
  function createMcpServer() {
532
1146
  const server2 = new McpServer({
533
1147
  name: "q-ring",
534
1148
  version: "0.2.0"
535
1149
  });
536
- 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");
537
1153
  const projectPathSchema = z.string().optional().describe("Project root path for project-scoped secrets");
538
1154
  const envSchema = z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)");
539
1155
  server2.tool(
@@ -543,12 +1159,24 @@ function createMcpServer() {
543
1159
  key: z.string().describe("The secret key name"),
544
1160
  scope: scopeSchema,
545
1161
  projectPath: projectPathSchema,
546
- env: envSchema
1162
+ env: envSchema,
1163
+ teamId: teamIdSchema,
1164
+ orgId: orgIdSchema
547
1165
  },
548
1166
  async (params) => {
549
- const value = getSecret(params.key, opts(params));
550
- if (value === null) return text(`Secret "${params.key}" not found`, true);
551
- 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
+ }
552
1180
  }
553
1181
  );
554
1182
  server2.tool(
@@ -560,9 +1188,13 @@ function createMcpServer() {
560
1188
  tag: z.string().optional().describe("Filter by tag"),
561
1189
  expired: z.boolean().optional().describe("Show only expired secrets"),
562
1190
  stale: z.boolean().optional().describe("Show only stale secrets (75%+ decay)"),
563
- filter: z.string().optional().describe("Glob pattern on key name (e.g., 'API_*')")
1191
+ filter: z.string().optional().describe("Glob pattern on key name (e.g., 'API_*')"),
1192
+ teamId: teamIdSchema,
1193
+ orgId: orgIdSchema
564
1194
  },
565
1195
  async (params) => {
1196
+ const toolBlock = enforceToolPolicy("list_secrets", params.projectPath);
1197
+ if (toolBlock) return toolBlock;
566
1198
  let entries = listSecrets(opts(params));
567
1199
  if (params.tag) {
568
1200
  entries = entries.filter(
@@ -622,9 +1254,13 @@ function createMcpServer() {
622
1254
  description: z.string().optional().describe("Human-readable description"),
623
1255
  tags: z.array(z.string()).optional().describe("Tags for organization"),
624
1256
  rotationFormat: z.enum(["hex", "base64", "alphanumeric", "uuid", "api-key", "token", "password"]).optional().describe("Format for auto-rotation when this secret expires"),
625
- rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')")
1257
+ rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')"),
1258
+ teamId: teamIdSchema,
1259
+ orgId: orgIdSchema
626
1260
  },
627
1261
  async (params) => {
1262
+ const toolBlock = enforceToolPolicy("set_secret", params.projectPath);
1263
+ if (toolBlock) return toolBlock;
628
1264
  const o = opts(params);
629
1265
  if (params.env) {
630
1266
  const existing = getEnvelope(params.key, o);
@@ -662,9 +1298,13 @@ function createMcpServer() {
662
1298
  {
663
1299
  key: z.string().describe("The secret key name"),
664
1300
  scope: scopeSchema,
665
- projectPath: projectPathSchema
1301
+ projectPath: projectPathSchema,
1302
+ teamId: teamIdSchema,
1303
+ orgId: orgIdSchema
666
1304
  },
667
1305
  async (params) => {
1306
+ const toolBlock = enforceToolPolicy("delete_secret", params.projectPath);
1307
+ if (toolBlock) return toolBlock;
668
1308
  const deleted = deleteSecret(params.key, opts(params));
669
1309
  return text(
670
1310
  deleted ? `Deleted "${params.key}"` : `Secret "${params.key}" not found`,
@@ -678,9 +1318,13 @@ function createMcpServer() {
678
1318
  {
679
1319
  key: z.string().describe("The secret key name"),
680
1320
  scope: scopeSchema,
681
- projectPath: projectPathSchema
1321
+ projectPath: projectPathSchema,
1322
+ teamId: teamIdSchema,
1323
+ orgId: orgIdSchema
682
1324
  },
683
1325
  async (params) => {
1326
+ const toolBlock = enforceToolPolicy("has_secret", params.projectPath);
1327
+ if (toolBlock) return toolBlock;
684
1328
  return text(hasSecret(params.key, opts(params)) ? "true" : "false");
685
1329
  }
686
1330
  );
@@ -693,9 +1337,13 @@ function createMcpServer() {
693
1337
  tags: z.array(z.string()).optional().describe("Only export secrets with any of these tags"),
694
1338
  scope: scopeSchema,
695
1339
  projectPath: projectPathSchema,
696
- env: envSchema
1340
+ env: envSchema,
1341
+ teamId: teamIdSchema,
1342
+ orgId: orgIdSchema
697
1343
  },
698
1344
  async (params) => {
1345
+ const toolBlock = enforceToolPolicy("export_secrets", params.projectPath);
1346
+ if (toolBlock) return toolBlock;
699
1347
  const output = exportSecrets({
700
1348
  ...opts(params),
701
1349
  format: params.format,
@@ -717,6 +1365,8 @@ function createMcpServer() {
717
1365
  dryRun: z.boolean().optional().default(false).describe("Preview what would be imported without saving")
718
1366
  },
719
1367
  async (params) => {
1368
+ const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath);
1369
+ if (toolBlock) return toolBlock;
720
1370
  const result = importDotenv(params.content, {
721
1371
  scope: params.scope,
722
1372
  projectPath: params.projectPath ?? process.cwd(),
@@ -743,6 +1393,8 @@ function createMcpServer() {
743
1393
  projectPath: projectPathSchema
744
1394
  },
745
1395
  async (params) => {
1396
+ const toolBlock = enforceToolPolicy("check_project", params.projectPath);
1397
+ if (toolBlock) return toolBlock;
746
1398
  const projectPath = params.projectPath ?? process.cwd();
747
1399
  const config = readProjectConfig(projectPath);
748
1400
  if (!config?.secrets || Object.keys(config.secrets).length === 0) {
@@ -793,6 +1445,8 @@ function createMcpServer() {
793
1445
  env: envSchema
794
1446
  },
795
1447
  async (params) => {
1448
+ const toolBlock = enforceToolPolicy("env_generate", params.projectPath);
1449
+ if (toolBlock) return toolBlock;
796
1450
  const projectPath = params.projectPath ?? process.cwd();
797
1451
  const config = readProjectConfig(projectPath);
798
1452
  if (!config?.secrets || Object.keys(config.secrets).length === 0) {
@@ -836,9 +1490,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
836
1490
  {
837
1491
  key: z.string().describe("The secret key name"),
838
1492
  scope: scopeSchema,
839
- projectPath: projectPathSchema
1493
+ projectPath: projectPathSchema,
1494
+ teamId: teamIdSchema,
1495
+ orgId: orgIdSchema
840
1496
  },
841
1497
  async (params) => {
1498
+ const toolBlock = enforceToolPolicy("inspect_secret", params.projectPath);
1499
+ if (toolBlock) return toolBlock;
842
1500
  const result = getEnvelope(params.key, opts(params));
843
1501
  if (!result) return text(`Secret "${params.key}" not found`, true);
844
1502
  const { envelope, scope } = result;
@@ -879,6 +1537,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
879
1537
  projectPath: projectPathSchema
880
1538
  },
881
1539
  async (params) => {
1540
+ const toolBlock = enforceToolPolicy("detect_environment", params.projectPath);
1541
+ if (toolBlock) return toolBlock;
882
1542
  const result = collapseEnvironment({
883
1543
  projectPath: params.projectPath ?? process.cwd()
884
1544
  });
@@ -899,9 +1559,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
899
1559
  prefix: z.string().optional().describe("Prefix for api-key/token format"),
900
1560
  saveAs: z.string().optional().describe("If provided, save the generated secret with this key name"),
901
1561
  scope: scopeSchema.default("global"),
902
- projectPath: projectPathSchema
1562
+ projectPath: projectPathSchema,
1563
+ teamId: teamIdSchema,
1564
+ orgId: orgIdSchema
903
1565
  },
904
1566
  async (params) => {
1567
+ const toolBlock = enforceToolPolicy("generate_secret", params.projectPath);
1568
+ if (toolBlock) return toolBlock;
905
1569
  const secret = generateSecret({
906
1570
  format: params.format,
907
1571
  length: params.length,
@@ -932,6 +1596,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
932
1596
  targetProjectPath: z.string().optional()
933
1597
  },
934
1598
  async (params) => {
1599
+ const toolBlock = enforceToolPolicy("entangle_secrets", params.sourceProjectPath);
1600
+ if (toolBlock) return toolBlock;
935
1601
  entangleSecrets(
936
1602
  params.sourceKey,
937
1603
  {
@@ -961,6 +1627,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
961
1627
  targetProjectPath: z.string().optional()
962
1628
  },
963
1629
  async (params) => {
1630
+ const toolBlock = enforceToolPolicy("disentangle_secrets", params.sourceProjectPath);
1631
+ if (toolBlock) return toolBlock;
964
1632
  disentangleSecrets(
965
1633
  params.sourceKey,
966
1634
  {
@@ -987,6 +1655,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
987
1655
  maxReads: z.number().optional().describe("Self-destruct after N reads")
988
1656
  },
989
1657
  async (params) => {
1658
+ const toolBlock = enforceToolPolicy("tunnel_create");
1659
+ if (toolBlock) return toolBlock;
990
1660
  const id = tunnelCreate(params.value, {
991
1661
  ttlSeconds: params.ttlSeconds,
992
1662
  maxReads: params.maxReads
@@ -1001,6 +1671,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1001
1671
  id: z.string().describe("Tunnel ID")
1002
1672
  },
1003
1673
  async (params) => {
1674
+ const toolBlock = enforceToolPolicy("tunnel_read");
1675
+ if (toolBlock) return toolBlock;
1004
1676
  const value = tunnelRead(params.id);
1005
1677
  if (value === null) {
1006
1678
  return text(`Tunnel "${params.id}" not found or expired`, true);
@@ -1013,6 +1685,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1013
1685
  "List active tunneled secrets (IDs and metadata only, never values).",
1014
1686
  {},
1015
1687
  async () => {
1688
+ const toolBlock = enforceToolPolicy("tunnel_list");
1689
+ if (toolBlock) return toolBlock;
1016
1690
  const tunnels = tunnelList();
1017
1691
  if (tunnels.length === 0) return text("No active tunnels");
1018
1692
  const lines = tunnels.map((t) => {
@@ -1035,6 +1709,8 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1035
1709
  id: z.string().describe("Tunnel ID")
1036
1710
  },
1037
1711
  async (params) => {
1712
+ const toolBlock = enforceToolPolicy("tunnel_destroy");
1713
+ if (toolBlock) return toolBlock;
1038
1714
  const destroyed = tunnelDestroy(params.id);
1039
1715
  return text(
1040
1716
  destroyed ? `Destroyed ${params.id}` : `Tunnel "${params.id}" not found`,
@@ -1049,9 +1725,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1049
1725
  keys: z.array(z.string()).optional().describe("Specific keys to pack (all if omitted)"),
1050
1726
  passphrase: z.string().describe("Encryption passphrase"),
1051
1727
  scope: scopeSchema,
1052
- projectPath: projectPathSchema
1728
+ projectPath: projectPathSchema,
1729
+ teamId: teamIdSchema,
1730
+ orgId: orgIdSchema
1053
1731
  },
1054
1732
  async (params) => {
1733
+ const toolBlock = enforceToolPolicy("teleport_pack", params.projectPath);
1734
+ if (toolBlock) return toolBlock;
1055
1735
  const o = opts(params);
1056
1736
  const entries = listSecrets(o);
1057
1737
  const secrets = [];
@@ -1075,9 +1755,13 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1075
1755
  passphrase: z.string().describe("Decryption passphrase"),
1076
1756
  scope: scopeSchema.default("global"),
1077
1757
  projectPath: projectPathSchema,
1758
+ teamId: teamIdSchema,
1759
+ orgId: orgIdSchema,
1078
1760
  dryRun: z.boolean().optional().default(false).describe("Preview without importing")
1079
1761
  },
1080
1762
  async (params) => {
1763
+ const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
1764
+ if (toolBlock) return toolBlock;
1081
1765
  try {
1082
1766
  const payload = teleportUnpack(params.bundle, params.passphrase);
1083
1767
  if (params.dryRun) {
@@ -1104,6 +1788,8 @@ ${preview}`);
1104
1788
  limit: z.number().optional().default(20).describe("Max events to return")
1105
1789
  },
1106
1790
  async (params) => {
1791
+ const toolBlock = enforceToolPolicy("audit_log");
1792
+ if (toolBlock) return toolBlock;
1107
1793
  const events = queryAudit({
1108
1794
  key: params.key,
1109
1795
  action: params.action,
@@ -1128,6 +1814,8 @@ ${preview}`);
1128
1814
  key: z.string().optional().describe("Check anomalies for a specific key")
1129
1815
  },
1130
1816
  async (params) => {
1817
+ const toolBlock = enforceToolPolicy("detect_anomalies");
1818
+ if (toolBlock) return toolBlock;
1131
1819
  const anomalies = detectAnomalies(params.key);
1132
1820
  if (anomalies.length === 0) return text("No anomalies detected");
1133
1821
  const lines = anomalies.map(
@@ -1141,9 +1829,13 @@ ${preview}`);
1141
1829
  "Run a comprehensive health check on all secrets: decay status, staleness, anomalies, entropy assessment.",
1142
1830
  {
1143
1831
  scope: scopeSchema,
1144
- projectPath: projectPathSchema
1832
+ projectPath: projectPathSchema,
1833
+ teamId: teamIdSchema,
1834
+ orgId: orgIdSchema
1145
1835
  },
1146
1836
  async (params) => {
1837
+ const toolBlock = enforceToolPolicy("health_check", params.projectPath);
1838
+ if (toolBlock) return toolBlock;
1147
1839
  const entries = listSecrets(opts(params));
1148
1840
  const anomalies = detectAnomalies();
1149
1841
  let healthy = 0;
@@ -1193,9 +1885,13 @@ ${preview}`);
1193
1885
  key: z.string().describe("The secret key name"),
1194
1886
  provider: z.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
1195
1887
  scope: scopeSchema,
1196
- projectPath: projectPathSchema
1888
+ projectPath: projectPathSchema,
1889
+ teamId: teamIdSchema,
1890
+ orgId: orgIdSchema
1197
1891
  },
1198
1892
  async (params) => {
1893
+ const toolBlock = enforceToolPolicy("validate_secret", params.projectPath);
1894
+ if (toolBlock) return toolBlock;
1199
1895
  const value = getSecret(params.key, opts(params));
1200
1896
  if (value === null) return text(`Secret "${params.key}" not found`, true);
1201
1897
  const envelope = getEnvelope(params.key, opts(params));
@@ -1209,7 +1905,9 @@ ${preview}`);
1209
1905
  "List all available validation providers for secret liveness testing.",
1210
1906
  {},
1211
1907
  async () => {
1212
- const providers = registry.listProviders().map((p) => ({
1908
+ const toolBlock = enforceToolPolicy("list_providers");
1909
+ if (toolBlock) return toolBlock;
1910
+ const providers = registry2.listProviders().map((p) => ({
1213
1911
  name: p.name,
1214
1912
  description: p.description,
1215
1913
  prefixes: p.prefixes ?? []
@@ -1234,6 +1932,8 @@ ${preview}`);
1234
1932
  description: z.string().optional().describe("Human-readable description")
1235
1933
  },
1236
1934
  async (params) => {
1935
+ const toolBlock = enforceToolPolicy("register_hook");
1936
+ if (toolBlock) return toolBlock;
1237
1937
  if (!params.key && !params.keyPattern && !params.tag) {
1238
1938
  return text("At least one match criterion required: key, keyPattern, or tag", true);
1239
1939
  }
@@ -1260,6 +1960,8 @@ ${preview}`);
1260
1960
  "List all registered secret change hooks with their match criteria, type, and status.",
1261
1961
  {},
1262
1962
  async () => {
1963
+ const toolBlock = enforceToolPolicy("list_hooks");
1964
+ if (toolBlock) return toolBlock;
1263
1965
  const hooks = listHooks();
1264
1966
  if (hooks.length === 0) return text("No hooks registered");
1265
1967
  return text(JSON.stringify(hooks, null, 2));
@@ -1272,6 +1974,8 @@ ${preview}`);
1272
1974
  id: z.string().describe("Hook ID to remove")
1273
1975
  },
1274
1976
  async (params) => {
1977
+ const toolBlock = enforceToolPolicy("remove_hook");
1978
+ if (toolBlock) return toolBlock;
1275
1979
  const removed = removeHook(params.id);
1276
1980
  return text(
1277
1981
  removed ? `Removed hook ${params.id}` : `Hook "${params.id}" not found`,
@@ -1279,6 +1983,194 @@ ${preview}`);
1279
1983
  );
1280
1984
  }
1281
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
+ );
1282
2174
  let dashboardInstance = null;
1283
2175
  server2.tool(
1284
2176
  "status_dashboard",
@@ -1287,10 +2179,12 @@ ${preview}`);
1287
2179
  port: z.number().optional().default(9876).describe("Port to serve on")
1288
2180
  },
1289
2181
  async (params) => {
2182
+ const toolBlock = enforceToolPolicy("status_dashboard");
2183
+ if (toolBlock) return toolBlock;
1290
2184
  if (dashboardInstance) {
1291
2185
  return text(`Dashboard already running at http://127.0.0.1:${dashboardInstance.port}`);
1292
2186
  }
1293
- const { startDashboardServer } = await import("./dashboard-32PCZF7D.js");
2187
+ const { startDashboardServer } = await import("./dashboard-Q5OQRQCX.js");
1294
2188
  dashboardInstance = startDashboardServer({ port: params.port });
1295
2189
  return text(`Dashboard started at http://127.0.0.1:${dashboardInstance.port}
1296
2190
  Open this URL in a browser to see live quantum status.`);
@@ -1304,6 +2198,8 @@ Open this URL in a browser to see live quantum status.`);
1304
2198
  projectPaths: z.array(z.string()).optional().describe("Project paths to monitor")
1305
2199
  },
1306
2200
  async (params) => {
2201
+ const toolBlock = enforceToolPolicy("agent_scan");
2202
+ if (toolBlock) return toolBlock;
1307
2203
  const report = runHealthScan({
1308
2204
  autoRotate: params.autoRotate,
1309
2205
  projectPaths: params.projectPaths ?? [process.cwd()]
@@ -1311,6 +2207,128 @@ Open this URL in a browser to see live quantum status.`);
1311
2207
  return text(JSON.stringify(report, null, 2));
1312
2208
  }
1313
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
+ );
1314
2332
  return server2;
1315
2333
  }
1316
2334