@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/README.md +519 -10
- package/dist/chunk-5JBU7TWN.js +1576 -0
- package/dist/chunk-5JBU7TWN.js.map +1 -0
- package/dist/chunk-WG4ZKN7Q.js +1632 -0
- package/dist/chunk-WG4ZKN7Q.js.map +1 -0
- package/dist/{dashboard-X3ONQFLV.js → dashboard-JT5ZNLT5.js} +41 -16
- package/dist/dashboard-JT5ZNLT5.js.map +1 -0
- package/dist/{dashboard-QQWKOOI5.js → dashboard-Q5OQRQCX.js} +41 -16
- package/dist/dashboard-Q5OQRQCX.js.map +1 -0
- package/dist/index.js +1878 -39
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1610 -21
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-3WTTWJYU.js +0 -653
- package/dist/chunk-3WTTWJYU.js.map +0 -1
- package/dist/chunk-F4SPZ774.js +0 -675
- package/dist/chunk-F4SPZ774.js.map +0 -1
- package/dist/dashboard-QQWKOOI5.js.map +0 -1
- package/dist/dashboard-X3ONQFLV.js.map +0 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
|
277
|
-
if (
|
|
278
|
-
|
|
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
|
|
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-
|
|
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
|
|