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