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