@vellumai/assistant 0.4.49 → 0.4.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +24 -33
- package/README.md +3 -3
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/keychain-broker.md +6 -6
- package/docs/architecture/memory.md +180 -119
- package/knip.json +32 -0
- package/package.json +3 -2
- package/src/__tests__/agent-loop.test.ts +3 -1
- package/src/__tests__/anthropic-provider.test.ts +114 -23
- package/src/__tests__/approval-cascade.test.ts +1 -15
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
- package/src/__tests__/btw-routes.test.ts +61 -5
- package/src/__tests__/canonical-guardian-store.test.ts +95 -0
- package/src/__tests__/checker.test.ts +13 -0
- package/src/__tests__/config-schema.test.ts +1 -68
- package/src/__tests__/config-watcher.test.ts +8 -0
- package/src/__tests__/context-memory-e2e.test.ts +11 -100
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +8 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +8 -7
- package/src/__tests__/credential-vault-unit.test.ts +23 -18
- package/src/__tests__/credential-vault.test.ts +30 -18
- package/src/__tests__/credentials-cli.test.ts +257 -82
- package/src/__tests__/cu-unified-flow.test.ts +532 -0
- package/src/__tests__/date-context.test.ts +93 -77
- package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -0
- package/src/__tests__/history-repair.test.ts +245 -0
- package/src/__tests__/host-cu-proxy.test.ts +165 -3
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +36 -7
- package/src/__tests__/integration-status.test.ts +31 -30
- package/src/__tests__/invite-redemption-service.test.ts +166 -13
- package/src/__tests__/invite-routes-http.test.ts +166 -5
- package/src/__tests__/keychain-broker-client.test.ts +4 -4
- package/src/__tests__/list-messages-attachments.test.ts +193 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
- package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
- package/src/__tests__/memory-recall-quality.test.ts +244 -407
- package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
- package/src/__tests__/memory-regressions.test.ts +477 -2841
- package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
- package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
- package/src/__tests__/mime-builder.test.ts +28 -0
- package/src/__tests__/native-web-search.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +824 -31
- package/src/__tests__/oauth-provider-profiles.test.ts +1 -1
- package/src/__tests__/oauth-store.test.ts +363 -17
- package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
- package/src/__tests__/registry.test.ts +0 -1
- package/src/__tests__/relay-server.test.ts +55 -1
- package/src/__tests__/schedule-tools.test.ts +32 -0
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +183 -0
- package/src/__tests__/secure-keys.test.ts +78 -18
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/server-history-render.test.ts +2 -2
- package/src/__tests__/session-abort-tool-results.test.ts +1 -14
- package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
- package/src/__tests__/session-agent-loop.test.ts +19 -15
- package/src/__tests__/session-confirmation-signals.test.ts +1 -15
- package/src/__tests__/session-error.test.ts +124 -2
- package/src/__tests__/session-history-web-search.test.ts +918 -0
- package/src/__tests__/session-pre-run-repair.test.ts +1 -14
- package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
- package/src/__tests__/session-queue.test.ts +37 -27
- package/src/__tests__/session-runtime-assembly.test.ts +54 -0
- package/src/__tests__/session-slash-known.test.ts +1 -15
- package/src/__tests__/session-slash-queue.test.ts +1 -15
- package/src/__tests__/session-slash-unknown.test.ts +1 -15
- package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
- package/src/__tests__/session-workspace-injection.test.ts +3 -37
- package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
- package/src/__tests__/skills-install-extract.test.ts +93 -0
- package/src/__tests__/skills.test.ts +2 -2
- package/src/__tests__/skillssh-registry.test.ts +451 -0
- package/src/__tests__/slack-channel-config.test.ts +10 -8
- package/src/__tests__/trust-store.test.ts +15 -0
- package/src/__tests__/twilio-config.test.ts +11 -10
- package/src/__tests__/twilio-provider.test.ts +9 -4
- package/src/__tests__/voice-invite-redemption.test.ts +85 -5
- package/src/agent/ax-tree-compaction.test.ts +51 -0
- package/src/agent/loop.ts +39 -12
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-request-resolvers.ts +14 -2
- package/src/bundler/compiler-tools.ts +66 -2
- package/src/calls/call-domain.ts +134 -3
- package/src/calls/call-store.ts +6 -0
- package/src/calls/relay-server.ts +44 -6
- package/src/calls/relay-setup-router.ts +17 -1
- package/src/calls/twilio-config.ts +5 -4
- package/src/calls/twilio-provider.ts +14 -9
- package/src/calls/twilio-rest.ts +10 -7
- package/src/calls/types.ts +3 -1
- package/src/cli/commands/config.ts +14 -9
- package/src/cli/commands/contacts.ts +3 -0
- package/src/cli/commands/credentials.ts +170 -174
- package/src/cli/commands/doctor.ts +11 -8
- package/src/cli/commands/keys.ts +9 -9
- package/src/cli/commands/mcp.ts +46 -59
- package/src/cli/commands/memory.ts +16 -165
- package/src/cli/commands/oauth/apps.ts +68 -10
- package/src/cli/commands/oauth/connections.ts +475 -105
- package/src/cli/commands/oauth/index.ts +3 -3
- package/src/cli/commands/oauth/providers.ts +18 -4
- package/src/cli/commands/sessions.ts +5 -2
- package/src/cli/commands/skills.ts +173 -1
- package/src/cli/http-client.ts +0 -20
- package/src/cli/main-screen.tsx +2 -2
- package/src/cli/program.ts +5 -6
- package/src/cli.ts +20 -22
- package/src/config/__tests__/feature-flag-registry-bundled.test.ts +39 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +1 -1
- package/src/config/bundled-skills/computer-use/tools/computer-use-observe.ts +12 -0
- package/src/config/bundled-skills/contacts/SKILL.md +35 -11
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/gmail/TOOLS.json +52 -0
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +13 -3
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +9 -2
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +5 -1
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +5 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +20 -0
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +2 -1
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +8 -2
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +2 -2
- package/src/config/bundled-skills/messaging/tools/shared.ts +7 -5
- package/src/config/bundled-skills/slack/tools/shared.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +1 -1
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +1 -1
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/loader.ts +6 -42
- package/src/config/schema.ts +1 -12
- package/src/config/schemas/memory-lifecycle.ts +0 -9
- package/src/config/schemas/memory-processing.ts +0 -180
- package/src/config/schemas/memory-retrieval.ts +32 -104
- package/src/config/schemas/memory.ts +0 -10
- package/src/config/types.ts +0 -4
- package/src/contacts/contact-store.ts +39 -2
- package/src/contacts/contacts-write.ts +9 -0
- package/src/context/window-manager.ts +4 -1
- package/src/daemon/config-watcher.ts +55 -2
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +114 -31
- package/src/daemon/handlers/config-ingress.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +59 -39
- package/src/daemon/handlers/config-telegram.ts +23 -14
- package/src/daemon/handlers/session-history.ts +1 -358
- package/src/daemon/handlers/sessions.ts +18 -13
- package/src/daemon/handlers/shared.ts +3 -17
- package/src/daemon/handlers/skills.ts +20 -1
- package/src/daemon/history-repair.ts +72 -8
- package/src/daemon/host-cu-proxy.ts +55 -26
- package/src/daemon/lifecycle.ts +39 -4
- package/src/daemon/mcp-reload-service.ts +2 -2
- package/src/daemon/message-types/computer-use.ts +1 -12
- package/src/daemon/message-types/memory.ts +4 -16
- package/src/daemon/message-types/messages.ts +1 -0
- package/src/daemon/message-types/sessions.ts +4 -42
- package/src/daemon/server.ts +6 -1
- package/src/daemon/session-agent-loop-handlers.ts +38 -0
- package/src/daemon/session-agent-loop.ts +334 -48
- package/src/daemon/session-error.ts +89 -6
- package/src/daemon/session-history.ts +17 -7
- package/src/daemon/session-media-retry.ts +6 -2
- package/src/daemon/session-memory.ts +69 -149
- package/src/daemon/session-process.ts +10 -1
- package/src/daemon/session-runtime-assembly.ts +49 -19
- package/src/daemon/session-slash.ts +3 -5
- package/src/daemon/session-surfaces.ts +4 -1
- package/src/daemon/session-tool-setup.ts +7 -1
- package/src/daemon/session.ts +12 -2
- package/src/email/providers/index.ts +2 -2
- package/src/instrument.ts +61 -1
- package/src/media/avatar-router.ts +1 -1
- package/src/memory/admin.ts +2 -191
- package/src/memory/canonical-guardian-store.ts +38 -2
- package/src/memory/conversation-crud.ts +0 -33
- package/src/memory/conversation-queries.ts +25 -83
- package/src/memory/db-init.ts +32 -0
- package/src/memory/embedding-backend.ts +84 -8
- package/src/memory/embedding-types.ts +9 -1
- package/src/memory/indexer.ts +7 -46
- package/src/memory/invite-store.ts +19 -0
- package/src/memory/items-extractor.ts +274 -76
- package/src/memory/job-handlers/backfill.ts +2 -127
- package/src/memory/job-handlers/cleanup.ts +2 -16
- package/src/memory/job-handlers/extraction.ts +2 -138
- package/src/memory/job-handlers/index-maintenance.ts +1 -6
- package/src/memory/job-handlers/summarization.ts +3 -148
- package/src/memory/job-utils.ts +21 -59
- package/src/memory/jobs-store.ts +1 -159
- package/src/memory/jobs-worker.ts +9 -52
- package/src/memory/migrations/104-core-indexes.ts +3 -3
- package/src/memory/migrations/149-oauth-tables.ts +2 -0
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
- package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
- package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
- package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
- package/src/memory/migrations/154-drop-fts.ts +20 -0
- package/src/memory/migrations/155-drop-conflicts.ts +7 -0
- package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
- package/src/memory/migrations/157-invite-contact-id.ts +104 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/qdrant-client.ts +148 -51
- package/src/memory/raw-query.ts +1 -1
- package/src/memory/retriever.test.ts +294 -273
- package/src/memory/retriever.ts +421 -645
- package/src/memory/schema/calls.ts +2 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/memory/schema/memory-core.ts +3 -48
- package/src/memory/schema/oauth.ts +2 -0
- package/src/memory/search/formatting.ts +263 -176
- package/src/memory/search/lexical.ts +1 -254
- package/src/memory/search/ranking.ts +0 -455
- package/src/memory/search/semantic.ts +100 -14
- package/src/memory/search/staleness.ts +47 -0
- package/src/memory/search/tier-classifier.ts +21 -0
- package/src/memory/search/types.ts +15 -77
- package/src/memory/task-memory-cleanup.ts +4 -6
- package/src/messaging/provider.ts +1 -1
- package/src/messaging/providers/gmail/adapter.ts +1 -1
- package/src/messaging/providers/gmail/mime-builder.ts +17 -7
- package/src/messaging/providers/telegram-bot/adapter.ts +17 -8
- package/src/messaging/providers/whatsapp/adapter.ts +13 -9
- package/src/messaging/registry.ts +9 -5
- package/src/oauth/byo-connection.test.ts +40 -25
- package/src/oauth/connect-orchestrator.ts +4 -10
- package/src/oauth/connection-resolver.ts +20 -6
- package/src/oauth/manual-token-connection.ts +5 -5
- package/src/oauth/oauth-store.ts +183 -31
- package/src/oauth/platform-connection.test.ts +1 -1
- package/src/oauth/provider-behaviors.ts +503 -4
- package/src/oauth/seed-providers.ts +214 -8
- package/src/oauth/token-persistence.ts +31 -16
- package/src/permissions/defaults.ts +1 -0
- package/src/permissions/trust-store.ts +23 -1
- package/src/playbooks/playbook-compiler.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -2
- package/src/providers/anthropic/client.ts +56 -126
- package/src/providers/types.ts +7 -1
- package/src/runtime/AGENTS.md +9 -0
- package/src/runtime/auth/route-policy.ts +6 -3
- package/src/runtime/channel-readiness-service.ts +48 -40
- package/src/runtime/guardian-reply-router.ts +24 -22
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/http-types.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +72 -12
- package/src/runtime/invite-service.ts +43 -0
- package/src/runtime/middleware/twilio-validation.ts +1 -1
- package/src/runtime/pending-interactions.ts +2 -2
- package/src/runtime/routes/brain-graph-routes.ts +10 -90
- package/src/runtime/routes/btw-routes.ts +10 -5
- package/src/runtime/routes/conversation-routes.ts +56 -11
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
- package/src/runtime/routes/integrations/slack/channel.ts +2 -2
- package/src/runtime/routes/integrations/telegram.ts +2 -2
- package/src/runtime/routes/integrations/twilio.ts +17 -17
- package/src/runtime/routes/invite-routes.ts +29 -4
- package/src/runtime/routes/memory-item-routes.test.ts +754 -0
- package/src/runtime/routes/memory-item-routes.ts +503 -0
- package/src/runtime/routes/secret-routes.ts +17 -0
- package/src/runtime/routes/session-management-routes.ts +3 -3
- package/src/runtime/routes/settings-routes.ts +3 -3
- package/src/runtime/routes/trust-rules-routes.ts +14 -0
- package/src/runtime/routes/workspace-routes.ts +9 -4
- package/src/runtime/routes/workspace-utils.ts +8 -2
- package/src/schedule/integration-status.ts +26 -19
- package/src/security/keychain-broker-client.ts +17 -4
- package/src/security/oauth2.ts +6 -7
- package/src/security/secure-keys.ts +44 -19
- package/src/security/token-manager.ts +46 -39
- package/src/services/vercel-deploy.ts +0 -24
- package/src/signals/confirm.ts +78 -0
- package/src/signals/mcp-reload.ts +18 -0
- package/src/skills/catalog-install.ts +74 -18
- package/src/skills/skillssh-registry.ts +503 -0
- package/src/tools/assets/search.ts +5 -1
- package/src/tools/computer-use/definitions.ts +0 -10
- package/src/tools/computer-use/registry.ts +1 -1
- package/src/tools/credentials/vault.ts +22 -7
- package/src/tools/memory/definitions.ts +4 -13
- package/src/tools/memory/handlers.test.ts +83 -103
- package/src/tools/memory/handlers.ts +50 -85
- package/src/tools/network/script-proxy/session-manager.ts +8 -8
- package/src/tools/schedule/create.ts +10 -3
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/skills/load.ts +25 -2
- package/src/watcher/provider-types.ts +1 -1
- package/src/watcher/providers/github.ts +1 -1
- package/src/watcher/providers/gmail.ts +3 -3
- package/src/watcher/providers/google-calendar.ts +3 -3
- package/src/watcher/providers/linear.ts +1 -1
- package/src/__tests__/clarification-resolver.test.ts +0 -193
- package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
- package/src/__tests__/conflict-policy.test.ts +0 -269
- package/src/__tests__/conflict-store.test.ts +0 -372
- package/src/__tests__/contradiction-checker.test.ts +0 -361
- package/src/__tests__/entity-extractor.test.ts +0 -211
- package/src/__tests__/entity-search.test.ts +0 -1117
- package/src/__tests__/profile-compiler.test.ts +0 -392
- package/src/__tests__/session-conflict-gate.test.ts +0 -1228
- package/src/__tests__/session-profile-injection.test.ts +0 -557
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
- package/src/daemon/session-conflict-gate.ts +0 -167
- package/src/daemon/session-dynamic-profile.ts +0 -77
- package/src/memory/clarification-resolver.ts +0 -417
- package/src/memory/conflict-intent.ts +0 -205
- package/src/memory/conflict-policy.ts +0 -127
- package/src/memory/conflict-store.ts +0 -410
- package/src/memory/contradiction-checker.ts +0 -508
- package/src/memory/entity-extractor.ts +0 -535
- package/src/memory/format-recall.ts +0 -47
- package/src/memory/fts-reconciler.ts +0 -165
- package/src/memory/job-handlers/conflict.ts +0 -200
- package/src/memory/profile-compiler.ts +0 -195
- package/src/memory/recall-cache.ts +0 -117
- package/src/memory/search/entity.ts +0 -535
- package/src/memory/search/query-expansion.test.ts +0 -70
- package/src/memory/search/query-expansion.ts +0 -118
- package/src/runtime/routes/mcp-routes.ts +0 -20
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
interface IntegrationProbe {
|
|
8
8
|
name: string;
|
|
9
9
|
category: string;
|
|
10
|
-
isConnected: () => boolean
|
|
10
|
+
isConnected: () => Promise<boolean>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// Registry — add new integrations here:
|
|
@@ -15,7 +15,7 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
15
15
|
{
|
|
16
16
|
name: "Gmail",
|
|
17
17
|
category: "email",
|
|
18
|
-
isConnected: () => isProviderConnected("integration:
|
|
18
|
+
isConnected: () => isProviderConnected("integration:google"),
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
name: "Slack",
|
|
@@ -25,39 +25,46 @@ const INTEGRATION_PROBES: IntegrationProbe[] = [
|
|
|
25
25
|
{
|
|
26
26
|
name: "Twilio",
|
|
27
27
|
category: "telephony",
|
|
28
|
-
isConnected: () => hasTwilioCredentials(),
|
|
28
|
+
isConnected: async () => hasTwilioCredentials(),
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: "Telegram",
|
|
32
32
|
category: "messaging",
|
|
33
|
-
isConnected: () => {
|
|
33
|
+
isConnected: async () => {
|
|
34
34
|
const conn = getConnectionByProvider("telegram");
|
|
35
35
|
return !!(conn && conn.status === "active");
|
|
36
36
|
},
|
|
37
37
|
},
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
export function getIntegrationSummary():
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
export async function getIntegrationSummary(): Promise<
|
|
41
|
+
Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
category: string;
|
|
44
|
+
connected: boolean;
|
|
45
|
+
}>
|
|
46
|
+
> {
|
|
47
|
+
return Promise.all(
|
|
48
|
+
INTEGRATION_PROBES.map(async (probe) => ({
|
|
49
|
+
name: probe.name,
|
|
50
|
+
category: probe.category,
|
|
51
|
+
connected: await probe.isConnected(),
|
|
52
|
+
})),
|
|
53
|
+
);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
export function formatIntegrationSummary(): string {
|
|
53
|
-
const summary = getIntegrationSummary();
|
|
56
|
+
export async function formatIntegrationSummary(): Promise<string> {
|
|
57
|
+
const summary = await getIntegrationSummary();
|
|
54
58
|
return summary
|
|
55
59
|
.map((s) => `${s.name} ${s.connected ? "\u2713" : "\u2717"}`)
|
|
56
60
|
.join(" | ");
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
export function hasCapability(category: string): boolean {
|
|
60
|
-
|
|
61
|
-
(probe) => probe.category === category
|
|
63
|
+
export async function hasCapability(category: string): Promise<boolean> {
|
|
64
|
+
const results = await Promise.all(
|
|
65
|
+
INTEGRATION_PROBES.filter((probe) => probe.category === category).map(
|
|
66
|
+
(probe) => probe.isConnected(),
|
|
67
|
+
),
|
|
62
68
|
);
|
|
69
|
+
return results.some(Boolean);
|
|
63
70
|
}
|
|
@@ -33,11 +33,18 @@ const REQUEST_TIMEOUT_MS = 5_000;
|
|
|
33
33
|
* back); `{ found: false }` means the key doesn't exist in the keychain. */
|
|
34
34
|
export type BrokerGetResult = { found: boolean; value?: string } | null;
|
|
35
35
|
|
|
36
|
+
/** Result of a `set()` call — distinguishes broker-unreachable from an active
|
|
37
|
+
* rejection so callers can log meaningful diagnostics. */
|
|
38
|
+
export type BrokerSetResult =
|
|
39
|
+
| { status: "ok" }
|
|
40
|
+
| { status: "unreachable" }
|
|
41
|
+
| { status: "rejected"; code: string; message: string };
|
|
42
|
+
|
|
36
43
|
export interface KeychainBrokerClient {
|
|
37
44
|
isAvailable(): boolean;
|
|
38
45
|
ping(): Promise<{ pong: boolean } | null>;
|
|
39
46
|
get(account: string): Promise<BrokerGetResult>;
|
|
40
|
-
set(account: string, value: string): Promise<
|
|
47
|
+
set(account: string, value: string): Promise<BrokerSetResult>;
|
|
41
48
|
del(account: string): Promise<boolean>;
|
|
42
49
|
list(): Promise<string[]>;
|
|
43
50
|
}
|
|
@@ -360,12 +367,18 @@ export function createBrokerClient(): KeychainBrokerClient {
|
|
|
360
367
|
}
|
|
361
368
|
},
|
|
362
369
|
|
|
363
|
-
async set(account: string, value: string): Promise<
|
|
370
|
+
async set(account: string, value: string): Promise<BrokerSetResult> {
|
|
364
371
|
try {
|
|
365
372
|
const response = await doRequest("key.set", { account, value });
|
|
366
|
-
|
|
373
|
+
if (!response) return { status: "unreachable" };
|
|
374
|
+
if (response.ok) return { status: "ok" };
|
|
375
|
+
return {
|
|
376
|
+
status: "rejected",
|
|
377
|
+
code: response.error?.code ?? "UNKNOWN",
|
|
378
|
+
message: response.error?.message ?? "unknown error",
|
|
379
|
+
};
|
|
367
380
|
} catch {
|
|
368
|
-
return
|
|
381
|
+
return { status: "unreachable" };
|
|
369
382
|
}
|
|
370
383
|
},
|
|
371
384
|
|
package/src/security/oauth2.ts
CHANGED
|
@@ -420,18 +420,17 @@ export interface OAuth2PreparedFlow {
|
|
|
420
420
|
* URL directly in chat and the callback arrives asynchronously via the gateway.
|
|
421
421
|
*
|
|
422
422
|
* Supports two transports:
|
|
423
|
-
* - **
|
|
424
|
-
*
|
|
425
|
-
* - **
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* (channel) sessions.
|
|
423
|
+
* - **loopback** (default): starts a temporary localhost server to receive the
|
|
424
|
+
* callback. Works without any public URL or tunnel.
|
|
425
|
+
* - **gateway**: routes callbacks through the public ingress URL.
|
|
426
|
+
* Requires `ingress.publicBaseUrl` to be configured. Used for providers that
|
|
427
|
+
* don't support localhost redirects (e.g. Twitter, Notion).
|
|
429
428
|
*/
|
|
430
429
|
export async function prepareOAuth2Flow(
|
|
431
430
|
config: OAuth2Config,
|
|
432
431
|
options?: OAuth2FlowOptions,
|
|
433
432
|
): Promise<OAuth2PreparedFlow> {
|
|
434
|
-
const transport = options?.callbackTransport ?? "
|
|
433
|
+
const transport = options?.callbackTransport ?? "loopback";
|
|
435
434
|
|
|
436
435
|
if (transport === "loopback") {
|
|
437
436
|
return prepareLoopbackFlow(config, options?.loopbackPort);
|
|
@@ -3,14 +3,17 @@
|
|
|
3
3
|
* available (macOS app embedded), with transparent fallback to the
|
|
4
4
|
* encrypted-at-rest file store.
|
|
5
5
|
*
|
|
6
|
-
* Async variants try the
|
|
6
|
+
* Async variants try the encrypted store first; sync variants always use the
|
|
7
7
|
* encrypted store (startup code paths cannot do async I/O).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { getLogger } from "../util/logger.js";
|
|
10
11
|
import * as encryptedStore from "./encrypted-store.js";
|
|
11
12
|
import type { KeychainBrokerClient } from "./keychain-broker-client.js";
|
|
12
13
|
import { createBrokerClient } from "./keychain-broker-client.js";
|
|
13
14
|
|
|
15
|
+
const log = getLogger("secure-keys");
|
|
16
|
+
|
|
14
17
|
let _broker: KeychainBrokerClient | undefined;
|
|
15
18
|
|
|
16
19
|
function getBroker(): KeychainBrokerClient {
|
|
@@ -79,30 +82,33 @@ export function isDowngradedFromKeychain(): boolean {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
// ---------------------------------------------------------------------------
|
|
82
|
-
// Async variants — try
|
|
85
|
+
// Async variants — try encrypted store first, fall back to broker
|
|
83
86
|
// ---------------------------------------------------------------------------
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
|
-
* Async version of `getSecureKey`.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* while the broker was unavailable or via sync `setSecureKey`).
|
|
89
|
+
* Async version of `getSecureKey`. Checks the encrypted store first
|
|
90
|
+
* (instant) since `setSecureKeyAsync` always writes to both stores.
|
|
91
|
+
* Falls back to the broker for keys that may exist only in the macOS
|
|
92
|
+
* Keychain. Returns `undefined` if the key is not found in either store.
|
|
91
93
|
*/
|
|
92
94
|
export async function getSecureKeyAsync(
|
|
93
95
|
account: string,
|
|
94
96
|
): Promise<string | undefined> {
|
|
97
|
+
// Check encrypted store first (sync, instant). Since setSecureKeyAsync
|
|
98
|
+
// always writes to both broker and encrypted store, a hit here is
|
|
99
|
+
// authoritative and avoids the broker IPC round-trip.
|
|
100
|
+
const encResult = encryptedStore.getKey(account);
|
|
101
|
+
if (encResult != null && encResult.length > 0) return encResult;
|
|
102
|
+
|
|
103
|
+
// Not in encrypted store — try broker as fallback for keys that may
|
|
104
|
+
// exist only in the macOS Keychain (e.g. written by the app directly).
|
|
95
105
|
const broker = getBroker();
|
|
96
106
|
if (broker.isAvailable()) {
|
|
97
107
|
const result = await broker.get(account);
|
|
98
|
-
|
|
99
|
-
if (result == null) return encryptedStore.getKey(account);
|
|
100
|
-
// Broker found the key — use it
|
|
101
|
-
if (result.found) return result.value;
|
|
102
|
-
// Broker says not found — check encrypted store as fallback
|
|
103
|
-
return encryptedStore.getKey(account);
|
|
108
|
+
if (result?.found) return result.value;
|
|
104
109
|
}
|
|
105
|
-
|
|
110
|
+
|
|
111
|
+
return undefined;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
/**
|
|
@@ -112,7 +118,7 @@ export async function getSecureKeyAsync(
|
|
|
112
118
|
*
|
|
113
119
|
* If the broker is available but `broker.set()` fails we return `false`
|
|
114
120
|
* immediately — falling through to an encrypted-store-only write would
|
|
115
|
-
* leave the
|
|
121
|
+
* leave the two stores out of sync.
|
|
116
122
|
*/
|
|
117
123
|
export async function setSecureKeyAsync(
|
|
118
124
|
account: string,
|
|
@@ -120,13 +126,32 @@ export async function setSecureKeyAsync(
|
|
|
120
126
|
): Promise<boolean> {
|
|
121
127
|
const broker = getBroker();
|
|
122
128
|
if (broker.isAvailable()) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
129
|
+
const result = await broker.set(account, value);
|
|
130
|
+
if (result.status !== "ok") {
|
|
131
|
+
log.warn(
|
|
132
|
+
{
|
|
133
|
+
account,
|
|
134
|
+
brokerStatus: result.status,
|
|
135
|
+
...(result.status === "rejected"
|
|
136
|
+
? { brokerCode: result.code, brokerMessage: result.message }
|
|
137
|
+
: {}),
|
|
138
|
+
},
|
|
139
|
+
"Broker set failed for secure key",
|
|
140
|
+
);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
125
143
|
// Broker succeeded — also persist to encrypted store for sync callers.
|
|
126
144
|
const encOk = encryptedStore.setKey(account, value);
|
|
145
|
+
if (!encOk) {
|
|
146
|
+
log.warn({ account }, "Encrypted store set failed after broker success");
|
|
147
|
+
}
|
|
127
148
|
return encOk;
|
|
128
149
|
}
|
|
129
|
-
|
|
150
|
+
const encOk = encryptedStore.setKey(account, value);
|
|
151
|
+
if (!encOk) {
|
|
152
|
+
log.warn({ account }, "Encrypted store set failed (broker unavailable)");
|
|
153
|
+
}
|
|
154
|
+
return encOk;
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
/**
|
|
@@ -139,7 +164,7 @@ export async function setSecureKeyAsync(
|
|
|
139
164
|
*
|
|
140
165
|
* If the broker is available but `broker.del()` fails we return `"error"`
|
|
141
166
|
* immediately — falling through to an encrypted-store-only delete would
|
|
142
|
-
* leave the broker with the key,
|
|
167
|
+
* leave the broker with the key, causing stale reads on broker fallback.
|
|
143
168
|
*/
|
|
144
169
|
export async function deleteSecureKeyAsync(
|
|
145
170
|
account: string,
|
|
@@ -9,17 +9,22 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
getApp,
|
|
12
|
+
getConnection,
|
|
12
13
|
getConnectionByProvider,
|
|
13
14
|
getProvider,
|
|
14
15
|
updateConnection,
|
|
15
16
|
} from "../oauth/oauth-store.js";
|
|
16
17
|
import { getLogger } from "../util/logger.js";
|
|
17
18
|
import { refreshOAuth2Token, type TokenEndpointAuthMethod } from "./oauth2.js";
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
getSecureKey,
|
|
21
|
+
getSecureKeyAsync,
|
|
22
|
+
setSecureKeyAsync,
|
|
23
|
+
} from "./secure-keys.js";
|
|
19
24
|
|
|
20
25
|
const log = getLogger("token-manager");
|
|
21
26
|
|
|
22
|
-
const MESSAGING_SERVICES = new Set(["integration:
|
|
27
|
+
const MESSAGING_SERVICES = new Set(["integration:google", "integration:slack"]);
|
|
23
28
|
|
|
24
29
|
function recoveryHint(service: string): string {
|
|
25
30
|
const shortName = service.startsWith("integration:")
|
|
@@ -116,14 +121,14 @@ function recordRefreshFailure(service: string): void {
|
|
|
116
121
|
|
|
117
122
|
const inflightRefreshes = new Map<string, Promise<string>>();
|
|
118
123
|
|
|
119
|
-
function deduplicatedRefresh(service: string): Promise<string> {
|
|
120
|
-
const existing = inflightRefreshes.get(
|
|
124
|
+
function deduplicatedRefresh(service: string, connId: string): Promise<string> {
|
|
125
|
+
const existing = inflightRefreshes.get(connId);
|
|
121
126
|
if (existing) return existing;
|
|
122
127
|
|
|
123
|
-
const promise = doRefresh(service).finally(() => {
|
|
124
|
-
inflightRefreshes.delete(
|
|
128
|
+
const promise = doRefresh(service, connId).finally(() => {
|
|
129
|
+
inflightRefreshes.delete(connId);
|
|
125
130
|
});
|
|
126
|
-
inflightRefreshes.set(
|
|
131
|
+
inflightRefreshes.set(connId, promise);
|
|
127
132
|
return promise;
|
|
128
133
|
}
|
|
129
134
|
|
|
@@ -157,18 +162,11 @@ export class TokenExpiredError extends Error {
|
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
/**
|
|
160
|
-
* Check whether
|
|
161
|
-
* within the buffer window, based on the `expiresAt` field in the
|
|
162
|
-
* oauth_connection row.
|
|
165
|
+
* Check whether a token is expired or will expire within the buffer window.
|
|
163
166
|
*/
|
|
164
|
-
function isTokenExpired(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (!conn?.expiresAt) return false;
|
|
168
|
-
return Date.now() >= conn.expiresAt - EXPIRY_BUFFER_MS;
|
|
169
|
-
} catch {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
167
|
+
function isTokenExpired(expiresAt: number | null): boolean {
|
|
168
|
+
if (!expiresAt) return false;
|
|
169
|
+
return Date.now() >= expiresAt - EXPIRY_BUFFER_MS;
|
|
172
170
|
}
|
|
173
171
|
|
|
174
172
|
// ── Refresh config resolution ─────────────────────────────────────────
|
|
@@ -191,8 +189,11 @@ interface RefreshConfig {
|
|
|
191
189
|
* authMethod. Throws `TokenExpiredError` if the connection is not found
|
|
192
190
|
* or incomplete.
|
|
193
191
|
*/
|
|
194
|
-
function resolveRefreshConfig(
|
|
195
|
-
|
|
192
|
+
async function resolveRefreshConfig(
|
|
193
|
+
service: string,
|
|
194
|
+
connId: string,
|
|
195
|
+
): Promise<RefreshConfig> {
|
|
196
|
+
const conn = getConnection(connId);
|
|
196
197
|
if (!conn) {
|
|
197
198
|
throw new TokenExpiredError(
|
|
198
199
|
service,
|
|
@@ -217,17 +218,17 @@ function resolveRefreshConfig(service: string): RefreshConfig {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
const tokenUrl = provider.tokenUrl;
|
|
220
|
-
const
|
|
221
|
-
if (!tokenUrl || !
|
|
221
|
+
const resolvedClientId = app.clientId;
|
|
222
|
+
if (!tokenUrl || !resolvedClientId) {
|
|
222
223
|
throw new TokenExpiredError(
|
|
223
224
|
service,
|
|
224
225
|
`Missing OAuth2 refresh config for "${service}".${recoveryHint(service)}`,
|
|
225
226
|
);
|
|
226
227
|
}
|
|
227
228
|
|
|
228
|
-
const secret =
|
|
229
|
+
const secret = await getSecureKeyAsync(app.clientSecretCredentialPath);
|
|
229
230
|
|
|
230
|
-
const refreshToken =
|
|
231
|
+
const refreshToken = await getSecureKeyAsync(
|
|
231
232
|
`oauth_connection/${conn.id}/refresh_token`,
|
|
232
233
|
);
|
|
233
234
|
|
|
@@ -238,7 +239,7 @@ function resolveRefreshConfig(service: string): RefreshConfig {
|
|
|
238
239
|
return {
|
|
239
240
|
connId: conn.id,
|
|
240
241
|
tokenUrl,
|
|
241
|
-
clientId,
|
|
242
|
+
clientId: resolvedClientId,
|
|
242
243
|
secret,
|
|
243
244
|
refreshToken,
|
|
244
245
|
authMethod,
|
|
@@ -254,10 +255,15 @@ function resolveRefreshConfig(service: string): RefreshConfig {
|
|
|
254
255
|
* Returns the new access token on success.
|
|
255
256
|
* Throws `TokenExpiredError` if refresh is not possible.
|
|
256
257
|
*/
|
|
257
|
-
async function doRefresh(service: string): Promise<string> {
|
|
258
|
-
const refreshConfig = resolveRefreshConfig(service);
|
|
259
|
-
const {
|
|
260
|
-
|
|
258
|
+
async function doRefresh(service: string, connId: string): Promise<string> {
|
|
259
|
+
const refreshConfig = await resolveRefreshConfig(service, connId);
|
|
260
|
+
const {
|
|
261
|
+
tokenUrl,
|
|
262
|
+
clientId: resolvedClientId,
|
|
263
|
+
secret,
|
|
264
|
+
authMethod,
|
|
265
|
+
refreshToken,
|
|
266
|
+
} = refreshConfig;
|
|
261
267
|
|
|
262
268
|
if (!refreshToken) {
|
|
263
269
|
throw new TokenExpiredError(
|
|
@@ -266,8 +272,8 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
266
272
|
);
|
|
267
273
|
}
|
|
268
274
|
|
|
269
|
-
if (isRefreshBreakerOpen(
|
|
270
|
-
const state = refreshBreakers.get(
|
|
275
|
+
if (isRefreshBreakerOpen(connId)) {
|
|
276
|
+
const state = refreshBreakers.get(connId)!;
|
|
271
277
|
const remainingMs = state.cooldownMs - (Date.now() - state.openedAt);
|
|
272
278
|
throw new TokenExpiredError(
|
|
273
279
|
service,
|
|
@@ -282,13 +288,13 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
282
288
|
try {
|
|
283
289
|
result = await refreshOAuth2Token(
|
|
284
290
|
tokenUrl,
|
|
285
|
-
|
|
291
|
+
resolvedClientId,
|
|
286
292
|
refreshToken,
|
|
287
293
|
secret,
|
|
288
294
|
authMethod,
|
|
289
295
|
);
|
|
290
296
|
} catch (err) {
|
|
291
|
-
recordRefreshFailure(
|
|
297
|
+
recordRefreshFailure(connId);
|
|
292
298
|
if (isCredentialError(err)) {
|
|
293
299
|
const msg = err instanceof Error ? err.message : String(err);
|
|
294
300
|
throw new TokenExpiredError(
|
|
@@ -349,7 +355,7 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
349
355
|
);
|
|
350
356
|
}
|
|
351
357
|
|
|
352
|
-
recordRefreshSuccess(
|
|
358
|
+
recordRefreshSuccess(connId);
|
|
353
359
|
log.info({ service }, "OAuth2 access token refreshed successfully");
|
|
354
360
|
return result.accessToken;
|
|
355
361
|
}
|
|
@@ -368,12 +374,13 @@ async function doRefresh(service: string): Promise<string> {
|
|
|
368
374
|
export async function withValidToken<T>(
|
|
369
375
|
service: string,
|
|
370
376
|
callback: (token: string) => Promise<T>,
|
|
377
|
+
clientId?: string,
|
|
371
378
|
): Promise<T> {
|
|
372
|
-
const conn = getConnectionByProvider(service);
|
|
379
|
+
const conn = getConnectionByProvider(service, clientId);
|
|
373
380
|
let token = conn
|
|
374
381
|
? getSecureKey(`oauth_connection/${conn.id}/access_token`)
|
|
375
382
|
: undefined;
|
|
376
|
-
if (!token) {
|
|
383
|
+
if (!token || !conn) {
|
|
377
384
|
throw new TokenExpiredError(
|
|
378
385
|
service,
|
|
379
386
|
`No access token found for "${service}". Authorization required.${recoveryHint(service)}`,
|
|
@@ -381,15 +388,15 @@ export async function withValidToken<T>(
|
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
// Proactively refresh if expired or about to expire.
|
|
384
|
-
if (isTokenExpired(
|
|
385
|
-
token = await deduplicatedRefresh(service);
|
|
391
|
+
if (isTokenExpired(conn.expiresAt)) {
|
|
392
|
+
token = await deduplicatedRefresh(service, conn.id);
|
|
386
393
|
}
|
|
387
394
|
|
|
388
395
|
try {
|
|
389
396
|
return await callback(token);
|
|
390
397
|
} catch (err: unknown) {
|
|
391
398
|
if (is401Error(err)) {
|
|
392
|
-
token = await deduplicatedRefresh(service);
|
|
399
|
+
token = await deduplicatedRefresh(service, conn.id);
|
|
393
400
|
return callback(token);
|
|
394
401
|
}
|
|
395
402
|
throw err;
|
|
@@ -55,27 +55,3 @@ export async function deployHtmlToVercel(opts: {
|
|
|
55
55
|
|
|
56
56
|
return { url: publicUrl, deploymentId: data.id };
|
|
57
57
|
}
|
|
58
|
-
|
|
59
|
-
export async function deleteVercelDeployment(
|
|
60
|
-
deploymentId: string,
|
|
61
|
-
token: string,
|
|
62
|
-
): Promise<void> {
|
|
63
|
-
const response = await fetch(
|
|
64
|
-
`https://api.vercel.com/v13/deployments/${deploymentId}`,
|
|
65
|
-
{
|
|
66
|
-
method: "DELETE",
|
|
67
|
-
headers: {
|
|
68
|
-
Authorization: `Bearer ${token}`,
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
const text = await response.text();
|
|
75
|
-
throw new ProviderError(
|
|
76
|
-
`Vercel delete deployment failed (${response.status}): ${text}`,
|
|
77
|
-
"vercel",
|
|
78
|
-
response.status,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle confirmation decisions delivered via signal files from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* The built-in CLI writes JSON to `signals/confirm` instead of making an
|
|
5
|
+
* HTTP POST to `/v1/confirm`. The daemon's ConfigWatcher detects the file
|
|
6
|
+
* change and invokes {@link handleConfirmationSignal}, which reads the
|
|
7
|
+
* payload and resolves the pending interaction in-process.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import type { UserDecision } from "../permissions/types.js";
|
|
14
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
15
|
+
import { getLogger } from "../util/logger.js";
|
|
16
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
17
|
+
|
|
18
|
+
const log = getLogger("signal:confirm");
|
|
19
|
+
|
|
20
|
+
const VALID_DECISIONS: ReadonlySet<string> = new Set<string>([
|
|
21
|
+
"allow",
|
|
22
|
+
"allow_10m",
|
|
23
|
+
"allow_thread",
|
|
24
|
+
"always_allow",
|
|
25
|
+
"always_allow_high_risk",
|
|
26
|
+
"deny",
|
|
27
|
+
"always_deny",
|
|
28
|
+
"temporary_override",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
function isUserDecision(value: string): value is UserDecision {
|
|
32
|
+
return VALID_DECISIONS.has(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the `signals/confirm` file and resolve the pending interaction.
|
|
37
|
+
* Called by ConfigWatcher when the signal file is written or modified.
|
|
38
|
+
*/
|
|
39
|
+
export function handleConfirmationSignal(): void {
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(
|
|
42
|
+
join(getWorkspaceDir(), "signals", "confirm"),
|
|
43
|
+
"utf-8",
|
|
44
|
+
);
|
|
45
|
+
const parsed = JSON.parse(content) as {
|
|
46
|
+
requestId?: string;
|
|
47
|
+
decision?: string;
|
|
48
|
+
};
|
|
49
|
+
const { requestId, decision } = parsed;
|
|
50
|
+
|
|
51
|
+
if (!requestId || typeof requestId !== "string") {
|
|
52
|
+
log.warn("Confirmation signal missing requestId");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!decision || !isUserDecision(decision)) {
|
|
56
|
+
log.warn({ decision }, "Confirmation signal has invalid decision");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const interaction = pendingInteractions.resolve(requestId);
|
|
61
|
+
if (!interaction) {
|
|
62
|
+
log.warn({ requestId }, "No pending interaction for confirmation signal");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interaction.session.handleConfirmationResponse(
|
|
67
|
+
requestId,
|
|
68
|
+
decision,
|
|
69
|
+
undefined,
|
|
70
|
+
undefined,
|
|
71
|
+
undefined,
|
|
72
|
+
{ source: "button" },
|
|
73
|
+
);
|
|
74
|
+
log.info({ requestId, decision }, "Confirmation resolved via signal file");
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.error({ err }, "Failed to handle confirmation signal");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle MCP reload signals from the CLI.
|
|
3
|
+
*
|
|
4
|
+
* When the CLI writes to `signals/mcp-reload`, the daemon's ConfigWatcher
|
|
5
|
+
* detects the file change and invokes {@link handleMcpReloadSignal} to
|
|
6
|
+
* restart MCP servers with the latest configuration.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { reloadMcpServers } from "../daemon/mcp-reload-service.js";
|
|
10
|
+
import { getLogger } from "../util/logger.js";
|
|
11
|
+
|
|
12
|
+
const log = getLogger("signal:mcp-reload");
|
|
13
|
+
|
|
14
|
+
export function handleMcpReloadSignal(): void {
|
|
15
|
+
reloadMcpServers().catch((err: unknown) => {
|
|
16
|
+
log.error({ err }, "MCP reload triggered by signal file failed");
|
|
17
|
+
});
|
|
18
|
+
}
|