@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
@@ -24,79 +24,55 @@ mock.module("../util/logger.js", () => ({
24
24
  }));
25
25
 
26
26
  // ---------------------------------------------------------------------------
27
- // Mock platform detection so tests can simulate non-macOS environments
28
- // where keychain backend selection is exercised.
27
+ // Broker client mock set up before importing secure-keys so the
28
+ // module-level `createBrokerClient()` call picks up our mock.
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
- let mockIsMacOS = false;
32
-
33
- mock.module("../util/platform.js", () => ({
34
- isMacOS: () => mockIsMacOS,
35
- isLinux: () => !mockIsMacOS,
36
- isWindows: () => false,
37
- getPlatformName: () => (mockIsMacOS ? "darwin" : "linux"),
38
- getClipboardCommand: () => null,
31
+ let mockBrokerAvailable = false;
32
+ let mockBrokerStore: Map<string, string> = new Map();
33
+ let mockBrokerGetError = false;
34
+ let mockBrokerSetError = false;
35
+ let mockBrokerDelError = false;
36
+
37
+ mock.module("../security/keychain-broker-client.js", () => ({
38
+ createBrokerClient: () => ({
39
+ isAvailable: () => mockBrokerAvailable,
40
+ ping: async () => (mockBrokerAvailable ? { pong: true } : null),
41
+ get: async (account: string) => {
42
+ // null = broker error (fall back to encrypted store)
43
+ if (mockBrokerGetError) return null;
44
+ const value = mockBrokerStore.get(account);
45
+ if (value !== undefined) return { found: true, value };
46
+ return { found: false };
47
+ },
48
+ set: async (account: string, value: string) => {
49
+ if (mockBrokerSetError) return false;
50
+ mockBrokerStore.set(account, value);
51
+ return true;
52
+ },
53
+ del: async (account: string) => {
54
+ if (mockBrokerDelError) return false;
55
+ const existed = mockBrokerStore.has(account);
56
+ mockBrokerStore.delete(account);
57
+ return existed;
58
+ },
59
+ list: async () => Array.from(mockBrokerStore.keys()),
60
+ }),
39
61
  }));
40
62
 
41
- // ---------------------------------------------------------------------------
42
- // Keychain simulation via _overrideDeps — avoids process-global mock.module
43
- // for keychain.js which leaks into keychain.test.ts.
44
- // ---------------------------------------------------------------------------
45
-
46
- import { _overrideDeps, _resetDeps } from "../security/keychain.js";
47
-
48
- let keychainAvailable = false;
49
- let keychainFailAtRuntime = false;
50
- const keychainStore = new Map<string, string>();
51
-
52
- function installKeychainDeps(): void {
53
- _overrideDeps({
54
- isMacOS: () => keychainAvailable,
55
- isLinux: () => false,
56
- execFileSync: ((cmd: string, args: string[]) => {
57
- if (keychainFailAtRuntime) throw new Error("Keychain runtime error");
58
-
59
- if (cmd === "security") {
60
- if (args.includes("list-keychains")) return "";
61
-
62
- if (args.includes("find-generic-password")) {
63
- const aIdx = args.indexOf("-a");
64
- const account = args[aIdx + 1];
65
- const val = keychainStore.get(account);
66
- if (!val) throw Object.assign(new Error("not found"), { status: 44 });
67
- return val + "\n";
68
- }
69
-
70
- if (args.includes("add-generic-password")) {
71
- const aIdx = args.indexOf("-a");
72
- const account = args[aIdx + 1];
73
- const wIdx = args.indexOf("-w");
74
- const value = args[wIdx + 1];
75
- keychainStore.set(account, value);
76
- return "";
77
- }
78
-
79
- if (args.includes("delete-generic-password")) {
80
- const aIdx = args.indexOf("-a");
81
- const account = args[aIdx + 1];
82
- keychainStore.delete(account);
83
- return "";
84
- }
85
- }
86
-
87
- return "";
88
- }) as typeof import("node:child_process").execFileSync,
89
- });
90
- }
91
-
92
63
  import { _setStorePath } from "../security/encrypted-store.js";
93
64
  import {
94
65
  _resetBackend,
95
66
  _setBackend,
96
67
  deleteSecureKey,
68
+ deleteSecureKeyAsync,
69
+ getBackendType,
97
70
  getSecureKey,
71
+ getSecureKeyAsync,
72
+ isDowngradedFromKeychain,
98
73
  listSecureKeys,
99
74
  setSecureKey,
75
+ setSecureKeyAsync,
100
76
  } from "../security/secure-keys.js";
101
77
 
102
78
  // ---------------------------------------------------------------------------
@@ -111,13 +87,14 @@ const STORE_PATH = join(TEST_DIR, "keys.enc");
111
87
 
112
88
  describe("secure-keys", () => {
113
89
  beforeEach(() => {
114
- // Clean state
115
- keychainAvailable = false;
116
- keychainFailAtRuntime = false;
117
- mockIsMacOS = false;
118
- keychainStore.clear();
119
90
  _resetBackend();
120
- installKeychainDeps();
91
+
92
+ // Reset broker mock state
93
+ mockBrokerAvailable = false;
94
+ mockBrokerStore = new Map();
95
+ mockBrokerGetError = false;
96
+ mockBrokerSetError = false;
97
+ mockBrokerDelError = false;
121
98
 
122
99
  if (existsSync(TEST_DIR)) {
123
100
  rmSync(TEST_DIR, { recursive: true });
@@ -132,7 +109,6 @@ describe("secure-keys", () => {
132
109
  });
133
110
 
134
111
  afterAll(() => {
135
- _resetDeps();
136
112
  if (existsSync(TEST_DIR)) {
137
113
  rmSync(TEST_DIR, { recursive: true });
138
114
  }
@@ -142,54 +118,24 @@ describe("secure-keys", () => {
142
118
  // Backend selection
143
119
  // -----------------------------------------------------------------------
144
120
  describe("backend selection", () => {
145
- test("uses encrypted store when keychain is unavailable", () => {
146
- keychainAvailable = false;
147
- _resetBackend();
148
- setSecureKey("anthropic", "sk-test-123");
149
- expect(getSecureKey("anthropic")).toBe("sk-test-123");
150
- // Should be in encrypted store, not keychain
151
- expect(keychainStore.has("anthropic")).toBe(false);
152
- expect(existsSync(STORE_PATH)).toBe(true);
121
+ test("returns encrypted when broker is unavailable", () => {
122
+ expect(getBackendType()).toBe("encrypted");
153
123
  });
154
124
 
155
- test("uses keychain when available", () => {
156
- keychainAvailable = true;
157
- _resetBackend();
158
- setSecureKey("anthropic", "sk-test-456");
159
- expect(getSecureKey("anthropic")).toBe("sk-test-456");
160
- // Should be in keychain, not encrypted store
161
- expect(keychainStore.get("anthropic")).toBe("sk-test-456");
162
- expect(existsSync(STORE_PATH)).toBe(false);
125
+ test("returns broker when broker is available", () => {
126
+ mockBrokerAvailable = true;
127
+ expect(getBackendType()).toBe("broker");
163
128
  });
164
129
 
165
- test("caches backend selection", () => {
166
- keychainAvailable = false;
167
- _resetBackend();
168
- setSecureKey("test", "val1");
169
-
170
- // Change availability — should still use encrypted store
171
- keychainAvailable = true;
172
- setSecureKey("test2", "val2");
173
- expect(keychainStore.has("test2")).toBe(false);
174
- expect(existsSync(STORE_PATH)).toBe(true);
175
- });
176
-
177
- test("uses encrypted store on macOS even when keychain is available", () => {
178
- mockIsMacOS = true;
179
- keychainAvailable = true;
180
- _resetBackend();
181
- setSecureKey("anthropic", "sk-mac-test");
182
- expect(getSecureKey("anthropic")).toBe("sk-mac-test");
183
- // Should be in encrypted store, not keychain
184
- expect(keychainStore.has("anthropic")).toBe(false);
185
- expect(existsSync(STORE_PATH)).toBe(true);
130
+ test("isDowngradedFromKeychain always returns false", () => {
131
+ expect(isDowngradedFromKeychain()).toBe(false);
186
132
  });
187
133
  });
188
134
 
189
135
  // -----------------------------------------------------------------------
190
- // CRUD operations (via encrypted store backend)
136
+ // CRUD operations (via encrypted store backend — sync)
191
137
  // -----------------------------------------------------------------------
192
- describe("CRUD with encrypted backend", () => {
138
+ describe("CRUD with encrypted backend (sync)", () => {
193
139
  test("set and get a key", () => {
194
140
  setSecureKey("openai", "sk-openai-789");
195
141
  expect(getSecureKey("openai")).toBe("sk-openai-789");
@@ -201,12 +147,12 @@ describe("secure-keys", () => {
201
147
 
202
148
  test("delete removes a key", () => {
203
149
  setSecureKey("gemini", "gem-key");
204
- expect(deleteSecureKey("gemini")).toBe(true);
150
+ expect(deleteSecureKey("gemini")).toBe("deleted");
205
151
  expect(getSecureKey("gemini")).toBeUndefined();
206
152
  });
207
153
 
208
- test("delete returns false for nonexistent key", () => {
209
- expect(deleteSecureKey("missing")).toBe(false);
154
+ test("delete returns not-found for nonexistent key", () => {
155
+ expect(deleteSecureKey("missing")).toBe("not-found");
210
156
  });
211
157
 
212
158
  test("listSecureKeys returns all keys", () => {
@@ -220,143 +166,156 @@ describe("secure-keys", () => {
220
166
  });
221
167
 
222
168
  // -----------------------------------------------------------------------
223
- // CRUD operations (via keychain backend)
169
+ // Sync variants always use encrypted store even when broker is available
224
170
  // -----------------------------------------------------------------------
225
- describe("CRUD with keychain backend", () => {
226
- beforeEach(() => {
227
- keychainAvailable = true;
228
- _resetBackend();
171
+ describe("sync variants ignore broker", () => {
172
+ test("getSecureKey uses encrypted store even when broker is available", () => {
173
+ mockBrokerAvailable = true;
174
+ mockBrokerStore.set("api-key", "broker-value");
175
+ // Sync getter should not see broker-only keys
176
+ expect(getSecureKey("api-key")).toBeUndefined();
177
+ // But encrypted store keys should work
178
+ setSecureKey("api-key", "encrypted-value");
179
+ expect(getSecureKey("api-key")).toBe("encrypted-value");
229
180
  });
230
181
 
231
- test("set and get a key", () => {
232
- setSecureKey("anthropic", "sk-ant-123");
233
- expect(getSecureKey("anthropic")).toBe("sk-ant-123");
182
+ test("setSecureKey uses encrypted store even when broker is available", () => {
183
+ mockBrokerAvailable = true;
184
+ setSecureKey("api-key", "encrypted-value");
185
+ expect(getSecureKey("api-key")).toBe("encrypted-value");
186
+ // Should not have written to broker
187
+ expect(mockBrokerStore.has("api-key")).toBe(false);
234
188
  });
235
189
 
236
- test("delete removes a key", () => {
237
- setSecureKey("anthropic", "sk-ant-123");
238
- deleteSecureKey("anthropic");
239
- expect(getSecureKey("anthropic")).toBeUndefined();
240
- });
241
-
242
- test("listSecureKeys returns empty for keychain backend", () => {
243
- setSecureKey("anthropic", "val");
244
- // Keychain doesn't support listing
245
- expect(listSecureKeys()).toEqual([]);
190
+ test("deleteSecureKey uses encrypted store even when broker is available", () => {
191
+ mockBrokerAvailable = true;
192
+ mockBrokerStore.set("api-key", "broker-value");
193
+ setSecureKey("api-key", "encrypted-value");
194
+ deleteSecureKey("api-key");
195
+ expect(getSecureKey("api-key")).toBeUndefined();
196
+ // Broker value should be untouched
197
+ expect(mockBrokerStore.has("api-key")).toBe(true);
246
198
  });
247
199
  });
248
200
 
249
201
  // -----------------------------------------------------------------------
250
- // _setBackend
202
+ // Async variants — broker available path
251
203
  // -----------------------------------------------------------------------
252
- describe("_setBackend", () => {
253
- test("forces encrypted backend", () => {
254
- _setBackend("encrypted");
255
- setSecureKey("test", "value");
256
- expect(existsSync(STORE_PATH)).toBe(true);
257
- expect(keychainStore.has("test")).toBe(false);
204
+ describe("async variants with broker available", () => {
205
+ test("getSecureKeyAsync returns broker value when available", async () => {
206
+ mockBrokerAvailable = true;
207
+ mockBrokerStore.set("api-key", "broker-value");
208
+ setSecureKey("api-key", "encrypted-value");
209
+ expect(await getSecureKeyAsync("api-key")).toBe("broker-value");
258
210
  });
259
211
 
260
- test("forces keychain backend", () => {
261
- keychainAvailable = true;
262
- _setBackend("keychain");
263
- setSecureKey("test", "value");
264
- expect(keychainStore.get("test")).toBe("value");
212
+ test("getSecureKeyAsync falls back to encrypted store when broker reports not-found", async () => {
213
+ mockBrokerAvailable = true;
214
+ // Broker has nothing for this key — returns { found: false }.
215
+ // Keys may exist only in the encrypted store (written while broker
216
+ // was unavailable or via sync setSecureKey), so we must fall back.
217
+ setSecureKey("api-key", "encrypted-value");
218
+ expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
265
219
  });
266
220
 
267
- test("reset re-evaluates backend", () => {
268
- keychainAvailable = true;
269
- _setBackend("keychain");
270
- setSecureKey("k1", "v1");
271
- expect(keychainStore.get("k1")).toBe("v1");
272
-
273
- _setBackend(undefined); // reset
274
- keychainAvailable = false;
275
- setSecureKey("k2", "v2");
276
- expect(keychainStore.has("k2")).toBe(false);
277
- expect(getSecureKey("k2")).toBe("v2");
221
+ test("getSecureKeyAsync returns undefined when neither broker nor encrypted store has key", async () => {
222
+ mockBrokerAvailable = true;
223
+ // Neither store has the key — should return undefined
224
+ expect(await getSecureKeyAsync("missing-key")).toBeUndefined();
278
225
  });
279
- });
280
226
 
281
- // -----------------------------------------------------------------------
282
- // Keychain runtime failure fallback
283
- // -----------------------------------------------------------------------
284
- describe("keychain runtime fallback", () => {
285
- beforeEach(() => {
286
- keychainAvailable = true;
287
- _resetBackend();
227
+ test("getSecureKeyAsync falls back to encrypted store on broker error", async () => {
228
+ mockBrokerAvailable = true;
229
+ mockBrokerGetError = true;
230
+ setSecureKey("api-key", "encrypted-value");
231
+ expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
288
232
  });
289
233
 
290
- test("setSecureKey falls back to encrypted store when keychain fails at runtime", () => {
291
- keychainFailAtRuntime = true;
292
- const result = setSecureKey("anthropic", "sk-test-fallback");
234
+ test("setSecureKeyAsync writes to broker and encrypted store", async () => {
235
+ mockBrokerAvailable = true;
236
+ const result = await setSecureKeyAsync("api-key", "new-value");
293
237
  expect(result).toBe(true);
294
- // Should have stored in encrypted store
295
- expect(keychainStore.has("anthropic")).toBe(false);
296
- expect(existsSync(STORE_PATH)).toBe(true);
297
- // Subsequent gets should also use encrypted store now
298
- expect(getSecureKey("anthropic")).toBe("sk-test-fallback");
238
+ expect(mockBrokerStore.get("api-key")).toBe("new-value");
239
+ // Also persisted to encrypted store for sync callers
240
+ expect(getSecureKey("api-key")).toBe("new-value");
299
241
  });
300
242
 
301
- test("deleteSecureKey for nonexistent key does not downgrade backend", () => {
302
- // Deleting a key that doesn't exist should NOT trigger a downgrade
303
- const result = deleteSecureKey("nonexistent");
243
+ test("setSecureKeyAsync returns false on broker set error (no silent fallback)", async () => {
244
+ mockBrokerAvailable = true;
245
+ mockBrokerSetError = true;
246
+ const result = await setSecureKeyAsync("api-key", "new-value");
247
+ // Must return false — falling through to encrypted-only write would
248
+ // leave the broker with stale data that async readers still see.
304
249
  expect(result).toBe(false);
305
- // Backend should still be keychain — verify by storing a new key
306
- setSecureKey("anthropic", "sk-still-keychain");
307
- expect(keychainStore.get("anthropic")).toBe("sk-still-keychain");
308
- expect(existsSync(STORE_PATH)).toBe(false);
250
+ expect(mockBrokerStore.has("api-key")).toBe(false);
251
+ // Encrypted store should NOT have been written either.
252
+ expect(getSecureKey("api-key")).toBeUndefined();
309
253
  });
310
254
 
311
- test("deleteSecureKey falls back to encrypted store when keychain fails at runtime", () => {
312
- // First store successfully in encrypted store via fallback
313
- keychainFailAtRuntime = true;
314
- setSecureKey("openai", "sk-openai-test");
315
- // Delete should also use encrypted store
316
- const result = deleteSecureKey("openai");
317
- expect(result).toBe(true);
318
- expect(getSecureKey("openai")).toBeUndefined();
255
+ test("deleteSecureKeyAsync deletes from broker and encrypted store", async () => {
256
+ mockBrokerAvailable = true;
257
+ mockBrokerStore.set("api-key", "broker-value");
258
+ setSecureKey("api-key", "encrypted-value");
259
+ const result = await deleteSecureKeyAsync("api-key");
260
+ expect(result).toBe("deleted");
261
+ expect(mockBrokerStore.has("api-key")).toBe(false);
262
+ expect(getSecureKey("api-key")).toBeUndefined();
319
263
  });
320
264
 
321
- test("deleteSecureKey triggers downgrade when key exists in keychain but keychain starts failing", () => {
322
- // Step 1: Store a key while keychain is working
323
- setSecureKey("anthropic", "sk-ant-exists");
324
- expect(keychainStore.get("anthropic")).toBe("sk-ant-exists");
265
+ test("deleteSecureKeyAsync returns error on broker del error (no silent fallback)", async () => {
266
+ mockBrokerAvailable = true;
267
+ mockBrokerDelError = true;
268
+ setSecureKey("api-key", "encrypted-value");
269
+ const result = await deleteSecureKeyAsync("api-key");
270
+ // Must return "error" — falling through to encrypted-only delete would
271
+ // leave the broker with the key, and async readers would still see it.
272
+ expect(result).toBe("error");
273
+ // Encrypted store should NOT have been modified either.
274
+ expect(getSecureKey("api-key")).toBe("encrypted-value");
275
+ });
276
+ });
325
277
 
326
- // Step 2: Keychain starts failing at runtime
327
- keychainFailAtRuntime = true;
278
+ // -----------------------------------------------------------------------
279
+ // Async variants — broker unavailable path
280
+ // -----------------------------------------------------------------------
281
+ describe("async variants with broker unavailable", () => {
282
+ test("getSecureKeyAsync uses encrypted store", async () => {
283
+ setSecureKey("api-key", "encrypted-value");
284
+ expect(await getSecureKeyAsync("api-key")).toBe("encrypted-value");
285
+ });
328
286
 
329
- // Step 3: Attempt to delete should trigger fallback/downgrade
330
- const result = deleteSecureKey("anthropic");
331
- // deleteKey returns false because keychain is failing, and fallback
332
- // encrypted store doesn't have the key, so the overall result is false.
333
- // But the important thing is that the backend was downgraded.
334
- expect(result).toBe(false);
287
+ test("getSecureKeyAsync returns undefined for missing key", async () => {
288
+ expect(await getSecureKeyAsync("missing")).toBeUndefined();
289
+ });
335
290
 
336
- // Step 4: Verify the backend has been downgraded to encrypted store.
337
- // Subsequent operations should use encrypted store.
338
- keychainFailAtRuntime = false;
339
- setSecureKey("openai", "sk-openai-new");
340
- // Should be in encrypted store, not keychain
341
- expect(keychainStore.has("openai")).toBe(false);
342
- expect(getSecureKey("openai")).toBe("sk-openai-new");
291
+ test("setSecureKeyAsync uses encrypted store", async () => {
292
+ const result = await setSecureKeyAsync("api-key", "new-value");
293
+ expect(result).toBe(true);
294
+ expect(getSecureKey("api-key")).toBe("new-value");
343
295
  });
344
296
 
345
- test("backend permanently downgrades after keychain runtime failure", () => {
346
- // Start with working keychain
347
- setSecureKey("anthropic", "key1");
348
- expect(keychainStore.get("anthropic")).toBe("key1");
349
-
350
- // Keychain starts failing
351
- keychainFailAtRuntime = true;
352
- setSecureKey("openai", "key2");
353
-
354
- // Backend should now be encrypted even if keychain "recovers"
355
- keychainFailAtRuntime = false;
356
- setSecureKey("gemini", "key3");
357
- // gemini should be in encrypted store, not keychain
358
- expect(keychainStore.has("gemini")).toBe(false);
359
- expect(getSecureKey("gemini")).toBe("key3");
297
+ test("deleteSecureKeyAsync uses encrypted store", async () => {
298
+ setSecureKey("api-key", "value");
299
+ const result = await deleteSecureKeyAsync("api-key");
300
+ expect(result).toBe("deleted");
301
+ expect(getSecureKey("api-key")).toBeUndefined();
302
+ });
303
+ });
304
+
305
+ // -----------------------------------------------------------------------
306
+ // _setBackend / _resetBackend (no-ops kept for test compat)
307
+ // -----------------------------------------------------------------------
308
+ describe("_setBackend", () => {
309
+ test("_setBackend is a no-op but does not throw", () => {
310
+ _setBackend("encrypted");
311
+ setSecureKey("test", "value");
312
+ expect(existsSync(STORE_PATH)).toBe(true);
313
+ });
314
+
315
+ test("_resetBackend is a no-op but does not throw", () => {
316
+ _resetBackend();
317
+ setSecureKey("test", "value");
318
+ expect(getSecureKey("test")).toBe("value");
360
319
  });
361
320
  });
362
321
  });
@@ -85,7 +85,7 @@ mock.module("../config/loader.js", () => ({
85
85
  apiKeys: {},
86
86
  skills: { entries: {}, allowBundled: true },
87
87
  memory: { retrieval: { injectionStrategy: "inline" } },
88
- permissions: { mode: "legacy" },
88
+ permissions: { mode: "workspace" },
89
89
  }),
90
90
  loadRawConfig: () => ({}),
91
91
  saveRawConfig: () => {},
@@ -26,7 +26,7 @@ const metadataByKey = new Map<
26
26
  mock.module("../security/secure-keys.js", () => ({
27
27
  getSecureKey: () => undefined,
28
28
  setSecureKey: setSecureKeyMock,
29
- deleteSecureKey: () => true,
29
+ deleteSecureKey: () => "deleted",
30
30
  listSecureKeys: () => [],
31
31
  getBackendType: () => null,
32
32
  isDowngradedFromKeychain: () => false,
@@ -76,7 +76,8 @@ mock.module("../config/loader.js", () => ({
76
76
  apiKeys: {},
77
77
  skills: { entries: {}, allowBundled: true },
78
78
  memory: { retrieval: { injectionStrategy: "inline" } },
79
- permissions: { mode: "legacy" },
79
+ permissions: { mode: "workspace" },
80
+ sandbox: { enabled: false },
80
81
  daemon: {
81
82
  startupSocketWaitMs: 5000,
82
83
  stopTimeoutMs: 5000,
@@ -520,7 +520,7 @@ describe("session-tool-setup app refresh side effects", () => {
520
520
  // ── app_create side effects ─────────────────────────────────────────
521
521
 
522
522
  describe("app_create side effects", () => {
523
- test("broadcasts app_files_changed after app_create", async () => {
523
+ test("broadcasts app_files_changed immediately after app_create", async () => {
524
524
  const ctx = makeCtx();
525
525
  const executor = makeFakeExecutor({
526
526
  content: JSON.stringify({ id: "new-app-1", name: "My App" }),
@@ -539,7 +539,7 @@ describe("session-tool-setup app refresh side effects", () => {
539
539
 
540
540
  await toolFn("app_create", { name: "My App", html: "<h1>hi</h1>" });
541
541
 
542
- expect(broadcastSpy).toHaveBeenCalledTimes(1);
542
+ expect(broadcastSpy.mock.calls.length).toBeGreaterThanOrEqual(1);
543
543
  expect((broadcastSpy.mock.calls as unknown[][])[0][0]).toEqual({
544
544
  type: "app_files_changed",
545
545
  appId: "new-app-1",
@@ -30,7 +30,10 @@ let currentConfig: Record<string, unknown> = {
30
30
  const DECLARED_SKILL_ID = "hatch-new-assistant";
31
31
  const DECLARED_FLAG_KEY = "feature_flags.hatch-new-assistant.enabled";
32
32
 
33
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
34
+ const realPlatform = require("../util/platform.js");
33
35
  mock.module("../util/platform.js", () => ({
36
+ ...realPlatform,
34
37
  getRootDir: () => TEST_DIR,
35
38
  getDataDir: () => TEST_DIR,
36
39
  getWorkspaceDir: () => TEST_DIR,
@@ -54,31 +57,53 @@ mock.module("../util/platform.js", () => ({
54
57
  isWindows: () => false,
55
58
  getPlatformName: () => "linux",
56
59
  getClipboardCommand: () => null,
60
+ readSessionToken: () => null,
57
61
  removeSocketFile: () => {},
58
62
  migratePath: () => {},
59
63
  migrateToWorkspaceLayout: () => {},
60
64
  migrateToDataLayout: () => {},
61
65
  }));
62
66
 
67
+ const noopLogger = new Proxy({} as Record<string, unknown>, {
68
+ get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
69
+ });
70
+
71
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
72
+ const realLogger = require("../util/logger.js");
63
73
  mock.module("../util/logger.js", () => ({
64
- getLogger: () =>
65
- new Proxy({} as Record<string, unknown>, {
66
- get: () => () => {},
67
- }),
74
+ ...realLogger,
75
+ getLogger: () => noopLogger,
76
+ getCliLogger: () => noopLogger,
68
77
  isDebug: () => false,
69
78
  truncateForLog: (v: string) => v,
79
+ initLogger: () => {},
80
+ pruneOldLogFiles: () => 0,
70
81
  }));
71
82
 
72
83
  mock.module("../config/loader.js", () => ({
73
84
  getConfig: () => currentConfig,
85
+ loadConfig: () => currentConfig,
86
+ loadRawConfig: () => ({}),
87
+ saveConfig: () => {},
88
+ saveRawConfig: () => {},
89
+ invalidateConfigCache: () => {},
90
+ getNestedValue: () => undefined,
91
+ setNestedValue: () => {},
92
+ syncConfigToLockfile: () => {},
74
93
  }));
75
94
 
95
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
96
+ const realUserReference = require("../config/user-reference.js");
76
97
  mock.module("../config/user-reference.js", () => ({
98
+ ...realUserReference,
77
99
  resolveUserReference: () => "TestUser",
78
100
  resolveUserPronouns: () => null,
79
101
  }));
80
102
 
103
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
104
+ const realCredentialMetadataStore = require("../tools/credentials/metadata-store.js");
81
105
  mock.module("../tools/credentials/metadata-store.js", () => ({
106
+ ...realCredentialMetadataStore,
82
107
  listCredentialMetadata: () => [],
83
108
  }));
84
109