@vellumai/assistant 0.7.2 → 0.8.0
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 +45 -29
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/docs/architecture/memory.md +5 -2
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +470 -25
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +48 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +6 -33
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +12 -7
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +457 -8
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +38 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +74 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +476 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +61 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +2 -20
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +36 -16
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +60 -5
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +78 -0
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-request-resolvers.ts +3 -32
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +11 -10
- package/src/cli/commands/oauth/__tests__/connect.test.ts +401 -219
- package/src/cli/commands/oauth/connect.ts +124 -40
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +13 -5
- package/src/config/loader.ts +199 -27
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +76 -12
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +38 -0
- package/src/daemon/conversation-agent-loop.ts +183 -43
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +22 -10
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +211 -29
- package/src/daemon/conversation-tool-setup.ts +66 -19
- package/src/daemon/conversation.ts +18 -23
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +26 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +47 -22
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +50 -4
- package/src/daemon/host-file-proxy.ts +44 -8
- package/src/daemon/host-transfer-proxy.ts +97 -6
- package/src/daemon/lifecycle.ts +167 -101
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +66 -15
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +22 -1
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +119 -0
- package/src/filing/filing-service.ts +29 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +149 -38
- package/src/ipc/gateway-client.ts +37 -3
- package/src/ipc/skill-server.ts +99 -42
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +34 -51
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +1 -16
- package/src/memory/context-search/sources/memory.ts +2 -3
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +136 -82
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +72 -61
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +11 -67
- package/src/memory/graph/graph-search.ts +4 -24
- package/src/memory/graph/retriever.test.ts +12 -1
- package/src/memory/graph/retriever.ts +10 -15
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +53 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +85 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +7 -0
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +10 -72
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +346 -255
- package/src/memory/v2/__tests__/consolidation-job.test.ts +61 -40
- package/src/memory/v2/__tests__/injection.test.ts +297 -190
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +326 -9
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +113 -196
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +77 -14
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +69 -20
- package/src/memory/v2/injection.ts +75 -68
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +41 -1
- package/src/memory/v2/qdrant.ts +306 -46
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +77 -110
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +26 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +17 -10
- package/src/notifications/copy-composer.ts +47 -0
- package/src/notifications/decision-engine.ts +46 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +36 -4
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +914 -0
- package/src/proactive-artifact/job.ts +366 -0
- package/src/proactive-artifact/message-copy.ts +58 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +27 -15
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +165 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +37 -17
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +136 -17
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/include-graph.ts +35 -13
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +14 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +43 -3
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +14 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +14 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -32,6 +32,51 @@ export function nonEmpty(value: string | undefined): string | undefined {
|
|
|
32
32
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
export function looksLikeIntermediaryInstruction(text: string): boolean {
|
|
36
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
37
|
+
const intermediaryAction =
|
|
38
|
+
"(?:tell|telling|ask|asking|remind|reminding|nudge|nudging|prompt|prompting|notify|notifying|encourage|encouraging|prime|priming|brief|briefing|coach|coaching)";
|
|
39
|
+
const target = "(?:the\\s+)?(?:guardian|recipient|user)";
|
|
40
|
+
return (
|
|
41
|
+
/\b(?:assistant|agent|system|model|watcher)\s+(?:should|needs?\s+to|must|can|could)\b/i.test(
|
|
42
|
+
normalized,
|
|
43
|
+
) ||
|
|
44
|
+
new RegExp(
|
|
45
|
+
`\\b(?:consider|try|please)\\s+${intermediaryAction}\\s+${target}\\b`,
|
|
46
|
+
"i",
|
|
47
|
+
).test(normalized) ||
|
|
48
|
+
new RegExp(
|
|
49
|
+
`\\b${intermediaryAction}\\s+${target}\\s+(?:to|that|about|with)\\b`,
|
|
50
|
+
"i",
|
|
51
|
+
).test(normalized) ||
|
|
52
|
+
new RegExp(
|
|
53
|
+
`\\b${target}\\s+(?:should|needs?\\s+to|must|might\\s+want\\s+to)\\b`,
|
|
54
|
+
"i",
|
|
55
|
+
).test(normalized) ||
|
|
56
|
+
new RegExp(`\\b(?:for|to)\\s+${target}\\s+to\\b`, "i").test(normalized)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildHeartbeatAlertCopy(
|
|
61
|
+
payload: Record<string, unknown>,
|
|
62
|
+
): RenderedChannelCopy {
|
|
63
|
+
const summary = str(
|
|
64
|
+
payload.summary,
|
|
65
|
+
str(payload.body, "Your assistant found something worth your attention."),
|
|
66
|
+
).trim();
|
|
67
|
+
const safePopupBody = looksLikeIntermediaryInstruction(summary)
|
|
68
|
+
? "I found something worth your attention in a heartbeat check. Open the conversation for details."
|
|
69
|
+
: summary;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
title: str(payload.title, "Heartbeat Alert"),
|
|
73
|
+
body: safePopupBody,
|
|
74
|
+
deliveryText: safePopupBody,
|
|
75
|
+
conversationTitle: str(payload.conversationTitle, "Heartbeat"),
|
|
76
|
+
conversationSeedMessage: summary,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
35
80
|
// ── Access-request copy contract ─────────────────────────────────────────────
|
|
36
81
|
//
|
|
37
82
|
// Deterministic helpers for building guardian-facing access-request copy.
|
|
@@ -505,6 +550,8 @@ const TEMPLATES: Partial<Record<NotificationSourceEventName, CopyTemplate>> = {
|
|
|
505
550
|
body: str(payload.body, "A watcher event requires your attention"),
|
|
506
551
|
}),
|
|
507
552
|
|
|
553
|
+
"heartbeat.alert": buildHeartbeatAlertCopy,
|
|
554
|
+
|
|
508
555
|
"tool_confirmation.required_action": (payload) => ({
|
|
509
556
|
title: "Tool Confirmation",
|
|
510
557
|
body: str(payload.toolName, "A tool") + " requires your confirmation",
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
composeFallbackCopy,
|
|
36
36
|
hasAccessRequestInstructions,
|
|
37
37
|
hasInviteFlowDirective,
|
|
38
|
+
looksLikeIntermediaryInstruction,
|
|
38
39
|
} from "./copy-composer.js";
|
|
39
40
|
import { createDecision } from "./decisions-store.js";
|
|
40
41
|
import {
|
|
@@ -127,11 +128,13 @@ function buildSystemPrompt(
|
|
|
127
128
|
``,
|
|
128
129
|
`Copy guidelines (three distinct outputs):`,
|
|
129
130
|
`- \`title\` and \`body\` are for native notification popups (e.g. vellum desktop/mobile) — keep them short and glanceable (title ≤ 8 words, body ≤ 2 sentences).`,
|
|
131
|
+
` - Write popup copy as final copy for the guardian or recipient. Do not write instructions for the assistant or another intermediary.`,
|
|
130
132
|
`- \`deliveryText\` is the channel-native message for chat channels (e.g. telegram). It must read naturally as a standalone message.`,
|
|
131
133
|
` - Do not prepend mechanical labels like "Conversation:".`,
|
|
132
134
|
` - Do not mention channel or transport names (e.g. Telegram, Slack, email) unless the event context explicitly requires it.`,
|
|
133
135
|
` - Do not repeat title/body verbatim unless that repetition is truly necessary.`,
|
|
134
136
|
` - Avoid meta-send phrasing (e.g. "I'd like to send a notification", "May I go ahead with that?"). Write the recipient-facing message directly.`,
|
|
137
|
+
` - Avoid intermediary-instruction phrasing like "consider telling the guardian", "ask the recipient to", or "the assistant should remind them". Rewrite it as final copy the recipient can act on directly.`,
|
|
135
138
|
` - For telegram: 1-2 concise sentences.`,
|
|
136
139
|
`- \`conversationSeedMessage\` is the opening message in the internal notification conversation — it can be richer and more contextual.`,
|
|
137
140
|
` - For vellum (desktop): 2-4 short sentences with useful context and clear next step if action is required.`,
|
|
@@ -664,6 +667,47 @@ function enforceAccessRequestInstructions(
|
|
|
664
667
|
};
|
|
665
668
|
}
|
|
666
669
|
|
|
670
|
+
function enforceHeartbeatAlertCopy(
|
|
671
|
+
decision: NotificationDecision,
|
|
672
|
+
signal: NotificationSignal,
|
|
673
|
+
): NotificationDecision {
|
|
674
|
+
if (signal.sourceEventName !== "heartbeat.alert") return decision;
|
|
675
|
+
if (!decision.shouldNotify || decision.selectedChannels.length === 0)
|
|
676
|
+
return decision;
|
|
677
|
+
|
|
678
|
+
const fallbackCopy = composeFallbackCopy(signal, decision.selectedChannels);
|
|
679
|
+
const nextCopy: Partial<Record<NotificationChannel, RenderedChannelCopy>> = {
|
|
680
|
+
...decision.renderedCopy,
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
for (const channel of decision.selectedChannels) {
|
|
684
|
+
const currentCopy = nextCopy[channel];
|
|
685
|
+
if (
|
|
686
|
+
currentCopy &&
|
|
687
|
+
!heartbeatCopyLooksLikeIntermediaryInstruction(currentCopy)
|
|
688
|
+
) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const safeCopy = fallbackCopy[channel];
|
|
692
|
+
if (!safeCopy) continue;
|
|
693
|
+
nextCopy[channel] = safeCopy;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
...decision,
|
|
698
|
+
renderedCopy: nextCopy,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function heartbeatCopyLooksLikeIntermediaryInstruction(
|
|
703
|
+
copy: RenderedChannelCopy,
|
|
704
|
+
): boolean {
|
|
705
|
+
return [copy.title, copy.body, copy.deliveryText].some(
|
|
706
|
+
(value) =>
|
|
707
|
+
typeof value === "string" && looksLikeIntermediaryInstruction(value),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
667
711
|
function ensureAccessRequestInstructionsInCopy(
|
|
668
712
|
copy: RenderedChannelCopy,
|
|
669
713
|
requestCode: string,
|
|
@@ -754,6 +798,7 @@ export async function evaluateSignal(
|
|
|
754
798
|
let decision = buildFallbackDecision(signal, availableChannels);
|
|
755
799
|
decision = enforceGuardianRequestCode(decision, signal);
|
|
756
800
|
decision = enforceAccessRequestInstructions(decision, signal);
|
|
801
|
+
decision = enforceHeartbeatAlertCopy(decision, signal);
|
|
757
802
|
decision = enforceGuardianCallConversationAffinity(decision, signal);
|
|
758
803
|
decision = enforceConversationAffinity(
|
|
759
804
|
decision,
|
|
@@ -783,6 +828,7 @@ export async function evaluateSignal(
|
|
|
783
828
|
|
|
784
829
|
decision = enforceGuardianRequestCode(decision, signal);
|
|
785
830
|
decision = enforceAccessRequestInstructions(decision, signal);
|
|
831
|
+
decision = enforceHeartbeatAlertCopy(decision, signal);
|
|
786
832
|
decision = enforceGuardianCallConversationAffinity(decision, signal);
|
|
787
833
|
decision = enforceConversationAffinity(
|
|
788
834
|
decision,
|
|
@@ -101,6 +101,10 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
|
|
|
101
101
|
description:
|
|
102
102
|
"OAuth credential health issue detected (expired, revoked, missing scopes)",
|
|
103
103
|
},
|
|
104
|
+
{
|
|
105
|
+
id: "heartbeat.alert",
|
|
106
|
+
description: "Heartbeat found something worth surfacing to the guardian",
|
|
107
|
+
},
|
|
104
108
|
] as const;
|
|
105
109
|
|
|
106
110
|
export type NotificationSourceEventName =
|
package/src/oauth/AGENTS.md
CHANGED
|
@@ -54,7 +54,9 @@ Most existing logos come from [Simple Icons](https://simpleicons.org) (CC0-licen
|
|
|
54
54
|
|
|
55
55
|
If the service is not on Simple Icons, source or create an SVG and convert it the same way. The result must be a true vector PDF (not a rasterized image wrapped in PDF) so it scales cleanly.
|
|
56
56
|
|
|
57
|
-
The `logoUrl` field in `seed-providers.ts`
|
|
57
|
+
The `logoUrl` field in `seed-providers.ts` serves as the remote fallback (most providers use a Simple Icons CDN URL like `https://cdn.simpleicons.org/acme`). The client renders the local PDF first, then falls back to `logoUrl`, then to an initials avatar.
|
|
58
|
+
|
|
59
|
+
For brands Simple Icons doesn't host (e.g. Salesforce, which Simple Icons removed for trademark reasons), use the same `glincker/thesvg` source via jsDelivr — `https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/<key>/default.svg`. The recognised `logoUrl` prefixes are enforced by `oauth-provider-seed-logos.test.ts`; if you need a third source, extend that allowlist alongside the manifest in `clients/shared/Resources/integration-logos-manifest.json`.
|
|
58
60
|
|
|
59
61
|
### 5. Secret patterns (if applicable) — `../security/secret-patterns.ts`
|
|
60
62
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
_clearAllOAuthConnectStates,
|
|
5
|
+
clearExpiredOAuthConnectStates,
|
|
6
|
+
getOAuthConnectState,
|
|
7
|
+
setOAuthConnectComplete,
|
|
8
|
+
setOAuthConnectError,
|
|
9
|
+
setOAuthConnectPending,
|
|
10
|
+
} from "../oauth-connect-state.js";
|
|
11
|
+
|
|
12
|
+
describe("oauth-connect-state", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
_clearAllOAuthConnectStates();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("setOAuthConnectPending → getOAuthConnectState returns pending", () => {
|
|
18
|
+
setOAuthConnectPending("state-1", "google");
|
|
19
|
+
const result = getOAuthConnectState("state-1");
|
|
20
|
+
expect(result).toMatchObject({ status: "pending", service: "google" });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("setOAuthConnectComplete without accountInfo → returns complete", () => {
|
|
24
|
+
setOAuthConnectComplete("state-1", "google");
|
|
25
|
+
const result = getOAuthConnectState("state-1");
|
|
26
|
+
expect(result).toMatchObject({ status: "complete", service: "google" });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("setOAuthConnectComplete with accountInfo → returns complete with accountInfo", () => {
|
|
30
|
+
setOAuthConnectComplete("state-1", "google", "user@example.com");
|
|
31
|
+
const result = getOAuthConnectState("state-1");
|
|
32
|
+
expect(result).toMatchObject({
|
|
33
|
+
status: "complete",
|
|
34
|
+
service: "google",
|
|
35
|
+
accountInfo: "user@example.com",
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("setOAuthConnectComplete with grantedScopes → returns complete with grantedScopes", () => {
|
|
40
|
+
setOAuthConnectComplete("state-1", "google", "user@example.com", ["scope:read", "scope:write"]);
|
|
41
|
+
const result = getOAuthConnectState("state-1");
|
|
42
|
+
expect(result).toMatchObject({
|
|
43
|
+
status: "complete",
|
|
44
|
+
service: "google",
|
|
45
|
+
accountInfo: "user@example.com",
|
|
46
|
+
grantedScopes: ["scope:read", "scope:write"],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("setOAuthConnectError → returns error with message", () => {
|
|
51
|
+
setOAuthConnectError("state-1", "google", "token exchange failed");
|
|
52
|
+
const result = getOAuthConnectState("state-1");
|
|
53
|
+
expect(result).toMatchObject({
|
|
54
|
+
status: "error",
|
|
55
|
+
service: "google",
|
|
56
|
+
error: "token exchange failed",
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("re-setting same state token overwrites previous", () => {
|
|
61
|
+
setOAuthConnectPending("state-1", "google");
|
|
62
|
+
setOAuthConnectComplete("state-1", "google", "user@example.com");
|
|
63
|
+
const result = getOAuthConnectState("state-1");
|
|
64
|
+
expect(result?.status).toBe("complete");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("getOAuthConnectState returns null for unknown state", () => {
|
|
68
|
+
expect(getOAuthConnectState("nonexistent")).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("_clearAllOAuthConnectStates removes all entries", () => {
|
|
72
|
+
setOAuthConnectPending("state-1", "google");
|
|
73
|
+
setOAuthConnectPending("state-2", "github");
|
|
74
|
+
_clearAllOAuthConnectStates();
|
|
75
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
76
|
+
expect(getOAuthConnectState("state-2")).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("clearExpiredOAuthConnectStates removes expired pending entries", () => {
|
|
80
|
+
setOAuthConnectPending("state-1", "google");
|
|
81
|
+
// Advance Date.now by 6 minutes past PENDING_TTL_MS (5 min)
|
|
82
|
+
const originalNow = Date.now;
|
|
83
|
+
Date.now = () => originalNow() + 6 * 60 * 1000;
|
|
84
|
+
clearExpiredOAuthConnectStates();
|
|
85
|
+
Date.now = originalNow;
|
|
86
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("clearExpiredOAuthConnectStates removes expired complete entries (past 60s grace)", () => {
|
|
90
|
+
setOAuthConnectComplete("state-1", "google");
|
|
91
|
+
const originalNow = Date.now;
|
|
92
|
+
Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
|
|
93
|
+
clearExpiredOAuthConnectStates();
|
|
94
|
+
Date.now = originalNow;
|
|
95
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("clearExpiredOAuthConnectStates removes expired error entries (past 60s grace)", () => {
|
|
99
|
+
setOAuthConnectError("state-1", "google", "token exchange failed");
|
|
100
|
+
const originalNow = Date.now;
|
|
101
|
+
Date.now = () => originalNow() + 2 * 60 * 1000; // advance 2 minutes past 60s grace
|
|
102
|
+
clearExpiredOAuthConnectStates();
|
|
103
|
+
Date.now = originalNow;
|
|
104
|
+
expect(getOAuthConnectState("state-1")).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("clearExpiredOAuthConnectStates does not remove non-expired pending entries", () => {
|
|
108
|
+
setOAuthConnectPending("state-1", "google");
|
|
109
|
+
clearExpiredOAuthConnectStates(); // called without advancing time
|
|
110
|
+
expect(getOAuthConnectState("state-1")).not.toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("sweep-on-insert: setOAuthConnectPending purges expired entries before inserting new one", () => {
|
|
114
|
+
// 1. Add an entry that will expire
|
|
115
|
+
setOAuthConnectPending("expired-state", "google");
|
|
116
|
+
|
|
117
|
+
// 2. Advance Date.now past the PENDING_TTL_MS (5 min)
|
|
118
|
+
const originalNow = Date.now;
|
|
119
|
+
Date.now = () => originalNow() + 6 * 60 * 1000;
|
|
120
|
+
|
|
121
|
+
// 3. Insert a new entry — this should trigger clearExpiredOAuthConnectStates() internally
|
|
122
|
+
setOAuthConnectPending("new-state", "github");
|
|
123
|
+
|
|
124
|
+
// 4. Restore Date.now before assertions (getOAuthConnectState also calls clearExpiredOAuthConnectStates)
|
|
125
|
+
Date.now = originalNow;
|
|
126
|
+
|
|
127
|
+
// The expired entry must have been swept out during the insert
|
|
128
|
+
// Use the map directly via getOAuthConnectState — expired-state is gone
|
|
129
|
+
// We call _clearAllOAuthConnectStates in beforeEach so we know the map started empty.
|
|
130
|
+
// After the insert the map should only contain "new-state".
|
|
131
|
+
const expiredResult = getOAuthConnectState("expired-state");
|
|
132
|
+
expect(expiredResult).toBeNull();
|
|
133
|
+
|
|
134
|
+
const newResult = getOAuthConnectState("new-state");
|
|
135
|
+
expect(newResult).toMatchObject({ status: "pending", service: "github" });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -81,6 +81,7 @@ export interface OAuthConnectOptions {
|
|
|
81
81
|
success: boolean;
|
|
82
82
|
service: string;
|
|
83
83
|
accountInfo?: string;
|
|
84
|
+
grantedScopes?: string[];
|
|
84
85
|
error?: string;
|
|
85
86
|
}) => void;
|
|
86
87
|
}
|
|
@@ -256,6 +257,7 @@ export async function orchestrateOAuthConnect(
|
|
|
256
257
|
success: true,
|
|
257
258
|
service: options.service,
|
|
258
259
|
accountInfo: stored.accountInfo ?? parsedAccountIdentifier,
|
|
260
|
+
grantedScopes: result.grantedScopes,
|
|
259
261
|
});
|
|
260
262
|
} catch (err) {
|
|
261
263
|
log.error(
|
|
@@ -62,7 +62,10 @@ mock.module("../platform/client.js", () => ({
|
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
|
|
64
64
|
import { BYOOAuthConnection } from "./byo-connection.js";
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
resolveEffectiveBaseUrl,
|
|
67
|
+
resolveOAuthConnection,
|
|
68
|
+
} from "./connection-resolver.js";
|
|
66
69
|
import { PlatformOAuthConnection } from "./platform-connection.js";
|
|
67
70
|
|
|
68
71
|
// ---------------------------------------------------------------------------
|
|
@@ -214,3 +217,65 @@ describe("resolveOAuthConnection", () => {
|
|
|
214
217
|
).rejects.toThrow(/No active OAuth connection found/);
|
|
215
218
|
});
|
|
216
219
|
});
|
|
220
|
+
|
|
221
|
+
describe("resolveEffectiveBaseUrl", () => {
|
|
222
|
+
const fallback = "https://login.salesforce.com";
|
|
223
|
+
|
|
224
|
+
test("uses instance_url from JSON-string metadata for Salesforce", () => {
|
|
225
|
+
const metadata = JSON.stringify({
|
|
226
|
+
instance_url: "https://acme.my.salesforce.com",
|
|
227
|
+
issued_at: "1714000000000",
|
|
228
|
+
});
|
|
229
|
+
expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
|
|
230
|
+
"https://acme.my.salesforce.com",
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("uses instance_url from already-parsed object metadata", () => {
|
|
235
|
+
const metadata = { instance_url: "https://na162.salesforce.com" };
|
|
236
|
+
expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
|
|
237
|
+
"https://na162.salesforce.com",
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("falls back to seed baseUrl when metadata is null", () => {
|
|
242
|
+
expect(resolveEffectiveBaseUrl("salesforce", fallback, null)).toBe(
|
|
243
|
+
fallback,
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("falls back to seed baseUrl when instance_url is empty string", () => {
|
|
248
|
+
const metadata = JSON.stringify({ instance_url: "" });
|
|
249
|
+
expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
|
|
250
|
+
fallback,
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("falls back to seed baseUrl when metadata is unparseable JSON", () => {
|
|
255
|
+
expect(
|
|
256
|
+
resolveEffectiveBaseUrl("salesforce", fallback, "{ not valid json"),
|
|
257
|
+
).toBe(fallback);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("falls back to seed baseUrl when instance_url is the wrong type", () => {
|
|
261
|
+
const metadata = JSON.stringify({ instance_url: 12345 });
|
|
262
|
+
expect(resolveEffectiveBaseUrl("salesforce", fallback, metadata)).toBe(
|
|
263
|
+
fallback,
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("ignores instance_url for non-Salesforce providers", () => {
|
|
268
|
+
// A different provider whose token response happens to include an
|
|
269
|
+
// instance_url-shaped field MUST NOT have its baseUrl rewritten.
|
|
270
|
+
const metadata = JSON.stringify({
|
|
271
|
+
instance_url: "https://attacker.example.com",
|
|
272
|
+
});
|
|
273
|
+
expect(
|
|
274
|
+
resolveEffectiveBaseUrl(
|
|
275
|
+
"google",
|
|
276
|
+
"https://gmail.googleapis.com/gmail/v1/users/me",
|
|
277
|
+
metadata,
|
|
278
|
+
),
|
|
279
|
+
).toBe("https://gmail.googleapis.com/gmail/v1/users/me");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -116,11 +116,65 @@ export async function resolveOAuthConnection(
|
|
|
116
116
|
return new BYOOAuthConnection({
|
|
117
117
|
id: conn.id,
|
|
118
118
|
provider: conn.provider,
|
|
119
|
-
baseUrl,
|
|
119
|
+
baseUrl: resolveEffectiveBaseUrl(conn.provider, baseUrl, conn.metadata),
|
|
120
120
|
accountInfo: conn.accountInfo,
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the effective API base URL for a connection, preferring per-tenant
|
|
126
|
+
* values stored on the connection's `metadata` over the provider's static
|
|
127
|
+
* seed value when applicable.
|
|
128
|
+
*
|
|
129
|
+
* Salesforce is the only provider that needs this: every org has its own
|
|
130
|
+
* API instance host (``acme.my.salesforce.com``, ``na162.salesforce.com``)
|
|
131
|
+
* which is returned in the OAuth token response as ``instance_url`` and
|
|
132
|
+
* captured into ``oauth_connection.metadata`` by ``storeOAuth2Tokens``.
|
|
133
|
+
* The seed's ``baseUrl`` for Salesforce is the login domain
|
|
134
|
+
* (``https://login.salesforce.com``) — correct for the OAuth handshake but
|
|
135
|
+
* wrong for REST API calls. Pulling the per-connection ``instance_url``
|
|
136
|
+
* here avoids forcing every caller to override ``baseUrl`` per-request.
|
|
137
|
+
*
|
|
138
|
+
* For all other providers the seed value is correct (single API domain),
|
|
139
|
+
* so we return it unchanged.
|
|
140
|
+
*
|
|
141
|
+
* If a future provider needs the same treatment, generalize via a
|
|
142
|
+
* declarative ``baseUrlMetadataKey`` field on the seed entry rather than
|
|
143
|
+
* adding more provider-name branches here.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveEffectiveBaseUrl(
|
|
146
|
+
provider: string,
|
|
147
|
+
fallbackBaseUrl: string,
|
|
148
|
+
rawMetadata: unknown,
|
|
149
|
+
): string {
|
|
150
|
+
if (provider !== "salesforce") return fallbackBaseUrl;
|
|
151
|
+
|
|
152
|
+
const metadata = parseConnectionMetadata(rawMetadata);
|
|
153
|
+
const instanceUrl = metadata?.instance_url;
|
|
154
|
+
if (typeof instanceUrl === "string" && instanceUrl.length > 0) {
|
|
155
|
+
return instanceUrl;
|
|
156
|
+
}
|
|
157
|
+
return fallbackBaseUrl;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseConnectionMetadata(
|
|
161
|
+
raw: unknown,
|
|
162
|
+
): Record<string, unknown> | undefined {
|
|
163
|
+
if (raw == null) return undefined;
|
|
164
|
+
if (typeof raw === "object") {
|
|
165
|
+
return raw as Record<string, unknown>;
|
|
166
|
+
}
|
|
167
|
+
if (typeof raw !== "string") return undefined;
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(raw);
|
|
170
|
+
return typeof parsed === "object" && parsed !== null
|
|
171
|
+
? (parsed as Record<string, unknown>)
|
|
172
|
+
: undefined;
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
124
178
|
// ---------------------------------------------------------------------------
|
|
125
179
|
// Platform connection ID resolution
|
|
126
180
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory OAuth connect flow status map.
|
|
3
|
+
*
|
|
4
|
+
* Tracks the current state of daemon-owned OAuth connect flows so the CLI
|
|
5
|
+
* can poll for completion via the IPC route.
|
|
6
|
+
*/
|
|
7
|
+
type OAuthConnectState =
|
|
8
|
+
| { status: "pending"; service: string; expiresAt: number }
|
|
9
|
+
| { status: "complete"; service: string; accountInfo?: string; grantedScopes?: string[]; completedAt: number }
|
|
10
|
+
| { status: "error"; service: string; error: string; failedAt: number };
|
|
11
|
+
|
|
12
|
+
const activeOAuthConnectFlows = new Map<string, OAuthConnectState>();
|
|
13
|
+
|
|
14
|
+
const PENDING_TTL_MS = 5 * 60 * 1000; // 5 min — matches oauth-callback-registry.ts:14
|
|
15
|
+
const COMPLETION_GRACE_MS = 60 * 1000; // 60s so the polling CLI gets one final read
|
|
16
|
+
|
|
17
|
+
export function setOAuthConnectPending(state: string, service: string): void {
|
|
18
|
+
clearExpiredOAuthConnectStates();
|
|
19
|
+
activeOAuthConnectFlows.set(state, {
|
|
20
|
+
status: "pending",
|
|
21
|
+
service,
|
|
22
|
+
expiresAt: Date.now() + PENDING_TTL_MS,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setOAuthConnectComplete(
|
|
27
|
+
state: string,
|
|
28
|
+
service: string,
|
|
29
|
+
accountInfo?: string,
|
|
30
|
+
grantedScopes?: string[],
|
|
31
|
+
): void {
|
|
32
|
+
clearExpiredOAuthConnectStates();
|
|
33
|
+
activeOAuthConnectFlows.set(state, {
|
|
34
|
+
status: "complete",
|
|
35
|
+
service,
|
|
36
|
+
accountInfo,
|
|
37
|
+
grantedScopes,
|
|
38
|
+
completedAt: Date.now(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setOAuthConnectError(
|
|
43
|
+
state: string,
|
|
44
|
+
service: string,
|
|
45
|
+
error: string,
|
|
46
|
+
): void {
|
|
47
|
+
clearExpiredOAuthConnectStates();
|
|
48
|
+
activeOAuthConnectFlows.set(state, {
|
|
49
|
+
status: "error",
|
|
50
|
+
service,
|
|
51
|
+
error,
|
|
52
|
+
failedAt: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getOAuthConnectState(state: string): OAuthConnectState | null {
|
|
57
|
+
clearExpiredOAuthConnectStates();
|
|
58
|
+
return activeOAuthConnectFlows.get(state) ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function clearExpiredOAuthConnectStates(): void {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
for (const [key, state] of activeOAuthConnectFlows) {
|
|
64
|
+
if (state.status === "pending" && now > state.expiresAt) {
|
|
65
|
+
activeOAuthConnectFlows.delete(key);
|
|
66
|
+
} else if (state.status === "complete" && now > state.completedAt + COMPLETION_GRACE_MS) {
|
|
67
|
+
activeOAuthConnectFlows.delete(key);
|
|
68
|
+
} else if (state.status === "error" && now > state.failedAt + COMPLETION_GRACE_MS) {
|
|
69
|
+
activeOAuthConnectFlows.delete(key);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Test-only helper — clears all state for test isolation. */
|
|
75
|
+
export function _clearAllOAuthConnectStates(): void {
|
|
76
|
+
activeOAuthConnectFlows.clear();
|
|
77
|
+
}
|
|
@@ -393,6 +393,7 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
393
393
|
{ scope: "project:delete", description: "Delete entire projects" },
|
|
394
394
|
],
|
|
395
395
|
loopbackPort: 17325,
|
|
396
|
+
managedServiceConfigKey: "todoist-oauth",
|
|
396
397
|
injectionTemplates: [
|
|
397
398
|
{
|
|
398
399
|
hostPattern: "api.todoist.com",
|
|
@@ -402,7 +403,7 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
402
403
|
},
|
|
403
404
|
],
|
|
404
405
|
appType: "App",
|
|
405
|
-
identityUrl: "https://api.todoist.com/
|
|
406
|
+
identityUrl: "https://api.todoist.com/api/v1/sync",
|
|
406
407
|
identityMethod: "POST",
|
|
407
408
|
identityHeaders: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
408
409
|
identityBody: "sync_token=*&resource_types=[%22user%22]",
|
|
@@ -429,6 +430,7 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
429
430
|
availableScopes:
|
|
430
431
|
"https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes",
|
|
431
432
|
loopbackPort: 17326,
|
|
433
|
+
managedServiceConfigKey: "discord-oauth",
|
|
432
434
|
injectionTemplates: [
|
|
433
435
|
{
|
|
434
436
|
hostPattern: "discord.com",
|
|
@@ -497,6 +499,7 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
497
499
|
defaultScopes: ["default"],
|
|
498
500
|
availableScopes: "https://developers.asana.com/docs/oauth-scopes",
|
|
499
501
|
loopbackPort: 17328,
|
|
502
|
+
managedServiceConfigKey: "asana-oauth",
|
|
500
503
|
injectionTemplates: [
|
|
501
504
|
{
|
|
502
505
|
hostPattern: "app.asana.com",
|
|
@@ -563,6 +566,7 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
563
566
|
availableScopes:
|
|
564
567
|
"https://developers.hubspot.com/docs/guides/apps/authentication/scopes",
|
|
565
568
|
loopbackPort: 17330,
|
|
569
|
+
managedServiceConfigKey: "hubspot-oauth",
|
|
566
570
|
injectionTemplates: [
|
|
567
571
|
{
|
|
568
572
|
hostPattern: "api.hubapi.com",
|
|
@@ -576,6 +580,59 @@ export const PROVIDER_SEED_DATA: Record<
|
|
|
576
580
|
identityResponsePaths: ["user", "hub_domain"],
|
|
577
581
|
},
|
|
578
582
|
|
|
583
|
+
salesforce: {
|
|
584
|
+
provider: "salesforce",
|
|
585
|
+
authorizeUrl: "https://login.salesforce.com/services/oauth2/authorize",
|
|
586
|
+
tokenExchangeUrl: "https://login.salesforce.com/services/oauth2/token",
|
|
587
|
+
refreshUrl: "https://login.salesforce.com/services/oauth2/token",
|
|
588
|
+
pingUrl: "https://login.salesforce.com/services/oauth2/userinfo",
|
|
589
|
+
// baseUrl points at the login domain — correct for the OAuth handshake
|
|
590
|
+
// and for ``/services/oauth2/userinfo``/``revoke`` calls. REST API calls
|
|
591
|
+
// to ``/services/data/...`` go to the per-org instance host returned in
|
|
592
|
+
// the token response as ``instance_url`` and stored on
|
|
593
|
+
// ``oauth_connection.metadata``. ``connection-resolver.ts`` substitutes
|
|
594
|
+
// that instance URL when constructing the BYO connection so callers
|
|
595
|
+
// don't need to override ``baseUrl`` per request.
|
|
596
|
+
baseUrl: "https://login.salesforce.com",
|
|
597
|
+
displayLabel: "Salesforce",
|
|
598
|
+
description: "CRM contacts, leads, and opportunities",
|
|
599
|
+
dashboardUrl:
|
|
600
|
+
"https://help.salesforce.com/s/articleView?id=sf.connected_app_create.htm&type=5",
|
|
601
|
+
clientIdPlaceholder: null,
|
|
602
|
+
logoUrl:
|
|
603
|
+
"https://cdn.jsdelivr.net/gh/glincker/thesvg@main/public/icons/salesforce/default.svg",
|
|
604
|
+
defaultScopes: ["api", "refresh_token", "openid", "email", "profile"],
|
|
605
|
+
availableScopes:
|
|
606
|
+
"https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_tokens_scopes.htm",
|
|
607
|
+
authorizeParams: { prompt: "consent" },
|
|
608
|
+
tokenEndpointAuthMethod: "client_secret_post",
|
|
609
|
+
loopbackPort: 17336,
|
|
610
|
+
// Salesforce REST traffic goes to per-org instance hosts like
|
|
611
|
+
// ``acme.my.salesforce.com`` and ``acme.lightning.force.com``.
|
|
612
|
+
// ``matchHostPattern`` only treats ``*.<domain>`` as a wildcard match —
|
|
613
|
+
// bare ``salesforce.com`` would only match the apex. Use wildcards so
|
|
614
|
+
// ``Authorization: Bearer`` injection actually fires on tenant hosts.
|
|
615
|
+
injectionTemplates: [
|
|
616
|
+
{
|
|
617
|
+
hostPattern: "*.salesforce.com",
|
|
618
|
+
injectionType: "header",
|
|
619
|
+
headerName: "Authorization",
|
|
620
|
+
valuePrefix: "Bearer ",
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
hostPattern: "*.force.com",
|
|
624
|
+
injectionType: "header",
|
|
625
|
+
headerName: "Authorization",
|
|
626
|
+
valuePrefix: "Bearer ",
|
|
627
|
+
},
|
|
628
|
+
],
|
|
629
|
+
revokeUrl: "https://login.salesforce.com/services/oauth2/revoke",
|
|
630
|
+
revokeBodyTemplate: { token: "{access_token}" },
|
|
631
|
+
appType: "Connected App",
|
|
632
|
+
identityUrl: "https://login.salesforce.com/services/oauth2/userinfo",
|
|
633
|
+
identityResponsePaths: ["email", "preferred_username"],
|
|
634
|
+
},
|
|
635
|
+
|
|
579
636
|
figma: {
|
|
580
637
|
provider: "figma",
|
|
581
638
|
authorizeUrl: "https://www.figma.com/oauth",
|