@vellumai/assistant 0.4.46 → 0.4.49
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/ARCHITECTURE.md +7 -7
- package/README.md +2 -23
- package/docs/architecture/integrations.md +45 -41
- package/docs/architecture/keychain-broker.md +3 -3
- package/docs/architecture/security.md +5 -5
- package/docs/runbook-trusted-contacts.md +3 -8
- package/hook-templates/debug-prompt-logger/hook.json +1 -1
- package/hook-templates/debug-prompt-logger/run.sh +1 -3
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +156 -0
- package/src/__tests__/approval-cascade.test.ts +810 -0
- package/src/__tests__/approval-primitive.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-attachments.test.ts +12 -34
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-guardian.test.ts +0 -2
- package/src/__tests__/channel-readiness-routes.test.ts +35 -25
- package/src/__tests__/channel-readiness-service.test.ts +10 -9
- package/src/__tests__/checker.test.ts +9 -29
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
- package/src/__tests__/computer-use-tools.test.ts +2 -19
- package/src/__tests__/config-watcher.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
- package/src/__tests__/context-image-dimensions.test.ts +332 -0
- package/src/__tests__/context-token-estimator.test.ts +196 -13
- package/src/__tests__/conversation-attention-store.test.ts +0 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +144 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +239 -26
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +111 -7
- package/src/__tests__/credential-vault-unit.test.ts +287 -54
- package/src/__tests__/credential-vault.test.ts +406 -12
- package/src/__tests__/credentials-cli.test.ts +82 -6
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/ephemeral-permissions.test.ts +3 -3
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-cu-proxy.test.ts +629 -0
- package/src/__tests__/host-shell-tool.test.ts +27 -15
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/ingress-url-consistency.test.ts +14 -21
- package/src/__tests__/integration-status.test.ts +38 -25
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/invite-routes-http.test.ts +10 -9
- package/src/__tests__/keychain-broker-client.test.ts +11 -43
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/notification-routing-intent.test.ts +0 -1
- package/src/__tests__/oauth-cli.test.ts +373 -14
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
- package/src/__tests__/oauth-scope-policy.test.ts +4 -6
- package/src/__tests__/oauth-store.test.ts +756 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -1
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +15 -21
- package/src/__tests__/recording-handler.test.ts +3 -4
- package/src/__tests__/registry.test.ts +2 -2
- package/src/__tests__/runtime-events-sse.test.ts +55 -7
- package/src/__tests__/schedule-store.test.ts +0 -1
- package/src/__tests__/scheduler-recurrence.test.ts +0 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/scoped-approval-grants.test.ts +0 -1
- package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-ingress-handler.test.ts +0 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/send-endpoint-busy.test.ts +21 -6
- package/src/__tests__/sequence-store.test.ts +0 -1
- package/src/__tests__/session-init.benchmark.test.ts +4 -5
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skill-include-graph.test.ts +66 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
- package/src/__tests__/skill-load-tool.test.ts +149 -1
- package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
- package/src/__tests__/skills-uninstall.test.ts +3 -3
- package/src/__tests__/skills.test.ts +3 -12
- package/src/__tests__/slack-channel-config.test.ts +76 -11
- package/src/__tests__/slack-share-routes.test.ts +17 -14
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
- package/src/__tests__/terminal-tools.test.ts +4 -3
- package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
- package/src/__tests__/tool-approval-handler.test.ts +0 -1
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
- package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
- package/src/__tests__/trust-store.test.ts +1 -22
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -20
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/agent/ax-tree-compaction.test.ts +235 -0
- package/src/agent/loop.ts +76 -130
- package/src/calls/call-domain.ts +8 -10
- package/src/calls/relay-server.ts +9 -13
- package/src/calls/twilio-config.ts +4 -8
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -1
- package/src/calls/twilio-routes.ts +1 -2
- package/src/calls/voice-ingress-preflight.ts +1 -1
- package/src/cli/commands/browser-relay.ts +46 -15
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/credentials.ts +110 -23
- package/src/cli/commands/oauth/apps.ts +255 -0
- package/src/cli/commands/oauth/connections.ts +299 -0
- package/src/cli/commands/oauth/index.ts +52 -0
- package/src/cli/commands/oauth/providers.ts +242 -0
- package/src/cli/commands/skills.ts +4 -338
- package/src/cli/program.ts +1 -5
- package/src/cli/reference.ts +1 -3
- package/src/cli.ts +3 -2
- package/src/config/assistant-feature-flags.ts +0 -3
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +22 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +90 -44
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +12 -15
- package/src/config/bundled-skills/settings/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +2 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/env-registry.ts +14 -83
- package/src/config/env.ts +11 -50
- package/src/config/feature-flag-registry.json +16 -16
- package/src/config/schema.ts +3 -1
- package/src/config/skills.ts +21 -2
- package/src/context/image-dimensions.ts +229 -0
- package/src/context/token-estimator.ts +75 -12
- package/src/context/window-manager.ts +49 -10
- package/src/daemon/assistant-attachments.ts +1 -13
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-ingress.ts +8 -33
- package/src/daemon/handlers/config-slack-channel.ts +76 -56
- package/src/daemon/handlers/config-telegram.ts +53 -24
- package/src/daemon/handlers/sessions.ts +10 -24
- package/src/daemon/handlers/shared.ts +0 -130
- package/src/daemon/host-cu-proxy.ts +401 -0
- package/src/daemon/lifecycle.ts +39 -63
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/computer-use.ts +2 -119
- package/src/daemon/message-types/host-cu.ts +19 -0
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/server.ts +14 -21
- package/src/daemon/session-agent-loop-handlers.ts +2 -0
- package/src/daemon/session-attachments.ts +1 -2
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-slash.ts +1 -1
- package/src/daemon/session-surfaces.ts +40 -28
- package/src/daemon/session-tool-setup.ts +20 -11
- package/src/daemon/session.ts +139 -16
- package/src/daemon/tool-side-effects.ts +2 -8
- package/src/daemon/watch-handler.ts +2 -2
- package/src/email/providers/index.ts +2 -1
- package/src/events/tool-metrics-listener.ts +2 -2
- package/src/hooks/manager.ts +1 -4
- package/src/inbound/public-ingress-urls.ts +7 -7
- package/src/instrument.ts +15 -1
- package/src/logfire.ts +16 -5
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +26 -3
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/conversation-key-store.ts +21 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/migrations/149-oauth-tables.ts +60 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/guardian.ts +1 -1
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/oauth.ts +65 -0
- package/src/messaging/provider.ts +19 -13
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +283 -122
- package/src/messaging/providers/gmail/people-client.ts +32 -24
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +19 -18
- package/src/messaging/providers/whatsapp/adapter.ts +17 -11
- package/src/messaging/registry.ts +2 -31
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/signal.ts +4 -5
- package/src/oauth/byo-connection.test.ts +537 -0
- package/src/oauth/byo-connection.ts +128 -0
- package/src/oauth/connect-orchestrator.ts +139 -56
- package/src/oauth/connect-types.ts +17 -23
- package/src/oauth/connection-resolver.ts +58 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/manual-token-connection.ts +104 -0
- package/src/oauth/oauth-store.ts +496 -0
- package/src/oauth/platform-connection.test.ts +192 -0
- package/src/oauth/platform-connection.ts +111 -0
- package/src/oauth/provider-behaviors.ts +124 -0
- package/src/oauth/scope-policy.ts +9 -2
- package/src/oauth/seed-providers.ts +161 -0
- package/src/oauth/token-persistence.ts +74 -78
- package/src/permissions/checker.ts +8 -4
- package/src/permissions/defaults.ts +0 -1
- package/src/permissions/prompter.ts +10 -1
- package/src/permissions/trust-store.ts +13 -0
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
- package/src/prompts/system-prompt.ts +70 -45
- package/src/providers/anthropic/client.ts +133 -24
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +1 -27
- package/src/runtime/AGENTS.md +17 -0
- package/src/runtime/auth/route-policy.ts +0 -3
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/channel-reply-delivery.ts +0 -40
- package/src/runtime/gateway-client.ts +0 -7
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +17 -10
- package/src/runtime/http-types.ts +2 -3
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -11
- package/src/runtime/pending-interactions.ts +14 -12
- package/src/runtime/routes/channel-delivery-routes.ts +0 -1
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/conversation-routes.ts +73 -19
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/events-routes.ts +21 -11
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/host-cu-routes.ts +97 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
- package/src/runtime/routes/integrations/slack/share.ts +6 -6
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/log-export-routes.ts +126 -8
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +113 -48
- package/src/runtime/routes/surface-action-routes.ts +1 -1
- package/src/runtime/routes/watch-routes.ts +128 -0
- package/src/schedule/integration-status.ts +10 -8
- package/src/security/credential-key.ts +14 -0
- package/src/security/keychain-broker-client.ts +5 -6
- package/src/security/oauth2.ts +1 -1
- package/src/security/token-manager.ts +145 -43
- package/src/skills/catalog-install.ts +358 -0
- package/src/skills/include-graph.ts +32 -0
- package/src/telegram/bot-username.ts +2 -3
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/browser/network-recorder.ts +1 -1
- package/src/tools/browser/network-recording-types.ts +1 -1
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/definitions.ts +46 -11
- package/src/tools/computer-use/registry.ts +4 -5
- package/src/tools/credentials/broker.ts +5 -4
- package/src/tools/credentials/metadata-store.ts +22 -74
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +139 -151
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/registry.ts +2 -7
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +62 -8
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +0 -5
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/tools/watch/watch-state.ts +0 -12
- package/src/util/logger.ts +7 -41
- package/src/util/platform.ts +9 -28
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +94 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
- package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
- package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
- package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
- package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
- package/src/cli/commands/dev.ts +0 -129
- package/src/cli/commands/map.ts +0 -391
- package/src/cli/commands/oauth.ts +0 -77
- package/src/config/bundled-skills/computer-use/tools/computer-use-request-control.ts +0 -16
- package/src/daemon/computer-use-session.ts +0 -1020
- package/src/daemon/ride-shotgun-handler.ts +0 -567
- package/src/oauth/provider-profiles.ts +0 -192
- package/src/prompts/computer-use-prompt.ts +0 -98
- package/src/runtime/routes/computer-use-routes.ts +0 -641
- package/src/runtime/telegram-streaming-delivery.test.ts +0 -597
- package/src/runtime/telegram-streaming-delivery.ts +0 -383
- package/src/tools/computer-use/request-computer-control.ts +0 -61
|
@@ -45,17 +45,95 @@ mock.module("../tools/registry.js", () => ({
|
|
|
45
45
|
registerTool: () => {},
|
|
46
46
|
}));
|
|
47
47
|
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Mock OAuth2 token refresh for token-manager deduplication tests
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
let mockRefreshOAuth2Token: ReturnType<
|
|
53
|
+
typeof mock<() => Promise<{ accessToken: string; expiresIn: number }>>
|
|
54
|
+
>;
|
|
55
|
+
|
|
56
|
+
mock.module("../security/oauth2.js", () => {
|
|
57
|
+
mockRefreshOAuth2Token = mock(() =>
|
|
58
|
+
Promise.resolve({
|
|
59
|
+
accessToken: "refreshed-access-token",
|
|
60
|
+
expiresIn: 3600,
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
return {
|
|
64
|
+
refreshOAuth2Token: mockRefreshOAuth2Token,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Mock oauth-store — token-manager reads refresh config from SQLite
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/** Mutable per-test map of provider connections for getConnectionByProvider */
|
|
73
|
+
const mockConnections = new Map<
|
|
74
|
+
string,
|
|
75
|
+
{
|
|
76
|
+
id: string;
|
|
77
|
+
providerKey: string;
|
|
78
|
+
oauthAppId: string;
|
|
79
|
+
expiresAt: number | null;
|
|
80
|
+
}
|
|
81
|
+
>();
|
|
82
|
+
const mockApps = new Map<
|
|
83
|
+
string,
|
|
84
|
+
{ id: string; providerKey: string; clientId: string }
|
|
85
|
+
>();
|
|
86
|
+
const mockProviders = new Map<
|
|
87
|
+
string,
|
|
88
|
+
{
|
|
89
|
+
key: string;
|
|
90
|
+
tokenUrl: string;
|
|
91
|
+
tokenEndpointAuthMethod?: string;
|
|
92
|
+
}
|
|
93
|
+
>();
|
|
94
|
+
|
|
95
|
+
let mockDisconnectOAuthProvider: ReturnType<
|
|
96
|
+
typeof mock<
|
|
97
|
+
(providerKey: string) => Promise<"disconnected" | "not-found" | "error">
|
|
98
|
+
>
|
|
99
|
+
>;
|
|
100
|
+
|
|
101
|
+
mock.module("../oauth/oauth-store.js", () => {
|
|
102
|
+
mockDisconnectOAuthProvider = mock((providerKey: string) =>
|
|
103
|
+
Promise.resolve(
|
|
104
|
+
mockConnections.has(providerKey)
|
|
105
|
+
? ("disconnected" as const)
|
|
106
|
+
: ("not-found" as const),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
return {
|
|
110
|
+
disconnectOAuthProvider: mockDisconnectOAuthProvider,
|
|
111
|
+
getConnectionByProvider: (service: string) => mockConnections.get(service),
|
|
112
|
+
getApp: (id: string) => mockApps.get(id),
|
|
113
|
+
getProvider: (key: string) => mockProviders.get(key),
|
|
114
|
+
updateConnection: () => {},
|
|
115
|
+
getMostRecentAppByProvider: () => undefined,
|
|
116
|
+
listConnections: () => [],
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
48
120
|
// ---------------------------------------------------------------------------
|
|
49
121
|
// Import the module under test
|
|
50
122
|
// ---------------------------------------------------------------------------
|
|
51
123
|
|
|
52
124
|
// getCredentialValue is no longer exported (sealed in PR 17) — use getSecureKey directly
|
|
53
125
|
|
|
126
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
54
127
|
import {
|
|
55
128
|
deleteSecureKey,
|
|
56
129
|
getSecureKey,
|
|
57
130
|
setSecureKey,
|
|
58
131
|
} from "../security/secure-keys.js";
|
|
132
|
+
import {
|
|
133
|
+
_resetInflightRefreshes,
|
|
134
|
+
_resetRefreshBreakers,
|
|
135
|
+
withValidToken,
|
|
136
|
+
} from "../security/token-manager.js";
|
|
59
137
|
import {
|
|
60
138
|
_setMetadataPath,
|
|
61
139
|
getCredentialMetadata,
|
|
@@ -120,7 +198,7 @@ async function executeVault(
|
|
|
120
198
|
};
|
|
121
199
|
}
|
|
122
200
|
|
|
123
|
-
const key =
|
|
201
|
+
const key = credentialKey(service, field);
|
|
124
202
|
const ok = setSecureKey(key, value);
|
|
125
203
|
if (!ok) {
|
|
126
204
|
return { content: "Error: failed to store credential", isError: true };
|
|
@@ -151,7 +229,7 @@ async function executeVault(
|
|
|
151
229
|
};
|
|
152
230
|
}
|
|
153
231
|
|
|
154
|
-
const key =
|
|
232
|
+
const key = credentialKey(service, field);
|
|
155
233
|
const result = deleteSecureKey(key);
|
|
156
234
|
if (result !== "deleted") {
|
|
157
235
|
return {
|
|
@@ -187,12 +265,15 @@ describe("credential_store tool", () => {
|
|
|
187
265
|
}
|
|
188
266
|
_setStorePath(STORE_PATH);
|
|
189
267
|
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
268
|
+
mockDisconnectOAuthProvider.mockClear();
|
|
269
|
+
mockConnections.clear();
|
|
190
270
|
});
|
|
191
271
|
|
|
192
272
|
afterEach(() => {
|
|
193
273
|
_setMetadataPath(null);
|
|
194
274
|
_setStorePath(null);
|
|
195
275
|
_resetBackend();
|
|
276
|
+
mockConnections.clear();
|
|
196
277
|
});
|
|
197
278
|
|
|
198
279
|
afterAll(() => {
|
|
@@ -553,7 +634,7 @@ describe("credential_store tool", () => {
|
|
|
553
634
|
|
|
554
635
|
// Delete the secret directly without going through the tool (simulates
|
|
555
636
|
// a divergence where metadata write failed after secret deletion)
|
|
556
|
-
deleteSecureKey("
|
|
637
|
+
deleteSecureKey(credentialKey("svc-a", "key"));
|
|
557
638
|
|
|
558
639
|
const result = await credentialStoreTool.execute(
|
|
559
640
|
{ action: "list" },
|
|
@@ -596,7 +677,7 @@ describe("credential_store tool", () => {
|
|
|
596
677
|
// -----------------------------------------------------------------------
|
|
597
678
|
describe("delete action", () => {
|
|
598
679
|
test("deletes a stored credential", async () => {
|
|
599
|
-
setSecureKey("
|
|
680
|
+
setSecureKey(credentialKey("gmail", "password"), "secret");
|
|
600
681
|
|
|
601
682
|
const result = await executeVault({
|
|
602
683
|
action: "delete",
|
|
@@ -607,7 +688,7 @@ describe("credential_store tool", () => {
|
|
|
607
688
|
expect(result.content).toBe("Deleted credential for gmail/password.");
|
|
608
689
|
|
|
609
690
|
// Verify it's actually gone
|
|
610
|
-
expect(getSecureKey("
|
|
691
|
+
expect(getSecureKey(credentialKey("gmail", "password"))).toBeUndefined();
|
|
611
692
|
});
|
|
612
693
|
|
|
613
694
|
test("returns error for non-existent credential", async () => {
|
|
@@ -637,6 +718,44 @@ describe("credential_store tool", () => {
|
|
|
637
718
|
expect(result.isError).toBe(true);
|
|
638
719
|
expect(result.content).toContain("field is required");
|
|
639
720
|
});
|
|
721
|
+
|
|
722
|
+
test("delete also disconnects OAuth connection for the service", async () => {
|
|
723
|
+
// Store a credential via the real tool so metadata exists
|
|
724
|
+
await credentialStoreTool.execute(
|
|
725
|
+
{
|
|
726
|
+
action: "store",
|
|
727
|
+
service: "integration:gmail",
|
|
728
|
+
field: "api_key",
|
|
729
|
+
value: "test-value",
|
|
730
|
+
},
|
|
731
|
+
_ctx,
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
// Simulate an active OAuth connection for this service
|
|
735
|
+
mockConnections.set("integration:gmail", {
|
|
736
|
+
id: "conn-gmail",
|
|
737
|
+
providerKey: "integration:gmail",
|
|
738
|
+
oauthAppId: "app-gmail",
|
|
739
|
+
expiresAt: Date.now() + 3600_000,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
const result = await credentialStoreTool.execute(
|
|
743
|
+
{
|
|
744
|
+
action: "delete",
|
|
745
|
+
service: "integration:gmail",
|
|
746
|
+
field: "api_key",
|
|
747
|
+
},
|
|
748
|
+
_ctx,
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
expect(result.isError).toBe(false);
|
|
752
|
+
expect(result.content).toContain("Deleted credential");
|
|
753
|
+
// Verify disconnectOAuthProvider was called with the service name
|
|
754
|
+
expect(mockDisconnectOAuthProvider).toHaveBeenCalledTimes(1);
|
|
755
|
+
expect(mockDisconnectOAuthProvider).toHaveBeenCalledWith(
|
|
756
|
+
"integration:gmail",
|
|
757
|
+
);
|
|
758
|
+
});
|
|
640
759
|
});
|
|
641
760
|
|
|
642
761
|
// -----------------------------------------------------------------------
|
|
@@ -644,12 +763,14 @@ describe("credential_store tool", () => {
|
|
|
644
763
|
// -----------------------------------------------------------------------
|
|
645
764
|
describe("credential value access", () => {
|
|
646
765
|
test("credential values are stored via secure keys", () => {
|
|
647
|
-
setSecureKey("
|
|
648
|
-
expect(getSecureKey("
|
|
766
|
+
setSecureKey(credentialKey("github", "token"), "ghp_abc123");
|
|
767
|
+
expect(getSecureKey(credentialKey("github", "token"))).toBe("ghp_abc123");
|
|
649
768
|
});
|
|
650
769
|
|
|
651
770
|
test("returns undefined for non-existent credential", () => {
|
|
652
|
-
expect(
|
|
771
|
+
expect(
|
|
772
|
+
getSecureKey(credentialKey("nonexistent", "field")),
|
|
773
|
+
).toBeUndefined();
|
|
653
774
|
});
|
|
654
775
|
});
|
|
655
776
|
|
|
@@ -1094,8 +1215,12 @@ describe("credential_store tool", () => {
|
|
|
1094
1215
|
value: "github-pass",
|
|
1095
1216
|
});
|
|
1096
1217
|
|
|
1097
|
-
expect(getSecureKey("
|
|
1098
|
-
|
|
1218
|
+
expect(getSecureKey(credentialKey("gmail", "password"))).toBe(
|
|
1219
|
+
"gmail-pass",
|
|
1220
|
+
);
|
|
1221
|
+
expect(getSecureKey(credentialKey("github", "password"))).toBe(
|
|
1222
|
+
"github-pass",
|
|
1223
|
+
);
|
|
1099
1224
|
});
|
|
1100
1225
|
|
|
1101
1226
|
test("same service with different fields do not collide", async () => {
|
|
@@ -1112,10 +1237,279 @@ describe("credential_store tool", () => {
|
|
|
1112
1237
|
value: "backup@example.com",
|
|
1113
1238
|
});
|
|
1114
1239
|
|
|
1115
|
-
expect(getSecureKey("
|
|
1116
|
-
expect(getSecureKey("
|
|
1240
|
+
expect(getSecureKey(credentialKey("gmail", "password"))).toBe("pass123");
|
|
1241
|
+
expect(getSecureKey(credentialKey("gmail", "recovery_email"))).toBe(
|
|
1117
1242
|
"backup@example.com",
|
|
1118
1243
|
);
|
|
1119
1244
|
});
|
|
1120
1245
|
});
|
|
1121
1246
|
});
|
|
1247
|
+
|
|
1248
|
+
// ---------------------------------------------------------------------------
|
|
1249
|
+
// Token refresh deduplication tests
|
|
1250
|
+
// ---------------------------------------------------------------------------
|
|
1251
|
+
|
|
1252
|
+
describe("withValidToken refresh deduplication", () => {
|
|
1253
|
+
beforeAll(() => {
|
|
1254
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
beforeEach(() => {
|
|
1258
|
+
_resetBackend();
|
|
1259
|
+
for (const entry of readdirSync(TEST_DIR)) {
|
|
1260
|
+
rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
_setStorePath(STORE_PATH);
|
|
1263
|
+
_setMetadataPath(join(TEST_DIR, "metadata.json"));
|
|
1264
|
+
_resetRefreshBreakers();
|
|
1265
|
+
_resetInflightRefreshes();
|
|
1266
|
+
mockRefreshOAuth2Token.mockClear();
|
|
1267
|
+
// Clear mock oauth-store maps
|
|
1268
|
+
mockConnections.clear();
|
|
1269
|
+
mockApps.clear();
|
|
1270
|
+
mockProviders.clear();
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
afterEach(() => {
|
|
1274
|
+
_setMetadataPath(null);
|
|
1275
|
+
_setStorePath(null);
|
|
1276
|
+
_resetBackend();
|
|
1277
|
+
_resetRefreshBreakers();
|
|
1278
|
+
_resetInflightRefreshes();
|
|
1279
|
+
mockConnections.clear();
|
|
1280
|
+
mockApps.clear();
|
|
1281
|
+
mockProviders.clear();
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
afterAll(() => {
|
|
1285
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Helper: set up a service with an access token, refresh token, and
|
|
1290
|
+
* mock DB data so that token refresh can proceed through doRefresh().
|
|
1291
|
+
*
|
|
1292
|
+
* OAuth-specific fields (tokenUrl, clientId, expiresAt) are now stored
|
|
1293
|
+
* in the SQLite oauth-store. The mock maps simulate the DB layer.
|
|
1294
|
+
*/
|
|
1295
|
+
function setupService(
|
|
1296
|
+
service: string,
|
|
1297
|
+
opts?: { expired?: boolean; accessToken?: string },
|
|
1298
|
+
) {
|
|
1299
|
+
const accessToken = opts?.accessToken ?? "old-access-token";
|
|
1300
|
+
|
|
1301
|
+
// Seed mock oauth-store maps so token-manager can resolve refresh config
|
|
1302
|
+
const appId = `app-${service}`;
|
|
1303
|
+
const connId = `conn-${service}`;
|
|
1304
|
+
|
|
1305
|
+
// Store access token under the oauth_connection key path that
|
|
1306
|
+
// withValidToken reads (not the legacy credentialKey path).
|
|
1307
|
+
setSecureKey(`oauth_connection/${connId}/access_token`, accessToken);
|
|
1308
|
+
mockProviders.set(service, {
|
|
1309
|
+
key: service,
|
|
1310
|
+
tokenUrl: "https://oauth.example.com/token",
|
|
1311
|
+
});
|
|
1312
|
+
mockApps.set(appId, {
|
|
1313
|
+
id: appId,
|
|
1314
|
+
providerKey: service,
|
|
1315
|
+
clientId: "test-client-id",
|
|
1316
|
+
});
|
|
1317
|
+
mockConnections.set(service, {
|
|
1318
|
+
id: connId,
|
|
1319
|
+
providerKey: service,
|
|
1320
|
+
oauthAppId: appId,
|
|
1321
|
+
expiresAt: opts?.expired
|
|
1322
|
+
? Date.now() - 60_000 // expired 1 minute ago
|
|
1323
|
+
: Date.now() + 3600_000, // expires in 1 hour
|
|
1324
|
+
});
|
|
1325
|
+
// Store refresh token and client_secret in secure keys (token-manager reads them)
|
|
1326
|
+
setSecureKey(
|
|
1327
|
+
`oauth_connection/${connId}/refresh_token`,
|
|
1328
|
+
"valid-refresh-token",
|
|
1329
|
+
);
|
|
1330
|
+
setSecureKey(`oauth_app/${appId}/client_secret`, "test-client-secret");
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
test("3 concurrent 401 refreshes for the same service call doRefresh exactly once", async () => {
|
|
1334
|
+
setupService("integration:gmail");
|
|
1335
|
+
|
|
1336
|
+
let resolveRefresh!: (value: {
|
|
1337
|
+
accessToken: string;
|
|
1338
|
+
expiresIn: number;
|
|
1339
|
+
}) => void;
|
|
1340
|
+
const refreshPromise = new Promise<{
|
|
1341
|
+
accessToken: string;
|
|
1342
|
+
expiresIn: number;
|
|
1343
|
+
}>((resolve) => {
|
|
1344
|
+
resolveRefresh = resolve;
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
mockRefreshOAuth2Token.mockImplementation(() => refreshPromise);
|
|
1348
|
+
|
|
1349
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1350
|
+
|
|
1351
|
+
const callback = async (token: string) => {
|
|
1352
|
+
if (token === "old-access-token") throw err401;
|
|
1353
|
+
return `result-with-${token}`;
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// Launch 3 concurrent withValidToken calls — all will get a non-expired
|
|
1357
|
+
// token first, call the callback, get a 401, and then try to refresh.
|
|
1358
|
+
const p1 = withValidToken("integration:gmail", callback);
|
|
1359
|
+
const p2 = withValidToken("integration:gmail", callback);
|
|
1360
|
+
const p3 = withValidToken("integration:gmail", callback);
|
|
1361
|
+
|
|
1362
|
+
// Let the event loop tick so all 3 calls enter the 401 retry path
|
|
1363
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1364
|
+
|
|
1365
|
+
// Resolve the single refresh attempt
|
|
1366
|
+
resolveRefresh({ accessToken: "new-token-123", expiresIn: 3600 });
|
|
1367
|
+
|
|
1368
|
+
const results = await Promise.all([p1, p2, p3]);
|
|
1369
|
+
|
|
1370
|
+
// All 3 should succeed with the refreshed token
|
|
1371
|
+
expect(results).toEqual([
|
|
1372
|
+
"result-with-new-token-123",
|
|
1373
|
+
"result-with-new-token-123",
|
|
1374
|
+
"result-with-new-token-123",
|
|
1375
|
+
]);
|
|
1376
|
+
|
|
1377
|
+
// refreshOAuth2Token should have been called exactly once
|
|
1378
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
test("concurrent refreshes for different services proceed independently", async () => {
|
|
1382
|
+
setupService("integration:gmail");
|
|
1383
|
+
setupService("integration:slack");
|
|
1384
|
+
|
|
1385
|
+
let resolveGmail!: (value: {
|
|
1386
|
+
accessToken: string;
|
|
1387
|
+
expiresIn: number;
|
|
1388
|
+
}) => void;
|
|
1389
|
+
let resolveSlack!: (value: {
|
|
1390
|
+
accessToken: string;
|
|
1391
|
+
expiresIn: number;
|
|
1392
|
+
}) => void;
|
|
1393
|
+
|
|
1394
|
+
const gmailPromise = new Promise<{
|
|
1395
|
+
accessToken: string;
|
|
1396
|
+
expiresIn: number;
|
|
1397
|
+
}>((resolve) => {
|
|
1398
|
+
resolveGmail = resolve;
|
|
1399
|
+
});
|
|
1400
|
+
const slackPromise = new Promise<{
|
|
1401
|
+
accessToken: string;
|
|
1402
|
+
expiresIn: number;
|
|
1403
|
+
}>((resolve) => {
|
|
1404
|
+
resolveSlack = resolve;
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
let refreshCallCount = 0;
|
|
1408
|
+
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
1409
|
+
refreshCallCount++;
|
|
1410
|
+
// Both services use the same tokenUrl in this test, so we track by
|
|
1411
|
+
// call order to return the correct deferred promise.
|
|
1412
|
+
if (refreshCallCount === 1) return gmailPromise;
|
|
1413
|
+
return slackPromise;
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1417
|
+
|
|
1418
|
+
const gmailCallback = async (token: string) => {
|
|
1419
|
+
if (token === "old-access-token") throw err401;
|
|
1420
|
+
return `gmail-${token}`;
|
|
1421
|
+
};
|
|
1422
|
+
const slackCallback = async (token: string) => {
|
|
1423
|
+
if (token === "old-access-token") throw err401;
|
|
1424
|
+
return `slack-${token}`;
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
const p1 = withValidToken("integration:gmail", gmailCallback);
|
|
1428
|
+
const p2 = withValidToken("integration:slack", slackCallback);
|
|
1429
|
+
|
|
1430
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
1431
|
+
|
|
1432
|
+
// Resolve both independently
|
|
1433
|
+
resolveGmail({ accessToken: "gmail-new-token", expiresIn: 3600 });
|
|
1434
|
+
resolveSlack({ accessToken: "slack-new-token", expiresIn: 3600 });
|
|
1435
|
+
|
|
1436
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
1437
|
+
|
|
1438
|
+
expect(r1).toBe("gmail-gmail-new-token");
|
|
1439
|
+
expect(r2).toBe("slack-slack-new-token");
|
|
1440
|
+
|
|
1441
|
+
// Both services should have triggered their own refresh
|
|
1442
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(2);
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
test("deduplication cleans up after refresh completes, allowing subsequent refreshes", async () => {
|
|
1446
|
+
setupService("integration:gmail");
|
|
1447
|
+
|
|
1448
|
+
let refreshCount = 0;
|
|
1449
|
+
mockRefreshOAuth2Token.mockImplementation(() => {
|
|
1450
|
+
refreshCount++;
|
|
1451
|
+
return Promise.resolve({
|
|
1452
|
+
accessToken: `token-${refreshCount}`,
|
|
1453
|
+
expiresIn: 3600,
|
|
1454
|
+
});
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1458
|
+
|
|
1459
|
+
// First call triggers a refresh (old token → 401 → refresh → token-1)
|
|
1460
|
+
const r1 = await withValidToken(
|
|
1461
|
+
"integration:gmail",
|
|
1462
|
+
async (token: string) => {
|
|
1463
|
+
if (token !== "token-1") throw err401;
|
|
1464
|
+
return token;
|
|
1465
|
+
},
|
|
1466
|
+
);
|
|
1467
|
+
expect(r1).toBe("token-1");
|
|
1468
|
+
expect(refreshCount).toBe(1);
|
|
1469
|
+
|
|
1470
|
+
// Second call also triggers a 401 to verify dedup state was cleaned up
|
|
1471
|
+
// and a new refresh is allowed (not deduplicated with the first).
|
|
1472
|
+
const r2 = await withValidToken(
|
|
1473
|
+
"integration:gmail",
|
|
1474
|
+
async (token: string) => {
|
|
1475
|
+
if (token !== "token-2") throw err401;
|
|
1476
|
+
return token;
|
|
1477
|
+
},
|
|
1478
|
+
);
|
|
1479
|
+
expect(r2).toBe("token-2");
|
|
1480
|
+
// Second refresh should have happened (not deduplicated with the first,
|
|
1481
|
+
// since the first already completed)
|
|
1482
|
+
expect(refreshCount).toBe(2);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
test("deduplication propagates refresh errors to all waiting callers", async () => {
|
|
1486
|
+
setupService("integration:gmail");
|
|
1487
|
+
|
|
1488
|
+
mockRefreshOAuth2Token.mockImplementation(() =>
|
|
1489
|
+
Promise.reject(
|
|
1490
|
+
Object.assign(
|
|
1491
|
+
new Error("OAuth2 token refresh failed (HTTP 401: invalid_grant)"),
|
|
1492
|
+
),
|
|
1493
|
+
),
|
|
1494
|
+
);
|
|
1495
|
+
|
|
1496
|
+
const err401 = Object.assign(new Error("Unauthorized"), { status: 401 });
|
|
1497
|
+
|
|
1498
|
+
const callback = async (token: string) => {
|
|
1499
|
+
if (token === "old-access-token") throw err401;
|
|
1500
|
+
return "should-not-reach";
|
|
1501
|
+
};
|
|
1502
|
+
|
|
1503
|
+
// Launch 2 concurrent calls — both should fail with the same error
|
|
1504
|
+
const p1 = withValidToken("integration:gmail", callback);
|
|
1505
|
+
const p2 = withValidToken("integration:gmail", callback);
|
|
1506
|
+
|
|
1507
|
+
const results = await Promise.allSettled([p1, p2]);
|
|
1508
|
+
|
|
1509
|
+
expect(results[0].status).toBe("rejected");
|
|
1510
|
+
expect(results[1].status).toBe("rejected");
|
|
1511
|
+
|
|
1512
|
+
// Only one actual refresh attempt
|
|
1513
|
+
expect(mockRefreshOAuth2Token).toHaveBeenCalledTimes(1);
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
|
|
5
|
+
import { credentialKey } from "../security/credential-key.js";
|
|
5
6
|
import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
6
7
|
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +164,26 @@ mock.module("../tools/credentials/metadata-store.js", () => ({
|
|
|
163
164
|
},
|
|
164
165
|
}));
|
|
165
166
|
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Mock oauth-store
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
let disconnectOAuthProviderCalls: string[] = [];
|
|
172
|
+
let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
|
|
173
|
+
"not-found";
|
|
174
|
+
|
|
175
|
+
mock.module("../oauth/oauth-store.js", () => ({
|
|
176
|
+
disconnectOAuthProvider: async (
|
|
177
|
+
providerKey: string,
|
|
178
|
+
): Promise<"disconnected" | "not-found" | "error"> => {
|
|
179
|
+
disconnectOAuthProviderCalls.push(providerKey);
|
|
180
|
+
return disconnectOAuthProviderResult;
|
|
181
|
+
},
|
|
182
|
+
getConnectionByProvider: (): undefined => undefined,
|
|
183
|
+
listConnections: (): never[] => [],
|
|
184
|
+
deleteConnection: (): boolean => false,
|
|
185
|
+
}));
|
|
186
|
+
|
|
166
187
|
// ---------------------------------------------------------------------------
|
|
167
188
|
// Import the module under test (after mocks are registered)
|
|
168
189
|
// ---------------------------------------------------------------------------
|
|
@@ -238,7 +259,7 @@ function seedCredential(
|
|
|
238
259
|
...extra,
|
|
239
260
|
};
|
|
240
261
|
metadataStore.push(record);
|
|
241
|
-
secureKeyStore.set(
|
|
262
|
+
secureKeyStore.set(credentialKey(service, field), secret);
|
|
242
263
|
return record;
|
|
243
264
|
}
|
|
244
265
|
|
|
@@ -280,6 +301,8 @@ describe("assistant credentials CLI", () => {
|
|
|
280
301
|
_listMetadataCalls = 0;
|
|
281
302
|
_getMetadataCalls = 0;
|
|
282
303
|
_getMetadataByIdCalls = 0;
|
|
304
|
+
disconnectOAuthProviderCalls = [];
|
|
305
|
+
disconnectOAuthProviderResult = "not-found";
|
|
283
306
|
process.exitCode = 0;
|
|
284
307
|
});
|
|
285
308
|
|
|
@@ -437,7 +460,7 @@ describe("assistant credentials CLI", () => {
|
|
|
437
460
|
expect(parsed.credentialId).toBeTruthy();
|
|
438
461
|
|
|
439
462
|
// Verify secret stored in mock map
|
|
440
|
-
expect(secureKeyStore.get("
|
|
463
|
+
expect(secureKeyStore.get(credentialKey("twilio", "account_sid"))).toBe(
|
|
441
464
|
"AC1234567890",
|
|
442
465
|
);
|
|
443
466
|
|
|
@@ -546,7 +569,7 @@ describe("assistant credentials CLI", () => {
|
|
|
546
569
|
expect(meta2!.updatedAt).toBeGreaterThan(firstUpdatedAt);
|
|
547
570
|
|
|
548
571
|
// Verify secret is overwritten
|
|
549
|
-
expect(secureKeyStore.get("
|
|
572
|
+
expect(secureKeyStore.get(credentialKey("twilio", "account_sid"))).toBe(
|
|
550
573
|
"new_value",
|
|
551
574
|
);
|
|
552
575
|
});
|
|
@@ -568,7 +591,9 @@ describe("assistant credentials CLI", () => {
|
|
|
568
591
|
expect(parsed.field).toBe("auth_token");
|
|
569
592
|
|
|
570
593
|
// Verify both removed
|
|
571
|
-
expect(secureKeyStore.has("
|
|
594
|
+
expect(secureKeyStore.has(credentialKey("twilio", "auth_token"))).toBe(
|
|
595
|
+
false,
|
|
596
|
+
);
|
|
572
597
|
expect(
|
|
573
598
|
metadataStore.find(
|
|
574
599
|
(m) => m.service === "twilio" && m.field === "auth_token",
|
|
@@ -608,6 +633,55 @@ describe("assistant credentials CLI", () => {
|
|
|
608
633
|
),
|
|
609
634
|
).toBeUndefined();
|
|
610
635
|
});
|
|
636
|
+
|
|
637
|
+
test("calls disconnectOAuthProvider for OAuth cleanup", async () => {
|
|
638
|
+
seedCredential("gmail", "access_token", "ya29.token_value");
|
|
639
|
+
|
|
640
|
+
const result = await runCli(["delete", "gmail:access_token", "--json"]);
|
|
641
|
+
expect(result.exitCode).toBe(0);
|
|
642
|
+
const parsed = JSON.parse(result.stdout);
|
|
643
|
+
expect(parsed.ok).toBe(true);
|
|
644
|
+
|
|
645
|
+
// disconnectOAuthProvider should have been called with the service name
|
|
646
|
+
expect(disconnectOAuthProviderCalls).toEqual(["gmail"]);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test("succeeds when only OAuth connection exists (no legacy credential)", async () => {
|
|
650
|
+
// No legacy credential seeded — only the OAuth disconnect finds something
|
|
651
|
+
disconnectOAuthProviderResult = "disconnected";
|
|
652
|
+
|
|
653
|
+
const result = await runCli(["delete", "gmail:access_token", "--json"]);
|
|
654
|
+
expect(result.exitCode).toBe(0);
|
|
655
|
+
const parsed = JSON.parse(result.stdout);
|
|
656
|
+
expect(parsed.ok).toBe(true);
|
|
657
|
+
expect(parsed.service).toBe("gmail");
|
|
658
|
+
expect(parsed.field).toBe("access_token");
|
|
659
|
+
|
|
660
|
+
expect(disconnectOAuthProviderCalls).toEqual(["gmail"]);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("demonstrates colon-in-service-name parsing limitation with integration:gmail", async () => {
|
|
664
|
+
// parseCredentialName("integration:gmail:access_token") splits on the
|
|
665
|
+
// first colon, yielding service="integration" and field="gmail:access_token".
|
|
666
|
+
// This is incorrect for the intended service "integration:gmail". The fix
|
|
667
|
+
// for this limitation is addressed by introducing a dedicated `disconnect`
|
|
668
|
+
// subcommand (PR 5).
|
|
669
|
+
const result = await runCli([
|
|
670
|
+
"delete",
|
|
671
|
+
"integration:gmail:access_token",
|
|
672
|
+
"--json",
|
|
673
|
+
]);
|
|
674
|
+
// The command parses as service="integration", field="gmail:access_token"
|
|
675
|
+
// which finds nothing and reports not-found.
|
|
676
|
+
expect(result.exitCode).toBe(1);
|
|
677
|
+
const parsed = JSON.parse(result.stdout);
|
|
678
|
+
expect(parsed.ok).toBe(false);
|
|
679
|
+
expect(parsed.error).toContain("not found");
|
|
680
|
+
|
|
681
|
+
// disconnectOAuthProvider was called with "integration" (wrong) instead
|
|
682
|
+
// of "integration:gmail" (intended).
|
|
683
|
+
expect(disconnectOAuthProviderCalls).toEqual(["integration"]);
|
|
684
|
+
});
|
|
611
685
|
});
|
|
612
686
|
|
|
613
687
|
// =========================================================================
|
|
@@ -833,8 +907,10 @@ describe("assistant credentials CLI", () => {
|
|
|
833
907
|
expect(parsed.value).toBe("instance_secret_abc123");
|
|
834
908
|
|
|
835
909
|
// Verify the correct key was looked up in the secure store
|
|
836
|
-
expect(secureKeyStore.has("
|
|
837
|
-
|
|
910
|
+
expect(secureKeyStore.has(credentialKey("twilio", "auth_token"))).toBe(
|
|
911
|
+
true,
|
|
912
|
+
);
|
|
913
|
+
expect(secureKeyStore.get(credentialKey("twilio", "auth_token"))).toBe(
|
|
838
914
|
"instance_secret_abc123",
|
|
839
915
|
);
|
|
840
916
|
});
|
|
@@ -328,11 +328,11 @@ describe("ephemeral-permissions", () => {
|
|
|
328
328
|
testConfig.permissions.mode = "workspace";
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
-
test("workspace mode
|
|
331
|
+
test("workspace mode prompts for workspace-scoped file_write (medium risk)", async () => {
|
|
332
332
|
const filePath = join(testDir, "workspace-test-file.txt");
|
|
333
333
|
const result = await check("file_write", { path: filePath }, testDir);
|
|
334
|
-
expect(result.decision).toBe("
|
|
335
|
-
expect(result.reason).toContain("
|
|
334
|
+
expect(result.decision).toBe("prompt");
|
|
335
|
+
expect(result.reason).toContain("medium risk");
|
|
336
336
|
});
|
|
337
337
|
|
|
338
338
|
test("workspace mode still prompts for file_write outside workspace", async () => {
|