@vellumai/assistant 0.4.46 → 0.4.48

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.
Files changed (194) hide show
  1. package/ARCHITECTURE.md +5 -5
  2. package/docs/architecture/security.md +5 -5
  3. package/package.json +1 -1
  4. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  5. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  6. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  7. package/src/__tests__/cli.test.ts +23 -0
  8. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  9. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  10. package/src/__tests__/credential-broker.test.ts +2 -1
  11. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  12. package/src/__tests__/credential-resolve.test.ts +5 -4
  13. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  14. package/src/__tests__/credential-security-invariants.test.ts +104 -6
  15. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  16. package/src/__tests__/credential-vault.test.ts +284 -12
  17. package/src/__tests__/credentials-cli.test.ts +11 -6
  18. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  19. package/src/__tests__/gemini-image-service.test.ts +75 -45
  20. package/src/__tests__/gemini-provider.test.ts +9 -6
  21. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  22. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  23. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  24. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  25. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  26. package/src/__tests__/integration-status.test.ts +53 -21
  27. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  28. package/src/__tests__/media-generate-image.test.ts +63 -2
  29. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  30. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  31. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  32. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  33. package/src/__tests__/schema-transforms.test.ts +226 -0
  34. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  35. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  36. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  37. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  38. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  39. package/src/__tests__/skills-uninstall.test.ts +2 -2
  40. package/src/__tests__/skills.test.ts +0 -9
  41. package/src/__tests__/slack-channel-config.test.ts +9 -8
  42. package/src/__tests__/slack-share-routes.test.ts +11 -6
  43. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  44. package/src/__tests__/twilio-config.test.ts +2 -1
  45. package/src/__tests__/twilio-provider.test.ts +4 -2
  46. package/src/__tests__/twilio-routes.test.ts +5 -4
  47. package/src/calls/call-domain.ts +7 -4
  48. package/src/calls/twilio-config.ts +2 -1
  49. package/src/calls/twilio-provider.ts +2 -1
  50. package/src/calls/twilio-rest.ts +2 -1
  51. package/src/cli/commands/browser-relay.ts +40 -15
  52. package/src/cli/commands/credentials.ts +9 -8
  53. package/src/cli/commands/oauth.ts +1 -1
  54. package/src/cli.ts +3 -2
  55. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  56. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  57. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  58. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  59. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  60. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  61. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  62. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  63. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  64. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  65. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  66. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  67. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  68. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  69. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  70. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  71. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  72. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  73. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  74. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  75. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  76. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  77. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  78. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  79. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  80. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  81. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  82. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  83. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  84. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  85. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  86. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  87. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  88. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  89. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  90. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  91. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  92. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  93. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  94. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  95. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  96. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  97. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  98. package/src/config/loader.ts +6 -0
  99. package/src/daemon/computer-use-session.ts +7 -1
  100. package/src/daemon/guardian-action-generators.ts +4 -5
  101. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  102. package/src/daemon/handlers/config-telegram.ts +33 -20
  103. package/src/daemon/lifecycle.ts +9 -1
  104. package/src/daemon/message-types/integrations.ts +1 -0
  105. package/src/daemon/ride-shotgun-handler.ts +3 -1
  106. package/src/daemon/session-messaging.ts +3 -1
  107. package/src/daemon/session-tool-setup.ts +18 -2
  108. package/src/daemon/session.ts +1 -1
  109. package/src/email/providers/index.ts +2 -1
  110. package/src/instrument.ts +15 -1
  111. package/src/media/app-icon-generator.ts +30 -4
  112. package/src/media/avatar-router.ts +26 -3
  113. package/src/media/gemini-image-service.ts +28 -2
  114. package/src/memory/guardian-action-store.ts +1 -1
  115. package/src/memory/schema/guardian.ts +1 -1
  116. package/src/messaging/provider.ts +16 -10
  117. package/src/messaging/providers/gmail/adapter.ts +40 -23
  118. package/src/messaging/providers/gmail/client.ts +203 -122
  119. package/src/messaging/providers/gmail/people-client.ts +26 -18
  120. package/src/messaging/providers/slack/adapter.ts +29 -19
  121. package/src/messaging/providers/slack/client.ts +265 -78
  122. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  123. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  124. package/src/messaging/registry.ts +2 -1
  125. package/src/oauth/byo-connection.test.ts +436 -0
  126. package/src/oauth/byo-connection.ts +112 -0
  127. package/src/oauth/connect-orchestrator.ts +27 -0
  128. package/src/oauth/connection-resolver.ts +34 -0
  129. package/src/oauth/connection.ts +38 -0
  130. package/src/oauth/platform-connection.test.ts +163 -0
  131. package/src/oauth/platform-connection.ts +110 -0
  132. package/src/oauth/provider-base-urls.ts +21 -0
  133. package/src/oauth/provider-profiles.ts +1 -1
  134. package/src/oauth/token-persistence.ts +20 -20
  135. package/src/permissions/checker.ts +5 -1
  136. package/src/prompts/system-prompt.ts +49 -12
  137. package/src/providers/gemini/client.ts +15 -6
  138. package/src/providers/managed-proxy/constants.ts +2 -2
  139. package/src/providers/managed-proxy/context.ts +5 -1
  140. package/src/providers/ratelimit.ts +17 -0
  141. package/src/providers/registry.ts +2 -2
  142. package/src/runtime/AGENTS.md +17 -0
  143. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  144. package/src/runtime/channel-readiness-service.ts +168 -195
  145. package/src/runtime/channel-readiness-types.ts +4 -0
  146. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  147. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  148. package/src/runtime/guardian-action-message-composer.ts +3 -23
  149. package/src/runtime/http-server.ts +9 -4
  150. package/src/runtime/http-types.ts +0 -1
  151. package/src/runtime/middleware/rate-limiter.ts +74 -20
  152. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  153. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  154. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  155. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  156. package/src/runtime/routes/integrations/twilio.ts +6 -5
  157. package/src/runtime/routes/secret-routes.ts +3 -2
  158. package/src/runtime/routes/settings-routes.ts +75 -17
  159. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  160. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  161. package/src/schedule/integration-status.ts +5 -4
  162. package/src/security/credential-key.ts +170 -0
  163. package/src/security/token-manager.ts +36 -7
  164. package/src/tools/apps/definitions.ts +0 -5
  165. package/src/tools/assets/materialize.ts +0 -5
  166. package/src/tools/assets/search.ts +0 -5
  167. package/src/tools/browser/headless-browser.ts +1 -67
  168. package/src/tools/claude-code/claude-code.ts +0 -5
  169. package/src/tools/computer-use/request-computer-control.ts +0 -5
  170. package/src/tools/credentials/broker.ts +6 -4
  171. package/src/tools/credentials/metadata-store.ts +72 -20
  172. package/src/tools/credentials/resolve.ts +2 -1
  173. package/src/tools/credentials/vault.ts +77 -16
  174. package/src/tools/filesystem/edit.ts +1 -6
  175. package/src/tools/filesystem/read.ts +0 -5
  176. package/src/tools/filesystem/write.ts +1 -6
  177. package/src/tools/host-filesystem/edit.ts +1 -6
  178. package/src/tools/host-filesystem/read.ts +1 -6
  179. package/src/tools/host-filesystem/write.ts +1 -6
  180. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  181. package/src/tools/memory/definitions.ts +0 -5
  182. package/src/tools/network/web-fetch.ts +0 -5
  183. package/src/tools/network/web-search.ts +0 -5
  184. package/src/tools/schema-transforms.ts +99 -0
  185. package/src/tools/skills/load.ts +0 -5
  186. package/src/tools/swarm/delegate.ts +0 -5
  187. package/src/tools/system/avatar-generator.ts +0 -5
  188. package/src/tools/ui-surface/definitions.ts +0 -15
  189. package/src/tools/watch/screen-watch.ts +0 -5
  190. package/src/version.ts +10 -0
  191. package/src/watcher/providers/github.ts +51 -52
  192. package/src/watcher/providers/gmail.ts +88 -80
  193. package/src/watcher/providers/google-calendar.ts +93 -86
  194. package/src/watcher/providers/linear.ts +87 -93
package/ARCHITECTURE.md CHANGED
@@ -347,8 +347,8 @@ Both tokens are stored in the secure key store (macOS Keychain with encrypted fi
347
347
 
348
348
  | Secure key | Content |
349
349
  | ------------------------------------ | -------------------------------------------------------------------------- |
350
- | `credential:slack_channel:bot_token` | Slack bot token (used for `chat.postMessage` and `auth.test`) |
351
- | `credential:slack_channel:app_token` | Slack app token (`xapp-...`, used for Socket Mode `apps.connections.open`) |
350
+ | `credential/slack_channel/bot_token` | Slack bot token (used for `chat.postMessage` and `auth.test`) |
351
+ | `credential/slack_channel/app_token` | Slack app token (`xapp-...`, used for Socket Mode `apps.connections.open`) |
352
352
 
353
353
  Workspace metadata (team ID, team name, bot user ID, bot username) is stored as JSON in the credential metadata store under `('slack_channel', 'bot_token')`.
354
354
 
@@ -641,7 +641,7 @@ The assistant feature-flag resolver (`src/config/assistant-feature-flags.ts`) is
641
641
  | **3. `skill_load` tool** | `executeSkillLoad()` in `tools/skills/load.ts` | If the model attempts to load a flagged-off skill by name, the tool returns an error: `"skill is currently unavailable (disabled by feature flag)"`. |
642
642
  | **4. Runtime tool projection** | `projectSkillTools()` in `daemon/session-skill-tools.ts` | Even if a skill was previously active in a session (has `<loaded_skill>` markers in history), the per-turn projection drops it when the flag is OFF. Already-registered tools are unregistered. |
643
643
  | **5. Included child skills** | `executeSkillLoad()` in `tools/skills/load.ts` | When a parent skill includes children via the `includes` directive, each child is independently checked against its feature flag. Flagged-off children are silently excluded from the loaded skill content. |
644
- | **6. Skill install gate** | `handleInstallSkill()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the handler checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
644
+ | **6. Skill install gate** | `handleSkillsInstall()` in `daemon/handlers/skills.ts` | When a client requests skill installation, the handler checks the skill's feature flag before proceeding. If the flag is OFF, the install is rejected with an error. |
645
645
 
646
646
  All six enforcement points derive the flag key via `skillFlagKey(skill)` — which returns `undefined` for ungated skills, short-circuiting the check — and then call `isAssistantFeatureFlagEnabled(flagKey, config)` for consistency.
647
647
 
@@ -657,7 +657,7 @@ All six enforcement points derive the flag key via `skillFlagKey(skill)` — whi
657
657
  | `src/tools/skills/load.ts` | `executeSkillLoad()` — enforcement points 3 and 5 |
658
658
  | `src/daemon/session-skill-tools.ts` | `projectSkillTools()` — enforcement point 4 |
659
659
  | `src/config/schema.ts` | `assistantFeatureFlagValues` field definition in `AssistantConfig` (Zod schema) |
660
- | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for client responses; `handleInstallSkill()` — enforcement point 6 |
660
+ | `src/daemon/handlers/skills.ts` | `handleSkillsList()` — uses `resolveSkillStates()` for client responses; `handleSkillsInstall()` — enforcement point 6 |
661
661
  | `meta/feature-flags/feature-flag-registry.json` | Unified feature flag registry (repo root) — all declared flags with scope, label, default values, and descriptions |
662
662
  | `src/config/feature-flag-registry.json` | Bundled copy of the unified registry for compiled binary resolution |
663
663
 
@@ -669,7 +669,7 @@ All six enforcement points derive the flag key via `skillFlagKey(skill)` — whi
669
669
  graph LR
670
670
  subgraph "macOS Keychain"
671
671
  K1["API Key<br/>service: vellum-assistant<br/>account: anthropic<br/>stored via /usr/bin/security CLI"]
672
- K2["Credential Secrets<br/>key: credential:{service}:{field}<br/>stored via secure-keys.ts<br/>(encrypted file fallback if Keychain unavailable)"]
672
+ K2["Credential Secrets<br/>key: credential/{service}/{field}<br/>stored via secure-keys.ts<br/>(encrypted file fallback if Keychain unavailable)"]
673
673
  end
674
674
 
675
675
  subgraph "UserDefaults (plist)"
@@ -179,7 +179,7 @@ File tool candidates include canonical (symlink-resolved) absolute paths via `no
179
179
  | `assistant/src/permissions/checker.ts` | `classifyRisk()`, `check()`, `buildCommandCandidates()`, allowlist/scope generation |
180
180
  | `assistant/src/permissions/shell-identity.ts` | `analyzeShellCommand()`, `deriveShellActionKeys()`, `buildShellCommandCandidates()`, `buildShellAllowlistOptions()` — parser-based shell command identity and action key derivation |
181
181
  | `assistant/src/permissions/trust-store.ts` | Rule persistence, `findHighestPriorityRule()`, execution-target matching, starter bundle |
182
- | `assistant/src/permissions/prompter.ts` | HTTP prompt flow: `confirmation_request` → `confirmation_response` |
182
+ | `assistant/src/permissions/prompter.ts` | HTTP prompt flow: `confirmation_request` → `confirmation_response` |
183
183
  | `assistant/src/permissions/defaults.ts` | Default rule templates (system ask rules for host tools, CU, etc.) |
184
184
  | `assistant/src/skills/version-hash.ts` | `computeSkillVersionHash()` — deterministic SHA-256 of skill source files |
185
185
  | `assistant/src/skills/path-classifier.ts` | `isSkillSourcePath()`, `normalizeFilePath()`, skill root detection |
@@ -233,7 +233,7 @@ sequenceDiagram
233
233
  UI->>HTTP: secret_response {requestId, value, delivery: "store"}
234
234
  HTTP->>Prompter: resolve(value, "store")
235
235
  Prompter->>Vault: {value, delivery: "store"}
236
- Vault->>Keychain: setSecureKey("credential:svc:field", value)
236
+ Vault->>Keychain: setSecureKey("credential/svc/field", value)
237
237
  Vault->>Model: "Credential stored securely" (no value in output)
238
238
  else One-Time Send (if enabled)
239
239
  UI->>HTTP: secret_response {requestId, value, delivery: "transient_send"}
@@ -272,7 +272,7 @@ graph TB
272
272
  TOOL["Tool (e.g. browser_fill_credential)"] --> BROKER["CredentialBroker.use(service, field, tool, domain)"]
273
273
  BROKER --> POLICY{"Check policy:<br/>allowedTools + allowedDomains"}
274
274
  POLICY -->|denied| REJECT["PolicyDenied error"]
275
- POLICY -->|allowed| FETCH["getSecureKey(credential:svc:field)"]
275
+ POLICY -->|allowed| FETCH["getSecureKey(credential/svc/field)"]
276
276
  FETCH --> INJECT["Inject value into tool execution<br/>(never returned to model)"]
277
277
  ```
278
278
 
@@ -290,7 +290,7 @@ The `allowOneTimeSend` config gate (default: `false`) enables a secondary "Send
290
290
 
291
291
  | Component | Location | What it stores |
292
292
  | ------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
293
- | Secret values | macOS Keychain (primary) or encrypted file fallback | Encrypted credential values keyed as `credential:{service}:{field}`. Falls back to encrypted file backend on Linux/headless or when Keychain is unavailable. |
293
+ | Secret values | macOS Keychain (primary) or encrypted file fallback | Encrypted credential values keyed as `credential/{service}/{field}`. Falls back to encrypted file backend on Linux/headless or when Keychain is unavailable. |
294
294
  | Credential metadata | `~/.vellum/workspace/data/credentials/metadata.json` | Service, field, label, policy (allowedTools, allowedDomains), timestamps |
295
295
  | Config | `~/.vellum/workspace/config.*` | `secretDetection` settings: enabled, action, entropyThreshold, allowOneTimeSend |
296
296
 
@@ -303,7 +303,7 @@ The `allowOneTimeSend` config gate (default: `false`) enables a secondary "Send
303
303
  | `assistant/src/tools/credentials/metadata-store.ts` | JSON file metadata CRUD for credential records |
304
304
  | `assistant/src/tools/credentials/broker.ts` | Brokered credential access with policy enforcement and transient send |
305
305
  | `assistant/src/tools/credentials/policy-validate.ts` | Policy input validation (allowedTools, allowedDomains) |
306
- | `assistant/src/permissions/secret-prompter.ts` | HTTP secret_request/secret_response flow |
306
+ | `assistant/src/permissions/secret-prompter.ts` | HTTP secret_request/secret_response flow |
307
307
  | `assistant/src/security/secret-scanner.ts` | Regex + entropy-based secret detection |
308
308
  | `assistant/src/security/secret-ingress.ts` | Inbound message secret blocking |
309
309
  | `clients/macos/.../SecretPromptManager.swift` | Floating panel UI for secure credential entry |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.4.46",
3
+ "version": "0.4.48",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -76,6 +76,7 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
76
76
  _setMetadataPath: () => {},
77
77
  }));
78
78
 
79
+ import { credentialKey } from "../security/credential-key.js";
79
80
  import { executeBrowserFillCredential } from "../tools/browser/browser-execution.js";
80
81
  import type { ToolContext } from "../tools/types.js";
81
82
 
@@ -142,7 +143,9 @@ describe("executeBrowserFillCredential", () => {
142
143
  '[data-vellum-eid="e1"]',
143
144
  "super-secret-password",
144
145
  );
145
- expect(mockGetSecureKey).toHaveBeenCalledWith("credential:gmail:password");
146
+ expect(mockGetSecureKey).toHaveBeenCalledWith(
147
+ credentialKey("gmail", "password"),
148
+ );
146
149
  });
147
150
 
148
151
  test("fills credential by CSS selector", async () => {
@@ -297,7 +300,7 @@ describe("executeBrowserFillCredential", () => {
297
300
  "password",
298
301
  );
299
302
  expect(mockGetSecureKey).toHaveBeenCalledWith(
300
- "credential:gmail:password",
303
+ credentialKey("gmail", "password"),
301
304
  );
302
305
  });
303
306
 
@@ -90,7 +90,7 @@ const GATEWAY_RETRIEVAL_BANLIST: Array<{
90
90
  bannedSnippets: [
91
91
  'curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/',
92
92
  "security find-generic-password",
93
- "secret-tool lookup service vellum-assistant account credential:ngrok:authtoken",
93
+ "secret-tool lookup service vellum-assistant account credential/ngrok/authtoken",
94
94
  ],
95
95
  },
96
96
  {
@@ -119,6 +119,7 @@ const KEYCHAIN_ALLOWLIST = new Set<string>([
119
119
  const KEYCHAIN_PATTERNS = [
120
120
  "security find-generic-password",
121
121
  "secret-tool lookup service vellum-assistant account credential:",
122
+ "secret-tool lookup service vellum-assistant account credential/",
122
123
  ];
123
124
 
124
125
  const HOST_BASH_RETRIEVAL_ALLOWLIST = new Set<string>([
@@ -52,6 +52,7 @@ mock.module("../email/service.js", () => ({
52
52
  // ---------------------------------------------------------------------------
53
53
 
54
54
  import { createReadinessService } from "../runtime/channel-readiness-service.js";
55
+ import { credentialKey } from "../security/credential-key.js";
55
56
 
56
57
  // ---------------------------------------------------------------------------
57
58
  // Tests
@@ -197,10 +198,10 @@ describe("channel readiness routes — email and WhatsApp probes", () => {
197
198
 
198
199
  test("reports ready when all Meta credentials and display number are configured", async () => {
199
200
  mockSecureKeys = {
200
- "credential:whatsapp:phone_number_id": "123456789",
201
- "credential:whatsapp:access_token": "EAAxxxxxx",
202
- "credential:whatsapp:app_secret": "abc123",
203
- "credential:whatsapp:webhook_verify_token": "my-verify-token",
201
+ [credentialKey("whatsapp", "phone_number_id")]: "123456789",
202
+ [credentialKey("whatsapp", "access_token")]: "EAAxxxxxx",
203
+ [credentialKey("whatsapp", "app_secret")]: "abc123",
204
+ [credentialKey("whatsapp", "webhook_verify_token")]: "my-verify-token",
204
205
  };
205
206
  mockRawConfig = {
206
207
  whatsapp: { phoneNumber: "+15551234567" },
@@ -215,10 +216,10 @@ describe("channel readiness routes — email and WhatsApp probes", () => {
215
216
 
216
217
  test("reports not ready when display phone number is missing", async () => {
217
218
  mockSecureKeys = {
218
- "credential:whatsapp:phone_number_id": "123456789",
219
- "credential:whatsapp:access_token": "EAAxxxxxx",
220
- "credential:whatsapp:app_secret": "abc123",
221
- "credential:whatsapp:webhook_verify_token": "my-verify-token",
219
+ [credentialKey("whatsapp", "phone_number_id")]: "123456789",
220
+ [credentialKey("whatsapp", "access_token")]: "EAAxxxxxx",
221
+ [credentialKey("whatsapp", "app_secret")]: "abc123",
222
+ [credentialKey("whatsapp", "webhook_verify_token")]: "my-verify-token",
222
223
  };
223
224
  mockRawConfig = {
224
225
  ingress: { publicBaseUrl: "https://example.com", enabled: true },
@@ -236,10 +237,10 @@ describe("channel readiness routes — email and WhatsApp probes", () => {
236
237
 
237
238
  test("checks each Meta credential individually", async () => {
238
239
  mockSecureKeys = {
239
- "credential:whatsapp:phone_number_id": "123456789",
240
+ [credentialKey("whatsapp", "phone_number_id")]: "123456789",
240
241
  // access_token missing
241
- "credential:whatsapp:app_secret": "abc123",
242
- "credential:whatsapp:webhook_verify_token": "my-verify-token",
242
+ [credentialKey("whatsapp", "app_secret")]: "abc123",
243
+ [credentialKey("whatsapp", "webhook_verify_token")]: "my-verify-token",
243
244
  };
244
245
  mockRawConfig = {
245
246
  ingress: { publicBaseUrl: "https://example.com", enabled: true },
@@ -272,10 +273,10 @@ describe("channel readiness routes — email and WhatsApp probes", () => {
272
273
 
273
274
  test("checks invite policy", async () => {
274
275
  mockSecureKeys = {
275
- "credential:whatsapp:phone_number_id": "123456789",
276
- "credential:whatsapp:access_token": "EAAxxxxxx",
277
- "credential:whatsapp:app_secret": "abc123",
278
- "credential:whatsapp:webhook_verify_token": "my-verify-token",
276
+ [credentialKey("whatsapp", "phone_number_id")]: "123456789",
277
+ [credentialKey("whatsapp", "access_token")]: "EAAxxxxxx",
278
+ [credentialKey("whatsapp", "app_secret")]: "abc123",
279
+ [credentialKey("whatsapp", "webhook_verify_token")]: "my-verify-token",
279
280
  };
280
281
  mockRawConfig = {
281
282
  whatsapp: { phoneNumber: "+15551234567" },
@@ -293,10 +294,10 @@ describe("channel readiness routes — email and WhatsApp probes", () => {
293
294
 
294
295
  test("checks ingress configuration", async () => {
295
296
  mockSecureKeys = {
296
- "credential:whatsapp:phone_number_id": "123456789",
297
- "credential:whatsapp:access_token": "EAAxxxxxx",
298
- "credential:whatsapp:app_secret": "abc123",
299
- "credential:whatsapp:webhook_verify_token": "my-verify-token",
297
+ [credentialKey("whatsapp", "phone_number_id")]: "123456789",
298
+ [credentialKey("whatsapp", "access_token")]: "EAAxxxxxx",
299
+ [credentialKey("whatsapp", "app_secret")]: "abc123",
300
+ [credentialKey("whatsapp", "webhook_verify_token")]: "my-verify-token",
300
301
  };
301
302
  mockRawConfig = {
302
303
  whatsapp: { phoneNumber: "+15551234567" },
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { describe, expect, test } from "bun:test";
2
3
 
3
4
  import {
@@ -66,3 +67,25 @@ describe("formatConfirmationCommandPreview", () => {
66
67
  expect(preview).toBe("edit /tmp/sample.txt");
67
68
  });
68
69
  });
70
+
71
+ const UUID_RE =
72
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
73
+
74
+ describe("new-session conversationKey format", () => {
75
+ test("uses a valid UUID, not a timestamp", () => {
76
+ // Mirror the key construction in startCli()
77
+ const key = `builtin-cli:${randomUUID()}`;
78
+ const suffix = key.replace("builtin-cli:", "");
79
+
80
+ expect(suffix).toMatch(UUID_RE);
81
+ // A numeric timestamp would parse to a finite number; a UUID must not.
82
+ expect(Number.isFinite(Number(suffix))).toBe(false);
83
+ });
84
+
85
+ test("generates unique keys across calls", () => {
86
+ const key1 = `builtin-cli:${randomUUID()}`;
87
+ const key2 = `builtin-cli:${randomUUID()}`;
88
+
89
+ expect(key1).not.toBe(key2);
90
+ });
91
+ });
@@ -49,6 +49,7 @@ mock.module("../tools/registry.js", () => ({
49
49
  // Imports under test
50
50
  // ---------------------------------------------------------------------------
51
51
 
52
+ import { credentialKey } from "../security/credential-key.js";
52
53
  import { setSecureKey } from "../security/secure-keys.js";
53
54
  import { CredentialBroker } from "../tools/credentials/broker.js";
54
55
  import {
@@ -92,7 +93,7 @@ describe("CredentialBroker.browserFill", () => {
92
93
  upsertCredentialMetadata("github", "token", {
93
94
  allowedTools: ["browser_fill_credential"],
94
95
  });
95
- setSecureKey("credential:github:token", "ghp_secret123");
96
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
96
97
 
97
98
  let filledValue: string | undefined;
98
99
  const result = await broker.browserFill({
@@ -114,7 +115,7 @@ describe("CredentialBroker.browserFill", () => {
114
115
  upsertCredentialMetadata("github", "token", {
115
116
  allowedTools: ["browser_fill_credential"],
116
117
  });
117
- setSecureKey("credential:github:token", "ghp_secret123");
118
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
118
119
 
119
120
  const result = await broker.browserFill({
120
121
  service: "github",
@@ -167,7 +168,7 @@ describe("CredentialBroker.browserFill", () => {
167
168
  upsertCredentialMetadata("github", "token", {
168
169
  allowedTools: ["browser_fill_credential"],
169
170
  });
170
- setSecureKey("credential:github:token", "ghp_secret123");
171
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
171
172
 
172
173
  const result = await broker.browserFill({
173
174
  service: "github",
@@ -193,8 +194,8 @@ describe("CredentialBroker.browserFill", () => {
193
194
  upsertCredentialMetadata("github", "password", {
194
195
  allowedTools: ["browser_fill_credential"],
195
196
  });
196
- setSecureKey("credential:github:username", "octocat");
197
- setSecureKey("credential:github:password", "hunter2");
197
+ setSecureKey(credentialKey("github", "username"), "octocat");
198
+ setSecureKey(credentialKey("github", "password"), "hunter2");
198
199
 
199
200
  const filled: Record<string, string> = {};
200
201
 
@@ -227,7 +228,7 @@ describe("CredentialBroker.browserFill", () => {
227
228
  allowedTools: ["browser_fill_credential"],
228
229
  allowedDomains: ["github.com"],
229
230
  });
230
- setSecureKey("credential:github:token", "ghp_secret123");
231
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
231
232
 
232
233
  const result = await broker.browserFill({
233
234
  service: "github",
@@ -245,7 +246,7 @@ describe("CredentialBroker.browserFill", () => {
245
246
  allowedTools: ["browser_fill_credential"],
246
247
  allowedDomains: ["github.com"],
247
248
  });
248
- setSecureKey("credential:github:token", "ghp_secret123");
249
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
249
250
 
250
251
  const result = await broker.browserFill({
251
252
  service: "github",
@@ -263,7 +264,7 @@ describe("CredentialBroker.browserFill", () => {
263
264
  allowedTools: ["browser_fill_credential"],
264
265
  allowedDomains: ["github.com"],
265
266
  });
266
- setSecureKey("credential:github:token", "ghp_secret123");
267
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
267
268
 
268
269
  const result = await broker.browserFill({
269
270
  service: "github",
@@ -286,7 +287,7 @@ describe("CredentialBroker.browserFill", () => {
286
287
  allowedTools: ["browser_fill_credential"],
287
288
  allowedDomains: ["github.com"],
288
289
  });
289
- setSecureKey("credential:github:token", "ghp_secret123");
290
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
290
291
 
291
292
  const result = await broker.browserFill({
292
293
  service: "github",
@@ -305,7 +306,7 @@ describe("CredentialBroker.browserFill", () => {
305
306
  upsertCredentialMetadata("github", "token", {
306
307
  allowedTools: ["browser_fill_credential"],
307
308
  });
308
- setSecureKey("credential:github:token", "ghp_secret123");
309
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
309
310
 
310
311
  // No domain provided and no allowedDomains policy — should succeed
311
312
  const result = await broker.browserFill({
@@ -322,7 +323,7 @@ describe("CredentialBroker.browserFill", () => {
322
323
  upsertCredentialMetadata("github", "token", {
323
324
  allowedTools: ["other_tool"],
324
325
  });
325
- setSecureKey("credential:github:token", "ghp_secret123");
326
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
326
327
 
327
328
  let fillCalled = false;
328
329
  const result = await broker.browserFill({
@@ -343,7 +344,7 @@ describe("CredentialBroker.browserFill", () => {
343
344
 
344
345
  test("denies fill when allowedTools is empty (fail-closed)", async () => {
345
346
  upsertCredentialMetadata("github", "token", { allowedTools: [] });
346
- setSecureKey("credential:github:token", "ghp_secret123");
347
+ setSecureKey(credentialKey("github", "token"), "ghp_secret123");
347
348
 
348
349
  const result = await broker.browserFill({
349
350
  service: "github",
@@ -362,7 +363,7 @@ describe("CredentialBroker.browserFill", () => {
362
363
  upsertCredentialMetadata("github", "token", {
363
364
  allowedTools: ["browser_fill_credential"],
364
365
  });
365
- setSecureKey("credential:github:token", "ghp_supersecret");
366
+ setSecureKey(credentialKey("github", "token"), "ghp_supersecret");
366
367
 
367
368
  const result = await broker.browserFill({
368
369
  service: "github",
@@ -388,7 +389,7 @@ describe("CredentialBroker.browserFill", () => {
388
389
  allowedTools: ["s3_upload", "cloudfront_invalidate"],
389
390
  allowedDomains: [],
390
391
  });
391
- setSecureKey("credential:aws:access_key", "AKIA_test");
392
+ setSecureKey(credentialKey("aws", "access_key"), "AKIA_test");
392
393
 
393
394
  let fillCalled = false;
394
395
  const result = await broker.browserFill({
@@ -413,7 +414,7 @@ describe("CredentialBroker.browserFill", () => {
413
414
  allowedTools: ["browser_fill_credential"],
414
415
  allowedDomains: ["github.com"],
415
416
  });
416
- setSecureKey("credential:github:pat", "ghp_fill_test");
417
+ setSecureKey(credentialKey("github", "pat"), "ghp_fill_test");
417
418
 
418
419
  let fillCalled = false;
419
420
  const result = await broker.browserFill({
@@ -437,7 +438,7 @@ describe("CredentialBroker.browserFill", () => {
437
438
  allowedTools: ["slack_post"],
438
439
  allowedDomains: ["slack.com"],
439
440
  });
440
- setSecureKey("credential:slack:bot_token", "xoxb-test");
441
+ setSecureKey(credentialKey("slack", "bot_token"), "xoxb-test");
441
442
 
442
443
  const result = await broker.browserFill({
443
444
  service: "slack",
@@ -460,7 +461,7 @@ describe("CredentialBroker.browserFill", () => {
460
461
  upsertCredentialMetadata("custom", "key", {
461
462
  allowedTools: [],
462
463
  });
463
- setSecureKey("credential:custom:key", "secret");
464
+ setSecureKey(credentialKey("custom", "key"), "secret");
464
465
 
465
466
  const result = await broker.browserFill({
466
467
  service: "custom",
@@ -490,7 +491,7 @@ describe("CredentialBroker.browserFill", () => {
490
491
  upsertCredentialMetadata("github", "token", {
491
492
  allowedTools: ["browser_fill_credential"],
492
493
  });
493
- setSecureKey("credential:github:token", "ghp_updated");
494
+ setSecureKey(credentialKey("github", "token"), "ghp_updated");
494
495
 
495
496
  let filledValue: string | undefined;
496
497
  const result = await broker.browserFill({
@@ -514,8 +515,8 @@ describe("CredentialBroker.browserFill", () => {
514
515
  upsertCredentialMetadata("github", "password", {
515
516
  allowedTools: ["other_tool"],
516
517
  });
517
- setSecureKey("credential:github:username", "octocat");
518
- setSecureKey("credential:github:password", "hunter2");
518
+ setSecureKey(credentialKey("github", "username"), "octocat");
519
+ setSecureKey(credentialKey("github", "password"), "hunter2");
519
520
 
520
521
  // username allows browser_fill_credential
521
522
  const r1 = await broker.browserFill({
@@ -548,8 +549,8 @@ describe("CredentialBroker.browserFill", () => {
548
549
  allowedTools: ["browser_fill_credential"],
549
550
  allowedDomains: ["gitlab.com"],
550
551
  });
551
- setSecureKey("credential:github:token", "gh_tok");
552
- setSecureKey("credential:gitlab:token", "gl_tok");
552
+ setSecureKey(credentialKey("github", "token"), "gh_tok");
553
+ setSecureKey(credentialKey("gitlab", "token"), "gl_tok");
553
554
 
554
555
  // github credential on github.com succeeds
555
556
  let filled1 = "";
@@ -48,6 +48,7 @@ mock.module("../tools/registry.js", () => ({
48
48
  // Imports under test
49
49
  // ---------------------------------------------------------------------------
50
50
 
51
+ import { credentialKey } from "../security/credential-key.js";
51
52
  import { setSecureKey } from "../security/secure-keys.js";
52
53
  import { CredentialBroker } from "../tools/credentials/broker.js";
53
54
  import {
@@ -85,7 +86,7 @@ describe("CredentialBroker.serverUse", () => {
85
86
  upsertCredentialMetadata("vercel", "api_token", {
86
87
  allowedTools: ["publish_page"],
87
88
  });
88
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
89
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
89
90
 
90
91
  const result = await broker.serverUse({
91
92
  service: "vercel",
@@ -109,7 +110,7 @@ describe("CredentialBroker.serverUse", () => {
109
110
  upsertCredentialMetadata("vercel", "api_token", {
110
111
  allowedTools: ["publish_page"],
111
112
  });
112
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
113
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
113
114
 
114
115
  const result = await broker.serverUse({
115
116
  service: "vercel",
@@ -161,7 +162,7 @@ describe("CredentialBroker.serverUse", () => {
161
162
  upsertCredentialMetadata("vercel", "api_token", {
162
163
  allowedTools: ["publish_page"],
163
164
  });
164
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
165
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
165
166
 
166
167
  const result = await broker.serverUse({
167
168
  service: "vercel",
@@ -182,7 +183,7 @@ describe("CredentialBroker.serverUse", () => {
182
183
  upsertCredentialMetadata("vercel", "api_token", {
183
184
  allowedTools: ["publish_page"],
184
185
  });
185
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
186
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
186
187
 
187
188
  const result = await broker.serverUse({
188
189
  service: "vercel",
@@ -201,7 +202,7 @@ describe("CredentialBroker.serverUse", () => {
201
202
  allowedTools: ["publish_page"],
202
203
  allowedDomains: ["vercel.com"],
203
204
  });
204
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
205
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
205
206
 
206
207
  const result = await broker.serverUse({
207
208
  service: "vercel",
@@ -222,7 +223,7 @@ describe("CredentialBroker.serverUse", () => {
222
223
  allowedTools: ["publish_page"],
223
224
  allowedDomains: [],
224
225
  });
225
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
226
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
226
227
 
227
228
  const result = await broker.serverUse({
228
229
  service: "vercel",
@@ -247,7 +248,7 @@ describe("CredentialBroker.serverUse", () => {
247
248
  upsertCredentialMetadata("aws", "access_key", {
248
249
  allowedTools: ["deploy_lambda", "s3_upload"],
249
250
  });
250
- setSecureKey("credential:aws:access_key", "AKIA_test");
251
+ setSecureKey(credentialKey("aws", "access_key"), "AKIA_test");
251
252
 
252
253
  const result = await broker.serverUse({
253
254
  service: "aws",
@@ -270,7 +271,7 @@ describe("CredentialBroker.serverUse", () => {
270
271
  upsertCredentialMetadata("stripe", "secret_key", {
271
272
  allowedTools: [],
272
273
  });
273
- setSecureKey("credential:stripe:secret_key", "sk_test_xyz");
274
+ setSecureKey(credentialKey("stripe", "secret_key"), "sk_test_xyz");
274
275
 
275
276
  const result = await broker.serverUse({
276
277
  service: "stripe",
@@ -291,7 +292,7 @@ describe("CredentialBroker.serverUse", () => {
291
292
  allowedTools: ["git_push"],
292
293
  allowedDomains: ["github.com"],
293
294
  });
294
- setSecureKey("credential:github:oauth_token", "gho_test");
295
+ setSecureKey(credentialKey("github", "oauth_token"), "gho_test");
295
296
 
296
297
  const result = await broker.serverUse({
297
298
  service: "github",
@@ -322,7 +323,7 @@ describe("CredentialBroker.serverUse", () => {
322
323
  upsertCredentialMetadata("vercel", "api_token", {
323
324
  allowedTools: ["publish_page", "unpublish_page"],
324
325
  });
325
- setSecureKey("credential:vercel:api_token", "tok_updated");
326
+ setSecureKey(credentialKey("vercel", "api_token"), "tok_updated");
326
327
 
327
328
  const result = await broker.serverUse({
328
329
  service: "vercel",
@@ -342,8 +343,8 @@ describe("CredentialBroker.serverUse", () => {
342
343
  upsertCredentialMetadata("vercel", "deploy_hook", {
343
344
  allowedTools: ["trigger_deploy"],
344
345
  });
345
- setSecureKey("credential:vercel:api_token", "tok_api");
346
- setSecureKey("credential:vercel:deploy_hook", "hook_secret");
346
+ setSecureKey(credentialKey("vercel", "api_token"), "tok_api");
347
+ setSecureKey(credentialKey("vercel", "deploy_hook"), "hook_secret");
347
348
 
348
349
  // api_token should deny trigger_deploy
349
350
  const r1 = await broker.serverUse({
@@ -377,8 +378,8 @@ describe("CredentialBroker.serverUse", () => {
377
378
  upsertCredentialMetadata("gitlab", "api_token", {
378
379
  allowedTools: ["gitlab_api"],
379
380
  });
380
- setSecureKey("credential:github:api_token", "gh_tok");
381
- setSecureKey("credential:gitlab:api_token", "gl_tok");
381
+ setSecureKey(credentialKey("github", "api_token"), "gh_tok");
382
+ setSecureKey(credentialKey("gitlab", "api_token"), "gl_tok");
382
383
 
383
384
  // github credential should not serve gitlab tool
384
385
  const r1 = broker.serverUseById({
@@ -395,8 +396,8 @@ describe("CredentialBroker.serverUse", () => {
395
396
  upsertCredentialMetadata("gitlab", "api_token", {
396
397
  allowedTools: ["gitlab_api"],
397
398
  });
398
- setSecureKey("credential:github:api_token", "gh_tok");
399
- setSecureKey("credential:gitlab:api_token", "gl_tok");
399
+ setSecureKey(credentialKey("github", "api_token"), "gh_tok");
400
+ setSecureKey(credentialKey("gitlab", "api_token"), "gl_tok");
400
401
 
401
402
  // github credential should not serve gitlab tool
402
403
  const r1 = await broker.serverUse({
@@ -458,7 +459,7 @@ describe("CredentialBroker.serverUseById", () => {
458
459
  },
459
460
  ],
460
461
  });
461
- setSecureKey("credential:fal:api_key", "fal-secret-key");
462
+ setSecureKey(credentialKey("fal", "api_key"), "fal-secret-key");
462
463
 
463
464
  const result = broker.serverUseById({
464
465
  credentialId: meta.credentialId,
@@ -483,7 +484,7 @@ describe("CredentialBroker.serverUseById", () => {
483
484
  const meta = upsertCredentialMetadata("fal", "api_key", {
484
485
  allowedTools: ["media_proxy"],
485
486
  });
486
- setSecureKey("credential:fal:api_key", "fal-secret-key");
487
+ setSecureKey(credentialKey("fal", "api_key"), "fal-secret-key");
487
488
 
488
489
  const result = broker.serverUseById({
489
490
  credentialId: meta.credentialId,
@@ -514,7 +515,7 @@ describe("CredentialBroker.serverUseById", () => {
514
515
  allowedTools: ["media_proxy"],
515
516
  allowedDomains: ["github.com"],
516
517
  });
517
- setSecureKey("credential:github:oauth_token", "gho_test");
518
+ setSecureKey(credentialKey("github", "oauth_token"), "gho_test");
518
519
 
519
520
  const result = broker.serverUseById({
520
521
  credentialId: meta.credentialId,
@@ -531,7 +532,7 @@ describe("CredentialBroker.serverUseById", () => {
531
532
  const meta = upsertCredentialMetadata("vercel", "api_token", {
532
533
  allowedTools: ["media_proxy"],
533
534
  });
534
- setSecureKey("credential:vercel:api_token", "test-vercel-token");
535
+ setSecureKey(credentialKey("vercel", "api_token"), "test-vercel-token");
535
536
 
536
537
  const result = broker.serverUseById({
537
538
  credentialId: meta.credentialId,
@@ -547,7 +548,7 @@ describe("CredentialBroker.serverUseById", () => {
547
548
  const meta = upsertCredentialMetadata("stripe", "secret_key", {
548
549
  allowedTools: [],
549
550
  });
550
- setSecureKey("credential:stripe:secret_key", "sk_test_xyz");
551
+ setSecureKey(credentialKey("stripe", "secret_key"), "sk_test_xyz");
551
552
 
552
553
  const result = broker.serverUseById({
553
554
  credentialId: meta.credentialId,
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
5
5
 
6
+ import { credentialKey } from "../security/credential-key.js";
6
7
  import { CredentialBroker } from "../tools/credentials/broker.js";
7
8
  import {
8
9
  _setMetadataPath,
@@ -125,7 +126,7 @@ describe("CredentialBroker", () => {
125
126
 
126
127
  const result = broker.consume(auth.token.tokenId);
127
128
  expect(result.success).toBe(true);
128
- expect(result.storageKey).toBe("credential:github:token");
129
+ expect(result.storageKey).toBe(credentialKey("github", "token"));
129
130
  });
130
131
 
131
132
  test("rejects double consumption", () => {