@i4ctime/q-ring 0.17.5 → 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/README.md +52 -10
- package/dist/{chunk-VMDD5QTJ.js → chunk-NTCTVLSK.js} +126 -38
- package/dist/chunk-NTCTVLSK.js.map +1 -0
- package/dist/{chunk-5ZCQSBVS.js → chunk-ZBPMEJRO.js} +142 -38
- package/dist/chunk-ZBPMEJRO.js.map +1 -0
- package/dist/dashboard-LICF6OZ5.js +699 -0
- package/dist/dashboard-LICF6OZ5.js.map +1 -0
- package/dist/dashboard-OEHI6OOD.js +699 -0
- package/dist/dashboard-OEHI6OOD.js.map +1 -0
- package/dist/index.js +833 -82
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +681 -172
- package/dist/mcp.js.map +1 -1
- package/package.json +10 -10
- package/dist/chunk-5ZCQSBVS.js.map +0 -1
- package/dist/chunk-VMDD5QTJ.js.map +0 -1
- package/dist/dashboard-K7LYTWSL.js +0 -685
- package/dist/dashboard-K7LYTWSL.js.map +0 -1
- package/dist/dashboard-ZN3FPR73.js +0 -685
- package/dist/dashboard-ZN3FPR73.js.map +0 -1
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-
|
|
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,
|
|
331
|
-
const decision = checkToolPolicy(toolName,
|
|
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
|
|
1118
|
-
const envResult = collapseEnvironment({ projectPath:
|
|
1289
|
+
const projectPath9 = opts2.projectPath ?? process.cwd();
|
|
1290
|
+
const envResult = collapseEnvironment({ projectPath: projectPath9 });
|
|
1119
1291
|
const secretsList = listSecrets({
|
|
1120
1292
|
...opts2,
|
|
1121
|
-
projectPath:
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1340
|
-
projectPath:
|
|
1341
|
-
teamId:
|
|
1342
|
-
orgId:
|
|
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
|
|
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:
|
|
1366
|
-
ttlSeconds:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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 =
|
|
1473
|
-
v:
|
|
1474
|
-
data:
|
|
1475
|
-
salt:
|
|
1476
|
-
iv:
|
|
1477
|
-
tag:
|
|
1478
|
-
createdAt:
|
|
1479
|
-
count:
|
|
1480
|
-
iter:
|
|
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 =
|
|
1483
|
-
secrets:
|
|
1484
|
-
|
|
1485
|
-
key:
|
|
1486
|
-
value:
|
|
1487
|
-
scope:
|
|
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:
|
|
1491
|
-
exportedBy:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1896
|
+
return createPrivateKey(identity);
|
|
1562
1897
|
} catch {
|
|
1563
|
-
throw new Error("
|
|
1898
|
+
throw new Error("ERR_TELEPORT_BAD_IDENTITY: private key is not a valid PEM");
|
|
1564
1899
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
|
1907
|
+
`ERR_TELEPORT_CORRUPT: invalid bundle shape (${parsedBundle.error.message})`
|
|
1569
1908
|
);
|
|
1570
1909
|
}
|
|
1571
|
-
|
|
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:
|
|
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
|
-
"
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
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
|
|
1620
|
-
"
|
|
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:
|
|
2108
|
+
bundle: z6.string().describe(
|
|
1624
2109
|
"Base64-encoded ciphertext returned by `teleport_pack`. Pass through whitespace untouched if possible."
|
|
1625
2110
|
),
|
|
1626
|
-
passphrase:
|
|
1627
|
-
"The
|
|
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:
|
|
1630
|
-
projectPath:
|
|
1631
|
-
teamId:
|
|
1632
|
-
orgId:
|
|
1633
|
-
dryRun:
|
|
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
|
|
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(
|
|
1646
|
-
${
|
|
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
|
|
1663
|
-
var { teamId:
|
|
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:
|
|
1674
|
-
action:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1754
|
-
projectPath:
|
|
1755
|
-
teamId:
|
|
1756
|
-
orgId:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
1852
|
-
var { teamId:
|
|
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:
|
|
2370
|
+
key: z8.string().describe(
|
|
1863
2371
|
"The exact key whose value should be tested upstream. Example: 'OPENAI_API_KEY'."
|
|
1864
2372
|
),
|
|
1865
|
-
provider:
|
|
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:
|
|
1869
|
-
projectPath:
|
|
1870
|
-
teamId:
|
|
1871
|
-
orgId:
|
|
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:
|
|
1914
|
-
provider:
|
|
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:
|
|
1918
|
-
projectPath:
|
|
1919
|
-
teamId:
|
|
1920
|
-
orgId:
|
|
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:
|
|
1948
|
-
projectPath:
|
|
1949
|
-
teamId:
|
|
1950
|
-
orgId:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
1996
|
-
tag:
|
|
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:
|
|
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:
|
|
2003
|
-
command:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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,
|
|
2781
|
+
function enforceExecPolicy(profile, command, args, projectPath9) {
|
|
2274
2782
|
const fullCommand = [command, ...args].join(" ");
|
|
2275
|
-
const policyDecision = checkExecPolicy(fullCommand,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
2676
|
-
projectPath:
|
|
2677
|
-
teamId:
|
|
2678
|
-
orgId:
|
|
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:
|
|
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:
|
|
2748
|
-
fix:
|
|
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:
|
|
2752
|
-
projectPath:
|
|
2753
|
-
teamId:
|
|
2754
|
-
orgId:
|
|
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:
|
|
2784
|
-
projectPath:
|
|
2785
|
-
teamId:
|
|
2786
|
-
orgId:
|
|
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:
|
|
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-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
3387
|
+
key: z11.string().describe(
|
|
2880
3388
|
"Memory key (free-form string). Convention: lowercase dotted namespaces, e.g. 'project.lastDeploy'."
|
|
2881
3389
|
),
|
|
2882
|
-
value:
|
|
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:
|
|
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:
|
|
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
|
|
2943
|
-
var { projectPath:
|
|
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:
|
|
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:
|
|
2957
|
-
key:
|
|
2958
|
-
command:
|
|
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:
|
|
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:
|
|
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);
|