@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
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Shared Chrome CDP session management.
3
+ *
4
+ * Consolidates the duplicated launch / readiness / window-management logic
5
+ * that was previously copy-pasted across the Amazon and DoorDash CLIs.
6
+ * Callers get back a {@link CdpSession} with structured metadata so they can
7
+ * make cleanup decisions (e.g. only kill Chrome if *we* launched it).
8
+ */
9
+
10
+ import { spawn as spawnChild } from "node:child_process";
11
+ import { homedir } from "node:os";
12
+ import { join as pathJoin } from "node:path";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const DEFAULT_CDP_PORT = 9222;
19
+ const DEFAULT_CDP_BASE = `http://localhost:${DEFAULT_CDP_PORT}`;
20
+ const DEFAULT_USER_DATA_DIR = pathJoin(
21
+ homedir(),
22
+ "Library/Application Support/Google/Chrome-CDP",
23
+ );
24
+ const CHROME_APP_PATH =
25
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export interface CdpSession {
32
+ /** Base URL for the CDP HTTP endpoints (e.g. `http://localhost:9222`). */
33
+ baseUrl: string;
34
+ /** Whether this helper launched Chrome (true) or it was already running (false). */
35
+ launchedByUs: boolean;
36
+ /** The `--user-data-dir` used for the Chrome instance. */
37
+ userDataDir: string;
38
+ }
39
+
40
+ export interface EnsureChromeOptions {
41
+ /** CDP port. Defaults to `9222`. */
42
+ port?: number;
43
+ /** User data directory for Chrome. Defaults to `~/Library/Application Support/Google/Chrome-CDP`. */
44
+ userDataDir?: string;
45
+ /** Initial URL to open when launching Chrome. */
46
+ startUrl?: string;
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Readiness check
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Returns `true` when a CDP endpoint is responding at the given base URL.
55
+ */
56
+ export async function isCdpReady(
57
+ cdpBase: string = DEFAULT_CDP_BASE,
58
+ ): Promise<boolean> {
59
+ try {
60
+ const res = await fetch(`${cdpBase}/json/version`);
61
+ return res.ok;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Launch / ensure
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Ensure a Chrome instance with CDP is available. If one is already listening
73
+ * on the target port, returns immediately. Otherwise spawns a new detached
74
+ * Chrome process and waits for the CDP endpoint to become ready.
75
+ *
76
+ * Returns a {@link CdpSession} with metadata about the running instance.
77
+ */
78
+ export async function ensureChromeWithCdp(
79
+ options: EnsureChromeOptions = {},
80
+ ): Promise<CdpSession> {
81
+ const port = options.port ?? DEFAULT_CDP_PORT;
82
+ const baseUrl = `http://localhost:${port}`;
83
+ const userDataDir = options.userDataDir ?? DEFAULT_USER_DATA_DIR;
84
+
85
+ if (await isCdpReady(baseUrl)) {
86
+ return { baseUrl, launchedByUs: false, userDataDir };
87
+ }
88
+
89
+ const args = [
90
+ `--remote-debugging-port=${port}`,
91
+ `--force-renderer-accessibility`,
92
+ `--user-data-dir=${userDataDir}`,
93
+ ];
94
+ if (options.startUrl) {
95
+ args.push(options.startUrl);
96
+ }
97
+
98
+ spawnChild(CHROME_APP_PATH, args, {
99
+ detached: true,
100
+ stdio: "ignore",
101
+ }).unref();
102
+
103
+ // Poll until CDP responds (up to 15 s)
104
+ for (let i = 0; i < 30; i++) {
105
+ await new Promise((r) => setTimeout(r, 500));
106
+ if (await isCdpReady(baseUrl)) {
107
+ return { baseUrl, launchedByUs: true, userDataDir };
108
+ }
109
+ }
110
+
111
+ throw new Error("Chrome started but CDP endpoint not responding after 15s");
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Window management helpers
116
+ // ---------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Look up the first page target and return its WebSocket debugger URL.
120
+ */
121
+ async function findPageTarget(cdpBase: string): Promise<string | null> {
122
+ const res = await fetch(`${cdpBase}/json/list`);
123
+ const targets = (await res.json()) as Array<{
124
+ type: string;
125
+ webSocketDebuggerUrl: string;
126
+ }>;
127
+ const page = targets.find((t) => t.type === "page");
128
+ return page?.webSocketDebuggerUrl ?? null;
129
+ }
130
+
131
+ /**
132
+ * Set the window state of the Chrome window owning the first page target.
133
+ * Used by both minimize and restore.
134
+ */
135
+ async function setWindowState(
136
+ cdpBase: string,
137
+ windowState: "minimized" | "normal",
138
+ ): Promise<void> {
139
+ const wsUrl = await findPageTarget(cdpBase);
140
+ if (!wsUrl) return;
141
+
142
+ const ws = new WebSocket(wsUrl);
143
+
144
+ await new Promise<void>((resolve, reject) => {
145
+ const timeout = setTimeout(() => {
146
+ ws.close();
147
+ reject(new Error(`CDP ${windowState} timed out`));
148
+ }, 5000);
149
+
150
+ ws.addEventListener("open", () => {
151
+ ws.send(JSON.stringify({ id: 1, method: "Browser.getWindowForTarget" }));
152
+ });
153
+
154
+ ws.addEventListener("message", (event) => {
155
+ const msg = JSON.parse(String(event.data)) as {
156
+ id: number;
157
+ result?: { windowId: number };
158
+ error?: { message: string };
159
+ };
160
+ if (msg.id === 1 && msg.result) {
161
+ ws.send(
162
+ JSON.stringify({
163
+ id: 2,
164
+ method: "Browser.setWindowBounds",
165
+ params: {
166
+ windowId: msg.result.windowId,
167
+ bounds: { windowState },
168
+ },
169
+ }),
170
+ );
171
+ } else if (msg.id === 1) {
172
+ clearTimeout(timeout);
173
+ ws.close();
174
+ reject(new Error("Browser.getWindowForTarget failed"));
175
+ } else if (msg.id === 2) {
176
+ clearTimeout(timeout);
177
+ ws.close();
178
+ if (msg.error) {
179
+ reject(
180
+ new Error(`Browser.setWindowBounds failed: ${msg.error.message}`),
181
+ );
182
+ } else {
183
+ resolve();
184
+ }
185
+ }
186
+ });
187
+
188
+ ws.addEventListener("error", (err) => {
189
+ clearTimeout(timeout);
190
+ reject(err);
191
+ });
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Minimize the Chrome window associated with the CDP session.
197
+ */
198
+ export async function minimizeChromeWindow(
199
+ cdpBase: string = DEFAULT_CDP_BASE,
200
+ ): Promise<void> {
201
+ await setWindowState(cdpBase, "minimized");
202
+ }
203
+
204
+ /**
205
+ * Restore (un-minimize) the Chrome window associated with the CDP session.
206
+ */
207
+ export async function restoreChromeWindow(
208
+ cdpBase: string = DEFAULT_CDP_BASE,
209
+ ): Promise<void> {
210
+ await setWindowState(cdpBase, "normal");
211
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ it,
8
+ mock,
9
+ } from "bun:test";
10
+
11
+ // Mock the logger to avoid side effects during tests
12
+ mock.module("../../util/logger.js", () => ({
13
+ getLogger: () => ({
14
+ info: () => {},
15
+ debug: () => {},
16
+ warn: () => {},
17
+ error: () => {},
18
+ }),
19
+ }));
20
+
21
+ const { NetworkRecorder } = await import("./network-recorder.js");
22
+
23
+ describe("NetworkRecorder", () => {
24
+ describe("startDirect CDP URL passthrough", () => {
25
+ const originalFetch = globalThis.fetch;
26
+ let fetchCalls: string[];
27
+
28
+ beforeEach(() => {
29
+ fetchCalls = [];
30
+ // Mock fetch to capture the URL and return a valid CDP version response.
31
+ globalThis.fetch = (async (url: string | URL | Request) => {
32
+ fetchCalls.push(String(url));
33
+ return new Response(
34
+ JSON.stringify({
35
+ webSocketDebuggerUrl: "ws://localhost:1234/devtools/browser/fake",
36
+ }),
37
+ { status: 200 },
38
+ );
39
+ }) as unknown as typeof fetch;
40
+ });
41
+
42
+ afterEach(() => {
43
+ globalThis.fetch = originalFetch;
44
+ });
45
+
46
+ // Safety net: restore fetch even if afterEach is skipped due to a test error
47
+ afterAll(() => {
48
+ globalThis.fetch = originalFetch;
49
+ });
50
+
51
+ it("uses constructor-provided cdpBaseUrl when called without arguments", async () => {
52
+ const customBase = "http://custom-host:9333";
53
+ const recorder = new NetworkRecorder(undefined, customBase);
54
+
55
+ // startDirect will fail at the WebSocket connect step, but we only
56
+ // care that fetch was called with the correct URL.
57
+ try {
58
+ await recorder.startDirect();
59
+ } catch {
60
+ // Expected — WebSocket connection will fail in test environment
61
+ }
62
+
63
+ expect(fetchCalls.length).toBeGreaterThanOrEqual(1);
64
+ expect(fetchCalls[0]).toBe(`${customBase}/json/version`);
65
+ expect(fetchCalls[0]).not.toContain("undefined");
66
+ });
67
+
68
+ it("uses explicit cdpBaseUrl argument when provided", async () => {
69
+ const constructorBase = "http://constructor-host:9222";
70
+ const explicitBase = "http://explicit-host:9444";
71
+ const recorder = new NetworkRecorder(undefined, constructorBase);
72
+
73
+ try {
74
+ await recorder.startDirect(explicitBase);
75
+ } catch {
76
+ // Expected — WebSocket connection will fail in test environment
77
+ }
78
+
79
+ expect(fetchCalls.length).toBeGreaterThanOrEqual(1);
80
+ expect(fetchCalls[0]).toBe(`${explicitBase}/json/version`);
81
+ });
82
+ });
83
+ });
@@ -18,8 +18,8 @@ const log = getLogger("network-recorder");
18
18
  /** Max response body size to capture (64 KB). */
19
19
  const MAX_BODY_SIZE = 64 * 1024;
20
20
 
21
- /** CDP endpoint to discover targets. */
22
- const CDP_BASE = "http://localhost:9222";
21
+ /** Default CDP endpoint used when no base URL is injected. */
22
+ const DEFAULT_CDP_BASE = "http://localhost:9222";
23
23
 
24
24
  /**
25
25
  * Minimal CDP client over WebSocket — talks the Chrome DevTools Protocol directly
@@ -115,7 +115,7 @@ export class NetworkRecorder {
115
115
  private entries = new Map<string, NetworkRecordedEntry>();
116
116
  private targetDomain?: string;
117
117
  private running = false;
118
- private cdpBaseUrl = CDP_BASE;
118
+ private cdpBaseUrl = DEFAULT_CDP_BASE;
119
119
  private attachedTargetIds = new Set<string>();
120
120
  private targetPollTimer?: ReturnType<typeof setInterval>;
121
121
 
@@ -130,20 +130,21 @@ export class NetworkRecorder {
130
130
  return this.entries.size;
131
131
  }
132
132
 
133
- constructor(targetDomain?: string) {
133
+ constructor(targetDomain?: string, cdpBaseUrl?: string) {
134
134
  this.targetDomain = targetDomain;
135
+ if (cdpBaseUrl) this.cdpBaseUrl = cdpBaseUrl;
135
136
  }
136
137
 
137
138
  /**
138
139
  * Connect directly to Chrome's CDP endpoint and start recording network events.
139
140
  * Attaches to the browser-level target so events from all tabs are captured.
140
141
  */
141
- async startDirect(cdpBaseUrl: string = CDP_BASE): Promise<void> {
142
+ async startDirect(cdpBaseUrl?: string): Promise<void> {
142
143
  if (this.running) return;
143
- this.cdpBaseUrl = cdpBaseUrl;
144
+ if (cdpBaseUrl) this.cdpBaseUrl = cdpBaseUrl;
144
145
 
145
146
  // Discover the browser's WebSocket debugger URL
146
- const versionRes = await fetch(`${cdpBaseUrl}/json/version`);
147
+ const versionRes = await fetch(`${this.cdpBaseUrl}/json/version`);
147
148
  const version = (await versionRes.json()) as {
148
149
  webSocketDebuggerUrl: string;
149
150
  };
@@ -9,7 +9,7 @@ import { getLogger } from "../../util/logger.js";
9
9
 
10
10
  const log = getLogger("x-auto-navigate");
11
11
 
12
- const CDP_BASE = "http://localhost:9222";
12
+ const DEFAULT_CDP_BASE = "http://localhost:9222";
13
13
 
14
14
  interface NavStep {
15
15
  label: string;
@@ -71,19 +71,25 @@ class MiniCDP {
71
71
  }
72
72
  }
73
73
 
74
+ export interface NavigateXPagesOptions {
75
+ abortSignal?: { aborted: boolean };
76
+ cdpBaseUrl?: string;
77
+ }
78
+
74
79
  /**
75
80
  * Navigate Chrome through X.com pages to trigger GraphQL calls.
76
81
  * The NetworkRecorder should already be attached and capturing.
77
82
  *
78
- * @param abortSignal Optional signal to stop navigation early.
83
+ * @param options Optional configuration for abort and CDP base URL.
79
84
  * @returns List of step labels that completed successfully.
80
85
  */
81
- export async function navigateXPages(abortSignal?: {
82
- aborted: boolean;
83
- }): Promise<string[]> {
86
+ export async function navigateXPages(
87
+ options?: NavigateXPagesOptions,
88
+ ): Promise<string[]> {
89
+ const { abortSignal, cdpBaseUrl = DEFAULT_CDP_BASE } = options ?? {};
84
90
  let wsUrl: string | null = null;
85
91
  try {
86
- const res = await fetch(`${CDP_BASE}/json/list`);
92
+ const res = await fetch(`${cdpBaseUrl}/json/list`);
87
93
  if (!res.ok) {
88
94
  log.warn("CDP not available for auto-navigation");
89
95
  return [];
@@ -30,6 +30,19 @@ export interface CredentialPolicy {
30
30
  /** How a credential value is injected into an outbound proxied request. */
31
31
  export type CredentialInjectionType = "header" | "query";
32
32
 
33
+ /** Reference to another credential whose value is composed with the primary value. */
34
+ export interface CredentialComposeRef {
35
+ /** Service of the credential to compose with. */
36
+ service: string;
37
+ /** Field of the credential to compose with. */
38
+ field: string;
39
+ /** Separator between the primary and composed values (e.g. ":"). */
40
+ separator: string;
41
+ }
42
+
43
+ /** Transform applied to a credential value after composition. */
44
+ export type CredentialValueTransform = "base64";
45
+
33
46
  /**
34
47
  * Describes where and how to inject a credential into proxied requests
35
48
  * matching a specific host pattern.
@@ -45,6 +58,17 @@ export interface CredentialInjectionTemplate {
45
58
  valuePrefix?: string;
46
59
  /** Query parameter name when injectionType is 'query'. */
47
60
  queryParamName?: string;
61
+ /**
62
+ * Compose this credential's value with another credential's value before injection.
63
+ * The result is `{primaryValue}{separator}{composedValue}`, optionally transformed
64
+ * by `valueTransform`.
65
+ */
66
+ composeWith?: CredentialComposeRef;
67
+ /**
68
+ * Transform applied to the (possibly composed) value before prepending `valuePrefix`.
69
+ * Applied after composition.
70
+ */
71
+ valueTransform?: CredentialValueTransform;
48
72
  }
49
73
 
50
74
  /** Input fields for specifying policy when storing a credential. */
@@ -10,9 +10,7 @@ import type { ToolDefinition } from "../../providers/types.js";
10
10
  import type { TokenEndpointAuthMethod } from "../../security/oauth2.js";
11
11
  import {
12
12
  deleteSecureKey,
13
- getBackendType,
14
13
  getSecureKey,
15
- isDowngradedFromKeychain,
16
14
  listSecureKeys,
17
15
  setSecureKey,
18
16
  } from "../../security/secure-keys.js";
@@ -426,30 +424,21 @@ class CredentialStoreTool implements Tool {
426
424
  }
427
425
 
428
426
  const allMetadata = listCredentialMetadata();
429
- // On the encrypted backend we can verify secrets still exist by reading
430
- // all key names once (instead of per-entry getSecureKey calls that each
431
- // re-read/re-derive the store). On keychain we trust metadata since the
432
- // OS keychain has no batch list API.
433
- // In downgraded mode (keychain failed, switched to encrypted), skip
434
- // batch verification because listSecureKeys() only returns keys from
435
- // the encrypted store — keychain-only credentials would be hidden.
436
- const downgraded = isDowngradedFromKeychain();
437
- const verifySecrets = getBackendType() === "encrypted" && !downgraded;
427
+ // Verify secrets still exist by reading all key names once (instead of
428
+ // per-entry getSecureKey calls that each re-read/re-derive the store).
438
429
  let secureKeySet: Set<string> | undefined;
439
- if (verifySecrets) {
440
- try {
441
- secureKeySet = new Set(listSecureKeys());
442
- } catch (err) {
443
- log.error(
444
- { err },
445
- "Failed to read secure store while listing credentials",
446
- );
447
- return {
448
- content:
449
- "Error: failed to read secure storage; cannot list credentials",
450
- isError: true,
451
- };
452
- }
430
+ try {
431
+ secureKeySet = new Set(listSecureKeys());
432
+ } catch (err) {
433
+ log.error(
434
+ { err },
435
+ "Failed to read secure store while listing credentials",
436
+ );
437
+ return {
438
+ content:
439
+ "Error: failed to read secure storage; cannot list credentials",
440
+ isError: true,
441
+ };
453
442
  }
454
443
  const entries = allMetadata
455
444
  .filter((m) => {
@@ -505,8 +494,14 @@ class CredentialStoreTool implements Tool {
505
494
  }
506
495
 
507
496
  const key = `credential:${service}:${field}`;
508
- const ok = deleteSecureKey(key);
509
- if (!ok) {
497
+ const result = deleteSecureKey(key);
498
+ if (result === "error") {
499
+ return {
500
+ content: `Error: failed to delete credential ${service}/${field} from secure storage`,
501
+ isError: true,
502
+ };
503
+ }
504
+ if (result === "not-found") {
510
505
  return {
511
506
  content: `Error: credential ${service}/${field} not found`,
512
507
  isError: true,
@@ -32,6 +32,7 @@ import { listCredentialMetadata } from "../../credentials/metadata-store.js";
32
32
  import type { CredentialInjectionTemplate } from "../../credentials/policy-types.js";
33
33
  import {
34
34
  resolveById,
35
+ resolveByServiceField,
35
36
  type ResolvedCredential,
36
37
  } from "../../credentials/resolve.js";
37
38
 
@@ -92,6 +93,35 @@ const sessions = new Map<ProxySessionId, ManagedSession>();
92
93
  */
93
94
  const acquireLocks = new Map<string, Promise<ProxySession>>();
94
95
 
96
+ /**
97
+ * Build the final header value for a matched credential injection template.
98
+ * Handles optional composition with a second credential and value transforms.
99
+ * Returns null if any referenced credential cannot be resolved.
100
+ */
101
+ function buildInjectedValue(
102
+ tpl: CredentialInjectionTemplate,
103
+ primaryValue: string,
104
+ ): string | null {
105
+ let value = primaryValue;
106
+
107
+ if (tpl.composeWith) {
108
+ const composed = resolveByServiceField(
109
+ tpl.composeWith.service,
110
+ tpl.composeWith.field,
111
+ );
112
+ if (!composed) return null;
113
+ const composedValue = getSecureKey(composed.storageKey);
114
+ if (!composedValue) return null;
115
+ value = `${value}${tpl.composeWith.separator}${composedValue}`;
116
+ }
117
+
118
+ if (tpl.valueTransform === "base64") {
119
+ value = Buffer.from(value).toString("base64");
120
+ }
121
+
122
+ return (tpl.valuePrefix ?? "") + value;
123
+ }
124
+
95
125
  /**
96
126
  * Resolve injection templates for a credential.
97
127
  *
@@ -295,8 +325,15 @@ export async function startSession(
295
325
  const value = getSecureKey(resolved.storageKey);
296
326
  if (!value) return req.headers;
297
327
 
298
- req.headers[tpl.headerName.toLowerCase()] =
299
- (tpl.valuePrefix ?? "") + value;
328
+ const headerValue = buildInjectedValue(tpl, value);
329
+ if (!headerValue) {
330
+ log.warn(
331
+ { host: req.hostname, credentialId: credId },
332
+ "MITM rewrite: blocking request — composeWith credential missing",
333
+ );
334
+ return null;
335
+ }
336
+ req.headers[tpl.headerName.toLowerCase()] = headerValue;
300
337
  return req.headers;
301
338
  }
302
339
 
@@ -377,7 +414,14 @@ export async function startSession(
377
414
  if (!value) return {};
378
415
 
379
416
  if (template.injectionType === "header" && template.headerName) {
380
- const headerValue = (template.valuePrefix ?? "") + value;
417
+ const headerValue = buildInjectedValue(template, value);
418
+ if (!headerValue) {
419
+ log.warn(
420
+ { hostname, credentialId },
421
+ "Policy: blocking matched request — composeWith credential missing",
422
+ );
423
+ return null;
424
+ }
381
425
  return { [template.headerName.toLowerCase()]: headerValue };
382
426
  }
383
427
  // Query param injection is handled via URL rewriting in the MITM path
@@ -266,6 +266,7 @@ export class PermissionChecker {
266
266
  persistentDecisionsAllowed,
267
267
  context.signal,
268
268
  temporaryOptionsAvailable,
269
+ context.toolUseId,
269
270
  );
270
271
 
271
272
  const decision = response.decision;
@@ -167,6 +167,8 @@ export interface ToolContext {
167
167
  requesterChatId?: string;
168
168
  /** Slack channel ID for channel-scoped permission enforcement. When set, tools are checked against the channel's permission profile. */
169
169
  channelPermissionChannelId?: string;
170
+ /** The tool_use block ID from the LLM response, used to correlate confirmation prompts with specific tool invocations. */
171
+ toolUseId?: string;
170
172
  }
171
173
 
172
174
  export interface DiffInfo {
@@ -61,8 +61,7 @@ export const uiShowTool: Tool = {
61
61
  "- Bulk actions: include `selectedRows` array with full row data for context\n\n" +
62
62
  "Presenting choices: When the user needs to make a choice or provide structured input, prefer interactive surfaces over plain text. " +
63
63
  "Use list (2-8 options, single select), form (structured input with typed fields), confirmation (destructive/important actions), or table (data review with selectable rows).\n\n" +
64
- "Tool chaining: After gathering data via tools (web search, browser, APIs), synthesize results into a visual output. " +
65
- "Exception: get_weather automatically renders its own surface with live API data — do NOT call ui_show, ui_update, app_create, or web_search after get_weather. Just respond with a brief summary.\n\n" +
64
+ "Tool chaining: After gathering data via tools (web search, browser, APIs), synthesize results into a visual output.\n\n" +
66
65
  'Task progress for multi-step workflows: Create a card with template "task_progress" and templateData containing steps. ' +
67
66
  "As each step completes, call ui_update to patch data.templateData (not top-level fields). " +
68
67
  'Set templateData.status to "completed" or "failed" when done.',
@@ -34,6 +34,8 @@ export interface WatchSession {
34
34
  recordingId?: string;
35
35
  /** Path where the learn recording was successfully saved (undefined if save failed) */
36
36
  savedRecordingPath?: string;
37
+ /** Reason the learn-mode bootstrap failed (CDP launch vs recorder attach) */
38
+ bootstrapFailureReason?: string;
37
39
  }
38
40
 
39
41
  /** Module-level map of watch sessions keyed by watchId. */