@vellumai/assistant 0.4.34 → 0.4.36

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 (251) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +4 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +91 -43
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +806 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +491 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +133 -242
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +177 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +175 -145
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +12 -1
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  175. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  176. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  177. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  178. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  179. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  180. package/src/memory/migrations/114-notifications.ts +37 -15
  181. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  182. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  183. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  184. package/src/memory/migrations/index.ts +2 -0
  185. package/src/memory/migrations/schema-introspection.ts +18 -0
  186. package/src/memory/schema-migration.ts +1 -0
  187. package/src/memory/shared-app-links-store.ts +1 -1
  188. package/src/messaging/registry.ts +27 -0
  189. package/src/notifications/README.md +79 -70
  190. package/src/notifications/broadcaster.ts +2 -1
  191. package/src/notifications/conversation-pairing.ts +147 -13
  192. package/src/notifications/copy-composer.ts +7 -3
  193. package/src/notifications/destination-resolver.ts +14 -1
  194. package/src/notifications/emit-signal.ts +3 -2
  195. package/src/notifications/signal.ts +105 -1
  196. package/src/notifications/types.ts +16 -0
  197. package/src/permissions/checker.ts +29 -3
  198. package/src/permissions/prompter.ts +11 -3
  199. package/src/runtime/access-request-helper.ts +2 -1
  200. package/src/runtime/auth/route-policy.ts +7 -1
  201. package/src/runtime/channel-invite-transport.ts +40 -63
  202. package/src/runtime/channel-invite-transports/email.ts +13 -39
  203. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  204. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  205. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  206. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  207. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  208. package/src/runtime/channel-readiness-service.ts +202 -45
  209. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  210. package/src/runtime/guardian-outbound-actions.ts +8 -5
  211. package/src/runtime/http-server.ts +5 -9
  212. package/src/runtime/http-types.ts +13 -1
  213. package/src/runtime/invite-instruction-generator.ts +178 -0
  214. package/src/runtime/invite-service.ts +22 -25
  215. package/src/runtime/migrations/migration-transport.ts +13 -0
  216. package/src/runtime/routes/app-routes.ts +1 -1
  217. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  218. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  219. package/src/runtime/routes/contact-routes.ts +54 -26
  220. package/src/runtime/routes/guardian-bootstrap-routes.ts +1 -1
  221. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  222. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  223. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  224. package/src/runtime/routes/integration-routes.ts +1 -1
  225. package/src/runtime/routes/invite-routes.ts +1 -1
  226. package/src/runtime/routes/secret-routes.ts +31 -7
  227. package/src/runtime/routes/surface-content-routes.ts +104 -0
  228. package/src/runtime/routes/twilio-routes.ts +32 -1
  229. package/src/runtime/routes/usage-routes.ts +114 -0
  230. package/src/runtime/tool-grant-request-helper.ts +2 -1
  231. package/src/security/encrypted-store.ts +9 -5
  232. package/src/security/keychain-broker-client.ts +393 -0
  233. package/src/security/secure-keys.ts +106 -321
  234. package/src/tools/apps/executors.ts +73 -0
  235. package/src/tools/browser/auto-navigate.ts +15 -6
  236. package/src/tools/browser/chrome-cdp.ts +211 -0
  237. package/src/tools/browser/network-recorder.test.ts +83 -0
  238. package/src/tools/browser/network-recorder.ts +8 -7
  239. package/src/tools/browser/x-auto-navigate.ts +12 -6
  240. package/src/tools/credentials/policy-types.ts +24 -0
  241. package/src/tools/credentials/vault.ts +22 -27
  242. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  243. package/src/tools/permission-checker.ts +1 -0
  244. package/src/tools/types.ts +2 -0
  245. package/src/tools/ui-surface/definitions.ts +1 -2
  246. package/src/tools/watch/watch-state.ts +2 -0
  247. package/src/__tests__/key-migration.test.ts +0 -240
  248. package/src/__tests__/keychain.test.ts +0 -286
  249. package/src/cli/core-commands.ts +0 -899
  250. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  251. package/src/security/keychain.ts +0 -490
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Tests that the channel readiness service returns real readiness snapshots
3
+ * for email and WhatsApp channels (not unsupported placeholders).
4
+ *
5
+ * Uses the same mock approach as channel-readiness-service.test.ts but
6
+ * exercises the createReadinessService factory to verify probe registration.
7
+ */
8
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Mocks — must be set up before importing the service
12
+ // ---------------------------------------------------------------------------
13
+
14
+ let mockTwilioPhoneNumberEnv: string | undefined;
15
+ let mockRawConfig: Record<string, unknown> | undefined;
16
+ let mockSecureKeys: Record<string, string>;
17
+ let mockHasTwilioCredentials: boolean;
18
+ let mockPrimaryInboxAddress: string | undefined;
19
+
20
+ mock.module("../calls/twilio-rest.js", () => ({
21
+ hasTwilioCredentials: () => mockHasTwilioCredentials,
22
+ getPhoneNumberSid: async () => null,
23
+ getTollFreeVerificationStatus: async () => null,
24
+ }));
25
+
26
+ mock.module("../config/env.js", () => ({
27
+ getTwilioPhoneNumberEnv: () => mockTwilioPhoneNumberEnv,
28
+ }));
29
+
30
+ mock.module("../config/loader.js", () => ({
31
+ loadRawConfig: () => mockRawConfig,
32
+ getConfig: () => {
33
+ const raw = mockRawConfig ?? {};
34
+ const wa = (raw.whatsapp ?? {}) as Record<string, unknown>;
35
+ return { whatsapp: { phoneNumber: (wa.phoneNumber as string) ?? "" } };
36
+ },
37
+ invalidateConfigCache: () => {},
38
+ }));
39
+
40
+ mock.module("../security/secure-keys.js", () => ({
41
+ getSecureKey: (key: string) => mockSecureKeys[key] ?? null,
42
+ }));
43
+
44
+ mock.module("../email/service.js", () => ({
45
+ getEmailService: () => ({
46
+ getPrimaryInboxAddress: async () => mockPrimaryInboxAddress,
47
+ }),
48
+ }));
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Import under test
52
+ // ---------------------------------------------------------------------------
53
+
54
+ import { createReadinessService } from "../runtime/channel-readiness-service.js";
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Tests
58
+ // ---------------------------------------------------------------------------
59
+
60
+ describe("channel readiness routes — email and WhatsApp probes", () => {
61
+ beforeEach(() => {
62
+ mockTwilioPhoneNumberEnv = undefined;
63
+ mockRawConfig = undefined;
64
+ mockSecureKeys = {};
65
+ mockHasTwilioCredentials = false;
66
+ mockPrimaryInboxAddress = undefined;
67
+ });
68
+
69
+ // -------------------------------------------------------------------------
70
+ // Email probe
71
+ // -------------------------------------------------------------------------
72
+
73
+ describe("email", () => {
74
+ test("returns real readiness snapshot (not unsupported)", async () => {
75
+ const service = createReadinessService();
76
+ const [snapshot] = await service.getReadiness("email");
77
+
78
+ expect(snapshot.channel).toBe("email");
79
+ // Should have real local checks, not the unsupported placeholder
80
+ expect(snapshot.localChecks.length).toBeGreaterThan(0);
81
+ expect(
82
+ snapshot.reasons.some((r) => r.code === "unsupported_channel"),
83
+ ).toBe(false);
84
+ });
85
+
86
+ test("reports not ready when AgentMail API key is missing", async () => {
87
+ mockRawConfig = {};
88
+ const service = createReadinessService();
89
+ const [snapshot] = await service.getReadiness("email");
90
+
91
+ expect(snapshot.ready).toBe(false);
92
+ expect(snapshot.reasons.some((r) => r.code === "agentmail_api_key")).toBe(
93
+ true,
94
+ );
95
+ });
96
+
97
+ test("checks invite policy", async () => {
98
+ mockSecureKeys = { agentmail: "test-key" };
99
+ mockRawConfig = {
100
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
101
+ };
102
+ const service = createReadinessService();
103
+ const [snapshot] = await service.getReadiness("email");
104
+
105
+ const inviteCheck = snapshot.localChecks.find(
106
+ (c) => c.name === "invite_policy",
107
+ );
108
+ expect(inviteCheck).toBeDefined();
109
+ // Email has codeRedemptionEnabled: true in the channel policy registry
110
+ expect(inviteCheck!.passed).toBe(true);
111
+ });
112
+
113
+ test("checks ingress configuration", async () => {
114
+ mockSecureKeys = { agentmail: "test-key" };
115
+ mockRawConfig = {};
116
+ const service = createReadinessService();
117
+ const [snapshot] = await service.getReadiness("email");
118
+
119
+ const ingressCheck = snapshot.localChecks.find(
120
+ (c) => c.name === "ingress",
121
+ );
122
+ expect(ingressCheck).toBeDefined();
123
+ expect(ingressCheck!.passed).toBe(false);
124
+ });
125
+
126
+ test("ready when all prerequisites are met (including inbox)", async () => {
127
+ mockSecureKeys = { agentmail: "test-key" };
128
+ mockRawConfig = {
129
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
130
+ };
131
+ mockPrimaryInboxAddress = "hello@example.agentmail.to";
132
+ const service = createReadinessService();
133
+ const [snapshot] = await service.getReadiness("email", true);
134
+
135
+ expect(snapshot.ready).toBe(true);
136
+ expect(snapshot.reasons).toHaveLength(0);
137
+ });
138
+
139
+ test("not ready when inbox is missing (remote check)", async () => {
140
+ mockSecureKeys = { agentmail: "test-key" };
141
+ mockRawConfig = {
142
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
143
+ };
144
+ mockPrimaryInboxAddress = undefined;
145
+ const service = createReadinessService();
146
+ const [snapshot] = await service.getReadiness("email", true);
147
+
148
+ expect(snapshot.ready).toBe(false);
149
+ expect(snapshot.reasons.some((r) => r.code === "inbox_configured")).toBe(
150
+ true,
151
+ );
152
+ });
153
+
154
+ test("local-only readiness still passes without inbox check", async () => {
155
+ mockSecureKeys = { agentmail: "test-key" };
156
+ mockRawConfig = {
157
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
158
+ };
159
+ // No inbox configured — explicitly opt out of remote checks
160
+ const service = createReadinessService();
161
+ const [snapshot] = await service.getReadiness("email", false);
162
+
163
+ // Local checks pass — remote inbox check is not included
164
+ expect(snapshot.ready).toBe(true);
165
+ });
166
+ });
167
+
168
+ // -------------------------------------------------------------------------
169
+ // WhatsApp probe — Meta WhatsApp Business API credentials
170
+ // -------------------------------------------------------------------------
171
+
172
+ describe("whatsapp", () => {
173
+ test("returns real readiness snapshot (not unsupported)", async () => {
174
+ const service = createReadinessService();
175
+ const [snapshot] = await service.getReadiness("whatsapp");
176
+
177
+ expect(snapshot.channel).toBe("whatsapp");
178
+ expect(snapshot.localChecks.length).toBeGreaterThan(0);
179
+ expect(
180
+ snapshot.reasons.some((r) => r.code === "unsupported_channel"),
181
+ ).toBe(false);
182
+ });
183
+
184
+ test("reports not ready when Meta credentials are missing", async () => {
185
+ mockRawConfig = {};
186
+ const service = createReadinessService();
187
+ const [snapshot] = await service.getReadiness("whatsapp");
188
+
189
+ expect(snapshot.ready).toBe(false);
190
+ expect(
191
+ snapshot.reasons.some((r) => r.code === "whatsapp_phone_number_id"),
192
+ ).toBe(true);
193
+ expect(
194
+ snapshot.reasons.some((r) => r.code === "whatsapp_access_token"),
195
+ ).toBe(true);
196
+ });
197
+
198
+ test("reports ready when all Meta credentials and display number are configured", async () => {
199
+ 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",
204
+ };
205
+ mockRawConfig = {
206
+ whatsapp: { phoneNumber: "+15551234567" },
207
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
208
+ };
209
+ const service = createReadinessService();
210
+ const [snapshot] = await service.getReadiness("whatsapp");
211
+
212
+ expect(snapshot.ready).toBe(true);
213
+ expect(snapshot.reasons).toHaveLength(0);
214
+ });
215
+
216
+ test("reports not ready when display phone number is missing", async () => {
217
+ 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",
222
+ };
223
+ mockRawConfig = {
224
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
225
+ };
226
+ const service = createReadinessService();
227
+ const [snapshot] = await service.getReadiness("whatsapp");
228
+
229
+ expect(snapshot.ready).toBe(false);
230
+ expect(
231
+ snapshot.reasons.some(
232
+ (r) => r.code === "whatsapp_display_phone_number",
233
+ ),
234
+ ).toBe(true);
235
+ });
236
+
237
+ test("checks each Meta credential individually", async () => {
238
+ mockSecureKeys = {
239
+ "credential:whatsapp:phone_number_id": "123456789",
240
+ // access_token missing
241
+ "credential:whatsapp:app_secret": "abc123",
242
+ "credential:whatsapp:webhook_verify_token": "my-verify-token",
243
+ };
244
+ mockRawConfig = {
245
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
246
+ };
247
+ const service = createReadinessService();
248
+ const [snapshot] = await service.getReadiness("whatsapp");
249
+
250
+ expect(snapshot.ready).toBe(false);
251
+
252
+ const phoneIdCheck = snapshot.localChecks.find(
253
+ (c) => c.name === "whatsapp_phone_number_id",
254
+ );
255
+ expect(phoneIdCheck!.passed).toBe(true);
256
+
257
+ const accessTokenCheck = snapshot.localChecks.find(
258
+ (c) => c.name === "whatsapp_access_token",
259
+ );
260
+ expect(accessTokenCheck!.passed).toBe(false);
261
+
262
+ const appSecretCheck = snapshot.localChecks.find(
263
+ (c) => c.name === "whatsapp_app_secret",
264
+ );
265
+ expect(appSecretCheck!.passed).toBe(true);
266
+
267
+ const webhookCheck = snapshot.localChecks.find(
268
+ (c) => c.name === "whatsapp_webhook_verify_token",
269
+ );
270
+ expect(webhookCheck!.passed).toBe(true);
271
+ });
272
+
273
+ test("checks invite policy", async () => {
274
+ 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",
279
+ };
280
+ mockRawConfig = {
281
+ whatsapp: { phoneNumber: "+15551234567" },
282
+ ingress: { publicBaseUrl: "https://example.com", enabled: true },
283
+ };
284
+ const service = createReadinessService();
285
+ const [snapshot] = await service.getReadiness("whatsapp");
286
+
287
+ const inviteCheck = snapshot.localChecks.find(
288
+ (c) => c.name === "invite_policy",
289
+ );
290
+ expect(inviteCheck).toBeDefined();
291
+ expect(inviteCheck!.passed).toBe(true);
292
+ });
293
+
294
+ test("checks ingress configuration", async () => {
295
+ 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",
300
+ };
301
+ mockRawConfig = {
302
+ whatsapp: { phoneNumber: "+15551234567" },
303
+ };
304
+ const service = createReadinessService();
305
+ const [snapshot] = await service.getReadiness("whatsapp");
306
+
307
+ const ingressCheck = snapshot.localChecks.find(
308
+ (c) => c.name === "ingress",
309
+ );
310
+ expect(ingressCheck).toBeDefined();
311
+ expect(ingressCheck!.passed).toBe(false);
312
+ });
313
+ });
314
+
315
+ // -------------------------------------------------------------------------
316
+ // Factory coverage — all channels registered
317
+ // -------------------------------------------------------------------------
318
+
319
+ describe("createReadinessService factory", () => {
320
+ test("registers probes for all deliverable channels including email and whatsapp", async () => {
321
+ const service = createReadinessService();
322
+ const snapshots = await service.getReadiness();
323
+
324
+ const channels = snapshots.map((s) => s.channel).sort();
325
+ expect(channels).toContain("email");
326
+ expect(channels).toContain("whatsapp");
327
+
328
+ // None should be unsupported placeholders
329
+ for (const s of snapshots) {
330
+ expect(s.reasons.some((r) => r.code === "unsupported_channel")).toBe(
331
+ false,
332
+ );
333
+ }
334
+ });
335
+ });
336
+ });
@@ -2570,7 +2570,7 @@ describe("Permission Checker", () => {
2570
2570
  expect(result.reason).toContain("Strict mode");
2571
2571
  });
2572
2572
 
2573
- test("legacy mode: file_write to skill source still prompts as High risk", async () => {
2573
+ test("workspace mode: file_write to skill source still prompts as High risk", async () => {
2574
2574
  testConfig.permissions.mode = "workspace";
2575
2575
  ensureSkillsDir();
2576
2576
  const skillPath = join(
@@ -2996,7 +2996,7 @@ describe("Permission Checker", () => {
2996
2996
  ensureSkillsDir();
2997
2997
  writeSkill("test-hash-skill", "Test Hash Skill");
2998
2998
 
2999
- // skill_load is Low risk, so with no trust rule in legacy mode it
2999
+ // skill_load is Low risk, so with no trust rule in workspace mode it
3000
3000
  // auto-allows. We set strict mode and add specific rules to verify
3001
3001
  // the correct candidates are generated.
3002
3002
  testConfig.permissions.mode = "strict";
@@ -3332,7 +3332,7 @@ describe("Permission Checker", () => {
3332
3332
  expect(result.matchedRule!.pattern).toBe("skill_load:pr34-bare-id");
3333
3333
  });
3334
3334
 
3335
- test("skill_load auto-allows in legacy mode (backward compat)", async () => {
3335
+ test("skill_load auto-allows in workspace mode", async () => {
3336
3336
  testConfig.permissions.mode = "workspace";
3337
3337
  const result = await check("skill_load", { skill: "any-skill" }, "/tmp");
3338
3338
  expect(result.decision).toBe("allow");
@@ -3855,7 +3855,7 @@ describe("Permission Checker", () => {
3855
3855
  // high-risk allow) if they choose. ────────────────────────────
3856
3856
 
3857
3857
  describe("Invariant 6: user can set broad rules if they choose", () => {
3858
- test("wildcard allow rule matches any command in legacy mode", async () => {
3858
+ test("wildcard allow rule matches any command in workspace mode", async () => {
3859
3859
  testConfig.permissions.mode = "workspace";
3860
3860
  addRule("bash", "*", "everywhere");
3861
3861
  const result = await check(
@@ -4208,7 +4208,7 @@ describe("Permission Checker", () => {
4208
4208
  });
4209
4209
  }
4210
4210
 
4211
- test("browser tools are auto-allowed in legacy mode", async () => {
4211
+ test("browser tools are auto-allowed in workspace mode", async () => {
4212
4212
  testConfig.permissions = { mode: "workspace" };
4213
4213
  for (const toolName of browserToolNames) {
4214
4214
  const result = await check(toolName, {}, "/tmp");
@@ -4404,7 +4404,7 @@ describe("computer-use tool permission defaults", () => {
4404
4404
  for (const name of cuToolNames) {
4405
4405
  const risk = await classifyRisk(name, {});
4406
4406
  // CU tools are proxy tools with RiskLevel.Low, but classifyRisk looks them up
4407
- // in the registry. In legacy mode, Low risk tools are auto-allowed.
4407
+ // in the registry. In workspace mode, Low risk tools are auto-allowed.
4408
4408
  expect(risk).toBe(RiskLevel.Low);
4409
4409
  }
4410
4410
  });