@i4ctime/q-ring 0.11.5 → 0.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp.js CHANGED
@@ -246,11 +246,21 @@ function enforceToolPolicy(toolName, projectPath8) {
246
246
  return null;
247
247
  }
248
248
  var commonSchemas = {
249
- teamId: z.string().optional().describe("Team identifier for team-scoped secrets"),
250
- orgId: z.string().optional().describe("Org identifier for org-scoped secrets"),
251
- scope: z.enum(["global", "project", "team", "org"]).optional().describe("Scope: global, project, team, or org"),
252
- projectPath: z.string().optional().describe("Project root path for project-scoped secrets"),
253
- env: z.string().optional().describe("Environment for superposition collapse (e.g., dev, staging, prod)")
249
+ teamId: z.string().optional().describe(
250
+ "Team identifier for team-scoped secrets. Required only when scope='team'. Example: 'acme-platform'."
251
+ ),
252
+ orgId: z.string().optional().describe(
253
+ "Organization identifier for org-scoped secrets. Required only when scope='org'. Example: 'acme-corp'."
254
+ ),
255
+ scope: z.enum(["global", "project", "team", "org"]).optional().describe(
256
+ "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)."
257
+ ),
258
+ projectPath: z.string().optional().describe(
259
+ "Absolute path to the project root for project-scoped secrets and policy resolution. Defaults to the MCP server's current working directory when omitted."
260
+ ),
261
+ env: z.string().optional().describe(
262
+ "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."
263
+ )
254
264
  };
255
265
 
256
266
  // src/mcp/tools/secrets.ts
@@ -258,9 +268,15 @@ var { teamId, orgId, scope, projectPath, env } = commonSchemas;
258
268
  function registerSecretTools(server2) {
259
269
  server2.tool(
260
270
  "get_secret",
261
- "[secrets] Retrieve a secret by key. Collapses superposition if the secret has multiple environment states. Records access in audit log (observer effect).",
271
+ [
272
+ "[secrets] Read the plaintext value of a single secret from the q-ring keyring.",
273
+ "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.",
274
+ "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."
275
+ ].join(" "),
262
276
  {
263
- key: z2.string().describe("The secret key name"),
277
+ key: z2.string().describe(
278
+ "Exact secret key name as stored in the keyring (case-sensitive). Example: 'OPENAI_API_KEY'."
279
+ ),
264
280
  scope,
265
281
  projectPath,
266
282
  env,
@@ -291,14 +307,26 @@ function registerSecretTools(server2) {
291
307
  );
292
308
  server2.tool(
293
309
  "list_secrets",
294
- "[secrets] List all secret keys with quantum metadata (scope, decay status, superposition states, entanglement, access count). Values are never exposed. Supports filtering by tag, expiry state, and key pattern.",
310
+ [
311
+ "[secrets] List secret keys and quantum metadata in the requested scope, never the values.",
312
+ "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.",
313
+ "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."
314
+ ].join(" "),
295
315
  {
296
316
  scope,
297
317
  projectPath,
298
- tag: z2.string().optional().describe("Filter by tag"),
299
- expired: z2.boolean().optional().describe("Show only expired secrets"),
300
- stale: z2.boolean().optional().describe("Show only stale secrets (75%+ decay)"),
301
- filter: z2.string().optional().describe("Glob pattern on key name (e.g., 'API_*')"),
318
+ tag: z2.string().optional().describe(
319
+ "Return only secrets that include this exact tag (case-sensitive). Example: 'production'."
320
+ ),
321
+ expired: z2.boolean().optional().describe(
322
+ "If true, return only secrets whose decay TTL has elapsed (lifetimePercent >= 100)."
323
+ ),
324
+ stale: z2.boolean().optional().describe(
325
+ "If true, return only secrets in the stale window (lifetimePercent >= 75 and not yet expired)."
326
+ ),
327
+ filter: z2.string().optional().describe(
328
+ "Glob pattern matched against the key name. Supports `*` and `?`. Examples: 'API_*', 'STRIPE_?_KEY'."
329
+ ),
302
330
  teamId,
303
331
  orgId
304
332
  },
@@ -338,18 +366,32 @@ function registerSecretTools(server2) {
338
366
  );
339
367
  server2.tool(
340
368
  "set_secret",
341
- "[secrets] Create or overwrite a secret value plus optional metadata (TTL/decay, per-env superposition, description, tags). Overwrites existing values for the same key/scope; records access in the audit log. Use import_dotenv for bulk .env ingest. Subject to tool policy; no external rate limits beyond provider calls when validating elsewhere.",
369
+ [
370
+ "[secrets] Create or overwrite a single secret value, optionally with TTL/decay, per-env superposition, description, tags, and rotation hints.",
371
+ "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.",
372
+ "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)."
373
+ ].join(" "),
342
374
  {
343
- key: z2.string().describe("The secret key name"),
344
- value: z2.string().describe("The secret value"),
375
+ key: z2.string().describe(
376
+ "Secret key name (UPPER_SNAKE_CASE recommended). Example: 'STRIPE_SECRET_KEY'."
377
+ ),
378
+ value: z2.string().describe(
379
+ "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."
380
+ ),
345
381
  scope: scope.default("global"),
346
382
  projectPath,
347
383
  env: z2.string().optional().describe(
348
- "If provided, sets the value for this specific environment (superposition)"
384
+ "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'."
385
+ ),
386
+ ttlSeconds: z2.number().optional().describe(
387
+ "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."
388
+ ),
389
+ description: z2.string().optional().describe(
390
+ "Free-text human-readable description shown in `inspect_secret` and the dashboard."
391
+ ),
392
+ tags: z2.array(z2.string()).optional().describe(
393
+ "Tag list for filtering and hook matching. Example: ['production', 'payments']."
349
394
  ),
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
395
  rotationFormat: z2.enum([
354
396
  "hex",
355
397
  "base64",
@@ -358,8 +400,12 @@ function registerSecretTools(server2) {
358
400
  "api-key",
359
401
  "token",
360
402
  "password"
361
- ]).optional().describe("Format for auto-rotation when this secret expires"),
362
- rotationPrefix: z2.string().optional().describe("Prefix for auto-rotation (e.g. 'sk-')"),
403
+ ]).optional().describe(
404
+ "Format used by `agent_scan --autoRotate` and `rotate_secret` when this secret expires. Pick the format that matches the upstream service's accepted shape."
405
+ ),
406
+ rotationPrefix: z2.string().optional().describe(
407
+ "Literal prefix prepended on auto-rotation (only used with rotationFormat 'api-key' or 'token'). Example: 'sk-'."
408
+ ),
363
409
  teamId,
364
410
  orgId
365
411
  },
@@ -401,9 +447,13 @@ function registerSecretTools(server2) {
401
447
  );
402
448
  server2.tool(
403
449
  "delete_secret",
404
- "[secrets] Permanently remove a secret value from the keyring for the given scope/path (not recoverable from q-ring). Does not remove hooks, tunnels, or entanglement metadata alone\u2014use remove_hook, tunnel_destroy, or disentangle_secrets respectively. Returns success or not-found text; subject to tool policy.",
450
+ [
451
+ "[secrets] Permanently remove a secret value (and all its env states) from the keyring for the given scope.",
452
+ "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.",
453
+ `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.`
454
+ ].join(" "),
405
455
  {
406
- key: z2.string().describe("The secret key name"),
456
+ key: z2.string().describe("Exact secret key name to delete. Example: 'OLD_API_KEY'."),
407
457
  scope,
408
458
  projectPath,
409
459
  teamId,
@@ -421,9 +471,13 @@ function registerSecretTools(server2) {
421
471
  );
422
472
  server2.tool(
423
473
  "has_secret",
424
- "[secrets] Check if a secret exists. Returns boolean. Never reveals the value. Respects decay \u2014 expired secrets return false.",
474
+ [
475
+ "[secrets] Check whether a secret exists in the requested scope without reading the value.",
476
+ "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.",
477
+ "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'."
478
+ ].join(" "),
425
479
  {
426
- key: z2.string().describe("The secret key name"),
480
+ key: z2.string().describe("Exact secret key name. Example: 'GITHUB_TOKEN'."),
427
481
  scope,
428
482
  projectPath,
429
483
  teamId,
@@ -437,11 +491,21 @@ function registerSecretTools(server2) {
437
491
  );
438
492
  server2.tool(
439
493
  "export_secrets",
440
- "[secrets] Export secrets as .env or JSON format. Collapses superposition. Supports filtering by specific keys or tags.",
494
+ [
495
+ "[secrets] Render multiple secrets as a single .env or JSON document for piping into another tool or file.",
496
+ "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.",
497
+ "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."
498
+ ].join(" "),
441
499
  {
442
- format: z2.enum(["env", "json"]).optional().default("env").describe("Output format"),
443
- keys: z2.array(z2.string()).optional().describe("Only export these specific key names"),
444
- tags: z2.array(z2.string()).optional().describe("Only export secrets with any of these tags"),
500
+ format: z2.enum(["env", "json"]).optional().default("env").describe(
501
+ `'env' renders KEY="value" lines suitable for a .env file; 'json' renders an object keyed by secret name. Defaults to 'env'.`
502
+ ),
503
+ keys: z2.array(z2.string()).optional().describe(
504
+ "Whitelist of exact key names to include. If omitted, every key in scope is considered (subject to `tags`)."
505
+ ),
506
+ tags: z2.array(z2.string()).optional().describe(
507
+ "Include only secrets tagged with at least one of these tags. Combined with `keys` as an AND filter when both are supplied."
508
+ ),
445
509
  scope,
446
510
  projectPath,
447
511
  env,
@@ -463,13 +527,23 @@ function registerSecretTools(server2) {
463
527
  );
464
528
  server2.tool(
465
529
  "import_dotenv",
466
- "[secrets] Import secrets from .env file content. Parses standard dotenv syntax (comments, quotes, multiline escapes) and stores each key/value pair in q-ring.",
530
+ [
531
+ "[secrets] Parse standard dotenv-formatted text and store each key/value pair into the keyring in one batch.",
532
+ "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.",
533
+ "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."
534
+ ].join(" "),
467
535
  {
468
- content: z2.string().describe("The .env file content to parse and import"),
536
+ content: z2.string().describe(
537
+ "Raw .env file content as a single string (newline-separated KEY=VALUE lines, comments allowed)."
538
+ ),
469
539
  scope: scope.default("global"),
470
540
  projectPath,
471
- skipExisting: z2.boolean().optional().default(false).describe("Skip keys that already exist in q-ring"),
472
- dryRun: z2.boolean().optional().default(false).describe("Preview what would be imported without saving")
541
+ skipExisting: z2.boolean().optional().default(false).describe(
542
+ "If true, leave already-present keys untouched and add them to the 'skipped' list instead of overwriting."
543
+ ),
544
+ dryRun: z2.boolean().optional().default(false).describe(
545
+ "If true, parse and report what would happen but do not write to the keyring. Useful for previewing imports before committing."
546
+ )
473
547
  },
474
548
  async (params) => {
475
549
  const toolBlock = enforceToolPolicy("import_dotenv", params.projectPath);
@@ -495,9 +569,13 @@ function registerSecretTools(server2) {
495
569
  );
496
570
  server2.tool(
497
571
  "inspect_secret",
498
- "[secrets] Show full quantum state of a secret: superposition states, decay status, entanglement links, access history. Never reveals the actual value.",
572
+ [
573
+ "[secrets] Show full metadata for a single secret \u2014 env states, decay window, entanglement links, access counters \u2014 without ever revealing the value.",
574
+ "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.",
575
+ "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."
576
+ ].join(" "),
499
577
  {
500
- key: z2.string().describe("The secret key name"),
578
+ key: z2.string().describe("Exact secret key name to inspect. Example: 'OPENAI_API_KEY'."),
501
579
  scope,
502
580
  projectPath,
503
581
  teamId,
@@ -542,7 +620,11 @@ function registerSecretTools(server2) {
542
620
  );
543
621
  server2.tool(
544
622
  "generate_secret",
545
- "[secrets] Generate a cryptographic secret (quantum noise). Formats: hex, base64, alphanumeric, uuid, api-key, token, password. Optionally save directly to the keyring.",
623
+ [
624
+ "[secrets] Generate a cryptographically random secret using Node's CSPRNG and optionally store it in the keyring in one step.",
625
+ "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.",
626
+ "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."
627
+ ].join(" "),
546
628
  {
547
629
  format: z2.enum([
548
630
  "hex",
@@ -552,10 +634,18 @@ function registerSecretTools(server2) {
552
634
  "api-key",
553
635
  "token",
554
636
  "password"
555
- ]).optional().default("api-key").describe("Output format"),
556
- length: z2.number().optional().describe("Length in bytes or characters"),
557
- prefix: z2.string().optional().describe("Prefix for api-key/token format"),
558
- saveAs: z2.string().optional().describe("If provided, save the generated secret with this key name"),
637
+ ]).optional().default("api-key").describe(
638
+ "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'."
639
+ ),
640
+ length: z2.number().optional().describe(
641
+ "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)."
642
+ ),
643
+ prefix: z2.string().optional().describe(
644
+ "Literal prefix prepended to the random portion. Only meaningful for 'api-key' and 'token'. Example: 'sk-' or 'svc_'."
645
+ ),
646
+ saveAs: z2.string().optional().describe(
647
+ "If provided, store the generated value at this key name in the keyring (one mutation). Omit to just return the value without persisting."
648
+ ),
559
649
  scope: scope.default("global"),
560
650
  projectPath,
561
651
  teamId,
@@ -584,14 +674,22 @@ function registerSecretTools(server2) {
584
674
  );
585
675
  server2.tool(
586
676
  "entangle_secrets",
587
- "[secrets] Link two keys so source updates/rotations propagate the same value to the target (mutates metadata; future writes sync). Reverse with disentangle_secrets without deleting values; do not confuse with set_secret (single-key write). Subject to tool policy.",
677
+ [
678
+ "[secrets] Link two keys (across the same or different scopes) so future writes/rotations of either propagate the same value to the other.",
679
+ "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).",
680
+ "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'."
681
+ ].join(" "),
588
682
  {
589
- sourceKey: z2.string().describe("Source secret key"),
590
- targetKey: z2.string().describe("Target secret key"),
683
+ sourceKey: z2.string().describe("First secret key in the pair. Example: 'STRIPE_SECRET_KEY'."),
684
+ targetKey: z2.string().describe("Second secret key to keep in lockstep with the source."),
591
685
  sourceScope: scope.default("global"),
592
686
  targetScope: scope.default("global"),
593
- sourceProjectPath: z2.string().optional(),
594
- targetProjectPath: z2.string().optional()
687
+ sourceProjectPath: z2.string().optional().describe(
688
+ "Project root for sourceKey when sourceScope='project'. Defaults to the server cwd."
689
+ ),
690
+ targetProjectPath: z2.string().optional().describe(
691
+ "Project root for targetKey when targetScope='project'. Defaults to the server cwd."
692
+ )
595
693
  },
596
694
  async (params) => {
597
695
  const toolBlock = enforceToolPolicy(
@@ -618,14 +716,18 @@ function registerSecretTools(server2) {
618
716
  );
619
717
  server2.tool(
620
718
  "disentangle_secrets",
621
- "[secrets] Remove the sync link between two keys so rotations stop propagating. Does not delete either secret\u2014use delete_secret to erase values. Contrast entangle_secrets (creates link). Safe if the link was already absent; updates metadata; subject to tool policy.",
719
+ [
720
+ "[secrets] Break the sync link between two previously entangled keys so future rotations no longer propagate.",
721
+ "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.",
722
+ "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'."
723
+ ].join(" "),
622
724
  {
623
- sourceKey: z2.string().describe("Source secret key"),
624
- targetKey: z2.string().describe("Target secret key"),
725
+ sourceKey: z2.string().describe("First key in the previously linked pair."),
726
+ targetKey: z2.string().describe("Second key in the previously linked pair."),
625
727
  sourceScope: scope.default("global"),
626
728
  targetScope: scope.default("global"),
627
- sourceProjectPath: z2.string().optional(),
628
- targetProjectPath: z2.string().optional()
729
+ sourceProjectPath: z2.string().optional().describe("Project root for sourceKey when sourceScope='project'."),
730
+ targetProjectPath: z2.string().optional().describe("Project root for targetKey when targetScope='project'.")
629
731
  },
630
732
  async (params) => {
631
733
  const toolBlock = enforceToolPolicy(
@@ -934,7 +1036,11 @@ var { teamId: teamId2, orgId: orgId2, scope: scope2, projectPath: projectPath2,
934
1036
  function registerProjectTools(server2) {
935
1037
  server2.tool(
936
1038
  "check_project",
937
- "[project] Validate project secrets against the .q-ring.json manifest. Returns which required secrets are present, missing, expired, or stale. Use this to verify project readiness.",
1039
+ [
1040
+ "[project] Compare the keys declared in the project's `.q-ring.json` manifest against what is actually present in the keyring.",
1041
+ "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.",
1042
+ "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."
1043
+ ].join(" "),
938
1044
  {
939
1045
  projectPath: projectPath2
940
1046
  },
@@ -1001,7 +1107,11 @@ function registerProjectTools(server2) {
1001
1107
  );
1002
1108
  server2.tool(
1003
1109
  "env_generate",
1004
- "[project] Generate .env file content from the project manifest (.q-ring.json). Resolves each declared secret from q-ring, collapses superposition, and returns .env formatted output. Warns about missing or expired secrets.",
1110
+ [
1111
+ "[project] Render a complete `.env` file body from the project's `.q-ring.json` manifest, resolving each declared key from the keyring.",
1112
+ "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.",
1113
+ "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."
1114
+ ].join(" "),
1005
1115
  {
1006
1116
  projectPath: projectPath2,
1007
1117
  env: env2
@@ -1048,7 +1158,11 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1048
1158
  );
1049
1159
  server2.tool(
1050
1160
  "detect_environment",
1051
- "[project] Detect the current environment context (wavefunction collapse). Returns the detected environment and its source (NODE_ENV, git branch, project config, etc.).",
1161
+ [
1162
+ "[project] Resolve which environment slug (e.g. 'dev', 'staging', 'prod') the current invocation should collapse to.",
1163
+ "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.",
1164
+ "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."
1165
+ ].join(" "),
1052
1166
  {
1053
1167
  projectPath: projectPath2
1054
1168
  },
@@ -1071,7 +1185,11 @@ ${warnings.map((w) => `# ${w}`).join("\n")}` : output;
1071
1185
  );
1072
1186
  server2.tool(
1073
1187
  "get_project_context",
1074
- "[agent] Get a safe, redacted overview of the project's secrets, environment, manifest, providers, hooks, and recent audit activity. No secret values are ever exposed. Use this to understand what secrets exist before asking to read them.",
1188
+ [
1189
+ "[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.",
1190
+ "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.",
1191
+ "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."
1192
+ ].join(" "),
1075
1193
  {
1076
1194
  scope: scope2,
1077
1195
  projectPath: projectPath2,
@@ -1095,11 +1213,21 @@ import { z as z3 } from "zod";
1095
1213
  function registerTunnelTools(server2) {
1096
1214
  server2.tool(
1097
1215
  "tunnel_create",
1098
- "[tunnel] Create an ephemeral secret that exists only in memory (quantum tunneling). Never persisted to disk. Optional TTL and max-reads for self-destruction.",
1216
+ [
1217
+ "[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.",
1218
+ "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.",
1219
+ "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`."
1220
+ ].join(" "),
1099
1221
  {
1100
- value: z3.string().describe("The secret value"),
1101
- ttlSeconds: z3.number().optional().describe("Auto-expire after N seconds"),
1102
- maxReads: z3.number().optional().describe("Self-destruct after N reads")
1222
+ value: z3.string().describe(
1223
+ "The plaintext value to tunnel. Held only in process memory; never logged."
1224
+ ),
1225
+ ttlSeconds: z3.number().optional().describe(
1226
+ "Auto-destroy the tunnel after this many seconds. Omit for no time limit (then a `maxReads` is highly recommended)."
1227
+ ),
1228
+ maxReads: z3.number().optional().describe(
1229
+ "Self-destruct after this many successful `tunnel_read` calls. Use 1 for true one-shot delivery."
1230
+ )
1103
1231
  },
1104
1232
  async (params) => {
1105
1233
  const toolBlock = enforceToolPolicy("tunnel_create");
@@ -1113,9 +1241,15 @@ function registerTunnelTools(server2) {
1113
1241
  );
1114
1242
  server2.tool(
1115
1243
  "tunnel_read",
1116
- "[tunnel] Read an ephemeral tunneled secret by ID. May self-destruct if max-reads is reached.",
1244
+ [
1245
+ "[tunnel] Fetch the value stashed by a prior `tunnel_create` call by its ID.",
1246
+ "Use exactly once per intended consumer; the value is destructive-by-design and may self-delete after this call.",
1247
+ "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."
1248
+ ].join(" "),
1117
1249
  {
1118
- id: z3.string().describe("Tunnel ID")
1250
+ id: z3.string().describe(
1251
+ "The opaque tunnel ID returned by `tunnel_create`. Case-sensitive."
1252
+ )
1119
1253
  },
1120
1254
  async (params) => {
1121
1255
  const toolBlock = enforceToolPolicy("tunnel_read");
@@ -1131,7 +1265,11 @@ function registerTunnelTools(server2) {
1131
1265
  );
1132
1266
  server2.tool(
1133
1267
  "tunnel_list",
1134
- "[tunnel] List active tunneled secrets (IDs and metadata only, never values).",
1268
+ [
1269
+ "[tunnel] Enumerate all currently-active tunnels in the q-ring server with their remaining read budget and time-to-live.",
1270
+ "Use to audit what is still in memory or to look up an ID you forgot; values are never included in the output.",
1271
+ "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."
1272
+ ].join(" "),
1135
1273
  {},
1136
1274
  async () => {
1137
1275
  const toolBlock = enforceToolPolicy("tunnel_list");
@@ -1156,9 +1294,13 @@ function registerTunnelTools(server2) {
1156
1294
  );
1157
1295
  server2.tool(
1158
1296
  "tunnel_destroy",
1159
- "[tunnel] Immediately destroy a tunneled secret.",
1297
+ [
1298
+ "[tunnel] Immediately remove a tunnel from memory, regardless of remaining reads or TTL.",
1299
+ "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.",
1300
+ "Mutates in-memory state only. Returns 'Destroyed ID' on success or a not-found error if the ID is unknown or already gone."
1301
+ ].join(" "),
1160
1302
  {
1161
- id: z3.string().describe("Tunnel ID")
1303
+ id: z3.string().describe("The opaque tunnel ID to destroy.")
1162
1304
  },
1163
1305
  async (params) => {
1164
1306
  const toolBlock = enforceToolPolicy("tunnel_destroy");
@@ -1293,10 +1435,18 @@ var { teamId: teamId3, orgId: orgId3, scope: scope3, projectPath: projectPath3 }
1293
1435
  function registerTeleportTools(server2) {
1294
1436
  server2.tool(
1295
1437
  "teleport_pack",
1296
- "[teleport] Pack secrets into an AES-256-GCM encrypted bundle for sharing between machines (quantum teleportation).",
1438
+ [
1439
+ "[teleport] Encrypt one or more secrets into a single AES-256-GCM bundle string that can be safely transferred between machines.",
1440
+ "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.",
1441
+ "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."
1442
+ ].join(" "),
1297
1443
  {
1298
- keys: z5.array(z5.string()).optional().describe("Specific keys to pack (all if omitted)"),
1299
- passphrase: z5.string().describe("Encryption passphrase"),
1444
+ keys: z5.array(z5.string()).optional().describe(
1445
+ "Whitelist of exact key names to include. Omit to pack every secret in the requested scope."
1446
+ ),
1447
+ passphrase: z5.string().describe(
1448
+ "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."
1449
+ ),
1300
1450
  scope: scope3,
1301
1451
  projectPath: projectPath3,
1302
1452
  teamId: teamId3,
@@ -1322,15 +1472,25 @@ function registerTeleportTools(server2) {
1322
1472
  );
1323
1473
  server2.tool(
1324
1474
  "teleport_unpack",
1325
- "[teleport] Decrypt and import secrets from a teleport bundle.",
1475
+ [
1476
+ "[teleport] Decrypt a bundle produced by `teleport_pack` and import each contained secret into the local keyring.",
1477
+ "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.",
1478
+ "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."
1479
+ ].join(" "),
1326
1480
  {
1327
- bundle: z5.string().describe("Base64-encoded encrypted bundle"),
1328
- passphrase: z5.string().describe("Decryption passphrase"),
1481
+ bundle: z5.string().describe(
1482
+ "Base64-encoded ciphertext returned by `teleport_pack`. Pass through whitespace untouched if possible."
1483
+ ),
1484
+ passphrase: z5.string().describe(
1485
+ "The same passphrase that was used to pack this bundle. Bad passphrases return an authentication error rather than wrong plaintext."
1486
+ ),
1329
1487
  scope: scope3.default("global"),
1330
1488
  projectPath: projectPath3,
1331
1489
  teamId: teamId3,
1332
1490
  orgId: orgId3,
1333
- dryRun: z5.boolean().optional().default(false).describe("Preview without importing")
1491
+ dryRun: z5.boolean().optional().default(false).describe(
1492
+ "If true, decrypt and report what would be written but do not mutate the keyring. Useful for verifying bundle contents before commit."
1493
+ )
1334
1494
  },
1335
1495
  async (params) => {
1336
1496
  const toolBlock = enforceToolPolicy("teleport_unpack", params.projectPath);
@@ -1365,9 +1525,15 @@ var { teamId: teamId4, orgId: orgId4, scope: scope4, projectPath: projectPath4 }
1365
1525
  function registerAuditTools(server2) {
1366
1526
  server2.tool(
1367
1527
  "audit_log",
1368
- "[audit] Query the audit log for secret access history (observer effect). Shows who accessed what and when.",
1528
+ [
1529
+ "[audit] Query the q-ring audit log \u2014 a tamper-evident record of every read/write/delete touching a secret.",
1530
+ "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.",
1531
+ "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."
1532
+ ].join(" "),
1369
1533
  {
1370
- key: z6.string().optional().describe("Filter by key"),
1534
+ key: z6.string().optional().describe(
1535
+ "Limit to events touching this exact key. Omit for the full log."
1536
+ ),
1371
1537
  action: z6.enum([
1372
1538
  "read",
1373
1539
  "write",
@@ -1379,8 +1545,12 @@ function registerAuditTools(server2) {
1379
1545
  "tunnel",
1380
1546
  "teleport",
1381
1547
  "collapse"
1382
- ]).optional().describe("Filter by action"),
1383
- limit: z6.number().optional().default(20).describe("Max events to return")
1548
+ ]).optional().describe(
1549
+ "Limit to a single action verb (e.g. 'read' to see only reads). Omit for all actions."
1550
+ ),
1551
+ limit: z6.number().optional().default(20).describe(
1552
+ "Maximum events to return, newest first. Defaults to 20. Increase for deeper investigations."
1553
+ )
1384
1554
  },
1385
1555
  async (params) => {
1386
1556
  const toolBlock = enforceToolPolicy("audit_log");
@@ -1404,9 +1574,15 @@ function registerAuditTools(server2) {
1404
1574
  );
1405
1575
  server2.tool(
1406
1576
  "detect_anomalies",
1407
- "[audit] Read-only scan of audit history for burst reads and unusual-hour access (text lines per finding). Optional key filter. Use health_check for full decay inventory + anomaly count in scope; use agent_scan for multi-project JSON reports or optional auto-rotation. Does not mutate secrets.",
1577
+ [
1578
+ "[audit] Scan the audit history for suspicious access patterns \u2014 burst reads of the same key, off-hours access, and other heuristics.",
1579
+ "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.",
1580
+ "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."
1581
+ ].join(" "),
1408
1582
  {
1409
- key: z6.string().optional().describe("Check anomalies for a specific key")
1583
+ key: z6.string().optional().describe(
1584
+ "If provided, narrow the scan to this exact key. Omit to scan across every key in the audit log."
1585
+ )
1410
1586
  },
1411
1587
  async (params) => {
1412
1588
  const toolBlock = enforceToolPolicy("detect_anomalies");
@@ -1419,7 +1595,11 @@ function registerAuditTools(server2) {
1419
1595
  );
1420
1596
  server2.tool(
1421
1597
  "health_check",
1422
- "[health] Read-only scoped pass: decay/stale/expired counts, per-secret issue lines, plus audit-derived anomalies. No writes. Use check_project for .q-ring.json manifest compliance; use detect_anomalies for audit-pattern-only triage; use agent_scan for multi-project JSON or optional autoRotate credential replacement.",
1598
+ [
1599
+ "[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.",
1600
+ "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.",
1601
+ "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."
1602
+ ].join(" "),
1423
1603
  {
1424
1604
  scope: scope4,
1425
1605
  projectPath: projectPath4,
@@ -1473,7 +1653,11 @@ function registerAuditTools(server2) {
1473
1653
  );
1474
1654
  server2.tool(
1475
1655
  "verify_audit_chain",
1476
- "[audit] Verify the tamper-evident hash chain of the audit log. Returns integrity status and the first break point if tampered.",
1656
+ [
1657
+ "[audit] Recompute the SHA-256 hash chain over the audit log and confirm no event has been mutated, deleted, or reordered.",
1658
+ "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.",
1659
+ "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."
1660
+ ].join(" "),
1477
1661
  {},
1478
1662
  async () => {
1479
1663
  const toolBlock = enforceToolPolicy("verify_audit_chain");
@@ -1484,11 +1668,21 @@ function registerAuditTools(server2) {
1484
1668
  );
1485
1669
  server2.tool(
1486
1670
  "export_audit",
1487
- "[audit] Export audit events in a portable format (jsonl, json, or csv) with optional time range filtering.",
1671
+ [
1672
+ "[audit] Export the audit log as a portable text artifact suitable for archiving or feeding into another SIEM/analyzer.",
1673
+ "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.",
1674
+ "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."
1675
+ ].join(" "),
1488
1676
  {
1489
- since: z6.string().optional().describe("Start date (ISO 8601)"),
1490
- until: z6.string().optional().describe("End date (ISO 8601)"),
1491
- format: z6.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe("Output format")
1677
+ since: z6.string().optional().describe(
1678
+ "Inclusive lower bound on event timestamp, ISO 8601. Example: '2026-04-01T00:00:00Z'. Omit for no lower bound."
1679
+ ),
1680
+ until: z6.string().optional().describe(
1681
+ "Inclusive upper bound on event timestamp, ISO 8601. Omit for now/no upper bound."
1682
+ ),
1683
+ format: z6.enum(["jsonl", "json", "csv"]).optional().default("jsonl").describe(
1684
+ "Output format. 'jsonl' (default) is most stream-friendly; 'json' is a single array; 'csv' is spreadsheet-friendly."
1685
+ )
1492
1686
  },
1493
1687
  async (params) => {
1494
1688
  const toolBlock = enforceToolPolicy("export_audit");
@@ -1509,10 +1703,18 @@ var { teamId: teamId5, orgId: orgId5, scope: scope5, projectPath: projectPath5 }
1509
1703
  function registerValidationTools(server2) {
1510
1704
  server2.tool(
1511
1705
  "validate_secret",
1512
- "[validation] Test if a secret is actually valid with its target service (e.g., OpenAI, Stripe, GitHub). Uses provider auto-detection based on key prefixes, or accepts an explicit provider name. Never logs the secret value.",
1706
+ [
1707
+ "[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.",
1708
+ "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.",
1709
+ "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)."
1710
+ ].join(" "),
1513
1711
  {
1514
- key: z7.string().describe("The secret key name"),
1515
- provider: z7.string().optional().describe("Force a specific provider (openai, stripe, github, aws, http)"),
1712
+ key: z7.string().describe(
1713
+ "The exact key whose value should be tested upstream. Example: 'OPENAI_API_KEY'."
1714
+ ),
1715
+ provider: z7.string().optional().describe(
1716
+ "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."
1717
+ ),
1516
1718
  scope: scope5,
1517
1719
  projectPath: projectPath5,
1518
1720
  teamId: teamId5,
@@ -1531,7 +1733,11 @@ function registerValidationTools(server2) {
1531
1733
  );
1532
1734
  server2.tool(
1533
1735
  "list_providers",
1534
- "[validation] List all available validation providers for secret liveness testing.",
1736
+ [
1737
+ "[validation] Enumerate the secret-validation providers q-ring knows how to call (OpenAI, Stripe, GitHub, \u2026) along with their auto-detect prefixes.",
1738
+ "Use to discover what `provider` string to pass to `validate_secret`/`rotate_secret`, or to check whether your custom provider is registered.",
1739
+ "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."
1740
+ ].join(" "),
1535
1741
  {},
1536
1742
  async () => {
1537
1743
  const toolBlock = enforceToolPolicy("list_providers");
@@ -1546,10 +1752,16 @@ function registerValidationTools(server2) {
1546
1752
  );
1547
1753
  server2.tool(
1548
1754
  "rotate_secret",
1549
- "[validation] Attempt issuer-native rotation of a secret via its detected or specified provider. Returns rotation result.",
1755
+ [
1756
+ "[validation] Ask the upstream provider to issue a fresh credential for this secret and store the new value back into the keyring.",
1757
+ "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.",
1758
+ "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."
1759
+ ].join(" "),
1550
1760
  {
1551
- key: z7.string().describe("The secret key to rotate"),
1552
- provider: z7.string().optional().describe("Force a specific provider"),
1761
+ key: z7.string().describe("Exact key to rotate. Must already exist in the keyring."),
1762
+ provider: z7.string().optional().describe(
1763
+ "Force a specific provider id (see `list_providers`). Omit to auto-detect from the current value or the secret's stored provider hint."
1764
+ ),
1553
1765
  scope: scope5,
1554
1766
  projectPath: projectPath5,
1555
1767
  teamId: teamId5,
@@ -1573,7 +1785,11 @@ function registerValidationTools(server2) {
1573
1785
  );
1574
1786
  server2.tool(
1575
1787
  "ci_validate_secrets",
1576
- "[validation] CI-oriented batch validation: validates all accessible secrets against their providers and returns a structured pass/fail report.",
1788
+ [
1789
+ "[validation] Validate every accessible secret in the requested scope against its detected provider in a single batch and return a structured pass/fail report.",
1790
+ "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.",
1791
+ "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."
1792
+ ].join(" "),
1577
1793
  {
1578
1794
  scope: scope5,
1579
1795
  projectPath: projectPath5,
@@ -1613,19 +1829,45 @@ import { z as z8 } from "zod";
1613
1829
  function registerHookTools(server2) {
1614
1830
  server2.tool(
1615
1831
  "register_hook",
1616
- "[hooks] Register a webhook/callback that fires when a secret is updated, deleted, or rotated. Supports shell commands, HTTP webhooks, and process signals.",
1832
+ [
1833
+ "[hooks] Register a side-effect (shell command, HTTP webhook, or process signal) that fires automatically when a matching secret is written, deleted, or rotated.",
1834
+ "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).",
1835
+ "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`)."
1836
+ ].join(" "),
1617
1837
  {
1618
- type: z8.enum(["shell", "http", "signal"]).describe("Hook type"),
1619
- key: z8.string().optional().describe("Trigger on exact key match"),
1620
- keyPattern: z8.string().optional().describe("Trigger on key glob pattern (e.g. DB_*)"),
1621
- tag: z8.string().optional().describe("Trigger on secrets with this tag"),
1622
- scope: z8.enum(["global", "project"]).optional().describe("Trigger only for this scope"),
1623
- actions: z8.array(z8.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe("Which actions trigger this hook"),
1624
- command: z8.string().optional().describe("Shell command to execute (for shell type)"),
1625
- url: z8.string().optional().describe("URL to POST to (for http type)"),
1626
- signalTarget: z8.string().optional().describe("Process name or PID (for signal type)"),
1627
- signalName: z8.string().optional().default("SIGHUP").describe("Signal to send (for signal type)"),
1628
- description: z8.string().optional().describe("Human-readable description")
1838
+ type: z8.enum(["shell", "http", "signal"]).describe(
1839
+ "Hook delivery mechanism. 'shell' runs a local command, 'http' POSTs JSON to a URL, 'signal' sends an OS signal to a named process."
1840
+ ),
1841
+ key: z8.string().optional().describe(
1842
+ "Trigger only on this exact key name. Pick at most one of `key` / `keyPattern` / `tag` (or combine for stricter matching)."
1843
+ ),
1844
+ keyPattern: z8.string().optional().describe(
1845
+ "Trigger on any key matching this glob pattern. Examples: 'DB_*', 'STRIPE_*'."
1846
+ ),
1847
+ tag: z8.string().optional().describe(
1848
+ "Trigger on any secret carrying this exact tag. Combinable with key/keyPattern as an AND filter."
1849
+ ),
1850
+ scope: z8.enum(["global", "project"]).optional().describe(
1851
+ "Restrict the hook to secrets in this scope. Omit to fire across both global and project secrets."
1852
+ ),
1853
+ actions: z8.array(z8.enum(["write", "delete", "rotate"])).optional().default(["write", "delete", "rotate"]).describe(
1854
+ "Which lifecycle actions trigger this hook. Defaults to all three."
1855
+ ),
1856
+ command: z8.string().optional().describe(
1857
+ "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."
1858
+ ),
1859
+ url: z8.string().optional().describe(
1860
+ "Required when type='http'. Full URL to POST a JSON body `{ id, key, scope, action, timestamp }` to (the value itself is never sent)."
1861
+ ),
1862
+ signalTarget: z8.string().optional().describe(
1863
+ "Required when type='signal'. Either a numeric PID or a process name resolvable via `ps`."
1864
+ ),
1865
+ signalName: z8.string().optional().default("SIGHUP").describe(
1866
+ "Signal name to send (e.g. 'SIGHUP', 'SIGUSR1'). Defaults to SIGHUP, which most daemons treat as 'reload config'."
1867
+ ),
1868
+ description: z8.string().optional().describe(
1869
+ "Free-text human-readable description, surfaced by `list_hooks` and the dashboard."
1870
+ )
1629
1871
  },
1630
1872
  async (params) => {
1631
1873
  const toolBlock = enforceToolPolicy("register_hook");
@@ -1656,7 +1898,11 @@ function registerHookTools(server2) {
1656
1898
  );
1657
1899
  server2.tool(
1658
1900
  "list_hooks",
1659
- "[hooks] List all registered secret change hooks with their match criteria, type, and status.",
1901
+ [
1902
+ "[hooks] Enumerate every registered lifecycle hook with its match criteria, delivery type, enabled flag, and description.",
1903
+ "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.",
1904
+ "Read-only. Returns pretty-printed JSON array of hook entries, or 'No hooks registered' when the registry is empty."
1905
+ ].join(" "),
1660
1906
  {},
1661
1907
  async () => {
1662
1908
  const toolBlock = enforceToolPolicy("list_hooks");
@@ -1668,9 +1914,15 @@ function registerHookTools(server2) {
1668
1914
  );
1669
1915
  server2.tool(
1670
1916
  "remove_hook",
1671
- "[hooks] Remove a lifecycle hook entry by id from the hook registry only (stops callbacks; does not touch secret values). Call list_hooks first for ids. Contrast delete_secret (credential removal) or tunnel_destroy (ephemeral tunnel). Returns success or not-found; subject to tool policy.",
1917
+ [
1918
+ "[hooks] Detach a single lifecycle hook by its registry id so it stops firing.",
1919
+ "Use to retire a specific webhook/command without touching any secrets; prefer `delete_secret` to remove a credential and `tunnel_destroy` for ephemeral tunnels.",
1920
+ "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."
1921
+ ].join(" "),
1672
1922
  {
1673
- id: z8.string().describe("Hook ID to remove")
1923
+ id: z8.string().describe(
1924
+ "Hook id returned by `register_hook` or visible in `list_hooks` (opaque string)."
1925
+ )
1674
1926
  },
1675
1927
  async (params) => {
1676
1928
  const toolBlock = enforceToolPolicy("remove_hook");
@@ -2203,13 +2455,27 @@ var { teamId: teamId6, orgId: orgId6, scope: scope6, projectPath: projectPath6 }
2203
2455
  function registerToolingTools(server2) {
2204
2456
  server2.tool(
2205
2457
  "exec_with_secrets",
2206
- "[exec] Run a shell command securely. Project secrets are injected into the environment, and any secret values in the output are automatically redacted to prevent leaking into transcripts.",
2458
+ [
2459
+ "[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.",
2460
+ "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.",
2461
+ "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."
2462
+ ].join(" "),
2207
2463
  {
2208
- command: z9.string().describe("Command to run"),
2209
- args: z9.array(z9.string()).optional().describe("Command arguments"),
2210
- keys: z9.array(z9.string()).optional().describe("Only inject these specific keys"),
2211
- tags: z9.array(z9.string()).optional().describe("Only inject secrets with these tags"),
2212
- profile: z9.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe("Exec profile: unrestricted, restricted, or ci"),
2464
+ command: z9.string().describe(
2465
+ "Executable name or full command to run. Example: 'pnpm', 'node', '/usr/bin/env'. Must be allowed by exec policy."
2466
+ ),
2467
+ args: z9.array(z9.string()).optional().describe(
2468
+ "Positional arguments passed to `command`. Example: ['run', 'db:migrate']. Each element is passed verbatim with no extra shell parsing."
2469
+ ),
2470
+ keys: z9.array(z9.string()).optional().describe(
2471
+ "Whitelist of exact key names to inject. Omit to inject every secret in scope (subject to `tags`)."
2472
+ ),
2473
+ tags: z9.array(z9.string()).optional().describe(
2474
+ "Inject only secrets carrying at least one of these tags. Combinable with `keys` as an AND filter."
2475
+ ),
2476
+ profile: z9.enum(["unrestricted", "restricted", "ci"]).optional().default("restricted").describe(
2477
+ "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."
2478
+ ),
2213
2479
  scope: scope6,
2214
2480
  projectPath: projectPath6,
2215
2481
  teamId: teamId6,
@@ -2251,9 +2517,15 @@ ${result.stderr}`);
2251
2517
  );
2252
2518
  server2.tool(
2253
2519
  "scan_codebase_for_secrets",
2254
- "[scan] Scan a directory for hardcoded secrets using regex heuristics and Shannon entropy analysis. Returns file paths, line numbers, and the matched key/value to help migrate legacy codebases into q-ring.",
2520
+ [
2521
+ "[scan] Walk a directory tree and flag plausible hardcoded secrets using regex heuristics plus Shannon-entropy scoring on string literals.",
2522
+ "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).",
2523
+ "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."
2524
+ ].join(" "),
2255
2525
  {
2256
- dirPath: z9.string().describe("Absolute or relative path to the directory to scan")
2526
+ dirPath: z9.string().describe(
2527
+ "Directory to scan, absolute or relative to the server cwd. The scan recurses into subdirectories."
2528
+ )
2257
2529
  },
2258
2530
  async (params) => {
2259
2531
  const toolBlock = enforceToolPolicy("scan_codebase_for_secrets");
@@ -2274,10 +2546,18 @@ ${result.stderr}`);
2274
2546
  );
2275
2547
  server2.tool(
2276
2548
  "lint_files",
2277
- "[scan] Scan specific files for hardcoded secrets. Optionally auto-fix by replacing them with process.env references and storing the values in q-ring.",
2549
+ [
2550
+ "[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.",
2551
+ "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.",
2552
+ "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.'."
2553
+ ].join(" "),
2278
2554
  {
2279
- files: z9.array(z9.string()).describe("File paths to lint"),
2280
- fix: z9.boolean().optional().default(false).describe("Auto-replace and store secrets"),
2555
+ files: z9.array(z9.string()).describe(
2556
+ "Absolute or relative paths to lint. Non-existent paths surface as scan errors."
2557
+ ),
2558
+ fix: z9.boolean().optional().default(false).describe(
2559
+ "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."
2560
+ ),
2281
2561
  scope: scope6,
2282
2562
  projectPath: projectPath6,
2283
2563
  teamId: teamId6,
@@ -2306,7 +2586,11 @@ ${result.stderr}`);
2306
2586
  );
2307
2587
  server2.tool(
2308
2588
  "analyze_secrets",
2309
- "[agent] Analyze secret usage patterns and provide optimization suggestions including most accessed, stale, unused, and rotation recommendations.",
2589
+ [
2590
+ "[agent] Cross-reference the secrets in scope with recent audit events to produce a usage profile and rotation/retirement suggestions.",
2591
+ "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.",
2592
+ "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."
2593
+ ].join(" "),
2310
2594
  {
2311
2595
  scope: scope6,
2312
2596
  projectPath: projectPath6,
@@ -2339,9 +2623,15 @@ ${result.stderr}`);
2339
2623
  let dashboardInstance = null;
2340
2624
  server2.tool(
2341
2625
  "status_dashboard",
2342
- "[dashboard] Launch the quantum status dashboard \u2014 a local SSE-driven web page showing live KPIs (secrets, env, protected, approvals, hooks, 24h reads, anomalies), health summary, environment, .q-ring.json manifest gaps, governance policy summary, sortable searchable secrets table, decay/superposition/entanglement/tunnel cards, active approvals & hooks, agent memory, anomaly alerts, and a filterable 24h audit feed. Returns the URL to open in a browser. Never exposes secret values.",
2626
+ [
2627
+ "[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.",
2628
+ "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.",
2629
+ "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."
2630
+ ].join(" "),
2343
2631
  {
2344
- port: z9.number().optional().default(9876).describe("Port to serve on")
2632
+ port: z9.number().optional().default(9876).describe(
2633
+ "TCP port to listen on (default 9876). Pick another port if 9876 is already in use; the call fails if binding errors."
2634
+ )
2345
2635
  },
2346
2636
  async (params) => {
2347
2637
  const toolBlock = enforceToolPolicy("status_dashboard");
@@ -2361,10 +2651,18 @@ Open this URL in a browser to see live quantum status.`
2361
2651
  );
2362
2652
  server2.tool(
2363
2653
  "agent_scan",
2364
- "[agent] Multi-project health pass: decay, staleness, audit anomalies, manifest gaps; returns JSON. Prefer health_check for a read-only scoped decay/anomaly text summary (no writes). Prefer detect_anomalies for audit-pattern spikes on one key. With autoRotate=true, overwrites expired secret values in the keyring (credential change\u2014not undoable); leave false unless intentional rotation. Same policy gates as other MCP tools; no separate external auth.",
2654
+ [
2655
+ "[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.",
2656
+ "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.",
2657
+ "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."
2658
+ ].join(" "),
2365
2659
  {
2366
- autoRotate: z9.boolean().optional().default(false).describe("Auto-rotate expired secrets with generated values"),
2367
- projectPaths: z9.array(z9.string()).optional().describe("Project paths to monitor")
2660
+ autoRotate: z9.boolean().optional().default(false).describe(
2661
+ "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."
2662
+ ),
2663
+ projectPaths: z9.array(z9.string()).optional().describe(
2664
+ "List of absolute project roots to scan. Defaults to `[server.cwd]` when omitted."
2665
+ )
2368
2666
  },
2369
2667
  async (params) => {
2370
2668
  const toolBlock = enforceToolPolicy("agent_scan");
@@ -2383,10 +2681,18 @@ import { z as z10 } from "zod";
2383
2681
  function registerAgentTools(server2) {
2384
2682
  server2.tool(
2385
2683
  "agent_remember",
2386
- "[agent] Store a key-value pair in encrypted agent memory that persists across sessions. Use this to remember decisions, rotation history, or project-specific context.",
2684
+ [
2685
+ "[agent] Persist a non-secret key/value note in encrypted, on-disk agent memory that survives across MCP sessions.",
2686
+ "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.",
2687
+ `Mutates the encrypted memory store. Idempotent: rewriting the same key with a new value simply overwrites. Returns 'Remembered "KEY"' on success.`
2688
+ ].join(" "),
2387
2689
  {
2388
- key: z10.string().describe("Memory key"),
2389
- value: z10.string().describe("Value to store")
2690
+ key: z10.string().describe(
2691
+ "Memory key (free-form string). Convention: lowercase dotted namespaces, e.g. 'project.lastDeploy'."
2692
+ ),
2693
+ value: z10.string().describe(
2694
+ "Plain-string value to store. JSON-stringify structured data on the caller side if needed."
2695
+ )
2390
2696
  },
2391
2697
  async (params) => {
2392
2698
  const toolBlock = enforceToolPolicy("agent_remember");
@@ -2397,9 +2703,15 @@ function registerAgentTools(server2) {
2397
2703
  );
2398
2704
  server2.tool(
2399
2705
  "agent_recall",
2400
- "[agent] Retrieve a value from agent memory, or list all stored keys if no key is provided.",
2706
+ [
2707
+ "[agent] Read a value from encrypted agent memory, or list every stored key when no specific key is supplied.",
2708
+ "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.",
2709
+ "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'."
2710
+ ].join(" "),
2401
2711
  {
2402
- key: z10.string().optional().describe("Memory key to recall (omit to list all)")
2712
+ key: z10.string().optional().describe(
2713
+ "Memory key to read. Omit to list every stored key (without values)."
2714
+ )
2403
2715
  },
2404
2716
  async (params) => {
2405
2717
  const toolBlock = enforceToolPolicy("agent_recall");
@@ -2419,9 +2731,13 @@ function registerAgentTools(server2) {
2419
2731
  );
2420
2732
  server2.tool(
2421
2733
  "agent_forget",
2422
- "[agent] Delete a key from agent memory.",
2734
+ [
2735
+ "[agent] Permanently delete a single key from encrypted agent memory.",
2736
+ "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).",
2737
+ `Destructive: there is no recycle bin. Returns 'Forgot "KEY"' on success or a not-found error if the key was already absent.`
2738
+ ].join(" "),
2423
2739
  {
2424
- key: z10.string().describe("Memory key to forget")
2740
+ key: z10.string().describe("Memory key to delete.")
2425
2741
  },
2426
2742
  async (params) => {
2427
2743
  const toolBlock = enforceToolPolicy("agent_forget");
@@ -2441,12 +2757,24 @@ var { projectPath: projectPath7 } = commonSchemas;
2441
2757
  function registerPolicyTools(server2) {
2442
2758
  server2.tool(
2443
2759
  "check_policy",
2444
- "[policy] Check if an action is allowed by the project's governance policy. Returns the policy decision and source.",
2760
+ [
2761
+ "[policy] Ask whether a single intended action would be allowed by the project's `.q-ring.json` policy without actually performing it.",
2762
+ "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.",
2763
+ "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."
2764
+ ].join(" "),
2445
2765
  {
2446
- action: z11.enum(["tool", "key_read", "exec"]).describe("Type of policy check"),
2447
- toolName: z11.string().optional().describe("Tool name to check (for action=tool)"),
2448
- key: z11.string().optional().describe("Secret key to check (for action=key_read)"),
2449
- command: z11.string().optional().describe("Command to check (for action=exec)"),
2766
+ action: z11.enum(["tool", "key_read", "exec"]).describe(
2767
+ "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`)."
2768
+ ),
2769
+ toolName: z11.string().optional().describe(
2770
+ "Tool id to evaluate, e.g. 'rotate_secret'. Required when `action` is 'tool'."
2771
+ ),
2772
+ key: z11.string().optional().describe(
2773
+ "Secret key name to evaluate. Required when `action` is 'key_read'."
2774
+ ),
2775
+ command: z11.string().optional().describe(
2776
+ "Command to evaluate against the exec allowlist/denylist. Required when `action` is 'exec'."
2777
+ ),
2450
2778
  projectPath: projectPath7
2451
2779
  },
2452
2780
  async (params) => {
@@ -2470,7 +2798,11 @@ function registerPolicyTools(server2) {
2470
2798
  );
2471
2799
  server2.tool(
2472
2800
  "get_policy_summary",
2473
- "[policy] Get a summary of the project's governance policy configuration.",
2801
+ [
2802
+ "[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.",
2803
+ "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.",
2804
+ "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."
2805
+ ].join(" "),
2474
2806
  {
2475
2807
  projectPath: projectPath7
2476
2808
  },