@vellumai/assistant 0.5.11 → 0.5.13
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/Dockerfile +42 -9
- package/docs/architecture/integrations.md +34 -32
- package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/node_modules/@vellumai/credential-storage/src/index.ts +1 -1
- package/openapi.yaml +87 -9
- package/package.json +1 -1
- package/src/__tests__/catalog-cache.test.ts +164 -0
- package/src/__tests__/catalog-search.test.ts +61 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
- package/src/__tests__/conversation-error.test.ts +3 -2
- package/src/__tests__/credential-security-invariants.test.ts +9 -15
- package/src/__tests__/credential-vault-unit.test.ts +32 -34
- package/src/__tests__/credential-vault.test.ts +25 -33
- package/src/__tests__/credentials-cli.test.ts +3 -3
- package/src/__tests__/daemon-credential-client.test.ts +2 -2
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/host-bash-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-proxy.test.ts +90 -0
- package/src/__tests__/host-file-proxy.test.ts +89 -0
- package/src/__tests__/integration-status.test.ts +5 -5
- package/src/__tests__/list-messages-attachments.test.ts +171 -0
- package/src/__tests__/mcp-abort-signal.test.ts +205 -0
- package/src/__tests__/messaging-send-tool.test.ts +5 -5
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
- package/src/__tests__/oauth-cli.test.ts +126 -119
- package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/onboarding-template-contract.test.ts +2 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
- package/src/__tests__/slack-share-routes.test.ts +5 -5
- package/src/__tests__/system-prompt.test.ts +39 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
- package/src/cli/AGENTS.md +47 -7
- package/src/cli/commands/browser-relay.ts +2 -17
- package/src/cli/commands/contacts.ts +6 -4
- package/src/cli/commands/conversations.ts +13 -1
- package/src/cli/commands/credential-execution.ts +16 -1
- package/src/cli/commands/credentials.ts +2 -8
- package/src/cli/commands/oauth/__tests__/connect.test.ts +29 -108
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +13 -87
- package/src/cli/commands/oauth/__tests__/mode.test.ts +22 -69
- package/src/cli/commands/oauth/__tests__/ping.test.ts +20 -79
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
- package/src/cli/commands/oauth/__tests__/status.test.ts +12 -40
- package/src/cli/commands/oauth/__tests__/token.test.ts +3 -50
- package/src/cli/commands/oauth/apps.ts +63 -44
- package/src/cli/commands/oauth/connect.ts +187 -155
- package/src/cli/commands/oauth/disconnect.ts +27 -75
- package/src/cli/commands/oauth/index.ts +36 -46
- package/src/cli/commands/oauth/mode.ts +22 -34
- package/src/cli/commands/oauth/ping.ts +19 -45
- package/src/cli/commands/oauth/providers.ts +569 -62
- package/src/cli/commands/oauth/request.ts +36 -48
- package/src/cli/commands/oauth/shared.ts +1 -19
- package/src/cli/commands/oauth/status.ts +14 -25
- package/src/cli/commands/oauth/token.ts +25 -34
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/cli/commands/platform/connect.ts +104 -0
- package/src/cli/commands/platform/disconnect.ts +118 -0
- package/src/cli/commands/{platform.ts → platform/index.ts} +108 -38
- package/src/cli/commands/sequence.ts +5 -4
- package/src/cli/commands/shotgun.ts +16 -0
- package/src/cli/commands/skills.ts +173 -41
- package/src/cli/commands/usage.ts +5 -11
- package/src/cli/lib/daemon-credential-client.ts +22 -38
- package/src/cli/program.ts +1 -1
- package/src/config/assistant-feature-flags.ts +3 -7
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/conversations/SKILL.md +20 -0
- package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
- package/src/config/bundled-skills/gmail/SKILL.md +13 -13
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -7
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/bundled-tool-registry.ts +5 -0
- package/src/config/feature-flag-registry.json +2 -2
- package/src/credential-execution/client.ts +15 -3
- package/src/daemon/conversation-agent-loop.ts +2 -0
- package/src/daemon/conversation-error.ts +36 -6
- package/src/daemon/conversation-messaging.ts +9 -0
- package/src/daemon/conversation-runtime-assembly.ts +33 -0
- package/src/daemon/conversation-surfaces.ts +120 -14
- package/src/daemon/conversation.ts +5 -0
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/handlers/skills.ts +148 -3
- package/src/daemon/host-bash-proxy.ts +16 -0
- package/src/daemon/host-cu-proxy.ts +16 -0
- package/src/daemon/host-file-proxy.ts +16 -0
- package/src/daemon/lifecycle.ts +56 -5
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/message-types/guardian-actions.ts +2 -0
- package/src/daemon/message-types/host-bash.ts +6 -1
- package/src/daemon/message-types/host-cu.ts +6 -1
- package/src/daemon/message-types/host-file.ts +6 -1
- package/src/daemon/message-types/integrations.ts +0 -1
- package/src/daemon/server.ts +29 -2
- package/src/hooks/cli.ts +74 -0
- package/src/inbound/platform-callback-registration.ts +7 -12
- package/src/index.ts +0 -12
- package/src/mcp/client.ts +6 -1
- package/src/mcp/manager.ts +2 -1
- package/src/memory/conversation-crud.ts +92 -3
- package/src/memory/conversation-key-store.ts +26 -0
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/db-init.ts +16 -0
- package/src/memory/journal-memory.ts +8 -2
- package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
- package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
- package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
- package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/oauth.ts +11 -0
- package/src/messaging/provider.ts +13 -12
- package/src/messaging/providers/gmail/adapter.ts +44 -35
- package/src/messaging/providers/slack/adapter.ts +63 -33
- package/src/messaging/providers/telegram-bot/adapter.ts +6 -8
- package/src/messaging/providers/whatsapp/adapter.ts +6 -8
- package/src/notifications/adapters/telegram.ts +78 -2
- package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
- package/src/oauth/byo-connection.test.ts +22 -24
- package/src/oauth/connect-orchestrator.ts +37 -76
- package/src/oauth/connect-types.ts +7 -65
- package/src/oauth/connection-resolver.test.ts +13 -13
- package/src/oauth/connection-resolver.ts +3 -4
- package/src/oauth/identity-verifier.ts +177 -0
- package/src/oauth/oauth-store.ts +228 -3
- package/src/oauth/platform-connection.test.ts +56 -6
- package/src/oauth/platform-connection.ts +8 -1
- package/src/oauth/seed-providers.ts +247 -34
- package/src/permissions/checker.ts +127 -1
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +54 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -5
- package/src/providers/anthropic/client.ts +2 -33
- package/src/runtime/guardian-action-service.ts +7 -2
- package/src/runtime/http-server.ts +12 -18
- package/src/runtime/http-types.ts +8 -1
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/conversation-management-routes.ts +31 -0
- package/src/runtime/routes/conversation-routes.ts +79 -4
- package/src/runtime/routes/guardian-action-routes.ts +15 -2
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
- package/src/runtime/routes/integrations/slack/share.ts +1 -1
- package/src/runtime/routes/oauth-apps.ts +2 -1
- package/src/runtime/routes/secret-routes.ts +45 -15
- package/src/runtime/routes/settings-routes.ts +12 -19
- package/src/runtime/routes/skills-routes.ts +45 -4
- package/src/schedule/integration-status.ts +2 -2
- package/src/security/ces-rpc-credential-backend.ts +19 -16
- package/src/security/oauth-completion-page.ts +153 -0
- package/src/security/oauth2.ts +3 -17
- package/src/security/secure-keys.ts +207 -7
- package/src/security/token-manager.ts +3 -6
- package/src/signals/bash.ts +6 -1
- package/src/skills/catalog-cache.ts +44 -0
- package/src/skills/catalog-search.ts +18 -0
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/credentials/post-connect-hooks.ts +1 -1
- package/src/tools/credentials/vault.ts +34 -45
- package/src/tools/host-terminal/host-shell.ts +16 -3
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/skills/sandbox-runner.ts +16 -3
- package/src/tools/terminal/shell.ts +16 -3
- package/src/util/logger.ts +11 -1
- package/src/util/platform.ts +1 -91
- package/src/util/sentry-log-stream.ts +51 -0
- package/src/watcher/providers/github.ts +2 -2
- package/src/watcher/providers/gmail.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +1 -1
- package/src/watcher/providers/linear.ts +2 -2
- package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
- package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/cli/commands/oauth/connections.ts +0 -255
- package/src/oauth/provider-behaviors.ts +0 -634
|
@@ -483,13 +483,9 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
483
483
|
});
|
|
484
484
|
|
|
485
485
|
test("upsertCredentialMetadata does not accept oauth2ClientSecret or other OAuth fields", () => {
|
|
486
|
-
const record = upsertCredentialMetadata(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
{
|
|
490
|
-
allowedTools: ["api_request"],
|
|
491
|
-
},
|
|
492
|
-
);
|
|
486
|
+
const record = upsertCredentialMetadata("google", "access_token", {
|
|
487
|
+
allowedTools: ["api_request"],
|
|
488
|
+
});
|
|
493
489
|
expect("oauth2ClientSecret" in record).toBe(false);
|
|
494
490
|
expect("oauth2TokenUrl" in record).toBe(false);
|
|
495
491
|
expect("oauth2ClientId" in record).toBe(false);
|
|
@@ -497,14 +493,14 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
497
493
|
|
|
498
494
|
test("client secret is read from secure store, not metadata", async () => {
|
|
499
495
|
await setSecureKeyAsync(
|
|
500
|
-
credentialKey("
|
|
496
|
+
credentialKey("google", "client_secret"),
|
|
501
497
|
"my-secret",
|
|
502
498
|
);
|
|
503
|
-
upsertCredentialMetadata("
|
|
499
|
+
upsertCredentialMetadata("google", "access_token", {
|
|
504
500
|
allowedTools: ["api_request"],
|
|
505
501
|
});
|
|
506
502
|
|
|
507
|
-
const meta = getCredentialMetadata("
|
|
503
|
+
const meta = getCredentialMetadata("google", "access_token");
|
|
508
504
|
expect(meta).toBeDefined();
|
|
509
505
|
expect("oauth2ClientSecret" in meta!).toBe(false);
|
|
510
506
|
// OAuth-specific fields are no longer in metadata (v5)
|
|
@@ -513,9 +509,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
513
509
|
|
|
514
510
|
// Secret is in secure store
|
|
515
511
|
expect(
|
|
516
|
-
await getSecureKeyAsync(
|
|
517
|
-
credentialKey("integration:google", "client_secret"),
|
|
518
|
-
),
|
|
512
|
+
await getSecureKeyAsync(credentialKey("google", "client_secret")),
|
|
519
513
|
).toBe("my-secret");
|
|
520
514
|
});
|
|
521
515
|
|
|
@@ -525,7 +519,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
525
519
|
credentials: [
|
|
526
520
|
{
|
|
527
521
|
credentialId: "cred-v2-secret",
|
|
528
|
-
service: "
|
|
522
|
+
service: "google",
|
|
529
523
|
field: "access_token",
|
|
530
524
|
allowedTools: [],
|
|
531
525
|
allowedDomains: [],
|
|
@@ -543,7 +537,7 @@ describe("Invariant 6: oauth2ClientSecret not in metadata, only in secure store"
|
|
|
543
537
|
"utf-8",
|
|
544
538
|
);
|
|
545
539
|
|
|
546
|
-
const meta = getCredentialMetadata("
|
|
540
|
+
const meta = getCredentialMetadata("google", "access_token");
|
|
547
541
|
expect(meta).toBeDefined();
|
|
548
542
|
expect("oauth2ClientSecret" in meta!).toBe(false);
|
|
549
543
|
|
|
@@ -755,8 +755,8 @@ describe("credential_store tool — prompt action", () => {
|
|
|
755
755
|
describe("credential_store tool — oauth2_connect error paths", () => {
|
|
756
756
|
/** Well-known provider rows returned by the mocked getProvider */
|
|
757
757
|
const wellKnownProviders: Record<string, object> = {
|
|
758
|
-
|
|
759
|
-
key: "
|
|
758
|
+
google: {
|
|
759
|
+
key: "google",
|
|
760
760
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
761
761
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
762
762
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -764,8 +764,8 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
764
764
|
callbackTransport: "loopback",
|
|
765
765
|
loopbackPort: 8756,
|
|
766
766
|
},
|
|
767
|
-
|
|
768
|
-
key: "
|
|
767
|
+
slack: {
|
|
768
|
+
key: "slack",
|
|
769
769
|
authUrl: "https://slack.com/oauth/v2/authorize",
|
|
770
770
|
tokenUrl: "https://slack.com/api/oauth.v2.access",
|
|
771
771
|
defaultScopes: JSON.stringify(["channels:read"]),
|
|
@@ -779,7 +779,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
779
779
|
_setStorePath(STORE_PATH);
|
|
780
780
|
_resetBackend();
|
|
781
781
|
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
782
|
-
// Return well-known provider rows so vault.ts knows
|
|
782
|
+
// Return well-known provider rows so vault.ts knows google/slack are
|
|
783
783
|
// registered, and custom providers return undefined.
|
|
784
784
|
mockGetProvider.mockImplementation(
|
|
785
785
|
(key: string) => wellKnownProviders[key] ?? undefined,
|
|
@@ -880,22 +880,21 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
880
880
|
expect(result.content).toContain("mock-auth-url.example.com");
|
|
881
881
|
});
|
|
882
882
|
|
|
883
|
-
test("
|
|
884
|
-
//
|
|
883
|
+
test("rejects missing client_id for google", async () => {
|
|
884
|
+
// Missing client_id should fail
|
|
885
885
|
const result = await credentialStoreTool.execute(
|
|
886
886
|
{
|
|
887
887
|
action: "oauth2_connect",
|
|
888
|
-
service: "
|
|
888
|
+
service: "google",
|
|
889
889
|
},
|
|
890
890
|
_ctx,
|
|
891
891
|
);
|
|
892
892
|
expect(result.isError).toBe(true);
|
|
893
|
-
// Should NOT require auth_url/token_url/scopes — those are well-known for gmail
|
|
894
893
|
// Should fail on client_id since none is stored
|
|
895
894
|
expect(result.content).toContain("client_id is required");
|
|
896
895
|
});
|
|
897
896
|
|
|
898
|
-
test("resolves slack
|
|
897
|
+
test("resolves slack to its canonical name", async () => {
|
|
899
898
|
const result = await credentialStoreTool.execute(
|
|
900
899
|
{
|
|
901
900
|
action: "oauth2_connect",
|
|
@@ -912,13 +911,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
912
911
|
// and store client_secret in the secure store.
|
|
913
912
|
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
914
913
|
id: "test-app-id",
|
|
915
|
-
providerKey: "
|
|
914
|
+
providerKey: "google",
|
|
916
915
|
clientId: "stored-client-id-123",
|
|
917
916
|
clientSecretCredentialPath: "oauth_app/test-app-id/client_secret",
|
|
918
917
|
createdAt: Date.now(),
|
|
919
918
|
}));
|
|
920
919
|
mockGetProvider.mockImplementation(() => ({
|
|
921
|
-
key: "
|
|
920
|
+
key: "google",
|
|
922
921
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
923
922
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
924
923
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -934,16 +933,16 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
934
933
|
const result = await credentialStoreTool.execute(
|
|
935
934
|
{
|
|
936
935
|
action: "oauth2_connect",
|
|
937
|
-
service: "
|
|
936
|
+
service: "google",
|
|
938
937
|
},
|
|
939
938
|
{ ..._ctx, isInteractive: false },
|
|
940
939
|
);
|
|
941
940
|
|
|
942
941
|
// Should pass client_id and client_secret checks — the flow proceeds
|
|
943
|
-
// through the channel path (
|
|
942
|
+
// through the channel path (google uses loopback transport so no
|
|
944
943
|
// public ingress URL is needed) and returns the authorization URL.
|
|
945
944
|
expect(result.isError).toBe(false);
|
|
946
|
-
expect(result.content).toContain("To connect
|
|
945
|
+
expect(result.content).toContain("To connect google, open this link");
|
|
947
946
|
expect(result.content).not.toContain("client_id is required");
|
|
948
947
|
expect(result.content).not.toContain("client_secret is required");
|
|
949
948
|
|
|
@@ -958,13 +957,10 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
958
957
|
// most-recent-app heuristic) so the secret comes from the correct app.
|
|
959
958
|
mockGetAppByProviderAndClientId.mockImplementation(
|
|
960
959
|
(providerKey: string, cId: string) => {
|
|
961
|
-
if (
|
|
962
|
-
providerKey === "integration:google" &&
|
|
963
|
-
cId === "caller-supplied-client-id"
|
|
964
|
-
) {
|
|
960
|
+
if (providerKey === "google" && cId === "caller-supplied-client-id") {
|
|
965
961
|
return {
|
|
966
962
|
id: "matched-app-id",
|
|
967
|
-
providerKey: "
|
|
963
|
+
providerKey: "google",
|
|
968
964
|
clientId: "caller-supplied-client-id",
|
|
969
965
|
clientSecretCredentialPath:
|
|
970
966
|
"oauth_app/matched-app-id/client_secret",
|
|
@@ -975,7 +971,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
975
971
|
},
|
|
976
972
|
);
|
|
977
973
|
mockGetProvider.mockImplementation(() => ({
|
|
978
|
-
key: "
|
|
974
|
+
key: "google",
|
|
979
975
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
980
976
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
981
977
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -991,7 +987,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
991
987
|
const result = await credentialStoreTool.execute(
|
|
992
988
|
{
|
|
993
989
|
action: "oauth2_connect",
|
|
994
|
-
service: "
|
|
990
|
+
service: "google",
|
|
995
991
|
client_id: "caller-supplied-client-id",
|
|
996
992
|
},
|
|
997
993
|
{ ..._ctx, isInteractive: false },
|
|
@@ -999,7 +995,7 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
999
995
|
|
|
1000
996
|
// Should succeed — client_secret resolved from the matched app
|
|
1001
997
|
expect(result.isError).toBe(false);
|
|
1002
|
-
expect(result.content).toContain("To connect
|
|
998
|
+
expect(result.content).toContain("To connect google, open this link");
|
|
1003
999
|
// getMostRecentAppByProvider should NOT have been called since client_id was known
|
|
1004
1000
|
expect(mockGetMostRecentAppByProvider).not.toHaveBeenCalled();
|
|
1005
1001
|
|
|
@@ -1013,13 +1009,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
1013
1009
|
// use getMostRecentAppByProvider (the fallback heuristic).
|
|
1014
1010
|
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
1015
1011
|
id: "recent-app-id",
|
|
1016
|
-
providerKey: "
|
|
1012
|
+
providerKey: "google",
|
|
1017
1013
|
clientId: "recent-client-id",
|
|
1018
1014
|
clientSecretCredentialPath: "oauth_app/recent-app-id/client_secret",
|
|
1019
1015
|
createdAt: Date.now(),
|
|
1020
1016
|
}));
|
|
1021
1017
|
mockGetProvider.mockImplementation(() => ({
|
|
1022
|
-
key: "
|
|
1018
|
+
key: "google",
|
|
1023
1019
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1024
1020
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1025
1021
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
@@ -1035,13 +1031,13 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
1035
1031
|
const result = await credentialStoreTool.execute(
|
|
1036
1032
|
{
|
|
1037
1033
|
action: "oauth2_connect",
|
|
1038
|
-
service: "
|
|
1034
|
+
service: "google",
|
|
1039
1035
|
},
|
|
1040
1036
|
{ ..._ctx, isInteractive: false },
|
|
1041
1037
|
);
|
|
1042
1038
|
|
|
1043
1039
|
expect(result.isError).toBe(false);
|
|
1044
|
-
expect(result.content).toContain("To connect
|
|
1040
|
+
expect(result.content).toContain("To connect google, open this link");
|
|
1045
1041
|
// getAppByProviderAndClientId should NOT have been called since client_id was unknown
|
|
1046
1042
|
expect(mockGetAppByProviderAndClientId).not.toHaveBeenCalled();
|
|
1047
1043
|
|
|
@@ -1056,23 +1052,24 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
1056
1052
|
// report the missing secret error.
|
|
1057
1053
|
mockGetAppByProviderAndClientId.mockImplementation(() => undefined);
|
|
1058
1054
|
mockGetProvider.mockImplementation(() => ({
|
|
1059
|
-
key: "
|
|
1055
|
+
key: "google",
|
|
1060
1056
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1061
1057
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1062
1058
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
1059
|
+
requiresClientSecret: 1,
|
|
1063
1060
|
}));
|
|
1064
1061
|
|
|
1065
1062
|
const result = await credentialStoreTool.execute(
|
|
1066
1063
|
{
|
|
1067
1064
|
action: "oauth2_connect",
|
|
1068
|
-
service: "
|
|
1065
|
+
service: "google",
|
|
1069
1066
|
client_id: "unknown-client-id",
|
|
1070
1067
|
},
|
|
1071
1068
|
_ctx,
|
|
1072
1069
|
);
|
|
1073
1070
|
|
|
1074
1071
|
expect(result.isError).toBe(true);
|
|
1075
|
-
expect(result.content).toContain("client_secret is required for
|
|
1072
|
+
expect(result.content).toContain("client_secret is required for google");
|
|
1076
1073
|
// getMostRecentAppByProvider should NOT have been called
|
|
1077
1074
|
expect(mockGetMostRecentAppByProvider).not.toHaveBeenCalled();
|
|
1078
1075
|
|
|
@@ -1087,27 +1084,28 @@ describe("credential_store tool — oauth2_connect error paths", () => {
|
|
|
1087
1084
|
// guardrail.
|
|
1088
1085
|
mockGetMostRecentAppByProvider.mockImplementation(() => ({
|
|
1089
1086
|
id: "test-app-id-no-secret",
|
|
1090
|
-
providerKey: "
|
|
1087
|
+
providerKey: "google",
|
|
1091
1088
|
clientId: "stored-client-id-456",
|
|
1092
1089
|
createdAt: Date.now(),
|
|
1093
1090
|
}));
|
|
1094
1091
|
mockGetProvider.mockImplementation(() => ({
|
|
1095
|
-
key: "
|
|
1092
|
+
key: "google",
|
|
1096
1093
|
authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
1097
1094
|
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
1098
1095
|
defaultScopes: JSON.stringify(["https://mail.google.com/"]),
|
|
1096
|
+
requiresClientSecret: 1,
|
|
1099
1097
|
}));
|
|
1100
1098
|
|
|
1101
1099
|
const result = await credentialStoreTool.execute(
|
|
1102
1100
|
{
|
|
1103
1101
|
action: "oauth2_connect",
|
|
1104
|
-
service: "
|
|
1102
|
+
service: "google",
|
|
1105
1103
|
},
|
|
1106
1104
|
_ctx,
|
|
1107
1105
|
);
|
|
1108
1106
|
|
|
1109
1107
|
expect(result.isError).toBe(true);
|
|
1110
|
-
expect(result.content).toContain("client_secret is required for
|
|
1108
|
+
expect(result.content).toContain("client_secret is required for google");
|
|
1111
1109
|
|
|
1112
1110
|
// Reset mocks
|
|
1113
1111
|
mockGetMostRecentAppByProvider.mockImplementation(() => undefined);
|
|
@@ -736,7 +736,7 @@ describe("credential_store tool", () => {
|
|
|
736
736
|
await credentialStoreTool.execute(
|
|
737
737
|
{
|
|
738
738
|
action: "store",
|
|
739
|
-
service: "
|
|
739
|
+
service: "google",
|
|
740
740
|
field: "api_key",
|
|
741
741
|
value: "test-value",
|
|
742
742
|
},
|
|
@@ -744,9 +744,9 @@ describe("credential_store tool", () => {
|
|
|
744
744
|
);
|
|
745
745
|
|
|
746
746
|
// Simulate an active OAuth connection for this service
|
|
747
|
-
mockConnections.set("
|
|
747
|
+
mockConnections.set("google", {
|
|
748
748
|
id: "conn-gmail",
|
|
749
|
-
providerKey: "
|
|
749
|
+
providerKey: "google",
|
|
750
750
|
oauthAppId: "app-gmail",
|
|
751
751
|
expiresAt: Date.now() + 3600_000,
|
|
752
752
|
});
|
|
@@ -754,7 +754,7 @@ describe("credential_store tool", () => {
|
|
|
754
754
|
const result = await credentialStoreTool.execute(
|
|
755
755
|
{
|
|
756
756
|
action: "delete",
|
|
757
|
-
service: "
|
|
757
|
+
service: "google",
|
|
758
758
|
field: "api_key",
|
|
759
759
|
},
|
|
760
760
|
_ctx,
|
|
@@ -764,9 +764,7 @@ describe("credential_store tool", () => {
|
|
|
764
764
|
expect(result.content).toContain("Deleted credential");
|
|
765
765
|
// Verify disconnectOAuthProvider was called with the service name
|
|
766
766
|
expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
|
|
767
|
-
expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
|
|
768
|
-
"integration:google",
|
|
769
|
-
);
|
|
767
|
+
expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith("google");
|
|
770
768
|
});
|
|
771
769
|
});
|
|
772
770
|
|
|
@@ -1354,7 +1352,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1354
1352
|
}
|
|
1355
1353
|
|
|
1356
1354
|
test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
|
|
1357
|
-
await setupService("
|
|
1355
|
+
await setupService("google");
|
|
1358
1356
|
|
|
1359
1357
|
let resolveRefresh!: (value: {
|
|
1360
1358
|
accessToken: string;
|
|
@@ -1378,9 +1376,9 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1378
1376
|
|
|
1379
1377
|
// Launch 3 concurrent withValidToken calls — all will get a non-expired
|
|
1380
1378
|
// token first, call the callback, get a 401, and then try to refresh.
|
|
1381
|
-
const p1 = withValidToken("
|
|
1382
|
-
const p2 = withValidToken("
|
|
1383
|
-
const p3 = withValidToken("
|
|
1379
|
+
const p1 = withValidToken("google", callback);
|
|
1380
|
+
const p2 = withValidToken("google", callback);
|
|
1381
|
+
const p3 = withValidToken("google", callback);
|
|
1384
1382
|
|
|
1385
1383
|
// Let the event loop tick so all 3 calls enter the 401 retry path
|
|
1386
1384
|
await new Promise((r) => setTimeout(r, 10));
|
|
@@ -1402,8 +1400,8 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1402
1400
|
});
|
|
1403
1401
|
|
|
1404
1402
|
test("concurrent refreshes for different services proceed independently", async () => {
|
|
1405
|
-
await setupService("
|
|
1406
|
-
await setupService("
|
|
1403
|
+
await setupService("google");
|
|
1404
|
+
await setupService("slack");
|
|
1407
1405
|
|
|
1408
1406
|
let resolveGmail!: (value: {
|
|
1409
1407
|
accessToken: string;
|
|
@@ -1447,8 +1445,8 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1447
1445
|
return `slack-${token}`;
|
|
1448
1446
|
};
|
|
1449
1447
|
|
|
1450
|
-
const p1 = withValidToken("
|
|
1451
|
-
const p2 = withValidToken("
|
|
1448
|
+
const p1 = withValidToken("google", gmailCallback);
|
|
1449
|
+
const p2 = withValidToken("slack", slackCallback);
|
|
1452
1450
|
|
|
1453
1451
|
await new Promise((r) => setTimeout(r, 10));
|
|
1454
1452
|
|
|
@@ -1466,7 +1464,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1466
1464
|
});
|
|
1467
1465
|
|
|
1468
1466
|
test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
|
|
1469
|
-
await setupService("
|
|
1467
|
+
await setupService("google");
|
|
1470
1468
|
|
|
1471
1469
|
let refreshCount = 0;
|
|
1472
1470
|
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
@@ -1480,25 +1478,19 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1480
1478
|
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1481
1479
|
|
|
1482
1480
|
// First call triggers a refresh (old token → 401 → refresh → token-1)
|
|
1483
|
-
const r1 = await withValidToken(
|
|
1484
|
-
"
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
return token;
|
|
1488
|
-
},
|
|
1489
|
-
);
|
|
1481
|
+
const r1 = await withValidToken("google", async (token: string) => {
|
|
1482
|
+
if (token !== "token-1") throw err401;
|
|
1483
|
+
return token;
|
|
1484
|
+
});
|
|
1490
1485
|
expect(r1).toBe("token-1");
|
|
1491
1486
|
expect(refreshCount).toBe(1);
|
|
1492
1487
|
|
|
1493
1488
|
// Second call also triggers a 401 to verify dedup state was cleaned up
|
|
1494
1489
|
// and a new refresh is allowed (not deduplicated with the first).
|
|
1495
|
-
const r2 = await withValidToken(
|
|
1496
|
-
"
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
return token;
|
|
1500
|
-
},
|
|
1501
|
-
);
|
|
1490
|
+
const r2 = await withValidToken("google", async (token: string) => {
|
|
1491
|
+
if (token !== "token-2") throw err401;
|
|
1492
|
+
return token;
|
|
1493
|
+
});
|
|
1502
1494
|
expect(r2).toBe("token-2");
|
|
1503
1495
|
// Second refresh should have happened (not deduplicated with the first,
|
|
1504
1496
|
// since the first already completed)
|
|
@@ -1506,7 +1498,7 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1506
1498
|
});
|
|
1507
1499
|
|
|
1508
1500
|
test("deduplication propagates refresh errors to all waiting callers", async () => {
|
|
1509
|
-
await setupService("
|
|
1501
|
+
await setupService("google");
|
|
1510
1502
|
|
|
1511
1503
|
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1512
1504
|
Promise.reject(
|
|
@@ -1524,8 +1516,8 @@ describe("withValidToken refresh deduplication", () => {
|
|
|
1524
1516
|
};
|
|
1525
1517
|
|
|
1526
1518
|
// Launch 2 concurrent calls — both should fail with the same error
|
|
1527
|
-
const p1 = withValidToken("
|
|
1528
|
-
const p2 = withValidToken("
|
|
1519
|
+
const p1 = withValidToken("google", callback);
|
|
1520
|
+
const p2 = withValidToken("google", callback);
|
|
1529
1521
|
|
|
1530
1522
|
const results = await Promise.allSettled([p1, p2]);
|
|
1531
1523
|
|
|
@@ -1059,7 +1059,7 @@ describe("assistant credentials CLI", () => {
|
|
|
1059
1059
|
const setResult = await runCli([
|
|
1060
1060
|
"set",
|
|
1061
1061
|
"--service",
|
|
1062
|
-
"
|
|
1062
|
+
"google",
|
|
1063
1063
|
"--field",
|
|
1064
1064
|
"client_secret",
|
|
1065
1065
|
"secret123",
|
|
@@ -1068,13 +1068,13 @@ describe("assistant credentials CLI", () => {
|
|
|
1068
1068
|
expect(setResult.exitCode).toBe(0);
|
|
1069
1069
|
const setParsed = JSON.parse(setResult.stdout);
|
|
1070
1070
|
expect(setParsed.ok).toBe(true);
|
|
1071
|
-
expect(setParsed.service).toBe("
|
|
1071
|
+
expect(setParsed.service).toBe("google");
|
|
1072
1072
|
expect(setParsed.field).toBe("client_secret");
|
|
1073
1073
|
|
|
1074
1074
|
const revealResult = await runCli([
|
|
1075
1075
|
"reveal",
|
|
1076
1076
|
"--service",
|
|
1077
|
-
"
|
|
1077
|
+
"google",
|
|
1078
1078
|
"--field",
|
|
1079
1079
|
"client_secret",
|
|
1080
1080
|
"--json",
|
|
@@ -110,13 +110,13 @@ describe("daemon credential read requests", () => {
|
|
|
110
110
|
|
|
111
111
|
test("preserves compound credential service names on metadata reads", async () => {
|
|
112
112
|
const result = await getSecureKeyResultViaDaemon(
|
|
113
|
-
credentialKey("
|
|
113
|
+
credentialKey("google", "client_secret"),
|
|
114
114
|
);
|
|
115
115
|
|
|
116
116
|
expect(result).toEqual({ value: "secret-value", unreachable: false });
|
|
117
117
|
expect(getRequestBody()).toEqual({
|
|
118
118
|
type: "credential",
|
|
119
|
-
name: "
|
|
119
|
+
name: "google:client_secret",
|
|
120
120
|
reveal: true,
|
|
121
121
|
});
|
|
122
122
|
});
|
|
@@ -50,6 +50,13 @@ describe("first-greeting", () => {
|
|
|
50
50
|
expect(isWakeUpGreeting("Wake Up, My Friend.", 0)).toBe(true);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("returns true for punctuation variations", () => {
|
|
54
|
+
writeFileSync(join(tempDir, "BOOTSTRAP.md"), "bootstrap content");
|
|
55
|
+
expect(isWakeUpGreeting("Wake up, my friend!", 0)).toBe(true);
|
|
56
|
+
expect(isWakeUpGreeting("Wake up, my friend?", 0)).toBe(true);
|
|
57
|
+
expect(isWakeUpGreeting("Wake up, my friend", 0)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
53
60
|
it("returns false when content doesn't match wake-up greeting", () => {
|
|
54
61
|
writeFileSync(join(tempDir, "BOOTSTRAP.md"), "bootstrap content");
|
|
55
62
|
expect(isWakeUpGreeting("Hello", 0)).toBe(false);
|
|
@@ -215,6 +215,29 @@ describe("HostBashProxy", () => {
|
|
|
215
215
|
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
test("sends host_bash_cancel to client on abort", async () => {
|
|
219
|
+
setup();
|
|
220
|
+
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
const resultPromise = proxy.request(
|
|
223
|
+
{ command: "echo hello" },
|
|
224
|
+
"session-1",
|
|
225
|
+
controller.signal,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
229
|
+
const requestId = sent.requestId as string;
|
|
230
|
+
|
|
231
|
+
controller.abort();
|
|
232
|
+
await resultPromise;
|
|
233
|
+
|
|
234
|
+
// Second message should be the cancel
|
|
235
|
+
expect(sentMessages).toHaveLength(2);
|
|
236
|
+
const cancelMsg = sentMessages[1] as Record<string, unknown>;
|
|
237
|
+
expect(cancelMsg.type).toBe("host_bash_cancel");
|
|
238
|
+
expect(cancelMsg.requestId).toBe(requestId);
|
|
239
|
+
});
|
|
240
|
+
|
|
218
241
|
test("returns immediately if signal already aborted", async () => {
|
|
219
242
|
setup();
|
|
220
243
|
|
|
@@ -272,6 +295,62 @@ describe("HostBashProxy", () => {
|
|
|
272
295
|
// The promise should reject since dispose rejects pending
|
|
273
296
|
expect(resultPromise).rejects.toThrow("Host bash proxy disposed");
|
|
274
297
|
});
|
|
298
|
+
|
|
299
|
+
test("sends host_bash_cancel for each pending request on dispose", () => {
|
|
300
|
+
setup();
|
|
301
|
+
|
|
302
|
+
const p1 = proxy.request({ command: "echo a" }, "session-1");
|
|
303
|
+
const p2 = proxy.request({ command: "echo b" }, "session-1");
|
|
304
|
+
p1.catch(() => {}); // Expected rejection on dispose
|
|
305
|
+
p2.catch(() => {}); // Expected rejection on dispose
|
|
306
|
+
|
|
307
|
+
const requestIds = (sentMessages as Array<Record<string, unknown>>).map(
|
|
308
|
+
(m) => m.requestId as string,
|
|
309
|
+
);
|
|
310
|
+
expect(requestIds).toHaveLength(2);
|
|
311
|
+
|
|
312
|
+
proxy.dispose();
|
|
313
|
+
|
|
314
|
+
// After the 2 request messages, dispose should have sent 2 cancel messages
|
|
315
|
+
const cancelMessages = sentMessages
|
|
316
|
+
.slice(2)
|
|
317
|
+
.filter(
|
|
318
|
+
(m) => (m as Record<string, unknown>).type === "host_bash_cancel",
|
|
319
|
+
) as Array<Record<string, unknown>>;
|
|
320
|
+
expect(cancelMessages).toHaveLength(2);
|
|
321
|
+
expect(cancelMessages.map((m) => m.requestId)).toContain(requestIds[0]);
|
|
322
|
+
expect(cancelMessages.map((m) => m.requestId)).toContain(requestIds[1]);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe("late resolve after abort", () => {
|
|
327
|
+
test("resolve is a no-op after abort (entry already deleted)", async () => {
|
|
328
|
+
setup();
|
|
329
|
+
|
|
330
|
+
const controller = new AbortController();
|
|
331
|
+
const resultPromise = proxy.request(
|
|
332
|
+
{ command: "echo hello" },
|
|
333
|
+
"session-1",
|
|
334
|
+
controller.signal,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
338
|
+
const requestId = sent.requestId as string;
|
|
339
|
+
|
|
340
|
+
controller.abort();
|
|
341
|
+
const result = await resultPromise;
|
|
342
|
+
expect(result.content).toContain("Aborted");
|
|
343
|
+
|
|
344
|
+
// Late resolve should be silently ignored (no throw, no double-resolve)
|
|
345
|
+
proxy.resolve(requestId, {
|
|
346
|
+
stdout: "late",
|
|
347
|
+
stderr: "",
|
|
348
|
+
exitCode: 0,
|
|
349
|
+
timedOut: false,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
353
|
+
});
|
|
275
354
|
});
|
|
276
355
|
|
|
277
356
|
describe("updateSender", () => {
|