@vellumai/assistant 0.4.35 → 0.4.37

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 (239) 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 +5 -2
  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 +29 -0
  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 +814 -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 +494 -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} +134 -245
  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 +175 -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 +135 -34
  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 +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -17,7 +17,10 @@ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
17
17
  const testDir = mkdtempSync(join(tmpdir(), "tc-approval-notifier-test-"));
18
18
 
19
19
  // ── Platform mock ──
20
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
21
+ const realPlatform = require("../util/platform.js");
20
22
  mock.module("../util/platform.js", () => ({
23
+ ...realPlatform,
21
24
  getDataDir: () => testDir,
22
25
  isMacOS: () => process.platform === "darwin",
23
26
  isLinux: () => process.platform === "linux",
@@ -34,7 +37,10 @@ mock.module("../util/platform.js", () => ({
34
37
  }));
35
38
 
36
39
  // ── Logger mock ──
40
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
41
+ const realLogger = require("../util/logger.js");
37
42
  mock.module("../util/logger.js", () => ({
43
+ ...realLogger,
38
44
  getLogger: () =>
39
45
  new Proxy({} as Record<string, unknown>, {
40
46
  get: () => () => {},
@@ -130,9 +136,11 @@ mock.module("../config/env.js", () => ({
130
136
  }));
131
137
 
132
138
  // ── User reference mock ──
139
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
140
+ const realUserReference = require("../config/user-reference.js");
133
141
  mock.module("../config/user-reference.js", () => ({
142
+ ...realUserReference,
134
143
  resolveUserReference: () => "my human",
135
- DEFAULT_USER_REFERENCE: "my human",
136
144
  resolveGuardianName: (guardianDisplayName?: string | null): string => {
137
145
  // Mirror the real implementation: USER.md name > guardianDisplayName > default
138
146
  const userRef = "my human"; // In tests, resolveUserReference() returns this
@@ -79,9 +79,9 @@ mock.module("../security/secure-keys.js", () => ({
79
79
  deleteSecureKey: (account: string) => {
80
80
  if (account in secureKeyStore) {
81
81
  delete secureKeyStore[account];
82
- return true;
82
+ return "deleted";
83
83
  }
84
- return false;
84
+ return "not-found";
85
85
  },
86
86
  listSecureKeys: () => Object.keys(secureKeyStore),
87
87
  getBackendType: () => "encrypted",
@@ -10,7 +10,7 @@ mock.module("../security/secure-keys.js", () => ({
10
10
  secureKeyStore[account] = value;
11
11
  return true;
12
12
  },
13
- deleteSecureKey: () => true,
13
+ deleteSecureKey: () => "deleted",
14
14
  listSecureKeys: () => Object.keys(secureKeyStore),
15
15
  getBackendType: () => "encrypted",
16
16
  isDowngradedFromKeychain: () => false,
@@ -0,0 +1,339 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ const testDir = mkdtempSync(join(tmpdir(), "usage-routes-test-"));
7
+
8
+ mock.module("../util/platform.js", () => ({
9
+ getDataDir: () => testDir,
10
+ isMacOS: () => process.platform === "darwin",
11
+ isLinux: () => process.platform === "linux",
12
+ isWindows: () => process.platform === "win32",
13
+ getSocketPath: () => join(testDir, "test.sock"),
14
+ getPidPath: () => join(testDir, "test.pid"),
15
+ getDbPath: () => join(testDir, "test.db"),
16
+ getLogPath: () => join(testDir, "test.log"),
17
+ ensureDataDir: () => {},
18
+ }));
19
+
20
+ mock.module("../util/logger.js", () => ({
21
+ getLogger: () =>
22
+ new Proxy({} as Record<string, unknown>, {
23
+ get: () => () => {},
24
+ }),
25
+ }));
26
+
27
+ import { getSqlite, initializeDb, resetDb } from "../memory/db.js";
28
+ import { recordUsageEvent } from "../memory/llm-usage-store.js";
29
+ import { usageRouteDefinitions } from "../runtime/routes/usage-routes.js";
30
+
31
+ initializeDb();
32
+
33
+ afterAll(() => {
34
+ resetDb();
35
+ try {
36
+ rmSync(testDir, { recursive: true });
37
+ } catch {
38
+ /* best effort */
39
+ }
40
+ });
41
+
42
+ function clearUsageEvents() {
43
+ getSqlite().run("DELETE FROM llm_usage_events");
44
+ }
45
+
46
+ // Build a simple dispatch helper from route definitions
47
+ const routes = usageRouteDefinitions();
48
+
49
+ function dispatch(method: string, path: string): Promise<Response> | Response {
50
+ const url = new URL(`http://localhost/v1/${path}`);
51
+ const req = new Request(url.toString(), { method });
52
+ const route = routes.find(
53
+ (r) =>
54
+ r.method === method &&
55
+ `usage/${url.pathname.split("/v1/usage/")[1]?.split("?")[0]}` ===
56
+ r.endpoint,
57
+ );
58
+ if (!route) throw new Error(`No route for ${method} /v1/${path}`);
59
+ return route.handler({
60
+ req,
61
+ url,
62
+ server: null as never,
63
+ authContext: {} as never,
64
+ params: {},
65
+ });
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Seed data helper
70
+ // ---------------------------------------------------------------------------
71
+
72
+ function seedEvents() {
73
+ const day1 = new Date("2025-01-15T10:00:00Z").getTime();
74
+ const day2 = new Date("2025-01-16T14:00:00Z").getTime();
75
+
76
+ // Two events on day 1, one on day 2
77
+ recordUsageEvent(
78
+ {
79
+ conversationId: "conv-1",
80
+ runId: "run-1",
81
+ requestId: "req-1",
82
+ actor: "main_agent",
83
+ provider: "anthropic",
84
+ model: "claude-sonnet-4-20250514",
85
+ inputTokens: 1000,
86
+ outputTokens: 200,
87
+ cacheCreationInputTokens: 50,
88
+ cacheReadInputTokens: 100,
89
+ },
90
+ { estimatedCostUsd: 0.005, pricingStatus: "priced" },
91
+ );
92
+ // Backdate the first event
93
+ getSqlite().run(
94
+ "UPDATE llm_usage_events SET created_at = ? WHERE request_id = 'req-1'",
95
+ [day1],
96
+ );
97
+
98
+ recordUsageEvent(
99
+ {
100
+ conversationId: "conv-1",
101
+ runId: "run-1",
102
+ requestId: "req-2",
103
+ actor: "context_compactor",
104
+ provider: "anthropic",
105
+ model: "claude-haiku-3",
106
+ inputTokens: 500,
107
+ outputTokens: 100,
108
+ cacheCreationInputTokens: 0,
109
+ cacheReadInputTokens: 0,
110
+ },
111
+ { estimatedCostUsd: 0.001, pricingStatus: "priced" },
112
+ );
113
+ getSqlite().run(
114
+ "UPDATE llm_usage_events SET created_at = ? WHERE request_id = 'req-2'",
115
+ [day1 + 3600_000],
116
+ );
117
+
118
+ recordUsageEvent(
119
+ {
120
+ conversationId: "conv-2",
121
+ runId: "run-2",
122
+ requestId: "req-3",
123
+ actor: "main_agent",
124
+ provider: "openai",
125
+ model: "gpt-4o",
126
+ inputTokens: 2000,
127
+ outputTokens: 400,
128
+ cacheCreationInputTokens: 0,
129
+ cacheReadInputTokens: 0,
130
+ },
131
+ { estimatedCostUsd: 0, pricingStatus: "unpriced" },
132
+ );
133
+ getSqlite().run(
134
+ "UPDATE llm_usage_events SET created_at = ? WHERE request_id = 'req-3'",
135
+ [day2],
136
+ );
137
+
138
+ return { day1, day2 };
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Tests
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe("usage routes", () => {
146
+ beforeEach(clearUsageEvents);
147
+
148
+ // -- query parsing / validation --
149
+
150
+ describe("query parameter validation", () => {
151
+ test("returns 400 when from/to are missing", async () => {
152
+ const res = await dispatch("GET", "usage/totals");
153
+ expect(res.status).toBe(400);
154
+ const body = (await res.json()) as { error: { message: string } };
155
+ expect(body.error.message).toContain("from");
156
+ });
157
+
158
+ test("returns 400 when from is missing", async () => {
159
+ const res = await dispatch("GET", "usage/totals?to=1000");
160
+ expect(res.status).toBe(400);
161
+ });
162
+
163
+ test("returns 400 when to is missing", async () => {
164
+ const res = await dispatch("GET", "usage/totals?from=1000");
165
+ expect(res.status).toBe(400);
166
+ });
167
+
168
+ test("returns 400 when from/to are not numbers", async () => {
169
+ const res = await dispatch("GET", "usage/totals?from=abc&to=def");
170
+ expect(res.status).toBe(400);
171
+ const body = (await res.json()) as { error: { message: string } };
172
+ expect(body.error.message).toContain("valid numbers");
173
+ });
174
+
175
+ test("returns 400 when from > to", async () => {
176
+ const res = await dispatch("GET", "usage/totals?from=2000&to=1000");
177
+ expect(res.status).toBe(400);
178
+ const body = (await res.json()) as { error: { message: string } };
179
+ expect(body.error.message).toContain("less than or equal");
180
+ });
181
+ });
182
+
183
+ // -- totals --
184
+
185
+ describe("GET /v1/usage/totals", () => {
186
+ test("returns zeros for empty range", async () => {
187
+ const res = await dispatch("GET", "usage/totals?from=0&to=999999999999");
188
+ expect(res.status).toBe(200);
189
+ const body = (await res.json()) as Record<string, number>;
190
+ expect(body.totalInputTokens).toBe(0);
191
+ expect(body.totalOutputTokens).toBe(0);
192
+ expect(body.totalEstimatedCostUsd).toBe(0);
193
+ expect(body.eventCount).toBe(0);
194
+ });
195
+
196
+ test("returns correct totals for seeded data", async () => {
197
+ const { day1, day2 } = seedEvents();
198
+ const from = day1 - 1000;
199
+ const to = day2 + 1000;
200
+
201
+ const res = await dispatch("GET", `usage/totals?from=${from}&to=${to}`);
202
+ expect(res.status).toBe(200);
203
+ const body = (await res.json()) as Record<string, number>;
204
+ expect(body.totalInputTokens).toBe(3500);
205
+ expect(body.totalOutputTokens).toBe(700);
206
+ expect(body.totalCacheCreationTokens).toBe(50);
207
+ expect(body.totalCacheReadTokens).toBe(100);
208
+ expect(body.eventCount).toBe(3);
209
+ expect(body.pricedEventCount).toBe(2);
210
+ expect(body.unpricedEventCount).toBe(1);
211
+ });
212
+
213
+ test("filters by time range", async () => {
214
+ const { day1 } = seedEvents();
215
+ // Only day 1 events
216
+ const from = day1 - 1000;
217
+ const to = day1 + 86400_000 - 1;
218
+
219
+ const res = await dispatch("GET", `usage/totals?from=${from}&to=${to}`);
220
+ const body = (await res.json()) as Record<string, number>;
221
+ expect(body.eventCount).toBe(2);
222
+ expect(body.totalInputTokens).toBe(1500);
223
+ });
224
+ });
225
+
226
+ // -- daily buckets --
227
+
228
+ describe("GET /v1/usage/daily", () => {
229
+ test("returns empty buckets array for empty range", async () => {
230
+ const res = await dispatch("GET", "usage/daily?from=0&to=999999999999");
231
+ expect(res.status).toBe(200);
232
+ const body = (await res.json()) as { buckets: unknown[] };
233
+ expect(body.buckets).toEqual([]);
234
+ });
235
+
236
+ test("returns daily buckets for seeded data", async () => {
237
+ const { day1, day2 } = seedEvents();
238
+ const from = day1 - 1000;
239
+ const to = day2 + 1000;
240
+
241
+ const res = await dispatch("GET", `usage/daily?from=${from}&to=${to}`);
242
+ expect(res.status).toBe(200);
243
+ const body = (await res.json()) as {
244
+ buckets: Array<{
245
+ date: string;
246
+ totalInputTokens: number;
247
+ eventCount: number;
248
+ }>;
249
+ };
250
+ expect(body.buckets).toHaveLength(2);
251
+ expect(body.buckets[0].date).toBe("2025-01-15");
252
+ expect(body.buckets[0].eventCount).toBe(2);
253
+ expect(body.buckets[1].date).toBe("2025-01-16");
254
+ expect(body.buckets[1].eventCount).toBe(1);
255
+ });
256
+ });
257
+
258
+ // -- breakdown --
259
+
260
+ describe("GET /v1/usage/breakdown", () => {
261
+ test("returns 400 when groupBy is missing", async () => {
262
+ const res = await dispatch(
263
+ "GET",
264
+ "usage/breakdown?from=0&to=999999999999",
265
+ );
266
+ expect(res.status).toBe(400);
267
+ const body = (await res.json()) as { error: { message: string } };
268
+ expect(body.error.message).toContain("groupBy");
269
+ });
270
+
271
+ test("returns 400 for invalid groupBy value", async () => {
272
+ const res = await dispatch(
273
+ "GET",
274
+ "usage/breakdown?from=0&to=999999999999&groupBy=invalid",
275
+ );
276
+ expect(res.status).toBe(400);
277
+ const body = (await res.json()) as { error: { message: string } };
278
+ expect(body.error.message).toContain("invalid");
279
+ });
280
+
281
+ test("groups by provider", async () => {
282
+ const { day1, day2 } = seedEvents();
283
+ const from = day1 - 1000;
284
+ const to = day2 + 1000;
285
+
286
+ const res = await dispatch(
287
+ "GET",
288
+ `usage/breakdown?from=${from}&to=${to}&groupBy=provider`,
289
+ );
290
+ expect(res.status).toBe(200);
291
+ const body = (await res.json()) as {
292
+ breakdown: Array<{
293
+ group: string;
294
+ totalInputTokens: number;
295
+ eventCount: number;
296
+ }>;
297
+ };
298
+ expect(body.breakdown).toHaveLength(2);
299
+ const groups = body.breakdown.map((b) => b.group).sort();
300
+ expect(groups).toEqual(["anthropic", "openai"]);
301
+ });
302
+
303
+ test("groups by actor", async () => {
304
+ const { day1, day2 } = seedEvents();
305
+ const from = day1 - 1000;
306
+ const to = day2 + 1000;
307
+
308
+ const res = await dispatch(
309
+ "GET",
310
+ `usage/breakdown?from=${from}&to=${to}&groupBy=actor`,
311
+ );
312
+ expect(res.status).toBe(200);
313
+ const body = (await res.json()) as {
314
+ breakdown: Array<{ group: string; eventCount: number }>;
315
+ };
316
+ expect(body.breakdown).toHaveLength(2);
317
+ const assistantGroup = body.breakdown.find(
318
+ (b) => b.group === "main_agent",
319
+ );
320
+ expect(assistantGroup?.eventCount).toBe(2);
321
+ });
322
+
323
+ test("groups by model", async () => {
324
+ const { day1, day2 } = seedEvents();
325
+ const from = day1 - 1000;
326
+ const to = day2 + 1000;
327
+
328
+ const res = await dispatch(
329
+ "GET",
330
+ `usage/breakdown?from=${from}&to=${to}&groupBy=model`,
331
+ );
332
+ expect(res.status).toBe(200);
333
+ const body = (await res.json()) as {
334
+ breakdown: Array<{ group: string; eventCount: number }>;
335
+ };
336
+ expect(body.breakdown).toHaveLength(3);
337
+ });
338
+ });
339
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tests for the WhatsApp channel invite adapter.
3
+ *
4
+ * WhatsApp uses Meta WhatsApp Business API, not Twilio. The display phone
5
+ * number is resolved from workspace config (`whatsapp.phoneNumber`), falling
6
+ * back to undefined (triggering generic instructions) when not configured.
7
+ */
8
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Mocks — must be set up before importing the adapter
12
+ // ---------------------------------------------------------------------------
13
+
14
+ let mockWhatsAppPhoneNumber: string | undefined;
15
+ let mockGetConfigThrows = false;
16
+
17
+ mock.module("../config/loader.js", () => ({
18
+ loadRawConfig: () => ({}),
19
+ getConfig: () => {
20
+ if (mockGetConfigThrows) throw new Error("config not found");
21
+ return { whatsapp: { phoneNumber: mockWhatsAppPhoneNumber ?? "" } };
22
+ },
23
+ invalidateConfigCache: () => {},
24
+ }));
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Import under test
28
+ // ---------------------------------------------------------------------------
29
+
30
+ import { whatsappInviteAdapter } from "../runtime/channel-invite-transports/whatsapp.js";
31
+ import { resolveWhatsAppDisplayNumber } from "../runtime/channel-invite-transports/whatsapp.js";
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Tests
35
+ // ---------------------------------------------------------------------------
36
+
37
+ describe("whatsapp invite adapter", () => {
38
+ beforeEach(() => {
39
+ mockWhatsAppPhoneNumber = undefined;
40
+ mockGetConfigThrows = false;
41
+ });
42
+
43
+ test("adapter is registered for the whatsapp channel", () => {
44
+ expect(whatsappInviteAdapter.channel).toBe("whatsapp");
45
+ });
46
+
47
+ // -------------------------------------------------------------------------
48
+ // Handle resolution — configured path
49
+ // -------------------------------------------------------------------------
50
+
51
+ test("returns configured phone number from workspace config", () => {
52
+ mockWhatsAppPhoneNumber = "+15551234567";
53
+ const handle = whatsappInviteAdapter.resolveChannelHandle!();
54
+ expect(handle).toBe("+15551234567");
55
+ });
56
+
57
+ test("resolveWhatsAppDisplayNumber returns configured number", () => {
58
+ mockWhatsAppPhoneNumber = "+15559876543";
59
+ expect(resolveWhatsAppDisplayNumber()).toBe("+15559876543");
60
+ });
61
+
62
+ // -------------------------------------------------------------------------
63
+ // Handle resolution — unconfigured fallback
64
+ // -------------------------------------------------------------------------
65
+
66
+ test("returns undefined when whatsapp config is missing", () => {
67
+ const handle = whatsappInviteAdapter.resolveChannelHandle!();
68
+ expect(handle).toBeUndefined();
69
+ });
70
+
71
+ test("returns undefined when phoneNumber is empty string", () => {
72
+ mockWhatsAppPhoneNumber = "";
73
+ const handle = whatsappInviteAdapter.resolveChannelHandle!();
74
+ expect(handle).toBeUndefined();
75
+ });
76
+
77
+ test("returns undefined when config loading throws", () => {
78
+ mockGetConfigThrows = true;
79
+ const handle = whatsappInviteAdapter.resolveChannelHandle!();
80
+ expect(handle).toBeUndefined();
81
+ });
82
+
83
+ // -------------------------------------------------------------------------
84
+ // Adapter shape
85
+ // -------------------------------------------------------------------------
86
+
87
+ test("does not implement buildShareLink", () => {
88
+ expect(whatsappInviteAdapter.buildShareLink).toBeUndefined();
89
+ });
90
+
91
+ test("does not implement extractInboundToken", () => {
92
+ expect(whatsappInviteAdapter.extractInboundToken).toBeUndefined();
93
+ });
94
+ });
package/src/agent/loop.ts CHANGED
@@ -115,6 +115,7 @@ export class AgentLoop {
115
115
  name: string,
116
116
  input: Record<string, unknown>,
117
117
  onOutput?: (chunk: string) => void,
118
+ toolUseId?: string,
118
119
  ) => Promise<{
119
120
  content: string;
120
121
  isError: boolean;
@@ -140,6 +141,7 @@ export class AgentLoop {
140
141
  name: string,
141
142
  input: Record<string, unknown>,
142
143
  onOutput?: (chunk: string) => void,
144
+ toolUseId?: string,
143
145
  ) => Promise<{
144
146
  content: string;
145
147
  isError: boolean;
@@ -507,6 +509,7 @@ export class AgentLoop {
507
509
  chunk,
508
510
  });
509
511
  },
512
+ toolUse.id,
510
513
  );
511
514
 
512
515
  const toolDurationMs = Date.now() - toolStart;
@@ -291,7 +291,6 @@ export async function getCheckoutSummary(): Promise<CheckoutSummary> {
291
291
  export async function placeOrder(
292
292
  opts: {
293
293
  paymentMethodId?: string;
294
- deliverySlotId?: string;
295
294
  } = {},
296
295
  ): Promise<PlaceOrderResult> {
297
296
  const { tabId } = await prepareRequest();
@@ -19,6 +19,10 @@ import {
19
19
  getCanonicalGuardianRequest,
20
20
  } from "../memory/canonical-guardian-store.js";
21
21
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
22
+ import {
23
+ isNotificationSourceChannel,
24
+ type NotificationSourceChannel,
25
+ } from "../notifications/signal.js";
22
26
  import { addRule } from "../permissions/trust-store.js";
23
27
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
24
28
  import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
@@ -345,7 +349,11 @@ const accessRequestResolver: GuardianRequestResolver = {
345
349
 
346
350
  async resolve(ctx: ResolverContext): Promise<ResolverResult> {
347
351
  const { request, decision, channelDeliveryContext } = ctx;
348
- const channel = request.sourceChannel ?? "unknown";
352
+ const channel: NotificationSourceChannel = isNotificationSourceChannel(
353
+ request.sourceChannel,
354
+ )
355
+ ? request.sourceChannel
356
+ : "vellum";
349
357
  const requesterExternalUserId = request.requesterExternalUserId ?? "";
350
358
  const requesterChatId =
351
359
  request.requesterChatId ?? request.requesterExternalUserId ?? "";
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Core packaging logic for .vellumapp zip archives.
2
+ * Core packaging logic for .vellum zip archives.
3
3
  *
4
4
  * Reads an app from the app-store, generates a manifest, and produces a
5
5
  * zip archive written to a temp file.
6
6
  */
7
7
 
8
- import { createHash, randomUUID } from "node:crypto";
9
- import { createWriteStream } from "node:fs";
8
+ import { createHash } from "node:crypto";
9
+ import { createWriteStream, existsSync, readFileSync } from "node:fs";
10
10
  import { readFile, stat, writeFile } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { extname, join } from "node:path";
@@ -14,7 +14,7 @@ import { extname, join } from "node:path";
14
14
  import archiver from "archiver";
15
15
  import JSZip from "jszip";
16
16
 
17
- import { getApp } from "../memory/app-store.js";
17
+ import { getApp, getAppsDir } from "../memory/app-store.js";
18
18
  import { computeContentId } from "../util/content-id.js";
19
19
  import { getLogger } from "../util/logger.js";
20
20
  import type { SigningCallback } from "./bundle-signer.js";
@@ -27,7 +27,6 @@ const bundlerLog = getLogger("app-bundler");
27
27
  import { APP_VERSION } from "../version.js";
28
28
  const PACKAGE_VERSION = APP_VERSION;
29
29
 
30
- const SHORT_HASH_LENGTH = 8;
31
30
  const HASH_DISPLAY_LENGTH = 12;
32
31
  const MAX_BUNDLE_SIZE_BYTES = 25 * 1024 * 1024; // 25 MB
33
32
  const ASSET_FETCH_TIMEOUT_MS = 10_000;
@@ -173,15 +172,17 @@ export async function materializeAssets(
173
172
  export interface BundleResult {
174
173
  bundlePath: string;
175
174
  manifest: AppManifest;
175
+ /** Base64-encoded PNG of the app icon, if one was generated. */
176
+ iconImageBase64?: string;
176
177
  }
177
178
 
178
179
  /**
179
- * Package an app into a .vellumapp zip archive.
180
+ * Package an app into a .vellum zip archive.
180
181
  *
181
182
  * @param appId - The ID of the app to package (from the app-store).
182
183
  * @param requestSignature - Optional callback to request an Ed25519 signature from the Swift client.
183
184
  * If provided, the bundle will be signed and include a signature.json.
184
- * @returns The path to the created .vellumapp file and the manifest.
185
+ * @returns The path to the created .vellum file and the manifest.
185
186
  * @throws If the app is not found, or the bundle exceeds the size limit.
186
187
  */
187
188
  export async function packageApp(
@@ -236,10 +237,12 @@ export async function packageApp(
236
237
  const allAssets = [...allAssetsMap.values()];
237
238
 
238
239
  // Create the zip archive
239
- const bundleFilename = `${app.name.replace(
240
- /[^a-zA-Z0-9_-]/g,
241
- "_",
242
- )}-${randomUUID().slice(0, SHORT_HASH_LENGTH)}.vellumapp`;
240
+ const safeName = app.name.replace(/[/\\:*?"<>|]/g, "_").trim() || "App";
241
+ const uniqueSuffix = createHash("sha256")
242
+ .update(`${appId}-${Date.now()}`)
243
+ .digest("hex")
244
+ .slice(0, 8);
245
+ const bundleFilename = `${safeName}-${uniqueSuffix}.vellum`;
243
246
  const bundlePath = join(tmpdir(), bundleFilename);
244
247
 
245
248
  await new Promise<void>((resolve, reject) => {
@@ -277,6 +280,12 @@ export async function packageApp(
277
280
  archive.append(asset.data, { name: asset.archivePath });
278
281
  }
279
282
 
283
+ // Include app icon if one was generated
284
+ const iconPath = join(getAppsDir(), appId, "icon.png");
285
+ if (existsSync(iconPath)) {
286
+ archive.append(readFileSync(iconPath), { name: "icon.png" });
287
+ }
288
+
280
289
  archive.finalize();
281
290
  });
282
291
 
@@ -318,5 +327,12 @@ export async function packageApp(
318
327
  );
319
328
  }
320
329
 
321
- return { bundlePath, manifest };
330
+ // Read icon for inclusion in the response
331
+ let iconImageBase64: string | undefined;
332
+ const iconFilePath = join(getAppsDir(), appId, "icon.png");
333
+ if (existsSync(iconFilePath)) {
334
+ iconImageBase64 = readFileSync(iconFilePath).toString("base64");
335
+ }
336
+
337
+ return { bundlePath, manifest, iconImageBase64 };
322
338
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * BundleScanner — Security validation and static analysis for .vellumapp bundles.
2
+ * BundleScanner — Security validation and static analysis for .vellum bundles.
3
3
  *
4
4
  * Validates zip bundles before they are opened, returning structured results
5
5
  * with block-level (reject) and warn-level (flag) findings.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Bundle signing for .vellumapp archives.
2
+ * Bundle signing for .vellum archives.
3
3
  *
4
4
  * Computes content hashes, constructs a canonical signing payload,
5
5
  * and requests an Ed25519 signature from the Swift client via IPC.
@@ -78,9 +78,9 @@ async function computeContentHashes(
78
78
  }
79
79
 
80
80
  /**
81
- * Sign a .vellumapp bundle.
81
+ * Sign a .vellum bundle.
82
82
  *
83
- * @param bundlePath - Path to the .vellumapp zip archive.
83
+ * @param bundlePath - Path to the .vellum zip archive.
84
84
  * @param requestSignature - Callback to request a signature from the Swift client.
85
85
  * @returns The SignatureJson to embed in the archive.
86
86
  */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types and serialization for .vellumapp manifest files.
2
+ * Types and serialization for .vellum manifest files.
3
3
  */
4
4
 
5
5
  export interface AppManifest {