@i4ctime/q-ring 0.11.5 → 0.12.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 +55 -51
- package/dist/{chunk-A4RZVP3P.js → chunk-5TQNDJT3.js} +160 -90
- package/dist/chunk-5TQNDJT3.js.map +1 -0
- package/dist/{chunk-MQM4DLPI.js → chunk-YWDKAZWS.js} +165 -90
- package/dist/chunk-YWDKAZWS.js.map +1 -0
- package/dist/dashboard-6XKC5BL4.js +630 -0
- package/dist/dashboard-6XKC5BL4.js.map +1 -0
- package/dist/dashboard-HHJY73JX.js +630 -0
- package/dist/dashboard-HHJY73JX.js.map +1 -0
- package/dist/index.js +45 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +516 -166
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -4
- package/dist/chunk-A4RZVP3P.js.map +0 -1
- package/dist/chunk-MQM4DLPI.js.map +0 -1
- package/dist/dashboard-FSCJDLJX.js +0 -1313
- package/dist/dashboard-FSCJDLJX.js.map +0 -1
- package/dist/dashboard-OXUD62RV.js +0 -1313
- package/dist/dashboard-OXUD62RV.js.map +0 -1
package/dist/mcp.js
CHANGED
|
@@ -32,13 +32,14 @@ import {
|
|
|
32
32
|
registry,
|
|
33
33
|
remember,
|
|
34
34
|
removeHook,
|
|
35
|
+
setPolicyRoot,
|
|
35
36
|
setSecret,
|
|
36
37
|
tunnelCreate,
|
|
37
38
|
tunnelDestroy,
|
|
38
39
|
tunnelList,
|
|
39
40
|
tunnelRead,
|
|
40
41
|
verifyAuditChain
|
|
41
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-YWDKAZWS.js";
|
|
42
43
|
|
|
43
44
|
// src/mcp.ts
|
|
44
45
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -118,24 +119,28 @@ function generateSecret(opts2 = {}) {
|
|
|
118
119
|
}
|
|
119
120
|
case "password": {
|
|
120
121
|
const len = opts2.length ?? 24;
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
const classCharsets = [
|
|
123
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
124
|
+
"abcdefghijklmnopqrstuvwxyz",
|
|
125
|
+
"0123456789",
|
|
126
|
+
"!@#$%^&*()-_=+"
|
|
127
|
+
];
|
|
128
|
+
const guaranteed = classCharsets.slice(0, Math.min(classCharsets.length, len)).map((cs) => randomString(cs, 1));
|
|
129
|
+
const remaining = Math.max(0, len - guaranteed.length);
|
|
130
|
+
const chars = [
|
|
131
|
+
...guaranteed,
|
|
132
|
+
...remaining > 0 ? randomString(PASSWORD_CHARS, remaining).split("") : []
|
|
133
|
+
];
|
|
134
|
+
for (let i = chars.length - 1; i > 0; i--) {
|
|
135
|
+
const j = randomInt(i + 1);
|
|
136
|
+
[chars[i], chars[j]] = [chars[j], chars[i]];
|
|
137
|
+
}
|
|
138
|
+
return chars.join("");
|
|
131
139
|
}
|
|
132
140
|
default:
|
|
133
141
|
return randomBytes(32).toString("hex");
|
|
134
142
|
}
|
|
135
143
|
}
|
|
136
|
-
function replaceAt(str, index, char) {
|
|
137
|
-
return str.slice(0, index) + char + str.slice(index + 1);
|
|
138
|
-
}
|
|
139
144
|
function estimateEntropy(secret) {
|
|
140
145
|
const charsets = [
|
|
141
146
|
{ regex: /[a-z]/, size: 26 },
|
|
@@ -173,8 +178,11 @@ function parseDotenv(content) {
|
|
|
173
178
|
'"': '"'
|
|
174
179
|
};
|
|
175
180
|
value = value.replace(/\\([nrt"\\])/g, (_, ch) => escapeMap[ch] ?? ch);
|
|
176
|
-
if (
|
|
177
|
-
|
|
181
|
+
if (!line.includes('"') && !line.includes("'")) {
|
|
182
|
+
const commentMatch = value.match(/\s#/);
|
|
183
|
+
if (commentMatch && commentMatch.index !== void 0) {
|
|
184
|
+
value = value.slice(0, commentMatch.index).trim();
|
|
185
|
+
}
|
|
178
186
|
}
|
|
179
187
|
if (key) result.set(key, value);
|
|
180
188
|
}
|
|
@@ -182,9 +190,14 @@ function parseDotenv(content) {
|
|
|
182
190
|
}
|
|
183
191
|
function importDotenv(filePathOrContent, options = {}) {
|
|
184
192
|
let content;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
const source = options.source ?? "cli";
|
|
194
|
+
if (source === "cli") {
|
|
195
|
+
try {
|
|
196
|
+
content = readFileSync(filePathOrContent, "utf8");
|
|
197
|
+
} catch {
|
|
198
|
+
content = filePathOrContent;
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
188
201
|
content = filePathOrContent;
|
|
189
202
|
}
|
|
190
203
|
const pairs = parseDotenv(content);
|
|
@@ -246,11 +259,21 @@ function enforceToolPolicy(toolName, projectPath8) {
|
|
|
246
259
|
return null;
|
|
247
260
|
}
|
|
248
261
|
var commonSchemas = {
|
|
249
|
-
teamId: z.string().optional().describe(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
teamId: z.string().optional().describe(
|
|
263
|
+
"Team identifier for team-scoped secrets. Required only when scope='team'. Example: 'acme-platform'."
|
|
264
|
+
),
|
|
265
|
+
orgId: z.string().optional().describe(
|
|
266
|
+
"Organization identifier for org-scoped secrets. Required only when scope='org'. Example: 'acme-corp'."
|
|
267
|
+
),
|
|
268
|
+
scope: z.enum(["global", "project", "team", "org"]).optional().describe(
|
|
269
|
+
"Where the secret lives. 'global' = user keyring (default if omitted on reads), 'project' = scoped to projectPath, 'team' = team-shared (needs teamId), 'org' = org-shared (needs orgId)."
|
|
270
|
+
),
|
|
271
|
+
projectPath: z.string().optional().describe(
|
|
272
|
+
"Absolute path to the project root for project-scoped secrets and policy resolution. Defaults to the MCP server's current working directory when omitted."
|
|
273
|
+
),
|
|
274
|
+
env: z.string().optional().describe(
|
|
275
|
+
"Environment slug used to collapse superposition when a secret has multiple per-env states. Examples: 'dev', 'staging', 'prod'. If omitted, the secret's defaultEnv is used."
|
|
276
|
+
)
|
|
254
277
|
};
|
|
255
278
|
|
|
256
279
|
// src/mcp/tools/secrets.ts
|
|
@@ -258,9 +281,15 @@ var { teamId, orgId, scope, projectPath, env } = commonSchemas;
|
|
|
258
281
|
function registerSecretTools(server2) {
|
|
259
282
|
server2.tool(
|
|
260
283
|
"get_secret",
|
|
261
|
-
|
|
284
|
+
[
|
|
285
|
+
"[secrets] Read the plaintext value of a single secret from the q-ring keyring.",
|
|
286
|
+
"Use when an agent needs the actual credential to call an external API or inject into a runtime; prefer `inspect_secret` to see metadata only, `has_secret` for presence-only checks, and `exec_with_secrets` to run a command without exposing the value to chat.",
|
|
287
|
+
"Side effects: collapses superposition (selects the per-env state) and writes a 'read' event to the audit log (observer effect). Subject to project tool/key policy and may be denied with a 'Policy Denied' message. Returns JSON `{ ok, data: { key, value } }` on success or an error message if missing/blocked."
|
|
288
|
+
].join(" "),
|
|
262
289
|
{
|
|
263
|
-
key: z2.string().describe(
|
|
290
|
+
key: z2.string().describe(
|
|
291
|
+
"Exact secret key name as stored in the keyring (case-sensitive). Example: 'OPENAI_API_KEY'."
|
|
292
|
+
),
|
|
264
293
|
scope,
|
|
265
294
|
projectPath,
|
|
266
295
|
env,
|
|
@@ -291,14 +320,26 @@ function registerSecretTools(server2) {
|
|
|
291
320
|
);
|
|
292
321
|
server2.tool(
|
|
293
322
|
"list_secrets",
|
|
294
|
-
|
|
323
|
+
[
|
|
324
|
+
"[secrets] List secret keys and quantum metadata in the requested scope, never the values.",
|
|
325
|
+
"Use to discover what secrets exist before reading or writing; pair with `inspect_secret` for full metadata on one key, `analyze_secrets` for usage trends, or `health_check` for decay/anomaly summaries.",
|
|
326
|
+
"Read-only; safe to call repeatedly. Returns JSON `{ ok, data: { entries: [...] } }` where each entry has scope, key, stateKeys (env names if superposed), expired, stale, lifetimePercent, timeRemaining, entangledCount, accessCount."
|
|
327
|
+
].join(" "),
|
|
295
328
|
{
|
|
296
329
|
scope,
|
|
297
330
|
projectPath,
|
|
298
|
-
tag: z2.string().optional().describe(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
331
|
+
tag: z2.string().optional().describe(
|
|
332
|
+
"Return only secrets that include this exact tag (case-sensitive). Example: 'production'."
|
|
333
|
+
),
|
|
334
|
+
expired: z2.boolean().optional().describe(
|
|
335
|
+
"If true, return only secrets whose decay TTL has elapsed (lifetimePercent >= 100)."
|
|
336
|
+
),
|
|
337
|
+
stale: z2.boolean().optional().describe(
|
|
338
|
+
"If true, return only secrets in the stale window (lifetimePercent >= 75 and not yet expired)."
|
|
339
|
+
),
|
|
340
|
+
filter: z2.string().optional().describe(
|
|
341
|
+
"Glob pattern matched against the key name. Supports `*` and `?`. Examples: 'API_*', 'STRIPE_?_KEY'."
|
|
342
|
+
),
|
|
302
343
|
teamId,
|
|
303
344
|
orgId
|
|
304
345
|
},
|
|
@@ -338,18 +379,32 @@ function registerSecretTools(server2) {
|
|
|
338
379
|
);
|
|
339
380
|
server2.tool(
|
|
340
381
|
"set_secret",
|
|
341
|
-
|
|
382
|
+
[
|
|
383
|
+
"[secrets] Create or overwrite a single secret value, optionally with TTL/decay, per-env superposition, description, tags, and rotation hints.",
|
|
384
|
+
"Use to add or update one key at a time; prefer `import_dotenv` for bulk .env ingest, `generate_secret` (with saveAs) to generate-and-store in one step, and `entangle_secrets` instead of duplicating the same value under two keys.",
|
|
385
|
+
"Mutates the keyring (overwrites any existing value at the same key/scope), writes a 'write' event to the audit log, and triggers any matching hooks. Subject to tool policy. Returns a short confirmation text like '[scope] KEY saved' (or '[scope] KEY set for env:NAME' when `env` is provided)."
|
|
386
|
+
].join(" "),
|
|
342
387
|
{
|
|
343
|
-
key: z2.string().describe(
|
|
344
|
-
|
|
388
|
+
key: z2.string().describe(
|
|
389
|
+
"Secret key name (UPPER_SNAKE_CASE recommended). Example: 'STRIPE_SECRET_KEY'."
|
|
390
|
+
),
|
|
391
|
+
value: z2.string().describe(
|
|
392
|
+
"The secret value to store. Stored as-is; never logged or echoed. May be empty only when `env` is provided to register a new env without a default."
|
|
393
|
+
),
|
|
345
394
|
scope: scope.default("global"),
|
|
346
395
|
projectPath,
|
|
347
396
|
env: z2.string().optional().describe(
|
|
348
|
-
"If
|
|
397
|
+
"If set, writes this value to the named per-env state (superposition) instead of the default slot. Existing default value is preserved as state 'default'. Example: 'prod'."
|
|
398
|
+
),
|
|
399
|
+
ttlSeconds: z2.number().optional().describe(
|
|
400
|
+
"Quantum decay window in seconds. After this many seconds the secret is marked expired (still readable, but `has_secret` returns false and `health_check` flags it). Omit for no decay."
|
|
401
|
+
),
|
|
402
|
+
description: z2.string().optional().describe(
|
|
403
|
+
"Free-text human-readable description shown in `inspect_secret` and the dashboard."
|
|
404
|
+
),
|
|
405
|
+
tags: z2.array(z2.string()).optional().describe(
|
|
406
|
+
"Tag list for filtering and hook matching. Example: ['production', 'payments']."
|
|
349
407
|
),
|
|
350
|
-
ttlSeconds: z2.number().optional().describe("Time-to-live in seconds (quantum decay)"),
|
|
351
|
-
description: z2.string().optional().describe("Human-readable description"),
|
|
352
|
-
tags: z2.array(z2.string()).optional().describe("Tags for organization"),
|
|
353
408
|
rotationFormat: z2.enum([
|
|
354
409
|
"hex",
|
|
355
410
|
"base64",
|
|
@@ -358,8 +413,12 @@ function registerSecretTools(server2) {
|
|
|
358
413
|
"api-key",
|
|
359
414
|
"token",
|
|
360
415
|
"password"
|
|
361
|
-
]).optional().describe(
|
|
362
|
-
|
|
416
|
+
]).optional().describe(
|
|
417
|
+
"Format used by `agent_scan --autoRotate` and `rotate_secret` when this secret expires. Pick the format that matches the upstream service's accepted shape."
|
|
418
|
+
),
|
|
419
|
+
rotationPrefix: z2.string().optional().describe(
|
|
420
|
+
"Literal prefix prepended on auto-rotation (only used with rotationFormat 'api-key' or 'token'). Example: 'sk-'."
|
|
421
|
+
),
|
|
363
422
|
teamId,
|
|
364
423
|
orgId
|
|
365
424
|
},
|
|
@@ -401,9 +460,13 @@ function registerSecretTools(server2) {
|
|
|
401
460
|
);
|
|
402
461
|
server2.tool(
|
|
403
462
|
"delete_secret",
|
|
404
|
-
|
|
463
|
+
[
|
|
464
|
+
"[secrets] Permanently remove a secret value (and all its env states) from the keyring for the given scope.",
|
|
465
|
+
"Use when a credential is being retired or was created in error; prefer `disentangle_secrets` to break a sync link without erasing values, `remove_hook` to detach lifecycle callbacks, and `tunnel_destroy` for ephemeral tunnels.",
|
|
466
|
+
`Destructive and not undoable from q-ring (no built-in trash). Writes a 'delete' event to the audit log and fires matching hooks. Returns 'Deleted "KEY"' on success or a not-found error if the key did not exist in the requested scope. Subject to tool policy.`
|
|
467
|
+
].join(" "),
|
|
405
468
|
{
|
|
406
|
-
key: z2.string().describe("
|
|
469
|
+
key: z2.string().describe("Exact secret key name to delete. Example: 'OLD_API_KEY'."),
|
|
407
470
|
scope,
|
|
408
471
|
projectPath,
|
|
409
472
|
teamId,
|
|
@@ -421,9 +484,13 @@ function registerSecretTools(server2) {
|
|
|
421
484
|
);
|
|
422
485
|
server2.tool(
|
|
423
486
|
"has_secret",
|
|
424
|
-
|
|
487
|
+
[
|
|
488
|
+
"[secrets] Check whether a secret exists in the requested scope without reading the value.",
|
|
489
|
+
"Use as a cheap precondition before reading or writing \u2014 for example, to skip prompting the user for a key that is already configured. Prefer `inspect_secret` when you also need metadata.",
|
|
490
|
+
"Read-only; does not record a 'read' in the audit log. Decay-aware: returns 'false' for expired secrets even though the value is still in the store. Returns the literal text 'true' or 'false'."
|
|
491
|
+
].join(" "),
|
|
425
492
|
{
|
|
426
|
-
key: z2.string().describe("
|
|
493
|
+
key: z2.string().describe("Exact secret key name. Example: 'GITHUB_TOKEN'."),
|
|
427
494
|
scope,
|
|
428
495
|
projectPath,
|
|
429
496
|
teamId,
|
|
@@ -437,11 +504,21 @@ function registerSecretTools(server2) {
|
|
|
437
504
|
);
|
|
438
505
|
server2.tool(
|
|
439
506
|
"export_secrets",
|
|
440
|
-
|
|
507
|
+
[
|
|
508
|
+
"[secrets] Render multiple secrets as a single .env or JSON document for piping into another tool or file.",
|
|
509
|
+
"Use to materialize secrets for a one-off export or copy; prefer `env_generate` when you want output driven by the project's `.q-ring.json` manifest, and `teleport_pack` for an encrypted bundle to share between machines.",
|
|
510
|
+
"Reads values (collapses superposition for the requested env) and writes one 'export' event per included secret to the audit log. Returns the rendered text directly (no JSON wrapper). Returns an error if no secrets matched the filters. Values are surfaced in plaintext \u2014 handle with care."
|
|
511
|
+
].join(" "),
|
|
441
512
|
{
|
|
442
|
-
format: z2.enum(["env", "json"]).optional().default("env").describe(
|
|
443
|
-
|
|
444
|
-
|
|
513
|
+
format: z2.enum(["env", "json"]).optional().default("env").describe(
|
|
514
|
+
`'env' renders KEY="value" lines suitable for a .env file; 'json' renders an object keyed by secret name. Defaults to 'env'.`
|
|
515
|
+
),
|
|
516
|
+
keys: z2.array(z2.string()).optional().describe(
|
|
517
|
+
"Whitelist of exact key names to include. If omitted, every key in scope is considered (subject to `tags`)."
|
|
518
|
+
),
|
|
519
|
+
tags: z2.array(z2.string()).optional().describe(
|
|
520
|
+
"Include only secrets tagged with at least one of these tags. Combined with `keys` as an AND filter when both are supplied."
|
|
521
|
+
),
|
|
445
522
|
scope,
|
|
446
523
|
projectPath,
|
|
447
524
|
env,
|
|
@@ -463,13 +540,23 @@ function registerSecretTools(server2) {
|
|
|
463
540
|
);
|
|
464
541
|
server2.tool(
|
|
465
542
|
"import_dotenv",
|
|
466
|
-
|
|
543
|
+
[
|
|
544
|
+
"[secrets] Parse standard dotenv-formatted text and store each key/value pair into the keyring in one batch.",
|
|
545
|
+
"Use when migrating an existing `.env` file into q-ring or onboarding a new project; prefer `set_secret` for a single key, and `teleport_unpack` to import an encrypted bundle.",
|
|
546
|
+
"Mutates the keyring (one write per parsed key) and emits a 'write' audit event for each. Supports comments, single/double quotes, and `\\n` escapes. Returns a multiline summary listing imported keys and any skipped (existing) keys; in dryRun mode no writes happen and the same summary is produced for review."
|
|
547
|
+
].join(" "),
|
|
467
548
|
{
|
|
468
|
-
content: z2.string().describe(
|
|
549
|
+
content: z2.string().describe(
|
|
550
|
+
"Raw .env file content as a single string (newline-separated KEY=VALUE lines, comments allowed)."
|
|
551
|
+
),
|
|
469
552
|
scope: scope.default("global"),
|
|
470
553
|
projectPath,
|
|
471
|
-
skipExisting: z2.boolean().optional().default(false).describe(
|
|
472
|
-
|
|
554
|
+
skipExisting: z2.boolean().optional().default(false).describe(
|
|
555
|
+
"If true, leave already-present keys untouched and add them to the 'skipped' list instead of overwriting."
|
|
556
|
+
),
|
|
557
|
+
dryRun: z2.boolean().optional().default(false).describe(
|
|
558
|
+
"If true, parse and report what would happen but do not write to the keyring. Useful for previewing imports before committing."
|
|
559
|
+
)
|
|
473
560
|
},
|
|
474
561
|
async (params) => {
|
|
475
562
|
const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath);
|
|
@@ -495,9 +582,13 @@ function registerSecretTools(server2) {
|
|
|
495
582
|
);
|
|
496
583
|
server2.tool(
|
|
497
584
|
"inspect_secret",
|
|
498
|
-
|
|
585
|
+
[
|
|
586
|
+
"[secrets] Show full metadata for a single secret \u2014 env states, decay window, entanglement links, access counters \u2014 without ever revealing the value.",
|
|
587
|
+
"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.",
|
|
588
|
+
"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."
|
|
589
|
+
].join(" "),
|
|
499
590
|
{
|
|
500
|
-
key: z2.string().describe("
|
|
591
|
+
key: z2.string().describe("Exact secret key name to inspect. Example: 'OPENAI_API_KEY'."),
|
|
501
592
|
scope,
|
|
502
593
|
projectPath,
|
|
503
594
|
teamId,
|
|
@@ -542,7 +633,11 @@ function registerSecretTools(server2) {
|
|
|
542
633
|
);
|
|
543
634
|
server2.tool(
|
|
544
635
|
"generate_secret",
|
|
545
|
-
|
|
636
|
+
[
|
|
637
|
+
"[secrets] Generate a cryptographically random secret using Node's CSPRNG and optionally store it in the keyring in one step.",
|
|
638
|
+
"Use to create new credentials that you control (signing keys, internal tokens, passwords); for issuer-issued credentials (Stripe/OpenAI etc.) use `rotate_secret` to ask the upstream provider for a fresh key, and use `set_secret` for values you already have in hand.",
|
|
639
|
+
"If `saveAs` is provided this mutates the keyring (one 'write' event) and returns a summary like 'Generated and saved as \"KEY\" (FORMAT, ~N bits entropy)'. Without `saveAs` the call is read-only and returns JSON `{ ok, data: { value } }` containing the freshly generated string."
|
|
640
|
+
].join(" "),
|
|
546
641
|
{
|
|
547
642
|
format: z2.enum([
|
|
548
643
|
"hex",
|
|
@@ -552,10 +647,18 @@ function registerSecretTools(server2) {
|
|
|
552
647
|
"api-key",
|
|
553
648
|
"token",
|
|
554
649
|
"password"
|
|
555
|
-
]).optional().default("api-key").describe(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
650
|
+
]).optional().default("api-key").describe(
|
|
651
|
+
"Output shape. 'hex' / 'base64' / 'alphanumeric' = raw random string of `length` characters; 'uuid' = RFC4122 v4; 'api-key' / 'token' = random alphanumeric with optional `prefix`; 'password' = mixed-case alphanumeric with symbols. Defaults to 'api-key'."
|
|
652
|
+
),
|
|
653
|
+
length: z2.number().optional().describe(
|
|
654
|
+
"Number of characters (or bytes for hex/base64) to generate. Ignored for 'uuid'. Defaults to a sensible per-format value (e.g. 32 for api-key)."
|
|
655
|
+
),
|
|
656
|
+
prefix: z2.string().optional().describe(
|
|
657
|
+
"Literal prefix prepended to the random portion. Only meaningful for 'api-key' and 'token'. Example: 'sk-' or 'svc_'."
|
|
658
|
+
),
|
|
659
|
+
saveAs: z2.string().optional().describe(
|
|
660
|
+
"If provided, store the generated value at this key name in the keyring (one mutation). Omit to just return the value without persisting."
|
|
661
|
+
),
|
|
559
662
|
scope: scope.default("global"),
|
|
560
663
|
projectPath,
|
|
561
664
|
teamId,
|
|
@@ -584,14 +687,22 @@ function registerSecretTools(server2) {
|
|
|
584
687
|
);
|
|
585
688
|
server2.tool(
|
|
586
689
|
"entangle_secrets",
|
|
587
|
-
|
|
690
|
+
[
|
|
691
|
+
"[secrets] Link two keys (across the same or different scopes) so future writes/rotations of either propagate the same value to the other.",
|
|
692
|
+
"Use when one logical credential lives under multiple names (e.g. `STRIPE_SECRET_KEY` global and project) and should never drift; prefer `set_secret` for unrelated values, and reverse the link with `disentangle_secrets` (does not delete values).",
|
|
693
|
+
"Mutates only the metadata of both envelopes \u2014 the values themselves are not changed by this call. Idempotent: re-running on an already-entangled pair is a no-op. Subject to tool policy. Returns a short confirmation: 'Entangled: SOURCE <-> TARGET'."
|
|
694
|
+
].join(" "),
|
|
588
695
|
{
|
|
589
|
-
sourceKey: z2.string().describe("
|
|
590
|
-
targetKey: z2.string().describe("
|
|
696
|
+
sourceKey: z2.string().describe("First secret key in the pair. Example: 'STRIPE_SECRET_KEY'."),
|
|
697
|
+
targetKey: z2.string().describe("Second secret key to keep in lockstep with the source."),
|
|
591
698
|
sourceScope: scope.default("global"),
|
|
592
699
|
targetScope: scope.default("global"),
|
|
593
|
-
sourceProjectPath: z2.string().optional()
|
|
594
|
-
|
|
700
|
+
sourceProjectPath: z2.string().optional().describe(
|
|
701
|
+
"Project root for sourceKey when sourceScope='project'. Defaults to the server cwd."
|
|
702
|
+
),
|
|
703
|
+
targetProjectPath: z2.string().optional().describe(
|
|
704
|
+
"Project root for targetKey when targetScope='project'. Defaults to the server cwd."
|
|
705
|
+
)
|
|
595
706
|
},
|
|
596
707
|
async (params) => {
|
|
597
708
|
const toolBlock = enforceToolPolicy(
|
|
@@ -618,14 +729,18 @@ function registerSecretTools(server2) {
|
|
|
618
729
|
);
|
|
619
730
|
server2.tool(
|
|
620
731
|
"disentangle_secrets",
|
|
621
|
-
|
|
732
|
+
[
|
|
733
|
+
"[secrets] Break the sync link between two previously entangled keys so future rotations no longer propagate.",
|
|
734
|
+
"Use when one of the keys is being retired or should diverge intentionally; pair with `delete_secret` if you also want to erase one of the values, and use `entangle_secrets` to recreate the link.",
|
|
735
|
+
"Mutates only metadata; the current values remain untouched. Safe and idempotent \u2014 running on a pair that was never linked returns success without effect. Subject to tool policy. Returns 'Disentangled: SOURCE </> TARGET'."
|
|
736
|
+
].join(" "),
|
|
622
737
|
{
|
|
623
|
-
sourceKey: z2.string().describe("
|
|
624
|
-
targetKey: z2.string().describe("
|
|
738
|
+
sourceKey: z2.string().describe("First key in the previously linked pair."),
|
|
739
|
+
targetKey: z2.string().describe("Second key in the previously linked pair."),
|
|
625
740
|
sourceScope: scope.default("global"),
|
|
626
741
|
targetScope: scope.default("global"),
|
|
627
|
-
sourceProjectPath: z2.string().optional(),
|
|
628
|
-
targetProjectPath: z2.string().optional()
|
|
742
|
+
sourceProjectPath: z2.string().optional().describe("Project root for sourceKey when sourceScope='project'."),
|
|
743
|
+
targetProjectPath: z2.string().optional().describe("Project root for targetKey when targetScope='project'.")
|
|
629
744
|
},
|
|
630
745
|
async (params) => {
|
|
631
746
|
const toolBlock = enforceToolPolicy(
|
|
@@ -934,7 +1049,11 @@ var { teamId: teamId2, orgId: orgId2, scope: scope2, projectPath: projectPath2,
|
|
|
934
1049
|
function registerProjectTools(server2) {
|
|
935
1050
|
server2.tool(
|
|
936
1051
|
"check_project",
|
|
937
|
-
|
|
1052
|
+
[
|
|
1053
|
+
"[project] Compare the keys declared in the project's `.q-ring.json` manifest against what is actually present in the keyring.",
|
|
1054
|
+
"Use as the canonical 'is this project ready to run' gate before starting a dev server, deploying, or onboarding a teammate; prefer `health_check` for a scope-wide decay sweep (no manifest), and `agent_scan` for multi-project scans with optional auto-rotation.",
|
|
1055
|
+
"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."
|
|
1056
|
+
].join(" "),
|
|
938
1057
|
{
|
|
939
1058
|
projectPath: projectPath2
|
|
940
1059
|
},
|
|
@@ -1001,7 +1120,11 @@ function registerProjectTools(server2) {
|
|
|
1001
1120
|
);
|
|
1002
1121
|
server2.tool(
|
|
1003
1122
|
"env_generate",
|
|
1004
|
-
|
|
1123
|
+
[
|
|
1124
|
+
"[project] Render a complete `.env` file body from the project's `.q-ring.json` manifest, resolving each declared key from the keyring.",
|
|
1125
|
+
"Use when a build step or local runtime needs a real `.env` materialized on disk and you want exactly the keys the manifest declares; prefer `export_secrets` when you want every key in scope (manifest-agnostic) and `exec_with_secrets` to inject secrets into a child process without writing them to a file.",
|
|
1126
|
+
"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."
|
|
1127
|
+
].join(" "),
|
|
1005
1128
|
{
|
|
1006
1129
|
projectPath: projectPath2,
|
|
1007
1130
|
env: env2
|
|
@@ -1048,7 +1171,11 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1048
1171
|
);
|
|
1049
1172
|
server2.tool(
|
|
1050
1173
|
"detect_environment",
|
|
1051
|
-
|
|
1174
|
+
[
|
|
1175
|
+
"[project] Resolve which environment slug (e.g. 'dev', 'staging', 'prod') the current invocation should collapse to.",
|
|
1176
|
+
"Use before reading secrets when you want to mirror the same env q-ring would auto-pick (e.g. to log it, or to pass through to another tool); prefer passing an explicit `env` to `get_secret`/`env_generate` when you already know which env you want.",
|
|
1177
|
+
"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."
|
|
1178
|
+
].join(" "),
|
|
1052
1179
|
{
|
|
1053
1180
|
projectPath: projectPath2
|
|
1054
1181
|
},
|
|
@@ -1071,7 +1198,11 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
|
|
|
1071
1198
|
);
|
|
1072
1199
|
server2.tool(
|
|
1073
1200
|
"get_project_context",
|
|
1074
|
-
|
|
1201
|
+
[
|
|
1202
|
+
"[agent] Return a single redacted snapshot of everything an AI agent typically wants to know about this project: secrets present (keys + metadata only), detected env, manifest declarations, configured providers, registered hooks, and recent audit activity.",
|
|
1203
|
+
"Use this as the very first call in a session to orient the agent before it asks for any individual secret; prefer `list_secrets` for a flat key listing, `check_project` for manifest-vs-keyring drift, and `audit_log` for a deeper access trail.",
|
|
1204
|
+
"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."
|
|
1205
|
+
].join(" "),
|
|
1075
1206
|
{
|
|
1076
1207
|
scope: scope2,
|
|
1077
1208
|
projectPath: projectPath2,
|
|
@@ -1095,11 +1226,21 @@ import { z as z3 } from "zod";
|
|
|
1095
1226
|
function registerTunnelTools(server2) {
|
|
1096
1227
|
server2.tool(
|
|
1097
1228
|
"tunnel_create",
|
|
1098
|
-
|
|
1229
|
+
[
|
|
1230
|
+
"[tunnel] Stash a one-shot or short-lived secret in the q-ring server's process memory and return an ID that can be used to read it back.",
|
|
1231
|
+
"Use for handing a one-time value to another tool/process without persisting it (npm OTP codes, magic-link tokens, copy/paste between machines via a relay); prefer `set_secret` with `ttlSeconds` when you actually want a tracked, auditable secret.",
|
|
1232
|
+
"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`."
|
|
1233
|
+
].join(" "),
|
|
1099
1234
|
{
|
|
1100
|
-
value: z3.string().describe(
|
|
1101
|
-
|
|
1102
|
-
|
|
1235
|
+
value: z3.string().describe(
|
|
1236
|
+
"The plaintext value to tunnel. Held only in process memory; never logged."
|
|
1237
|
+
),
|
|
1238
|
+
ttlSeconds: z3.number().optional().describe(
|
|
1239
|
+
"Auto-destroy the tunnel after this many seconds. Omit for no time limit (then a `maxReads` is highly recommended)."
|
|
1240
|
+
),
|
|
1241
|
+
maxReads: z3.number().optional().describe(
|
|
1242
|
+
"Self-destruct after this many successful `tunnel_read` calls. Use 1 for true one-shot delivery."
|
|
1243
|
+
)
|
|
1103
1244
|
},
|
|
1104
1245
|
async (params) => {
|
|
1105
1246
|
const toolBlock = enforceToolPolicy("tunnel_create");
|
|
@@ -1113,9 +1254,15 @@ function registerTunnelTools(server2) {
|
|
|
1113
1254
|
);
|
|
1114
1255
|
server2.tool(
|
|
1115
1256
|
"tunnel_read",
|
|
1116
|
-
|
|
1257
|
+
[
|
|
1258
|
+
"[tunnel] Fetch the value stashed by a prior `tunnel_create` call by its ID.",
|
|
1259
|
+
"Use exactly once per intended consumer; the value is destructive-by-design and may self-delete after this call.",
|
|
1260
|
+
"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."
|
|
1261
|
+
].join(" "),
|
|
1117
1262
|
{
|
|
1118
|
-
id: z3.string().describe(
|
|
1263
|
+
id: z3.string().describe(
|
|
1264
|
+
"The opaque tunnel ID returned by `tunnel_create`. Case-sensitive."
|
|
1265
|
+
)
|
|
1119
1266
|
},
|
|
1120
1267
|
async (params) => {
|
|
1121
1268
|
const toolBlock = enforceToolPolicy("tunnel_read");
|
|
@@ -1131,7 +1278,11 @@ function registerTunnelTools(server2) {
|
|
|
1131
1278
|
);
|
|
1132
1279
|
server2.tool(
|
|
1133
1280
|
"tunnel_list",
|
|
1134
|
-
|
|
1281
|
+
[
|
|
1282
|
+
"[tunnel] Enumerate all currently-active tunnels in the q-ring server with their remaining read budget and time-to-live.",
|
|
1283
|
+
"Use to audit what is still in memory or to look up an ID you forgot; values are never included in the output.",
|
|
1284
|
+
"Read-only. Returns one line per tunnel formatted as `id | reads:N | max:N | expires:Ns`, or the literal text 'No active tunnels' when the list is empty."
|
|
1285
|
+
].join(" "),
|
|
1135
1286
|
{},
|
|
1136
1287
|
async () => {
|
|
1137
1288
|
const toolBlock = enforceToolPolicy("tunnel_list");
|
|
@@ -1156,9 +1307,13 @@ function registerTunnelTools(server2) {
|
|
|
1156
1307
|
);
|
|
1157
1308
|
server2.tool(
|
|
1158
1309
|
"tunnel_destroy",
|
|
1159
|
-
|
|
1310
|
+
[
|
|
1311
|
+
"[tunnel] Immediately remove a tunnel from memory, regardless of remaining reads or TTL.",
|
|
1312
|
+
"Use when a tunneled value should be cancelled before delivery (e.g. wrong recipient, secret already rotated); prefer letting `maxReads`/TTL handle cleanup for normal flows.",
|
|
1313
|
+
"Mutates in-memory state only. Returns 'Destroyed ID' on success or a not-found error if the ID is unknown or already gone."
|
|
1314
|
+
].join(" "),
|
|
1160
1315
|
{
|
|
1161
|
-
id: z3.string().describe("
|
|
1316
|
+
id: z3.string().describe("The opaque tunnel ID to destroy.")
|
|
1162
1317
|
},
|
|
1163
1318
|
async (params) => {
|
|
1164
1319
|
const toolBlock = enforceToolPolicy("tunnel_destroy");
|
|
@@ -1187,7 +1342,8 @@ var ALGORITHM = "aes-256-gcm";
|
|
|
1187
1342
|
var KEY_LENGTH = 32;
|
|
1188
1343
|
var IV_LENGTH = 12;
|
|
1189
1344
|
var SALT_LENGTH = 32;
|
|
1190
|
-
var PBKDF2_ITERATIONS =
|
|
1345
|
+
var PBKDF2_ITERATIONS = 21e4;
|
|
1346
|
+
var LEGACY_PBKDF2_ITERATIONS = 1e5;
|
|
1191
1347
|
var TeleportBundleSchema = z4.object({
|
|
1192
1348
|
v: z4.literal(1),
|
|
1193
1349
|
data: z4.string(),
|
|
@@ -1195,7 +1351,8 @@ var TeleportBundleSchema = z4.object({
|
|
|
1195
1351
|
iv: z4.string(),
|
|
1196
1352
|
tag: z4.string(),
|
|
1197
1353
|
createdAt: z4.string(),
|
|
1198
|
-
count: z4.number()
|
|
1354
|
+
count: z4.number(),
|
|
1355
|
+
iter: z4.number().optional()
|
|
1199
1356
|
});
|
|
1200
1357
|
var TeleportPayloadSchema = z4.object({
|
|
1201
1358
|
secrets: z4.array(
|
|
@@ -1208,8 +1365,8 @@ var TeleportPayloadSchema = z4.object({
|
|
|
1208
1365
|
exportedAt: z4.string(),
|
|
1209
1366
|
exportedBy: z4.string().optional()
|
|
1210
1367
|
});
|
|
1211
|
-
function deriveKey(passphrase, salt) {
|
|
1212
|
-
return pbkdf2Sync(passphrase, salt,
|
|
1368
|
+
function deriveKey(passphrase, salt, iterations = PBKDF2_ITERATIONS) {
|
|
1369
|
+
return pbkdf2Sync(passphrase, salt, iterations, KEY_LENGTH, "sha512");
|
|
1213
1370
|
}
|
|
1214
1371
|
function teleportPack(secrets, passphrase) {
|
|
1215
1372
|
const payload = {
|
|
@@ -1219,7 +1376,7 @@ function teleportPack(secrets, passphrase) {
|
|
|
1219
1376
|
const plaintext = JSON.stringify(payload);
|
|
1220
1377
|
const salt = randomBytes2(SALT_LENGTH);
|
|
1221
1378
|
const iv = randomBytes2(IV_LENGTH);
|
|
1222
|
-
const key = deriveKey(passphrase, salt);
|
|
1379
|
+
const key = deriveKey(passphrase, salt, PBKDF2_ITERATIONS);
|
|
1223
1380
|
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
1224
1381
|
const encrypted = Buffer.concat([
|
|
1225
1382
|
cipher.update(plaintext, "utf8"),
|
|
@@ -1233,7 +1390,8 @@ function teleportPack(secrets, passphrase) {
|
|
|
1233
1390
|
iv: iv.toString("base64"),
|
|
1234
1391
|
tag: tag.toString("base64"),
|
|
1235
1392
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1236
|
-
count: secrets.length
|
|
1393
|
+
count: secrets.length,
|
|
1394
|
+
iter: PBKDF2_ITERATIONS
|
|
1237
1395
|
};
|
|
1238
1396
|
return Buffer.from(JSON.stringify(bundle)).toString("base64");
|
|
1239
1397
|
}
|
|
@@ -1261,7 +1419,7 @@ function teleportUnpack(encoded, passphrase) {
|
|
|
1261
1419
|
const iv = Buffer.from(bundle.iv, "base64");
|
|
1262
1420
|
const tag = Buffer.from(bundle.tag, "base64");
|
|
1263
1421
|
const encrypted = Buffer.from(bundle.data, "base64");
|
|
1264
|
-
const key = deriveKey(passphrase, salt);
|
|
1422
|
+
const key = deriveKey(passphrase, salt, bundle.iter ?? LEGACY_PBKDF2_ITERATIONS);
|
|
1265
1423
|
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
1266
1424
|
decipher.setAuthTag(tag);
|
|
1267
1425
|
let decrypted;
|
|
@@ -1293,10 +1451,18 @@ var { teamId: teamId3, orgId: orgId3, scope: scope3, projectPath: projectPath3 }
|
|
|
1293
1451
|
function registerTeleportTools(server2) {
|
|
1294
1452
|
server2.tool(
|
|
1295
1453
|
"teleport_pack",
|
|
1296
|
-
|
|
1454
|
+
[
|
|
1455
|
+
"[teleport] Encrypt one or more secrets into a single AES-256-GCM bundle string that can be safely transferred between machines.",
|
|
1456
|
+
"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.",
|
|
1457
|
+
"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."
|
|
1458
|
+
].join(" "),
|
|
1297
1459
|
{
|
|
1298
|
-
keys: z5.array(z5.string()).optional().describe(
|
|
1299
|
-
|
|
1460
|
+
keys: z5.array(z5.string()).optional().describe(
|
|
1461
|
+
"Whitelist of exact key names to include. Omit to pack every secret in the requested scope."
|
|
1462
|
+
),
|
|
1463
|
+
passphrase: z5.string().describe(
|
|
1464
|
+
"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."
|
|
1465
|
+
),
|
|
1300
1466
|
scope: scope3,
|
|
1301
1467
|
projectPath: projectPath3,
|
|
1302
1468
|
teamId: teamId3,
|
|
@@ -1322,15 +1488,25 @@ function registerTeleportTools(server2) {
|
|
|
1322
1488
|
);
|
|
1323
1489
|
server2.tool(
|
|
1324
1490
|
"teleport_unpack",
|
|
1325
|
-
|
|
1491
|
+
[
|
|
1492
|
+
"[teleport] Decrypt a bundle produced by `teleport_pack` and import each contained secret into the local keyring.",
|
|
1493
|
+
"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.",
|
|
1494
|
+
"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."
|
|
1495
|
+
].join(" "),
|
|
1326
1496
|
{
|
|
1327
|
-
bundle: z5.string().describe(
|
|
1328
|
-
|
|
1497
|
+
bundle: z5.string().describe(
|
|
1498
|
+
"Base64-encoded ciphertext returned by `teleport_pack`. Pass through whitespace untouched if possible."
|
|
1499
|
+
),
|
|
1500
|
+
passphrase: z5.string().describe(
|
|
1501
|
+
"The same passphrase that was used to pack this bundle. Bad passphrases return an authentication error rather than wrong plaintext."
|
|
1502
|
+
),
|
|
1329
1503
|
scope: scope3.default("global"),
|
|
1330
1504
|
projectPath: projectPath3,
|
|
1331
1505
|
teamId: teamId3,
|
|
1332
1506
|
orgId: orgId3,
|
|
1333
|
-
dryRun: z5.boolean().optional().default(false).describe(
|
|
1507
|
+
dryRun: z5.boolean().optional().default(false).describe(
|
|
1508
|
+
"If true, decrypt and report what would be written but do not mutate the keyring. Useful for verifying bundle contents before commit."
|
|
1509
|
+
)
|
|
1334
1510
|
},
|
|
1335
1511
|
async (params) => {
|
|
1336
1512
|
const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
|
|
@@ -1365,9 +1541,15 @@ var { teamId: teamId4, orgId: orgId4, scope: scope4, projectPath: projectPath4 }
|
|
|
1365
1541
|
function registerAuditTools(server2) {
|
|
1366
1542
|
server2.tool(
|
|
1367
1543
|
"audit_log",
|
|
1368
|
-
|
|
1544
|
+
[
|
|
1545
|
+
"[audit] Query the q-ring audit log \u2014 a tamper-evident record of every read/write/delete touching a secret.",
|
|
1546
|
+
"Use to investigate 'who accessed KEY recently?' or to feed an agent the access timeline for a specific credential; prefer `detect_anomalies` for automated unusual-pattern detection and `health_check` for decay-state-plus-anomalies in one call.",
|
|
1547
|
+
"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."
|
|
1548
|
+
].join(" "),
|
|
1369
1549
|
{
|
|
1370
|
-
key: z6.string().optional().describe(
|
|
1550
|
+
key: z6.string().optional().describe(
|
|
1551
|
+
"Limit to events touching this exact key. Omit for the full log."
|
|
1552
|
+
),
|
|
1371
1553
|
action: z6.enum([
|
|
1372
1554
|
"read",
|
|
1373
1555
|
"write",
|
|
@@ -1379,8 +1561,12 @@ function registerAuditTools(server2) {
|
|
|
1379
1561
|
"tunnel",
|
|
1380
1562
|
"teleport",
|
|
1381
1563
|
"collapse"
|
|
1382
|
-
]).optional().describe(
|
|
1383
|
-
|
|
1564
|
+
]).optional().describe(
|
|
1565
|
+
"Limit to a single action verb (e.g. 'read' to see only reads). Omit for all actions."
|
|
1566
|
+
),
|
|
1567
|
+
limit: z6.number().optional().default(20).describe(
|
|
1568
|
+
"Maximum events to return, newest first. Defaults to 20. Increase for deeper investigations."
|
|
1569
|
+
)
|
|
1384
1570
|
},
|
|
1385
1571
|
async (params) => {
|
|
1386
1572
|
const toolBlock = enforceToolPolicy("audit_log");
|
|
@@ -1404,9 +1590,15 @@ function registerAuditTools(server2) {
|
|
|
1404
1590
|
);
|
|
1405
1591
|
server2.tool(
|
|
1406
1592
|
"detect_anomalies",
|
|
1407
|
-
|
|
1593
|
+
[
|
|
1594
|
+
"[audit] Scan the audit history for suspicious access patterns \u2014 burst reads of the same key, off-hours access, and other heuristics.",
|
|
1595
|
+
"Use as a quick triage signal when investigating a single key or before letting an agent rotate credentials; prefer `health_check` for a scope-wide decay+anomaly summary, and `agent_scan` for multi-project JSON reports with optional auto-rotation.",
|
|
1596
|
+
"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."
|
|
1597
|
+
].join(" "),
|
|
1408
1598
|
{
|
|
1409
|
-
key: z6.string().optional().describe(
|
|
1599
|
+
key: z6.string().optional().describe(
|
|
1600
|
+
"If provided, narrow the scan to this exact key. Omit to scan across every key in the audit log."
|
|
1601
|
+
)
|
|
1410
1602
|
},
|
|
1411
1603
|
async (params) => {
|
|
1412
1604
|
const toolBlock = enforceToolPolicy("detect_anomalies");
|
|
@@ -1419,7 +1611,11 @@ function registerAuditTools(server2) {
|
|
|
1419
1611
|
);
|
|
1420
1612
|
server2.tool(
|
|
1421
1613
|
"health_check",
|
|
1422
|
-
|
|
1614
|
+
[
|
|
1615
|
+
"[health] Run a single read-only sweep over every secret in the requested scope and report counts of healthy/stale/expired secrets plus any current audit anomalies.",
|
|
1616
|
+
"Use as the default 'is everything OK?' command for an agent or operator; prefer `check_project` to validate manifest compliance specifically, `detect_anomalies` for audit-only triage, and `agent_scan` for multi-project JSON output or optional auto-rotation.",
|
|
1617
|
+
"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."
|
|
1618
|
+
].join(" "),
|
|
1423
1619
|
{
|
|
1424
1620
|
scope: scope4,
|
|
1425
1621
|
projectPath: projectPath4,
|
|
@@ -1473,7 +1669,11 @@ function registerAuditTools(server2) {
|
|
|
1473
1669
|
);
|
|
1474
1670
|
server2.tool(
|
|
1475
1671
|
"verify_audit_chain",
|
|
1476
|
-
|
|
1672
|
+
[
|
|
1673
|
+
"[audit] Recompute the SHA-256 hash chain over the audit log and confirm no event has been mutated, deleted, or reordered.",
|
|
1674
|
+
"Use periodically as a tamper-evidence check, or whenever you suspect the audit log has been touched outside q-ring; the result is informational \u2014 this tool does not repair the chain if it is broken.",
|
|
1675
|
+
"Read-only. Returns JSON `{ ok, valid, brokenAt? }` where `valid` is `true` for an intact chain and `brokenAt` (when present) names the first event whose hash did not match."
|
|
1676
|
+
].join(" "),
|
|
1477
1677
|
{},
|
|
1478
1678
|
async () => {
|
|
1479
1679
|
const toolBlock = enforceToolPolicy("verify_audit_chain");
|
|
@@ -1484,11 +1684,21 @@ function registerAuditTools(server2) {
|
|
|
1484
1684
|
);
|
|
1485
1685
|
server2.tool(
|
|
1486
1686
|
"export_audit",
|
|
1487
|
-
|
|
1687
|
+
[
|
|
1688
|
+
"[audit] Export the audit log as a portable text artifact suitable for archiving or feeding into another SIEM/analyzer.",
|
|
1689
|
+
"Use for compliance exports, after-the-fact investigations, or to hand the trail to a non-MCP consumer; prefer `audit_log` for an in-conversation tail and `verify_audit_chain` to confirm integrity before exporting.",
|
|
1690
|
+
"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."
|
|
1691
|
+
].join(" "),
|
|
1488
1692
|
{
|
|
1489
|
-
since: z6.string().optional().describe(
|
|
1490
|
-
|
|
1491
|
-
|
|
1693
|
+
since: z6.string().optional().describe(
|
|
1694
|
+
"Inclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound."
|
|
1695
|
+
),
|
|
1696
|
+
until: z6.string().optional().describe(
|
|
1697
|
+
"Inclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound."
|
|
1698
|
+
),
|
|
1699
|
+
format: z6.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe(
|
|
1700
|
+
"Output format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly."
|
|
1701
|
+
)
|
|
1492
1702
|
},
|
|
1493
1703
|
async (params) => {
|
|
1494
1704
|
const toolBlock = enforceToolPolicy("export_audit");
|
|
@@ -1509,10 +1719,18 @@ var { teamId: teamId5, orgId: orgId5, scope: scope5, projectPath: projectPath5 }
|
|
|
1509
1719
|
function registerValidationTools(server2) {
|
|
1510
1720
|
server2.tool(
|
|
1511
1721
|
"validate_secret",
|
|
1512
|
-
|
|
1722
|
+
[
|
|
1723
|
+
"[validation] Test whether a stored secret is still accepted by its upstream service (OpenAI, Stripe, GitHub, AWS, generic HTTP, etc.) by making a minimal authenticated request.",
|
|
1724
|
+
"Use to confirm liveness before relying on a credential or as the verification step after `rotate_secret`; prefer `ci_validate_secrets` for a batch run across every key in scope.",
|
|
1725
|
+
"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)."
|
|
1726
|
+
].join(" "),
|
|
1513
1727
|
{
|
|
1514
|
-
key: z7.string().describe(
|
|
1515
|
-
|
|
1728
|
+
key: z7.string().describe(
|
|
1729
|
+
"The exact key whose value should be tested upstream. Example: 'OPENAI_API_KEY'."
|
|
1730
|
+
),
|
|
1731
|
+
provider: z7.string().optional().describe(
|
|
1732
|
+
"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."
|
|
1733
|
+
),
|
|
1516
1734
|
scope: scope5,
|
|
1517
1735
|
projectPath: projectPath5,
|
|
1518
1736
|
teamId: teamId5,
|
|
@@ -1531,7 +1749,11 @@ function registerValidationTools(server2) {
|
|
|
1531
1749
|
);
|
|
1532
1750
|
server2.tool(
|
|
1533
1751
|
"list_providers",
|
|
1534
|
-
|
|
1752
|
+
[
|
|
1753
|
+
"[validation] Enumerate the secret-validation providers q-ring knows how to call (OpenAI, Stripe, GitHub, \u2026) along with their auto-detect prefixes.",
|
|
1754
|
+
"Use to discover what `provider` string to pass to `validate_secret`/`rotate_secret`, or to check whether your custom provider is registered.",
|
|
1755
|
+
"Read-only. Returns JSON array of `{ name, description, prefixes }` objects. `prefixes` are the literal key-value prefixes (e.g. 'sk-' for OpenAI) used for auto-detection."
|
|
1756
|
+
].join(" "),
|
|
1535
1757
|
{},
|
|
1536
1758
|
async () => {
|
|
1537
1759
|
const toolBlock = enforceToolPolicy("list_providers");
|
|
@@ -1546,10 +1768,16 @@ function registerValidationTools(server2) {
|
|
|
1546
1768
|
);
|
|
1547
1769
|
server2.tool(
|
|
1548
1770
|
"rotate_secret",
|
|
1549
|
-
|
|
1771
|
+
[
|
|
1772
|
+
"[validation] Ask the upstream provider to issue a fresh credential for this secret and store the new value back into the keyring.",
|
|
1773
|
+
"Use when a secret is expiring, leaked, or part of a scheduled rotation; prefer `generate_secret` for self-managed values you fully control, and `agent_scan --autoRotate` for sweep-style rotation across multiple expired keys.",
|
|
1774
|
+
"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."
|
|
1775
|
+
].join(" "),
|
|
1550
1776
|
{
|
|
1551
|
-
key: z7.string().describe("
|
|
1552
|
-
provider: z7.string().optional().describe(
|
|
1777
|
+
key: z7.string().describe("Exact key to rotate. Must already exist in the keyring."),
|
|
1778
|
+
provider: z7.string().optional().describe(
|
|
1779
|
+
"Force a specific provider id (see `list_providers`). Omit to auto-detect from the current value or the secret's stored provider hint."
|
|
1780
|
+
),
|
|
1553
1781
|
scope: scope5,
|
|
1554
1782
|
projectPath: projectPath5,
|
|
1555
1783
|
teamId: teamId5,
|
|
@@ -1573,7 +1801,11 @@ function registerValidationTools(server2) {
|
|
|
1573
1801
|
);
|
|
1574
1802
|
server2.tool(
|
|
1575
1803
|
"ci_validate_secrets",
|
|
1576
|
-
|
|
1804
|
+
[
|
|
1805
|
+
"[validation] Validate every accessible secret in the requested scope against its detected provider in a single batch and return a structured pass/fail report.",
|
|
1806
|
+
"Use as a CI gate ('do all our credentials still work before deploy?') or as a pre-rotation health pass; prefer `validate_secret` for a single key.",
|
|
1807
|
+
"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."
|
|
1808
|
+
].join(" "),
|
|
1577
1809
|
{
|
|
1578
1810
|
scope: scope5,
|
|
1579
1811
|
projectPath: projectPath5,
|
|
@@ -1613,19 +1845,45 @@ import { z as z8 } from "zod";
|
|
|
1613
1845
|
function registerHookTools(server2) {
|
|
1614
1846
|
server2.tool(
|
|
1615
1847
|
"register_hook",
|
|
1616
|
-
|
|
1848
|
+
[
|
|
1849
|
+
"[hooks] Register a side-effect (shell command, HTTP webhook, or process signal) that fires automatically when a matching secret is written, deleted, or rotated.",
|
|
1850
|
+
"Use to keep external systems in sync (restart a service after rotation, post to Slack on delete, kick a build); prefer `agent_remember` for storing facts an agent should recall later, and `register_hook` is not the right tool for time-based scheduled rotation (use `agent_scan` for that).",
|
|
1851
|
+
"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`)."
|
|
1852
|
+
].join(" "),
|
|
1617
1853
|
{
|
|
1618
|
-
type: z8.enum(["shell", "http", "signal"]).describe(
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1854
|
+
type: z8.enum(["shell", "http", "signal"]).describe(
|
|
1855
|
+
"Hook delivery mechanism. 'shell' runs a local command, 'http' POSTs JSON to a URL, 'signal' sends an OS signal to a named process."
|
|
1856
|
+
),
|
|
1857
|
+
key: z8.string().optional().describe(
|
|
1858
|
+
"Trigger only on this exact key name. Pick at most one of `key` / `keyPattern` / `tag` (or combine for stricter matching)."
|
|
1859
|
+
),
|
|
1860
|
+
keyPattern: z8.string().optional().describe(
|
|
1861
|
+
"Trigger on any key matching this glob pattern. Examples: 'DB_*', 'STRIPE_*'."
|
|
1862
|
+
),
|
|
1863
|
+
tag: z8.string().optional().describe(
|
|
1864
|
+
"Trigger on any secret carrying this exact tag. Combinable with key/keyPattern as an AND filter."
|
|
1865
|
+
),
|
|
1866
|
+
scope: z8.enum(["global", "project"]).optional().describe(
|
|
1867
|
+
"Restrict the hook to secrets in this scope. Omit to fire across both global and project secrets."
|
|
1868
|
+
),
|
|
1869
|
+
actions: z8.array(z8.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe(
|
|
1870
|
+
"Which lifecycle actions trigger this hook. Defaults to all three."
|
|
1871
|
+
),
|
|
1872
|
+
command: z8.string().optional().describe(
|
|
1873
|
+
"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."
|
|
1874
|
+
),
|
|
1875
|
+
url: z8.string().optional().describe(
|
|
1876
|
+
"Required when type='http'. Full URL to POST a JSON body `{ id, key, scope, action, timestamp }` to (the value itself is never sent)."
|
|
1877
|
+
),
|
|
1878
|
+
signalTarget: z8.string().optional().describe(
|
|
1879
|
+
"Required when type='signal'. Either a numeric PID or a process name resolvable via `ps`."
|
|
1880
|
+
),
|
|
1881
|
+
signalName: z8.string().optional().default("SIGHUP").describe(
|
|
1882
|
+
"Signal name to send (e.g. 'SIGHUP', 'SIGUSR1'). Defaults to SIGHUP, which most daemons treat as 'reload config'."
|
|
1883
|
+
),
|
|
1884
|
+
description: z8.string().optional().describe(
|
|
1885
|
+
"Free-text human-readable description, surfaced by `list_hooks` and the dashboard."
|
|
1886
|
+
)
|
|
1629
1887
|
},
|
|
1630
1888
|
async (params) => {
|
|
1631
1889
|
const toolBlock = enforceToolPolicy("register_hook");
|
|
@@ -1656,7 +1914,11 @@ function registerHookTools(server2) {
|
|
|
1656
1914
|
);
|
|
1657
1915
|
server2.tool(
|
|
1658
1916
|
"list_hooks",
|
|
1659
|
-
|
|
1917
|
+
[
|
|
1918
|
+
"[hooks] Enumerate every registered lifecycle hook with its match criteria, delivery type, enabled flag, and description.",
|
|
1919
|
+
"Use to find a hook's `id` before calling `remove_hook`, audit what side effects are wired up, or diagnose why a hook did not fire.",
|
|
1920
|
+
"Read-only. Returns pretty-printed JSON array of hook entries, or 'No hooks registered' when the registry is empty."
|
|
1921
|
+
].join(" "),
|
|
1660
1922
|
{},
|
|
1661
1923
|
async () => {
|
|
1662
1924
|
const toolBlock = enforceToolPolicy("list_hooks");
|
|
@@ -1668,9 +1930,15 @@ function registerHookTools(server2) {
|
|
|
1668
1930
|
);
|
|
1669
1931
|
server2.tool(
|
|
1670
1932
|
"remove_hook",
|
|
1671
|
-
|
|
1933
|
+
[
|
|
1934
|
+
"[hooks] Detach a single lifecycle hook by its registry id so it stops firing.",
|
|
1935
|
+
"Use to retire a specific webhook/command without touching any secrets; prefer `delete_secret` to remove a credential and `tunnel_destroy` for ephemeral tunnels.",
|
|
1936
|
+
"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."
|
|
1937
|
+
].join(" "),
|
|
1672
1938
|
{
|
|
1673
|
-
id: z8.string().describe(
|
|
1939
|
+
id: z8.string().describe(
|
|
1940
|
+
"Hook id returned by `register_hook` or visible in `list_hooks` (opaque string)."
|
|
1941
|
+
)
|
|
1674
1942
|
},
|
|
1675
1943
|
async (params) => {
|
|
1676
1944
|
const toolBlock = enforceToolPolicy("remove_hook");
|
|
@@ -1920,7 +2188,8 @@ async function execCommand(opts2) {
|
|
|
1920
2188
|
"dig",
|
|
1921
2189
|
"nslookup"
|
|
1922
2190
|
]);
|
|
1923
|
-
|
|
2191
|
+
const commandBase = opts2.command.split(/[\\/]/).pop() ?? opts2.command;
|
|
2192
|
+
if (profile.allowNetwork === false && networkTools.has(commandBase)) {
|
|
1924
2193
|
const msg = `[QRING] Execution blocked: network access is disabled for profile "${profile.name}", command "${opts2.command}" is considered network-related`;
|
|
1925
2194
|
if (opts2.captureOutput) {
|
|
1926
2195
|
return resolve({ code: 126, stdout: "", stderr: msg });
|
|
@@ -2203,13 +2472,27 @@ var { teamId: teamId6, orgId: orgId6, scope: scope6, projectPath: projectPath6 }
|
|
|
2203
2472
|
function registerToolingTools(server2) {
|
|
2204
2473
|
server2.tool(
|
|
2205
2474
|
"exec_with_secrets",
|
|
2206
|
-
|
|
2475
|
+
[
|
|
2476
|
+
"[exec] Run a child shell command with project secrets injected as environment variables and any leaked secret values redacted from captured stdout/stderr before they return to the agent.",
|
|
2477
|
+
"Use to let an agent run a script that needs credentials (`npm run db:migrate`, `terraform plan`, `vercel deploy`) without ever putting plaintext values in the chat; prefer `env_generate` if you need to write a `.env` file to disk and `validate_secret` for upstream liveness checks.",
|
|
2478
|
+
"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."
|
|
2479
|
+
].join(" "),
|
|
2207
2480
|
{
|
|
2208
|
-
command: z9.string().describe(
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2481
|
+
command: z9.string().describe(
|
|
2482
|
+
"Executable name or full command to run. Example: 'pnpm', 'node', '/usr/bin/env'. Must be allowed by exec policy."
|
|
2483
|
+
),
|
|
2484
|
+
args: z9.array(z9.string()).optional().describe(
|
|
2485
|
+
"Positional arguments passed to `command`. Example: ['run', 'db:migrate']. Each element is passed verbatim with no extra shell parsing."
|
|
2486
|
+
),
|
|
2487
|
+
keys: z9.array(z9.string()).optional().describe(
|
|
2488
|
+
"Whitelist of exact key names to inject. Omit to inject every secret in scope (subject to `tags`)."
|
|
2489
|
+
),
|
|
2490
|
+
tags: z9.array(z9.string()).optional().describe(
|
|
2491
|
+
"Inject only secrets carrying at least one of these tags. Combinable with `keys` as an AND filter."
|
|
2492
|
+
),
|
|
2493
|
+
profile: z9.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe(
|
|
2494
|
+
"Exec sandbox profile. 'restricted' (default) limits PATH and inheritable env vars; 'ci' is restricted plus CI-friendly defaults (no TTY); 'unrestricted' inherits the full server environment \u2014 only pick this when you understand the leak risk."
|
|
2495
|
+
),
|
|
2213
2496
|
scope: scope6,
|
|
2214
2497
|
projectPath: projectPath6,
|
|
2215
2498
|
teamId: teamId6,
|
|
@@ -2251,9 +2534,15 @@ ${result.stderr}`);
|
|
|
2251
2534
|
);
|
|
2252
2535
|
server2.tool(
|
|
2253
2536
|
"scan_codebase_for_secrets",
|
|
2254
|
-
|
|
2537
|
+
[
|
|
2538
|
+
"[scan] Walk a directory tree and flag plausible hardcoded secrets using regex heuristics plus Shannon-entropy scoring on string literals.",
|
|
2539
|
+
"Use as a one-shot 'is anything leaking in this repo?' audit before commit/release; prefer `lint_files` when you already know the specific files to check (and want optional auto-fix).",
|
|
2540
|
+
"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."
|
|
2541
|
+
].join(" "),
|
|
2255
2542
|
{
|
|
2256
|
-
dirPath: z9.string().describe(
|
|
2543
|
+
dirPath: z9.string().describe(
|
|
2544
|
+
"Directory to scan, absolute or relative to the server cwd. The scan recurses into subdirectories."
|
|
2545
|
+
)
|
|
2257
2546
|
},
|
|
2258
2547
|
async (params) => {
|
|
2259
2548
|
const toolBlock = enforceToolPolicy("scan_codebase_for_secrets");
|
|
@@ -2274,10 +2563,18 @@ ${result.stderr}`);
|
|
|
2274
2563
|
);
|
|
2275
2564
|
server2.tool(
|
|
2276
2565
|
"lint_files",
|
|
2277
|
-
|
|
2566
|
+
[
|
|
2567
|
+
"[scan] Inspect a specific list of files for hardcoded secrets and, when `fix` is true, replace each finding with `process.env.KEY` while storing the extracted value into the keyring.",
|
|
2568
|
+
"Use to migrate a known set of files (e.g. just-changed files in a pre-commit hook) into q-ring; prefer `scan_codebase_for_secrets` for a whole-tree audit and `import_dotenv` to ingest an existing .env.",
|
|
2569
|
+
"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.'."
|
|
2570
|
+
].join(" "),
|
|
2278
2571
|
{
|
|
2279
|
-
files: z9.array(z9.string()).describe(
|
|
2280
|
-
|
|
2572
|
+
files: z9.array(z9.string()).describe(
|
|
2573
|
+
"Absolute or relative paths to lint. Non-existent paths surface as scan errors."
|
|
2574
|
+
),
|
|
2575
|
+
fix: z9.boolean().optional().default(false).describe(
|
|
2576
|
+
"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."
|
|
2577
|
+
),
|
|
2281
2578
|
scope: scope6,
|
|
2282
2579
|
projectPath: projectPath6,
|
|
2283
2580
|
teamId: teamId6,
|
|
@@ -2306,7 +2603,11 @@ ${result.stderr}`);
|
|
|
2306
2603
|
);
|
|
2307
2604
|
server2.tool(
|
|
2308
2605
|
"analyze_secrets",
|
|
2309
|
-
|
|
2606
|
+
[
|
|
2607
|
+
"[agent] Cross-reference the secrets in scope with recent audit events to produce a usage profile and rotation/retirement suggestions.",
|
|
2608
|
+
"Use as a quarterly hygiene check or as input to a planner that decides what to rotate or delete; prefer `health_check` for decay-only triage and `audit_log` to inspect access timelines for one key.",
|
|
2609
|
+
"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."
|
|
2610
|
+
].join(" "),
|
|
2310
2611
|
{
|
|
2311
2612
|
scope: scope6,
|
|
2312
2613
|
projectPath: projectPath6,
|
|
@@ -2339,32 +2640,46 @@ ${result.stderr}`);
|
|
|
2339
2640
|
let dashboardInstance = null;
|
|
2340
2641
|
server2.tool(
|
|
2341
2642
|
"status_dashboard",
|
|
2342
|
-
|
|
2643
|
+
[
|
|
2644
|
+
"[dashboard] Start a local web dashboard (`http://127.0.0.1:PORT`) that streams live KPIs, secret tables, manifest gaps, hooks, audit events, and anomalies via Server-Sent Events.",
|
|
2645
|
+
"Use when an operator (or an agent on behalf of one) wants a richer visual surface than chat output; prefer `health_check` / `analyze_secrets` for one-shot text summaries inside the conversation.",
|
|
2646
|
+
"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."
|
|
2647
|
+
].join(" "),
|
|
2343
2648
|
{
|
|
2344
|
-
port: z9.number().optional().default(9876).describe(
|
|
2649
|
+
port: z9.number().optional().default(9876).describe(
|
|
2650
|
+
"TCP port to listen on (default 9876). Pick another port if 9876 is already in use; the call fails if binding errors."
|
|
2651
|
+
)
|
|
2345
2652
|
},
|
|
2346
2653
|
async (params) => {
|
|
2347
2654
|
const toolBlock = enforceToolPolicy("status_dashboard");
|
|
2348
2655
|
if (toolBlock) return toolBlock;
|
|
2349
2656
|
if (dashboardInstance) {
|
|
2350
2657
|
return text(
|
|
2351
|
-
`Dashboard already running at
|
|
2658
|
+
`Dashboard already running at ${dashboardInstance.url}`
|
|
2352
2659
|
);
|
|
2353
2660
|
}
|
|
2354
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2661
|
+
const { startDashboardServer } = await import("./dashboard-HHJY73JX.js");
|
|
2355
2662
|
dashboardInstance = startDashboardServer({ port: params.port });
|
|
2356
2663
|
return text(
|
|
2357
|
-
`Dashboard started at
|
|
2358
|
-
Open this URL in a browser to see live quantum status.`
|
|
2664
|
+
`Dashboard started at ${dashboardInstance.url}
|
|
2665
|
+
Open this URL in a browser to see live quantum status. The token is required for access.`
|
|
2359
2666
|
);
|
|
2360
2667
|
}
|
|
2361
2668
|
);
|
|
2362
2669
|
server2.tool(
|
|
2363
2670
|
"agent_scan",
|
|
2364
|
-
|
|
2671
|
+
[
|
|
2672
|
+
"[agent] Run a multi-project health pass that gathers decay status, audit anomalies, and `.q-ring.json` manifest gaps across one or more project paths and (optionally) auto-rotates expired secrets with freshly generated values.",
|
|
2673
|
+
"Use as the canonical 'agent maintenance loop' across a portfolio of repos; prefer `health_check` for a single read-only scope, `detect_anomalies` for audit-only triage, and `check_project` for a single-project manifest check.",
|
|
2674
|
+
"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."
|
|
2675
|
+
].join(" "),
|
|
2365
2676
|
{
|
|
2366
|
-
autoRotate: z9.boolean().optional().default(false).describe(
|
|
2367
|
-
|
|
2677
|
+
autoRotate: z9.boolean().optional().default(false).describe(
|
|
2678
|
+
"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."
|
|
2679
|
+
),
|
|
2680
|
+
projectPaths: z9.array(z9.string()).optional().describe(
|
|
2681
|
+
"List of absolute project roots to scan. Defaults to `[server.cwd]` when omitted."
|
|
2682
|
+
)
|
|
2368
2683
|
},
|
|
2369
2684
|
async (params) => {
|
|
2370
2685
|
const toolBlock = enforceToolPolicy("agent_scan");
|
|
@@ -2383,10 +2698,18 @@ import { z as z10 } from "zod";
|
|
|
2383
2698
|
function registerAgentTools(server2) {
|
|
2384
2699
|
server2.tool(
|
|
2385
2700
|
"agent_remember",
|
|
2386
|
-
|
|
2701
|
+
[
|
|
2702
|
+
"[agent] Persist a non-secret key/value note in encrypted, on-disk agent memory that survives across MCP sessions.",
|
|
2703
|
+
"Use to record stable agent context \u2014 last rotation date for a key, the user's deployment preferences, decisions taken in earlier sessions; do NOT use this to store secrets (use `set_secret` instead) and prefer chat scratchpad for purely transient state.",
|
|
2704
|
+
`Mutates the encrypted memory store. Idempotent: rewriting the same key with a new value simply overwrites. Returns 'Remembered "KEY"' on success.`
|
|
2705
|
+
].join(" "),
|
|
2387
2706
|
{
|
|
2388
|
-
key: z10.string().describe(
|
|
2389
|
-
|
|
2707
|
+
key: z10.string().describe(
|
|
2708
|
+
"Memory key (free-form string). Convention: lowercase dotted namespaces, e.g. 'project.lastDeploy'."
|
|
2709
|
+
),
|
|
2710
|
+
value: z10.string().describe(
|
|
2711
|
+
"Plain-string value to store. JSON-stringify structured data on the caller side if needed."
|
|
2712
|
+
)
|
|
2390
2713
|
},
|
|
2391
2714
|
async (params) => {
|
|
2392
2715
|
const toolBlock = enforceToolPolicy("agent_remember");
|
|
@@ -2397,9 +2720,15 @@ function registerAgentTools(server2) {
|
|
|
2397
2720
|
);
|
|
2398
2721
|
server2.tool(
|
|
2399
2722
|
"agent_recall",
|
|
2400
|
-
|
|
2723
|
+
[
|
|
2724
|
+
"[agent] Read a value from encrypted agent memory, or list every stored key when no specific key is supplied.",
|
|
2725
|
+
"Use at the start of an agent loop to rehydrate prior context, or to look up a single remembered fact; prefer `get_project_context` for a redacted overview of secrets and `get_secret` for actual credential values.",
|
|
2726
|
+
"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'."
|
|
2727
|
+
].join(" "),
|
|
2401
2728
|
{
|
|
2402
|
-
key: z10.string().optional().describe(
|
|
2729
|
+
key: z10.string().optional().describe(
|
|
2730
|
+
"Memory key to read. Omit to list every stored key (without values)."
|
|
2731
|
+
)
|
|
2403
2732
|
},
|
|
2404
2733
|
async (params) => {
|
|
2405
2734
|
const toolBlock = enforceToolPolicy("agent_recall");
|
|
@@ -2419,9 +2748,13 @@ function registerAgentTools(server2) {
|
|
|
2419
2748
|
);
|
|
2420
2749
|
server2.tool(
|
|
2421
2750
|
"agent_forget",
|
|
2422
|
-
|
|
2751
|
+
[
|
|
2752
|
+
"[agent] Permanently delete a single key from encrypted agent memory.",
|
|
2753
|
+
"Use to retract obsolete or misremembered context; prefer overwriting via `agent_remember` when you just want to update the value, and use `delete_secret` for actual credentials (which never live in agent memory).",
|
|
2754
|
+
`Destructive: there is no recycle bin. Returns 'Forgot "KEY"' on success or a not-found error if the key was already absent.`
|
|
2755
|
+
].join(" "),
|
|
2423
2756
|
{
|
|
2424
|
-
key: z10.string().describe("Memory key to
|
|
2757
|
+
key: z10.string().describe("Memory key to delete.")
|
|
2425
2758
|
},
|
|
2426
2759
|
async (params) => {
|
|
2427
2760
|
const toolBlock = enforceToolPolicy("agent_forget");
|
|
@@ -2441,12 +2774,24 @@ var { projectPath: projectPath7 } = commonSchemas;
|
|
|
2441
2774
|
function registerPolicyTools(server2) {
|
|
2442
2775
|
server2.tool(
|
|
2443
2776
|
"check_policy",
|
|
2444
|
-
|
|
2777
|
+
[
|
|
2778
|
+
"[policy] Ask whether a single intended action would be allowed by the project's `.q-ring.json` policy without actually performing it.",
|
|
2779
|
+
"Use as a dry-run before calling a potentially-blocked tool, attempting to read a sensitive key, or invoking `exec_with_secrets` with a non-trivial command; prefer `get_policy_summary` for a one-shot overview of the entire policy.",
|
|
2780
|
+
"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."
|
|
2781
|
+
].join(" "),
|
|
2445
2782
|
{
|
|
2446
|
-
action: z11.enum(["tool", "key_read", "exec"]).describe(
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2783
|
+
action: z11.enum(["tool", "key_read", "exec"]).describe(
|
|
2784
|
+
"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`)."
|
|
2785
|
+
),
|
|
2786
|
+
toolName: z11.string().optional().describe(
|
|
2787
|
+
"Tool id to evaluate, e.g. 'rotate_secret'. Required when `action` is 'tool'."
|
|
2788
|
+
),
|
|
2789
|
+
key: z11.string().optional().describe(
|
|
2790
|
+
"Secret key name to evaluate. Required when `action` is 'key_read'."
|
|
2791
|
+
),
|
|
2792
|
+
command: z11.string().optional().describe(
|
|
2793
|
+
"Command to evaluate against the exec allowlist/denylist. Required when `action` is 'exec'."
|
|
2794
|
+
),
|
|
2450
2795
|
projectPath: projectPath7
|
|
2451
2796
|
},
|
|
2452
2797
|
async (params) => {
|
|
@@ -2470,7 +2815,11 @@ function registerPolicyTools(server2) {
|
|
|
2470
2815
|
);
|
|
2471
2816
|
server2.tool(
|
|
2472
2817
|
"get_policy_summary",
|
|
2473
|
-
|
|
2818
|
+
[
|
|
2819
|
+
"[policy] Return a high-level summary of the project's `.q-ring.json` governance policy \u2014 counts of allow/deny rules for tools, key reads, exec commands, plus approval and rotation requirements.",
|
|
2820
|
+
"Use to orient an agent (or the user) on what guardrails are active before attempting policy-restricted actions; prefer `check_policy` for a precise per-action verdict.",
|
|
2821
|
+
"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."
|
|
2822
|
+
].join(" "),
|
|
2474
2823
|
{
|
|
2475
2824
|
projectPath: projectPath7
|
|
2476
2825
|
},
|
|
@@ -2502,6 +2851,7 @@ function registerMcpTools(server2) {
|
|
|
2502
2851
|
|
|
2503
2852
|
// src/mcp/server.ts
|
|
2504
2853
|
function createMcpServer() {
|
|
2854
|
+
setPolicyRoot(process.cwd());
|
|
2505
2855
|
const server2 = new McpServer({
|
|
2506
2856
|
name: "q-ring",
|
|
2507
2857
|
version: PACKAGE_VERSION
|