@i4ctime/q-ring 0.17.6 → 0.18.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
+ Entry,
3
4
  PACKAGE_VERSION,
4
5
  checkDecay,
5
6
  checkExecPolicy,
@@ -33,6 +34,7 @@ import {
33
34
  registry,
34
35
  remember,
35
36
  removeHook,
37
+ rotationStatus,
36
38
  setAuditAgentLabel,
37
39
  setPolicyRoot,
38
40
  setSecret,
@@ -42,7 +44,7 @@ import {
42
44
  tunnelList,
43
45
  tunnelRead,
44
46
  verifyAuditChain
45
- } from "./chunk-VMDD5QTJ.js";
47
+ } from "./chunk-NTCTVLSK.js";
46
48
 
47
49
  // src/mcp.ts
48
50
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -59,6 +61,8 @@ var TOOL_ANNOTATIONS = {
59
61
  get_secret: READ,
60
62
  list_secrets: READ,
61
63
  set_secret: hints(false, true, true, false),
64
+ promote_secret: hints(false, true, true, false),
65
+ diff_environments: hints(true, false, true, false),
62
66
  delete_secret: hints(false, true, true, false),
63
67
  has_secret: READ,
64
68
  export_secrets: READ,
@@ -327,8 +331,8 @@ function opts(params) {
327
331
  source: "mcp"
328
332
  };
329
333
  }
330
- function enforceToolPolicy(toolName, projectPath8) {
331
- const decision = checkToolPolicy(toolName, projectPath8);
334
+ function enforceToolPolicy(toolName, projectPath9) {
335
+ const decision = checkToolPolicy(toolName, projectPath9);
332
336
  if (!decision.allowed) {
333
337
  return text(`Policy Denied: ${decision.reason} (source: ${decision.policySource})`, true);
334
338
  }
@@ -475,6 +479,9 @@ function registerSecretTools(server2) {
475
479
  rotationPrefix: z2.string().optional().describe(
476
480
  "Literal prefix prepended on auto-rotation (only used with rotationFormat 'api-key' or 'token'). Example: 'sk-'."
477
481
  ),
482
+ rotateEveryDays: z2.number().int().min(1).max(3650).optional().describe(
483
+ "Rotation reminder interval in days (1-3650). `inspect_secret` then reports rotation state ok / due-soon / overdue, and the dashboard and `qring rotate:due` surface it. Purely a reminder \u2014 nothing rotates automatically. Omit to keep the existing interval."
484
+ ),
478
485
  teamId,
479
486
  orgId
480
487
  },
@@ -498,7 +505,8 @@ function registerSecretTools(server2) {
498
505
  description: params.description,
499
506
  tags: params.tags,
500
507
  rotationFormat: params.rotationFormat,
501
- rotationPrefix: params.rotationPrefix
508
+ rotationPrefix: params.rotationPrefix,
509
+ rotateEveryDays: params.rotateEveryDays
502
510
  });
503
511
  return text(`[${params.scope ?? "global"}] ${params.key} set for env:${params.env}`);
504
512
  }
@@ -508,7 +516,8 @@ function registerSecretTools(server2) {
508
516
  description: params.description,
509
517
  tags: params.tags,
510
518
  rotationFormat: params.rotationFormat,
511
- rotationPrefix: params.rotationPrefix
519
+ rotationPrefix: params.rotationPrefix,
520
+ rotateEveryDays: params.rotateEveryDays
512
521
  });
513
522
  return text(`[${params.scope ?? "global"}] ${params.key} saved`);
514
523
  }
@@ -644,7 +653,7 @@ function registerSecretTools(server2) {
644
653
  [
645
654
  "[secrets] Show full metadata for a single secret \u2014 env states, decay window, entanglement links, access counters \u2014 without ever revealing the value.",
646
655
  "Use when you need to understand the shape of a key before reading it or to debug 'why is this expired/stale'; prefer `get_secret` for the actual value, `list_secrets` for a many-key overview, and `audit_log` for the full access timeline.",
647
- "Read-only; does not write a 'read' event since the value is not exposed. Returns pretty-printed JSON with fields: key, scope, type ('superposition'|'collapsed'), created, updated, accessCount, lastAccessed, environments, defaultEnv, decay { expired, stale, lifetimePercent, timeRemaining }, entangled, description, tags. Errors with not-found if the key is absent."
656
+ "Read-only; does not write a 'read' event since the value is not exposed. Returns pretty-printed JSON with fields: key, scope, type ('superposition'|'collapsed'), created, updated, accessCount, lastAccessed, environments, defaultEnv, decay { expired, stale, lifetimePercent, timeRemaining }, rotation { lastRotatedAt, ageDays, rotateEveryDays, dueAt, daysUntilDue, state: 'ok'|'due-soon'|'overdue'|'unscheduled' }, entangled, description, tags. Errors with not-found if the key is absent."
648
657
  ].join(" "),
649
658
  {
650
659
  key: z2.string().describe("Exact secret key name to inspect. Example: 'OPENAI_API_KEY'."),
@@ -682,6 +691,7 @@ function registerSecretTools(server2) {
682
691
  timeRemaining: decay.timeRemaining
683
692
  };
684
693
  }
694
+ info.rotation = rotationStatus(envelope.meta);
685
695
  if (envelope.meta.entangled?.length) {
686
696
  info.entangled = envelope.meta.entangled;
687
697
  }
@@ -821,6 +831,168 @@ function registerSecretTools(server2) {
821
831
  );
822
832
  }
823
833
 
834
+ // src/mcp/tools/environments.ts
835
+ import { z as z3 } from "zod";
836
+
837
+ // src/core/promote.ts
838
+ var PromoteConflictError = class extends Error {
839
+ code = "ERR_PROMOTE_CONFLICT";
840
+ constructor(key, to) {
841
+ super(
842
+ `"${key}" already has a different value for env "${to}" \u2014 pass force to overwrite it`
843
+ );
844
+ }
845
+ };
846
+ function assertEnvName(env3, label) {
847
+ if (!/^[A-Za-z0-9_.-]{1,64}$/.test(env3)) {
848
+ throw new Error(`${label} environment name "${env3}" is invalid (letters, digits, _ . - only)`);
849
+ }
850
+ }
851
+ function promoteSecret(key, opts2) {
852
+ assertEnvName(opts2.from, "source");
853
+ assertEnvName(opts2.to, "target");
854
+ if (opts2.from === opts2.to) {
855
+ throw new Error(`source and target environment are both "${opts2.from}"`);
856
+ }
857
+ const found = getEnvelope(key, opts2);
858
+ if (!found) throw new Error(`Secret "${key}" not found`);
859
+ const { envelope, scope: scope8 } = found;
860
+ const states = envelope.states;
861
+ if (!states) {
862
+ throw new Error(
863
+ `"${key}" has a single value, not per-environment states \u2014 set one with: qring set ${key} --env ${opts2.from}`
864
+ );
865
+ }
866
+ const source = states[opts2.from];
867
+ if (source === void 0) {
868
+ const available = Object.keys(states).join(", ") || "none";
869
+ throw new Error(`"${key}" has no value for env "${opts2.from}" (available: ${available})`);
870
+ }
871
+ const current = states[opts2.to];
872
+ const previous = current === void 0 ? "absent" : current === source ? "same" : "different";
873
+ if (previous === "same") {
874
+ return { key, scope: scope8, from: opts2.from, to: opts2.to, previous, changed: false };
875
+ }
876
+ if (previous === "different" && !opts2.force) {
877
+ throw new PromoteConflictError(key, opts2.to);
878
+ }
879
+ const nextStates = { ...states, [opts2.to]: source };
880
+ setSecret(key, "", {
881
+ ...opts2,
882
+ scope: scope8,
883
+ states: nextStates,
884
+ defaultEnv: envelope.defaultEnv
885
+ });
886
+ return { key, scope: scope8, from: opts2.from, to: opts2.to, previous, changed: true };
887
+ }
888
+ function diffEnvironments(opts2) {
889
+ assertEnvName(opts2.a, "first");
890
+ assertEnvName(opts2.b, "second");
891
+ const wanted = opts2.keys ? new Set(opts2.keys) : null;
892
+ const entries = [];
893
+ for (const entry of listSecrets(opts2)) {
894
+ if (wanted && !wanted.has(entry.key)) continue;
895
+ const states = entry.envelope?.states;
896
+ let status;
897
+ if (!states) {
898
+ status = "collapsed";
899
+ } else {
900
+ const va = states[opts2.a];
901
+ const vb = states[opts2.b];
902
+ if (va === void 0 && vb === void 0) continue;
903
+ if (va === void 0) status = "only-b";
904
+ else if (vb === void 0) status = "only-a";
905
+ else status = va === vb ? "same" : "different";
906
+ }
907
+ entries.push({ key: entry.key, scope: entry.scope, status });
908
+ }
909
+ const summary = {
910
+ same: 0,
911
+ different: 0,
912
+ "only-a": 0,
913
+ "only-b": 0,
914
+ collapsed: 0
915
+ };
916
+ for (const e of entries) summary[e.status] += 1;
917
+ const drift = summary.different + summary["only-a"] + summary["only-b"] > 0;
918
+ return { a: opts2.a, b: opts2.b, entries, summary, drift };
919
+ }
920
+
921
+ // src/mcp/tools/environments.ts
922
+ var { teamId: teamId2, orgId: orgId2, scope: scope2, projectPath: projectPath2 } = commonSchemas;
923
+ function registerEnvironmentTools(server2) {
924
+ server2.tool(
925
+ "promote_secret",
926
+ [
927
+ "[secrets] Copy one secret's value from a source environment to a target environment (superposition states), e.g. staging \u2192 prod, without the value ever leaving the keyring.",
928
+ "Use when an environment must match another for a single key; use `diff_environments` first to see what differs. Refuses to overwrite a differing target unless `force` is true; a target that already matches is a no-op.",
929
+ "Mutates the keyring (one 'write' audit event, hooks fire) only when the target changes. Returns { key, scope, from, to, previous: 'absent'|'same'|'different', changed }. Never returns the value."
930
+ ].join(" "),
931
+ {
932
+ key: z3.string().describe("Secret key name. Example: 'DATABASE_URL'."),
933
+ from: z3.string().describe("Source environment state. Example: 'staging'."),
934
+ to: z3.string().describe("Target environment state. Example: 'prod'."),
935
+ force: z3.boolean().default(false).describe("Overwrite a differing target value. Default false \u2192 the call fails with ERR_PROMOTE_CONFLICT instead."),
936
+ scope: scope2.default("global"),
937
+ projectPath: projectPath2,
938
+ teamId: teamId2,
939
+ orgId: orgId2
940
+ },
941
+ toolAnnotations("promote_secret"),
942
+ async (params) => {
943
+ const toolBlock = enforceToolPolicy("promote_secret", params.projectPath);
944
+ if (toolBlock) return toolBlock;
945
+ try {
946
+ const result = promoteSecret(params.key, {
947
+ ...opts(params),
948
+ from: params.from,
949
+ to: params.to,
950
+ force: params.force
951
+ });
952
+ return text(JSON.stringify({ ok: true, data: result }, null, 2));
953
+ } catch (err) {
954
+ const code = err instanceof PromoteConflictError ? err.code : "ERR_PROMOTE";
955
+ const message = err instanceof Error ? err.message : String(err);
956
+ return text(JSON.stringify({ ok: false, code, error: message }), true);
957
+ }
958
+ }
959
+ );
960
+ server2.tool(
961
+ "diff_environments",
962
+ [
963
+ "[secrets] Compare two environments across the visible secrets and report, per key, whether the values are the same, different, or present on only one side \u2014 statuses only, never values.",
964
+ "Use before a deploy or promotion to see environment drift; follow up with `promote_secret` per key. A single-value (collapsed) secret applies to every environment and is reported as 'collapsed'.",
965
+ "Read-only apart from a 'list' audit event. Returns { a, b, entries: [{ key, scope, status }], summary, drift }."
966
+ ].join(" "),
967
+ {
968
+ envA: z3.string().describe("First environment. Example: 'staging'."),
969
+ envB: z3.string().describe("Second environment. Example: 'prod'."),
970
+ keys: z3.array(z3.string()).optional().describe("Restrict the comparison to these keys."),
971
+ scope: scope2.optional(),
972
+ projectPath: projectPath2,
973
+ teamId: teamId2,
974
+ orgId: orgId2
975
+ },
976
+ toolAnnotations("diff_environments"),
977
+ async (params) => {
978
+ const toolBlock = enforceToolPolicy("diff_environments", params.projectPath);
979
+ if (toolBlock) return toolBlock;
980
+ try {
981
+ const result = diffEnvironments({
982
+ ...opts(params),
983
+ a: params.envA,
984
+ b: params.envB,
985
+ keys: params.keys
986
+ });
987
+ return text(JSON.stringify({ ok: true, data: result }, null, 2));
988
+ } catch (err) {
989
+ const message = err instanceof Error ? err.message : String(err);
990
+ return text(JSON.stringify({ ok: false, error: message }), true);
991
+ }
992
+ }
993
+ );
994
+ }
995
+
824
996
  // src/core/validate.ts
825
997
  function makeRequest(url, headers, timeoutMs = 1e4) {
826
998
  return httpRequest({ url, method: "GET", headers, timeoutMs });
@@ -1114,11 +1286,11 @@ async function ciValidateBatch(secrets) {
1114
1286
 
1115
1287
  // src/core/context.ts
1116
1288
  function getProjectContext(opts2 = {}) {
1117
- const projectPath8 = opts2.projectPath ?? process.cwd();
1118
- const envResult = collapseEnvironment({ projectPath: projectPath8 });
1289
+ const projectPath9 = opts2.projectPath ?? process.cwd();
1290
+ const envResult = collapseEnvironment({ projectPath: projectPath9 });
1119
1291
  const secretsList = listSecrets({
1120
1292
  ...opts2,
1121
- projectPath: projectPath8,
1293
+ projectPath: projectPath9,
1122
1294
  silent: true
1123
1295
  });
1124
1296
  let expiredCount = 0;
@@ -1148,7 +1320,7 @@ function getProjectContext(opts2 = {}) {
1148
1320
  };
1149
1321
  });
1150
1322
  let manifest = null;
1151
- const config = readProjectConfig(projectPath8);
1323
+ const config = readProjectConfig(projectPath9);
1152
1324
  if (config?.secrets) {
1153
1325
  const declaredKeys = Object.keys(config.secrets);
1154
1326
  const existingKeys = new Set(secrets.map((s) => s.key));
@@ -1163,7 +1335,7 @@ function getProjectContext(opts2 = {}) {
1163
1335
  timestamp: e.timestamp
1164
1336
  }));
1165
1337
  return {
1166
- projectPath: projectPath8,
1338
+ projectPath: projectPath9,
1167
1339
  environment: envResult ? { env: envResult.env, source: envResult.source } : null,
1168
1340
  secrets,
1169
1341
  totalSecrets: secrets.length,
@@ -1179,7 +1351,7 @@ function getProjectContext(opts2 = {}) {
1179
1351
  }
1180
1352
 
1181
1353
  // src/mcp/tools/project.ts
1182
- var { teamId: teamId2, orgId: orgId2, scope: scope2, projectPath: projectPath2, env: env2 } = commonSchemas;
1354
+ var { teamId: teamId3, orgId: orgId3, scope: scope3, projectPath: projectPath3, env: env2 } = commonSchemas;
1183
1355
  function registerProjectTools(server2) {
1184
1356
  server2.tool(
1185
1357
  "check_project",
@@ -1189,7 +1361,7 @@ function registerProjectTools(server2) {
1189
1361
  "Read-only; does not mutate the keyring or audit log materially beyond a 'list' read. Returns JSON `{ total, present, missing, expired, stale, ready, secrets: [...] }` where `ready` is true only when nothing is missing or expired. Errors with 'No secrets manifest found in .q-ring.json' if the project has no manifest."
1190
1362
  ].join(" "),
1191
1363
  {
1192
- projectPath: projectPath2
1364
+ projectPath: projectPath3
1193
1365
  },
1194
1366
  toolAnnotations("check_project"),
1195
1367
  async (params) => {
@@ -1261,7 +1433,7 @@ function registerProjectTools(server2) {
1261
1433
  "Reads values (records 'read' audit events) and collapses superposition for the requested env. Returns the raw `.env` text, with `# MISSING (required): KEY` / `# EXPIRED: KEY` / `# STALE: KEY` warnings appended as comments. Missing keys appear as commented-out `# KEY=` placeholders so the file remains a valid drop-in."
1262
1434
  ].join(" "),
1263
1435
  {
1264
- projectPath: projectPath2,
1436
+ projectPath: projectPath3,
1265
1437
  env: env2
1266
1438
  },
1267
1439
  toolAnnotations("env_generate"),
@@ -1313,7 +1485,7 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1313
1485
  "Read-only; checks the QRING_ENV env var, NODE_ENV, the project's `.q-ring.json`, and the current git branch in priority order. Returns JSON `{ env, source }` (e.g. `{ env: 'dev', source: 'NODE_ENV' }`), or a plain message indicating that no env could be detected."
1314
1486
  ].join(" "),
1315
1487
  {
1316
- projectPath: projectPath2
1488
+ projectPath: projectPath3
1317
1489
  },
1318
1490
  toolAnnotations("detect_environment"),
1319
1491
  async (params) => {
@@ -1336,10 +1508,10 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1336
1508
  "Read-only and value-safe \u2014 no plaintext secret values are ever included. Returns a single pretty-printed JSON document; shape is intentionally broad and may grow over time, so read defensively."
1337
1509
  ].join(" "),
1338
1510
  {
1339
- scope: scope2,
1340
- projectPath: projectPath2,
1341
- teamId: teamId2,
1342
- orgId: orgId2
1511
+ scope: scope3,
1512
+ projectPath: projectPath3,
1513
+ teamId: teamId3,
1514
+ orgId: orgId3
1343
1515
  },
1344
1516
  toolAnnotations("get_project_context"),
1345
1517
  async (params) => {
@@ -1352,7 +1524,7 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1352
1524
  }
1353
1525
 
1354
1526
  // src/mcp/tools/tunnel.ts
1355
- import { z as z3 } from "zod";
1527
+ import { z as z4 } from "zod";
1356
1528
  function registerTunnelTools(server2) {
1357
1529
  server2.tool(
1358
1530
  "tunnel_create",
@@ -1362,11 +1534,11 @@ function registerTunnelTools(server2) {
1362
1534
  "Mutates only in-memory state \u2014 the value never touches disk and is lost on server restart. Subject to tool policy. Returns JSON `{ ok, data: { id } }` where `id` is an opaque string to pass to `tunnel_read`/`tunnel_destroy`."
1363
1535
  ].join(" "),
1364
1536
  {
1365
- value: z3.string().describe("The plaintext value to tunnel. Held only in process memory; never logged."),
1366
- ttlSeconds: z3.number().optional().describe(
1537
+ value: z4.string().describe("The plaintext value to tunnel. Held only in process memory; never logged."),
1538
+ ttlSeconds: z4.number().optional().describe(
1367
1539
  "Auto-destroy the tunnel after this many seconds. Omit for no time limit (then a `maxReads` is highly recommended)."
1368
1540
  ),
1369
- maxReads: z3.number().optional().describe(
1541
+ maxReads: z4.number().optional().describe(
1370
1542
  "Self-destruct after this many successful `tunnel_read` calls. Use 1 for true one-shot delivery."
1371
1543
  )
1372
1544
  },
@@ -1389,7 +1561,7 @@ function registerTunnelTools(server2) {
1389
1561
  "Increments the read counter and may auto-destroy the tunnel if `maxReads` was set. Returns JSON `{ ok, data: { id, value } }` on success, or an error 'Tunnel \"...\" not found or expired' if the tunnel has been destroyed, hit its TTL, or never existed."
1390
1562
  ].join(" "),
1391
1563
  {
1392
- id: z3.string().describe("The opaque tunnel ID returned by `tunnel_create`. Case-sensitive.")
1564
+ id: z4.string().describe("The opaque tunnel ID returned by `tunnel_create`. Case-sensitive.")
1393
1565
  },
1394
1566
  toolAnnotations("tunnel_read"),
1395
1567
  async (params) => {
@@ -1437,7 +1609,7 @@ function registerTunnelTools(server2) {
1437
1609
  "Mutates in-memory state only. Returns 'Destroyed ID' on success or a not-found error if the ID is unknown or already gone."
1438
1610
  ].join(" "),
1439
1611
  {
1440
- id: z3.string().describe("The opaque tunnel ID to destroy.")
1612
+ id: z4.string().describe("The opaque tunnel ID to destroy.")
1441
1613
  },
1442
1614
  toolAnnotations("tunnel_destroy"),
1443
1615
  async (params) => {
@@ -1453,46 +1625,80 @@ function registerTunnelTools(server2) {
1453
1625
  }
1454
1626
 
1455
1627
  // src/mcp/tools/teleport.ts
1456
- import { z as z5 } from "zod";
1628
+ import { z as z6 } from "zod";
1457
1629
 
1458
1630
  // src/core/teleport.ts
1459
1631
  import {
1460
1632
  randomBytes as randomBytes2,
1461
1633
  createCipheriv,
1462
1634
  createDecipheriv,
1635
+ createHash,
1636
+ createPrivateKey,
1637
+ createPublicKey,
1638
+ diffieHellman,
1639
+ generateKeyPairSync,
1640
+ hkdfSync,
1463
1641
  pbkdf2Sync
1464
1642
  } from "crypto";
1465
- import { z as z4 } from "zod";
1643
+ import { z as z5 } from "zod";
1466
1644
  var ALGORITHM = "aes-256-gcm";
1467
1645
  var KEY_LENGTH = 32;
1468
1646
  var IV_LENGTH = 12;
1469
1647
  var SALT_LENGTH = 32;
1470
1648
  var PBKDF2_ITERATIONS = 21e4;
1471
1649
  var LEGACY_PBKDF2_ITERATIONS = 1e5;
1472
- var TeleportBundleSchema = z4.object({
1473
- v: z4.literal(1),
1474
- data: z4.string(),
1475
- salt: z4.string(),
1476
- iv: z4.string(),
1477
- tag: z4.string(),
1478
- createdAt: z4.string(),
1479
- count: z4.number(),
1480
- iter: z4.number().optional()
1650
+ var TeleportBundleSchema = z5.object({
1651
+ v: z5.literal(1),
1652
+ data: z5.string(),
1653
+ salt: z5.string(),
1654
+ iv: z5.string(),
1655
+ tag: z5.string(),
1656
+ createdAt: z5.string(),
1657
+ count: z5.number(),
1658
+ iter: z5.number().optional()
1481
1659
  });
1482
- var TeleportPayloadSchema = z4.object({
1483
- secrets: z4.array(
1484
- z4.object({
1485
- key: z4.string(),
1486
- value: z4.string(),
1487
- scope: z4.string().optional()
1660
+ var TeleportPayloadSchema = z5.object({
1661
+ secrets: z5.array(
1662
+ z5.object({
1663
+ key: z5.string(),
1664
+ value: z5.string(),
1665
+ scope: z5.string().optional()
1488
1666
  })
1489
1667
  ),
1490
- exportedAt: z4.string(),
1491
- exportedBy: z4.string().optional()
1668
+ exportedAt: z5.string(),
1669
+ exportedBy: z5.string().optional()
1492
1670
  });
1493
1671
  function deriveKey(passphrase, salt, iterations = PBKDF2_ITERATIONS) {
1494
1672
  return pbkdf2Sync(passphrase, salt, iterations, KEY_LENGTH, "sha512");
1495
1673
  }
1674
+ function decodeBundle(encoded) {
1675
+ let bundleJson;
1676
+ try {
1677
+ bundleJson = Buffer.from(encoded, "base64").toString("utf8");
1678
+ } catch {
1679
+ throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
1680
+ }
1681
+ try {
1682
+ return JSON.parse(bundleJson);
1683
+ } catch {
1684
+ throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
1685
+ }
1686
+ }
1687
+ function parsePayload(decrypted) {
1688
+ let rawPayload;
1689
+ try {
1690
+ rawPayload = JSON.parse(decrypted.toString("utf8"));
1691
+ } catch {
1692
+ throw new Error("ERR_TELEPORT_CORRUPT: decrypted payload is not valid JSON");
1693
+ }
1694
+ const payload = TeleportPayloadSchema.safeParse(rawPayload);
1695
+ if (!payload.success) {
1696
+ throw new Error(
1697
+ `ERR_TELEPORT_CORRUPT: invalid payload (${payload.error.message})`
1698
+ );
1699
+ }
1700
+ return payload.data;
1701
+ }
1496
1702
  function teleportPack(secrets, passphrase) {
1497
1703
  const payload = {
1498
1704
  secrets,
@@ -1521,18 +1727,7 @@ function teleportPack(secrets, passphrase) {
1521
1727
  return Buffer.from(JSON.stringify(bundle)).toString("base64");
1522
1728
  }
1523
1729
  function teleportUnpack(encoded, passphrase) {
1524
- let bundleJson;
1525
- try {
1526
- bundleJson = Buffer.from(encoded, "base64").toString("utf8");
1527
- } catch {
1528
- throw new Error("ERR_TELEPORT_CORRUPT: invalid base64 bundle");
1529
- }
1530
- let rawBundle;
1531
- try {
1532
- rawBundle = JSON.parse(bundleJson);
1533
- } catch {
1534
- throw new Error("ERR_TELEPORT_CORRUPT: bundle is not valid JSON");
1535
- }
1730
+ const rawBundle = decodeBundle(encoded);
1536
1731
  const parsedBundle = TeleportBundleSchema.safeParse(rawBundle);
1537
1732
  if (!parsedBundle.success) {
1538
1733
  throw new Error(
@@ -1556,47 +1751,336 @@ function teleportUnpack(encoded, passphrase) {
1556
1751
  } catch {
1557
1752
  throw new Error("ERR_TELEPORT_BAD_PASSPHRASE: decryption failed (wrong passphrase or corrupt data)");
1558
1753
  }
1559
- let rawPayload;
1754
+ return parsePayload(decrypted);
1755
+ }
1756
+ var RECIPIENT_PREFIX = "qring1";
1757
+ var RECIPIENT_RE = /^qring1([A-Za-z0-9_-]{43})$/;
1758
+ var X25519_RAW_LENGTH = 32;
1759
+ var V2_AAD = "qring-teleport-v2";
1760
+ var V2_WRAP_INFO = "qring-teleport-v2-wrap";
1761
+ var TELEPORT_KEYRING_SERVICE = "q-ring-teleport";
1762
+ var TELEPORT_KEYRING_ACCOUNT = "identity";
1763
+ var TeleportBundleV2Schema = z5.object({
1764
+ v: z5.literal(2),
1765
+ createdAt: z5.string(),
1766
+ count: z5.number(),
1767
+ ephemeral: z5.string(),
1768
+ recipients: z5.array(
1769
+ z5.object({
1770
+ id: z5.string(),
1771
+ wrap: z5.string(),
1772
+ iv: z5.string(),
1773
+ tag: z5.string()
1774
+ })
1775
+ ).min(1),
1776
+ iv: z5.string(),
1777
+ tag: z5.string(),
1778
+ data: z5.string()
1779
+ });
1780
+ function rawPublicKey(key) {
1781
+ const jwk = key.export({ format: "jwk" });
1782
+ if (jwk.kty !== "OKP" || jwk.crv !== "X25519" || typeof jwk.x !== "string") {
1783
+ throw new Error("ERR_TELEPORT_BAD_RECIPIENT: not an X25519 public key");
1784
+ }
1785
+ const raw = Buffer.from(jwk.x, "base64url");
1786
+ if (raw.length !== X25519_RAW_LENGTH) {
1787
+ throw new Error("ERR_TELEPORT_BAD_RECIPIENT: not an X25519 public key");
1788
+ }
1789
+ return raw;
1790
+ }
1791
+ function publicKeyFromRaw(raw) {
1792
+ return createPublicKey({
1793
+ key: { kty: "OKP", crv: "X25519", x: raw.toString("base64url") },
1794
+ format: "jwk"
1795
+ });
1796
+ }
1797
+ function formatRecipient(publicKey) {
1798
+ const raw = Buffer.isBuffer(publicKey) ? publicKey : rawPublicKey(publicKey);
1799
+ if (raw.length !== X25519_RAW_LENGTH) {
1800
+ throw new Error("ERR_TELEPORT_BAD_RECIPIENT: public key must be 32 raw bytes");
1801
+ }
1802
+ return `${RECIPIENT_PREFIX}${raw.toString("base64url")}`;
1803
+ }
1804
+ function parseRecipient(str) {
1805
+ const trimmed = typeof str === "string" ? str.trim() : "";
1806
+ const match = RECIPIENT_RE.exec(trimmed);
1807
+ if (!match) {
1808
+ throw new Error(
1809
+ `ERR_TELEPORT_BAD_RECIPIENT: expected "${RECIPIENT_PREFIX}" followed by a base64url X25519 public key (run \`qring teleport identity\` on the recipient's machine to get one)`
1810
+ );
1811
+ }
1812
+ const raw = Buffer.from(match[1], "base64url");
1813
+ if (raw.length !== X25519_RAW_LENGTH) {
1814
+ throw new Error(
1815
+ "ERR_TELEPORT_BAD_RECIPIENT: recipient key does not decode to 32 bytes"
1816
+ );
1817
+ }
1818
+ return raw;
1819
+ }
1820
+ function recipientId(rawPub) {
1821
+ return createHash("sha256").update(rawPub).digest("hex").slice(0, 8);
1822
+ }
1823
+ function deriveWrapKey(shared, ephemeralPub, recipientPub) {
1824
+ return Buffer.from(
1825
+ hkdfSync(
1826
+ "sha256",
1827
+ shared,
1828
+ Buffer.concat([ephemeralPub, recipientPub]),
1829
+ V2_WRAP_INFO,
1830
+ KEY_LENGTH
1831
+ )
1832
+ );
1833
+ }
1834
+ function teleportPackFor(secrets, recipients) {
1835
+ const seen = /* @__PURE__ */ new Map();
1836
+ for (const r of recipients) {
1837
+ const raw = parseRecipient(r);
1838
+ seen.set(recipientId(raw), raw);
1839
+ }
1840
+ if (seen.size === 0) {
1841
+ throw new Error("ERR_TELEPORT_NO_RECIPIENTS: at least one recipient is required");
1842
+ }
1843
+ const payload = {
1844
+ secrets,
1845
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
1846
+ };
1847
+ const plaintext = JSON.stringify(payload);
1848
+ const cek = randomBytes2(KEY_LENGTH);
1849
+ const iv = randomBytes2(IV_LENGTH);
1850
+ const cipher = createCipheriv(ALGORITHM, cek, iv);
1851
+ cipher.setAAD(Buffer.from(V2_AAD, "utf8"));
1852
+ const encrypted = Buffer.concat([
1853
+ cipher.update(plaintext, "utf8"),
1854
+ cipher.final()
1855
+ ]);
1856
+ const tag = cipher.getAuthTag();
1857
+ const ephemeral = generateKeyPairSync("x25519");
1858
+ const ephemeralPub = rawPublicKey(ephemeral.publicKey);
1859
+ const wrapped = [];
1860
+ for (const [id, recipientPub] of seen) {
1861
+ const shared = diffieHellman({
1862
+ privateKey: ephemeral.privateKey,
1863
+ publicKey: publicKeyFromRaw(recipientPub)
1864
+ });
1865
+ const wrapKey = deriveWrapKey(shared, ephemeralPub, recipientPub);
1866
+ shared.fill(0);
1867
+ const wrapIv = randomBytes2(IV_LENGTH);
1868
+ const wrapCipher = createCipheriv(ALGORITHM, wrapKey, wrapIv);
1869
+ wrapCipher.setAAD(Buffer.from(id, "utf8"));
1870
+ const wrap = Buffer.concat([wrapCipher.update(cek), wrapCipher.final()]);
1871
+ const wrapTag = wrapCipher.getAuthTag();
1872
+ wrapKey.fill(0);
1873
+ wrapped.push({
1874
+ id,
1875
+ wrap: wrap.toString("base64"),
1876
+ iv: wrapIv.toString("base64"),
1877
+ tag: wrapTag.toString("base64")
1878
+ });
1879
+ }
1880
+ cek.fill(0);
1881
+ const bundle = {
1882
+ v: 2,
1883
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1884
+ count: secrets.length,
1885
+ ephemeral: ephemeralPub.toString("base64url"),
1886
+ recipients: wrapped,
1887
+ iv: iv.toString("base64"),
1888
+ tag: tag.toString("base64"),
1889
+ data: encrypted.toString("base64")
1890
+ };
1891
+ return Buffer.from(JSON.stringify(bundle)).toString("base64");
1892
+ }
1893
+ function toPrivateKey(identity) {
1894
+ if (typeof identity !== "string") return identity;
1560
1895
  try {
1561
- rawPayload = JSON.parse(decrypted.toString("utf8"));
1896
+ return createPrivateKey(identity);
1562
1897
  } catch {
1563
- throw new Error("ERR_TELEPORT_CORRUPT: decrypted payload is not valid JSON");
1898
+ throw new Error("ERR_TELEPORT_BAD_IDENTITY: private key is not a valid PEM");
1564
1899
  }
1565
- const payload = TeleportPayloadSchema.safeParse(rawPayload);
1566
- if (!payload.success) {
1900
+ }
1901
+ function teleportUnpackWith(encoded, identity) {
1902
+ const privateKey = toPrivateKey(identity);
1903
+ const rawBundle = decodeBundle(encoded);
1904
+ const parsedBundle = TeleportBundleV2Schema.safeParse(rawBundle);
1905
+ if (!parsedBundle.success) {
1567
1906
  throw new Error(
1568
- `ERR_TELEPORT_CORRUPT: invalid payload (${payload.error.message})`
1907
+ `ERR_TELEPORT_CORRUPT: invalid bundle shape (${parsedBundle.error.message})`
1569
1908
  );
1570
1909
  }
1571
- return payload.data;
1910
+ const bundle = parsedBundle.data;
1911
+ const myPub = rawPublicKey(createPublicKey(privateKey));
1912
+ const myId = recipientId(myPub);
1913
+ const mine = bundle.recipients.filter((r) => r.id === myId);
1914
+ if (mine.length === 0) {
1915
+ const ids = bundle.recipients.map((r) => r.id).join(", ");
1916
+ throw new Error(
1917
+ `ERR_TELEPORT_NOT_A_RECIPIENT: bundle is addressed to [${ids}], not to ${myId}`
1918
+ );
1919
+ }
1920
+ const ephemeralPub = Buffer.from(bundle.ephemeral, "base64url");
1921
+ if (ephemeralPub.length !== X25519_RAW_LENGTH) {
1922
+ throw new Error("ERR_TELEPORT_CORRUPT: ephemeral key is not 32 bytes");
1923
+ }
1924
+ let ephemeralKey;
1925
+ try {
1926
+ ephemeralKey = publicKeyFromRaw(ephemeralPub);
1927
+ } catch {
1928
+ throw new Error("ERR_TELEPORT_CORRUPT: ephemeral key is not a valid X25519 point");
1929
+ }
1930
+ const shared = diffieHellman({ privateKey, publicKey: ephemeralKey });
1931
+ const wrapKey = deriveWrapKey(shared, ephemeralPub, myPub);
1932
+ shared.fill(0);
1933
+ let cek = null;
1934
+ for (const entry of mine) {
1935
+ try {
1936
+ const decipher = createDecipheriv(
1937
+ ALGORITHM,
1938
+ wrapKey,
1939
+ Buffer.from(entry.iv, "base64")
1940
+ );
1941
+ decipher.setAAD(Buffer.from(entry.id, "utf8"));
1942
+ decipher.setAuthTag(Buffer.from(entry.tag, "base64"));
1943
+ cek = Buffer.concat([
1944
+ decipher.update(Buffer.from(entry.wrap, "base64")),
1945
+ decipher.final()
1946
+ ]);
1947
+ break;
1948
+ } catch {
1949
+ cek = null;
1950
+ }
1951
+ }
1952
+ wrapKey.fill(0);
1953
+ if (cek === null || cek.length !== KEY_LENGTH) {
1954
+ cek?.fill(0);
1955
+ throw new Error(
1956
+ "ERR_TELEPORT_CORRUPT: could not unwrap content key (tampered bundle or mismatched identity)"
1957
+ );
1958
+ }
1959
+ let decrypted;
1960
+ try {
1961
+ const decipher = createDecipheriv(
1962
+ ALGORITHM,
1963
+ cek,
1964
+ Buffer.from(bundle.iv, "base64")
1965
+ );
1966
+ decipher.setAAD(Buffer.from(V2_AAD, "utf8"));
1967
+ decipher.setAuthTag(Buffer.from(bundle.tag, "base64"));
1968
+ decrypted = Buffer.concat([
1969
+ decipher.update(Buffer.from(bundle.data, "base64")),
1970
+ decipher.final()
1971
+ ]);
1972
+ } catch {
1973
+ throw new Error("ERR_TELEPORT_CORRUPT: payload authentication failed (tampered bundle)");
1974
+ } finally {
1975
+ cek.fill(0);
1976
+ }
1977
+ return parsePayload(decrypted);
1978
+ }
1979
+ function inspectTeleportBundle(encoded) {
1980
+ const raw = decodeBundle(encoded);
1981
+ const v1 = TeleportBundleSchema.safeParse(raw);
1982
+ if (v1.success) return { v: 1, count: v1.data.count };
1983
+ const v2 = TeleportBundleV2Schema.safeParse(raw);
1984
+ if (v2.success) {
1985
+ return {
1986
+ v: 2,
1987
+ count: v2.data.count,
1988
+ recipients: v2.data.recipients.map((r) => r.id)
1989
+ };
1990
+ }
1991
+ const version = raw !== null && typeof raw === "object" && "v" in raw ? String(raw.v) : "unknown";
1992
+ throw new Error(
1993
+ `ERR_TELEPORT_CORRUPT: unsupported or malformed bundle (v=${version})`
1994
+ );
1995
+ }
1996
+ function teleportUnpackAuto(encoded, creds) {
1997
+ const info = inspectTeleportBundle(encoded);
1998
+ if (info.v === 1) {
1999
+ if (creds.passphrase === void 0) {
2000
+ throw new Error(
2001
+ "ERR_TELEPORT_PASSPHRASE_REQUIRED: this is a passphrase (v1) bundle"
2002
+ );
2003
+ }
2004
+ return teleportUnpack(encoded, creds.passphrase);
2005
+ }
2006
+ if (creds.identity === void 0) {
2007
+ throw new Error(
2008
+ "ERR_TELEPORT_NO_IDENTITY: this bundle is addressed to recipient keys \u2014 run `qring teleport keygen` to create yours"
2009
+ );
2010
+ }
2011
+ return teleportUnpackWith(encoded, creds.identity);
2012
+ }
2013
+ function identityFrom(privateKey) {
2014
+ const raw = rawPublicKey(createPublicKey(privateKey));
2015
+ return { privateKey, recipient: formatRecipient(raw), id: recipientId(raw) };
2016
+ }
2017
+ function loadTeleportIdentity() {
2018
+ const stored = new Entry(
2019
+ TELEPORT_KEYRING_SERVICE,
2020
+ TELEPORT_KEYRING_ACCOUNT
2021
+ ).getPassword();
2022
+ if (!stored) return null;
2023
+ let privateKey;
2024
+ try {
2025
+ privateKey = createPrivateKey({
2026
+ key: Buffer.from(stored, "base64"),
2027
+ format: "der",
2028
+ type: "pkcs8"
2029
+ });
2030
+ } catch {
2031
+ throw new Error(
2032
+ "ERR_TELEPORT_BAD_IDENTITY: stored teleport identity is unreadable \u2014 run `qring teleport keygen --force`"
2033
+ );
2034
+ }
2035
+ return identityFrom(privateKey);
1572
2036
  }
1573
2037
 
1574
2038
  // src/mcp/tools/teleport.ts
1575
- var { teamId: teamId3, orgId: orgId3, scope: scope3, projectPath: projectPath3 } = commonSchemas;
2039
+ var { teamId: teamId4, orgId: orgId4, scope: scope4, projectPath: projectPath4 } = commonSchemas;
1576
2040
  function registerTeleportTools(server2) {
1577
2041
  server2.tool(
1578
2042
  "teleport_pack",
1579
2043
  [
1580
2044
  "[teleport] Encrypt one or more secrets into a single AES-256-GCM bundle string that can be safely transferred between machines.",
1581
2045
  "Use to hand off a curated set of credentials to another developer or environment; prefer `export_secrets` for plaintext .env output (single machine, trusted) and `tunnel_create` for ephemeral one-shot delivery on the same machine.",
1582
- "Reads each secret value (records 'export' audit events) and produces a base64-encoded ciphertext. The bundle is unreadable without the same passphrase via `teleport_unpack`. Returns the bundle string directly. Errors with 'No secrets to pack' if the filter matched zero secrets."
2046
+ "Two modes, exactly one required: `passphrase` (v1, symmetric \u2014 receiver needs the same string) or `recipients` (v2, public-key \u2014 each receiver's `qring1...` string from `qring teleport identity`; no shared secret, only the listed identities can open it).",
2047
+ "Reads each secret value (records 'export' audit events) and produces a base64-encoded ciphertext. Returns the bundle string directly. Errors with 'No secrets to pack' if the filter matched zero secrets."
1583
2048
  ].join(" "),
1584
2049
  {
1585
- keys: z5.array(z5.string()).optional().describe(
2050
+ keys: z6.array(z6.string()).optional().describe(
1586
2051
  "Whitelist of exact key names to include. Omit to pack every secret in the requested scope."
1587
2052
  ),
1588
- passphrase: z5.string().describe(
1589
- "Symmetric passphrase used to derive the AES-256-GCM key. The receiver must supply the same string to `teleport_unpack`. Pick something high-entropy and share it out-of-band."
2053
+ passphrase: z6.string().optional().describe(
2054
+ "Symmetric passphrase used to derive the AES-256-GCM key (v1 bundle). The receiver must supply the same string to `teleport_unpack`. Pick something high-entropy and share it out-of-band. Mutually exclusive with `recipients`."
1590
2055
  ),
1591
- scope: scope3,
1592
- projectPath: projectPath3,
1593
- teamId: teamId3,
1594
- orgId: orgId3
2056
+ recipients: z6.array(z6.string()).optional().describe(
2057
+ "Recipient public keys (`qring1...` strings, one per teammate) for a v2 recipient pack. The bundle can only be opened by the matching private keys, which each receiver created with `qring teleport keygen`. Mutually exclusive with `passphrase`."
2058
+ ),
2059
+ scope: scope4,
2060
+ projectPath: projectPath4,
2061
+ teamId: teamId4,
2062
+ orgId: orgId4
1595
2063
  },
1596
2064
  toolAnnotations("teleport_pack"),
1597
2065
  async (params) => {
1598
2066
  const toolBlock = enforceToolPolicy("teleport_pack", params.projectPath);
1599
2067
  if (toolBlock) return toolBlock;
2068
+ const recipients = (params.recipients ?? []).flatMap((r) => r.split(",")).map((r) => r.trim()).filter((r) => r.length > 0);
2069
+ const hasPassphrase = typeof params.passphrase === "string" && params.passphrase.length > 0;
2070
+ if (hasPassphrase === recipients.length > 0) {
2071
+ return text(
2072
+ "teleport_pack needs exactly one of `passphrase` (v1) or `recipients` (v2)",
2073
+ true
2074
+ );
2075
+ }
2076
+ for (const r of recipients) {
2077
+ try {
2078
+ parseRecipient(r);
2079
+ } catch (err) {
2080
+ const msg = err instanceof Error ? err.message : String(err);
2081
+ return text(`Bad recipient "${r}": ${msg}`, true);
2082
+ }
2083
+ }
1600
2084
  const o = opts(params);
1601
2085
  const entries = listSecrets(o);
1602
2086
  const secrets = [];
@@ -1608,7 +2092,7 @@ function registerTeleportTools(server2) {
1608
2092
  }
1609
2093
  }
1610
2094
  if (secrets.length === 0) return text("No secrets to pack", true);
1611
- const bundle = teleportPack(secrets, params.passphrase);
2095
+ const bundle = recipients.length > 0 ? teleportPackFor(secrets, recipients) : teleportPack(secrets, params.passphrase);
1612
2096
  return text(bundle);
1613
2097
  }
1614
2098
  );
@@ -1616,21 +2100,22 @@ function registerTeleportTools(server2) {
1616
2100
  "teleport_unpack",
1617
2101
  [
1618
2102
  "[teleport] Decrypt a bundle produced by `teleport_pack` and import each contained secret into the local keyring.",
1619
- "Use on the receiving machine after a packer hands you the bundle and passphrase out-of-band; prefer `dryRun=true` first to preview what will be written.",
1620
- "When dryRun is false this mutates the keyring (one 'write' event per imported secret) at the requested scope. Bad passphrase or tampered bundle returns JSON `{ ok: false, error: { message } }` with `isError: true`. On success returns 'Imported N secret(s) from teleport bundle'; in dryRun mode returns 'Would import N secrets:' followed by a `KEY [scope]` listing."
2103
+ "Use on the receiving machine after a packer hands you the bundle; prefer `dryRun=true` first to preview what will be written.",
2104
+ "Passphrase (v1) bundles need `passphrase`. Recipient (v2) bundles need no input: this machine's teleport identity is read from the OS keyring (create one with `qring teleport keygen`; the private key is never returned).",
2105
+ "When dryRun is false this mutates the keyring (one 'write' event per imported secret) at the requested scope. Bad passphrase, missing identity, not-a-recipient or tampered bundle returns JSON `{ ok: false, error: { message } }` with `isError: true`. On success returns 'Imported N secret(s) from teleport bundle'; in dryRun mode returns 'Would import N secrets:' followed by a `KEY [scope]` listing (v2 also lists the recipient ids the bundle is addressed to)."
1621
2106
  ].join(" "),
1622
2107
  {
1623
- bundle: z5.string().describe(
2108
+ bundle: z6.string().describe(
1624
2109
  "Base64-encoded ciphertext returned by `teleport_pack`. Pass through whitespace untouched if possible."
1625
2110
  ),
1626
- passphrase: z5.string().describe(
1627
- "The same passphrase that was used to pack this bundle. Bad passphrases return an authentication error rather than wrong plaintext."
2111
+ passphrase: z6.string().optional().describe(
2112
+ "The passphrase used to pack a v1 bundle. Omit for v2 recipient bundles (decrypted with this machine's keyring identity). Bad passphrases return an authentication error rather than wrong plaintext."
1628
2113
  ),
1629
- scope: scope3.default("global"),
1630
- projectPath: projectPath3,
1631
- teamId: teamId3,
1632
- orgId: orgId3,
1633
- dryRun: z5.boolean().optional().default(false).describe(
2114
+ scope: scope4.default("global"),
2115
+ projectPath: projectPath4,
2116
+ teamId: teamId4,
2117
+ orgId: orgId4,
2118
+ dryRun: z6.boolean().optional().default(false).describe(
1634
2119
  "If true, decrypt and report what would be written but do not mutate the keyring. Useful for verifying bundle contents before commit."
1635
2120
  )
1636
2121
  },
@@ -1639,11 +2124,34 @@ function registerTeleportTools(server2) {
1639
2124
  const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
1640
2125
  if (toolBlock) return toolBlock;
1641
2126
  try {
1642
- const payload = teleportUnpack(params.bundle, params.passphrase);
2127
+ const info = inspectTeleportBundle(params.bundle);
2128
+ let header = "";
2129
+ let identity = null;
2130
+ if (info.v === 2) {
2131
+ identity = loadTeleportIdentity();
2132
+ if (!identity) {
2133
+ throw new Error(
2134
+ "ERR_TELEPORT_NO_IDENTITY: this bundle is addressed to recipient keys and this machine has no teleport identity \u2014 run `qring teleport keygen` first"
2135
+ );
2136
+ }
2137
+ const ids = info.recipients.map((id) => id === identity.id ? `${id} (you)` : id).join(", ");
2138
+ header = `Addressed to recipient id(s): ${ids}
2139
+ `;
2140
+ } else if (params.passphrase === void 0) {
2141
+ throw new Error(
2142
+ "ERR_TELEPORT_PASSPHRASE_REQUIRED: this is a passphrase (v1) bundle \u2014 pass `passphrase`"
2143
+ );
2144
+ }
2145
+ const payload = teleportUnpackAuto(params.bundle, {
2146
+ passphrase: params.passphrase,
2147
+ identity: identity?.privateKey
2148
+ });
1643
2149
  if (params.dryRun) {
1644
2150
  const preview = payload.secrets.map((s) => `${s.key} [${s.scope ?? "global"}]`).join("\n");
1645
- return text(`Would import ${payload.secrets.length} secrets:
1646
- ${preview}`);
2151
+ return text(
2152
+ `${header}Would import ${payload.secrets.length} secrets:
2153
+ ${preview}`
2154
+ );
1647
2155
  }
1648
2156
  const o = opts(params);
1649
2157
  for (const s of payload.secrets) {
@@ -1659,8 +2167,8 @@ ${preview}`);
1659
2167
  }
1660
2168
 
1661
2169
  // src/mcp/tools/audit.ts
1662
- import { z as z6 } from "zod";
1663
- var { teamId: teamId4, orgId: orgId4, scope: scope4, projectPath: projectPath4 } = commonSchemas;
2170
+ import { z as z7 } from "zod";
2171
+ var { teamId: teamId5, orgId: orgId5, scope: scope5, projectPath: projectPath5 } = commonSchemas;
1664
2172
  function registerAuditTools(server2) {
1665
2173
  server2.tool(
1666
2174
  "audit_log",
@@ -1670,8 +2178,8 @@ function registerAuditTools(server2) {
1670
2178
  "Read-only. Returns one line per event in chronological order, formatted `timestamp | action | key | [scope] | env:NAME | detail`. Returns 'No audit events found' when the filter matches nothing."
1671
2179
  ].join(" "),
1672
2180
  {
1673
- key: z6.string().optional().describe("Limit to events touching this exact key. Omit for the full log."),
1674
- action: z6.enum([
2181
+ key: z7.string().optional().describe("Limit to events touching this exact key. Omit for the full log."),
2182
+ action: z7.enum([
1675
2183
  "read",
1676
2184
  "write",
1677
2185
  "delete",
@@ -1691,10 +2199,10 @@ function registerAuditTools(server2) {
1691
2199
  ]).optional().describe(
1692
2200
  "Limit to a single action verb (e.g. 'read' to see only reads). Omit for all actions."
1693
2201
  ),
1694
- agent: z6.string().optional().describe(
2202
+ agent: z7.string().optional().describe(
1695
2203
  "Limit to events stamped with this agent label (clientInfo name@version). Omit for all agents."
1696
2204
  ),
1697
- limit: z6.number().optional().default(20).describe(
2205
+ limit: z7.number().optional().default(20).describe(
1698
2206
  "Maximum events to return, newest first. Defaults to 20. Increase for deeper investigations."
1699
2207
  )
1700
2208
  },
@@ -1728,7 +2236,7 @@ function registerAuditTools(server2) {
1728
2236
  "Read-only; never mutates secrets or the audit log. Returns one line per finding formatted `[type] description`, or 'No anomalies detected' when the log looks clean."
1729
2237
  ].join(" "),
1730
2238
  {
1731
- key: z6.string().optional().describe(
2239
+ key: z7.string().optional().describe(
1732
2240
  "If provided, narrow the scan to this exact key. Omit to scan across every key in the audit log."
1733
2241
  )
1734
2242
  },
@@ -1750,10 +2258,10 @@ function registerAuditTools(server2) {
1750
2258
  "Read-only \u2014 never writes. Returns a multi-line text summary: header counts (Total / Healthy / Stale / Expired / No decay / Anomalies), then per-secret `EXPIRED:` / `STALE:` issue lines, then per-anomaly `[type] description` lines."
1751
2259
  ].join(" "),
1752
2260
  {
1753
- scope: scope4,
1754
- projectPath: projectPath4,
1755
- teamId: teamId4,
1756
- orgId: orgId4
2261
+ scope: scope5,
2262
+ projectPath: projectPath5,
2263
+ teamId: teamId5,
2264
+ orgId: orgId5
1757
2265
  },
1758
2266
  toolAnnotations("health_check"),
1759
2267
  async (params) => {
@@ -1821,13 +2329,13 @@ function registerAuditTools(server2) {
1821
2329
  "Read-only. Returns the rendered text directly (no JSON wrapper). 'jsonl' is one event per line; 'json' is a single array; 'csv' is a header row plus events. Time filters are applied to the event timestamps before formatting."
1822
2330
  ].join(" "),
1823
2331
  {
1824
- since: z6.string().optional().describe(
2332
+ since: z7.string().optional().describe(
1825
2333
  "Inclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound."
1826
2334
  ),
1827
- until: z6.string().optional().describe(
2335
+ until: z7.string().optional().describe(
1828
2336
  "Inclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound."
1829
2337
  ),
1830
- format: z6.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe(
2338
+ format: z7.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe(
1831
2339
  "Output format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly."
1832
2340
  )
1833
2341
  },
@@ -1848,8 +2356,8 @@ function registerAuditTools(server2) {
1848
2356
  }
1849
2357
 
1850
2358
  // src/mcp/tools/validation.ts
1851
- import { z as z7 } from "zod";
1852
- var { teamId: teamId5, orgId: orgId5, scope: scope5, projectPath: projectPath5 } = commonSchemas;
2359
+ import { z as z8 } from "zod";
2360
+ var { teamId: teamId6, orgId: orgId6, scope: scope6, projectPath: projectPath6 } = commonSchemas;
1853
2361
  function registerValidationTools(server2) {
1854
2362
  server2.tool(
1855
2363
  "validate_secret",
@@ -1859,16 +2367,16 @@ function registerValidationTools(server2) {
1859
2367
  "Side effects: makes one outbound network request per call (may incur tiny provider-side rate-limit cost). Records 'read' for the underlying secret value in the audit log; the value itself is never logged. Returns JSON `{ valid, provider, status?, message?, rateLimit?, ... }` (provider-specific shape)."
1860
2368
  ].join(" "),
1861
2369
  {
1862
- key: z7.string().describe(
2370
+ key: z8.string().describe(
1863
2371
  "The exact key whose value should be tested upstream. Example: 'OPENAI_API_KEY'."
1864
2372
  ),
1865
- provider: z7.string().optional().describe(
2373
+ provider: z8.string().optional().describe(
1866
2374
  "Force a specific provider id. Built-ins include 'openai', 'stripe', 'github', 'aws', 'http'. Omit to auto-detect from the value's prefix or the secret's stored provider hint."
1867
2375
  ),
1868
- scope: scope5,
1869
- projectPath: projectPath5,
1870
- teamId: teamId5,
1871
- orgId: orgId5
2376
+ scope: scope6,
2377
+ projectPath: projectPath6,
2378
+ teamId: teamId6,
2379
+ orgId: orgId6
1872
2380
  },
1873
2381
  toolAnnotations("validate_secret"),
1874
2382
  async (params) => {
@@ -1910,14 +2418,14 @@ function registerValidationTools(server2) {
1910
2418
  "Mutates the keyring with the newly-issued value if rotation succeeds (one 'write' audit event), and makes outbound network requests against the provider's rotation API. Returns JSON `{ rotated, newValue?, message?, ... }`. If `rotated` is false, the existing value is left untouched."
1911
2419
  ].join(" "),
1912
2420
  {
1913
- key: z7.string().describe("Exact key to rotate. Must already exist in the keyring."),
1914
- provider: z7.string().optional().describe(
2421
+ key: z8.string().describe("Exact key to rotate. Must already exist in the keyring."),
2422
+ provider: z8.string().optional().describe(
1915
2423
  "Force a specific provider id (see `list_providers`). Omit to auto-detect from the current value or the secret's stored provider hint."
1916
2424
  ),
1917
- scope: scope5,
1918
- projectPath: projectPath5,
1919
- teamId: teamId5,
1920
- orgId: orgId5
2425
+ scope: scope6,
2426
+ projectPath: projectPath6,
2427
+ teamId: teamId6,
2428
+ orgId: orgId6
1921
2429
  },
1922
2430
  toolAnnotations("rotate_secret"),
1923
2431
  async (params) => {
@@ -1944,10 +2452,10 @@ function registerValidationTools(server2) {
1944
2452
  "Side effects: one outbound request per validatable secret (cost scales with N). Reads each secret value (records 'read' audit events). Returns JSON `{ total, valid, invalid, results: [...] }` listing per-key status, provider, and error messages where applicable. Returns 'No secrets to validate' if nothing in scope has a provider mapping."
1945
2453
  ].join(" "),
1946
2454
  {
1947
- scope: scope5,
1948
- projectPath: projectPath5,
1949
- teamId: teamId5,
1950
- orgId: orgId5
2455
+ scope: scope6,
2456
+ projectPath: projectPath6,
2457
+ teamId: teamId6,
2458
+ orgId: orgId6
1951
2459
  },
1952
2460
  toolAnnotations("ci_validate_secrets"),
1953
2461
  async (params) => {
@@ -1976,7 +2484,7 @@ function registerValidationTools(server2) {
1976
2484
  }
1977
2485
 
1978
2486
  // src/mcp/tools/hooks.ts
1979
- import { z as z8 } from "zod";
2487
+ import { z as z9 } from "zod";
1980
2488
  function registerHookTools(server2) {
1981
2489
  server2.tool(
1982
2490
  "register_hook",
@@ -1986,33 +2494,33 @@ function registerHookTools(server2) {
1986
2494
  "Mutates the hook registry on disk. At least one match criterion (`key`, `keyPattern`, or `tag`) is required \u2014 calls without any return an error. Returns JSON of the registered hook entry including its assigned `id` (use that `id` with `remove_hook`)."
1987
2495
  ].join(" "),
1988
2496
  {
1989
- type: z8.enum(["shell", "http", "signal"]).describe(
2497
+ type: z9.enum(["shell", "http", "signal"]).describe(
1990
2498
  "Hook delivery mechanism. 'shell' runs a local command, 'http' POSTs JSON to a URL, 'signal' sends an OS signal to a named process."
1991
2499
  ),
1992
- key: z8.string().optional().describe(
2500
+ key: z9.string().optional().describe(
1993
2501
  "Trigger only on this exact key name. Pick at most one of `key` / `keyPattern` / `tag` (or combine for stricter matching)."
1994
2502
  ),
1995
- keyPattern: z8.string().optional().describe("Trigger on any key matching this glob pattern. Examples: 'DB_*', 'STRIPE_*'."),
1996
- tag: z8.string().optional().describe(
2503
+ keyPattern: z9.string().optional().describe("Trigger on any key matching this glob pattern. Examples: 'DB_*', 'STRIPE_*'."),
2504
+ tag: z9.string().optional().describe(
1997
2505
  "Trigger on any secret carrying this exact tag. Combinable with key/keyPattern as an AND filter."
1998
2506
  ),
1999
- scope: z8.enum(["global", "project"]).optional().describe(
2507
+ scope: z9.enum(["global", "project"]).optional().describe(
2000
2508
  "Restrict the hook to secrets in this scope. Omit to fire across both global and project secrets."
2001
2509
  ),
2002
- actions: z8.array(z8.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe("Which lifecycle actions trigger this hook. Defaults to all three."),
2003
- command: z8.string().optional().describe(
2510
+ actions: z9.array(z9.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe("Which lifecycle actions trigger this hook. Defaults to all three."),
2511
+ command: z9.string().optional().describe(
2004
2512
  "Required when type='shell'. The literal shell command to run; q-ring exposes the matching key as $QRING_HOOK_KEY and action as $QRING_HOOK_ACTION."
2005
2513
  ),
2006
- url: z8.string().optional().describe(
2514
+ url: z9.string().optional().describe(
2007
2515
  "Required when type='http'. Full URL to POST a JSON body `{ id, key, scope, action, timestamp }` to (the value itself is never sent)."
2008
2516
  ),
2009
- signalTarget: z8.string().optional().describe(
2517
+ signalTarget: z9.string().optional().describe(
2010
2518
  "Required when type='signal'. Either a numeric PID or a process name resolvable via `ps`."
2011
2519
  ),
2012
- signalName: z8.string().optional().default("SIGHUP").describe(
2520
+ signalName: z9.string().optional().default("SIGHUP").describe(
2013
2521
  "Signal name to send (e.g. 'SIGHUP', 'SIGUSR1'). Defaults to SIGHUP, which most daemons treat as 'reload config'."
2014
2522
  ),
2015
- description: z8.string().optional().describe(
2523
+ description: z9.string().optional().describe(
2016
2524
  "Free-text human-readable description, surfaced by `list_hooks` and the dashboard."
2017
2525
  )
2018
2526
  },
@@ -2066,7 +2574,7 @@ function registerHookTools(server2) {
2066
2574
  "Mutates the hook registry only \u2014 does not touch secret values, audit log, or env states. Idempotent in spirit: removing an already-absent id returns a not-found error rather than partial work. Returns 'Removed hook ID' on success."
2067
2575
  ].join(" "),
2068
2576
  {
2069
- id: z8.string().describe(
2577
+ id: z9.string().describe(
2070
2578
  "Hook id returned by `register_hook` or visible in `list_hooks` (opaque string)."
2071
2579
  )
2072
2580
  },
@@ -2084,7 +2592,7 @@ function registerHookTools(server2) {
2084
2592
  }
2085
2593
 
2086
2594
  // src/mcp/tools/tooling.ts
2087
- import { z as z9 } from "zod";
2595
+ import { z as z10 } from "zod";
2088
2596
 
2089
2597
  // src/utils/colors.ts
2090
2598
  var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
@@ -2270,9 +2778,9 @@ var RedactionTransform = class extends Transform {
2270
2778
  callback();
2271
2779
  }
2272
2780
  };
2273
- function enforceExecPolicy(profile, command, args, projectPath8) {
2781
+ function enforceExecPolicy(profile, command, args, projectPath9) {
2274
2782
  const fullCommand = [command, ...args].join(" ");
2275
- const policyDecision = checkExecPolicy(fullCommand, projectPath8);
2783
+ const policyDecision = checkExecPolicy(fullCommand, projectPath9);
2276
2784
  if (!policyDecision.allowed) {
2277
2785
  throw new Error(`Policy Denied: ${policyDecision.reason}`);
2278
2786
  }
@@ -2647,7 +3155,7 @@ function lintFiles(files, opts2 = {}) {
2647
3155
  }
2648
3156
 
2649
3157
  // src/mcp/tools/tooling.ts
2650
- var { teamId: teamId6, orgId: orgId6, scope: scope6, projectPath: projectPath6 } = commonSchemas;
3158
+ var { teamId: teamId7, orgId: orgId7, scope: scope7, projectPath: projectPath7 } = commonSchemas;
2651
3159
  function registerToolingTools(server2) {
2652
3160
  server2.tool(
2653
3161
  "exec_with_secrets",
@@ -2657,25 +3165,25 @@ function registerToolingTools(server2) {
2657
3165
  "Spawns a real child process \u2014 has whatever side effects the command itself causes (writes, network, exec). Subject to BOTH tool policy and exec policy (allowlist/denylist). Returns a text body with `Exit code: N` then `STDOUT:` and `STDERR:` blocks; both streams are scrubbed against the secret values that were injected."
2658
3166
  ].join(" "),
2659
3167
  {
2660
- command: z9.string().describe(
3168
+ command: z10.string().describe(
2661
3169
  "Executable name or full command to run. Example: 'pnpm', 'node', '/usr/bin/env'. Must be allowed by exec policy."
2662
3170
  ),
2663
- args: z9.array(z9.string()).optional().describe(
3171
+ args: z10.array(z10.string()).optional().describe(
2664
3172
  "Positional arguments passed to `command`. Example: ['run', 'db:migrate']. Each element is passed verbatim with no extra shell parsing."
2665
3173
  ),
2666
- keys: z9.array(z9.string()).optional().describe(
3174
+ keys: z10.array(z10.string()).optional().describe(
2667
3175
  "Whitelist of exact key names to inject. Omit to inject every secret in scope (subject to `tags`)."
2668
3176
  ),
2669
- tags: z9.array(z9.string()).optional().describe(
3177
+ tags: z10.array(z10.string()).optional().describe(
2670
3178
  "Inject only secrets carrying at least one of these tags. Combinable with `keys` as an AND filter."
2671
3179
  ),
2672
- profile: z9.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe(
3180
+ profile: z10.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe(
2673
3181
  "Exec sandbox profile. 'restricted' (default) denies network-tool binaries (curl, wget, ssh, scp, nc, netcat, ncat) AND common interpreters/shells (python, node, deno, bun, perl, ruby, php, sh, bash, zsh) \u2014 since those could otherwise egress the injected secrets \u2014 strips proxy env vars, and caps runtime at 30s. It still is NOT a real OS sandbox (it does not restrict PATH, and some allowed binary could in principle make network calls); for genuinely untrusted commands use OS-level isolation (containers, network namespaces). 'ci' allows network and interpreters with a 300s cap and blocks a few destructive commands; 'unrestricted' inherits the full server environment. Define a custom profile in .q-ring.json to allow specific interpreters with secrets."
2674
3182
  ),
2675
- scope: scope6,
2676
- projectPath: projectPath6,
2677
- teamId: teamId6,
2678
- orgId: orgId6
3183
+ scope: scope7,
3184
+ projectPath: projectPath7,
3185
+ teamId: teamId7,
3186
+ orgId: orgId7
2679
3187
  },
2680
3188
  toolAnnotations("exec_with_secrets"),
2681
3189
  async (params) => {
@@ -2717,7 +3225,7 @@ ${result.stderr}`);
2717
3225
  "Read-only \u2014 never modifies source files. Honors `.gitignore`. Returns JSON array of `{ file, line, key, value, kind }` findings, or 'No hardcoded secrets found in the specified directory.' when clean. False positives are possible \u2014 review before treating as ground truth."
2718
3226
  ].join(" "),
2719
3227
  {
2720
- dirPath: z9.string().describe(
3228
+ dirPath: z10.string().describe(
2721
3229
  "Directory to scan, absolute or relative to the server cwd. The scan recurses into subdirectories."
2722
3230
  )
2723
3231
  },
@@ -2744,14 +3252,14 @@ ${result.stderr}`);
2744
3252
  "With `fix: false` this is read-only. With `fix: true` this MUTATES the listed source files in place (review with git diff!) and writes one new secret per finding to the keyring. Returns a JSON array of `{ file, line, key, value, kind }` findings, or 'No hardcoded secrets found in the specified files.'."
2745
3253
  ].join(" "),
2746
3254
  {
2747
- files: z9.array(z9.string()).describe("Absolute or relative paths to lint. Non-existent paths surface as scan errors."),
2748
- fix: z9.boolean().optional().default(false).describe(
3255
+ files: z10.array(z10.string()).describe("Absolute or relative paths to lint. Non-existent paths surface as scan errors."),
3256
+ fix: z10.boolean().optional().default(false).describe(
2749
3257
  "If true, rewrite the source files to read `process.env.KEY` and store the extracted value in the keyring. If false (default), only report findings."
2750
3258
  ),
2751
- scope: scope6,
2752
- projectPath: projectPath6,
2753
- teamId: teamId6,
2754
- orgId: orgId6
3259
+ scope: scope7,
3260
+ projectPath: projectPath7,
3261
+ teamId: teamId7,
3262
+ orgId: orgId7
2755
3263
  },
2756
3264
  toolAnnotations("lint_files"),
2757
3265
  async (params) => {
@@ -2780,10 +3288,10 @@ ${result.stderr}`);
2780
3288
  "Read-only; uses the most recent ~500 audit events. Returns JSON `{ total, expired, stale, neverAccessed: [...], noRotationFormat: [...], mostAccessed: [{ key, reads }] }`. `neverAccessed` and `noRotationFormat` are good candidates for cleanup or for adding rotation hints."
2781
3289
  ].join(" "),
2782
3290
  {
2783
- scope: scope6,
2784
- projectPath: projectPath6,
2785
- teamId: teamId6,
2786
- orgId: orgId6
3291
+ scope: scope7,
3292
+ projectPath: projectPath7,
3293
+ teamId: teamId7,
3294
+ orgId: orgId7
2787
3295
  },
2788
3296
  toolAnnotations("analyze_secrets"),
2789
3297
  async (params) => {
@@ -2818,7 +3326,7 @@ ${result.stderr}`);
2818
3326
  "Side effect: binds an HTTP server on the requested port (one process-wide instance \u2014 re-running returns the existing URL instead of starting a second server). Never exposes secret values. Returns the URL string to open in a browser."
2819
3327
  ].join(" "),
2820
3328
  {
2821
- port: z9.number().optional().default(9876).describe(
3329
+ port: z10.number().optional().default(9876).describe(
2822
3330
  "TCP port to listen on (default 9876). Pick another port if 9876 is already in use; the call fails if binding errors."
2823
3331
  )
2824
3332
  },
@@ -2829,7 +3337,7 @@ ${result.stderr}`);
2829
3337
  if (dashboardInstance) {
2830
3338
  return text(`Dashboard already running at ${dashboardInstance.url}`);
2831
3339
  }
2832
- const { startDashboardServer } = await import("./dashboard-K7LYTWSL.js");
3340
+ const { startDashboardServer } = await import("./dashboard-OEHI6OOD.js");
2833
3341
  dashboardInstance = startDashboardServer({ port: params.port });
2834
3342
  return text(
2835
3343
  `Dashboard started at ${dashboardInstance.url}
@@ -2845,10 +3353,10 @@ Open this URL in a browser to see live quantum status. The token is required for
2845
3353
  "With `autoRotate=false` (default) this is read-only. With `autoRotate=true` it OVERWRITES expired secret values in the keyring with generated replacements \u2014 credential changes that may break upstream integrations until they are propagated. Subject to tool policy. Returns a JSON report of per-project findings and any rotations performed."
2846
3354
  ].join(" "),
2847
3355
  {
2848
- autoRotate: z9.boolean().optional().default(false).describe(
3356
+ autoRotate: z10.boolean().optional().default(false).describe(
2849
3357
  "If true, replace expired secrets with newly generated values (using each secret's `rotationFormat`/`rotationPrefix`). Only enable when intentional rotation is desired \u2014 this is destructive on the upstream side."
2850
3358
  ),
2851
- projectPaths: z9.array(z9.string()).optional().describe(
3359
+ projectPaths: z10.array(z10.string()).optional().describe(
2852
3360
  "List of absolute project roots to scan. Defaults to `[server.cwd]` when omitted."
2853
3361
  )
2854
3362
  },
@@ -2866,7 +3374,7 @@ Open this URL in a browser to see live quantum status. The token is required for
2866
3374
  }
2867
3375
 
2868
3376
  // src/mcp/tools/agent.ts
2869
- import { z as z10 } from "zod";
3377
+ import { z as z11 } from "zod";
2870
3378
  function registerAgentTools(server2) {
2871
3379
  server2.tool(
2872
3380
  "agent_remember",
@@ -2876,10 +3384,10 @@ function registerAgentTools(server2) {
2876
3384
  `Mutates the encrypted memory store. Idempotent: rewriting the same key with a new value simply overwrites. Returns 'Remembered "KEY"' on success.`
2877
3385
  ].join(" "),
2878
3386
  {
2879
- key: z10.string().describe(
3387
+ key: z11.string().describe(
2880
3388
  "Memory key (free-form string). Convention: lowercase dotted namespaces, e.g. 'project.lastDeploy'."
2881
3389
  ),
2882
- value: z10.string().describe(
3390
+ value: z11.string().describe(
2883
3391
  "Plain-string value to store. JSON-stringify structured data on the caller side if needed."
2884
3392
  )
2885
3393
  },
@@ -2899,7 +3407,7 @@ function registerAgentTools(server2) {
2899
3407
  "Read-only. With a `key` argument: returns JSON `{ ok, data: { key, value } }` or a not-found error. Without `key`: returns a JSON listing of every stored key (no values), or 'Agent memory is empty'."
2900
3408
  ].join(" "),
2901
3409
  {
2902
- key: z10.string().optional().describe("Memory key to read. Omit to list every stored key (without values).")
3410
+ key: z11.string().optional().describe("Memory key to read. Omit to list every stored key (without values).")
2903
3411
  },
2904
3412
  toolAnnotations("agent_recall"),
2905
3413
  async (params) => {
@@ -2923,7 +3431,7 @@ function registerAgentTools(server2) {
2923
3431
  `Destructive: there is no recycle bin. Returns 'Forgot "KEY"' on success or a not-found error if the key was already absent.`
2924
3432
  ].join(" "),
2925
3433
  {
2926
- key: z10.string().describe("Memory key to delete.")
3434
+ key: z11.string().describe("Memory key to delete.")
2927
3435
  },
2928
3436
  toolAnnotations("agent_forget"),
2929
3437
  async (params) => {
@@ -2939,8 +3447,8 @@ function registerAgentTools(server2) {
2939
3447
  }
2940
3448
 
2941
3449
  // src/mcp/tools/policy.ts
2942
- import { z as z11 } from "zod";
2943
- var { projectPath: projectPath7 } = commonSchemas;
3450
+ import { z as z12 } from "zod";
3451
+ var { projectPath: projectPath8 } = commonSchemas;
2944
3452
  function registerPolicyTools(server2) {
2945
3453
  server2.tool(
2946
3454
  "check_policy",
@@ -2950,15 +3458,15 @@ function registerPolicyTools(server2) {
2950
3458
  "Read-only. Returns JSON `{ allowed, reason?, policySource }` describing the decision. Returns an error 'Missing required parameter for the selected action type' if the matching argument for the chosen `action` is not supplied."
2951
3459
  ].join(" "),
2952
3460
  {
2953
- action: z11.enum(["tool", "key_read", "exec"]).describe(
3461
+ action: z12.enum(["tool", "key_read", "exec"]).describe(
2954
3462
  "Which policy surface to query. 'tool' = MCP tool gate (needs `toolName`); 'key_read' = secret read gate (needs `key`); 'exec' = exec_with_secrets command gate (needs `command`)."
2955
3463
  ),
2956
- toolName: z11.string().optional().describe("Tool id to evaluate, e.g. 'rotate_secret'. Required when `action` is 'tool'."),
2957
- key: z11.string().optional().describe("Secret key name to evaluate. Required when `action` is 'key_read'."),
2958
- command: z11.string().optional().describe(
3464
+ toolName: z12.string().optional().describe("Tool id to evaluate, e.g. 'rotate_secret'. Required when `action` is 'tool'."),
3465
+ key: z12.string().optional().describe("Secret key name to evaluate. Required when `action` is 'key_read'."),
3466
+ command: z12.string().optional().describe(
2959
3467
  "Command to evaluate against the exec allowlist/denylist. Required when `action` is 'exec'."
2960
3468
  ),
2961
- projectPath: projectPath7
3469
+ projectPath: projectPath8
2962
3470
  },
2963
3471
  toolAnnotations("check_policy"),
2964
3472
  async (params) => {
@@ -2985,7 +3493,7 @@ function registerPolicyTools(server2) {
2985
3493
  "Read-only. Returns pretty-printed JSON; missing policy file returns an empty/default summary rather than an error so callers can branch on the counts."
2986
3494
  ].join(" "),
2987
3495
  {
2988
- projectPath: projectPath7
3496
+ projectPath: projectPath8
2989
3497
  },
2990
3498
  toolAnnotations("get_policy_summary"),
2991
3499
  async (params) => {
@@ -3062,6 +3570,7 @@ function registerMcpResources(server2) {
3062
3570
  function registerMcpTools(server2) {
3063
3571
  registerMcpResources(server2);
3064
3572
  registerSecretTools(server2);
3573
+ registerEnvironmentTools(server2);
3065
3574
  registerProjectTools(server2);
3066
3575
  registerTunnelTools(server2);
3067
3576
  registerTeleportTools(server2);