@vellumai/assistant 0.4.50 → 0.4.51
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/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +19 -18
- package/src/__tests__/credential-vault.test.ts +17 -17
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +121 -32
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/oauth-cli.test.ts +286 -60
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +243 -11
- package/src/__tests__/relay-server.test.ts +9 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +71 -16
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +58 -9
- package/src/calls/call-domain.ts +3 -4
- package/src/calls/relay-server.ts +1 -1
- package/src/calls/twilio-config.ts +4 -3
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +7 -5
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/oauth/apps.ts +40 -11
- package/src/cli/commands/oauth/connections.ts +66 -30
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +3 -3
- package/src/cli.ts +16 -12
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/loader.ts +6 -42
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/daemon/config-watcher.ts +8 -13
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/message-types/sessions.ts +0 -42
- package/src/daemon/server.ts +0 -6
- package/src/daemon/session-slash.ts +3 -5
- package/src/email/providers/index.ts +2 -2
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/conversation-queries.ts +3 -80
- package/src/memory/db-init.ts +4 -0
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +1 -1
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +32 -24
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +83 -17
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +208 -8
- package/src/oauth/token-persistence.ts +20 -13
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +71 -29
- package/src/runtime/invite-service.ts +40 -22
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +47 -10
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/workspace-routes.ts +7 -3
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +19 -16
- package/src/security/token-manager.ts +13 -6
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/tools/credentials/vault.ts +22 -5
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +2 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
|
@@ -151,7 +151,7 @@ sequenceDiagram
|
|
|
151
151
|
|
|
152
152
|
Note over UI,API: Tool Execution Flow
|
|
153
153
|
Tool->>TokenMgr: withValidToken("gmail", callback)
|
|
154
|
-
TokenMgr->>Store: getConnectionByProvider("integration:
|
|
154
|
+
TokenMgr->>Store: getConnectionByProvider("integration:google")
|
|
155
155
|
TokenMgr->>Vault: getSecureKey("oauth_connection/{conn.id}/access_token")
|
|
156
156
|
TokenMgr->>Store: check oauth_connections.expires_at
|
|
157
157
|
alt Token expired
|
|
@@ -228,7 +228,7 @@ The OAuth extensibility layer makes adding a new OAuth provider a declarative op
|
|
|
228
228
|
|
|
229
229
|
Protocol fields (`authUrl`, `tokenUrl`, `defaultScopes`, `scopePolicy`, `callbackTransport`) are stored in the `oauth_providers` database table rather than in code.
|
|
230
230
|
|
|
231
|
-
Registered providers: `integration:
|
|
231
|
+
Registered providers: `integration:google`, `integration:slack`, `integration:notion`. Short aliases (e.g. `gmail`, `slack`) are resolved via `resolveService()`.
|
|
232
232
|
|
|
233
233
|
### Scope Policy Engine
|
|
234
234
|
|
|
@@ -55,11 +55,11 @@ graph LR
|
|
|
55
55
|
|
|
56
56
|
### TypeScript side (runtime + gateway)
|
|
57
57
|
|
|
58
|
-
| File | Role
|
|
59
|
-
| -------------------------------------------------- |
|
|
60
|
-
| `assistant/src/security/keychain-broker-client.ts` | Async UDS client for the runtime. Persistent socket connection, request/response correlation, auth token caching with auto-refresh on `UNAUTHORIZED`. Falls back gracefully (returns safe defaults, never throws).
|
|
61
|
-
| `assistant/src/security/secure-keys.ts` | Unified API surface. Sync variants use encrypted store only. Async variants (`getSecureKeyAsync`, `setSecureKeyAsync`, `deleteSecureKeyAsync`)
|
|
62
|
-
| `gateway/src/credential-reader.ts` | Read-only credential reader. Tries broker via native async UDS connection (`node:net`), falls back to encrypted store. All public credential read functions are async.
|
|
58
|
+
| File | Role |
|
|
59
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `assistant/src/security/keychain-broker-client.ts` | Async UDS client for the runtime. Persistent socket connection, request/response correlation, auth token caching with auto-refresh on `UNAUTHORIZED`. Falls back gracefully (returns safe defaults, never throws). |
|
|
61
|
+
| `assistant/src/security/secure-keys.ts` | Unified API surface. Sync variants use encrypted store only. Async variants (`getSecureKeyAsync`, `setSecureKeyAsync`, `deleteSecureKeyAsync`) check the encrypted store first for reads (instant), falling back to the broker for keys that may exist only in the macOS Keychain. **Writes** go to both stores; return `false` on broker failure (no encrypted-store fallback). **Deletes** return `"deleted"`, `"not-found"`, or `"error"` to let callers distinguish idempotent no-ops from real failures. |
|
|
62
|
+
| `gateway/src/credential-reader.ts` | Read-only credential reader. Tries broker via native async UDS connection (`node:net`), falls back to encrypted store. All public credential read functions are async. |
|
|
63
63
|
|
|
64
64
|
## Message Contract
|
|
65
65
|
|
|
@@ -181,6 +181,6 @@ Sync APIs are acceptable for startup paths (e.g. provider initialization, config
|
|
|
181
181
|
|
|
182
182
|
## Migration
|
|
183
183
|
|
|
184
|
-
Existing encrypted store keys remain accessible — the encrypted store
|
|
184
|
+
Existing encrypted store keys remain accessible — async reads check the encrypted store **first** (instant), falling back to the broker for keys that may exist only in the macOS Keychain. Successful writes from async code paths go to both the broker (keychain) and the encrypted store, keeping both in sync. If a broker write or delete fails, the operation returns `false` without falling back to the encrypted store alone, preventing stale divergence. Callers must inspect the boolean return value and handle failures (typically by logging a warning). There is no one-time migration step required.
|
|
185
185
|
|
|
186
186
|
The old `keychain.ts` module (which called `/usr/bin/security` CLI directly) has been deleted. The old keychain-to-encrypted migration code has been removed. All keychain access now flows exclusively through the broker.
|
package/knip.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entry": ["src/**/*.test.ts", "src/**/__tests__/**/*.ts", "scripts/**/*.ts"],
|
|
3
|
+
"project": ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts"],
|
|
4
|
+
"ignore": [
|
|
5
|
+
"src/browser-extension-relay/client.ts",
|
|
6
|
+
"src/contacts/index.ts",
|
|
7
|
+
"src/daemon/main.ts",
|
|
8
|
+
"src/daemon/tls-certs.ts",
|
|
9
|
+
"src/errors.ts",
|
|
10
|
+
"src/events/index.ts",
|
|
11
|
+
"src/followups/index.ts",
|
|
12
|
+
"src/playbooks/index.ts",
|
|
13
|
+
"src/runtime/auth/index.ts",
|
|
14
|
+
"src/tasks/candidate-store.ts",
|
|
15
|
+
"src/tools/browser/api-map.ts",
|
|
16
|
+
"src/tools/browser/auto-navigate.ts",
|
|
17
|
+
"src/tools/browser/headless-browser.ts",
|
|
18
|
+
"src/tools/browser/recording-store.ts",
|
|
19
|
+
"src/tools/tasks/index.ts"
|
|
20
|
+
],
|
|
21
|
+
"ignoreDependencies": [
|
|
22
|
+
"@hono/node-server",
|
|
23
|
+
"@vellumai/cli",
|
|
24
|
+
"esbuild",
|
|
25
|
+
"hono",
|
|
26
|
+
"ink",
|
|
27
|
+
"preact",
|
|
28
|
+
"quicktype-core",
|
|
29
|
+
"tree-sitter-bash",
|
|
30
|
+
"typescript-json-schema"
|
|
31
|
+
]
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vellumai/assistant",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -16,13 +16,14 @@
|
|
|
16
16
|
"format": "prettier --write .",
|
|
17
17
|
"format:check": "prettier --check .",
|
|
18
18
|
"lint": "eslint",
|
|
19
|
+
"lint:unused": "knip --include files,dependencies,unlisted",
|
|
19
20
|
"typecheck": "bunx tsc --noEmit",
|
|
20
21
|
"test": "bash scripts/test.sh",
|
|
21
22
|
"test:coverage": "COVERAGE=true bash scripts/test.sh",
|
|
22
23
|
"test:stable": "EXCLUDE_EXPERIMENTAL=true bash scripts/test.sh",
|
|
23
24
|
"test:bench": "find src/__tests__ -maxdepth 1 -type f -name '*.benchmark.test.ts' -print0 | xargs -0 -P 1 -I {} bun test {}",
|
|
24
25
|
"test:filesystem-tools": "bash scripts/test-filesystem-tools.sh",
|
|
25
|
-
"postinstall": "cd .. && (git config core.hooksPath || git config core.hooksPath .githooks 2>/dev/null || true) && (bun run meta/feature-flags/sync-bundled-copies.ts 2>/dev/null || true)"
|
|
26
|
+
"postinstall": "cd .. && (git config core.hooksPath || git config core.hooksPath .githooks 2>/dev/null || true) && ([ -f meta/feature-flags/sync-bundled-copies.ts ] && bun run meta/feature-flags/sync-bundled-copies.ts 2>/dev/null || true)"
|
|
26
27
|
},
|
|
27
28
|
"dependencies": {
|
|
28
29
|
"@anthropic-ai/claude-agent-sdk": "^0.2.42",
|
|
@@ -19,13 +19,21 @@ mock.module("../util/logger.js", () => ({
|
|
|
19
19
|
}),
|
|
20
20
|
}));
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
conversationId:
|
|
24
|
-
|
|
25
|
-
})
|
|
22
|
+
const mockGetConversationByKey = mock(
|
|
23
|
+
(_key: string): { conversationId: string } | null => ({
|
|
24
|
+
conversationId: "conv-test-123",
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
26
27
|
|
|
27
28
|
mock.module("../memory/conversation-key-store.js", () => ({
|
|
28
|
-
|
|
29
|
+
getConversationByKey: mockGetConversationByKey,
|
|
30
|
+
// Ensure getOrCreateConversation is never called — BTW must not create
|
|
31
|
+
// persistent conversations.
|
|
32
|
+
getOrCreateConversation: () => {
|
|
33
|
+
throw new Error(
|
|
34
|
+
"getOrCreateConversation must not be called from btw-routes",
|
|
35
|
+
);
|
|
36
|
+
},
|
|
29
37
|
}));
|
|
30
38
|
|
|
31
39
|
const mockAddMessage = mock(() => {});
|
|
@@ -323,4 +331,52 @@ describe("POST /v1/btw", () => {
|
|
|
323
331
|
// processing should still be false — the handler never sets it
|
|
324
332
|
expect(session.processing).toBe(false);
|
|
325
333
|
});
|
|
334
|
+
|
|
335
|
+
// -- No conversation creation (regression) --
|
|
336
|
+
|
|
337
|
+
test("unknown conversationKey does not create a DB conversation", async () => {
|
|
338
|
+
// Simulate a greeting request for a draft thread — no conversation exists.
|
|
339
|
+
mockGetConversationByKey.mockReturnValueOnce(null);
|
|
340
|
+
|
|
341
|
+
const session = makeMockSession();
|
|
342
|
+
const deps = makeSendMessageDeps(session);
|
|
343
|
+
const getOrCreateSessionSpy = deps.getOrCreateSession as ReturnType<
|
|
344
|
+
typeof mock
|
|
345
|
+
>;
|
|
346
|
+
|
|
347
|
+
const res = await callHandler(
|
|
348
|
+
{ conversationKey: "greeting-abc123", content: "Generate a greeting" },
|
|
349
|
+
{ sendMessageDeps: deps },
|
|
350
|
+
);
|
|
351
|
+
await readStream(res);
|
|
352
|
+
|
|
353
|
+
expect(res.status).toBe(200);
|
|
354
|
+
|
|
355
|
+
// Read-only lookup should be called
|
|
356
|
+
expect(mockGetConversationByKey).toHaveBeenCalledWith("greeting-abc123");
|
|
357
|
+
|
|
358
|
+
// Session should be created with the raw key (no DB conversation created)
|
|
359
|
+
expect(getOrCreateSessionSpy).toHaveBeenCalledWith("greeting-abc123");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("known conversationKey resolves to existing conversation ID", async () => {
|
|
363
|
+
mockGetConversationByKey.mockReturnValueOnce({
|
|
364
|
+
conversationId: "existing-conv-id",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const session = makeMockSession();
|
|
368
|
+
const deps = makeSendMessageDeps(session);
|
|
369
|
+
const getOrCreateSessionSpy = deps.getOrCreateSession as ReturnType<
|
|
370
|
+
typeof mock
|
|
371
|
+
>;
|
|
372
|
+
|
|
373
|
+
const res = await callHandler(
|
|
374
|
+
{ conversationKey: "my-thread-key", content: "What is 2+2?" },
|
|
375
|
+
{ sendMessageDeps: deps },
|
|
376
|
+
);
|
|
377
|
+
await readStream(res);
|
|
378
|
+
|
|
379
|
+
expect(res.status).toBe(200);
|
|
380
|
+
expect(getOrCreateSessionSpy).toHaveBeenCalledWith("existing-conv-id");
|
|
381
|
+
});
|
|
326
382
|
});
|
|
@@ -120,6 +120,14 @@ mock.module("../providers/registry.js", () => ({
|
|
|
120
120
|
initializeProviders: () => {},
|
|
121
121
|
}));
|
|
122
122
|
|
|
123
|
+
mock.module("../signals/mcp-reload.js", () => ({
|
|
124
|
+
handleMcpReloadSignal: () => {},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
mock.module("../signals/confirm.js", () => ({
|
|
128
|
+
handleConfirmationSignal: () => {},
|
|
129
|
+
}));
|
|
130
|
+
|
|
123
131
|
let resetAllowlistCallCount = 0;
|
|
124
132
|
mock.module("../security/secret-allowlist.js", () => ({
|
|
125
133
|
resetAllowlist: () => {
|
|
@@ -236,6 +236,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
|
|
|
236
236
|
"oauth/oauth-store.ts", // OAuth provider disconnect (delete stored tokens)
|
|
237
237
|
"cli/commands/oauth/connections.ts", // CLI OAuth connection delete (legacy credential cleanup)
|
|
238
238
|
"oauth/manual-token-connection.ts", // manual-token provider backfill (keychain credential existence check)
|
|
239
|
+
"cli/commands/doctor.ts", // CLI diagnostic API key verification via secure storage
|
|
239
240
|
]);
|
|
240
241
|
|
|
241
242
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -504,7 +505,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
504
505
|
|
|
505
506
|
test("upsertCredentialMetadata does not accept oauth2ClientSecret or other OAuth fields", () => {
|
|
506
507
|
const record = upsertCredentialMetadata(
|
|
507
|
-
"integration:
|
|
508
|
+
"integration:google",
|
|
508
509
|
"access_token",
|
|
509
510
|
{
|
|
510
511
|
allowedTools: ["api_request"],
|
|
@@ -517,14 +518,14 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
517
518
|
|
|
518
519
|
test("client secret is read from secure store, not metadata", () => {
|
|
519
520
|
setSecureKey(
|
|
520
|
-
credentialKey("integration:
|
|
521
|
+
credentialKey("integration:google", "client_secret"),
|
|
521
522
|
"my-secret",
|
|
522
523
|
);
|
|
523
|
-
upsertCredentialMetadata("integration:
|
|
524
|
+
upsertCredentialMetadata("integration:google", "access_token", {
|
|
524
525
|
allowedTools: ["api_request"],
|
|
525
526
|
});
|
|
526
527
|
|
|
527
|
-
const meta = getCredentialMetadata("integration:
|
|
528
|
+
const meta = getCredentialMetadata("integration:google", "access_token");
|
|
528
529
|
expect(meta).toBeDefined();
|
|
529
530
|
expect("oauth2ClientSecret" in meta!).toBe(false);
|
|
530
531
|
// OAuth-specific fields are no longer in metadata (v5)
|
|
@@ -533,7 +534,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
533
534
|
|
|
534
535
|
// Secret is in secure store
|
|
535
536
|
expect(
|
|
536
|
-
getSecureKey(credentialKey("integration:
|
|
537
|
+
getSecureKey(credentialKey("integration:google", "client_secret")),
|
|
537
538
|
).toBe("my-secret");
|
|
538
539
|
});
|
|
539
540
|
|
|
@@ -543,7 +544,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
543
544
|
credentials: [
|
|
544
545
|
{
|
|
545
546
|
credentialId: "cred-v2-secret",
|
|
546
|
-
service: "integration:
|
|
547
|
+
service: "integration:google",
|
|
547
548
|
field: "access_token",
|
|
548
549
|
allowedTools: [],
|
|
549
550
|
allowedDomains: [],
|
|
@@ -561,7 +562,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
561
562
|
"utf-8",
|
|
562
563
|
);
|
|
563
564
|
|
|
564
|
-
const meta = getCredentialMetadata("integration:
|
|
565
|
+
const meta = getCredentialMetadata("integration:google", "access_token");
|
|
565
566
|
expect(meta).toBeDefined();
|
|
566
567
|
expect("oauth2ClientSecret" in meta!).toBe(false);
|
|
567
568
|
|
|
@@ -531,8 +531,8 @@ describe("credential_store tool — prompt action", () => {
|
|
|
531
531
|
describe("credential_store tool — oauth2_connect error paths", () => {
|
|
532
532
|
/** Well-known provider rows returned by the mocked getProvider */
|
|
533
533
|
const wellKnownProviders: Record<string, object> = {
|
|
534
|
-
"integration:
|
|
535
|
-
key: "integration:
|
|
534
|
+
"integration:google": {
|
|
535
|
+
key: "integration:google",
|
|
536
536
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
537
537
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
538
538
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -624,9 +624,10 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
624
624
|
expect(result.content).toContain("client_id is required");
|
|
625
625
|
});
|
|
626
626
|
|
|
627
|
-
test("
|
|
628
|
-
//
|
|
629
|
-
//
|
|
627
|
+
test("non-interactive loopback oauth2_connect returns deferred auth URL", async () => {
|
|
628
|
+
// After the blanket non-interactive guard was removed (#16337),
|
|
629
|
+
// loopback-transport flows succeed with a deferred auth URL so the
|
|
630
|
+
// agent can present it to the user.
|
|
630
631
|
mockGetProvider.mockImplementation((key: string) => {
|
|
631
632
|
if (key === "custom-svc") {
|
|
632
633
|
return {
|
|
@@ -651,11 +652,11 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
651
652
|
},
|
|
652
653
|
{ ..._ctx, isInteractive: false },
|
|
653
654
|
);
|
|
654
|
-
expect(result.isError).toBe(
|
|
655
|
-
expect(result.content).toContain("
|
|
655
|
+
expect(result.isError).toBe(false);
|
|
656
|
+
expect(result.content).toContain("mock-auth-url.example.com");
|
|
656
657
|
});
|
|
657
658
|
|
|
658
|
-
test("resolves gmail alias to integration:
|
|
659
|
+
test("resolves gmail alias to integration:google", async () => {
|
|
659
660
|
// Even with alias resolution, missing client_id should still fail
|
|
660
661
|
const result = await credentialStoreTool.execute(
|
|
661
662
|
{
|
|
@@ -687,13 +688,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
687
688
|
// and store client_secret in the secure store.
|
|
688
689
|
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
689
690
|
id: "test-app-id",
|
|
690
|
-
providerKey: "integration:
|
|
691
|
+
providerKey: "integration:google",
|
|
691
692
|
clientId: "stored-client-id-123",
|
|
692
693
|
clientSecretCredentialPath: "oauth_app/test-app-id/client_secret",
|
|
693
694
|
createdAt: Date.now(),
|
|
694
695
|
}));
|
|
695
696
|
mockGetProvider.mockImplementation(() => ({
|
|
696
|
-
key: "integration:
|
|
697
|
+
key: "integration:google",
|
|
697
698
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
698
699
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
699
700
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -731,12 +732,12 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
731
732
|
mockGetAppByProviderAndClientId.mockImplementation(
|
|
732
733
|
(providerKey: string, cId: string) => {
|
|
733
734
|
if (
|
|
734
|
-
providerKey === "integration:
|
|
735
|
+
providerKey === "integration:google" &&
|
|
735
736
|
cId === "caller-supplied-client-id"
|
|
736
737
|
) {
|
|
737
738
|
return {
|
|
738
739
|
id: "matched-app-id",
|
|
739
|
-
providerKey: "integration:
|
|
740
|
+
providerKey: "integration:google",
|
|
740
741
|
clientId: "caller-supplied-client-id",
|
|
741
742
|
clientSecretCredentialPath:
|
|
742
743
|
"oauth_app/matched-app-id/client_secret",
|
|
@@ -747,7 +748,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
747
748
|
},
|
|
748
749
|
);
|
|
749
750
|
mockGetProvider.mockImplementation(() => ({
|
|
750
|
-
key: "integration:
|
|
751
|
+
key: "integration:google",
|
|
751
752
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
752
753
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
753
754
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -782,13 +783,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
782
783
|
// use getMostRecentAppByProvider (the fallback heuristic).
|
|
783
784
|
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
784
785
|
id: "recent-app-id",
|
|
785
|
-
providerKey: "integration:
|
|
786
|
+
providerKey: "integration:google",
|
|
786
787
|
clientId: "recent-client-id",
|
|
787
788
|
clientSecretCredentialPath: "oauth_app/recent-app-id/client_secret",
|
|
788
789
|
createdAt: Date.now(),
|
|
789
790
|
}));
|
|
790
791
|
mockGetProvider.mockImplementation(() => ({
|
|
791
|
-
key: "integration:
|
|
792
|
+
key: "integration:google",
|
|
792
793
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
793
794
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
794
795
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -822,7 +823,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
822
823
|
// report the missing secret error.
|
|
823
824
|
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
824
825
|
mockGetProvider.mockImplementation(() => ({
|
|
825
|
-
key: "integration:
|
|
826
|
+
key: "integration:google",
|
|
826
827
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
827
828
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
828
829
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -853,12 +854,12 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
853
854
|
// guardrail.
|
|
854
855
|
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
855
856
|
id: "test-app-id-no-secret",
|
|
856
|
-
providerKey: "integration:
|
|
857
|
+
providerKey: "integration:google",
|
|
857
858
|
clientId: "stored-client-id-456",
|
|
858
859
|
createdAt: Date.now(),
|
|
859
860
|
}));
|
|
860
861
|
mockGetProvider.mockImplementation(() => ({
|
|
861
|
-
key: "integration:
|
|
862
|
+
key: "integration:google",
|
|
862
863
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
863
864
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
864
865
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -735,7 +735,7 @@ describe("credential_store tool", () => {
|
|
|
735
735
|
await credentialStoreTool.execute(
|
|
736
736
|
{
|
|
737
737
|
action: "store",
|
|
738
|
-
service: "integration:
|
|
738
|
+
service: "integration:google",
|
|
739
739
|
field: "api_key",
|
|
740
740
|
value: "test-value",
|
|
741
741
|
},
|
|
@@ -743,9 +743,9 @@ describe("credential_store tool", () => {
|
|
|
743
743
|
);
|
|
744
744
|
|
|
745
745
|
// Simulate an active OAuth connection for this service
|
|
746
|
-
mockConnections.set("integration:
|
|
746
|
+
mockConnections.set("integration:google", {
|
|
747
747
|
id: "conn-gmail",
|
|
748
|
-
providerKey: "integration:
|
|
748
|
+
providerKey: "integration:google",
|
|
749
749
|
oauthAppId: "app-gmail",
|
|
750
750
|
expiresAt: Date.now() + 3600_000,
|
|
751
751
|
});
|
|
@@ -753,7 +753,7 @@ describe("credential_store tool", () => {
|
|
|
753
753
|
const result = await credentialStoreTool.execute(
|
|
754
754
|
{
|
|
755
755
|
action: "delete",
|
|
756
|
-
service: "integration:
|
|
756
|
+
service: "integration:google",
|
|
757
757
|
field: "api_key",
|
|
758
758
|
},
|
|
759
759
|
_ctx,
|
|
@@ -764,7 +764,7 @@ describe("credential_store tool", () => {
|
|
|
764
764
|
// Verify disconnectOAuthProvider was called with the service name
|
|
765
765
|
expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
|
|
766
766
|
expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
|
|
767
|
-
"integration:
|
|
767
|
+
"integration:google",
|
|
768
768
|
);
|
|
769
769
|
});
|
|
770
770
|
});
|
|
@@ -1343,7 +1343,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1343
1343
|
}
|
|
1344
1344
|
|
|
1345
1345
|
test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
|
|
1346
|
-
setupService("integration:
|
|
1346
|
+
setupService("integration:google");
|
|
1347
1347
|
|
|
1348
1348
|
let resolveRefresh!: (value: {
|
|
1349
1349
|
accessToken: string;
|
|
@@ -1367,9 +1367,9 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1367
1367
|
|
|
1368
1368
|
// Launch 3 concurrent withValidToken calls — all will get a non-expired
|
|
1369
1369
|
// token first, call the callback, get a 401, and then try to refresh.
|
|
1370
|
-
const p1 = withValidToken("integration:
|
|
1371
|
-
const p2 = withValidToken("integration:
|
|
1372
|
-
const p3 = withValidToken("integration:
|
|
1370
|
+
const p1 = withValidToken("integration:google", callback);
|
|
1371
|
+
const p2 = withValidToken("integration:google", callback);
|
|
1372
|
+
const p3 = withValidToken("integration:google", callback);
|
|
1373
1373
|
|
|
1374
1374
|
// Let the event loop tick so all 3 calls enter the 401 retry path
|
|
1375
1375
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -1391,7 +1391,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1391
1391
|
});
|
|
1392
1392
|
|
|
1393
1393
|
test("concurrent refreshes for different services proceed independently", async () => {
|
|
1394
|
-
setupService("integration:
|
|
1394
|
+
setupService("integration:google");
|
|
1395
1395
|
setupService("integration:slack");
|
|
1396
1396
|
|
|
1397
1397
|
let resolveGmail!: (value: {
|
|
@@ -1436,7 +1436,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1436
1436
|
return `slack-${token}`;
|
|
1437
1437
|
};
|
|
1438
1438
|
|
|
1439
|
-
const p1 = withValidToken("integration:
|
|
1439
|
+
const p1 = withValidToken("integration:google", gmailCallback);
|
|
1440
1440
|
const p2 = withValidToken("integration:slack", slackCallback);
|
|
1441
1441
|
|
|
1442
1442
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -1455,7 +1455,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1455
1455
|
});
|
|
1456
1456
|
|
|
1457
1457
|
test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
|
|
1458
|
-
setupService("integration:
|
|
1458
|
+
setupService("integration:google");
|
|
1459
1459
|
|
|
1460
1460
|
let refreshCount = 0;
|
|
1461
1461
|
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
@@ -1470,7 +1470,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1470
1470
|
|
|
1471
1471
|
// First call triggers a refresh (old token → 401 → refresh → token-1)
|
|
1472
1472
|
const r1 = await withValidToken(
|
|
1473
|
-
"integration:
|
|
1473
|
+
"integration:google",
|
|
1474
1474
|
async (token: string) => {
|
|
1475
1475
|
if (token !== "token-1") throw err401;
|
|
1476
1476
|
return token;
|
|
@@ -1482,7 +1482,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1482
1482
|
// Second call also triggers a 401 to verify dedup state was cleaned up
|
|
1483
1483
|
// and a new refresh is allowed (not deduplicated with the first).
|
|
1484
1484
|
const r2 = await withValidToken(
|
|
1485
|
-
"integration:
|
|
1485
|
+
"integration:google",
|
|
1486
1486
|
async (token: string) => {
|
|
1487
1487
|
if (token !== "token-2") throw err401;
|
|
1488
1488
|
return token;
|
|
@@ -1495,7 +1495,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1495
1495
|
});
|
|
1496
1496
|
|
|
1497
1497
|
test("deduplication propagates refresh errors to all waiting callers", async () => {
|
|
1498
|
-
setupService("integration:
|
|
1498
|
+
setupService("integration:google");
|
|
1499
1499
|
|
|
1500
1500
|
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1501
1501
|
Promise.reject(
|
|
@@ -1513,8 +1513,8 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1513
1513
|
};
|
|
1514
1514
|
|
|
1515
1515
|
// Launch 2 concurrent calls — both should fail with the same error
|
|
1516
|
-
const p1 = withValidToken("integration:
|
|
1517
|
-
const p2 = withValidToken("integration:
|
|
1516
|
+
const p1 = withValidToken("integration:google", callback);
|
|
1517
|
+
const p2 = withValidToken("integration:google", callback);
|
|
1518
1518
|
|
|
1519
1519
|
const results = await Promise.allSettled([p1, p2]);
|
|
1520
1520
|
|