@vellumai/assistant 0.4.33 → 0.4.35

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 (149) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/access-request-decision.test.ts +2 -3
  3. package/src/__tests__/actor-token-service.test.ts +4 -11
  4. package/src/__tests__/approval-primitive.test.ts +0 -45
  5. package/src/__tests__/assistant-id-boundary-guard.test.ts +169 -0
  6. package/src/__tests__/callback-handoff-copy.test.ts +0 -1
  7. package/src/__tests__/channel-approval-routes.test.ts +5 -45
  8. package/src/__tests__/channel-guardian.test.ts +122 -345
  9. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
  10. package/src/__tests__/contacts-tools.test.ts +4 -5
  11. package/src/__tests__/conversation-attention-store.test.ts +2 -65
  12. package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
  13. package/src/__tests__/conversation-pairing.test.ts +0 -1
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -2
  15. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
  16. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
  17. package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
  18. package/src/__tests__/guardian-grant-minting.test.ts +0 -1
  19. package/src/__tests__/guardian-routing-state.test.ts +0 -3
  20. package/src/__tests__/inbound-invite-redemption.test.ts +0 -3
  21. package/src/__tests__/non-member-access-request.test.ts +0 -7
  22. package/src/__tests__/notification-broadcaster.test.ts +1 -2
  23. package/src/__tests__/notification-decision-fallback.test.ts +0 -2
  24. package/src/__tests__/notification-decision-strategy.test.ts +0 -1
  25. package/src/__tests__/relay-server.test.ts +11 -83
  26. package/src/__tests__/scoped-approval-grants.test.ts +9 -40
  27. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
  28. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  29. package/src/__tests__/send-notification-tool.test.ts +0 -1
  30. package/src/__tests__/slack-inbound-verification.test.ts +2 -4
  31. package/src/__tests__/thread-seed-composer.test.ts +0 -1
  32. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  33. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
  34. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -5
  35. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -17
  36. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -13
  37. package/src/__tests__/trusted-contact-verification.test.ts +3 -15
  38. package/src/__tests__/twilio-routes.test.ts +2 -2
  39. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  40. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -37
  41. package/src/approvals/approval-primitive.ts +0 -15
  42. package/src/approvals/guardian-decision-primitive.ts +0 -3
  43. package/src/approvals/guardian-request-resolvers.ts +0 -5
  44. package/src/calls/call-domain.ts +0 -3
  45. package/src/calls/call-store.ts +0 -3
  46. package/src/calls/guardian-action-sweep.ts +2 -1
  47. package/src/calls/guardian-dispatch.ts +1 -2
  48. package/src/calls/relay-access-wait.ts +0 -4
  49. package/src/calls/relay-server.ts +3 -11
  50. package/src/calls/relay-setup-router.ts +1 -2
  51. package/src/calls/relay-verification.ts +0 -1
  52. package/src/calls/twilio-routes.ts +0 -3
  53. package/src/calls/types.ts +0 -1
  54. package/src/calls/voice-session-bridge.ts +0 -1
  55. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +100 -171
  56. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
  57. package/src/contacts/contact-store.ts +13 -88
  58. package/src/contacts/contacts-write.ts +3 -11
  59. package/src/contacts/types.ts +0 -1
  60. package/src/daemon/handlers/config-channels.ts +16 -42
  61. package/src/daemon/handlers/config-inbox.ts +6 -6
  62. package/src/daemon/handlers/contacts.ts +3 -11
  63. package/src/daemon/handlers/index.ts +0 -2
  64. package/src/daemon/session-process.ts +0 -4
  65. package/src/memory/conversation-attention-store.ts +4 -19
  66. package/src/memory/conversation-crud.ts +0 -2
  67. package/src/memory/db-init.ts +4 -0
  68. package/src/memory/guardian-action-store.ts +0 -12
  69. package/src/memory/guardian-approvals.ts +35 -80
  70. package/src/memory/guardian-rate-limits.ts +1 -14
  71. package/src/memory/guardian-verification.ts +6 -34
  72. package/src/memory/invite-store.ts +5 -14
  73. package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
  74. package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
  75. package/src/memory/migrations/038-actor-token-records.ts +8 -1
  76. package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
  77. package/src/memory/migrations/110-channel-guardian.ts +27 -6
  78. package/src/memory/migrations/112-assistant-inbox.ts +39 -15
  79. package/src/memory/migrations/114-notifications.ts +37 -15
  80. package/src/memory/migrations/117-conversation-attention.ts +33 -9
  81. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  82. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  83. package/src/memory/migrations/index.ts +1 -0
  84. package/src/memory/migrations/registry.ts +14 -1
  85. package/src/memory/migrations/schema-introspection.ts +18 -0
  86. package/src/memory/schema/calls.ts +0 -7
  87. package/src/memory/schema/contacts.ts +0 -8
  88. package/src/memory/schema/guardian.ts +0 -5
  89. package/src/memory/schema/infrastructure.ts +0 -2
  90. package/src/memory/schema/notifications.ts +3 -17
  91. package/src/memory/scoped-approval-grants.ts +2 -24
  92. package/src/notifications/adapters/sms.ts +2 -1
  93. package/src/notifications/broadcaster.ts +1 -6
  94. package/src/notifications/decision-engine.ts +3 -4
  95. package/src/notifications/deliveries-store.ts +0 -4
  96. package/src/notifications/destination-resolver.ts +4 -6
  97. package/src/notifications/deterministic-checks.ts +1 -6
  98. package/src/notifications/emit-signal.ts +4 -11
  99. package/src/notifications/events-store.ts +7 -17
  100. package/src/notifications/preference-summary.ts +2 -2
  101. package/src/notifications/preferences-store.ts +2 -9
  102. package/src/notifications/signal.ts +0 -1
  103. package/src/notifications/thread-candidates.ts +1 -11
  104. package/src/notifications/types.ts +0 -3
  105. package/src/runtime/access-request-helper.ts +3 -10
  106. package/src/runtime/actor-refresh-token-store.ts +0 -6
  107. package/src/runtime/actor-token-store.ts +3 -16
  108. package/src/runtime/actor-trust-resolver.ts +1 -4
  109. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  110. package/src/runtime/auth/credential-service.ts +1 -15
  111. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  112. package/src/runtime/channel-guardian-service.ts +15 -46
  113. package/src/runtime/channel-invite-transport.ts +8 -0
  114. package/src/runtime/channel-invite-transports/email.ts +4 -0
  115. package/src/runtime/channel-invite-transports/slack.ts +6 -0
  116. package/src/runtime/channel-invite-transports/sms.ts +4 -0
  117. package/src/runtime/channel-invite-transports/telegram.ts +6 -0
  118. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  119. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  120. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  121. package/src/runtime/guardian-outbound-actions.ts +2 -12
  122. package/src/runtime/guardian-vellum-migration.ts +2 -3
  123. package/src/runtime/http-server.ts +3 -10
  124. package/src/runtime/http-types.ts +13 -1
  125. package/src/runtime/invite-redemption-service.ts +1 -14
  126. package/src/runtime/local-actor-identity.ts +2 -5
  127. package/src/runtime/routes/access-request-decision.ts +0 -1
  128. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -9
  129. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  130. package/src/runtime/routes/contact-routes.ts +15 -40
  131. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  132. package/src/runtime/routes/global-search-routes.ts +0 -2
  133. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -7
  134. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  135. package/src/runtime/routes/inbound-message-handler.ts +0 -3
  136. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -43
  137. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -4
  138. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  139. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  140. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  141. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  142. package/src/runtime/routes/pairing-routes.ts +4 -4
  143. package/src/runtime/routes/surface-content-routes.ts +104 -0
  144. package/src/runtime/tool-grant-request-helper.ts +0 -1
  145. package/src/tools/browser/browser-manager.ts +22 -21
  146. package/src/tools/browser/runtime-check.ts +111 -6
  147. package/src/tools/calls/call-start.ts +1 -3
  148. package/src/tools/followups/followup_create.ts +1 -2
  149. package/src/tools/tool-approval-handler.ts +0 -2
@@ -1,20 +1,20 @@
1
1
  ---
2
2
  name: "Google OAuth Setup"
3
- description: "Set up Google Cloud OAuth credentials for Gmail and Calendar using browser automation"
3
+ description: "Set up Google Cloud OAuth credentials for Gmail and Calendar"
4
4
  user-invocable: true
5
5
  credential-setup-for: "gmail"
6
- includes: ["browser", "public-ingress"]
7
- metadata: {"vellum": {"emoji": "\ud83d\udd11"}}
6
+ includes: ["public-ingress"]
7
+ metadata: { "vellum": { "emoji": "\ud83d\udd11" } }
8
8
  ---
9
9
 
10
10
  You are helping your user set up Google Cloud OAuth credentials so Gmail and Google Calendar integrations can connect.
11
11
 
12
12
  ## Client Check
13
13
 
14
- Determine whether the user has browser automation available (macOS desktop app) or is on a non-interactive channel (Telegram, SMS, etc.).
14
+ Determine which setup path to use based on the user's client:
15
15
 
16
- - **macOS desktop app**: Follow the **Automated Setup** path below.
17
- - **Telegram or other channel** (no browser automation): Follow the **Manual Setup for Channels** path below.
16
+ - **macOS desktop app**: Follow **Path B: CLI Setup** below.
17
+ - **Telegram or other channel** (no browser automation): Follow **Path A: Manual Setup for Channels** below.
18
18
 
19
19
  ---
20
20
 
@@ -29,6 +29,7 @@ Tell the user:
29
29
  > **Setting up Gmail & Calendar from Telegram**
30
30
  >
31
31
  > Since I can't automate the browser from here, I'll walk you through each step with direct links. You'll need:
32
+ >
32
33
  > 1. A Google account with access to Google Cloud Console
33
34
  > 2. About 5 minutes
34
35
  >
@@ -96,6 +97,7 @@ Tell the user:
96
97
  ### Channel Step 5: Create OAuth Credentials (Web Application)
97
98
 
98
99
  Before sending Step 4 to the user, resolve the concrete callback URL:
100
+
99
101
  - Read the configured public gateway URL (`ingress.publicBaseUrl`). If it is missing, run the `public-ingress` skill first.
100
102
  - Build `oauthCallbackUrl` as `<public gateway URL>/webhooks/oauth/callback`.
101
103
  - When you send the instructions below, replace `OAUTH_CALLBACK_URL` with that concrete value. Never send placeholders literally.
@@ -140,7 +142,7 @@ credential_store store:
140
142
 
141
143
  **Step 6b: Client Secret (requires split entry to avoid security filters)**
142
144
 
143
- The Client Secret starts with `GOCSPX-` which triggers the ingress secret scanner on channel messages. To work around this, ask the user to send only the portion *after* the prefix.
145
+ The Client Secret starts with `GOCSPX-` which triggers the ingress secret scanner on channel messages. To work around this, ask the user to send only the portion _after_ the prefix.
144
146
 
145
147
  Tell the user:
146
148
 
@@ -192,229 +194,165 @@ After the user authorizes (they'll come back and say so, or you can suggest they
192
194
 
193
195
  ---
194
196
 
195
- # Path B: Automated Setup (macOS Desktop App)
196
-
197
- You will automate the entire GCP setup via the browser while the user watches in the Chrome window on the side. The user's only manual actions are: signing in to their Google account, and copy-pasting credentials from the Chrome window into secure prompts.
198
-
199
- ## Browser Interaction Principles
200
-
201
- Google Cloud Console's UI changes frequently. Do NOT memorize or depend on specific element IDs, CSS selectors, or DOM structures. Instead:
202
-
203
- 1. **Snapshot first, act second.** Before every interaction, use `browser_snapshot` to discover interactive elements and their IDs. This is your primary navigation tool; it gives you the accessibility tree with clickable/typeable element IDs. Use `browser_screenshot` for visual context when the snapshot alone isn't enough.
204
- 2. **Adapt to what you see.** If an element's label or position differs from what you expect, use the snapshot to find the correct element. GCP may rename buttons, reorganize menus, or change form layouts at any time.
205
- 3. **Verify after every action.** After clicking, typing, or navigating, take a new snapshot to confirm the action succeeded. If it didn't, try an alternative interaction (e.g., if a dropdown didn't open on click, try pressing Space or Enter on the element).
206
- 4. **Never assume DOM structure.** Dropdowns may be `<select>`, `<mat-select>`, `<div role="listbox">`, or something else entirely. Use the snapshot to identify element types and interact accordingly.
207
- 5. **When stuck after 2 attempts, describe and ask.** Take a screenshot, describe what you see to the user, and ask for guidance.
208
-
209
- ## Anti-Loop Guardrails
197
+ # Path B: CLI Setup (macOS Desktop App)
210
198
 
211
- Each step has a **retry budget of 3 attempts**. An attempt is one try at the step's primary action (e.g., clicking a button, filling a form). If a step fails after 3 attempts:
199
+ **IMPORTANT: Always use `host_bash` (not `bash`) for all commands in this path.** The `gcloud` and `gws` CLIs need host access for Homebrew/npm installation, browser-based authentication, and interactive terminal prompts none of which are available inside the sandbox.
212
200
 
213
- 1. **Stop trying.** Do not continue retrying the same approach.
214
- 2. **Fall back to manual.** Tell the user what you were trying to do and ask them to complete that step manually in the Chrome window (which they can see on the side). Give them the direct URL and clear text instructions.
215
- 3. **Resume automation** at the next step once the user confirms the manual step is done.
201
+ You will set up Google Cloud OAuth credentials using the `gcloud` and `gws` command-line tools. This avoids browser automation entirely the user only needs to sign in once via the browser and copy-paste credentials from terminal output into secure prompts.
216
202
 
217
- If **two or more steps** require manual fallback, abandon the automated flow entirely and switch to giving the user the remaining steps as clear text instructions with links, using "Desktop app" as the OAuth application type.
203
+ ## CLI Step 1: Confirm
218
204
 
219
- ## Things That Do Not Work: Do Not Attempt
220
-
221
- These actions are technically impossible in the browser automation environment. Attempting them wastes time and leads to loops:
222
-
223
- - **Downloading files.** `browser_click` on a Download button does not save files to disk. There is NO JSON file to find at `~/Downloads` or anywhere else. Never click Download buttons.
224
- - **Clipboard operations.** You cannot copy/paste via browser automation. The user must manually copy values from the Chrome window.
225
- - **Deleting and recreating OAuth clients** to get a fresh secret. This orphans the stored client_id and causes `invalid_client` errors.
226
- - **Navigating away from the credential dialog** before both credentials are stored. You will lose the Client Secret display and cannot get it back without creating a new client.
227
-
228
- ## Step 1: Single Upfront Confirmation
229
-
230
- Use `ui_show` with `surface_type: "confirmation"`. Set `message` to just the title, and `detail` to the body:
205
+ Use `ui_show` with `surface_type: "confirmation"`:
231
206
 
232
207
  - **message:** `Set up Google Cloud for Gmail & Calendar`
233
208
  - **detail:**
234
209
  > Here's what will happen:
235
- > 1. **A browser opens on the side** so you can watch everything I do
236
- > 2. **You sign in** to your Google account in the browser
237
- > 3. **I automate everything** including project creation, APIs, OAuth config, and credentials
238
- > 4. **One copy-paste** where I'll ask you to copy the Client Secret from the browser into a secure prompt
210
+ >
211
+ > 1. **Install CLI tools** (`gcloud` and `gws`) if not already installed
212
+ > 2. **You sign in** to your Google account once via the browser
213
+ > 3. **CLI automates everything** project creation, APIs, consent screen, and credentials
214
+ > 4. **You copy-paste credentials** from the terminal output into secure prompts
239
215
  > 5. **You authorize Vellum** with one click
240
216
  >
241
- > The whole thing takes 2-3 minutes. Ready?
242
-
243
- If the user declines, acknowledge and stop. No further confirmations are needed after this point.
244
-
245
- ## Step 2: Open Google Cloud Console and Sign In
217
+ > Takes about a minute after first-time setup. Ready?
246
218
 
247
- **Goal:** The user is signed in and the Google Cloud Console dashboard is loaded.
248
-
249
- Navigate to `https://console.cloud.google.com/`.
250
-
251
- Take a screenshot to check the page state:
252
-
253
- - **Sign-in page:** Tell the user: "Please sign in to your Google account in the Chrome window on the right side of your screen." Then auto-detect sign-in completion by polling with `browser_screenshot` every 5-10 seconds to check if the URL has moved away from `accounts.google.com` to `console.cloud.google.com`. Do NOT ask the user to "let me know when you're done"; detect it automatically. Once sign-in is detected, tell the user: "Signed in! Starting the automated setup now..."
254
- - **Already signed in:** Tell the user: "Already signed in, starting setup now..." and continue immediately.
255
- - **CAPTCHA:** The browser automation's built-in handoff will handle this. If it persists, tell the user: "There's a CAPTCHA in the browser, please complete it and I'll continue automatically."
256
-
257
- **What you should see when done:** URL contains `console.cloud.google.com` and no sign-in overlay is visible.
258
-
259
- ## Step 3: Create or Select a Project
260
-
261
- **Goal:** A GCP project named "Vellum Assistant" exists and is selected.
262
-
263
- Tell the user: "Creating Google Cloud project..."
264
-
265
- Navigate to `https://console.cloud.google.com/projectcreate`.
266
-
267
- Take a `browser_snapshot`. Find the project name input field (look for an element with label containing "Project name" or a text input near the top of the form). Type "Vellum Assistant" into it.
268
-
269
- Look for a "Create" button in the snapshot and click it. Wait 10-15 seconds for project creation, then take a screenshot to check for:
270
- - **Success message** or redirect to the new project dashboard. Note the project ID from the URL or page content.
271
- - **"Project name already in use" error**: that's fine. Navigate to `https://console.cloud.google.com/cloud-resource-manager` to find and select the existing "Vellum Assistant" project. Use `browser_extract` to read the project ID from the page.
272
- - **Organization restriction or quota error**: tell the user what happened and ask them to resolve it.
273
-
274
- **What you should see when done:** The project selector in the top bar shows the project name, and you have the project ID (something like `vellum-assistant-12345`).
219
+ If the user declines, acknowledge and stop.
275
220
 
276
- Tell the user: "Project created!"
221
+ ## CLI Step 2: Install Prerequisites
277
222
 
278
- ## Step 4: Enable Gmail and Calendar APIs
223
+ Check for and install each prerequisite. If any installation fails (e.g., Homebrew not available, corporate restrictions), tell the user what went wrong and provide manual installation instructions.
279
224
 
280
- **Goal:** Both the Gmail API and Google Calendar API are enabled for the project.
225
+ ### gcloud
281
226
 
282
- Tell the user: "Enabling Gmail and Calendar APIs..."
227
+ ```bash
228
+ which gcloud
229
+ ```
283
230
 
284
- Navigate to each API's library page and enable it if not already enabled:
285
- 1. `https://console.cloud.google.com/apis/library/gmail.googleapis.com?project=PROJECT_ID`
286
- 2. `https://console.cloud.google.com/apis/library/calendar-json.googleapis.com?project=PROJECT_ID`
231
+ If missing:
287
232
 
288
- For each page: take a `browser_snapshot`. Look for:
289
- - **"Enable" button**: click it, wait a few seconds, take another snapshot to confirm.
290
- - **"Manage" button or "API enabled" text**: the API is already enabled. Skip it.
233
+ ```bash
234
+ brew install google-cloud-sdk
235
+ ```
291
236
 
292
- **What you should see when done:** Both API pages show "Manage" or "API enabled" status.
237
+ After installation, verify it works:
293
238
 
294
- Tell the user: "APIs enabled!"
239
+ ```bash
240
+ gcloud --version
241
+ ```
295
242
 
296
- ## Step 5: Configure OAuth Consent Screen
243
+ ### gws
297
244
 
298
- **Goal:** An OAuth consent screen is configured with External user type, the required scopes, and the user added as a test user.
245
+ ```bash
246
+ which gws
247
+ ```
299
248
 
300
- Tell the user: "Setting up OAuth consent screen. This is the longest step but it's fully automated..."
249
+ If missing:
301
250
 
302
- Navigate to `https://console.cloud.google.com/apis/credentials/consent?project=PROJECT_ID`.
251
+ ```bash
252
+ npm install -g @googleworkspace/cli
253
+ ```
303
254
 
304
- Take a `browser_snapshot` and `browser_screenshot`. Check the page state:
255
+ After installation, verify it works:
305
256
 
306
- ### If the consent screen is already configured
257
+ ```bash
258
+ gws --version
259
+ ```
307
260
 
308
- You'll see a dashboard showing the app name ("Vellum Assistant" or similar) with an "Edit App" button. **Skip to Step 6.**
261
+ ## CLI Step 3: Sign In to Google
309
262
 
310
- ### If you see a user type selection (External / Internal)
263
+ Tell the user: "Opening your browser so you can sign in to Google..."
311
264
 
312
- Select **"External"** and click **Create** or **Get Started**.
265
+ ```bash
266
+ gcloud auth login
267
+ ```
313
268
 
314
- ### Consent screen form (wizard or single-page)
269
+ This opens the browser for Google sign-in. Wait for the command to complete — it prints the authenticated account email on success.
315
270
 
316
- Google Cloud uses either a multi-page wizard or a single-page form. Adapt to what you see:
271
+ If the user is already authenticated (`gcloud auth list` shows an active account), skip this step and tell the user: "Already signed in, continuing setup..."
317
272
 
318
- **App information section:**
319
- - **App name**: Type "Vellum Assistant" in the app name field.
320
- - **User support email**: This is typically a dropdown showing the signed-in user's email. Use `browser_snapshot` to find a `<select>` or clickable dropdown element near "User support email". Select the user's email.
321
- - **Developer contact email**: Type the user's email into this field. (Use the same email visible in the support email dropdown if you can read it, or use `browser_extract` to find the email shown on the page.)
322
- - Click **Save and Continue** if on a multi-page wizard.
273
+ ## CLI Step 4: GCP Project Setup
323
274
 
324
- **Scopes section:**
325
- - Click **"Add or Remove Scopes"** (or similar button).
326
- - In the scope picker dialog, look for a text input labeled **"Manually add scopes"** or **"Filter"** at the bottom or top of the dialog.
327
- - Paste all 6 scopes at once as a comma-separated string into that input:
328
- ```
329
- https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/gmail.modify,https://www.googleapis.com/auth/gmail.send,https://www.googleapis.com/auth/calendar.readonly,https://www.googleapis.com/auth/calendar.events,https://www.googleapis.com/auth/userinfo.email
330
- ```
331
- - Click **"Add to Table"** or **"Update"** to confirm the scopes.
332
- - If no manual input is available, you'll need to search for and check each scope individually using the scope tree. Search for each scope URL in the filter box and check its checkbox.
333
- - Click **Save and Continue** (or **Update** then **Save and Continue**).
275
+ Tell the user: "Setting up your Google Cloud project, APIs, and credentials..."
334
276
 
335
- **Test users section:**
336
- - Click **"Add Users"** or similar.
337
- - Enter the user's email address.
338
- - Click **Add** then **Save and Continue**.
277
+ ```bash
278
+ gws auth setup
279
+ ```
339
280
 
340
- **Summary section:**
341
- - Click **"Back to Dashboard"** or **"Submit"**.
281
+ This command automates:
342
282
 
343
- **What you should see when done:** A consent screen dashboard showing "Vellum Assistant" as the app name.
283
+ - GCP project creation (or selection of an existing one)
284
+ - OAuth consent screen configuration
285
+ - OAuth credential creation
344
286
 
345
- Tell the user: "Consent screen configured!"
287
+ Wait for the command to complete. It may have interactive prompts — let them run in the terminal and the user can respond if needed.
346
288
 
347
- ## Step 6: Create OAuth Credentials and Capture Them
289
+ Note the **project ID** from the output you'll need it for the next step.
348
290
 
349
- **Goal:** A "Desktop app" OAuth client exists, and both its Client ID and Client Secret are stored in the vault.
291
+ ## CLI Step 5: Enable Additional APIs
350
292
 
351
- Tell the user: "Creating OAuth credentials..."
293
+ `gws auth setup` enables the APIs it needs, but Vellum also requires the Calendar and People APIs. Enable them explicitly using the project ID from step 4:
352
294
 
353
- ### 6a: Create the credential
295
+ ```bash
296
+ gcloud services enable calendar-json.googleapis.com --project=PROJECT_ID
297
+ gcloud services enable people.googleapis.com --project=PROJECT_ID
298
+ ```
354
299
 
355
- Navigate to `https://console.cloud.google.com/apis/credentials?project=PROJECT_ID`.
300
+ If either command reports the API is already enabled, that's fine — continue.
356
301
 
357
- Take a `browser_snapshot`. Find and click a button labeled **"Create Credentials"** or **"+ Create Credentials"**. A dropdown menu should appear. Take another snapshot and click **"OAuth client ID"**.
302
+ ## CLI Step 5b: Update OAuth Consent Screen Scopes
358
303
 
359
- On the creation form (take a snapshot to see the fields):
360
- - **Application type**: Find the dropdown and select **"Desktop app"**. This may be a `<select>` element or a custom dropdown. Use the snapshot to identify it. You might need to click the dropdown first, then take another snapshot to see the options, then click "Desktop app".
361
- - **Name**: Type "Vellum Assistant" in the name field.
362
- - Do NOT add any redirect URIs. The desktop app flow doesn't need them.
304
+ `gws auth setup` configures the consent screen with Gmail scopes only. Vellum also needs Calendar, Contacts, and userinfo scopes. Tell the user to add the missing scopes:
363
305
 
364
- Click **"Create"** to submit the form.
306
+ > **Update consent screen scopes**
307
+ >
308
+ > Open: `https://console.cloud.google.com/apis/credentials/consent/edit?project=PROJECT_ID`
309
+ >
310
+ > 1. Click through to the **Scopes** page (click **Save and Continue** on the first page)
311
+ > 2. Click **Add or Remove Scopes** and ensure all of these are present:
312
+ > - `https://www.googleapis.com/auth/gmail.readonly`
313
+ > - `https://www.googleapis.com/auth/gmail.modify`
314
+ > - `https://www.googleapis.com/auth/gmail.send`
315
+ > - `https://www.googleapis.com/auth/calendar.readonly`
316
+ > - `https://www.googleapis.com/auth/calendar.events`
317
+ > - `https://www.googleapis.com/auth/userinfo.email`
318
+ > - `https://www.googleapis.com/auth/contacts.readonly`
319
+ > 3. Click **Update**, then **Save and Continue** through the remaining pages
365
320
 
366
- ### 6b: Capture credentials from the dialog
321
+ (Substitute the actual project ID into the URL.)
367
322
 
368
- After creation, a dialog will display the **Client ID** and **Client Secret**. This is the critical step.
323
+ The Gmail scopes may already be present from `gws auth setup` add any that are missing.
369
324
 
370
- **First**, try to auto-read the **Client ID** using `browser_extract`. The Client ID matches the pattern `*.apps.googleusercontent.com`. Search the extracted text for this pattern. If found, store it:
325
+ ## CLI Step 6: Collect Credentials
371
326
 
372
- ```
373
- credential_store store:
374
- service: "integration:gmail"
375
- field: "client_id"
376
- value: "<the Client ID extracted from the page>"
377
- ```
327
+ The `gws auth setup` output or the GCP Console shows the Client ID and Client Secret. Ask the user to copy-paste them into secure prompts.
378
328
 
379
- If `browser_extract` fails to find the Client ID, prompt the user instead:
329
+ **Client ID:**
380
330
 
381
331
  ```
382
332
  credential_store prompt:
383
333
  service: "integration:gmail"
384
334
  field: "client_id"
385
335
  label: "Google OAuth Client ID"
386
- description: "Copy the Client ID from the dialog in the Chrome window and paste it here. It looks like 123456789-xxxxx.apps.googleusercontent.com"
336
+ description: "Copy the Client ID from the setup output or GCP Console. It looks like 123456789-xxxxx.apps.googleusercontent.com"
387
337
  placeholder: "xxxxx.apps.googleusercontent.com"
388
338
  ```
389
339
 
390
- **Then**, whether the Client ID was auto-read or prompted, tell the user:
391
-
392
- > "Got the Client ID! Now I need the Client Secret. You can see it in the dialog in the Chrome window. It starts with `GOCSPX-`. Please copy it and paste it into the secure prompt below."
393
-
394
- And present the secure prompt:
340
+ **Client Secret:**
395
341
 
396
342
  ```
397
343
  credential_store prompt:
398
344
  service: "integration:gmail"
399
345
  field: "client_secret"
400
346
  label: "Google OAuth Client Secret"
401
- description: "Copy the Client Secret from the Google Cloud Console dialog and paste it here."
347
+ description: "Copy the Client Secret from the setup output or GCP Console. It starts with GOCSPX-"
402
348
  placeholder: "GOCSPX-..."
403
349
  ```
404
350
 
405
- Wait for the user to complete the prompt. **Do not take any other browser actions until the user has pasted the secret.** The dialog must stay open so they can see and copy the value.
406
-
407
- If the user has trouble locating the secret, take a `browser_screenshot` and describe where the secret field is on the screen, but do NOT attempt to read the secret value yourself. It must come from the user for accuracy.
408
-
409
- **What you should see when done:** `credential_store list` shows both `client_id` and `client_secret` for `integration:gmail`.
410
-
411
- Tell the user: "Credentials stored securely!"
351
+ Wait for both prompts to be completed before continuing.
412
352
 
413
- ## Step 7: OAuth2 Authorization
353
+ ## CLI Step 7: Authorize
414
354
 
415
- **Goal:** The user authorizes Vellum to access their Gmail and Calendar via OAuth.
416
-
417
- Tell the user: "Starting the authorization flow — a Google sign-in page will open in a few seconds. Just click 'Allow' when it appears."
355
+ Tell the user: "Starting the authorization flow a Google sign-in page will open. Just click 'Allow' when it appears."
418
356
 
419
357
  Use `credential_store` with:
420
358
 
@@ -429,15 +367,6 @@ This auto-reads client_id and client_secret from the secure store and auto-fills
429
367
 
430
368
  **Verify:** The `oauth2_connect` call returns a success message with the connected account email.
431
369
 
432
- ## Step 8: Done!
370
+ ## CLI Step 8: Done!
433
371
 
434
372
  Tell the user: "**Gmail and Calendar are connected!** You can now read, search, and send emails, plus view and manage your calendar. Try asking me to check your inbox or show your upcoming events!"
435
-
436
- ## Error Handling
437
-
438
- - **Page load failures:** Retry navigation once. If it still fails, tell the user and ask them to check their internet connection.
439
- - **Permission errors in GCP:** The user may need billing enabled or organization-level permissions. Explain clearly and ask them to resolve it.
440
- - **Consent screen already configured:** Don't overwrite. Skip to credential creation.
441
- - **Element not found:** Take a fresh `browser_snapshot` to re-assess. The GCP UI may have changed. Describe what you see and try alternative approaches. If stuck after 2 attempts, ask the user for guidance. They can see the Chrome window too.
442
- - **OAuth flow timeout or failure:** Offer to retry. The credentials are already stored, so reconnecting only requires re-running the authorization flow.
443
- - **Any unexpected state:** Take a `browser_screenshot`, describe what you see, and ask the user for guidance.
@@ -131,7 +131,6 @@ export async function run(
131
131
  sourceEventName,
132
132
  sourceChannel: "assistant_tool",
133
133
  sourceSessionId,
134
- assistantId: context.assistantId,
135
134
  attentionHints: {
136
135
  requiresAction: parseBool(input.requires_action, true),
137
136
  urgency,
@@ -1,4 +1,4 @@
1
- import { and, asc, desc, eq, isNull, like, or, sql } from "drizzle-orm";
1
+ import { and, asc, desc, eq, like, sql } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
4
  import { getDb } from "../memory/db.js";
@@ -41,7 +41,6 @@ function parseContact(row: typeof contacts.$inferSelect): Contact {
41
41
  role: row.role as Contact["role"],
42
42
  contactType: (row.contactType as Contact["contactType"]) ?? "human",
43
43
  principalId: row.principalId,
44
- assistantId: row.assistantId ?? null,
45
44
  };
46
45
  }
47
46
 
@@ -105,7 +104,7 @@ interface SyncChannelData {
105
104
 
106
105
  // ── CRUD ─────────────────────────────────────────────────────────────
107
106
 
108
- /** Retrieve a contact by ID without assistantId scoping.
107
+ /** Retrieve a contact by ID.
109
108
  * Used by functions that have already resolved identity through channel lookups. */
110
109
  export function getContactInternal(id: string): ContactWithChannels | null {
111
110
  const db = getDb();
@@ -114,21 +113,9 @@ export function getContactInternal(id: string): ContactWithChannels | null {
114
113
  return withChannels(parseContact(row));
115
114
  }
116
115
 
117
- export function getContact(
118
- id: string,
119
- assistantId: string,
120
- ): ContactWithChannels | null {
116
+ export function getContact(id: string): ContactWithChannels | null {
121
117
  const db = getDb();
122
- const row = db
123
- .select()
124
- .from(contacts)
125
- .where(
126
- and(
127
- eq(contacts.id, id),
128
- or(eq(contacts.assistantId, assistantId), isNull(contacts.assistantId)),
129
- ),
130
- )
131
- .get();
118
+ const row = db.select().from(contacts).where(eq(contacts.id, id)).get();
132
119
  if (!row) return null;
133
120
  return withChannels(parseContact(row));
134
121
  }
@@ -154,7 +141,6 @@ export function upsertContact(params: {
154
141
  role?: ContactRole;
155
142
  contactType?: ContactType;
156
143
  principalId?: string | null;
157
- assistantId: string;
158
144
  channels?: SyncChannelData[];
159
145
  }): ContactWithChannels & { created: boolean } {
160
146
  const db = getDb();
@@ -180,8 +166,6 @@ export function upsertContact(params: {
180
166
  updateSet.contactType = params.contactType;
181
167
  if (params.principalId !== undefined)
182
168
  updateSet.principalId = params.principalId;
183
- if (params.assistantId !== undefined)
184
- updateSet.assistantId = params.assistantId;
185
169
 
186
170
  db.update(contacts)
187
171
  .set(updateSet)
@@ -245,8 +229,6 @@ export function upsertContact(params: {
245
229
  updateSet.contactType = params.contactType;
246
230
  if (params.principalId !== undefined)
247
231
  updateSet.principalId = params.principalId;
248
- if (params.assistantId !== undefined)
249
- updateSet.assistantId = params.assistantId;
250
232
 
251
233
  db.update(contacts)
252
234
  .set(updateSet)
@@ -272,7 +254,6 @@ export function upsertContact(params: {
272
254
  role: params.role ?? "contact",
273
255
  contactType: params.contactType ?? "human",
274
256
  principalId: params.principalId ?? null,
275
- assistantId: params.assistantId,
276
257
  createdAt: now,
277
258
  updatedAt: now,
278
259
  })
@@ -404,7 +385,6 @@ function syncChannels(
404
385
  }
405
386
 
406
387
  export function searchContacts(params: {
407
- assistantId: string;
408
388
  query?: string;
409
389
  channelAddress?: string;
410
390
  channelType?: string;
@@ -426,20 +406,10 @@ export function searchContacts(params: {
426
406
  .where(
427
407
  params.channelType
428
408
  ? and(
429
- or(
430
- eq(contacts.assistantId, params.assistantId),
431
- isNull(contacts.assistantId),
432
- ),
433
409
  eq(contactChannels.type, params.channelType),
434
410
  like(contactChannels.address, `%${normalizedAddress}%`),
435
411
  )
436
- : and(
437
- or(
438
- eq(contacts.assistantId, params.assistantId),
439
- isNull(contacts.assistantId),
440
- ),
441
- like(contactChannels.address, `%${normalizedAddress}%`),
442
- ),
412
+ : and(like(contactChannels.address, `%${normalizedAddress}%`)),
443
413
  )
444
414
  .all();
445
415
 
@@ -467,15 +437,7 @@ export function searchContacts(params: {
467
437
  .select({ contactId: contactChannels.contactId })
468
438
  .from(contactChannels)
469
439
  .innerJoin(contacts, eq(contactChannels.contactId, contacts.id))
470
- .where(
471
- and(
472
- or(
473
- eq(contacts.assistantId, params.assistantId),
474
- isNull(contacts.assistantId),
475
- ),
476
- eq(contactChannels.type, params.channelType),
477
- ),
478
- )
440
+ .where(eq(contactChannels.type, params.channelType))
479
441
  .all();
480
442
 
481
443
  const contactIds = [...new Set(channelRows.map((r) => r.contactId))];
@@ -497,12 +459,7 @@ export function searchContacts(params: {
497
459
  }
498
460
 
499
461
  // Search by display name, optionally filtered by channelType
500
- const conditions = [
501
- or(
502
- eq(contacts.assistantId, params.assistantId),
503
- isNull(contacts.assistantId),
504
- )!,
505
- ];
462
+ const conditions = [];
506
463
  if (params.query) {
507
464
  const sanitized = escapeLike(params.query);
508
465
  if (!sanitized && !params.role && !params.contactType) return [];
@@ -560,7 +517,6 @@ export function searchContacts(params: {
560
517
  }
561
518
 
562
519
  export function listContacts(
563
- assistantId: string,
564
520
  limit = 50,
565
521
  role?: ContactRole,
566
522
  contactType?: ContactType,
@@ -568,9 +524,7 @@ export function listContacts(
568
524
  ): ContactWithChannels[] {
569
525
  const db = getDb();
570
526
  const effectiveLimit = opts?.uncapped ? limit : Math.min(limit, 200);
571
- const conditions = [
572
- or(eq(contacts.assistantId, assistantId), isNull(contacts.assistantId))!,
573
- ];
527
+ const conditions = [];
574
528
  if (role) conditions.push(eq(contacts.role, role));
575
529
  if (contactType) conditions.push(eq(contacts.contactType, contactType));
576
530
  const rows = db
@@ -595,7 +549,6 @@ export function listContacts(
595
549
  export function mergeContacts(
596
550
  keepId: string,
597
551
  mergeId: string,
598
- assistantId: string,
599
552
  ): ContactWithChannels {
600
553
  const db = getDb();
601
554
 
@@ -607,30 +560,14 @@ export function mergeContacts(
607
560
  const keep = tx
608
561
  .select()
609
562
  .from(contacts)
610
- .where(
611
- and(
612
- eq(contacts.id, keepId),
613
- or(
614
- eq(contacts.assistantId, assistantId),
615
- isNull(contacts.assistantId),
616
- ),
617
- ),
618
- )
563
+ .where(eq(contacts.id, keepId))
619
564
  .get();
620
565
  if (!keep) throw new Error(`Contact "${keepId}" not found`);
621
566
 
622
567
  const merge = tx
623
568
  .select()
624
569
  .from(contacts)
625
- .where(
626
- and(
627
- eq(contacts.id, mergeId),
628
- or(
629
- eq(contacts.assistantId, assistantId),
630
- isNull(contacts.assistantId),
631
- ),
632
- ),
633
- )
570
+ .where(eq(contacts.id, mergeId))
634
571
  .get();
635
572
  if (!merge) throw new Error(`Contact "${mergeId}" not found`);
636
573
 
@@ -646,8 +583,6 @@ export function mergeContacts(
646
583
  lastInteraction: mergedLastInteraction,
647
584
  notes: [keep.notes, merge.notes].filter(Boolean).join("\n") || null,
648
585
  updatedAt: now,
649
- // Rebind legacy null-scoped contacts to prevent cross-assistant leakage
650
- ...(keep.assistantId == null ? { assistantId } : {}),
651
586
  })
652
587
  .where(eq(contacts.id, keepId))
653
588
  .run();
@@ -805,14 +740,12 @@ export function findContactChannel(params: {
805
740
  */
806
741
  export function findGuardianForChannel(
807
742
  channelType: string,
808
- assistantId: string,
809
743
  ): { contact: Contact; channel: ContactChannel } | null {
810
744
  const db = getDb();
811
745
  const conditions = [
812
746
  eq(contacts.role, "guardian"),
813
747
  eq(contactChannels.type, channelType),
814
748
  eq(contactChannels.status, "active"),
815
- eq(contacts.assistantId, assistantId),
816
749
  ];
817
750
  const rows = db
818
751
  .select({
@@ -841,16 +774,12 @@ export function findGuardianForChannel(
841
774
  *
842
775
  * Returns true if a channel was found and revoked, false otherwise.
843
776
  */
844
- export function revokeGuardianChannel(
845
- channelType: string,
846
- assistantId: string,
847
- ): boolean {
777
+ export function revokeGuardianChannel(channelType: string): boolean {
848
778
  const db = getDb();
849
779
  const conditions = [
850
780
  eq(contacts.role, "guardian"),
851
781
  eq(contactChannels.type, channelType),
852
782
  eq(contactChannels.status, "active"),
853
- eq(contacts.assistantId, assistantId),
854
783
  ];
855
784
  const rows = db
856
785
  .select({
@@ -885,7 +814,7 @@ export function revokeGuardianChannel(
885
814
  * pick a guardian that has no active channels.
886
815
  * Returns channels ordered by most-recently-verified first.
887
816
  */
888
- export function listGuardianChannels(assistantId: string): {
817
+ export function listGuardianChannels(): {
889
818
  contact: Contact;
890
819
  channels: ContactChannel[];
891
820
  } | null {
@@ -898,11 +827,7 @@ export function listGuardianChannels(assistantId: string): {
898
827
  .from(contacts)
899
828
  .innerJoin(contactChannels, eq(contacts.id, contactChannels.contactId))
900
829
  .where(
901
- and(
902
- eq(contacts.role, "guardian"),
903
- eq(contactChannels.status, "active"),
904
- eq(contacts.assistantId, assistantId),
905
- ),
830
+ and(eq(contacts.role, "guardian"), eq(contactChannels.status, "active")),
906
831
  )
907
832
  .orderBy(desc(contactChannels.verifiedAt))
908
833
  .all();