@i4ctime/q-ring 0.9.8 → 0.11.0

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