@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.
- package/package.json +1 -1
- package/src/__tests__/access-request-decision.test.ts +2 -3
- package/src/__tests__/actor-token-service.test.ts +4 -11
- package/src/__tests__/approval-primitive.test.ts +0 -45
- package/src/__tests__/assistant-id-boundary-guard.test.ts +169 -0
- package/src/__tests__/callback-handoff-copy.test.ts +0 -1
- package/src/__tests__/channel-approval-routes.test.ts +5 -45
- package/src/__tests__/channel-guardian.test.ts +122 -345
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
- package/src/__tests__/contacts-tools.test.ts +4 -5
- package/src/__tests__/conversation-attention-store.test.ts +2 -65
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -1
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -2
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
- package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
- package/src/__tests__/guardian-grant-minting.test.ts +0 -1
- package/src/__tests__/guardian-routing-state.test.ts +0 -3
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +0 -7
- package/src/__tests__/notification-broadcaster.test.ts +1 -2
- package/src/__tests__/notification-decision-fallback.test.ts +0 -2
- package/src/__tests__/notification-decision-strategy.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +11 -83
- package/src/__tests__/scoped-approval-grants.test.ts +9 -40
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
- package/src/__tests__/send-endpoint-busy.test.ts +0 -1
- package/src/__tests__/send-notification-tool.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +2 -4
- package/src/__tests__/thread-seed-composer.test.ts +0 -1
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -5
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -17
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -13
- package/src/__tests__/trusted-contact-verification.test.ts +3 -15
- package/src/__tests__/twilio-routes.test.ts +2 -2
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -37
- package/src/approvals/approval-primitive.ts +0 -15
- package/src/approvals/guardian-decision-primitive.ts +0 -3
- package/src/approvals/guardian-request-resolvers.ts +0 -5
- package/src/calls/call-domain.ts +0 -3
- package/src/calls/call-store.ts +0 -3
- package/src/calls/guardian-action-sweep.ts +2 -1
- package/src/calls/guardian-dispatch.ts +1 -2
- package/src/calls/relay-access-wait.ts +0 -4
- package/src/calls/relay-server.ts +3 -11
- package/src/calls/relay-setup-router.ts +1 -2
- package/src/calls/relay-verification.ts +0 -1
- package/src/calls/twilio-routes.ts +0 -3
- package/src/calls/types.ts +0 -1
- package/src/calls/voice-session-bridge.ts +0 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +100 -171
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
- package/src/contacts/contact-store.ts +13 -88
- package/src/contacts/contacts-write.ts +3 -11
- package/src/contacts/types.ts +0 -1
- package/src/daemon/handlers/config-channels.ts +16 -42
- package/src/daemon/handlers/config-inbox.ts +6 -6
- package/src/daemon/handlers/contacts.ts +3 -11
- package/src/daemon/handlers/index.ts +0 -2
- package/src/daemon/session-process.ts +0 -4
- package/src/memory/conversation-attention-store.ts +4 -19
- package/src/memory/conversation-crud.ts +0 -2
- package/src/memory/db-init.ts +4 -0
- package/src/memory/guardian-action-store.ts +0 -12
- package/src/memory/guardian-approvals.ts +35 -80
- package/src/memory/guardian-rate-limits.ts +1 -14
- package/src/memory/guardian-verification.ts +6 -34
- package/src/memory/invite-store.ts +5 -14
- package/src/memory/migrations/026-guardian-verification-sessions.ts +28 -9
- package/src/memory/migrations/027a-guardian-bootstrap-token.ts +16 -3
- package/src/memory/migrations/038-actor-token-records.ts +8 -1
- package/src/memory/migrations/039-actor-refresh-token-records.ts +11 -2
- package/src/memory/migrations/110-channel-guardian.ts +27 -6
- package/src/memory/migrations/112-assistant-inbox.ts +39 -15
- package/src/memory/migrations/114-notifications.ts +37 -15
- package/src/memory/migrations/117-conversation-attention.ts +33 -9
- package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +14 -1
- package/src/memory/migrations/schema-introspection.ts +18 -0
- package/src/memory/schema/calls.ts +0 -7
- package/src/memory/schema/contacts.ts +0 -8
- package/src/memory/schema/guardian.ts +0 -5
- package/src/memory/schema/infrastructure.ts +0 -2
- package/src/memory/schema/notifications.ts +3 -17
- package/src/memory/scoped-approval-grants.ts +2 -24
- package/src/notifications/adapters/sms.ts +2 -1
- package/src/notifications/broadcaster.ts +1 -6
- package/src/notifications/decision-engine.ts +3 -4
- package/src/notifications/deliveries-store.ts +0 -4
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/deterministic-checks.ts +1 -6
- package/src/notifications/emit-signal.ts +4 -11
- package/src/notifications/events-store.ts +7 -17
- package/src/notifications/preference-summary.ts +2 -2
- package/src/notifications/preferences-store.ts +2 -9
- package/src/notifications/signal.ts +0 -1
- package/src/notifications/thread-candidates.ts +1 -11
- package/src/notifications/types.ts +0 -3
- package/src/runtime/access-request-helper.ts +3 -10
- package/src/runtime/actor-refresh-token-store.ts +0 -6
- package/src/runtime/actor-token-store.ts +3 -16
- package/src/runtime/actor-trust-resolver.ts +1 -4
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
- package/src/runtime/auth/credential-service.ts +1 -15
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/channel-guardian-service.ts +15 -46
- package/src/runtime/channel-invite-transport.ts +8 -0
- package/src/runtime/channel-invite-transports/email.ts +4 -0
- package/src/runtime/channel-invite-transports/slack.ts +6 -0
- package/src/runtime/channel-invite-transports/sms.ts +4 -0
- package/src/runtime/channel-invite-transports/telegram.ts +6 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
- package/src/runtime/guardian-action-followup-executor.ts +3 -2
- package/src/runtime/guardian-action-grant-minter.ts +0 -1
- package/src/runtime/guardian-outbound-actions.ts +2 -12
- package/src/runtime/guardian-vellum-migration.ts +2 -3
- package/src/runtime/http-server.ts +3 -10
- package/src/runtime/http-types.ts +13 -1
- package/src/runtime/invite-redemption-service.ts +1 -14
- package/src/runtime/local-actor-identity.ts +2 -5
- package/src/runtime/routes/access-request-decision.ts +0 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -9
- package/src/runtime/routes/channel-readiness-routes.ts +29 -18
- package/src/runtime/routes/contact-routes.ts +15 -40
- package/src/runtime/routes/conversation-attention-routes.ts +0 -2
- package/src/runtime/routes/global-search-routes.ts +0 -2
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -7
- package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +0 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -43
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -4
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
- package/src/runtime/routes/pairing-routes.ts +4 -4
- package/src/runtime/routes/surface-content-routes.ts +104 -0
- package/src/runtime/tool-grant-request-helper.ts +0 -1
- package/src/tools/browser/browser-manager.ts +22 -21
- package/src/tools/browser/runtime-check.ts +111 -6
- package/src/tools/calls/call-start.ts +1 -3
- package/src/tools/followups/followup_create.ts +1 -2
- 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
|
|
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: ["
|
|
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
|
|
14
|
+
Determine which setup path to use based on the user's client:
|
|
15
15
|
|
|
16
|
-
- **macOS desktop app**: Follow
|
|
17
|
-
- **Telegram or other channel** (no browser automation): Follow
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
## CLI Step 1: Confirm
|
|
218
204
|
|
|
219
|
-
|
|
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
|
-
>
|
|
236
|
-
>
|
|
237
|
-
>
|
|
238
|
-
>
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
221
|
+
## CLI Step 2: Install Prerequisites
|
|
277
222
|
|
|
278
|
-
|
|
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
|
-
|
|
225
|
+
### gcloud
|
|
281
226
|
|
|
282
|
-
|
|
227
|
+
```bash
|
|
228
|
+
which gcloud
|
|
229
|
+
```
|
|
283
230
|
|
|
284
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
233
|
+
```bash
|
|
234
|
+
brew install google-cloud-sdk
|
|
235
|
+
```
|
|
291
236
|
|
|
292
|
-
|
|
237
|
+
After installation, verify it works:
|
|
293
238
|
|
|
294
|
-
|
|
239
|
+
```bash
|
|
240
|
+
gcloud --version
|
|
241
|
+
```
|
|
295
242
|
|
|
296
|
-
|
|
243
|
+
### gws
|
|
297
244
|
|
|
298
|
-
|
|
245
|
+
```bash
|
|
246
|
+
which gws
|
|
247
|
+
```
|
|
299
248
|
|
|
300
|
-
|
|
249
|
+
If missing:
|
|
301
250
|
|
|
302
|
-
|
|
251
|
+
```bash
|
|
252
|
+
npm install -g @googleworkspace/cli
|
|
253
|
+
```
|
|
303
254
|
|
|
304
|
-
|
|
255
|
+
After installation, verify it works:
|
|
305
256
|
|
|
306
|
-
|
|
257
|
+
```bash
|
|
258
|
+
gws --version
|
|
259
|
+
```
|
|
307
260
|
|
|
308
|
-
|
|
261
|
+
## CLI Step 3: Sign In to Google
|
|
309
262
|
|
|
310
|
-
|
|
263
|
+
Tell the user: "Opening your browser so you can sign in to Google..."
|
|
311
264
|
|
|
312
|
-
|
|
265
|
+
```bash
|
|
266
|
+
gcloud auth login
|
|
267
|
+
```
|
|
313
268
|
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
- Click **Add** then **Save and Continue**.
|
|
277
|
+
```bash
|
|
278
|
+
gws auth setup
|
|
279
|
+
```
|
|
339
280
|
|
|
340
|
-
|
|
341
|
-
- Click **"Back to Dashboard"** or **"Submit"**.
|
|
281
|
+
This command automates:
|
|
342
282
|
|
|
343
|
-
|
|
283
|
+
- GCP project creation (or selection of an existing one)
|
|
284
|
+
- OAuth consent screen configuration
|
|
285
|
+
- OAuth credential creation
|
|
344
286
|
|
|
345
|
-
|
|
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
|
-
|
|
289
|
+
Note the **project ID** from the output — you'll need it for the next step.
|
|
348
290
|
|
|
349
|
-
|
|
291
|
+
## CLI Step 5: Enable Additional APIs
|
|
350
292
|
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
+
If either command reports the API is already enabled, that's fine — continue.
|
|
356
301
|
|
|
357
|
-
|
|
302
|
+
## CLI Step 5b: Update OAuth Consent Screen Scopes
|
|
358
303
|
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
(Substitute the actual project ID into the URL.)
|
|
367
322
|
|
|
368
|
-
|
|
323
|
+
The Gmail scopes may already be present from `gws auth setup` — add any that are missing.
|
|
369
324
|
|
|
370
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
|
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:
|
|
353
|
+
## CLI Step 7: Authorize
|
|
414
354
|
|
|
415
|
-
|
|
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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, asc, desc, eq,
|
|
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
|
|
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(
|
|
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();
|