@i4ctime/q-ring 0.9.8 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -3
- package/dist/{chunk-JV4JFWV6.js → chunk-A4RZVP3P.js} +669 -223
- package/dist/chunk-A4RZVP3P.js.map +1 -0
- package/dist/{chunk-ZV7LRKBA.js → chunk-MQM4DLPI.js} +693 -241
- package/dist/chunk-MQM4DLPI.js.map +1 -0
- package/dist/dashboard-FSCJDLJX.js +1313 -0
- package/dist/dashboard-FSCJDLJX.js.map +1 -0
- package/dist/dashboard-OXUD62RV.js +1313 -0
- package/dist/dashboard-OXUD62RV.js.map +1 -0
- package/dist/index.js +2455 -1993
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +1798 -1661
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -3
- package/dist/chunk-JV4JFWV6.js.map +0 -1
- package/dist/chunk-ZV7LRKBA.js.map +0 -1
- package/dist/dashboard-4H474M2N.js +0 -557
- package/dist/dashboard-4H474M2N.js.map +0 -1
- package/dist/dashboard-5L4ILERJ.js +0 -557
- package/dist/dashboard-5L4ILERJ.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
PACKAGE_VERSION,
|
|
3
4
|
checkDecay,
|
|
4
5
|
checkExecPolicy,
|
|
5
6
|
checkKeyReadPolicy,
|
|
@@ -13,19 +14,23 @@ import {
|
|
|
13
14
|
exportAudit,
|
|
14
15
|
exportSecrets,
|
|
15
16
|
fireHooks,
|
|
17
|
+
forget,
|
|
16
18
|
getEnvelope,
|
|
17
19
|
getExecMaxRuntime,
|
|
18
20
|
getPolicySummary,
|
|
19
21
|
getSecret,
|
|
20
22
|
hasSecret,
|
|
21
|
-
|
|
23
|
+
httpRequest,
|
|
22
24
|
listHooks,
|
|
25
|
+
listMemory,
|
|
23
26
|
listSecrets,
|
|
24
27
|
logAudit,
|
|
25
28
|
queryAudit,
|
|
26
29
|
readProjectConfig,
|
|
30
|
+
recall,
|
|
27
31
|
registerHook,
|
|
28
32
|
registry,
|
|
33
|
+
remember,
|
|
29
34
|
removeHook,
|
|
30
35
|
setSecret,
|
|
31
36
|
tunnelCreate,
|
|
@@ -33,14 +38,34 @@ import {
|
|
|
33
38
|
tunnelList,
|
|
34
39
|
tunnelRead,
|
|
35
40
|
verifyAuditChain
|
|
36
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-MQM4DLPI.js";
|
|
37
42
|
|
|
38
43
|
// src/mcp.ts
|
|
39
44
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
40
45
|
|
|
41
46
|
// src/mcp/server.ts
|
|
42
47
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
43
|
-
|
|
48
|
+
|
|
49
|
+
// src/mcp/tools/secrets.ts
|
|
50
|
+
import { z as z2 } from "zod";
|
|
51
|
+
|
|
52
|
+
// src/services/list-secrets-filter.ts
|
|
53
|
+
function globKeyPatternToRegexSource(pattern) {
|
|
54
|
+
let out = "";
|
|
55
|
+
for (const c2 of pattern) {
|
|
56
|
+
if (c2 === "*") out += ".*";
|
|
57
|
+
else if (c2 === "?") out += ".";
|
|
58
|
+
else if ("\\^$+{}()|[]".includes(c2)) out += "\\" + c2;
|
|
59
|
+
else if (c2 === ".") out += "\\.";
|
|
60
|
+
else out += c2;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
function filterSecretsByKeyGlob(entries, filter) {
|
|
65
|
+
if (!filter?.trim()) return entries;
|
|
66
|
+
const regex = new RegExp("^" + globKeyPatternToRegexSource(filter) + "$", "i");
|
|
67
|
+
return entries.filter((e) => regex.test(e.key));
|
|
68
|
+
}
|
|
44
69
|
|
|
45
70
|
// src/core/noise.ts
|
|
46
71
|
import { randomBytes, randomInt } from "crypto";
|
|
@@ -125,148 +150,6 @@ function estimateEntropy(secret) {
|
|
|
125
150
|
return poolSize > 0 ? Math.floor(Math.log2(poolSize) * secret.length) : 0;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
|
-
// src/utils/colors.ts
|
|
129
|
-
var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
|
|
130
|
-
|
|
131
|
-
// src/core/agent.ts
|
|
132
|
-
function defaultConfig() {
|
|
133
|
-
return {
|
|
134
|
-
intervalSeconds: 60,
|
|
135
|
-
autoRotate: false,
|
|
136
|
-
projectPaths: [process.cwd()],
|
|
137
|
-
verbose: false
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
function runHealthScan(config = {}) {
|
|
141
|
-
const cfg = { ...defaultConfig(), ...config };
|
|
142
|
-
const report = {
|
|
143
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
144
|
-
totalSecrets: 0,
|
|
145
|
-
healthy: 0,
|
|
146
|
-
stale: 0,
|
|
147
|
-
expired: 0,
|
|
148
|
-
anomalies: 0,
|
|
149
|
-
rotated: [],
|
|
150
|
-
warnings: []
|
|
151
|
-
};
|
|
152
|
-
const globalEntries = listSecrets({ scope: "global", source: "agent" });
|
|
153
|
-
const projectEntries = cfg.projectPaths.flatMap(
|
|
154
|
-
(pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
|
|
155
|
-
);
|
|
156
|
-
const allEntries = [...globalEntries, ...projectEntries];
|
|
157
|
-
report.totalSecrets = allEntries.length;
|
|
158
|
-
for (const entry of allEntries) {
|
|
159
|
-
if (!entry.envelope) continue;
|
|
160
|
-
const decay = checkDecay(entry.envelope);
|
|
161
|
-
if (decay.isExpired) {
|
|
162
|
-
report.expired++;
|
|
163
|
-
report.warnings.push(
|
|
164
|
-
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
165
|
-
);
|
|
166
|
-
if (cfg.autoRotate) {
|
|
167
|
-
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
168
|
-
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
169
|
-
const newValue = generateSecret({ format: fmt, prefix });
|
|
170
|
-
setSecret(entry.key, newValue, {
|
|
171
|
-
scope: entry.scope,
|
|
172
|
-
projectPath: cfg.projectPaths[0],
|
|
173
|
-
source: "agent"
|
|
174
|
-
});
|
|
175
|
-
report.rotated.push(entry.key);
|
|
176
|
-
logAudit({
|
|
177
|
-
action: "write",
|
|
178
|
-
key: entry.key,
|
|
179
|
-
scope: entry.scope,
|
|
180
|
-
source: "agent",
|
|
181
|
-
detail: "auto-rotated by agent (expired)"
|
|
182
|
-
});
|
|
183
|
-
fireHooks({
|
|
184
|
-
action: "rotate",
|
|
185
|
-
key: entry.key,
|
|
186
|
-
scope: entry.scope,
|
|
187
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
188
|
-
source: "agent"
|
|
189
|
-
}, entry.envelope?.meta.tags).catch(() => {
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
} else if (decay.isStale) {
|
|
193
|
-
report.stale++;
|
|
194
|
-
report.warnings.push(
|
|
195
|
-
`STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
|
|
196
|
-
);
|
|
197
|
-
} else {
|
|
198
|
-
report.healthy++;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const anomalies = detectAnomalies();
|
|
202
|
-
report.anomalies = anomalies.length;
|
|
203
|
-
for (const a of anomalies) {
|
|
204
|
-
report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
|
|
205
|
-
}
|
|
206
|
-
return report;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// src/core/teleport.ts
|
|
210
|
-
import {
|
|
211
|
-
randomBytes as randomBytes2,
|
|
212
|
-
createCipheriv,
|
|
213
|
-
createDecipheriv,
|
|
214
|
-
pbkdf2Sync
|
|
215
|
-
} from "crypto";
|
|
216
|
-
var ALGORITHM = "aes-256-gcm";
|
|
217
|
-
var KEY_LENGTH = 32;
|
|
218
|
-
var IV_LENGTH = 16;
|
|
219
|
-
var SALT_LENGTH = 32;
|
|
220
|
-
var PBKDF2_ITERATIONS = 1e5;
|
|
221
|
-
function deriveKey(passphrase, salt) {
|
|
222
|
-
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
223
|
-
}
|
|
224
|
-
function teleportPack(secrets, passphrase) {
|
|
225
|
-
const payload = {
|
|
226
|
-
secrets,
|
|
227
|
-
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
228
|
-
};
|
|
229
|
-
const plaintext = JSON.stringify(payload);
|
|
230
|
-
const salt = randomBytes2(SALT_LENGTH);
|
|
231
|
-
const iv = randomBytes2(IV_LENGTH);
|
|
232
|
-
const key = deriveKey(passphrase, salt);
|
|
233
|
-
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
234
|
-
const encrypted = Buffer.concat([
|
|
235
|
-
cipher.update(plaintext, "utf8"),
|
|
236
|
-
cipher.final()
|
|
237
|
-
]);
|
|
238
|
-
const tag = cipher.getAuthTag();
|
|
239
|
-
const bundle = {
|
|
240
|
-
v: 1,
|
|
241
|
-
data: encrypted.toString("base64"),
|
|
242
|
-
salt: salt.toString("base64"),
|
|
243
|
-
iv: iv.toString("base64"),
|
|
244
|
-
tag: tag.toString("base64"),
|
|
245
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
246
|
-
count: secrets.length
|
|
247
|
-
};
|
|
248
|
-
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
249
|
-
}
|
|
250
|
-
function teleportUnpack(encoded, passphrase) {
|
|
251
|
-
const bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
252
|
-
const bundle = JSON.parse(bundleJson);
|
|
253
|
-
if (bundle.v !== 1) {
|
|
254
|
-
throw new Error(`Unsupported teleport bundle version: ${bundle.v}`);
|
|
255
|
-
}
|
|
256
|
-
const salt = Buffer.from(bundle.salt, "base64");
|
|
257
|
-
const iv = Buffer.from(bundle.iv, "base64");
|
|
258
|
-
const tag = Buffer.from(bundle.tag, "base64");
|
|
259
|
-
const encrypted = Buffer.from(bundle.data, "base64");
|
|
260
|
-
const key = deriveKey(passphrase, salt);
|
|
261
|
-
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
262
|
-
decipher.setAuthTag(tag);
|
|
263
|
-
const decrypted = Buffer.concat([
|
|
264
|
-
decipher.update(encrypted),
|
|
265
|
-
decipher.final()
|
|
266
|
-
]);
|
|
267
|
-
return JSON.parse(decrypted.toString("utf8"));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
153
|
// src/core/import.ts
|
|
271
154
|
import { readFileSync } from "fs";
|
|
272
155
|
function parseDotenv(content) {
|
|
@@ -334,445 +217,444 @@ function importDotenv(filePathOrContent, options = {}) {
|
|
|
334
217
|
return result;
|
|
335
218
|
}
|
|
336
219
|
|
|
337
|
-
// src/
|
|
338
|
-
import {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
|
|
345
|
-
maxRuntimeSeconds: 30,
|
|
346
|
-
allowNetwork: false,
|
|
347
|
-
stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
|
|
348
|
-
},
|
|
349
|
-
ci: {
|
|
350
|
-
name: "ci",
|
|
351
|
-
maxRuntimeSeconds: 300,
|
|
352
|
-
allowNetwork: true,
|
|
353
|
-
denyCommands: ["rm -rf /", "mkfs", "dd if="]
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
function getProfile(name) {
|
|
357
|
-
if (!name) return BUILTIN_PROFILES.unrestricted;
|
|
358
|
-
return BUILTIN_PROFILES[name] ?? { name };
|
|
220
|
+
// src/mcp/tools/_shared.ts
|
|
221
|
+
import { z } from "zod";
|
|
222
|
+
function text(t, isError = false) {
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: "text", text: t }],
|
|
225
|
+
...isError ? { isError: true } : {}
|
|
226
|
+
};
|
|
359
227
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (this.patterns.length === 0) {
|
|
378
|
-
this.push(chunk);
|
|
379
|
-
return callback();
|
|
380
|
-
}
|
|
381
|
-
const text2 = this.tail + chunk.toString();
|
|
382
|
-
let redacted = text2;
|
|
383
|
-
for (const { value, replacement } of this.patterns) {
|
|
384
|
-
redacted = redacted.split(value).join(replacement);
|
|
385
|
-
}
|
|
386
|
-
if (redacted.length < this.maxLen) {
|
|
387
|
-
this.tail = redacted;
|
|
388
|
-
return callback();
|
|
389
|
-
}
|
|
390
|
-
const outputLen = redacted.length - this.maxLen + 1;
|
|
391
|
-
const output = redacted.slice(0, outputLen);
|
|
392
|
-
this.tail = redacted.slice(outputLen);
|
|
393
|
-
this.push(output);
|
|
394
|
-
callback();
|
|
228
|
+
function opts(params) {
|
|
229
|
+
return {
|
|
230
|
+
scope: params.scope,
|
|
231
|
+
projectPath: params.projectPath ?? process.cwd(),
|
|
232
|
+
teamId: params.teamId,
|
|
233
|
+
orgId: params.orgId,
|
|
234
|
+
env: params.env,
|
|
235
|
+
source: "mcp"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function enforceToolPolicy(toolName, projectPath8) {
|
|
239
|
+
const decision = checkToolPolicy(toolName, projectPath8);
|
|
240
|
+
if (!decision.allowed) {
|
|
241
|
+
return text(
|
|
242
|
+
`Policy Denied: ${decision.reason} (source: ${decision.policySource})`,
|
|
243
|
+
true
|
|
244
|
+
);
|
|
395
245
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
var commonSchemas = {
|
|
249
|
+
teamId: z.string().optional().describe("Team identifier for team-scoped secrets"),
|
|
250
|
+
orgId: z.string().optional().describe("Org identifier for org-scoped secrets"),
|
|
251
|
+
scope: z.enum(["global", "project", "team", "org"]).optional().describe("Scope: global, project, team, or org"),
|
|
252
|
+
projectPath: z.string().optional().describe("Project root path for project-scoped secrets"),
|
|
253
|
+
env: z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)")
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/mcp/tools/secrets.ts
|
|
257
|
+
var { teamId, orgId, scope, projectPath, env } = commonSchemas;
|
|
258
|
+
function registerSecretTools(server2) {
|
|
259
|
+
server2.tool(
|
|
260
|
+
"get_secret",
|
|
261
|
+
"[secrets] Retrieve a secret by key. Collapses superposition if the secret has multiple environment states. Records access in audit log (observer effect).",
|
|
262
|
+
{
|
|
263
|
+
key: z2.string().describe("The secret key name"),
|
|
264
|
+
scope,
|
|
265
|
+
projectPath,
|
|
266
|
+
env,
|
|
267
|
+
teamId,
|
|
268
|
+
orgId
|
|
269
|
+
},
|
|
270
|
+
async (params) => {
|
|
271
|
+
const toolBlock = enforceToolPolicy("get_secret", params.projectPath);
|
|
272
|
+
if (toolBlock) return toolBlock;
|
|
273
|
+
try {
|
|
274
|
+
const keyBlock = checkKeyReadPolicy(
|
|
275
|
+
params.key,
|
|
276
|
+
void 0,
|
|
277
|
+
params.projectPath
|
|
278
|
+
);
|
|
279
|
+
if (!keyBlock.allowed) {
|
|
280
|
+
return text(`Policy Denied: ${keyBlock.reason}`, true);
|
|
281
|
+
}
|
|
282
|
+
const value = getSecret(params.key, opts(params));
|
|
283
|
+
if (value === null) return text(`Secret "${params.key}" not found`, true);
|
|
284
|
+
return text(
|
|
285
|
+
JSON.stringify({ ok: true, data: { key: params.key, value } }, null, 2)
|
|
286
|
+
);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
return text(err instanceof Error ? err.message : String(err), true);
|
|
401
289
|
}
|
|
402
|
-
this.push(final);
|
|
403
290
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const envMap = {};
|
|
430
|
-
for (const [k, v] of Object.entries(process.env)) {
|
|
431
|
-
if (v !== void 0) envMap[k] = v;
|
|
432
|
-
}
|
|
433
|
-
if (profile.stripEnvVars) {
|
|
434
|
-
for (const key of profile.stripEnvVars) {
|
|
435
|
-
delete envMap[key];
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
const secretsToRedact = /* @__PURE__ */ new Set();
|
|
439
|
-
let entries = listSecrets({
|
|
440
|
-
scope: opts2.scope,
|
|
441
|
-
projectPath: opts2.projectPath,
|
|
442
|
-
source: opts2.source ?? "cli",
|
|
443
|
-
silent: true
|
|
444
|
-
// list silently
|
|
445
|
-
});
|
|
446
|
-
if (opts2.keys?.length) {
|
|
447
|
-
const keySet = new Set(opts2.keys);
|
|
448
|
-
entries = entries.filter((e) => keySet.has(e.key));
|
|
449
|
-
}
|
|
450
|
-
if (opts2.tags?.length) {
|
|
451
|
-
entries = entries.filter(
|
|
452
|
-
(e) => opts2.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
for (const entry of entries) {
|
|
456
|
-
if (entry.envelope) {
|
|
457
|
-
const decay = checkDecay(entry.envelope);
|
|
458
|
-
if (decay.isExpired) continue;
|
|
459
|
-
}
|
|
460
|
-
const val = getSecret(entry.key, {
|
|
461
|
-
scope: entry.scope,
|
|
462
|
-
projectPath: opts2.projectPath,
|
|
463
|
-
env: opts2.env,
|
|
464
|
-
source: opts2.source ?? "cli",
|
|
465
|
-
silent: false
|
|
466
|
-
// Log access for execution
|
|
467
|
-
});
|
|
468
|
-
if (val !== null) {
|
|
469
|
-
envMap[entry.key] = val;
|
|
470
|
-
if (val.length > 5) {
|
|
471
|
-
secretsToRedact.add(val);
|
|
291
|
+
);
|
|
292
|
+
server2.tool(
|
|
293
|
+
"list_secrets",
|
|
294
|
+
"[secrets] 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.",
|
|
295
|
+
{
|
|
296
|
+
scope,
|
|
297
|
+
projectPath,
|
|
298
|
+
tag: z2.string().optional().describe("Filter by tag"),
|
|
299
|
+
expired: z2.boolean().optional().describe("Show only expired secrets"),
|
|
300
|
+
stale: z2.boolean().optional().describe("Show only stale secrets (75%+ decay)"),
|
|
301
|
+
filter: z2.string().optional().describe("Glob pattern on key name (e.g., 'API_*')"),
|
|
302
|
+
teamId,
|
|
303
|
+
orgId
|
|
304
|
+
},
|
|
305
|
+
async (params) => {
|
|
306
|
+
const toolBlock = enforceToolPolicy("list_secrets", params.projectPath);
|
|
307
|
+
if (toolBlock) return toolBlock;
|
|
308
|
+
let entries = listSecrets(opts(params));
|
|
309
|
+
if (params.tag) {
|
|
310
|
+
entries = entries.filter(
|
|
311
|
+
(e) => e.envelope?.meta.tags?.includes(params.tag)
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (params.expired) {
|
|
315
|
+
entries = entries.filter((e) => e.decay?.isExpired);
|
|
472
316
|
}
|
|
317
|
+
if (params.stale) {
|
|
318
|
+
entries = entries.filter(
|
|
319
|
+
(e) => e.decay?.isStale && !e.decay?.isExpired
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (params.filter) {
|
|
323
|
+
entries = filterSecretsByKeyGlob(entries, params.filter);
|
|
324
|
+
}
|
|
325
|
+
const rows = entries.map((e) => ({
|
|
326
|
+
scope: e.scope,
|
|
327
|
+
key: e.key,
|
|
328
|
+
stateKeys: e.envelope?.states ? Object.keys(e.envelope.states) : void 0,
|
|
329
|
+
expired: !!e.decay?.isExpired,
|
|
330
|
+
stale: !!e.decay?.isStale && !e.decay?.isExpired,
|
|
331
|
+
lifetimePercent: e.decay?.lifetimePercent,
|
|
332
|
+
timeRemaining: e.decay?.timeRemaining ?? null,
|
|
333
|
+
entangledCount: e.envelope?.meta.entangled?.length ?? 0,
|
|
334
|
+
accessCount: e.envelope?.meta.accessCount ?? 0
|
|
335
|
+
}));
|
|
336
|
+
return text(JSON.stringify({ ok: true, data: { entries: rows } }, null, 2));
|
|
473
337
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
"
|
|
480
|
-
"
|
|
481
|
-
"
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
"
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
338
|
+
);
|
|
339
|
+
server2.tool(
|
|
340
|
+
"set_secret",
|
|
341
|
+
"[secrets] Store a secret with optional quantum metadata: TTL (decay), environment state (superposition), description, tags.",
|
|
342
|
+
{
|
|
343
|
+
key: z2.string().describe("The secret key name"),
|
|
344
|
+
value: z2.string().describe("The secret value"),
|
|
345
|
+
scope: scope.default("global"),
|
|
346
|
+
projectPath,
|
|
347
|
+
env: z2.string().optional().describe(
|
|
348
|
+
"If provided, sets the value for this specific environment (superposition)"
|
|
349
|
+
),
|
|
350
|
+
ttlSeconds: z2.number().optional().describe("Time-to-live in seconds (quantum decay)"),
|
|
351
|
+
description: z2.string().optional().describe("Human-readable description"),
|
|
352
|
+
tags: z2.array(z2.string()).optional().describe("Tags for organization"),
|
|
353
|
+
rotationFormat: z2.enum([
|
|
354
|
+
"hex",
|
|
355
|
+
"base64",
|
|
356
|
+
"alphanumeric",
|
|
357
|
+
"uuid",
|
|
358
|
+
"api-key",
|
|
359
|
+
"token",
|
|
360
|
+
"password"
|
|
361
|
+
]).optional().describe("Format for auto-rotation when this secret expires"),
|
|
362
|
+
rotationPrefix: z2.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')"),
|
|
363
|
+
teamId,
|
|
364
|
+
orgId
|
|
365
|
+
},
|
|
366
|
+
async (params) => {
|
|
367
|
+
const toolBlock = enforceToolPolicy("set_secret", params.projectPath);
|
|
368
|
+
if (toolBlock) return toolBlock;
|
|
369
|
+
const o = opts(params);
|
|
370
|
+
if (params.env) {
|
|
371
|
+
const existing = getEnvelope(params.key, o);
|
|
372
|
+
const states = existing?.envelope?.states ?? {};
|
|
373
|
+
states[params.env] = params.value;
|
|
374
|
+
if (existing?.envelope?.value && !states["default"]) {
|
|
375
|
+
states["default"] = existing.envelope.value;
|
|
376
|
+
}
|
|
377
|
+
setSecret(params.key, "", {
|
|
378
|
+
...o,
|
|
379
|
+
states,
|
|
380
|
+
defaultEnv: existing?.envelope?.defaultEnv ?? params.env,
|
|
381
|
+
ttlSeconds: params.ttlSeconds,
|
|
382
|
+
description: params.description,
|
|
383
|
+
tags: params.tags,
|
|
384
|
+
rotationFormat: params.rotationFormat,
|
|
385
|
+
rotationPrefix: params.rotationPrefix
|
|
386
|
+
});
|
|
387
|
+
return text(
|
|
388
|
+
`[${params.scope ?? "global"}] ${params.key} set for env:${params.env}`
|
|
389
|
+
);
|
|
493
390
|
}
|
|
494
|
-
|
|
495
|
-
|
|
391
|
+
setSecret(params.key, params.value, {
|
|
392
|
+
...o,
|
|
393
|
+
ttlSeconds: params.ttlSeconds,
|
|
394
|
+
description: params.description,
|
|
395
|
+
tags: params.tags,
|
|
396
|
+
rotationFormat: params.rotationFormat,
|
|
397
|
+
rotationPrefix: params.rotationPrefix
|
|
398
|
+
});
|
|
399
|
+
return text(`[${params.scope ?? "global"}] ${params.key} saved`);
|
|
496
400
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
401
|
+
);
|
|
402
|
+
server2.tool(
|
|
403
|
+
"delete_secret",
|
|
404
|
+
"[secrets] Remove a secret from the keyring.",
|
|
405
|
+
{
|
|
406
|
+
key: z2.string().describe("The secret key name"),
|
|
407
|
+
scope,
|
|
408
|
+
projectPath,
|
|
409
|
+
teamId,
|
|
410
|
+
orgId
|
|
411
|
+
},
|
|
412
|
+
async (params) => {
|
|
413
|
+
const toolBlock = enforceToolPolicy("delete_secret", params.projectPath);
|
|
414
|
+
if (toolBlock) return toolBlock;
|
|
415
|
+
const deleted = deleteSecret(params.key, opts(params));
|
|
416
|
+
return text(
|
|
417
|
+
deleted ? `Deleted "${params.key}"` : `Secret "${params.key}" not found`,
|
|
418
|
+
!deleted
|
|
419
|
+
);
|
|
509
420
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
421
|
+
);
|
|
422
|
+
server2.tool(
|
|
423
|
+
"has_secret",
|
|
424
|
+
"[secrets] Check if a secret exists. Returns boolean. Never reveals the value. Respects decay \u2014 expired secrets return false.",
|
|
425
|
+
{
|
|
426
|
+
key: z2.string().describe("The secret key name"),
|
|
427
|
+
scope,
|
|
428
|
+
projectPath,
|
|
429
|
+
teamId,
|
|
430
|
+
orgId
|
|
431
|
+
},
|
|
432
|
+
async (params) => {
|
|
433
|
+
const toolBlock = enforceToolPolicy("has_secret", params.projectPath);
|
|
434
|
+
if (toolBlock) return toolBlock;
|
|
435
|
+
return text(hasSecret(params.key, opts(params)) ? "true" : "false");
|
|
522
436
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
"coverage",
|
|
549
|
-
".cursor",
|
|
550
|
-
"venv",
|
|
551
|
-
"__pycache__"
|
|
552
|
-
]);
|
|
553
|
-
var IGNORE_EXTS = /* @__PURE__ */ new Set([
|
|
554
|
-
".png",
|
|
555
|
-
".jpg",
|
|
556
|
-
".jpeg",
|
|
557
|
-
".gif",
|
|
558
|
-
".ico",
|
|
559
|
-
".svg",
|
|
560
|
-
".webp",
|
|
561
|
-
".mp4",
|
|
562
|
-
".mp3",
|
|
563
|
-
".wav",
|
|
564
|
-
".ogg",
|
|
565
|
-
".pdf",
|
|
566
|
-
".zip",
|
|
567
|
-
".tar",
|
|
568
|
-
".gz",
|
|
569
|
-
".xz",
|
|
570
|
-
".ttf",
|
|
571
|
-
".woff",
|
|
572
|
-
".woff2",
|
|
573
|
-
".eot",
|
|
574
|
-
".exe",
|
|
575
|
-
".dll",
|
|
576
|
-
".so",
|
|
577
|
-
".dylib",
|
|
578
|
-
".lock"
|
|
579
|
-
]);
|
|
580
|
-
var SECRET_KEYWORDS = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/i;
|
|
581
|
-
function calculateEntropy(str) {
|
|
582
|
-
if (!str) return 0;
|
|
583
|
-
const len = str.length;
|
|
584
|
-
const frequencies = /* @__PURE__ */ new Map();
|
|
585
|
-
for (let i = 0; i < len; i++) {
|
|
586
|
-
const char = str[i];
|
|
587
|
-
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
588
|
-
}
|
|
589
|
-
let entropy = 0;
|
|
590
|
-
for (const count of frequencies.values()) {
|
|
591
|
-
const p = count / len;
|
|
592
|
-
entropy -= p * Math.log2(p);
|
|
593
|
-
}
|
|
594
|
-
return entropy;
|
|
595
|
-
}
|
|
596
|
-
function scanCodebase(dir) {
|
|
597
|
-
const results = [];
|
|
598
|
-
function walk(currentDir) {
|
|
599
|
-
let entries;
|
|
600
|
-
try {
|
|
601
|
-
entries = readdirSync(currentDir);
|
|
602
|
-
} catch {
|
|
603
|
-
return;
|
|
437
|
+
);
|
|
438
|
+
server2.tool(
|
|
439
|
+
"export_secrets",
|
|
440
|
+
"[secrets] Export secrets as .env or JSON format. Collapses superposition. Supports filtering by specific keys or tags.",
|
|
441
|
+
{
|
|
442
|
+
format: z2.enum(["env", "json"]).optional().default("env").describe("Output format"),
|
|
443
|
+
keys: z2.array(z2.string()).optional().describe("Only export these specific key names"),
|
|
444
|
+
tags: z2.array(z2.string()).optional().describe("Only export secrets with any of these tags"),
|
|
445
|
+
scope,
|
|
446
|
+
projectPath,
|
|
447
|
+
env,
|
|
448
|
+
teamId,
|
|
449
|
+
orgId
|
|
450
|
+
},
|
|
451
|
+
async (params) => {
|
|
452
|
+
const toolBlock = enforceToolPolicy("export_secrets", params.projectPath);
|
|
453
|
+
if (toolBlock) return toolBlock;
|
|
454
|
+
const output = exportSecrets({
|
|
455
|
+
...opts(params),
|
|
456
|
+
format: params.format,
|
|
457
|
+
keys: params.keys,
|
|
458
|
+
tags: params.tags
|
|
459
|
+
});
|
|
460
|
+
if (!output.trim()) return text("No secrets matched the filters", true);
|
|
461
|
+
return text(output);
|
|
604
462
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
463
|
+
);
|
|
464
|
+
server2.tool(
|
|
465
|
+
"import_dotenv",
|
|
466
|
+
"[secrets] Import secrets from .env file content. Parses standard dotenv syntax (comments, quotes, multiline escapes) and stores each key/value pair in q-ring.",
|
|
467
|
+
{
|
|
468
|
+
content: z2.string().describe("The .env file content to parse and import"),
|
|
469
|
+
scope: scope.default("global"),
|
|
470
|
+
projectPath,
|
|
471
|
+
skipExisting: z2.boolean().optional().default(false).describe("Skip keys that already exist in q-ring"),
|
|
472
|
+
dryRun: z2.boolean().optional().default(false).describe("Preview what would be imported without saving")
|
|
473
|
+
},
|
|
474
|
+
async (params) => {
|
|
475
|
+
const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath);
|
|
476
|
+
if (toolBlock) return toolBlock;
|
|
477
|
+
const result = importDotenv(params.content, {
|
|
478
|
+
scope: params.scope,
|
|
479
|
+
projectPath: params.projectPath ?? process.cwd(),
|
|
480
|
+
source: "mcp",
|
|
481
|
+
skipExisting: params.skipExisting,
|
|
482
|
+
dryRun: params.dryRun
|
|
483
|
+
});
|
|
484
|
+
const lines = [
|
|
485
|
+
params.dryRun ? "Dry run \u2014 no changes made" : `Imported ${result.imported.length} secret(s)`
|
|
486
|
+
];
|
|
487
|
+
if (result.imported.length > 0) {
|
|
488
|
+
lines.push(`Keys: ${result.imported.join(", ")}`);
|
|
613
489
|
}
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
} else if (stat.isFile()) {
|
|
617
|
-
const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
|
|
618
|
-
if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
|
|
619
|
-
let content;
|
|
620
|
-
try {
|
|
621
|
-
content = readFileSync2(fullPath, "utf8");
|
|
622
|
-
} catch {
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (content.includes("\0")) continue;
|
|
626
|
-
const lines = content.split(/\r?\n/);
|
|
627
|
-
for (let i = 0; i < lines.length; i++) {
|
|
628
|
-
const line = lines[i];
|
|
629
|
-
if (line.length > 500) continue;
|
|
630
|
-
const match = line.match(SECRET_KEYWORDS);
|
|
631
|
-
if (match) {
|
|
632
|
-
const varName = match[1];
|
|
633
|
-
const value = match[3];
|
|
634
|
-
if (value.length < 8) continue;
|
|
635
|
-
const lowerValue = value.toLowerCase();
|
|
636
|
-
if (lowerValue.includes("example") || lowerValue.includes("your_") || lowerValue.includes("placeholder") || lowerValue.includes("replace_me")) {
|
|
637
|
-
continue;
|
|
638
|
-
}
|
|
639
|
-
const entropy = calculateEntropy(value);
|
|
640
|
-
if (entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_")) {
|
|
641
|
-
const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
|
|
642
|
-
results.push({
|
|
643
|
-
file: relPath || fullPath,
|
|
644
|
-
line: i + 1,
|
|
645
|
-
keyName: varName,
|
|
646
|
-
match: value,
|
|
647
|
-
context: line.trim(),
|
|
648
|
-
entropy: parseFloat(entropy.toFixed(2))
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
490
|
+
if (result.skipped.length > 0) {
|
|
491
|
+
lines.push(`Skipped (existing): ${result.skipped.join(", ")}`);
|
|
653
492
|
}
|
|
493
|
+
return text(lines.join("\n"));
|
|
654
494
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
495
|
+
);
|
|
496
|
+
server2.tool(
|
|
497
|
+
"inspect_secret",
|
|
498
|
+
"[secrets] Show full quantum state of a secret: superposition states, decay status, entanglement links, access history. Never reveals the actual value.",
|
|
499
|
+
{
|
|
500
|
+
key: z2.string().describe("The secret key name"),
|
|
501
|
+
scope,
|
|
502
|
+
projectPath,
|
|
503
|
+
teamId,
|
|
504
|
+
orgId
|
|
505
|
+
},
|
|
506
|
+
async (params) => {
|
|
507
|
+
const toolBlock = enforceToolPolicy("inspect_secret", params.projectPath);
|
|
508
|
+
if (toolBlock) return toolBlock;
|
|
509
|
+
const result = getEnvelope(params.key, opts(params));
|
|
510
|
+
if (!result) return text(`Secret "${params.key}" not found`, true);
|
|
511
|
+
const { envelope, scope: entryScope } = result;
|
|
512
|
+
const decay = checkDecay(envelope);
|
|
513
|
+
const info = {
|
|
514
|
+
key: params.key,
|
|
515
|
+
scope: entryScope,
|
|
516
|
+
type: envelope.states ? "superposition" : "collapsed",
|
|
517
|
+
created: envelope.meta.createdAt,
|
|
518
|
+
updated: envelope.meta.updatedAt,
|
|
519
|
+
accessCount: envelope.meta.accessCount,
|
|
520
|
+
lastAccessed: envelope.meta.lastAccessedAt ?? "never"
|
|
521
|
+
};
|
|
522
|
+
if (envelope.states) {
|
|
523
|
+
info.environments = Object.keys(envelope.states);
|
|
524
|
+
info.defaultEnv = envelope.defaultEnv;
|
|
525
|
+
}
|
|
526
|
+
if (decay.timeRemaining) {
|
|
527
|
+
info.decay = {
|
|
528
|
+
expired: decay.isExpired,
|
|
529
|
+
stale: decay.isStale,
|
|
530
|
+
lifetimePercent: decay.lifetimePercent,
|
|
531
|
+
timeRemaining: decay.timeRemaining
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
if (envelope.meta.entangled?.length) {
|
|
535
|
+
info.entangled = envelope.meta.entangled;
|
|
536
|
+
}
|
|
537
|
+
if (envelope.meta.description)
|
|
538
|
+
info.description = envelope.meta.description;
|
|
539
|
+
if (envelope.meta.tags?.length) info.tags = envelope.meta.tags;
|
|
540
|
+
return text(JSON.stringify(info, null, 2));
|
|
695
541
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
context: line.trim(),
|
|
731
|
-
entropy: parseFloat(entropy.toFixed(2)),
|
|
732
|
-
fixed: shouldFix
|
|
542
|
+
);
|
|
543
|
+
server2.tool(
|
|
544
|
+
"generate_secret",
|
|
545
|
+
"[secrets] Generate a cryptographic secret (quantum noise). Formats: hex, base64, alphanumeric, uuid, api-key, token, password. Optionally save directly to the keyring.",
|
|
546
|
+
{
|
|
547
|
+
format: z2.enum([
|
|
548
|
+
"hex",
|
|
549
|
+
"base64",
|
|
550
|
+
"alphanumeric",
|
|
551
|
+
"uuid",
|
|
552
|
+
"api-key",
|
|
553
|
+
"token",
|
|
554
|
+
"password"
|
|
555
|
+
]).optional().default("api-key").describe("Output format"),
|
|
556
|
+
length: z2.number().optional().describe("Length in bytes or characters"),
|
|
557
|
+
prefix: z2.string().optional().describe("Prefix for api-key/token format"),
|
|
558
|
+
saveAs: z2.string().optional().describe("If provided, save the generated secret with this key name"),
|
|
559
|
+
scope: scope.default("global"),
|
|
560
|
+
projectPath,
|
|
561
|
+
teamId,
|
|
562
|
+
orgId
|
|
563
|
+
},
|
|
564
|
+
async (params) => {
|
|
565
|
+
const toolBlock = enforceToolPolicy("generate_secret", params.projectPath);
|
|
566
|
+
if (toolBlock) return toolBlock;
|
|
567
|
+
const secret = generateSecret({
|
|
568
|
+
format: params.format,
|
|
569
|
+
length: params.length,
|
|
570
|
+
prefix: params.prefix
|
|
571
|
+
});
|
|
572
|
+
if (params.saveAs) {
|
|
573
|
+
setSecret(params.saveAs, secret, {
|
|
574
|
+
...opts(params),
|
|
575
|
+
description: `Generated ${params.format} secret`
|
|
733
576
|
});
|
|
577
|
+
const entropy = estimateEntropy(secret);
|
|
578
|
+
return text(
|
|
579
|
+
`Generated and saved as "${params.saveAs}" (${params.format}, ~${entropy} bits entropy)`
|
|
580
|
+
);
|
|
734
581
|
}
|
|
582
|
+
return text(JSON.stringify({ ok: true, data: { value: secret } }, null, 2));
|
|
735
583
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
584
|
+
);
|
|
585
|
+
server2.tool(
|
|
586
|
+
"entangle_secrets",
|
|
587
|
+
"[secrets] Create a quantum entanglement between two secrets. When the source is rotated/updated, the target automatically receives the same value.",
|
|
588
|
+
{
|
|
589
|
+
sourceKey: z2.string().describe("Source secret key"),
|
|
590
|
+
targetKey: z2.string().describe("Target secret key"),
|
|
591
|
+
sourceScope: scope.default("global"),
|
|
592
|
+
targetScope: scope.default("global"),
|
|
593
|
+
sourceProjectPath: z2.string().optional(),
|
|
594
|
+
targetProjectPath: z2.string().optional()
|
|
595
|
+
},
|
|
596
|
+
async (params) => {
|
|
597
|
+
const toolBlock = enforceToolPolicy(
|
|
598
|
+
"entangle_secrets",
|
|
599
|
+
params.sourceProjectPath
|
|
600
|
+
);
|
|
601
|
+
if (toolBlock) return toolBlock;
|
|
602
|
+
entangleSecrets(
|
|
603
|
+
params.sourceKey,
|
|
604
|
+
{
|
|
605
|
+
scope: params.sourceScope,
|
|
606
|
+
projectPath: params.sourceProjectPath ?? process.cwd(),
|
|
607
|
+
source: "mcp"
|
|
608
|
+
},
|
|
609
|
+
params.targetKey,
|
|
610
|
+
{
|
|
611
|
+
scope: params.targetScope,
|
|
612
|
+
projectPath: params.targetProjectPath ?? process.cwd(),
|
|
613
|
+
source: "mcp"
|
|
742
614
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
615
|
+
);
|
|
616
|
+
return text(`Entangled: ${params.sourceKey} <-> ${params.targetKey}`);
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
server2.tool(
|
|
620
|
+
"disentangle_secrets",
|
|
621
|
+
"[secrets] Remove a quantum entanglement between two secrets. They will no longer synchronize on rotation.",
|
|
622
|
+
{
|
|
623
|
+
sourceKey: z2.string().describe("Source secret key"),
|
|
624
|
+
targetKey: z2.string().describe("Target secret key"),
|
|
625
|
+
sourceScope: scope.default("global"),
|
|
626
|
+
targetScope: scope.default("global"),
|
|
627
|
+
sourceProjectPath: z2.string().optional(),
|
|
628
|
+
targetProjectPath: z2.string().optional()
|
|
629
|
+
},
|
|
630
|
+
async (params) => {
|
|
631
|
+
const toolBlock = enforceToolPolicy(
|
|
632
|
+
"disentangle_secrets",
|
|
633
|
+
params.sourceProjectPath
|
|
634
|
+
);
|
|
635
|
+
if (toolBlock) return toolBlock;
|
|
636
|
+
disentangleSecrets(
|
|
637
|
+
params.sourceKey,
|
|
638
|
+
{
|
|
639
|
+
scope: params.sourceScope,
|
|
640
|
+
projectPath: params.sourceProjectPath ?? process.cwd(),
|
|
641
|
+
source: "mcp"
|
|
642
|
+
},
|
|
643
|
+
params.targetKey,
|
|
644
|
+
{
|
|
645
|
+
scope: params.targetScope,
|
|
646
|
+
projectPath: params.targetProjectPath ?? process.cwd(),
|
|
647
|
+
source: "mcp"
|
|
750
648
|
}
|
|
751
|
-
|
|
752
|
-
|
|
649
|
+
);
|
|
650
|
+
return text(`Disentangled: ${params.sourceKey} </> ${params.targetKey}`);
|
|
753
651
|
}
|
|
754
|
-
|
|
755
|
-
return results;
|
|
756
|
-
}
|
|
757
|
-
function calculateEntropy2(str) {
|
|
758
|
-
if (!str) return 0;
|
|
759
|
-
const len = str.length;
|
|
760
|
-
const frequencies = /* @__PURE__ */ new Map();
|
|
761
|
-
for (let i = 0; i < len; i++) {
|
|
762
|
-
const ch = str[i];
|
|
763
|
-
frequencies.set(ch, (frequencies.get(ch) || 0) + 1);
|
|
764
|
-
}
|
|
765
|
-
let entropy = 0;
|
|
766
|
-
for (const count of frequencies.values()) {
|
|
767
|
-
const p = count / len;
|
|
768
|
-
entropy -= p * Math.log2(p);
|
|
769
|
-
}
|
|
770
|
-
return entropy;
|
|
652
|
+
);
|
|
771
653
|
}
|
|
772
654
|
|
|
773
655
|
// src/core/validate.ts
|
|
774
656
|
function makeRequest(url, headers, timeoutMs = 1e4) {
|
|
775
|
-
return
|
|
657
|
+
return httpRequest({ url, method: "GET", headers, timeoutMs });
|
|
776
658
|
}
|
|
777
659
|
var ProviderRegistry = class {
|
|
778
660
|
providers = /* @__PURE__ */ new Map();
|
|
@@ -964,1079 +846,1374 @@ async function rotateWithProvider(value, providerName) {
|
|
|
964
846
|
newValue
|
|
965
847
|
};
|
|
966
848
|
}
|
|
967
|
-
async function ciValidateBatch(secrets) {
|
|
968
|
-
const results = [];
|
|
969
|
-
for (const s of secrets) {
|
|
970
|
-
const validation = await validateSecret(s.value, {
|
|
971
|
-
provider: s.provider,
|
|
972
|
-
validationUrl: s.validationUrl
|
|
973
|
-
});
|
|
974
|
-
results.push({
|
|
975
|
-
key: s.key,
|
|
976
|
-
validation,
|
|
977
|
-
requiresRotation: validation.status === "invalid"
|
|
978
|
-
});
|
|
979
|
-
}
|
|
980
|
-
const failCount = results.filter((r) => !r.validation.valid).length;
|
|
981
|
-
return { results, allValid: failCount === 0, failCount };
|
|
849
|
+
async function ciValidateBatch(secrets) {
|
|
850
|
+
const results = [];
|
|
851
|
+
for (const s of secrets) {
|
|
852
|
+
const validation = await validateSecret(s.value, {
|
|
853
|
+
provider: s.provider,
|
|
854
|
+
validationUrl: s.validationUrl
|
|
855
|
+
});
|
|
856
|
+
results.push({
|
|
857
|
+
key: s.key,
|
|
858
|
+
validation,
|
|
859
|
+
requiresRotation: validation.status === "invalid"
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
const failCount = results.filter((r) => !r.validation.valid).length;
|
|
863
|
+
return { results, allValid: failCount === 0, failCount };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/core/context.ts
|
|
867
|
+
function getProjectContext(opts2 = {}) {
|
|
868
|
+
const projectPath8 = opts2.projectPath ?? process.cwd();
|
|
869
|
+
const envResult = collapseEnvironment({ projectPath: projectPath8 });
|
|
870
|
+
const secretsList = listSecrets({
|
|
871
|
+
...opts2,
|
|
872
|
+
projectPath: projectPath8,
|
|
873
|
+
silent: true
|
|
874
|
+
});
|
|
875
|
+
let expiredCount = 0;
|
|
876
|
+
let staleCount = 0;
|
|
877
|
+
let protectedCount = 0;
|
|
878
|
+
const secrets = secretsList.map((entry) => {
|
|
879
|
+
const meta = entry.envelope?.meta;
|
|
880
|
+
const decay = entry.decay;
|
|
881
|
+
if (decay?.isExpired) expiredCount++;
|
|
882
|
+
if (decay?.isStale) staleCount++;
|
|
883
|
+
if (meta?.requiresApproval) protectedCount++;
|
|
884
|
+
return {
|
|
885
|
+
key: entry.key,
|
|
886
|
+
scope: entry.scope,
|
|
887
|
+
tags: meta?.tags,
|
|
888
|
+
description: meta?.description,
|
|
889
|
+
provider: meta?.provider,
|
|
890
|
+
requiresApproval: meta?.requiresApproval,
|
|
891
|
+
jitProvider: meta?.jitProvider,
|
|
892
|
+
hasStates: !!(entry.envelope?.states && Object.keys(entry.envelope.states).length > 0),
|
|
893
|
+
isExpired: decay?.isExpired ?? false,
|
|
894
|
+
isStale: decay?.isStale ?? false,
|
|
895
|
+
timeRemaining: decay?.timeRemaining ?? null,
|
|
896
|
+
accessCount: meta?.accessCount ?? 0,
|
|
897
|
+
lastAccessed: meta?.lastAccessedAt ?? null,
|
|
898
|
+
rotationFormat: meta?.rotationFormat
|
|
899
|
+
};
|
|
900
|
+
});
|
|
901
|
+
let manifest = null;
|
|
902
|
+
const config = readProjectConfig(projectPath8);
|
|
903
|
+
if (config?.secrets) {
|
|
904
|
+
const declaredKeys = Object.keys(config.secrets);
|
|
905
|
+
const existingKeys = new Set(secrets.map((s) => s.key));
|
|
906
|
+
const missing = declaredKeys.filter((k) => !existingKeys.has(k));
|
|
907
|
+
manifest = { declared: declaredKeys.length, missing };
|
|
908
|
+
}
|
|
909
|
+
const recentEvents = queryAudit({ limit: 20 });
|
|
910
|
+
const recentActions = recentEvents.map((e) => ({
|
|
911
|
+
action: e.action,
|
|
912
|
+
key: e.key,
|
|
913
|
+
source: e.source,
|
|
914
|
+
timestamp: e.timestamp
|
|
915
|
+
}));
|
|
916
|
+
return {
|
|
917
|
+
projectPath: projectPath8,
|
|
918
|
+
environment: envResult ? { env: envResult.env, source: envResult.source } : null,
|
|
919
|
+
secrets,
|
|
920
|
+
totalSecrets: secrets.length,
|
|
921
|
+
expiredCount,
|
|
922
|
+
staleCount,
|
|
923
|
+
protectedCount,
|
|
924
|
+
manifest,
|
|
925
|
+
validationProviders: registry2.listProviders().map((p) => p.name),
|
|
926
|
+
jitProviders: registry.listProviders().map((p) => p.name),
|
|
927
|
+
hooksCount: listHooks().length,
|
|
928
|
+
recentActions
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/mcp/tools/project.ts
|
|
933
|
+
var { teamId: teamId2, orgId: orgId2, scope: scope2, projectPath: projectPath2, env: env2 } = commonSchemas;
|
|
934
|
+
function registerProjectTools(server2) {
|
|
935
|
+
server2.tool(
|
|
936
|
+
"check_project",
|
|
937
|
+
"[project] 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.",
|
|
938
|
+
{
|
|
939
|
+
projectPath: projectPath2
|
|
940
|
+
},
|
|
941
|
+
async (params) => {
|
|
942
|
+
const toolBlock = enforceToolPolicy("check_project", params.projectPath);
|
|
943
|
+
if (toolBlock) return toolBlock;
|
|
944
|
+
const pp = params.projectPath ?? process.cwd();
|
|
945
|
+
const config = readProjectConfig(pp);
|
|
946
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
947
|
+
return text("No secrets manifest found in .q-ring.json", true);
|
|
948
|
+
}
|
|
949
|
+
const results = [];
|
|
950
|
+
let presentCount = 0;
|
|
951
|
+
let missingCount = 0;
|
|
952
|
+
let expiredCount = 0;
|
|
953
|
+
let staleCount = 0;
|
|
954
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
955
|
+
const result = getEnvelope(key, { projectPath: pp, source: "mcp" });
|
|
956
|
+
if (!result) {
|
|
957
|
+
const status = manifest.required !== false ? "missing" : "optional_missing";
|
|
958
|
+
if (manifest.required !== false) missingCount++;
|
|
959
|
+
results.push({
|
|
960
|
+
key,
|
|
961
|
+
status,
|
|
962
|
+
required: manifest.required !== false,
|
|
963
|
+
description: manifest.description
|
|
964
|
+
});
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const decay = checkDecay(result.envelope);
|
|
968
|
+
if (decay.isExpired) {
|
|
969
|
+
expiredCount++;
|
|
970
|
+
results.push({
|
|
971
|
+
key,
|
|
972
|
+
status: "expired",
|
|
973
|
+
timeRemaining: decay.timeRemaining,
|
|
974
|
+
description: manifest.description
|
|
975
|
+
});
|
|
976
|
+
} else if (decay.isStale) {
|
|
977
|
+
staleCount++;
|
|
978
|
+
results.push({
|
|
979
|
+
key,
|
|
980
|
+
status: "stale",
|
|
981
|
+
lifetimePercent: decay.lifetimePercent,
|
|
982
|
+
timeRemaining: decay.timeRemaining,
|
|
983
|
+
description: manifest.description
|
|
984
|
+
});
|
|
985
|
+
} else {
|
|
986
|
+
presentCount++;
|
|
987
|
+
results.push({ key, status: "ok", description: manifest.description });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const summary = {
|
|
991
|
+
total: Object.keys(config.secrets).length,
|
|
992
|
+
present: presentCount,
|
|
993
|
+
missing: missingCount,
|
|
994
|
+
expired: expiredCount,
|
|
995
|
+
stale: staleCount,
|
|
996
|
+
ready: missingCount === 0 && expiredCount === 0,
|
|
997
|
+
secrets: results
|
|
998
|
+
};
|
|
999
|
+
return text(JSON.stringify(summary, null, 2));
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
server2.tool(
|
|
1003
|
+
"env_generate",
|
|
1004
|
+
"[project] 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.",
|
|
1005
|
+
{
|
|
1006
|
+
projectPath: projectPath2,
|
|
1007
|
+
env: env2
|
|
1008
|
+
},
|
|
1009
|
+
async (params) => {
|
|
1010
|
+
const toolBlock = enforceToolPolicy("env_generate", params.projectPath);
|
|
1011
|
+
if (toolBlock) return toolBlock;
|
|
1012
|
+
const pp = params.projectPath ?? process.cwd();
|
|
1013
|
+
const config = readProjectConfig(pp);
|
|
1014
|
+
if (!config?.secrets || Object.keys(config.secrets).length === 0) {
|
|
1015
|
+
return text("No secrets manifest found in .q-ring.json", true);
|
|
1016
|
+
}
|
|
1017
|
+
const lines = [];
|
|
1018
|
+
const warnings = [];
|
|
1019
|
+
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1020
|
+
const value = getSecret(key, {
|
|
1021
|
+
projectPath: pp,
|
|
1022
|
+
env: params.env,
|
|
1023
|
+
source: "mcp"
|
|
1024
|
+
});
|
|
1025
|
+
if (value === null) {
|
|
1026
|
+
if (manifest.required !== false) {
|
|
1027
|
+
warnings.push(`MISSING (required): ${key}`);
|
|
1028
|
+
}
|
|
1029
|
+
lines.push(`# ${key}=`);
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const result2 = getEnvelope(key, { projectPath: pp, source: "mcp" });
|
|
1033
|
+
if (result2) {
|
|
1034
|
+
const decay = checkDecay(result2.envelope);
|
|
1035
|
+
if (decay.isExpired) warnings.push(`EXPIRED: ${key}`);
|
|
1036
|
+
else if (decay.isStale) warnings.push(`STALE: ${key}`);
|
|
1037
|
+
}
|
|
1038
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1039
|
+
lines.push(`${key}="${escaped}"`);
|
|
1040
|
+
}
|
|
1041
|
+
const output = lines.join("\n");
|
|
1042
|
+
const result = warnings.length > 0 ? `${output}
|
|
1043
|
+
|
|
1044
|
+
# Warnings:
|
|
1045
|
+
${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
1046
|
+
return text(result);
|
|
1047
|
+
}
|
|
1048
|
+
);
|
|
1049
|
+
server2.tool(
|
|
1050
|
+
"detect_environment",
|
|
1051
|
+
"[project] Detect the current environment context (wavefunction collapse). Returns the detected environment and its source (NODE_ENV, git branch, project config, etc.).",
|
|
1052
|
+
{
|
|
1053
|
+
projectPath: projectPath2
|
|
1054
|
+
},
|
|
1055
|
+
async (params) => {
|
|
1056
|
+
const toolBlock = enforceToolPolicy(
|
|
1057
|
+
"detect_environment",
|
|
1058
|
+
params.projectPath
|
|
1059
|
+
);
|
|
1060
|
+
if (toolBlock) return toolBlock;
|
|
1061
|
+
const result = collapseEnvironment({
|
|
1062
|
+
projectPath: params.projectPath ?? process.cwd()
|
|
1063
|
+
});
|
|
1064
|
+
if (!result) {
|
|
1065
|
+
return text(
|
|
1066
|
+
"No environment detected. Set QRING_ENV, NODE_ENV, or create .q-ring.json"
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
return text(JSON.stringify(result, null, 2));
|
|
1070
|
+
}
|
|
1071
|
+
);
|
|
1072
|
+
server2.tool(
|
|
1073
|
+
"get_project_context",
|
|
1074
|
+
"[agent] 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.",
|
|
1075
|
+
{
|
|
1076
|
+
scope: scope2,
|
|
1077
|
+
projectPath: projectPath2,
|
|
1078
|
+
teamId: teamId2,
|
|
1079
|
+
orgId: orgId2
|
|
1080
|
+
},
|
|
1081
|
+
async (params) => {
|
|
1082
|
+
const toolBlock = enforceToolPolicy(
|
|
1083
|
+
"get_project_context",
|
|
1084
|
+
params.projectPath
|
|
1085
|
+
);
|
|
1086
|
+
if (toolBlock) return toolBlock;
|
|
1087
|
+
const context = getProjectContext(opts(params));
|
|
1088
|
+
return text(JSON.stringify(context, null, 2));
|
|
1089
|
+
}
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/mcp/tools/tunnel.ts
|
|
1094
|
+
import { z as z3 } from "zod";
|
|
1095
|
+
function registerTunnelTools(server2) {
|
|
1096
|
+
server2.tool(
|
|
1097
|
+
"tunnel_create",
|
|
1098
|
+
"[tunnel] Create an ephemeral secret that exists only in memory (quantum tunneling). Never persisted to disk. Optional TTL and max-reads for self-destruction.",
|
|
1099
|
+
{
|
|
1100
|
+
value: z3.string().describe("The secret value"),
|
|
1101
|
+
ttlSeconds: z3.number().optional().describe("Auto-expire after N seconds"),
|
|
1102
|
+
maxReads: z3.number().optional().describe("Self-destruct after N reads")
|
|
1103
|
+
},
|
|
1104
|
+
async (params) => {
|
|
1105
|
+
const toolBlock = enforceToolPolicy("tunnel_create");
|
|
1106
|
+
if (toolBlock) return toolBlock;
|
|
1107
|
+
const id = tunnelCreate(params.value, {
|
|
1108
|
+
ttlSeconds: params.ttlSeconds,
|
|
1109
|
+
maxReads: params.maxReads
|
|
1110
|
+
});
|
|
1111
|
+
return text(JSON.stringify({ ok: true, data: { id } }, null, 2));
|
|
1112
|
+
}
|
|
1113
|
+
);
|
|
1114
|
+
server2.tool(
|
|
1115
|
+
"tunnel_read",
|
|
1116
|
+
"[tunnel] Read an ephemeral tunneled secret by ID. May self-destruct if max-reads is reached.",
|
|
1117
|
+
{
|
|
1118
|
+
id: z3.string().describe("Tunnel ID")
|
|
1119
|
+
},
|
|
1120
|
+
async (params) => {
|
|
1121
|
+
const toolBlock = enforceToolPolicy("tunnel_read");
|
|
1122
|
+
if (toolBlock) return toolBlock;
|
|
1123
|
+
const value = tunnelRead(params.id);
|
|
1124
|
+
if (value === null) {
|
|
1125
|
+
return text(`Tunnel "${params.id}" not found or expired`, true);
|
|
1126
|
+
}
|
|
1127
|
+
return text(
|
|
1128
|
+
JSON.stringify({ ok: true, data: { id: params.id, value } }, null, 2)
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
);
|
|
1132
|
+
server2.tool(
|
|
1133
|
+
"tunnel_list",
|
|
1134
|
+
"[tunnel] List active tunneled secrets (IDs and metadata only, never values).",
|
|
1135
|
+
{},
|
|
1136
|
+
async () => {
|
|
1137
|
+
const toolBlock = enforceToolPolicy("tunnel_list");
|
|
1138
|
+
if (toolBlock) return toolBlock;
|
|
1139
|
+
const tunnels = tunnelList();
|
|
1140
|
+
if (tunnels.length === 0) return text("No active tunnels");
|
|
1141
|
+
const lines = tunnels.map((t) => {
|
|
1142
|
+
const parts = [t.id];
|
|
1143
|
+
parts.push(`reads:${t.accessCount}`);
|
|
1144
|
+
if (t.maxReads) parts.push(`max:${t.maxReads}`);
|
|
1145
|
+
if (t.expiresAt) {
|
|
1146
|
+
const rem = Math.max(
|
|
1147
|
+
0,
|
|
1148
|
+
Math.floor((t.expiresAt - Date.now()) / 1e3)
|
|
1149
|
+
);
|
|
1150
|
+
parts.push(`expires:${rem}s`);
|
|
1151
|
+
}
|
|
1152
|
+
return parts.join(" | ");
|
|
1153
|
+
});
|
|
1154
|
+
return text(lines.join("\n"));
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
1157
|
+
server2.tool(
|
|
1158
|
+
"tunnel_destroy",
|
|
1159
|
+
"[tunnel] Immediately destroy a tunneled secret.",
|
|
1160
|
+
{
|
|
1161
|
+
id: z3.string().describe("Tunnel ID")
|
|
1162
|
+
},
|
|
1163
|
+
async (params) => {
|
|
1164
|
+
const toolBlock = enforceToolPolicy("tunnel_destroy");
|
|
1165
|
+
if (toolBlock) return toolBlock;
|
|
1166
|
+
const destroyed = tunnelDestroy(params.id);
|
|
1167
|
+
return text(
|
|
1168
|
+
destroyed ? `Destroyed ${params.id}` : `Tunnel "${params.id}" not found`,
|
|
1169
|
+
!destroyed
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
);
|
|
982
1173
|
}
|
|
983
1174
|
|
|
984
|
-
// src/
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
manifest = { declared: declaredKeys.length, missing };
|
|
1026
|
-
}
|
|
1027
|
-
const recentEvents = queryAudit({ limit: 20 });
|
|
1028
|
-
const recentActions = recentEvents.map((e) => ({
|
|
1029
|
-
action: e.action,
|
|
1030
|
-
key: e.key,
|
|
1031
|
-
source: e.source,
|
|
1032
|
-
timestamp: e.timestamp
|
|
1033
|
-
}));
|
|
1034
|
-
return {
|
|
1035
|
-
projectPath,
|
|
1036
|
-
environment: envResult ? { env: envResult.env, source: envResult.source } : null,
|
|
1175
|
+
// src/mcp/tools/teleport.ts
|
|
1176
|
+
import { z as z5 } from "zod";
|
|
1177
|
+
|
|
1178
|
+
// src/core/teleport.ts
|
|
1179
|
+
import {
|
|
1180
|
+
randomBytes as randomBytes2,
|
|
1181
|
+
createCipheriv,
|
|
1182
|
+
createDecipheriv,
|
|
1183
|
+
pbkdf2Sync
|
|
1184
|
+
} from "crypto";
|
|
1185
|
+
import { z as z4 } from "zod";
|
|
1186
|
+
var ALGORITHM = "aes-256-gcm";
|
|
1187
|
+
var KEY_LENGTH = 32;
|
|
1188
|
+
var IV_LENGTH = 12;
|
|
1189
|
+
var SALT_LENGTH = 32;
|
|
1190
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
1191
|
+
var TeleportBundleSchema = z4.object({
|
|
1192
|
+
v: z4.literal(1),
|
|
1193
|
+
data: z4.string(),
|
|
1194
|
+
salt: z4.string(),
|
|
1195
|
+
iv: z4.string(),
|
|
1196
|
+
tag: z4.string(),
|
|
1197
|
+
createdAt: z4.string(),
|
|
1198
|
+
count: z4.number()
|
|
1199
|
+
});
|
|
1200
|
+
var TeleportPayloadSchema = z4.object({
|
|
1201
|
+
secrets: z4.array(
|
|
1202
|
+
z4.object({
|
|
1203
|
+
key: z4.string(),
|
|
1204
|
+
value: z4.string(),
|
|
1205
|
+
scope: z4.string().optional()
|
|
1206
|
+
})
|
|
1207
|
+
),
|
|
1208
|
+
exportedAt: z4.string(),
|
|
1209
|
+
exportedBy: z4.string().optional()
|
|
1210
|
+
});
|
|
1211
|
+
function deriveKey(passphrase, salt) {
|
|
1212
|
+
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
1213
|
+
}
|
|
1214
|
+
function teleportPack(secrets, passphrase) {
|
|
1215
|
+
const payload = {
|
|
1037
1216
|
secrets,
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1217
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1218
|
+
};
|
|
1219
|
+
const plaintext = JSON.stringify(payload);
|
|
1220
|
+
const salt = randomBytes2(SALT_LENGTH);
|
|
1221
|
+
const iv = randomBytes2(IV_LENGTH);
|
|
1222
|
+
const key = deriveKey(passphrase, salt);
|
|
1223
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
1224
|
+
const encrypted = Buffer.concat([
|
|
1225
|
+
cipher.update(plaintext, "utf8"),
|
|
1226
|
+
cipher.final()
|
|
1227
|
+
]);
|
|
1228
|
+
const tag = cipher.getAuthTag();
|
|
1229
|
+
const bundle = {
|
|
1230
|
+
v: 1,
|
|
1231
|
+
data: encrypted.toString("base64"),
|
|
1232
|
+
salt: salt.toString("base64"),
|
|
1233
|
+
iv: iv.toString("base64"),
|
|
1234
|
+
tag: tag.toString("base64"),
|
|
1235
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1236
|
+
count: secrets.length
|
|
1047
1237
|
};
|
|
1238
|
+
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
1048
1239
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
import { Entry } from "@napi-rs/keyring";
|
|
1056
|
-
var MEMORY_FILE = "agent-memory.enc";
|
|
1057
|
-
var KEYRING_SERVICE = "qring-memory-key";
|
|
1058
|
-
var KEYRING_ACCOUNT = "encryption-key";
|
|
1059
|
-
function getMemoryDir() {
|
|
1060
|
-
const dir = join2(homedir(), ".config", "q-ring");
|
|
1061
|
-
if (!existsSync2(dir)) {
|
|
1062
|
-
mkdirSync(dir, { recursive: true });
|
|
1240
|
+
function teleportUnpack(encoded, passphrase) {
|
|
1241
|
+
let bundleJson;
|
|
1242
|
+
try {
|
|
1243
|
+
bundleJson = Buffer.from(encoded, "base64").toString("utf8");
|
|
1244
|
+
} catch {
|
|
1245
|
+
throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
|
|
1063
1246
|
}
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
function getMemoryPath() {
|
|
1067
|
-
return join2(getMemoryDir(), MEMORY_FILE);
|
|
1068
|
-
}
|
|
1069
|
-
function deriveLegacyKey() {
|
|
1070
|
-
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1071
|
-
return createHash("sha256").update(fingerprint).digest();
|
|
1072
|
-
}
|
|
1073
|
-
function getOrCreateKey() {
|
|
1247
|
+
let rawBundle;
|
|
1074
1248
|
try {
|
|
1075
|
-
|
|
1076
|
-
const stored = entry.getPassword();
|
|
1077
|
-
if (stored) return Buffer.from(stored, "base64");
|
|
1078
|
-
const key = randomBytes3(32);
|
|
1079
|
-
entry.setPassword(key.toString("base64"));
|
|
1080
|
-
return key;
|
|
1249
|
+
rawBundle = JSON.parse(bundleJson);
|
|
1081
1250
|
} catch {
|
|
1082
|
-
|
|
1083
|
-
return deriveLegacyKey();
|
|
1251
|
+
throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
|
|
1084
1252
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
const
|
|
1097
|
-
const
|
|
1098
|
-
const encrypted = Buffer.from(parts[2], "base64");
|
|
1099
|
-
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
1253
|
+
const parsedBundle = TeleportBundleSchema.safeParse(rawBundle);
|
|
1254
|
+
if (!parsedBundle.success) {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
`ERR_TELEPORT_CORRUPT: invalid bundle shape (${parsedBundle.error.message})`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
const bundle = parsedBundle.data;
|
|
1260
|
+
const salt = Buffer.from(bundle.salt, "base64");
|
|
1261
|
+
const iv = Buffer.from(bundle.iv, "base64");
|
|
1262
|
+
const tag = Buffer.from(bundle.tag, "base64");
|
|
1263
|
+
const encrypted = Buffer.from(bundle.data, "base64");
|
|
1264
|
+
const key = deriveKey(passphrase, salt);
|
|
1265
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
1100
1266
|
decipher.setAuthTag(tag);
|
|
1101
|
-
|
|
1102
|
-
}
|
|
1103
|
-
function encrypt(data) {
|
|
1104
|
-
return encryptWith(data, getOrCreateKey());
|
|
1105
|
-
}
|
|
1106
|
-
function decrypt(blob) {
|
|
1107
|
-
const key = getOrCreateKey();
|
|
1267
|
+
let decrypted;
|
|
1108
1268
|
try {
|
|
1109
|
-
|
|
1269
|
+
decrypted = Buffer.concat([
|
|
1270
|
+
decipher.update(encrypted),
|
|
1271
|
+
decipher.final()
|
|
1272
|
+
]);
|
|
1110
1273
|
} catch {
|
|
1111
|
-
|
|
1112
|
-
const plain = decryptWith(blob, legacy);
|
|
1113
|
-
writeFileSync2(getMemoryPath(), encryptWith(plain, key), "utf8");
|
|
1114
|
-
return plain;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
function loadStore() {
|
|
1118
|
-
const path = getMemoryPath();
|
|
1119
|
-
if (!existsSync2(path)) {
|
|
1120
|
-
return { entries: {} };
|
|
1274
|
+
throw new Error("ERR_TELEPORT_BAD_PASSPHRASE: decryption failed (wrong passphrase or corrupt data)");
|
|
1121
1275
|
}
|
|
1276
|
+
let rawPayload;
|
|
1122
1277
|
try {
|
|
1123
|
-
|
|
1124
|
-
const decrypted = decrypt(raw);
|
|
1125
|
-
return JSON.parse(decrypted);
|
|
1278
|
+
rawPayload = JSON.parse(decrypted.toString("utf8"));
|
|
1126
1279
|
} catch {
|
|
1127
|
-
|
|
1280
|
+
throw new Error("ERR_TELEPORT_CORRUPT: decrypted payload is not valid JSON");
|
|
1128
1281
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
function remember(key, value) {
|
|
1136
|
-
const store = loadStore();
|
|
1137
|
-
store.entries[key] = {
|
|
1138
|
-
value,
|
|
1139
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1140
|
-
};
|
|
1141
|
-
saveStore(store);
|
|
1142
|
-
}
|
|
1143
|
-
function recall(key) {
|
|
1144
|
-
const store = loadStore();
|
|
1145
|
-
return store.entries[key]?.value ?? null;
|
|
1146
|
-
}
|
|
1147
|
-
function listMemory() {
|
|
1148
|
-
const store = loadStore();
|
|
1149
|
-
return Object.entries(store.entries).map(([key, entry]) => ({
|
|
1150
|
-
key,
|
|
1151
|
-
updatedAt: entry.updatedAt
|
|
1152
|
-
}));
|
|
1153
|
-
}
|
|
1154
|
-
function forget(key) {
|
|
1155
|
-
const store = loadStore();
|
|
1156
|
-
if (key in store.entries) {
|
|
1157
|
-
delete store.entries[key];
|
|
1158
|
-
saveStore(store);
|
|
1159
|
-
return true;
|
|
1282
|
+
const payload = TeleportPayloadSchema.safeParse(rawPayload);
|
|
1283
|
+
if (!payload.success) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`ERR_TELEPORT_CORRUPT: invalid payload (${payload.error.message})`
|
|
1286
|
+
);
|
|
1160
1287
|
}
|
|
1161
|
-
return
|
|
1288
|
+
return payload.data;
|
|
1162
1289
|
}
|
|
1163
1290
|
|
|
1164
|
-
// src/mcp/
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
content: [{ type: "text", text: t }],
|
|
1168
|
-
...isError ? { isError: true } : {}
|
|
1169
|
-
};
|
|
1170
|
-
}
|
|
1171
|
-
function opts(params) {
|
|
1172
|
-
return {
|
|
1173
|
-
scope: params.scope,
|
|
1174
|
-
projectPath: params.projectPath ?? process.cwd(),
|
|
1175
|
-
teamId: params.teamId,
|
|
1176
|
-
orgId: params.orgId,
|
|
1177
|
-
env: params.env,
|
|
1178
|
-
source: "mcp"
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
function enforceToolPolicy(toolName, projectPath) {
|
|
1182
|
-
const decision = checkToolPolicy(toolName, projectPath);
|
|
1183
|
-
if (!decision.allowed) {
|
|
1184
|
-
return text(`Policy Denied: ${decision.reason} (source: ${decision.policySource})`, true);
|
|
1185
|
-
}
|
|
1186
|
-
return null;
|
|
1187
|
-
}
|
|
1188
|
-
function createMcpServer() {
|
|
1189
|
-
const server2 = new McpServer({
|
|
1190
|
-
name: "q-ring",
|
|
1191
|
-
version: "0.2.0"
|
|
1192
|
-
});
|
|
1193
|
-
const teamIdSchema = z.string().optional().describe("Team identifier for team-scoped secrets");
|
|
1194
|
-
const orgIdSchema = z.string().optional().describe("Org identifier for org-scoped secrets");
|
|
1195
|
-
const scopeSchema = z.enum(["global", "project", "team", "org"]).optional().describe("Scope: global, project, team, or org");
|
|
1196
|
-
const projectPathSchema = z.string().optional().describe("Project root path for project-scoped secrets");
|
|
1197
|
-
const envSchema = z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)");
|
|
1291
|
+
// src/mcp/tools/teleport.ts
|
|
1292
|
+
var { teamId: teamId3, orgId: orgId3, scope: scope3, projectPath: projectPath3 } = commonSchemas;
|
|
1293
|
+
function registerTeleportTools(server2) {
|
|
1198
1294
|
server2.tool(
|
|
1199
|
-
"
|
|
1200
|
-
"
|
|
1295
|
+
"teleport_pack",
|
|
1296
|
+
"[teleport] Pack secrets into an AES-256-GCM encrypted bundle for sharing between machines (quantum teleportation).",
|
|
1201
1297
|
{
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
teamId:
|
|
1207
|
-
orgId:
|
|
1298
|
+
keys: z5.array(z5.string()).optional().describe("Specific keys to pack (all if omitted)"),
|
|
1299
|
+
passphrase: z5.string().describe("Encryption passphrase"),
|
|
1300
|
+
scope: scope3,
|
|
1301
|
+
projectPath: projectPath3,
|
|
1302
|
+
teamId: teamId3,
|
|
1303
|
+
orgId: orgId3
|
|
1208
1304
|
},
|
|
1209
1305
|
async (params) => {
|
|
1210
|
-
const toolBlock = enforceToolPolicy("
|
|
1306
|
+
const toolBlock = enforceToolPolicy("teleport_pack", params.projectPath);
|
|
1307
|
+
if (toolBlock) return toolBlock;
|
|
1308
|
+
const o = opts(params);
|
|
1309
|
+
const entries = listSecrets(o);
|
|
1310
|
+
const secrets = [];
|
|
1311
|
+
for (const entry of entries) {
|
|
1312
|
+
if (params.keys && !params.keys.includes(entry.key)) continue;
|
|
1313
|
+
const value = getSecret(entry.key, { ...o, scope: entry.scope });
|
|
1314
|
+
if (value !== null) {
|
|
1315
|
+
secrets.push({ key: entry.key, value, scope: entry.scope });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (secrets.length === 0) return text("No secrets to pack", true);
|
|
1319
|
+
const bundle = teleportPack(secrets, params.passphrase);
|
|
1320
|
+
return text(bundle);
|
|
1321
|
+
}
|
|
1322
|
+
);
|
|
1323
|
+
server2.tool(
|
|
1324
|
+
"teleport_unpack",
|
|
1325
|
+
"[teleport] Decrypt and import secrets from a teleport bundle.",
|
|
1326
|
+
{
|
|
1327
|
+
bundle: z5.string().describe("Base64-encoded encrypted bundle"),
|
|
1328
|
+
passphrase: z5.string().describe("Decryption passphrase"),
|
|
1329
|
+
scope: scope3.default("global"),
|
|
1330
|
+
projectPath: projectPath3,
|
|
1331
|
+
teamId: teamId3,
|
|
1332
|
+
orgId: orgId3,
|
|
1333
|
+
dryRun: z5.boolean().optional().default(false).describe("Preview without importing")
|
|
1334
|
+
},
|
|
1335
|
+
async (params) => {
|
|
1336
|
+
const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
|
|
1211
1337
|
if (toolBlock) return toolBlock;
|
|
1212
1338
|
try {
|
|
1213
|
-
const
|
|
1214
|
-
if (
|
|
1215
|
-
|
|
1339
|
+
const payload = teleportUnpack(params.bundle, params.passphrase);
|
|
1340
|
+
if (params.dryRun) {
|
|
1341
|
+
const preview = payload.secrets.map((s) => `${s.key} [${s.scope ?? "global"}]`).join("\n");
|
|
1342
|
+
return text(
|
|
1343
|
+
`Would import ${payload.secrets.length} secrets:
|
|
1344
|
+
${preview}`
|
|
1345
|
+
);
|
|
1216
1346
|
}
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1347
|
+
const o = opts(params);
|
|
1348
|
+
for (const s of payload.secrets) {
|
|
1349
|
+
setSecret(s.key, s.value, o);
|
|
1350
|
+
}
|
|
1351
|
+
return text(
|
|
1352
|
+
`Imported ${payload.secrets.length} secret(s) from teleport bundle`
|
|
1353
|
+
);
|
|
1220
1354
|
} catch (err) {
|
|
1221
|
-
|
|
1355
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1356
|
+
return text(JSON.stringify({ ok: false, error: { message: msg } }), true);
|
|
1222
1357
|
}
|
|
1223
1358
|
}
|
|
1224
1359
|
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/mcp/tools/audit.ts
|
|
1363
|
+
import { z as z6 } from "zod";
|
|
1364
|
+
var { teamId: teamId4, orgId: orgId4, scope: scope4, projectPath: projectPath4 } = commonSchemas;
|
|
1365
|
+
function registerAuditTools(server2) {
|
|
1225
1366
|
server2.tool(
|
|
1226
|
-
"
|
|
1227
|
-
"
|
|
1367
|
+
"audit_log",
|
|
1368
|
+
"[audit] Query the audit log for secret access history (observer effect). Shows who accessed what and when.",
|
|
1228
1369
|
{
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1370
|
+
key: z6.string().optional().describe("Filter by key"),
|
|
1371
|
+
action: z6.enum([
|
|
1372
|
+
"read",
|
|
1373
|
+
"write",
|
|
1374
|
+
"delete",
|
|
1375
|
+
"list",
|
|
1376
|
+
"export",
|
|
1377
|
+
"generate",
|
|
1378
|
+
"entangle",
|
|
1379
|
+
"tunnel",
|
|
1380
|
+
"teleport",
|
|
1381
|
+
"collapse"
|
|
1382
|
+
]).optional().describe("Filter by action"),
|
|
1383
|
+
limit: z6.number().optional().default(20).describe("Max events to return")
|
|
1237
1384
|
},
|
|
1238
1385
|
async (params) => {
|
|
1239
|
-
const toolBlock = enforceToolPolicy("
|
|
1386
|
+
const toolBlock = enforceToolPolicy("audit_log");
|
|
1240
1387
|
if (toolBlock) return toolBlock;
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
if (params.filter) {
|
|
1256
|
-
const escaped = params.filter.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1257
|
-
const regex = new RegExp("^" + escaped + "$", "i");
|
|
1258
|
-
entries = entries.filter((e) => regex.test(e.key));
|
|
1259
|
-
}
|
|
1260
|
-
if (entries.length === 0) return text("No secrets found");
|
|
1261
|
-
const lines = entries.map((e) => {
|
|
1262
|
-
const parts = [`[${e.scope}] ${e.key}`];
|
|
1263
|
-
if (e.envelope?.states) {
|
|
1264
|
-
parts.push(`states:[${Object.keys(e.envelope.states).join(",")}]`);
|
|
1265
|
-
}
|
|
1266
|
-
if (e.decay?.isExpired) {
|
|
1267
|
-
parts.push("EXPIRED");
|
|
1268
|
-
} else if (e.decay?.isStale) {
|
|
1269
|
-
parts.push(`stale(${e.decay.lifetimePercent}%)`);
|
|
1270
|
-
}
|
|
1271
|
-
if (e.decay?.timeRemaining && !e.decay.isExpired) {
|
|
1272
|
-
parts.push(`ttl:${e.decay.timeRemaining}`);
|
|
1273
|
-
}
|
|
1274
|
-
if (e.envelope?.meta.entangled?.length) {
|
|
1275
|
-
parts.push(`entangled:${e.envelope.meta.entangled.length}`);
|
|
1276
|
-
}
|
|
1277
|
-
if (e.envelope && e.envelope.meta.accessCount > 0) {
|
|
1278
|
-
parts.push(`reads:${e.envelope.meta.accessCount}`);
|
|
1279
|
-
}
|
|
1388
|
+
const events = queryAudit({
|
|
1389
|
+
key: params.key,
|
|
1390
|
+
action: params.action,
|
|
1391
|
+
limit: params.limit
|
|
1392
|
+
});
|
|
1393
|
+
if (events.length === 0) return text("No audit events found");
|
|
1394
|
+
const lines = events.map((e) => {
|
|
1395
|
+
const parts = [e.timestamp, e.action];
|
|
1396
|
+
if (e.key) parts.push(e.key);
|
|
1397
|
+
if (e.scope) parts.push(`[${e.scope}]`);
|
|
1398
|
+
if (e.env) parts.push(`env:${e.env}`);
|
|
1399
|
+
if (e.detail) parts.push(e.detail);
|
|
1280
1400
|
return parts.join(" | ");
|
|
1281
1401
|
});
|
|
1282
1402
|
return text(lines.join("\n"));
|
|
1283
1403
|
}
|
|
1284
1404
|
);
|
|
1285
1405
|
server2.tool(
|
|
1286
|
-
"
|
|
1287
|
-
"
|
|
1406
|
+
"detect_anomalies",
|
|
1407
|
+
"[audit] Scan for anomalous secret access patterns: burst reads, unusual-hour access. Returns findings and recommendations.",
|
|
1288
1408
|
{
|
|
1289
|
-
key:
|
|
1290
|
-
value: z.string().describe("The secret value"),
|
|
1291
|
-
scope: scopeSchema.default("global"),
|
|
1292
|
-
projectPath: projectPathSchema,
|
|
1293
|
-
env: z.string().optional().describe("If provided, sets the value for this specific environment (superposition)"),
|
|
1294
|
-
ttlSeconds: z.number().optional().describe("Time-to-live in seconds (quantum decay)"),
|
|
1295
|
-
description: z.string().optional().describe("Human-readable description"),
|
|
1296
|
-
tags: z.array(z.string()).optional().describe("Tags for organization"),
|
|
1297
|
-
rotationFormat: z.enum(["hex", "base64", "alphanumeric", "uuid", "api-key", "token", "password"]).optional().describe("Format for auto-rotation when this secret expires"),
|
|
1298
|
-
rotationPrefix: z.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')"),
|
|
1299
|
-
teamId: teamIdSchema,
|
|
1300
|
-
orgId: orgIdSchema
|
|
1409
|
+
key: z6.string().optional().describe("Check anomalies for a specific key")
|
|
1301
1410
|
},
|
|
1302
1411
|
async (params) => {
|
|
1303
|
-
const toolBlock = enforceToolPolicy("
|
|
1412
|
+
const toolBlock = enforceToolPolicy("detect_anomalies");
|
|
1304
1413
|
if (toolBlock) return toolBlock;
|
|
1305
|
-
const
|
|
1306
|
-
if (
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
states[params.env] = params.value;
|
|
1310
|
-
if (existing?.envelope?.value && !states["default"]) {
|
|
1311
|
-
states["default"] = existing.envelope.value;
|
|
1312
|
-
}
|
|
1313
|
-
setSecret(params.key, "", {
|
|
1314
|
-
...o,
|
|
1315
|
-
states,
|
|
1316
|
-
defaultEnv: existing?.envelope?.defaultEnv ?? params.env,
|
|
1317
|
-
ttlSeconds: params.ttlSeconds,
|
|
1318
|
-
description: params.description,
|
|
1319
|
-
tags: params.tags,
|
|
1320
|
-
rotationFormat: params.rotationFormat,
|
|
1321
|
-
rotationPrefix: params.rotationPrefix
|
|
1322
|
-
});
|
|
1323
|
-
return text(`[${params.scope ?? "global"}] ${params.key} set for env:${params.env}`);
|
|
1324
|
-
}
|
|
1325
|
-
setSecret(params.key, params.value, {
|
|
1326
|
-
...o,
|
|
1327
|
-
ttlSeconds: params.ttlSeconds,
|
|
1328
|
-
description: params.description,
|
|
1329
|
-
tags: params.tags,
|
|
1330
|
-
rotationFormat: params.rotationFormat,
|
|
1331
|
-
rotationPrefix: params.rotationPrefix
|
|
1332
|
-
});
|
|
1333
|
-
return text(`[${params.scope ?? "global"}] ${params.key} saved`);
|
|
1414
|
+
const anomalies = detectAnomalies(params.key);
|
|
1415
|
+
if (anomalies.length === 0) return text("No anomalies detected");
|
|
1416
|
+
const lines = anomalies.map((a) => `[${a.type}] ${a.description}`);
|
|
1417
|
+
return text(lines.join("\n"));
|
|
1334
1418
|
}
|
|
1335
1419
|
);
|
|
1336
1420
|
server2.tool(
|
|
1337
|
-
"
|
|
1338
|
-
"
|
|
1421
|
+
"health_check",
|
|
1422
|
+
"[health] Run a comprehensive health check on all secrets: decay status, staleness, anomalies, entropy assessment.",
|
|
1339
1423
|
{
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
orgId: orgIdSchema
|
|
1424
|
+
scope: scope4,
|
|
1425
|
+
projectPath: projectPath4,
|
|
1426
|
+
teamId: teamId4,
|
|
1427
|
+
orgId: orgId4
|
|
1345
1428
|
},
|
|
1346
1429
|
async (params) => {
|
|
1347
|
-
const toolBlock = enforceToolPolicy("
|
|
1430
|
+
const toolBlock = enforceToolPolicy("health_check", params.projectPath);
|
|
1348
1431
|
if (toolBlock) return toolBlock;
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1432
|
+
const entries = listSecrets(opts(params));
|
|
1433
|
+
const anomalies = detectAnomalies();
|
|
1434
|
+
let healthy = 0;
|
|
1435
|
+
let stale = 0;
|
|
1436
|
+
let expired = 0;
|
|
1437
|
+
let noDecay = 0;
|
|
1438
|
+
const issues = [];
|
|
1439
|
+
for (const entry of entries) {
|
|
1440
|
+
if (!entry.decay?.timeRemaining) {
|
|
1441
|
+
noDecay++;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (entry.decay.isExpired) {
|
|
1445
|
+
expired++;
|
|
1446
|
+
issues.push(`EXPIRED: ${entry.key}`);
|
|
1447
|
+
} else if (entry.decay.isStale) {
|
|
1448
|
+
stale++;
|
|
1449
|
+
issues.push(
|
|
1450
|
+
`STALE: ${entry.key} (${entry.decay.lifetimePercent}%, ${entry.decay.timeRemaining} left)`
|
|
1451
|
+
);
|
|
1452
|
+
} else {
|
|
1453
|
+
healthy++;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
const summary = [
|
|
1457
|
+
`Secrets: ${entries.length} total`,
|
|
1458
|
+
`Healthy: ${healthy} | Stale: ${stale} | Expired: ${expired} | No decay: ${noDecay}`,
|
|
1459
|
+
`Anomalies: ${anomalies.length}`
|
|
1460
|
+
];
|
|
1461
|
+
if (issues.length > 0) {
|
|
1462
|
+
summary.push("", "Issues:", ...issues);
|
|
1463
|
+
}
|
|
1464
|
+
if (anomalies.length > 0) {
|
|
1465
|
+
summary.push(
|
|
1466
|
+
"",
|
|
1467
|
+
"Anomalies:",
|
|
1468
|
+
...anomalies.map((a) => `[${a.type}] ${a.description}`)
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return text(summary.join("\n"));
|
|
1354
1472
|
}
|
|
1355
1473
|
);
|
|
1356
1474
|
server2.tool(
|
|
1357
|
-
"
|
|
1358
|
-
"
|
|
1359
|
-
{
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
projectPath: projectPathSchema,
|
|
1363
|
-
teamId: teamIdSchema,
|
|
1364
|
-
orgId: orgIdSchema
|
|
1365
|
-
},
|
|
1366
|
-
async (params) => {
|
|
1367
|
-
const toolBlock = enforceToolPolicy("has_secret", params.projectPath);
|
|
1475
|
+
"verify_audit_chain",
|
|
1476
|
+
"[audit] Verify the tamper-evident hash chain of the audit log. Returns integrity status and the first break point if tampered.",
|
|
1477
|
+
{},
|
|
1478
|
+
async () => {
|
|
1479
|
+
const toolBlock = enforceToolPolicy("verify_audit_chain");
|
|
1368
1480
|
if (toolBlock) return toolBlock;
|
|
1369
|
-
|
|
1481
|
+
const result = verifyAuditChain();
|
|
1482
|
+
return text(JSON.stringify(result, null, 2));
|
|
1370
1483
|
}
|
|
1371
1484
|
);
|
|
1372
1485
|
server2.tool(
|
|
1373
|
-
"
|
|
1374
|
-
"Export
|
|
1486
|
+
"export_audit",
|
|
1487
|
+
"[audit] Export audit events in a portable format (jsonl, json, or csv) with optional time range filtering.",
|
|
1375
1488
|
{
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
scope: scopeSchema,
|
|
1380
|
-
projectPath: projectPathSchema,
|
|
1381
|
-
env: envSchema,
|
|
1382
|
-
teamId: teamIdSchema,
|
|
1383
|
-
orgId: orgIdSchema
|
|
1489
|
+
since: z6.string().optional().describe("Start date (ISO 8601)"),
|
|
1490
|
+
until: z6.string().optional().describe("End date (ISO 8601)"),
|
|
1491
|
+
format: z6.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe("Output format")
|
|
1384
1492
|
},
|
|
1385
1493
|
async (params) => {
|
|
1386
|
-
const toolBlock = enforceToolPolicy("
|
|
1494
|
+
const toolBlock = enforceToolPolicy("export_audit");
|
|
1387
1495
|
if (toolBlock) return toolBlock;
|
|
1388
|
-
const output =
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
tags: params.tags
|
|
1496
|
+
const output = exportAudit({
|
|
1497
|
+
since: params.since,
|
|
1498
|
+
until: params.until,
|
|
1499
|
+
format: params.format
|
|
1393
1500
|
});
|
|
1394
|
-
if (!output.trim()) return text("No secrets matched the filters", true);
|
|
1395
1501
|
return text(output);
|
|
1396
1502
|
}
|
|
1397
1503
|
);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/mcp/tools/validation.ts
|
|
1507
|
+
import { z as z7 } from "zod";
|
|
1508
|
+
var { teamId: teamId5, orgId: orgId5, scope: scope5, projectPath: projectPath5 } = commonSchemas;
|
|
1509
|
+
function registerValidationTools(server2) {
|
|
1398
1510
|
server2.tool(
|
|
1399
|
-
"
|
|
1400
|
-
"
|
|
1511
|
+
"validate_secret",
|
|
1512
|
+
"[validation] 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.",
|
|
1401
1513
|
{
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1514
|
+
key: z7.string().describe("The secret key name"),
|
|
1515
|
+
provider: z7.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
|
|
1516
|
+
scope: scope5,
|
|
1517
|
+
projectPath: projectPath5,
|
|
1518
|
+
teamId: teamId5,
|
|
1519
|
+
orgId: orgId5
|
|
1407
1520
|
},
|
|
1408
1521
|
async (params) => {
|
|
1409
|
-
const toolBlock = enforceToolPolicy("
|
|
1522
|
+
const toolBlock = enforceToolPolicy("validate_secret", params.projectPath);
|
|
1410
1523
|
if (toolBlock) return toolBlock;
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
});
|
|
1418
|
-
const lines = [
|
|
1419
|
-
params.dryRun ? "Dry run \u2014 no changes made" : `Imported ${result.imported.length} secret(s)`
|
|
1420
|
-
];
|
|
1421
|
-
if (result.imported.length > 0) {
|
|
1422
|
-
lines.push(`Keys: ${result.imported.join(", ")}`);
|
|
1423
|
-
}
|
|
1424
|
-
if (result.skipped.length > 0) {
|
|
1425
|
-
lines.push(`Skipped (existing): ${result.skipped.join(", ")}`);
|
|
1426
|
-
}
|
|
1427
|
-
return text(lines.join("\n"));
|
|
1524
|
+
const value = getSecret(params.key, opts(params));
|
|
1525
|
+
if (value === null) return text(`Secret "${params.key}" not found`, true);
|
|
1526
|
+
const envelope = getEnvelope(params.key, opts(params));
|
|
1527
|
+
const provHint = params.provider ?? envelope?.envelope.meta.provider;
|
|
1528
|
+
const result = await validateSecret(value, { provider: provHint });
|
|
1529
|
+
return text(JSON.stringify(result, null, 2));
|
|
1428
1530
|
}
|
|
1429
1531
|
);
|
|
1430
1532
|
server2.tool(
|
|
1431
|
-
"
|
|
1432
|
-
"
|
|
1433
|
-
{
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
async (params) => {
|
|
1437
|
-
const toolBlock = enforceToolPolicy("check_project", params.projectPath);
|
|
1533
|
+
"list_providers",
|
|
1534
|
+
"[validation] List all available validation providers for secret liveness testing.",
|
|
1535
|
+
{},
|
|
1536
|
+
async () => {
|
|
1537
|
+
const toolBlock = enforceToolPolicy("list_providers");
|
|
1438
1538
|
if (toolBlock) return toolBlock;
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
let presentCount = 0;
|
|
1446
|
-
let missingCount = 0;
|
|
1447
|
-
let expiredCount = 0;
|
|
1448
|
-
let staleCount = 0;
|
|
1449
|
-
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1450
|
-
const result = getEnvelope(key, { projectPath, source: "mcp" });
|
|
1451
|
-
if (!result) {
|
|
1452
|
-
const status = manifest.required !== false ? "missing" : "optional_missing";
|
|
1453
|
-
if (manifest.required !== false) missingCount++;
|
|
1454
|
-
results.push({ key, status, required: manifest.required !== false, description: manifest.description });
|
|
1455
|
-
continue;
|
|
1456
|
-
}
|
|
1457
|
-
const decay = checkDecay(result.envelope);
|
|
1458
|
-
if (decay.isExpired) {
|
|
1459
|
-
expiredCount++;
|
|
1460
|
-
results.push({ key, status: "expired", timeRemaining: decay.timeRemaining, description: manifest.description });
|
|
1461
|
-
} else if (decay.isStale) {
|
|
1462
|
-
staleCount++;
|
|
1463
|
-
results.push({ key, status: "stale", lifetimePercent: decay.lifetimePercent, timeRemaining: decay.timeRemaining, description: manifest.description });
|
|
1464
|
-
} else {
|
|
1465
|
-
presentCount++;
|
|
1466
|
-
results.push({ key, status: "ok", description: manifest.description });
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
const summary = {
|
|
1470
|
-
total: Object.keys(config.secrets).length,
|
|
1471
|
-
present: presentCount,
|
|
1472
|
-
missing: missingCount,
|
|
1473
|
-
expired: expiredCount,
|
|
1474
|
-
stale: staleCount,
|
|
1475
|
-
ready: missingCount === 0 && expiredCount === 0,
|
|
1476
|
-
secrets: results
|
|
1477
|
-
};
|
|
1478
|
-
return text(JSON.stringify(summary, null, 2));
|
|
1539
|
+
const providers = registry2.listProviders().map((p) => ({
|
|
1540
|
+
name: p.name,
|
|
1541
|
+
description: p.description,
|
|
1542
|
+
prefixes: p.prefixes ?? []
|
|
1543
|
+
}));
|
|
1544
|
+
return text(JSON.stringify(providers, null, 2));
|
|
1479
1545
|
}
|
|
1480
1546
|
);
|
|
1481
1547
|
server2.tool(
|
|
1482
|
-
"
|
|
1483
|
-
"
|
|
1548
|
+
"rotate_secret",
|
|
1549
|
+
"[validation] Attempt issuer-native rotation of a secret via its detected or specified provider. Returns rotation result.",
|
|
1484
1550
|
{
|
|
1485
|
-
|
|
1486
|
-
|
|
1551
|
+
key: z7.string().describe("The secret key to rotate"),
|
|
1552
|
+
provider: z7.string().optional().describe("Force a specific provider"),
|
|
1553
|
+
scope: scope5,
|
|
1554
|
+
projectPath: projectPath5,
|
|
1555
|
+
teamId: teamId5,
|
|
1556
|
+
orgId: orgId5
|
|
1487
1557
|
},
|
|
1488
1558
|
async (params) => {
|
|
1489
|
-
const toolBlock = enforceToolPolicy("
|
|
1559
|
+
const toolBlock = enforceToolPolicy("rotate_secret", params.projectPath);
|
|
1490
1560
|
if (toolBlock) return toolBlock;
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
for (const [key, manifest] of Object.entries(config.secrets)) {
|
|
1499
|
-
const value = getSecret(key, {
|
|
1500
|
-
projectPath,
|
|
1501
|
-
env: params.env,
|
|
1561
|
+
const value = getSecret(params.key, opts(params));
|
|
1562
|
+
if (!value) return text(`Secret "${params.key}" not found`, true);
|
|
1563
|
+
const result = await rotateWithProvider(value, params.provider);
|
|
1564
|
+
if (result.rotated && result.newValue) {
|
|
1565
|
+
setSecret(params.key, result.newValue, {
|
|
1566
|
+
scope: params.scope ?? "global",
|
|
1567
|
+
projectPath: params.projectPath,
|
|
1502
1568
|
source: "mcp"
|
|
1503
1569
|
});
|
|
1504
|
-
if (value === null) {
|
|
1505
|
-
if (manifest.required !== false) {
|
|
1506
|
-
warnings.push(`MISSING (required): ${key}`);
|
|
1507
|
-
}
|
|
1508
|
-
lines.push(`# ${key}=`);
|
|
1509
|
-
continue;
|
|
1510
|
-
}
|
|
1511
|
-
const result2 = getEnvelope(key, { projectPath, source: "mcp" });
|
|
1512
|
-
if (result2) {
|
|
1513
|
-
const decay = checkDecay(result2.envelope);
|
|
1514
|
-
if (decay.isExpired) warnings.push(`EXPIRED: ${key}`);
|
|
1515
|
-
else if (decay.isStale) warnings.push(`STALE: ${key}`);
|
|
1516
|
-
}
|
|
1517
|
-
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1518
|
-
lines.push(`${key}="${escaped}"`);
|
|
1519
1570
|
}
|
|
1520
|
-
|
|
1521
|
-
const result = warnings.length > 0 ? `${output}
|
|
1522
|
-
|
|
1523
|
-
# Warnings:
|
|
1524
|
-
${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
1525
|
-
return text(result);
|
|
1571
|
+
return text(JSON.stringify(result, null, 2));
|
|
1526
1572
|
}
|
|
1527
1573
|
);
|
|
1528
1574
|
server2.tool(
|
|
1529
|
-
"
|
|
1530
|
-
"
|
|
1575
|
+
"ci_validate_secrets",
|
|
1576
|
+
"[validation] CI-oriented batch validation: validates all accessible secrets against their providers and returns a structured pass/fail report.",
|
|
1531
1577
|
{
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
orgId: orgIdSchema
|
|
1578
|
+
scope: scope5,
|
|
1579
|
+
projectPath: projectPath5,
|
|
1580
|
+
teamId: teamId5,
|
|
1581
|
+
orgId: orgId5
|
|
1537
1582
|
},
|
|
1538
1583
|
async (params) => {
|
|
1539
|
-
const toolBlock = enforceToolPolicy(
|
|
1584
|
+
const toolBlock = enforceToolPolicy(
|
|
1585
|
+
"ci_validate_secrets",
|
|
1586
|
+
params.projectPath
|
|
1587
|
+
);
|
|
1540
1588
|
if (toolBlock) return toolBlock;
|
|
1541
|
-
const
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
if (envelope.states) {
|
|
1555
|
-
info.environments = Object.keys(envelope.states);
|
|
1556
|
-
info.defaultEnv = envelope.defaultEnv;
|
|
1557
|
-
}
|
|
1558
|
-
if (decay.timeRemaining) {
|
|
1559
|
-
info.decay = {
|
|
1560
|
-
expired: decay.isExpired,
|
|
1561
|
-
stale: decay.isStale,
|
|
1562
|
-
lifetimePercent: decay.lifetimePercent,
|
|
1563
|
-
timeRemaining: decay.timeRemaining
|
|
1589
|
+
const entries = listSecrets(opts(params));
|
|
1590
|
+
const secrets = entries.map((e) => {
|
|
1591
|
+
const val = getSecret(e.key, {
|
|
1592
|
+
...opts(params),
|
|
1593
|
+
scope: e.scope,
|
|
1594
|
+
silent: true
|
|
1595
|
+
});
|
|
1596
|
+
if (!val) return null;
|
|
1597
|
+
return {
|
|
1598
|
+
key: e.key,
|
|
1599
|
+
value: val,
|
|
1600
|
+
provider: e.envelope?.meta.provider,
|
|
1601
|
+
validationUrl: e.envelope?.meta.validationUrl
|
|
1564
1602
|
};
|
|
1565
|
-
}
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
if (envelope.meta.description) info.description = envelope.meta.description;
|
|
1570
|
-
if (envelope.meta.tags?.length) info.tags = envelope.meta.tags;
|
|
1571
|
-
return text(JSON.stringify(info, null, 2));
|
|
1603
|
+
}).filter((s) => s !== null);
|
|
1604
|
+
if (secrets.length === 0) return text("No secrets to validate");
|
|
1605
|
+
const report = await ciValidateBatch(secrets);
|
|
1606
|
+
return text(JSON.stringify(report, null, 2));
|
|
1572
1607
|
}
|
|
1573
1608
|
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/mcp/tools/hooks.ts
|
|
1612
|
+
import { z as z8 } from "zod";
|
|
1613
|
+
function registerHookTools(server2) {
|
|
1574
1614
|
server2.tool(
|
|
1575
|
-
"
|
|
1576
|
-
"
|
|
1615
|
+
"register_hook",
|
|
1616
|
+
"[hooks] Register a webhook/callback that fires when a secret is updated, deleted, or rotated. Supports shell commands, HTTP webhooks, and process signals.",
|
|
1577
1617
|
{
|
|
1578
|
-
|
|
1618
|
+
type: z8.enum(["shell", "http", "signal"]).describe("Hook type"),
|
|
1619
|
+
key: z8.string().optional().describe("Trigger on exact key match"),
|
|
1620
|
+
keyPattern: z8.string().optional().describe("Trigger on key glob pattern (e.g. DB_*)"),
|
|
1621
|
+
tag: z8.string().optional().describe("Trigger on secrets with this tag"),
|
|
1622
|
+
scope: z8.enum(["global", "project"]).optional().describe("Trigger only for this scope"),
|
|
1623
|
+
actions: z8.array(z8.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe("Which actions trigger this hook"),
|
|
1624
|
+
command: z8.string().optional().describe("Shell command to execute (for shell type)"),
|
|
1625
|
+
url: z8.string().optional().describe("URL to POST to (for http type)"),
|
|
1626
|
+
signalTarget: z8.string().optional().describe("Process name or PID (for signal type)"),
|
|
1627
|
+
signalName: z8.string().optional().default("SIGHUP").describe("Signal to send (for signal type)"),
|
|
1628
|
+
description: z8.string().optional().describe("Human-readable description")
|
|
1579
1629
|
},
|
|
1580
1630
|
async (params) => {
|
|
1581
|
-
const toolBlock = enforceToolPolicy("
|
|
1631
|
+
const toolBlock = enforceToolPolicy("register_hook");
|
|
1582
1632
|
if (toolBlock) return toolBlock;
|
|
1583
|
-
|
|
1584
|
-
projectPath: params.projectPath ?? process.cwd()
|
|
1585
|
-
});
|
|
1586
|
-
if (!result) {
|
|
1633
|
+
if (!params.key && !params.keyPattern && !params.tag) {
|
|
1587
1634
|
return text(
|
|
1588
|
-
"
|
|
1635
|
+
"At least one match criterion required: key, keyPattern, or tag",
|
|
1636
|
+
true
|
|
1589
1637
|
);
|
|
1590
1638
|
}
|
|
1591
|
-
|
|
1639
|
+
const entry = registerHook({
|
|
1640
|
+
type: params.type,
|
|
1641
|
+
match: {
|
|
1642
|
+
key: params.key,
|
|
1643
|
+
keyPattern: params.keyPattern,
|
|
1644
|
+
tag: params.tag,
|
|
1645
|
+
scope: params.scope,
|
|
1646
|
+
action: params.actions
|
|
1647
|
+
},
|
|
1648
|
+
command: params.command,
|
|
1649
|
+
url: params.url,
|
|
1650
|
+
signal: params.signalTarget ? { target: params.signalTarget, signal: params.signalName } : void 0,
|
|
1651
|
+
description: params.description,
|
|
1652
|
+
enabled: true
|
|
1653
|
+
});
|
|
1654
|
+
return text(JSON.stringify(entry, null, 2));
|
|
1655
|
+
}
|
|
1656
|
+
);
|
|
1657
|
+
server2.tool(
|
|
1658
|
+
"list_hooks",
|
|
1659
|
+
"[hooks] List all registered secret change hooks with their match criteria, type, and status.",
|
|
1660
|
+
{},
|
|
1661
|
+
async () => {
|
|
1662
|
+
const toolBlock = enforceToolPolicy("list_hooks");
|
|
1663
|
+
if (toolBlock) return toolBlock;
|
|
1664
|
+
const hooks = listHooks();
|
|
1665
|
+
if (hooks.length === 0) return text("No hooks registered");
|
|
1666
|
+
return text(JSON.stringify(hooks, null, 2));
|
|
1667
|
+
}
|
|
1668
|
+
);
|
|
1669
|
+
server2.tool(
|
|
1670
|
+
"remove_hook",
|
|
1671
|
+
"[hooks] Remove a registered hook by ID.",
|
|
1672
|
+
{
|
|
1673
|
+
id: z8.string().describe("Hook ID to remove")
|
|
1674
|
+
},
|
|
1675
|
+
async (params) => {
|
|
1676
|
+
const toolBlock = enforceToolPolicy("remove_hook");
|
|
1677
|
+
if (toolBlock) return toolBlock;
|
|
1678
|
+
const removed = removeHook(params.id);
|
|
1679
|
+
return text(
|
|
1680
|
+
removed ? `Removed hook ${params.id}` : `Hook "${params.id}" not found`,
|
|
1681
|
+
!removed
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/mcp/tools/tooling.ts
|
|
1688
|
+
import { z as z9 } from "zod";
|
|
1689
|
+
|
|
1690
|
+
// src/utils/colors.ts
|
|
1691
|
+
var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
|
|
1692
|
+
|
|
1693
|
+
// src/core/agent.ts
|
|
1694
|
+
function defaultConfig() {
|
|
1695
|
+
return {
|
|
1696
|
+
intervalSeconds: 60,
|
|
1697
|
+
autoRotate: false,
|
|
1698
|
+
projectPaths: [process.cwd()],
|
|
1699
|
+
verbose: false
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
function runHealthScan(config = {}) {
|
|
1703
|
+
const cfg = { ...defaultConfig(), ...config };
|
|
1704
|
+
const report = {
|
|
1705
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1706
|
+
totalSecrets: 0,
|
|
1707
|
+
healthy: 0,
|
|
1708
|
+
stale: 0,
|
|
1709
|
+
expired: 0,
|
|
1710
|
+
anomalies: 0,
|
|
1711
|
+
rotated: [],
|
|
1712
|
+
warnings: []
|
|
1713
|
+
};
|
|
1714
|
+
const globalEntries = listSecrets({ scope: "global", source: "agent" });
|
|
1715
|
+
const projectEntries = cfg.projectPaths.flatMap(
|
|
1716
|
+
(pp) => listSecrets({ scope: "project", projectPath: pp, source: "agent" })
|
|
1717
|
+
);
|
|
1718
|
+
const allEntries = [...globalEntries, ...projectEntries];
|
|
1719
|
+
report.totalSecrets = allEntries.length;
|
|
1720
|
+
for (const entry of allEntries) {
|
|
1721
|
+
if (!entry.envelope) continue;
|
|
1722
|
+
const decay = checkDecay(entry.envelope);
|
|
1723
|
+
if (decay.isExpired) {
|
|
1724
|
+
report.expired++;
|
|
1725
|
+
report.warnings.push(
|
|
1726
|
+
`EXPIRED: ${entry.key} [${entry.scope}] \u2014 expired ${decay.timeRemaining}`
|
|
1727
|
+
);
|
|
1728
|
+
if (cfg.autoRotate) {
|
|
1729
|
+
const fmt = entry.envelope?.meta.rotationFormat ?? "api-key";
|
|
1730
|
+
const prefix = entry.envelope?.meta.rotationPrefix;
|
|
1731
|
+
const newValue = generateSecret({ format: fmt, prefix });
|
|
1732
|
+
setSecret(entry.key, newValue, {
|
|
1733
|
+
scope: entry.scope,
|
|
1734
|
+
projectPath: cfg.projectPaths[0],
|
|
1735
|
+
source: "agent"
|
|
1736
|
+
});
|
|
1737
|
+
report.rotated.push(entry.key);
|
|
1738
|
+
logAudit({
|
|
1739
|
+
action: "write",
|
|
1740
|
+
key: entry.key,
|
|
1741
|
+
scope: entry.scope,
|
|
1742
|
+
source: "agent",
|
|
1743
|
+
detail: "auto-rotated by agent (expired)"
|
|
1744
|
+
});
|
|
1745
|
+
fireHooks({
|
|
1746
|
+
action: "rotate",
|
|
1747
|
+
key: entry.key,
|
|
1748
|
+
scope: entry.scope,
|
|
1749
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1750
|
+
source: "agent"
|
|
1751
|
+
}, entry.envelope?.meta.tags).catch(() => {
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
} else if (decay.isStale) {
|
|
1755
|
+
report.stale++;
|
|
1756
|
+
report.warnings.push(
|
|
1757
|
+
`STALE: ${entry.key} [${entry.scope}] \u2014 ${decay.lifetimePercent}% lifetime, ${decay.timeRemaining} remaining`
|
|
1758
|
+
);
|
|
1759
|
+
} else {
|
|
1760
|
+
report.healthy++;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
const anomalies = detectAnomalies();
|
|
1764
|
+
report.anomalies = anomalies.length;
|
|
1765
|
+
for (const a of anomalies) {
|
|
1766
|
+
report.warnings.push(`ANOMALY [${a.type}]: ${a.description}`);
|
|
1767
|
+
}
|
|
1768
|
+
return report;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// src/core/exec.ts
|
|
1772
|
+
import { spawn } from "child_process";
|
|
1773
|
+
import { Transform } from "stream";
|
|
1774
|
+
var BUILTIN_PROFILES = {
|
|
1775
|
+
unrestricted: { name: "unrestricted" },
|
|
1776
|
+
restricted: {
|
|
1777
|
+
name: "restricted",
|
|
1778
|
+
denyCommands: ["curl", "wget", "ssh", "scp", "nc", "netcat", "ncat"],
|
|
1779
|
+
maxRuntimeSeconds: 30,
|
|
1780
|
+
allowNetwork: false,
|
|
1781
|
+
stripEnvVars: ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"]
|
|
1782
|
+
},
|
|
1783
|
+
ci: {
|
|
1784
|
+
name: "ci",
|
|
1785
|
+
maxRuntimeSeconds: 300,
|
|
1786
|
+
allowNetwork: true,
|
|
1787
|
+
denyCommands: ["rm -rf /", "mkfs", "dd if="]
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
function getProfile(name) {
|
|
1791
|
+
if (!name) return BUILTIN_PROFILES.unrestricted;
|
|
1792
|
+
return BUILTIN_PROFILES[name] ?? { name };
|
|
1793
|
+
}
|
|
1794
|
+
var RedactionTransform = class extends Transform {
|
|
1795
|
+
patterns = [];
|
|
1796
|
+
tail = "";
|
|
1797
|
+
maxLen = 0;
|
|
1798
|
+
constructor(secretsToRedact) {
|
|
1799
|
+
super();
|
|
1800
|
+
const validSecrets = secretsToRedact.filter((s) => s.length > 5);
|
|
1801
|
+
validSecrets.sort((a, b) => b.length - a.length);
|
|
1802
|
+
this.patterns = validSecrets.map((s) => ({
|
|
1803
|
+
value: s,
|
|
1804
|
+
replacement: "[QRING:REDACTED]"
|
|
1805
|
+
}));
|
|
1806
|
+
if (validSecrets.length > 0) {
|
|
1807
|
+
this.maxLen = validSecrets[0].length;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
_transform(chunk, _encoding, callback) {
|
|
1811
|
+
if (this.patterns.length === 0) {
|
|
1812
|
+
this.push(chunk);
|
|
1813
|
+
return callback();
|
|
1814
|
+
}
|
|
1815
|
+
const text2 = this.tail + chunk.toString();
|
|
1816
|
+
let redacted = text2;
|
|
1817
|
+
for (const { value, replacement } of this.patterns) {
|
|
1818
|
+
redacted = redacted.split(value).join(replacement);
|
|
1592
1819
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
const toolBlock = enforceToolPolicy("generate_secret", params.projectPath);
|
|
1609
|
-
if (toolBlock) return toolBlock;
|
|
1610
|
-
const secret = generateSecret({
|
|
1611
|
-
format: params.format,
|
|
1612
|
-
length: params.length,
|
|
1613
|
-
prefix: params.prefix
|
|
1614
|
-
});
|
|
1615
|
-
if (params.saveAs) {
|
|
1616
|
-
setSecret(params.saveAs, secret, {
|
|
1617
|
-
...opts(params),
|
|
1618
|
-
description: `Generated ${params.format} secret`
|
|
1619
|
-
});
|
|
1620
|
-
const entropy = estimateEntropy(secret);
|
|
1621
|
-
return text(
|
|
1622
|
-
`Generated and saved as "${params.saveAs}" (${params.format}, ~${entropy} bits entropy)`
|
|
1623
|
-
);
|
|
1820
|
+
if (redacted.length < this.maxLen) {
|
|
1821
|
+
this.tail = redacted;
|
|
1822
|
+
return callback();
|
|
1823
|
+
}
|
|
1824
|
+
const outputLen = redacted.length - this.maxLen + 1;
|
|
1825
|
+
const output = redacted.slice(0, outputLen);
|
|
1826
|
+
this.tail = redacted.slice(outputLen);
|
|
1827
|
+
this.push(output);
|
|
1828
|
+
callback();
|
|
1829
|
+
}
|
|
1830
|
+
_flush(callback) {
|
|
1831
|
+
if (this.tail) {
|
|
1832
|
+
let final = this.tail;
|
|
1833
|
+
for (const { value, replacement } of this.patterns) {
|
|
1834
|
+
final = final.split(value).join(replacement);
|
|
1624
1835
|
}
|
|
1625
|
-
|
|
1836
|
+
this.push(final);
|
|
1626
1837
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
{
|
|
1645
|
-
scope: params.sourceScope,
|
|
1646
|
-
projectPath: params.sourceProjectPath ?? process.cwd(),
|
|
1647
|
-
source: "mcp"
|
|
1648
|
-
},
|
|
1649
|
-
params.targetKey,
|
|
1650
|
-
{
|
|
1651
|
-
scope: params.targetScope,
|
|
1652
|
-
projectPath: params.targetProjectPath ?? process.cwd(),
|
|
1653
|
-
source: "mcp"
|
|
1654
|
-
}
|
|
1655
|
-
);
|
|
1656
|
-
return text(`Entangled: ${params.sourceKey} <-> ${params.targetKey}`);
|
|
1838
|
+
callback();
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1841
|
+
async function execCommand(opts2) {
|
|
1842
|
+
const profile = getProfile(opts2.profile);
|
|
1843
|
+
const fullCommand = [opts2.command, ...opts2.args].join(" ");
|
|
1844
|
+
const policyDecision = checkExecPolicy(fullCommand, opts2.projectPath);
|
|
1845
|
+
if (!policyDecision.allowed) {
|
|
1846
|
+
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
1847
|
+
}
|
|
1848
|
+
if (profile.denyCommands) {
|
|
1849
|
+
const denied = profile.denyCommands.find((d) => {
|
|
1850
|
+
const pattern = new RegExp(`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "i");
|
|
1851
|
+
return pattern.test(fullCommand);
|
|
1852
|
+
});
|
|
1853
|
+
if (denied) {
|
|
1854
|
+
throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
|
|
1657
1855
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
sourceKey: z.string().describe("Source secret key"),
|
|
1664
|
-
targetKey: z.string().describe("Target secret key"),
|
|
1665
|
-
sourceScope: scopeSchema.default("global"),
|
|
1666
|
-
targetScope: scopeSchema.default("global"),
|
|
1667
|
-
sourceProjectPath: z.string().optional(),
|
|
1668
|
-
targetProjectPath: z.string().optional()
|
|
1669
|
-
},
|
|
1670
|
-
async (params) => {
|
|
1671
|
-
const toolBlock = enforceToolPolicy("disentangle_secrets", params.sourceProjectPath);
|
|
1672
|
-
if (toolBlock) return toolBlock;
|
|
1673
|
-
disentangleSecrets(
|
|
1674
|
-
params.sourceKey,
|
|
1675
|
-
{
|
|
1676
|
-
scope: params.sourceScope,
|
|
1677
|
-
projectPath: params.sourceProjectPath ?? process.cwd(),
|
|
1678
|
-
source: "mcp"
|
|
1679
|
-
},
|
|
1680
|
-
params.targetKey,
|
|
1681
|
-
{
|
|
1682
|
-
scope: params.targetScope,
|
|
1683
|
-
projectPath: params.targetProjectPath ?? process.cwd(),
|
|
1684
|
-
source: "mcp"
|
|
1685
|
-
}
|
|
1686
|
-
);
|
|
1687
|
-
return text(`Disentangled: ${params.sourceKey} </> ${params.targetKey}`);
|
|
1856
|
+
}
|
|
1857
|
+
if (profile.allowCommands) {
|
|
1858
|
+
const allowed = profile.allowCommands.some((a) => fullCommand.startsWith(a));
|
|
1859
|
+
if (!allowed) {
|
|
1860
|
+
throw new Error(`Exec profile "${profile.name}" does not allow command "${opts2.command}"`);
|
|
1688
1861
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
},
|
|
1698
|
-
async (params) => {
|
|
1699
|
-
const toolBlock = enforceToolPolicy("tunnel_create");
|
|
1700
|
-
if (toolBlock) return toolBlock;
|
|
1701
|
-
const id = tunnelCreate(params.value, {
|
|
1702
|
-
ttlSeconds: params.ttlSeconds,
|
|
1703
|
-
maxReads: params.maxReads
|
|
1704
|
-
});
|
|
1705
|
-
return text(id);
|
|
1862
|
+
}
|
|
1863
|
+
const envMap = {};
|
|
1864
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1865
|
+
if (v !== void 0) envMap[k] = v;
|
|
1866
|
+
}
|
|
1867
|
+
if (profile.stripEnvVars) {
|
|
1868
|
+
for (const key of profile.stripEnvVars) {
|
|
1869
|
+
delete envMap[key];
|
|
1706
1870
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1871
|
+
}
|
|
1872
|
+
const secretsToRedact = /* @__PURE__ */ new Set();
|
|
1873
|
+
let entries = listSecrets({
|
|
1874
|
+
scope: opts2.scope,
|
|
1875
|
+
projectPath: opts2.projectPath,
|
|
1876
|
+
source: opts2.source ?? "cli",
|
|
1877
|
+
silent: true
|
|
1878
|
+
// list silently
|
|
1879
|
+
});
|
|
1880
|
+
if (opts2.keys?.length) {
|
|
1881
|
+
const keySet = new Set(opts2.keys);
|
|
1882
|
+
entries = entries.filter((e) => keySet.has(e.key));
|
|
1883
|
+
}
|
|
1884
|
+
if (opts2.tags?.length) {
|
|
1885
|
+
entries = entries.filter(
|
|
1886
|
+
(e) => opts2.tags.some((t) => e.envelope?.meta.tags?.includes(t))
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
for (const entry of entries) {
|
|
1890
|
+
if (entry.envelope) {
|
|
1891
|
+
const decay = checkDecay(entry.envelope);
|
|
1892
|
+
if (decay.isExpired) continue;
|
|
1893
|
+
}
|
|
1894
|
+
const val = getSecret(entry.key, {
|
|
1895
|
+
scope: entry.scope,
|
|
1896
|
+
projectPath: opts2.projectPath,
|
|
1897
|
+
env: opts2.env,
|
|
1898
|
+
source: opts2.source ?? "cli",
|
|
1899
|
+
silent: false
|
|
1900
|
+
// Log access for execution
|
|
1901
|
+
});
|
|
1902
|
+
if (val !== null) {
|
|
1903
|
+
envMap[entry.key] = val;
|
|
1904
|
+
if (val.length > 5) {
|
|
1905
|
+
secretsToRedact.add(val);
|
|
1720
1906
|
}
|
|
1721
|
-
return text(value);
|
|
1722
1907
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
return
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1908
|
+
}
|
|
1909
|
+
const maxRuntime = profile.maxRuntimeSeconds ?? getExecMaxRuntime(opts2.projectPath);
|
|
1910
|
+
return new Promise((resolve, reject) => {
|
|
1911
|
+
const networkTools = /* @__PURE__ */ new Set([
|
|
1912
|
+
"curl",
|
|
1913
|
+
"wget",
|
|
1914
|
+
"ping",
|
|
1915
|
+
"nc",
|
|
1916
|
+
"netcat",
|
|
1917
|
+
"ssh",
|
|
1918
|
+
"telnet",
|
|
1919
|
+
"ftp",
|
|
1920
|
+
"dig",
|
|
1921
|
+
"nslookup"
|
|
1922
|
+
]);
|
|
1923
|
+
if (profile.allowNetwork === false && networkTools.has(opts2.command)) {
|
|
1924
|
+
const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts2.command}" is considered network-related`;
|
|
1925
|
+
if (opts2.captureOutput) {
|
|
1926
|
+
return resolve({ code: 126, stdout: "", stderr: msg });
|
|
1927
|
+
}
|
|
1928
|
+
process.stderr.write(msg + "\n");
|
|
1929
|
+
return resolve({ code: 126, stdout: "", stderr: "" });
|
|
1744
1930
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
destroyed ? `Destroyed ${params.id}` : `Tunnel "${params.id}" not found`,
|
|
1758
|
-
!destroyed
|
|
1759
|
-
);
|
|
1931
|
+
const child = spawn(opts2.command, opts2.args, {
|
|
1932
|
+
env: envMap,
|
|
1933
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1934
|
+
shell: false
|
|
1935
|
+
});
|
|
1936
|
+
let timedOut = false;
|
|
1937
|
+
let timer;
|
|
1938
|
+
if (maxRuntime) {
|
|
1939
|
+
timer = setTimeout(() => {
|
|
1940
|
+
timedOut = true;
|
|
1941
|
+
child.kill("SIGKILL");
|
|
1942
|
+
}, maxRuntime * 1e3);
|
|
1760
1943
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
if (
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
const value = getSecret(entry.key, { ...o, scope: entry.scope });
|
|
1782
|
-
if (value !== null) {
|
|
1783
|
-
secrets.push({ key: entry.key, value, scope: entry.scope });
|
|
1784
|
-
}
|
|
1944
|
+
const stdoutRedact = new RedactionTransform([...secretsToRedact]);
|
|
1945
|
+
const stderrRedact = new RedactionTransform([...secretsToRedact]);
|
|
1946
|
+
if (child.stdout) child.stdout.pipe(stdoutRedact);
|
|
1947
|
+
if (child.stderr) child.stderr.pipe(stderrRedact);
|
|
1948
|
+
let stdoutStr = "";
|
|
1949
|
+
let stderrStr = "";
|
|
1950
|
+
if (opts2.captureOutput) {
|
|
1951
|
+
stdoutRedact.on("data", (d) => stdoutStr += d.toString());
|
|
1952
|
+
stderrRedact.on("data", (d) => stderrStr += d.toString());
|
|
1953
|
+
} else {
|
|
1954
|
+
stdoutRedact.pipe(process.stdout);
|
|
1955
|
+
stderrRedact.pipe(process.stderr);
|
|
1956
|
+
}
|
|
1957
|
+
child.on("close", (code) => {
|
|
1958
|
+
if (timer) clearTimeout(timer);
|
|
1959
|
+
if (timedOut) {
|
|
1960
|
+
resolve({ code: 124, stdout: stdoutStr, stderr: stderrStr + `
|
|
1961
|
+
[QRING] Process killed: exceeded ${maxRuntime}s runtime limit` });
|
|
1962
|
+
} else {
|
|
1963
|
+
resolve({ code: code ?? 0, stdout: stdoutStr, stderr: stderrStr });
|
|
1785
1964
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1965
|
+
});
|
|
1966
|
+
child.on("error", (err) => {
|
|
1967
|
+
if (timer) clearTimeout(timer);
|
|
1968
|
+
reject(err);
|
|
1969
|
+
});
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// src/core/scan.ts
|
|
1974
|
+
import { readFileSync as readFileSync2, readdirSync, statSync } from "fs";
|
|
1975
|
+
import { join } from "path";
|
|
1976
|
+
|
|
1977
|
+
// src/core/secrets-detect.ts
|
|
1978
|
+
var SECRET_ASSIGNMENT_PATTERN = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
|
|
1979
|
+
function calculateEntropy(str) {
|
|
1980
|
+
if (!str) return 0;
|
|
1981
|
+
const len = str.length;
|
|
1982
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
1983
|
+
for (let i = 0; i < len; i++) {
|
|
1984
|
+
const char = str[i];
|
|
1985
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
1986
|
+
}
|
|
1987
|
+
let entropy = 0;
|
|
1988
|
+
for (const count of frequencies.values()) {
|
|
1989
|
+
const p = count / len;
|
|
1990
|
+
entropy -= p * Math.log2(p);
|
|
1991
|
+
}
|
|
1992
|
+
return entropy;
|
|
1993
|
+
}
|
|
1994
|
+
function isPlaceholderValue(value) {
|
|
1995
|
+
const lv = value.toLowerCase();
|
|
1996
|
+
return lv.includes("example") || lv.includes("your_") || lv.includes("placeholder") || lv.includes("replace_me") || lv.includes("xxx");
|
|
1997
|
+
}
|
|
1998
|
+
function passesSecretHeuristic(value, entropy) {
|
|
1999
|
+
return entropy > 3.5 || value.startsWith("sk-") || value.startsWith("ghp_");
|
|
2000
|
+
}
|
|
2001
|
+
function findSecretsInLine(line) {
|
|
2002
|
+
if (line.length > 500) return [];
|
|
2003
|
+
const out = [];
|
|
2004
|
+
SECRET_ASSIGNMENT_PATTERN.lastIndex = 0;
|
|
2005
|
+
let match;
|
|
2006
|
+
while ((match = SECRET_ASSIGNMENT_PATTERN.exec(line)) !== null) {
|
|
2007
|
+
const varName = match[1];
|
|
2008
|
+
const quote = match[2];
|
|
2009
|
+
const value = match[3];
|
|
2010
|
+
if (value.length < 8) continue;
|
|
2011
|
+
if (isPlaceholderValue(value)) continue;
|
|
2012
|
+
const entropy = calculateEntropy(value);
|
|
2013
|
+
if (!passesSecretHeuristic(value, entropy)) continue;
|
|
2014
|
+
out.push({ varName, value, quote });
|
|
2015
|
+
}
|
|
2016
|
+
return out;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/core/scan.ts
|
|
2020
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
2021
|
+
"node_modules",
|
|
2022
|
+
".git",
|
|
2023
|
+
".next",
|
|
2024
|
+
"dist",
|
|
2025
|
+
"build",
|
|
2026
|
+
"coverage",
|
|
2027
|
+
".cursor",
|
|
2028
|
+
"venv",
|
|
2029
|
+
"__pycache__"
|
|
2030
|
+
]);
|
|
2031
|
+
var IGNORE_EXTS = /* @__PURE__ */ new Set([
|
|
2032
|
+
".png",
|
|
2033
|
+
".jpg",
|
|
2034
|
+
".jpeg",
|
|
2035
|
+
".gif",
|
|
2036
|
+
".ico",
|
|
2037
|
+
".svg",
|
|
2038
|
+
".webp",
|
|
2039
|
+
".mp4",
|
|
2040
|
+
".mp3",
|
|
2041
|
+
".wav",
|
|
2042
|
+
".ogg",
|
|
2043
|
+
".pdf",
|
|
2044
|
+
".zip",
|
|
2045
|
+
".tar",
|
|
2046
|
+
".gz",
|
|
2047
|
+
".xz",
|
|
2048
|
+
".ttf",
|
|
2049
|
+
".woff",
|
|
2050
|
+
".woff2",
|
|
2051
|
+
".eot",
|
|
2052
|
+
".exe",
|
|
2053
|
+
".dll",
|
|
2054
|
+
".so",
|
|
2055
|
+
".dylib",
|
|
2056
|
+
".lock"
|
|
2057
|
+
]);
|
|
2058
|
+
function scanCodebase(dir) {
|
|
2059
|
+
const results = [];
|
|
2060
|
+
function walk(currentDir) {
|
|
2061
|
+
let entries;
|
|
2062
|
+
try {
|
|
2063
|
+
entries = readdirSync(currentDir);
|
|
2064
|
+
} catch {
|
|
2065
|
+
return;
|
|
1789
2066
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
{
|
|
1795
|
-
bundle: z.string().describe("Base64-encoded encrypted bundle"),
|
|
1796
|
-
passphrase: z.string().describe("Decryption passphrase"),
|
|
1797
|
-
scope: scopeSchema.default("global"),
|
|
1798
|
-
projectPath: projectPathSchema,
|
|
1799
|
-
teamId: teamIdSchema,
|
|
1800
|
-
orgId: orgIdSchema,
|
|
1801
|
-
dryRun: z.boolean().optional().default(false).describe("Preview without importing")
|
|
1802
|
-
},
|
|
1803
|
-
async (params) => {
|
|
1804
|
-
const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
|
|
1805
|
-
if (toolBlock) return toolBlock;
|
|
2067
|
+
for (const entry of entries) {
|
|
2068
|
+
if (IGNORE_DIRS.has(entry)) continue;
|
|
2069
|
+
const fullPath = join(currentDir, entry);
|
|
2070
|
+
let stat;
|
|
1806
2071
|
try {
|
|
1807
|
-
|
|
1808
|
-
if (params.dryRun) {
|
|
1809
|
-
const preview = payload.secrets.map((s) => `${s.key} [${s.scope ?? "global"}]`).join("\n");
|
|
1810
|
-
return text(`Would import ${payload.secrets.length} secrets:
|
|
1811
|
-
${preview}`);
|
|
1812
|
-
}
|
|
1813
|
-
const o = opts(params);
|
|
1814
|
-
for (const s of payload.secrets) {
|
|
1815
|
-
setSecret(s.key, s.value, o);
|
|
1816
|
-
}
|
|
1817
|
-
return text(`Imported ${payload.secrets.length} secret(s) from teleport bundle`);
|
|
2072
|
+
stat = statSync(fullPath);
|
|
1818
2073
|
} catch {
|
|
1819
|
-
|
|
2074
|
+
continue;
|
|
1820
2075
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
},
|
|
1831
|
-
async (params) => {
|
|
1832
|
-
const toolBlock = enforceToolPolicy("audit_log");
|
|
1833
|
-
if (toolBlock) return toolBlock;
|
|
1834
|
-
const events = queryAudit({
|
|
1835
|
-
key: params.key,
|
|
1836
|
-
action: params.action,
|
|
1837
|
-
limit: params.limit
|
|
1838
|
-
});
|
|
1839
|
-
if (events.length === 0) return text("No audit events found");
|
|
1840
|
-
const lines = events.map((e) => {
|
|
1841
|
-
const parts = [e.timestamp, e.action];
|
|
1842
|
-
if (e.key) parts.push(e.key);
|
|
1843
|
-
if (e.scope) parts.push(`[${e.scope}]`);
|
|
1844
|
-
if (e.env) parts.push(`env:${e.env}`);
|
|
1845
|
-
if (e.detail) parts.push(e.detail);
|
|
1846
|
-
return parts.join(" | ");
|
|
1847
|
-
});
|
|
1848
|
-
return text(lines.join("\n"));
|
|
1849
|
-
}
|
|
1850
|
-
);
|
|
1851
|
-
server2.tool(
|
|
1852
|
-
"detect_anomalies",
|
|
1853
|
-
"Scan for anomalous secret access patterns: burst reads, unusual-hour access. Returns findings and recommendations.",
|
|
1854
|
-
{
|
|
1855
|
-
key: z.string().optional().describe("Check anomalies for a specific key")
|
|
1856
|
-
},
|
|
1857
|
-
async (params) => {
|
|
1858
|
-
const toolBlock = enforceToolPolicy("detect_anomalies");
|
|
1859
|
-
if (toolBlock) return toolBlock;
|
|
1860
|
-
const anomalies = detectAnomalies(params.key);
|
|
1861
|
-
if (anomalies.length === 0) return text("No anomalies detected");
|
|
1862
|
-
const lines = anomalies.map(
|
|
1863
|
-
(a) => `[${a.type}] ${a.description}`
|
|
1864
|
-
);
|
|
1865
|
-
return text(lines.join("\n"));
|
|
1866
|
-
}
|
|
1867
|
-
);
|
|
1868
|
-
server2.tool(
|
|
1869
|
-
"health_check",
|
|
1870
|
-
"Run a comprehensive health check on all secrets: decay status, staleness, anomalies, entropy assessment.",
|
|
1871
|
-
{
|
|
1872
|
-
scope: scopeSchema,
|
|
1873
|
-
projectPath: projectPathSchema,
|
|
1874
|
-
teamId: teamIdSchema,
|
|
1875
|
-
orgId: orgIdSchema
|
|
1876
|
-
},
|
|
1877
|
-
async (params) => {
|
|
1878
|
-
const toolBlock = enforceToolPolicy("health_check", params.projectPath);
|
|
1879
|
-
if (toolBlock) return toolBlock;
|
|
1880
|
-
const entries = listSecrets(opts(params));
|
|
1881
|
-
const anomalies = detectAnomalies();
|
|
1882
|
-
let healthy = 0;
|
|
1883
|
-
let stale = 0;
|
|
1884
|
-
let expired = 0;
|
|
1885
|
-
let noDecay = 0;
|
|
1886
|
-
const issues = [];
|
|
1887
|
-
for (const entry of entries) {
|
|
1888
|
-
if (!entry.decay?.timeRemaining) {
|
|
1889
|
-
noDecay++;
|
|
2076
|
+
if (stat.isDirectory()) {
|
|
2077
|
+
walk(fullPath);
|
|
2078
|
+
} else if (stat.isFile()) {
|
|
2079
|
+
const ext = fullPath.slice(fullPath.lastIndexOf(".")).toLowerCase();
|
|
2080
|
+
if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
|
|
2081
|
+
let content;
|
|
2082
|
+
try {
|
|
2083
|
+
content = readFileSync2(fullPath, "utf8");
|
|
2084
|
+
} catch {
|
|
1890
2085
|
continue;
|
|
1891
2086
|
}
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2087
|
+
if (content.includes("\0")) continue;
|
|
2088
|
+
const lines = content.split(/\r?\n/);
|
|
2089
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2090
|
+
const line = lines[i];
|
|
2091
|
+
const matches = findSecretsInLine(line);
|
|
2092
|
+
for (const m of matches) {
|
|
2093
|
+
const entropy = calculateEntropy(m.value);
|
|
2094
|
+
const relPath = fullPath.startsWith(dir) ? fullPath.slice(dir.length).replace(/^[/\\]+/, "") : fullPath;
|
|
2095
|
+
results.push({
|
|
2096
|
+
file: relPath || fullPath,
|
|
2097
|
+
line: i + 1,
|
|
2098
|
+
keyName: m.varName,
|
|
2099
|
+
match: m.value,
|
|
2100
|
+
context: line.trim(),
|
|
2101
|
+
entropy: parseFloat(entropy.toFixed(2))
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
1902
2104
|
}
|
|
1903
2105
|
}
|
|
1904
|
-
const summary = [
|
|
1905
|
-
`Secrets: ${entries.length} total`,
|
|
1906
|
-
`Healthy: ${healthy} | Stale: ${stale} | Expired: ${expired} | No decay: ${noDecay}`,
|
|
1907
|
-
`Anomalies: ${anomalies.length}`
|
|
1908
|
-
];
|
|
1909
|
-
if (issues.length > 0) {
|
|
1910
|
-
summary.push("", "Issues:", ...issues);
|
|
1911
|
-
}
|
|
1912
|
-
if (anomalies.length > 0) {
|
|
1913
|
-
summary.push(
|
|
1914
|
-
"",
|
|
1915
|
-
"Anomalies:",
|
|
1916
|
-
...anomalies.map((a) => `[${a.type}] ${a.description}`)
|
|
1917
|
-
);
|
|
1918
|
-
}
|
|
1919
|
-
return text(summary.join("\n"));
|
|
1920
|
-
}
|
|
1921
|
-
);
|
|
1922
|
-
server2.tool(
|
|
1923
|
-
"validate_secret",
|
|
1924
|
-
"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.",
|
|
1925
|
-
{
|
|
1926
|
-
key: z.string().describe("The secret key name"),
|
|
1927
|
-
provider: z.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
|
|
1928
|
-
scope: scopeSchema,
|
|
1929
|
-
projectPath: projectPathSchema,
|
|
1930
|
-
teamId: teamIdSchema,
|
|
1931
|
-
orgId: orgIdSchema
|
|
1932
|
-
},
|
|
1933
|
-
async (params) => {
|
|
1934
|
-
const toolBlock = enforceToolPolicy("validate_secret", params.projectPath);
|
|
1935
|
-
if (toolBlock) return toolBlock;
|
|
1936
|
-
const value = getSecret(params.key, opts(params));
|
|
1937
|
-
if (value === null) return text(`Secret "${params.key}" not found`, true);
|
|
1938
|
-
const envelope = getEnvelope(params.key, opts(params));
|
|
1939
|
-
const provHint = params.provider ?? envelope?.envelope.meta.provider;
|
|
1940
|
-
const result = await validateSecret(value, { provider: provHint });
|
|
1941
|
-
return text(JSON.stringify(result, null, 2));
|
|
1942
2106
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2107
|
+
}
|
|
2108
|
+
walk(dir);
|
|
2109
|
+
return results;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/core/linter.ts
|
|
2113
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync } from "fs";
|
|
2114
|
+
import { basename, extname } from "path";
|
|
2115
|
+
var ENV_REF_BY_EXT = {
|
|
2116
|
+
".ts": (k) => `process.env.${k}`,
|
|
2117
|
+
".tsx": (k) => `process.env.${k}`,
|
|
2118
|
+
".js": (k) => `process.env.${k}`,
|
|
2119
|
+
".jsx": (k) => `process.env.${k}`,
|
|
2120
|
+
".mjs": (k) => `process.env.${k}`,
|
|
2121
|
+
".cjs": (k) => `process.env.${k}`,
|
|
2122
|
+
".py": (k) => `os.environ["${k}"]`,
|
|
2123
|
+
".rb": (k) => `ENV["${k}"]`,
|
|
2124
|
+
".go": (k) => `os.Getenv("${k}")`,
|
|
2125
|
+
".rs": (k) => `std::env::var("${k}")`,
|
|
2126
|
+
".java": (k) => `System.getenv("${k}")`,
|
|
2127
|
+
".kt": (k) => `System.getenv("${k}")`,
|
|
2128
|
+
".cs": (k) => `Environment.GetEnvironmentVariable("${k}")`,
|
|
2129
|
+
".php": (k) => `getenv('${k}')`,
|
|
2130
|
+
".sh": (k) => `\${${k}}`,
|
|
2131
|
+
".bash": (k) => `\${${k}}`
|
|
2132
|
+
};
|
|
2133
|
+
function getEnvRef(filePath, keyName) {
|
|
2134
|
+
const ext = extname(filePath).toLowerCase();
|
|
2135
|
+
const formatter = ENV_REF_BY_EXT[ext];
|
|
2136
|
+
return formatter ? formatter(keyName) : `process.env.${keyName}`;
|
|
2137
|
+
}
|
|
2138
|
+
function lintFiles(files, opts2 = {}) {
|
|
2139
|
+
const results = [];
|
|
2140
|
+
for (const file of files) {
|
|
2141
|
+
if (!existsSync(file)) continue;
|
|
2142
|
+
let content;
|
|
2143
|
+
try {
|
|
2144
|
+
content = readFileSync3(file, "utf8");
|
|
2145
|
+
} catch {
|
|
2146
|
+
continue;
|
|
1957
2147
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2148
|
+
if (content.includes("\0")) continue;
|
|
2149
|
+
const lines = content.split(/\r?\n/);
|
|
2150
|
+
const fixes = [];
|
|
2151
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2152
|
+
const line = lines[i];
|
|
2153
|
+
const matches = findSecretsInLine(line);
|
|
2154
|
+
for (const m of matches) {
|
|
2155
|
+
const varNameUpper = m.varName.toUpperCase();
|
|
2156
|
+
const entropy = calculateEntropy(m.value);
|
|
2157
|
+
const shouldFix = opts2.fix === true;
|
|
2158
|
+
if (shouldFix) {
|
|
2159
|
+
const envRef = getEnvRef(file, varNameUpper);
|
|
2160
|
+
fixes.push({
|
|
2161
|
+
line: i,
|
|
2162
|
+
original: `${m.quote}${m.value}${m.quote}`,
|
|
2163
|
+
replacement: envRef,
|
|
2164
|
+
keyName: varNameUpper,
|
|
2165
|
+
value: m.value
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
results.push({
|
|
2169
|
+
file,
|
|
2170
|
+
line: i + 1,
|
|
2171
|
+
keyName: varNameUpper,
|
|
2172
|
+
match: m.value,
|
|
2173
|
+
context: line.trim(),
|
|
2174
|
+
entropy: parseFloat(entropy.toFixed(2)),
|
|
2175
|
+
fixed: shouldFix
|
|
2176
|
+
});
|
|
1980
2177
|
}
|
|
1981
|
-
const entry = registerHook({
|
|
1982
|
-
type: params.type,
|
|
1983
|
-
match: {
|
|
1984
|
-
key: params.key,
|
|
1985
|
-
keyPattern: params.keyPattern,
|
|
1986
|
-
tag: params.tag,
|
|
1987
|
-
scope: params.scope,
|
|
1988
|
-
action: params.actions
|
|
1989
|
-
},
|
|
1990
|
-
command: params.command,
|
|
1991
|
-
url: params.url,
|
|
1992
|
-
signal: params.signalTarget ? { target: params.signalTarget, signal: params.signalName } : void 0,
|
|
1993
|
-
description: params.description,
|
|
1994
|
-
enabled: true
|
|
1995
|
-
});
|
|
1996
|
-
return text(JSON.stringify(entry, null, 2));
|
|
1997
|
-
}
|
|
1998
|
-
);
|
|
1999
|
-
server2.tool(
|
|
2000
|
-
"list_hooks",
|
|
2001
|
-
"List all registered secret change hooks with their match criteria, type, and status.",
|
|
2002
|
-
{},
|
|
2003
|
-
async () => {
|
|
2004
|
-
const toolBlock = enforceToolPolicy("list_hooks");
|
|
2005
|
-
if (toolBlock) return toolBlock;
|
|
2006
|
-
const hooks = listHooks();
|
|
2007
|
-
if (hooks.length === 0) return text("No hooks registered");
|
|
2008
|
-
return text(JSON.stringify(hooks, null, 2));
|
|
2009
2178
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2179
|
+
if (opts2.fix && fixes.length > 0) {
|
|
2180
|
+
const fixLines = content.split(/\r?\n/);
|
|
2181
|
+
for (const fix of fixes.reverse()) {
|
|
2182
|
+
const lineIdx = fix.line;
|
|
2183
|
+
if (lineIdx >= 0 && lineIdx < fixLines.length) {
|
|
2184
|
+
fixLines[lineIdx] = fixLines[lineIdx].replace(fix.original, fix.replacement);
|
|
2185
|
+
}
|
|
2186
|
+
if (!hasSecret(fix.keyName, { scope: opts2.scope, projectPath: opts2.projectPath })) {
|
|
2187
|
+
setSecret(fix.keyName, fix.value, {
|
|
2188
|
+
scope: opts2.scope ?? "global",
|
|
2189
|
+
projectPath: opts2.projectPath,
|
|
2190
|
+
source: "cli",
|
|
2191
|
+
description: `Auto-imported from ${basename(file)}:${fix.line + 1}`
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
writeFileSync(file, fixLines.join("\n"), "utf8");
|
|
2025
2196
|
}
|
|
2026
|
-
|
|
2197
|
+
}
|
|
2198
|
+
return results;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/mcp/tools/tooling.ts
|
|
2202
|
+
var { teamId: teamId6, orgId: orgId6, scope: scope6, projectPath: projectPath6 } = commonSchemas;
|
|
2203
|
+
function registerToolingTools(server2) {
|
|
2027
2204
|
server2.tool(
|
|
2028
2205
|
"exec_with_secrets",
|
|
2029
|
-
"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.",
|
|
2206
|
+
"[exec] 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.",
|
|
2030
2207
|
{
|
|
2031
|
-
command:
|
|
2032
|
-
args:
|
|
2033
|
-
keys:
|
|
2034
|
-
tags:
|
|
2035
|
-
profile:
|
|
2036
|
-
scope:
|
|
2037
|
-
projectPath:
|
|
2038
|
-
teamId:
|
|
2039
|
-
orgId:
|
|
2208
|
+
command: z9.string().describe("Command to run"),
|
|
2209
|
+
args: z9.array(z9.string()).optional().describe("Command arguments"),
|
|
2210
|
+
keys: z9.array(z9.string()).optional().describe("Only inject these specific keys"),
|
|
2211
|
+
tags: z9.array(z9.string()).optional().describe("Only inject secrets with these tags"),
|
|
2212
|
+
profile: z9.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe("Exec profile: unrestricted, restricted, or ci"),
|
|
2213
|
+
scope: scope6,
|
|
2214
|
+
projectPath: projectPath6,
|
|
2215
|
+
teamId: teamId6,
|
|
2216
|
+
orgId: orgId6
|
|
2040
2217
|
},
|
|
2041
2218
|
async (params) => {
|
|
2042
2219
|
const toolBlock = enforceToolPolicy("exec_with_secrets", params.projectPath);
|
|
@@ -2065,15 +2242,18 @@ ${result.stdout}`);
|
|
|
2065
2242
|
${result.stderr}`);
|
|
2066
2243
|
return text(output.join("\n\n"));
|
|
2067
2244
|
} catch (err) {
|
|
2068
|
-
return text(
|
|
2245
|
+
return text(
|
|
2246
|
+
`Execution failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2247
|
+
true
|
|
2248
|
+
);
|
|
2069
2249
|
}
|
|
2070
2250
|
}
|
|
2071
2251
|
);
|
|
2072
2252
|
server2.tool(
|
|
2073
2253
|
"scan_codebase_for_secrets",
|
|
2074
|
-
"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.",
|
|
2254
|
+
"[scan] 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.",
|
|
2075
2255
|
{
|
|
2076
|
-
dirPath:
|
|
2256
|
+
dirPath: z9.string().describe("Absolute or relative path to the directory to scan")
|
|
2077
2257
|
},
|
|
2078
2258
|
async (params) => {
|
|
2079
2259
|
const toolBlock = enforceToolPolicy("scan_codebase_for_secrets");
|
|
@@ -2085,82 +2265,23 @@ ${result.stderr}`);
|
|
|
2085
2265
|
}
|
|
2086
2266
|
return text(JSON.stringify(results, null, 2));
|
|
2087
2267
|
} catch (err) {
|
|
2088
|
-
return text(
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
server2.tool(
|
|
2093
|
-
"get_project_context",
|
|
2094
|
-
"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.",
|
|
2095
|
-
{
|
|
2096
|
-
scope: scopeSchema,
|
|
2097
|
-
projectPath: projectPathSchema,
|
|
2098
|
-
teamId: teamIdSchema,
|
|
2099
|
-
orgId: orgIdSchema
|
|
2100
|
-
},
|
|
2101
|
-
async (params) => {
|
|
2102
|
-
const toolBlock = enforceToolPolicy("get_project_context", params.projectPath);
|
|
2103
|
-
if (toolBlock) return toolBlock;
|
|
2104
|
-
const context = getProjectContext(opts(params));
|
|
2105
|
-
return text(JSON.stringify(context, null, 2));
|
|
2106
|
-
}
|
|
2107
|
-
);
|
|
2108
|
-
server2.tool(
|
|
2109
|
-
"agent_remember",
|
|
2110
|
-
"Store a key-value pair in encrypted agent memory that persists across sessions. Use this to remember decisions, rotation history, or project-specific context.",
|
|
2111
|
-
{
|
|
2112
|
-
key: z.string().describe("Memory key"),
|
|
2113
|
-
value: z.string().describe("Value to store")
|
|
2114
|
-
},
|
|
2115
|
-
async (params) => {
|
|
2116
|
-
const toolBlock = enforceToolPolicy("agent_remember");
|
|
2117
|
-
if (toolBlock) return toolBlock;
|
|
2118
|
-
remember(params.key, params.value);
|
|
2119
|
-
return text(`Remembered "${params.key}"`);
|
|
2120
|
-
}
|
|
2121
|
-
);
|
|
2122
|
-
server2.tool(
|
|
2123
|
-
"agent_recall",
|
|
2124
|
-
"Retrieve a value from agent memory, or list all stored keys if no key is provided.",
|
|
2125
|
-
{
|
|
2126
|
-
key: z.string().optional().describe("Memory key to recall (omit to list all)")
|
|
2127
|
-
},
|
|
2128
|
-
async (params) => {
|
|
2129
|
-
const toolBlock = enforceToolPolicy("agent_recall");
|
|
2130
|
-
if (toolBlock) return toolBlock;
|
|
2131
|
-
if (!params.key) {
|
|
2132
|
-
const entries = listMemory();
|
|
2133
|
-
if (entries.length === 0) return text("Agent memory is empty");
|
|
2134
|
-
return text(JSON.stringify(entries, null, 2));
|
|
2268
|
+
return text(
|
|
2269
|
+
`Scan failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2270
|
+
true
|
|
2271
|
+
);
|
|
2135
2272
|
}
|
|
2136
|
-
const value = recall(params.key);
|
|
2137
|
-
if (value === null) return text(`No memory found for "${params.key}"`, true);
|
|
2138
|
-
return text(value);
|
|
2139
|
-
}
|
|
2140
|
-
);
|
|
2141
|
-
server2.tool(
|
|
2142
|
-
"agent_forget",
|
|
2143
|
-
"Delete a key from agent memory.",
|
|
2144
|
-
{
|
|
2145
|
-
key: z.string().describe("Memory key to forget")
|
|
2146
|
-
},
|
|
2147
|
-
async (params) => {
|
|
2148
|
-
const toolBlock = enforceToolPolicy("agent_forget");
|
|
2149
|
-
if (toolBlock) return toolBlock;
|
|
2150
|
-
const removed = forget(params.key);
|
|
2151
|
-
return text(removed ? `Forgot "${params.key}"` : `No memory found for "${params.key}"`, !removed);
|
|
2152
2273
|
}
|
|
2153
2274
|
);
|
|
2154
2275
|
server2.tool(
|
|
2155
2276
|
"lint_files",
|
|
2156
|
-
"Scan specific files for hardcoded secrets. Optionally auto-fix by replacing them with process.env references and storing the values in q-ring.",
|
|
2277
|
+
"[scan] Scan specific files for hardcoded secrets. Optionally auto-fix by replacing them with process.env references and storing the values in q-ring.",
|
|
2157
2278
|
{
|
|
2158
|
-
files:
|
|
2159
|
-
fix:
|
|
2160
|
-
scope:
|
|
2161
|
-
projectPath:
|
|
2162
|
-
teamId:
|
|
2163
|
-
orgId:
|
|
2279
|
+
files: z9.array(z9.string()).describe("File paths to lint"),
|
|
2280
|
+
fix: z9.boolean().optional().default(false).describe("Auto-replace and store secrets"),
|
|
2281
|
+
scope: scope6,
|
|
2282
|
+
projectPath: projectPath6,
|
|
2283
|
+
teamId: teamId6,
|
|
2284
|
+
orgId: orgId6
|
|
2164
2285
|
},
|
|
2165
2286
|
async (params) => {
|
|
2166
2287
|
const toolBlock = enforceToolPolicy("lint_files", params.projectPath);
|
|
@@ -2176,18 +2297,21 @@ ${result.stderr}`);
|
|
|
2176
2297
|
}
|
|
2177
2298
|
return text(JSON.stringify(results, null, 2));
|
|
2178
2299
|
} catch (err) {
|
|
2179
|
-
return text(
|
|
2300
|
+
return text(
|
|
2301
|
+
`Lint failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2302
|
+
true
|
|
2303
|
+
);
|
|
2180
2304
|
}
|
|
2181
2305
|
}
|
|
2182
2306
|
);
|
|
2183
2307
|
server2.tool(
|
|
2184
2308
|
"analyze_secrets",
|
|
2185
|
-
"Analyze secret usage patterns and provide optimization suggestions including most accessed, stale, unused, and rotation recommendations.",
|
|
2309
|
+
"[agent] Analyze secret usage patterns and provide optimization suggestions including most accessed, stale, unused, and rotation recommendations.",
|
|
2186
2310
|
{
|
|
2187
|
-
scope:
|
|
2188
|
-
projectPath:
|
|
2189
|
-
teamId:
|
|
2190
|
-
orgId:
|
|
2311
|
+
scope: scope6,
|
|
2312
|
+
projectPath: projectPath6,
|
|
2313
|
+
teamId: teamId6,
|
|
2314
|
+
orgId: orgId6
|
|
2191
2315
|
},
|
|
2192
2316
|
async (params) => {
|
|
2193
2317
|
const toolBlock = enforceToolPolicy("analyze_secrets", params.projectPath);
|
|
@@ -2215,28 +2339,32 @@ ${result.stderr}`);
|
|
|
2215
2339
|
let dashboardInstance = null;
|
|
2216
2340
|
server2.tool(
|
|
2217
2341
|
"status_dashboard",
|
|
2218
|
-
"Launch the quantum status dashboard \u2014 a local web page showing live health,
|
|
2342
|
+
"[dashboard] Launch the quantum status dashboard \u2014 a local SSE-driven web page showing live KPIs (secrets, env, protected, approvals, hooks, 24h reads, anomalies), health summary, environment, .q-ring.json manifest gaps, governance policy summary, sortable searchable secrets table, decay/superposition/entanglement/tunnel cards, active approvals & hooks, agent memory, anomaly alerts, and a filterable 24h audit feed. Returns the URL to open in a browser. Never exposes secret values.",
|
|
2219
2343
|
{
|
|
2220
|
-
port:
|
|
2344
|
+
port: z9.number().optional().default(9876).describe("Port to serve on")
|
|
2221
2345
|
},
|
|
2222
2346
|
async (params) => {
|
|
2223
2347
|
const toolBlock = enforceToolPolicy("status_dashboard");
|
|
2224
2348
|
if (toolBlock) return toolBlock;
|
|
2225
2349
|
if (dashboardInstance) {
|
|
2226
|
-
return text(
|
|
2350
|
+
return text(
|
|
2351
|
+
`Dashboard already running at http://127.0.0.1:${dashboardInstance.port}`
|
|
2352
|
+
);
|
|
2227
2353
|
}
|
|
2228
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2354
|
+
const { startDashboardServer } = await import("./dashboard-FSCJDLJX.js");
|
|
2229
2355
|
dashboardInstance = startDashboardServer({ port: params.port });
|
|
2230
|
-
return text(
|
|
2231
|
-
|
|
2356
|
+
return text(
|
|
2357
|
+
`Dashboard started at http://127.0.0.1:${dashboardInstance.port}
|
|
2358
|
+
Open this URL in a browser to see live quantum status.`
|
|
2359
|
+
);
|
|
2232
2360
|
}
|
|
2233
2361
|
);
|
|
2234
2362
|
server2.tool(
|
|
2235
2363
|
"agent_scan",
|
|
2236
|
-
"Run an autonomous agent health scan: checks decay, staleness, anomalies, and optionally auto-rotates expired secrets. Returns a structured report.",
|
|
2364
|
+
"[agent] Run an autonomous agent health scan: checks decay, staleness, anomalies, and optionally auto-rotates expired secrets. Returns a structured report.",
|
|
2237
2365
|
{
|
|
2238
|
-
autoRotate:
|
|
2239
|
-
projectPaths:
|
|
2366
|
+
autoRotate: z9.boolean().optional().default(false).describe("Auto-rotate expired secrets with generated values"),
|
|
2367
|
+
projectPaths: z9.array(z9.string()).optional().describe("Project paths to monitor")
|
|
2240
2368
|
},
|
|
2241
2369
|
async (params) => {
|
|
2242
2370
|
const toolBlock = enforceToolPolicy("agent_scan");
|
|
@@ -2248,100 +2376,78 @@ Open this URL in a browser to see live quantum status.`);
|
|
|
2248
2376
|
return text(JSON.stringify(report, null, 2));
|
|
2249
2377
|
}
|
|
2250
2378
|
);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// src/mcp/tools/agent.ts
|
|
2382
|
+
import { z as z10 } from "zod";
|
|
2383
|
+
function registerAgentTools(server2) {
|
|
2251
2384
|
server2.tool(
|
|
2252
|
-
"
|
|
2253
|
-
"
|
|
2254
|
-
{},
|
|
2255
|
-
async () => {
|
|
2256
|
-
const toolBlock = enforceToolPolicy("verify_audit_chain");
|
|
2257
|
-
if (toolBlock) return toolBlock;
|
|
2258
|
-
const result = verifyAuditChain();
|
|
2259
|
-
return text(JSON.stringify(result, null, 2));
|
|
2260
|
-
}
|
|
2261
|
-
);
|
|
2262
|
-
server2.tool(
|
|
2263
|
-
"export_audit",
|
|
2264
|
-
"Export audit events in a portable format (jsonl, json, or csv) with optional time range filtering.",
|
|
2385
|
+
"agent_remember",
|
|
2386
|
+
"[agent] Store a key-value pair in encrypted agent memory that persists across sessions. Use this to remember decisions, rotation history, or project-specific context.",
|
|
2265
2387
|
{
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
format: z.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe("Output format")
|
|
2388
|
+
key: z10.string().describe("Memory key"),
|
|
2389
|
+
value: z10.string().describe("Value to store")
|
|
2269
2390
|
},
|
|
2270
2391
|
async (params) => {
|
|
2271
|
-
const toolBlock = enforceToolPolicy("
|
|
2392
|
+
const toolBlock = enforceToolPolicy("agent_remember");
|
|
2272
2393
|
if (toolBlock) return toolBlock;
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
until: params.until,
|
|
2276
|
-
format: params.format
|
|
2277
|
-
});
|
|
2278
|
-
return text(output);
|
|
2394
|
+
remember(params.key, params.value);
|
|
2395
|
+
return text(`Remembered "${params.key}"`);
|
|
2279
2396
|
}
|
|
2280
2397
|
);
|
|
2281
2398
|
server2.tool(
|
|
2282
|
-
"
|
|
2283
|
-
"
|
|
2399
|
+
"agent_recall",
|
|
2400
|
+
"[agent] Retrieve a value from agent memory, or list all stored keys if no key is provided.",
|
|
2284
2401
|
{
|
|
2285
|
-
key:
|
|
2286
|
-
provider: z.string().optional().describe("Force a specific provider"),
|
|
2287
|
-
scope: scopeSchema,
|
|
2288
|
-
projectPath: projectPathSchema,
|
|
2289
|
-
teamId: teamIdSchema,
|
|
2290
|
-
orgId: orgIdSchema
|
|
2402
|
+
key: z10.string().optional().describe("Memory key to recall (omit to list all)")
|
|
2291
2403
|
},
|
|
2292
2404
|
async (params) => {
|
|
2293
|
-
const toolBlock = enforceToolPolicy("
|
|
2405
|
+
const toolBlock = enforceToolPolicy("agent_recall");
|
|
2294
2406
|
if (toolBlock) return toolBlock;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
setSecret(params.key, result.newValue, {
|
|
2300
|
-
scope: params.scope ?? "global",
|
|
2301
|
-
projectPath: params.projectPath,
|
|
2302
|
-
source: "mcp"
|
|
2303
|
-
});
|
|
2407
|
+
if (!params.key) {
|
|
2408
|
+
const entries = listMemory();
|
|
2409
|
+
if (entries.length === 0) return text("Agent memory is empty");
|
|
2410
|
+
return text(JSON.stringify(entries, null, 2));
|
|
2304
2411
|
}
|
|
2305
|
-
|
|
2412
|
+
const value = recall(params.key);
|
|
2413
|
+
if (value === null)
|
|
2414
|
+
return text(`No memory found for "${params.key}"`, true);
|
|
2415
|
+
return text(
|
|
2416
|
+
JSON.stringify({ ok: true, data: { key: params.key, value } }, null, 2)
|
|
2417
|
+
);
|
|
2306
2418
|
}
|
|
2307
2419
|
);
|
|
2308
2420
|
server2.tool(
|
|
2309
|
-
"
|
|
2310
|
-
"
|
|
2421
|
+
"agent_forget",
|
|
2422
|
+
"[agent] Delete a key from agent memory.",
|
|
2311
2423
|
{
|
|
2312
|
-
|
|
2313
|
-
projectPath: projectPathSchema,
|
|
2314
|
-
teamId: teamIdSchema,
|
|
2315
|
-
orgId: orgIdSchema
|
|
2424
|
+
key: z10.string().describe("Memory key to forget")
|
|
2316
2425
|
},
|
|
2317
2426
|
async (params) => {
|
|
2318
|
-
const toolBlock = enforceToolPolicy("
|
|
2427
|
+
const toolBlock = enforceToolPolicy("agent_forget");
|
|
2319
2428
|
if (toolBlock) return toolBlock;
|
|
2320
|
-
const
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
key: e.key,
|
|
2326
|
-
value: val,
|
|
2327
|
-
provider: e.envelope?.meta.provider,
|
|
2328
|
-
validationUrl: e.envelope?.meta.validationUrl
|
|
2329
|
-
};
|
|
2330
|
-
}).filter((s) => s !== null);
|
|
2331
|
-
if (secrets.length === 0) return text("No secrets to validate");
|
|
2332
|
-
const report = await ciValidateBatch(secrets);
|
|
2333
|
-
return text(JSON.stringify(report, null, 2));
|
|
2429
|
+
const removed = forget(params.key);
|
|
2430
|
+
return text(
|
|
2431
|
+
removed ? `Forgot "${params.key}"` : `No memory found for "${params.key}"`,
|
|
2432
|
+
!removed
|
|
2433
|
+
);
|
|
2334
2434
|
}
|
|
2335
2435
|
);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/mcp/tools/policy.ts
|
|
2439
|
+
import { z as z11 } from "zod";
|
|
2440
|
+
var { projectPath: projectPath7 } = commonSchemas;
|
|
2441
|
+
function registerPolicyTools(server2) {
|
|
2336
2442
|
server2.tool(
|
|
2337
2443
|
"check_policy",
|
|
2338
|
-
"Check if an action is allowed by the project's governance policy. Returns the policy decision and source.",
|
|
2444
|
+
"[policy] Check if an action is allowed by the project's governance policy. Returns the policy decision and source.",
|
|
2339
2445
|
{
|
|
2340
|
-
action:
|
|
2341
|
-
toolName:
|
|
2342
|
-
key:
|
|
2343
|
-
command:
|
|
2344
|
-
projectPath:
|
|
2446
|
+
action: z11.enum(["tool", "key_read", "exec"]).describe("Type of policy check"),
|
|
2447
|
+
toolName: z11.string().optional().describe("Tool name to check (for action=tool)"),
|
|
2448
|
+
key: z11.string().optional().describe("Secret key to check (for action=key_read)"),
|
|
2449
|
+
command: z11.string().optional().describe("Command to check (for action=exec)"),
|
|
2450
|
+
projectPath: projectPath7
|
|
2345
2451
|
},
|
|
2346
2452
|
async (params) => {
|
|
2347
2453
|
if (params.action === "tool" && params.toolName) {
|
|
@@ -2356,20 +2462,51 @@ Open this URL in a browser to see live quantum status.`);
|
|
|
2356
2462
|
const d = checkExecPolicy(params.command, params.projectPath);
|
|
2357
2463
|
return text(JSON.stringify(d, null, 2));
|
|
2358
2464
|
}
|
|
2359
|
-
return text(
|
|
2465
|
+
return text(
|
|
2466
|
+
"Missing required parameter for the selected action type",
|
|
2467
|
+
true
|
|
2468
|
+
);
|
|
2360
2469
|
}
|
|
2361
2470
|
);
|
|
2362
2471
|
server2.tool(
|
|
2363
2472
|
"get_policy_summary",
|
|
2364
|
-
"Get a summary of the project's governance policy configuration.",
|
|
2473
|
+
"[policy] Get a summary of the project's governance policy configuration.",
|
|
2365
2474
|
{
|
|
2366
|
-
projectPath:
|
|
2475
|
+
projectPath: projectPath7
|
|
2367
2476
|
},
|
|
2368
2477
|
async (params) => {
|
|
2478
|
+
const toolBlock = enforceToolPolicy(
|
|
2479
|
+
"get_policy_summary",
|
|
2480
|
+
params.projectPath
|
|
2481
|
+
);
|
|
2482
|
+
if (toolBlock) return toolBlock;
|
|
2369
2483
|
const summary = getPolicySummary(params.projectPath);
|
|
2370
2484
|
return text(JSON.stringify(summary, null, 2));
|
|
2371
2485
|
}
|
|
2372
2486
|
);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/mcp/tool-registration.ts
|
|
2490
|
+
function registerMcpTools(server2) {
|
|
2491
|
+
registerSecretTools(server2);
|
|
2492
|
+
registerProjectTools(server2);
|
|
2493
|
+
registerTunnelTools(server2);
|
|
2494
|
+
registerTeleportTools(server2);
|
|
2495
|
+
registerAuditTools(server2);
|
|
2496
|
+
registerValidationTools(server2);
|
|
2497
|
+
registerHookTools(server2);
|
|
2498
|
+
registerToolingTools(server2);
|
|
2499
|
+
registerAgentTools(server2);
|
|
2500
|
+
registerPolicyTools(server2);
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/mcp/server.ts
|
|
2504
|
+
function createMcpServer() {
|
|
2505
|
+
const server2 = new McpServer({
|
|
2506
|
+
name: "q-ring",
|
|
2507
|
+
version: PACKAGE_VERSION
|
|
2508
|
+
});
|
|
2509
|
+
registerMcpTools(server2);
|
|
2373
2510
|
return server2;
|
|
2374
2511
|
}
|
|
2375
2512
|
|