@vellumai/assistant 0.4.45 → 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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -69,6 +69,8 @@ mock.module("./policy-validate.js", () => ({
69
69
  }),
70
70
  }));
71
71
 
72
+ import { credentialKey } from "../security/credential-key.js";
73
+
72
74
  const { credentialStoreTool } = await import("../tools/credentials/vault.js");
73
75
 
74
76
  describe("one-time send override", () => {
@@ -96,7 +98,7 @@ describe("one-time send override", () => {
96
98
  expect(result.isError).toBe(true);
97
99
  expect(result.content).toContain("not enabled");
98
100
  // Value must NOT be stored in keychain
99
- expect(storedKeys.has("credential:svc:key")).toBe(false);
101
+ expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
100
102
  });
101
103
 
102
104
  test("transient_send succeeds when allowOneTimeSend is enabled", async () => {
@@ -119,7 +121,7 @@ describe("one-time send override", () => {
119
121
  expect(result.isError).toBe(false);
120
122
  expect(result.content).toContain("NOT saved");
121
123
  // Value must NOT be stored in keychain
122
- expect(storedKeys.has("credential:svc:key")).toBe(false);
124
+ expect(storedKeys.has(credentialKey("svc", "key"))).toBe(false);
123
125
  });
124
126
 
125
127
  test("store delivery always persists to keychain regardless of allowOneTimeSend", async () => {
@@ -138,7 +140,7 @@ describe("one-time send override", () => {
138
140
  );
139
141
  expect(result.isError).toBe(false);
140
142
  expect(result.content).toContain("stored");
141
- expect(storedKeys.has("credential:svc:key")).toBe(true);
143
+ expect(storedKeys.has(credentialKey("svc", "key"))).toBe(true);
142
144
  });
143
145
 
144
146
  test("transient_send response content never contains the secret value", async () => {
@@ -5,6 +5,7 @@ import {
5
5
  redirectToSecurePrompt,
6
6
  } from "../daemon/session-messaging.js";
7
7
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
8
+ import { credentialKey } from "../security/credential-key.js";
8
9
 
9
10
  const setSecureKeyMock = mock((_key?: string, _value?: string) => true);
10
11
  const upsertCredentialMetadataMock = mock(
@@ -108,7 +109,7 @@ describe("session-messaging secret redirect", () => {
108
109
  label: "Telegram Bot Token",
109
110
  });
110
111
  expect(setSecureKeyMock).toHaveBeenCalledWith(
111
- "credential:telegram:bot_token",
112
+ credentialKey("telegram", "bot_token"),
112
113
  "123456789:ABCDefGHIJklmnopQRSTuvwxyz012345678",
113
114
  );
114
115
  expect(upsertCredentialMetadataMock).toHaveBeenCalledWith(
@@ -158,7 +159,7 @@ describe("session-messaging secret redirect", () => {
158
159
  label: "Telegram Bot Token",
159
160
  });
160
161
  expect(setSecureKeyMock).toHaveBeenCalledWith(
161
- "credential:telegram:bot_token",
162
+ credentialKey("telegram", "bot_token"),
162
163
  "123456789:ABCDefGHIJklmnopQRSTuvwxyz012345678",
163
164
  );
164
165
  });
@@ -197,7 +198,7 @@ describe("session-messaging secret redirect", () => {
197
198
  label: "Telegram Bot Token",
198
199
  });
199
200
  expect(setSecureKeyMock).toHaveBeenCalledWith(
200
- "credential:telegram:bot_token",
201
+ credentialKey("telegram", "bot_token"),
201
202
  "123456789:ABCDefGHIJklmnopQRSTuvwxyz012345678",
202
203
  );
203
204
  });
@@ -231,7 +232,7 @@ describe("session-messaging secret redirect", () => {
231
232
  label: "Secure Credential Entry",
232
233
  });
233
234
  expect(setSecureKeyMock).toHaveBeenCalledWith(
234
- "credential:detected:Some Unknown Secret",
235
+ credentialKey("detected", "Some Unknown Secret"),
235
236
  "opaque-secret",
236
237
  );
237
238
  expect(upsertCredentialMetadataMock).toHaveBeenCalledWith(
@@ -58,7 +58,7 @@ describe("assistant skills uninstall", () => {
58
58
 
59
59
  // GIVEN a skill is installed locally
60
60
  installFakeSkill("weather");
61
- writeSkillsIndex("- weather\n- google-oauth-setup\n");
61
+ writeSkillsIndex("- weather\n- google-oauth-applescript\n");
62
62
 
63
63
  // WHEN we uninstall the skill
64
64
  uninstallSkillLocally("weather");
@@ -71,7 +71,7 @@ describe("assistant skills uninstall", () => {
71
71
  expect(index).not.toContain("weather");
72
72
 
73
73
  // AND other skills should remain in the index
74
- expect(index).toContain("google-oauth-setup");
74
+ expect(index).toContain("google-oauth-applescript");
75
75
  });
76
76
 
77
77
  test("errors when skill is not installed", () => {
@@ -675,15 +675,6 @@ describe("ingress-dependent setup skills declare public-ingress", () => {
675
675
  expect(includes).toContain("public-ingress");
676
676
  });
677
677
 
678
- test("google-oauth-setup includes public-ingress", () => {
679
- const includes = readSkillIncludes(
680
- FIRST_PARTY_SKILLS_DIR,
681
- "google-oauth-setup",
682
- );
683
- expect(includes).toBeDefined();
684
- expect(includes).toContain("public-ingress");
685
- });
686
-
687
678
  test("slack-oauth-setup includes browser", () => {
688
679
  const includes = readSkillIncludes(
689
680
  FIRST_PARTY_SKILLS_DIR,
@@ -172,6 +172,7 @@ import {
172
172
  getSlackChannelConfig,
173
173
  setSlackChannelConfig,
174
174
  } from "../daemon/handlers/config-slack-channel.js";
175
+ import { credentialKey } from "../security/credential-key.js";
175
176
 
176
177
  afterAll(() => {
177
178
  globalThis.fetch = originalFetch;
@@ -199,8 +200,8 @@ describe("Slack channel config handler", () => {
199
200
  });
200
201
 
201
202
  test("GET returns connected: true when both tokens are set", () => {
202
- secureKeyStore["credential:slack_channel:bot_token"] = "xoxb-test";
203
- secureKeyStore["credential:slack_channel:app_token"] = "xapp-test";
203
+ secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
204
+ secureKeyStore[credentialKey("slack_channel", "app_token")] = "xapp-test";
204
205
 
205
206
  const result = getSlackChannelConfig();
206
207
  expect(result.success).toBe(true);
@@ -210,7 +211,7 @@ describe("Slack channel config handler", () => {
210
211
  });
211
212
 
212
213
  test("GET returns metadata from config when available", () => {
213
- secureKeyStore["credential:slack_channel:bot_token"] = "xoxb-test";
214
+ secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
214
215
  configStore = {
215
216
  slack: {
216
217
  teamId: "T123",
@@ -240,7 +241,7 @@ describe("Slack channel config handler", () => {
240
241
  );
241
242
  expect(result.success).toBe(true);
242
243
  expect(result.hasAppToken).toBe(true);
243
- expect(secureKeyStore["credential:slack_channel:app_token"]).toBe(
244
+ expect(secureKeyStore[credentialKey("slack_channel", "app_token")]).toBe(
244
245
  "xapp-valid-token-123",
245
246
  );
246
247
  });
@@ -296,8 +297,8 @@ describe("Slack channel config handler", () => {
296
297
  });
297
298
 
298
299
  test("DELETE clears credentials and config", async () => {
299
- secureKeyStore["credential:slack_channel:bot_token"] = "xoxb-test";
300
- secureKeyStore["credential:slack_channel:app_token"] = "xapp-test";
300
+ secureKeyStore[credentialKey("slack_channel", "bot_token")] = "xoxb-test";
301
+ secureKeyStore[credentialKey("slack_channel", "app_token")] = "xapp-test";
301
302
  credentialMetadataStore.push({
302
303
  service: "slack_channel",
303
304
  field: "bot_token",
@@ -322,10 +323,10 @@ describe("Slack channel config handler", () => {
322
323
  expect(result.connected).toBe(false);
323
324
 
324
325
  expect(
325
- secureKeyStore["credential:slack_channel:bot_token"],
326
+ secureKeyStore[credentialKey("slack_channel", "bot_token")],
326
327
  ).toBeUndefined();
327
328
  expect(
328
- secureKeyStore["credential:slack_channel:app_token"],
329
+ secureKeyStore[credentialKey("slack_channel", "app_token")],
329
330
  ).toBeUndefined();
330
331
  expect(credentialMetadataStore).toHaveLength(0);
331
332
 
@@ -1,5 +1,7 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
+ import { credentialKey } from "../security/credential-key.js";
4
+
3
5
  // ---------------------------------------------------------------------------
4
6
  // Mocks — must be declared before any imports that pull in mocked modules
5
7
  // ---------------------------------------------------------------------------
@@ -105,7 +107,7 @@ describe("handleListSlackChannels", () => {
105
107
 
106
108
  test("returns channels sorted by type then name", async () => {
107
109
  secureKeyValues.set(
108
- "credential:integration:slack:access_token",
110
+ credentialKey("integration:slack", "access_token"),
109
111
  "xoxb-test",
110
112
  );
111
113
 
@@ -176,7 +178,10 @@ describe("handleListSlackChannels", () => {
176
178
  });
177
179
 
178
180
  test("falls back to legacy bot token", async () => {
179
- secureKeyValues.set("credential:slack_channel:bot_token", "xoxb-legacy");
181
+ secureKeyValues.set(
182
+ credentialKey("slack_channel", "bot_token"),
183
+ "xoxb-legacy",
184
+ );
180
185
 
181
186
  listConversationsResult = { ok: true, channels: [] };
182
187
 
@@ -194,7 +199,7 @@ describe("handleShareToSlackChannel", () => {
194
199
 
195
200
  test("returns 400 for malformed JSON", async () => {
196
201
  secureKeyValues.set(
197
- "credential:integration:slack:access_token",
202
+ credentialKey("integration:slack", "access_token"),
198
203
  "xoxb-test",
199
204
  );
200
205
  const req = new Request("http://localhost/v1/slack/share", {
@@ -208,7 +213,7 @@ describe("handleShareToSlackChannel", () => {
208
213
 
209
214
  test("returns 400 when missing required fields", async () => {
210
215
  secureKeyValues.set(
211
- "credential:integration:slack:access_token",
216
+ credentialKey("integration:slack", "access_token"),
212
217
  "xoxb-test",
213
218
  );
214
219
  const req = makeRequest({ appId: "app1" });
@@ -220,7 +225,7 @@ describe("handleShareToSlackChannel", () => {
220
225
 
221
226
  test("returns 404 when app not found", async () => {
222
227
  secureKeyValues.set(
223
- "credential:integration:slack:access_token",
228
+ credentialKey("integration:slack", "access_token"),
224
229
  "xoxb-test",
225
230
  );
226
231
  appStoreResult = null;
@@ -231,7 +236,7 @@ describe("handleShareToSlackChannel", () => {
231
236
 
232
237
  test("posts message and returns success", async () => {
233
238
  secureKeyValues.set(
234
- "credential:integration:slack:access_token",
239
+ credentialKey("integration:slack", "access_token"),
235
240
  "xoxb-test",
236
241
  );
237
242
  appStoreResult = {
@@ -38,6 +38,9 @@ mock.module("../config/loader.js", () => ({
38
38
 
39
39
  mock.module("../security/secure-keys.js", () => ({
40
40
  getSecureKey: (_keyId: string) => mockSecureKey,
41
+ setSecureKey: (_account: string, _value: string) => true,
42
+ deleteSecureKey: (_account: string) => "deleted" as const,
43
+ listSecureKeys: () => [] as string[],
41
44
  }));
42
45
 
43
46
  // Suppress logger output during tests
@@ -26,11 +26,12 @@ mock.module("../inbound/public-ingress-urls.js", () => ({
26
26
  }));
27
27
 
28
28
  import { getTwilioConfig } from "../calls/twilio-config.js";
29
+ import { credentialKey } from "../security/credential-key.js";
29
30
 
30
31
  describe("twilio-config", () => {
31
32
  beforeEach(() => {
32
33
  mockSecureKeys = {
33
- "credential:twilio:auth_token": "test_auth_token",
34
+ [credentialKey("twilio", "auth_token")]: "test_auth_token",
34
35
  };
35
36
  mockLoadConfigResult = {
36
37
  twilio: {
@@ -8,6 +8,8 @@ import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
10
 
11
+ import { credentialKey } from "../security/credential-key.js";
12
+
11
13
  const testDir = mkdtempSync(join(tmpdir(), "twilio-provider-test-"));
12
14
 
13
15
  mock.module("../util/platform.js", () => ({
@@ -40,8 +42,8 @@ mock.module("../config/loader.js", () => ({
40
42
 
41
43
  mock.module("../security/secure-keys.js", () => ({
42
44
  getSecureKey: (key: string) => {
43
- if (key === "credential:twilio:auth_token") return mockAuthToken;
44
- if (key === "credential:twilio:account_sid") return mockAccountSid;
45
+ if (key === credentialKey("twilio", "auth_token")) return mockAuthToken;
46
+ if (key === credentialKey("twilio", "account_sid")) return mockAccountSid;
45
47
  return undefined;
46
48
  },
47
49
  }));
@@ -47,12 +47,12 @@ function readMockTwilioAccountSid(): string | undefined {
47
47
  const twilio = (mockRawConfigStore.twilio ?? {}) as Record<string, unknown>;
48
48
  return (
49
49
  (twilio.accountSid as string | undefined) ??
50
- mockSecureKeyStore["credential:twilio:account_sid"]
50
+ mockSecureKeyStore[credentialKey("twilio", "account_sid")]
51
51
  );
52
52
  }
53
53
 
54
54
  function readMockTwilioAuthToken(): string | undefined {
55
- return mockSecureKeyStore["credential:twilio:auth_token"];
55
+ return mockSecureKeyStore[credentialKey("twilio", "auth_token")];
56
56
  }
57
57
 
58
58
  function readMockTwilioPhoneNumber(): string | undefined {
@@ -333,6 +333,7 @@ import {
333
333
  handleProvisionTwilioNumber,
334
334
  handleSetTwilioCredentials,
335
335
  } from "../runtime/routes/integrations/twilio.js";
336
+ import { credentialKey } from "../security/credential-key.js";
336
337
 
337
338
  initializeDb();
338
339
 
@@ -429,8 +430,8 @@ describe("twilio webhook routes", () => {
429
430
  twilio: { accountSid: "AC_existing", phoneNumber: "+15550001111" },
430
431
  };
431
432
  mockSecureKeyStore = {
432
- "credential:twilio:account_sid": "AC_existing",
433
- "credential:twilio:auth_token": "test-auth-token",
433
+ [credentialKey("twilio", "account_sid")]: "AC_existing",
434
+ [credentialKey("twilio", "auth_token")]: "test-auth-token",
434
435
  };
435
436
  mockAvailableNumbers = [{ phoneNumber: "+15556667777" }];
436
437
  mockProvisionedNumber = { phoneNumber: "+15556667777" };
@@ -402,7 +402,7 @@ describe("isVerificationControlPlaneInvocation", () => {
402
402
  test("detects endpoint despite malformed percent-encoding elsewhere in command", () => {
403
403
  const result = isVerificationControlPlaneInvocation("bash", {
404
404
  command:
405
- 'curl -H "X: %ZZ" http://localhost:3000/v1/channel-verification-sessions -d \'{"channel":"sms"}\'',
405
+ 'curl -H "X: %ZZ" http://localhost:3000/v1/channel-verification-sessions -d \'{"channel":"telegram"}\'',
406
406
  });
407
407
  expect(result).toBe(true);
408
408
  });
@@ -3,7 +3,7 @@
3
3
  ## Approval Flow Resilience
4
4
 
5
5
  - **Rich delivery failures must degrade gracefully.** If delivering a rich approval prompt (e.g., Telegram inline buttons) fails, fall back to plain text with instructions (e.g., `Reply "yes" to approve`) — never auto-deny.
6
- - **Non-rich channels** (SMS, http-api) receive plain-text approval prompts. The conversational approval engine handles free-text responses.
6
+ - **Non-rich channels** (http-api) receive plain-text approval prompts. The conversational approval engine handles free-text responses.
7
7
  - **Race conditions:** Always check whether a decision has already been resolved before delivering the engine's optimistic reply. If `handleChannelDecision` returns `applied: false`, deliver an "already resolved" notice and return `stale_ignored`.
8
8
  - **Requester self-cancel:** A requester with a pending guardian approval must be able to cancel their own request (but not self-approve).
9
9
  - **Unified guardian decision primitive:** All guardian decision paths (callback buttons, conversational engine, requester self-cancel) must route through `applyGuardianDecision()` in `assistant/src/approvals/guardian-decision-primitive.ts`. Do not inline decision logic (approve_always downgrade, approval record updates, grant minting) at individual callsites.
@@ -21,6 +21,7 @@ import { upsertBinding } from "../memory/external-conversation-store.js";
21
21
  import { revokeScopedApprovalGrantsForContext } from "../memory/scoped-approval-grants.js";
22
22
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
23
23
  import { isGuardian } from "../runtime/channel-verification-service.js";
24
+ import { credentialKey } from "../security/credential-key.js";
24
25
  import { getSecureKey } from "../security/secure-keys.js";
25
26
  import { getLogger } from "../util/logger.js";
26
27
  import { upsertActiveCallLease } from "./active-call-lease.js";
@@ -135,7 +136,7 @@ export type CallerIdentityResult =
135
136
  * For `assistant_number`: uses the Twilio phone number from
136
137
  * `getTwilioConfig()`. No eligibility check is performed — this is a fast path.
137
138
  * For `user_number`: uses `config.calls.callerIdentity.userNumber` or the
138
- * secure key `credential:twilio:user_phone_number`, then validates that the
139
+ * secure key `credential/twilio/user_phone_number`, then validates that the
139
140
  * number is usable as an outbound caller ID via the Twilio API.
140
141
  */
141
142
  export async function resolveCallerIdentity(
@@ -197,7 +198,9 @@ export async function resolveCallerIdentity(
197
198
  userNumber = getTwilioUserPhoneNumber()!;
198
199
  numberSource = "env_var";
199
200
  } else {
200
- const secureKeyValue = getSecureKey("credential:twilio:user_phone_number");
201
+ const secureKeyValue = getSecureKey(
202
+ credentialKey("twilio", "user_phone_number"),
203
+ );
201
204
  if (secureKeyValue) {
202
205
  userNumber = secureKeyValue;
203
206
  numberSource = "secure_key";
@@ -212,7 +215,7 @@ export async function resolveCallerIdentity(
212
215
  return {
213
216
  ok: false,
214
217
  error:
215
- "user_number mode requires a user phone number. Set calls.callerIdentity.userNumber in config or store credential:twilio:user_phone_number via the credential_store tool.",
218
+ "user_number mode requires a user phone number. Set calls.callerIdentity.userNumber in config or store credential/twilio/user_phone_number via the credential_store tool.",
216
219
  };
217
220
  }
218
221
 
@@ -223,7 +226,7 @@ export async function resolveCallerIdentity(
223
226
  );
224
227
  return {
225
228
  ok: false,
226
- error: `User phone number "${userNumber}" is not in E.164 format (must start with + followed by digits, e.g. +14155551234). Check calls.callerIdentity.userNumber in config or credential:twilio:user_phone_number.`,
229
+ error: `User phone number "${userNumber}" is not in E.164 format (must start with + followed by digits, e.g. +14155551234). Check calls.callerIdentity.userNumber in config or credential/twilio/user_phone_number.`,
227
230
  };
228
231
  }
229
232
 
@@ -4,6 +4,7 @@ import {
4
4
  getPublicBaseUrl,
5
5
  getTwilioRelayUrl,
6
6
  } from "../inbound/public-ingress-urls.js";
7
+ import { credentialKey } from "../security/credential-key.js";
7
8
  import { getSecureKey } from "../security/secure-keys.js";
8
9
  import { ConfigError } from "../util/errors.js";
9
10
  import { getLogger } from "../util/logger.js";
@@ -45,7 +46,7 @@ export function resolveTwilioPhoneNumber(): string {
45
46
  export function getTwilioConfig(): TwilioConfig {
46
47
  const config = loadConfig();
47
48
  const accountSid = config.twilio?.accountSid || "";
48
- const authToken = getSecureKey("credential:twilio:auth_token") || "";
49
+ const authToken = getSecureKey(credentialKey("twilio", "auth_token")) || "";
49
50
  const phoneNumber = resolveTwilioPhoneNumber();
50
51
  const webhookBaseUrl = getPublicBaseUrl(config);
51
52
 
@@ -1,5 +1,6 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
2
 
3
+ import { credentialKey } from "../security/credential-key.js";
3
4
  import { getSecureKey } from "../security/secure-keys.js";
4
5
  import { ProviderError } from "../util/errors.js";
5
6
  import { getLogger } from "../util/logger.js";
@@ -280,7 +281,7 @@ export class TwilioConversationRelayProvider implements VoiceProvider {
280
281
  * middleware) can check availability independently.
281
282
  */
282
283
  static getAuthToken(): string | null {
283
- return getSecureKey("credential:twilio:auth_token") || null;
284
+ return getSecureKey(credentialKey("twilio", "auth_token")) || null;
284
285
  }
285
286
 
286
287
  /**
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { loadConfig } from "../config/loader.js";
10
+ import { credentialKey } from "../security/credential-key.js";
10
11
  import { getSecureKey } from "../security/secure-keys.js";
11
12
  import { ConfigError, ProviderError } from "../util/errors.js";
12
13
 
@@ -28,7 +29,7 @@ function resolveAccountSid(): string | undefined {
28
29
 
29
30
  /** Resolve the Twilio Auth Token from the credential store. */
30
31
  function resolveAuthToken(): string | undefined {
31
- return getSecureKey("credential:twilio:auth_token") || undefined;
32
+ return getSecureKey(credentialKey("twilio", "auth_token")) || undefined;
32
33
  }
33
34
 
34
35
  /** Resolve Twilio credentials from config (SID) and credential store (token). Throws if not configured. */
@@ -124,7 +125,6 @@ export async function searchAvailableNumbers(
124
125
  areaCode?: string,
125
126
  ): Promise<AvailablePhoneNumber[]> {
126
127
  const params = new URLSearchParams({
127
- SmsEnabled: "true",
128
128
  VoiceEnabled: "true",
129
129
  });
130
130
  if (areaCode) params.set("AreaCode", areaCode);
@@ -1,5 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
 
3
+ import type { ExtensionCommand } from "../../browser-extension-relay/protocol.js";
4
+ import { extensionRelayServer } from "../../browser-extension-relay/server.js";
3
5
  import {
4
6
  initAuthSigningKey,
5
7
  isSigningKeyInitialized,
@@ -16,22 +18,36 @@ import {
16
18
  } from "../../tools/browser/chrome-cdp.js";
17
19
 
18
20
  // ---------------------------------------------------------------------------
19
- // Shared relay helper
21
+ // Shared relay helper — dual-path: in-process first, gateway fallback
20
22
  // ---------------------------------------------------------------------------
21
23
 
22
24
  async function relayCommand(command: Record<string, unknown>): Promise<void> {
23
25
  try {
24
- if (!isSigningKeyInitialized()) {
25
- initAuthSigningKey(loadOrCreateSigningKey());
26
- }
27
-
28
- const { data } = await gatewayPost<{
29
- id: string;
26
+ // Try the in-process extensionRelayServer first (for when CLI runs
27
+ // within the daemon). Falls back to gateway HTTP for out-of-process
28
+ // CLI contexts.
29
+ let data: {
30
+ id?: string;
30
31
  success: boolean;
31
32
  result?: unknown;
32
33
  error?: string;
33
34
  tabId?: number;
34
- }>("/v1/browser-relay/command", command);
35
+ };
36
+
37
+ try {
38
+ data = await extensionRelayServer.sendCommand(
39
+ command as Omit<ExtensionCommand, "id">,
40
+ );
41
+ } catch {
42
+ // In-process relay unavailable — fall back to gateway HTTP
43
+ if (!isSigningKeyInitialized()) {
44
+ initAuthSigningKey(loadOrCreateSigningKey());
45
+ }
46
+ ({ data } = await gatewayPost<typeof data>(
47
+ "/v1/browser-relay/command",
48
+ command,
49
+ ));
50
+ }
35
51
 
36
52
  if (data.success) {
37
53
  process.stdout.write(
@@ -367,15 +383,24 @@ Examples:
367
383
  )
368
384
  .action(async () => {
369
385
  try {
370
- if (!isSigningKeyInitialized()) {
371
- initAuthSigningKey(loadOrCreateSigningKey());
372
- }
373
- const data = await gatewayGet<{
386
+ // Dual-path: try in-process first, fall back to gateway HTTP
387
+ let data: {
374
388
  connected: boolean;
375
- connectionId?: string;
376
- lastHeartbeatAt?: number;
389
+ connectionId?: string | null;
390
+ lastHeartbeatAt?: number | null;
377
391
  pendingCommandCount: number;
378
- }>("/v1/browser-relay/status");
392
+ };
393
+
394
+ try {
395
+ data = extensionRelayServer.getStatus();
396
+ } catch {
397
+ // In-process relay unavailable — fall back to gateway HTTP
398
+ if (!isSigningKeyInitialized()) {
399
+ initAuthSigningKey(loadOrCreateSigningKey());
400
+ }
401
+ data = await gatewayGet<typeof data>("/v1/browser-relay/status");
402
+ }
403
+
379
404
  process.stdout.write(
380
405
  JSON.stringify({
381
406
  ok: true,
@@ -1,5 +1,6 @@
1
1
  import type { Command } from "commander";
2
2
 
3
+ import { credentialKey } from "../../security/credential-key.js";
3
4
  import {
4
5
  deleteSecureKeyAsync,
5
6
  getSecureKey,
@@ -127,7 +128,7 @@ export function registerCredentialsCommand(program: Command): void {
127
128
  "after",
128
129
  `
129
130
  Credentials are identified by name in service:field format, matching the
130
- storage convention used internally (credential:{service}:{field}):
131
+ storage convention used internally (credential/{service}/{field}):
131
132
 
132
133
  twilio:account_sid Twilio account SID
133
134
  twilio:auth_token Twilio auth token
@@ -198,7 +199,7 @@ Examples:
198
199
  }
199
200
 
200
201
  const credentials = allMetadata.map((m) => {
201
- const secret = getSecureKey(`credential:${m.service}:${m.field}`);
202
+ const secret = getSecureKey(credentialKey(m.service, m.field));
202
203
  return buildCredentialOutput(m, secret);
203
204
  });
204
205
 
@@ -273,7 +274,7 @@ Examples:
273
274
  }
274
275
 
275
276
  const { service, field } = parsed;
276
- const storageKey = `credential:${service}:${field}`;
277
+ const storageKey = credentialKey(service, field);
277
278
 
278
279
  assertMetadataWritable();
279
280
 
@@ -350,7 +351,7 @@ Examples:
350
351
  }
351
352
 
352
353
  const { service, field } = parsed;
353
- const storageKey = `credential:${service}:${field}`;
354
+ const storageKey = credentialKey(service, field);
354
355
 
355
356
  assertMetadataWritable();
356
357
 
@@ -424,11 +425,11 @@ Examples:
424
425
  return;
425
426
  }
426
427
  metadata = getCredentialMetadata(parsed.service, parsed.field);
427
- storageKey = `credential:${parsed.service}:${parsed.field}`;
428
+ storageKey = credentialKey(parsed.service, parsed.field);
428
429
  } else {
429
430
  metadata = getCredentialMetadataById(name);
430
431
  if (metadata) {
431
- storageKey = `credential:${metadata.service}:${metadata.field}`;
432
+ storageKey = credentialKey(metadata.service, metadata.field);
432
433
  } else {
433
434
  // No metadata found by UUID, and we can't determine the storage key
434
435
  writeOutput(cmd, { ok: false, error: "Credential not found" });
@@ -529,11 +530,11 @@ Examples:
529
530
  process.exitCode = 1;
530
531
  return;
531
532
  }
532
- storageKey = `credential:${parsed.service}:${parsed.field}`;
533
+ storageKey = credentialKey(parsed.service, parsed.field);
533
534
  } else {
534
535
  const metadata = getCredentialMetadataById(name);
535
536
  if (metadata) {
536
- storageKey = `credential:${metadata.service}:${metadata.field}`;
537
+ storageKey = credentialKey(metadata.service, metadata.field);
537
538
  } else {
538
539
  writeOutput(cmd, { ok: false, error: "Credential not found" });
539
540
  process.exitCode = 1;
@@ -17,7 +17,7 @@ guaranteed-valid access token, refreshing transparently if the stored token
17
17
  is expired or near-expiry. Callers never need to handle refresh themselves.
18
18
 
19
19
  The <service> argument is the short integration name (e.g. "twitter", "gmail",
20
- "slack"). Internally this maps to credential:integration:<service>:access_token.
20
+ "slack"). Internally this maps to credential/integration:<service>/access_token.
21
21
 
22
22
  Examples:
23
23
  $ assistant oauth token twitter
package/src/cli.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
2
3
  import { dirname } from "node:path";
3
4
  import * as readline from "node:readline";
@@ -503,7 +504,7 @@ export async function startCli(): Promise<void> {
503
504
  const trimmed = answer.trim().toLowerCase();
504
505
  if (trimmed === "n") {
505
506
  // Create a new conversation by using a unique key
506
- conversationKey = `builtin-cli:${Date.now()}`;
507
+ conversationKey = `builtin-cli:${randomUUID()}`;
507
508
  sessionId = "";
508
509
  pendingSessionPick = false;
509
510
  // Reconnect SSE with new conversation key
@@ -1147,7 +1148,7 @@ export async function startCli(): Promise<void> {
1147
1148
 
1148
1149
  if (content === "/new") {
1149
1150
  // Create a new conversation by using a unique key
1150
- conversationKey = `builtin-cli:${Date.now()}`;
1151
+ conversationKey = `builtin-cli:${randomUUID()}`;
1151
1152
  sessionId = "";
1152
1153
  reconnectSse().then(() => {
1153
1154
  process.stdout.write(
@@ -37,10 +37,6 @@
37
37
  "type": "string",
38
38
  "enum": ["general", "researcher", "coder", "reviewer"],
39
39
  "description": "Worker profile that scopes tool access. Defaults to general (backward compatible)."
40
- },
41
- "reason": {
42
- "type": "string",
43
- "description": "Brief non-technical explanation of what you are delegating and why, shown to the user as a status update. Use simple language a non-technical person would understand."
44
40
  }
45
41
  }
46
42
  },