@vellumai/assistant 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +2 -0
- package/README.md +88 -2
- package/eslint.config.mjs +31 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
- package/scripts/ipc/generate-swift.ts +31 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
- package/src/__tests__/approval-message-composer.test.ts +253 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-domain.test.ts +12 -2
- package/src/__tests__/call-orchestrator.test.ts +799 -249
- package/src/__tests__/call-pointer-messages.test.ts +148 -0
- package/src/__tests__/call-recovery.test.ts +3 -0
- package/src/__tests__/call-routes-http.test.ts +32 -2
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1277 -98
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +36 -50
- package/src/__tests__/channel-guardian.test.ts +630 -22
- package/src/__tests__/channel-readiness-service.test.ts +324 -0
- package/src/__tests__/checker.test.ts +14 -7
- package/src/__tests__/clarification-resolver.test.ts +44 -24
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
- package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
- package/src/__tests__/config-schema.test.ts +14 -8
- package/src/__tests__/context-window-manager.test.ts +30 -2
- package/src/__tests__/contradiction-checker.test.ts +20 -5
- package/src/__tests__/credential-security-invariants.test.ts +7 -2
- package/src/__tests__/daemon-lifecycle.test.ts +13 -12
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dictation-mode-detection.test.ts +63 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
- package/src/__tests__/entity-search.test.ts +615 -0
- package/src/__tests__/fuzzy-match-property.test.ts +5 -5
- package/src/__tests__/guardian-action-store.test.ts +123 -0
- package/src/__tests__/guardian-action-sweep.test.ts +277 -0
- package/src/__tests__/guardian-dispatch.test.ts +389 -0
- package/src/__tests__/guardian-question-copy.test.ts +47 -0
- package/src/__tests__/handlers-telegram-config.test.ts +4 -2
- package/src/__tests__/handlers-twilio-config.test.ts +533 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +291 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
- package/src/__tests__/messaging-send-tool.test.ts +65 -0
- package/src/__tests__/model-intents.test.ts +96 -0
- package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
- package/src/__tests__/provider-error-scenarios.test.ts +621 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
- package/src/__tests__/qdrant-manager.test.ts +27 -20
- package/src/__tests__/relay-server.test.ts +779 -40
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
- package/src/__tests__/run-orchestrator.test.ts +42 -4
- package/src/__tests__/runtime-runs-http.test.ts +17 -1
- package/src/__tests__/runtime-runs.test.ts +16 -0
- package/src/__tests__/schedule-store.test.ts +18 -4
- package/src/__tests__/scheduler-recurrence.test.ts +13 -4
- package/src/__tests__/session-abort-tool-results.test.ts +6 -0
- package/src/__tests__/session-agent-loop.test.ts +857 -0
- package/src/__tests__/session-conflict-gate.test.ts +6 -0
- package/src/__tests__/session-pre-run-repair.test.ts +6 -0
- package/src/__tests__/session-profile-injection.test.ts +6 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/session-queue.test.ts +6 -0
- package/src/__tests__/session-runtime-assembly.test.ts +321 -13
- package/src/__tests__/session-slash-known.test.ts +6 -0
- package/src/__tests__/session-slash-queue.test.ts +6 -0
- package/src/__tests__/session-slash-unknown.test.ts +6 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
- package/src/__tests__/session-workspace-injection.test.ts +6 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/skills.test.ts +2 -0
- package/src/__tests__/sms-messaging-provider.test.ts +126 -0
- package/src/__tests__/starter-task-flow.test.ts +2 -0
- package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +2 -0
- package/src/__tests__/task-management-tools.test.ts +2 -2
- package/src/__tests__/task-runner.test.ts +14 -4
- package/src/__tests__/terminal-tools.test.ts +25 -19
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
- package/src/__tests__/tool-executor.test.ts +23 -24
- package/src/__tests__/trust-store.test.ts +3 -3
- package/src/__tests__/twilio-rest.test.ts +29 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
- package/src/__tests__/twilio-routes.test.ts +167 -11
- package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
- package/src/__tests__/user-reference.test.ts +2 -0
- package/src/__tests__/voice-quality.test.ts +222 -0
- package/src/__tests__/web-search.test.ts +46 -30
- package/src/__tests__/work-item-output.test.ts +110 -0
- package/src/agent/loop.ts +1 -1
- package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
- package/src/amazon/client.ts +1418 -0
- package/src/amazon/request-extractor.ts +135 -0
- package/src/amazon/session.ts +109 -0
- package/src/autonomy/autonomy-store.ts +5 -5
- package/src/browser-extension-relay/client.ts +124 -0
- package/src/browser-extension-relay/protocol.ts +63 -0
- package/src/browser-extension-relay/server.ts +177 -0
- package/src/bundler/app-bundler.ts +3 -3
- package/src/bundler/bundle-signer.ts +1 -1
- package/src/bundler/signature-verifier.ts +1 -1
- package/src/calls/call-conversation-messages.ts +33 -0
- package/src/calls/call-domain.ts +114 -10
- package/src/calls/call-orchestrator.ts +268 -59
- package/src/calls/call-pointer-messages.ts +53 -0
- package/src/calls/call-recovery.ts +3 -8
- package/src/calls/call-store.ts +69 -87
- package/src/calls/elevenlabs-config.ts +3 -2
- package/src/calls/guardian-action-sweep.ts +105 -0
- package/src/calls/guardian-dispatch.ts +203 -0
- package/src/calls/guardian-question-copy.ts +133 -0
- package/src/calls/relay-server.ts +466 -8
- package/src/calls/speaker-identification.ts +1 -1
- package/src/calls/twilio-config.ts +22 -14
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +308 -7
- package/src/calls/twilio-routes.ts +65 -12
- package/src/calls/types.ts +3 -1
- package/src/channels/types.ts +25 -0
- package/src/cli/amazon.ts +815 -0
- package/src/cli/config-commands.ts +2 -2
- package/src/cli/core-commands.ts +4 -3
- package/src/cli/influencer.ts +244 -0
- package/src/cli/map.ts +89 -6
- package/src/cli.ts +1 -1
- package/src/config/agent-schema.ts +171 -0
- package/src/config/bundled-skills/amazon/SKILL.md +127 -0
- package/src/config/bundled-skills/amazon/icon.svg +13 -0
- package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
- package/src/config/bundled-skills/browser/SKILL.md +1 -0
- package/src/config/bundled-skills/browser/TOOLS.json +17 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
- package/src/config/bundled-skills/doordash/SKILL.md +51 -51
- package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
- package/src/config/bundled-skills/influencer/SKILL.md +144 -0
- package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
- package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
- package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
- package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
- package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
- package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
- package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
- package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
- package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
- package/src/config/bundled-skills/messaging/SKILL.md +33 -8
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
- package/src/config/bundled-skills/twitter/SKILL.md +19 -3
- package/src/config/bundled-skills/twitter/icon.svg +14 -0
- package/src/config/bundled-tool-registry.ts +310 -0
- package/src/config/calls-schema.ts +181 -0
- package/src/config/core-schema.ts +309 -0
- package/src/config/defaults.ts +28 -3
- package/src/config/env-registry.ts +162 -0
- package/src/config/env.ts +175 -0
- package/src/config/loader.ts +6 -6
- package/src/config/memory-schema.ts +528 -0
- package/src/config/sandbox-schema.ts +55 -0
- package/src/config/schema.ts +158 -1133
- package/src/config/skill-state.ts +1 -1
- package/src/config/skills-schema.ts +32 -0
- package/src/config/skills.ts +35 -24
- package/src/config/system-prompt.ts +131 -56
- package/src/config/templates/IDENTITY.md +2 -2
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +1 -0
- package/src/config/user-reference.ts +4 -9
- package/src/config/vellum-skills/catalog.json +6 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
- package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
- package/src/context/window-manager.ts +27 -7
- package/src/daemon/approval-generators.ts +186 -0
- package/src/daemon/approved-devices-store.ts +140 -0
- package/src/daemon/assistant-attachments.ts +1 -1
- package/src/daemon/classifier.ts +35 -32
- package/src/daemon/config-watcher.ts +1 -1
- package/src/daemon/daemon-control.ts +217 -0
- package/src/daemon/handlers/apps.ts +2 -3
- package/src/daemon/handlers/config-channels.ts +158 -0
- package/src/daemon/handlers/config-inbox.ts +540 -0
- package/src/daemon/handlers/config-ingress.ts +231 -0
- package/src/daemon/handlers/config-integrations.ts +258 -0
- package/src/daemon/handlers/config-model.ts +143 -0
- package/src/daemon/handlers/config-parental.ts +163 -0
- package/src/daemon/handlers/config-scheduling.ts +172 -0
- package/src/daemon/handlers/config-slack.ts +92 -0
- package/src/daemon/handlers/config-telegram.ts +301 -0
- package/src/daemon/handlers/config-tools.ts +177 -0
- package/src/daemon/handlers/config-trust.ts +104 -0
- package/src/daemon/handlers/config-twilio.ts +1080 -0
- package/src/daemon/handlers/config.ts +53 -1689
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +180 -0
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/identity.ts +14 -23
- package/src/daemon/handlers/index.ts +11 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +56 -5
- package/src/daemon/handlers/shared.ts +6 -1
- package/src/daemon/handlers/skills.ts +1 -1
- package/src/daemon/handlers/twitter-auth.ts +2 -0
- package/src/daemon/handlers/work-items.ts +17 -9
- package/src/daemon/handlers/workspace-files.ts +4 -3
- package/src/daemon/install-cli-launchers.ts +113 -0
- package/src/daemon/ipc-contract/apps.ts +356 -0
- package/src/daemon/ipc-contract/browser.ts +74 -0
- package/src/daemon/ipc-contract/computer-use.ts +151 -0
- package/src/daemon/ipc-contract/diagnostics.ts +56 -0
- package/src/daemon/ipc-contract/documents.ts +74 -0
- package/src/daemon/ipc-contract/inbox.ts +209 -0
- package/src/daemon/ipc-contract/integrations.ts +284 -0
- package/src/daemon/ipc-contract/memory.ts +48 -0
- package/src/daemon/ipc-contract/messages.ts +211 -0
- package/src/daemon/ipc-contract/pairing.ts +45 -0
- package/src/daemon/ipc-contract/parental-control.ts +95 -0
- package/src/daemon/ipc-contract/schedules.ts +97 -0
- package/src/daemon/ipc-contract/sessions.ts +315 -0
- package/src/daemon/ipc-contract/shared.ts +42 -0
- package/src/daemon/ipc-contract/skills.ts +120 -0
- package/src/daemon/ipc-contract/subagents.ts +58 -0
- package/src/daemon/ipc-contract/surfaces.ts +250 -0
- package/src/daemon/ipc-contract/trust.ts +60 -0
- package/src/daemon/ipc-contract/work-items.ts +225 -0
- package/src/daemon/ipc-contract/workspace.ts +113 -0
- package/src/daemon/ipc-contract-inventory.json +70 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +229 -2426
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -377
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +68 -3
- package/src/daemon/server.ts +66 -46
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +117 -275
- package/src/daemon/session-dynamic-profile.ts +1 -1
- package/src/daemon/session-history.ts +1 -1
- package/src/daemon/session-media-retry.ts +1 -1
- package/src/daemon/session-messaging.ts +37 -2
- package/src/daemon/session-notifiers.ts +5 -25
- package/src/daemon/session-process.ts +99 -59
- package/src/daemon/session-queue-manager.ts +96 -4
- package/src/daemon/session-runtime-assembly.ts +199 -10
- package/src/daemon/session-surfaces.ts +19 -4
- package/src/daemon/session-tool-setup.ts +30 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +35 -2
- package/src/daemon/shutdown-handlers.ts +122 -0
- package/src/daemon/trace-emitter.ts +1 -1
- package/src/daemon/watch-handler.ts +36 -33
- package/src/doordash/cart-queries.ts +787 -0
- package/src/doordash/client.ts +144 -127
- package/src/doordash/order-queries.ts +85 -0
- package/src/doordash/queries.ts +10 -1308
- package/src/doordash/search-queries.ts +203 -0
- package/src/doordash/session.ts +3 -2
- package/src/doordash/store-queries.ts +246 -0
- package/src/doordash/types.ts +367 -0
- package/src/email/providers/agentmail.ts +2 -1
- package/src/email/providers/index.ts +3 -2
- package/src/email/service.ts +3 -2
- package/src/errors.ts +43 -0
- package/src/home-base/prebuilt/seed.ts +1 -1
- package/src/hooks/cli.ts +6 -5
- package/src/hooks/config.ts +6 -8
- package/src/hooks/discovery.ts +6 -5
- package/src/hooks/manager.ts +4 -3
- package/src/hooks/runner.ts +2 -2
- package/src/hooks/templates.ts +5 -5
- package/src/inbound/public-ingress-urls.ts +6 -4
- package/src/index.ts +4 -2
- package/src/influencer/client.ts +1104 -0
- package/src/instrument.ts +4 -3
- package/src/logfire.ts +4 -3
- package/src/memory/admin.ts +25 -35
- package/src/memory/attachments-store.ts +4 -7
- package/src/memory/channel-delivery-store.ts +30 -1
- package/src/memory/channel-guardian-store.ts +202 -2
- package/src/memory/clarification-resolver.ts +37 -33
- package/src/memory/conflict-store.ts +67 -61
- package/src/memory/contradiction-checker.ts +141 -117
- package/src/memory/conversation-store.ts +335 -51
- package/src/memory/db-connection.ts +27 -4
- package/src/memory/db-init.ts +265 -4
- package/src/memory/db.ts +14 -1
- package/src/memory/embedding-backend.ts +27 -5
- package/src/memory/embedding-ollama.ts +2 -1
- package/src/memory/entity-extractor.ts +38 -35
- package/src/memory/guardian-action-store.ts +430 -0
- package/src/memory/inbox-escalation-projection.ts +59 -0
- package/src/memory/inbox-thread-store.ts +218 -0
- package/src/memory/ingress-invite-store.ts +338 -0
- package/src/memory/ingress-member-store.ts +350 -0
- package/src/memory/items-extractor.ts +91 -97
- package/src/memory/job-handlers/index-maintenance.ts +3 -3
- package/src/memory/job-handlers/media-processing.ts +69 -0
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +8 -10
- package/src/memory/jobs-worker.ts +55 -36
- package/src/memory/media-store.ts +759 -0
- package/src/memory/migrations/001-job-deferrals.ts +45 -0
- package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
- package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
- package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
- package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
- package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
- package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
- package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
- package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
- package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
- package/src/memory/migrations/index.ts +24 -0
- package/src/memory/migrations/registry.ts +79 -0
- package/src/memory/migrations/validate-migration-state.ts +69 -0
- package/src/memory/qdrant-manager.ts +49 -8
- package/src/memory/query-builder.ts +1 -1
- package/src/memory/raw-query.ts +119 -0
- package/src/memory/recall-cache.ts +4 -1
- package/src/memory/retriever.ts +165 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +228 -7
- package/src/memory/search/entity.ts +205 -31
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +27 -23
- package/src/memory/search/semantic.ts +157 -19
- package/src/memory/search/types.ts +24 -0
- package/src/memory/shared-app-links-store.ts +4 -5
- package/src/memory/validation.ts +19 -0
- package/src/messaging/draft-store.ts +5 -6
- package/src/messaging/provider-types.ts +2 -0
- package/src/messaging/providers/sms/adapter.ts +201 -0
- package/src/messaging/providers/sms/client.ts +93 -0
- package/src/messaging/providers/sms/types.ts +7 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
- package/src/messaging/providers/whatsapp/adapter.ts +136 -0
- package/src/messaging/providers/whatsapp/client.ts +67 -0
- package/src/messaging/style-analyzer.ts +5 -4
- package/src/messaging/thread-summarizer.ts +61 -69
- package/src/messaging/triage-engine.ts +62 -71
- package/src/migrations/config-merge.ts +53 -0
- package/src/migrations/data-layout.ts +68 -0
- package/src/migrations/data-merge.ts +33 -0
- package/src/migrations/hooks-merge.ts +90 -0
- package/src/migrations/index.ts +6 -0
- package/src/migrations/log.ts +23 -0
- package/src/migrations/skills-merge.ts +33 -0
- package/src/migrations/workspace-layout.ts +79 -0
- package/src/permissions/checker.ts +133 -11
- package/src/permissions/prompter.ts +14 -0
- package/src/permissions/shell-identity.ts +31 -1
- package/src/permissions/trust-store.ts +21 -1
- package/src/providers/anthropic/client.ts +4 -4
- package/src/providers/failover.ts +2 -2
- package/src/providers/model-intents.ts +70 -0
- package/src/providers/ollama/client.ts +2 -1
- package/src/providers/provider-send-message.ts +176 -0
- package/src/providers/registry.ts +71 -30
- package/src/providers/retry.ts +35 -1
- package/src/providers/types.ts +12 -1
- package/src/runtime/approval-conversation-turn.ts +97 -0
- package/src/runtime/approval-message-composer.ts +253 -0
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +11 -24
- package/src/runtime/channel-guardian-service.ts +88 -21
- package/src/runtime/channel-readiness-service.ts +418 -0
- package/src/runtime/channel-readiness-types.ts +35 -0
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +275 -717
- package/src/runtime/http-types.ts +59 -3
- package/src/runtime/middleware/auth.ts +116 -0
- package/src/runtime/middleware/error-handler.ts +33 -0
- package/src/runtime/middleware/twilio-validation.ts +127 -0
- package/src/runtime/routes/app-routes.ts +1 -1
- package/src/runtime/routes/call-routes.ts +51 -7
- package/src/runtime/routes/channel-delivery-routes.ts +170 -0
- package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
- package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
- package/src/runtime/routes/channel-route-shared.ts +144 -0
- package/src/runtime/routes/channel-routes.ts +32 -1588
- package/src/runtime/routes/conversation-routes.ts +50 -7
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/identity-routes.ts +126 -0
- package/src/runtime/routes/pairing-routes.ts +143 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +86 -35
- package/src/schedule/schedule-store.ts +36 -32
- package/src/schedule/scheduler.ts +3 -3
- package/src/security/encrypted-store.ts +5 -7
- package/src/security/oauth2.ts +45 -15
- package/src/security/parental-control-store.ts +183 -0
- package/src/security/secret-allowlist.ts +4 -3
- package/src/security/secret-scanner.ts +5 -5
- package/src/security/secure-keys.ts +1 -1
- package/src/security/token-manager.ts +3 -2
- package/src/services/vercel-deploy.ts +6 -2
- package/src/skills/tool-manifest.ts +3 -3
- package/src/skills/vellum-catalog-remote.ts +75 -16
- package/src/slack/slack-webhook.ts +2 -1
- package/src/swarm/orchestrator.ts +92 -1
- package/src/swarm/router-planner.ts +6 -9
- package/src/swarm/worker-prompts.ts +9 -12
- package/src/tasks/task-compiler.ts +19 -28
- package/src/tasks/task-runner.ts +1 -1
- package/src/tools/assets/materialize.ts +2 -2
- package/src/tools/assets/search.ts +15 -14
- package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
- package/src/tools/browser/auto-navigate.ts +1 -0
- package/src/tools/browser/browser-execution.ts +10 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -0
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/credentials/broker.ts +11 -2
- package/src/tools/credentials/metadata-store.ts +18 -14
- package/src/tools/credentials/post-connect-hooks.ts +61 -0
- package/src/tools/credentials/vault.ts +49 -23
- package/src/tools/execution-target.ts +11 -1
- package/src/tools/executor.ts +68 -9
- package/src/tools/host-terminal/cli-discover.ts +1 -1
- package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
- package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
- package/src/tools/network/script-proxy/server.ts +1 -1
- package/src/tools/network/script-proxy/session-manager.ts +6 -5
- package/src/tools/network/web-fetch.ts +18 -2
- package/src/tools/network/web-search.ts +8 -4
- package/src/tools/reminder/reminder-store.ts +14 -15
- package/src/tools/schedule/create.ts +1 -0
- package/src/tools/schedule/list.ts +2 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
- package/src/tools/skills/skill-script-runner.ts +24 -9
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- package/src/tools/tasks/work-item-enqueue.ts +2 -2
- package/src/tools/terminal/evaluate-typescript.ts +21 -12
- package/src/tools/terminal/parser.ts +50 -0
- package/src/tools/types.ts +2 -0
- package/src/tools/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- package/src/twitter/router.ts +1 -1
- package/src/twitter/session.ts +4 -3
- package/src/util/clipboard.ts +1 -1
- package/src/util/errors.ts +65 -8
- package/src/util/fs.ts +40 -0
- package/src/util/json.ts +10 -0
- package/src/util/log-redact.ts +189 -0
- package/src/util/logger.ts +19 -17
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +105 -363
- package/src/util/pricing.ts +1 -1
- package/src/util/promise-guard.ts +1 -1
- package/src/util/retry.ts +19 -0
- package/src/util/row-mapper.ts +79 -0
- package/src/util/silently.ts +21 -0
- package/src/watcher/engine.ts +5 -1
- package/src/watcher/provider-types.ts +20 -0
- package/src/watcher/providers/github.ts +156 -0
- package/src/watcher/providers/gmail.ts +1 -0
- package/src/watcher/providers/google-calendar.ts +1 -0
- package/src/watcher/providers/linear.ts +460 -0
- package/src/watcher/providers/slack.ts +1 -0
- package/src/work-items/work-item-runner.ts +1 -1
- package/src/workspace/git-service.ts +1 -1
- package/src/workspace/provider-commit-message-generator.ts +51 -22
- package/src/__tests__/call-bridge.test.ts +0 -517
- package/src/__tests__/session-process-bridge.test.ts +0 -244
- package/src/calls/call-bridge.ts +0 -168
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -1,1693 +1,57 @@
|
|
|
1
|
-
import * as net from 'node:net';
|
|
2
|
-
import { getConfig, loadRawConfig, saveRawConfig } from '../../config/loader.js';
|
|
3
|
-
import { initializeProviders } from '../../providers/registry.js';
|
|
4
|
-
import { addRule, removeRule, updateRule, getAllRules, acceptStarterBundle } from '../../permissions/trust-store.js';
|
|
5
|
-
import { classifyRisk, check, generateAllowlistOptions, generateScopeOptions } from '../../permissions/checker.js';
|
|
6
|
-
import { isSideEffectTool } from '../../tools/executor.js';
|
|
7
|
-
import { resolveExecutionTarget } from '../../tools/execution-target.js';
|
|
8
|
-
import { getAllTools } from '../../tools/registry.js';
|
|
9
|
-
import { listSchedules, updateSchedule, deleteSchedule, describeCronExpression } from '../../schedule/schedule-store.js';
|
|
10
|
-
import { listReminders, cancelReminder } from '../../tools/reminder/reminder-store.js';
|
|
11
|
-
import { getSecureKey, setSecureKey, deleteSecureKey } from '../../security/secure-keys.js';
|
|
12
|
-
import { upsertCredentialMetadata, deleteCredentialMetadata, getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
|
|
13
|
-
import { postToSlackWebhook } from '../../slack/slack-webhook.js';
|
|
14
|
-
import { getApp } from '../../memory/app-store.js';
|
|
15
|
-
import { readHttpToken } from '../../util/platform.js';
|
|
16
|
-
import type {
|
|
17
|
-
ModelSetRequest,
|
|
18
|
-
ImageGenModelSetRequest,
|
|
19
|
-
AddTrustRule,
|
|
20
|
-
RemoveTrustRule,
|
|
21
|
-
UpdateTrustRule,
|
|
22
|
-
ScheduleToggle,
|
|
23
|
-
ScheduleRemove,
|
|
24
|
-
ReminderCancel,
|
|
25
|
-
ShareToSlackRequest,
|
|
26
|
-
SlackWebhookConfigRequest,
|
|
27
|
-
IngressConfigRequest,
|
|
28
|
-
VercelApiConfigRequest,
|
|
29
|
-
TwitterIntegrationConfigRequest,
|
|
30
|
-
TelegramConfigRequest,
|
|
31
|
-
TwilioConfigRequest,
|
|
32
|
-
GuardianVerificationRequest,
|
|
33
|
-
ToolPermissionSimulateRequest,
|
|
34
|
-
} from '../ipc-protocol.js';
|
|
35
|
-
import {
|
|
36
|
-
hasTwilioCredentials,
|
|
37
|
-
listIncomingPhoneNumbers,
|
|
38
|
-
searchAvailableNumbers,
|
|
39
|
-
provisionPhoneNumber,
|
|
40
|
-
updatePhoneNumberWebhooks,
|
|
41
|
-
} from '../../calls/twilio-rest.js';
|
|
42
|
-
import {
|
|
43
|
-
getTwilioVoiceWebhookUrl,
|
|
44
|
-
getTwilioStatusCallbackUrl,
|
|
45
|
-
getTwilioSmsWebhookUrl,
|
|
46
|
-
type IngressConfig,
|
|
47
|
-
} from '../../inbound/public-ingress-urls.js';
|
|
48
|
-
import { createVerificationChallenge, getGuardianBinding, revokeBinding as revokeGuardianBinding } from '../../runtime/channel-guardian-service.js';
|
|
49
|
-
import { log, CONFIG_RELOAD_DEBOUNCE_MS, defineHandlers, type HandlerContext } from './shared.js';
|
|
50
|
-
import { MODEL_TO_PROVIDER } from '../session-slash.js';
|
|
51
|
-
|
|
52
|
-
// Lazily capture the env-provided INGRESS_PUBLIC_BASE_URL on first access
|
|
53
|
-
// rather than at module load time. The daemon loads ~/.vellum/.env inside
|
|
54
|
-
// runDaemon() (see lifecycle.ts), which runs AFTER static ES module imports
|
|
55
|
-
// resolve. A module-level snapshot would miss dotenv-provided values.
|
|
56
|
-
let _originalIngressEnvCaptured = false;
|
|
57
|
-
let _originalIngressEnv: string | undefined;
|
|
58
|
-
function getOriginalIngressEnv(): string | undefined {
|
|
59
|
-
if (!_originalIngressEnvCaptured) {
|
|
60
|
-
_originalIngressEnv = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
61
|
-
_originalIngressEnvCaptured = true;
|
|
62
|
-
}
|
|
63
|
-
return _originalIngressEnv;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const TELEGRAM_BOT_TOKEN_IN_URL_PATTERN = /\/bot\d{8,10}:[A-Za-z0-9_-]{30,120}\//g;
|
|
67
|
-
const TELEGRAM_BOT_TOKEN_PATTERN = /(?<![A-Za-z0-9_])\d{8,10}:[A-Za-z0-9_-]{30,120}(?![A-Za-z0-9_])/g;
|
|
68
|
-
|
|
69
|
-
function redactTelegramBotTokens(value: string): string {
|
|
70
|
-
return value
|
|
71
|
-
.replace(TELEGRAM_BOT_TOKEN_IN_URL_PATTERN, '/bot[REDACTED]/')
|
|
72
|
-
.replace(TELEGRAM_BOT_TOKEN_PATTERN, '[REDACTED]');
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function summarizeTelegramError(err: unknown): string {
|
|
76
|
-
const parts: string[] = [];
|
|
77
|
-
if (err instanceof Error) {
|
|
78
|
-
parts.push(err.message);
|
|
79
|
-
} else {
|
|
80
|
-
parts.push(String(err));
|
|
81
|
-
}
|
|
82
|
-
const path = (err as { path?: unknown })?.path;
|
|
83
|
-
if (typeof path === 'string' && path.length > 0) {
|
|
84
|
-
parts.push(`path=${path}`);
|
|
85
|
-
}
|
|
86
|
-
const code = (err as { code?: unknown })?.code;
|
|
87
|
-
if (typeof code === 'string' && code.length > 0) {
|
|
88
|
-
parts.push(`code=${code}`);
|
|
89
|
-
}
|
|
90
|
-
return redactTelegramBotTokens(parts.join(' '));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function handleModelGet(socket: net.Socket, ctx: HandlerContext): void {
|
|
94
|
-
const config = getConfig();
|
|
95
|
-
const configured = Object.keys(config.apiKeys).filter((k) => !!config.apiKeys[k]);
|
|
96
|
-
if (!configured.includes('ollama')) configured.push('ollama');
|
|
97
|
-
ctx.send(socket, {
|
|
98
|
-
type: 'model_info',
|
|
99
|
-
model: config.model,
|
|
100
|
-
provider: config.provider,
|
|
101
|
-
configuredProviders: configured,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function handleModelSet(
|
|
106
|
-
msg: ModelSetRequest,
|
|
107
|
-
socket: net.Socket,
|
|
108
|
-
ctx: HandlerContext,
|
|
109
|
-
): void {
|
|
110
|
-
try {
|
|
111
|
-
// If the requested model is already the current model AND the provider
|
|
112
|
-
// is already aligned with what MODEL_TO_PROVIDER expects, skip expensive
|
|
113
|
-
// reinitialization but still send model_info so the client confirms.
|
|
114
|
-
// If the provider has drifted (e.g. manual config edit), fall through
|
|
115
|
-
// so the full reinit path can repair it.
|
|
116
|
-
{
|
|
117
|
-
const current = getConfig();
|
|
118
|
-
const expectedProvider = MODEL_TO_PROVIDER[msg.model];
|
|
119
|
-
const providerAligned = !expectedProvider || current.provider === expectedProvider;
|
|
120
|
-
if (msg.model === current.model && providerAligned) {
|
|
121
|
-
const configured = Object.keys(current.apiKeys).filter((k) => !!current.apiKeys[k]);
|
|
122
|
-
if (!configured.includes('ollama')) configured.push('ollama');
|
|
123
|
-
ctx.send(socket, {
|
|
124
|
-
type: 'model_info',
|
|
125
|
-
model: current.model,
|
|
126
|
-
provider: current.provider,
|
|
127
|
-
configuredProviders: configured,
|
|
128
|
-
});
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Validate API key before switching
|
|
134
|
-
const provider = MODEL_TO_PROVIDER[msg.model];
|
|
135
|
-
if (provider && provider !== 'ollama') {
|
|
136
|
-
const currentConfig = getConfig();
|
|
137
|
-
if (!currentConfig.apiKeys[provider]) {
|
|
138
|
-
// Send current model_info so the client resyncs its optimistic state
|
|
139
|
-
// (don't use generic 'error' type — it would interrupt in-flight chat)
|
|
140
|
-
const configured = Object.keys(currentConfig.apiKeys).filter((k) => !!currentConfig.apiKeys[k]);
|
|
141
|
-
if (!configured.includes('ollama')) configured.push('ollama');
|
|
142
|
-
ctx.send(socket, { type: 'model_info', model: currentConfig.model, provider: currentConfig.provider, configuredProviders: configured });
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Use raw config to avoid persisting env-var API keys to disk
|
|
148
|
-
const raw = loadRawConfig();
|
|
149
|
-
raw.model = msg.model;
|
|
150
|
-
// Infer provider from model ID to keep provider and model in sync
|
|
151
|
-
raw.provider = provider ?? raw.provider;
|
|
152
|
-
|
|
153
|
-
// Suppress the file watcher callback — handleModelSet already does
|
|
154
|
-
// the full reload sequence; a redundant watcher-triggered reload
|
|
155
|
-
// would incorrectly evict sessions created after this method returns.
|
|
156
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
157
|
-
ctx.setSuppressConfigReload(true);
|
|
158
|
-
try {
|
|
159
|
-
saveRawConfig(raw);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
162
|
-
throw err;
|
|
163
|
-
}
|
|
164
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
165
|
-
|
|
166
|
-
// Re-initialize provider with the new model so LLM calls use it
|
|
167
|
-
const config = getConfig();
|
|
168
|
-
initializeProviders(config);
|
|
169
|
-
|
|
170
|
-
// Evict idle sessions immediately; mark busy ones as stale so they
|
|
171
|
-
// get recreated with the new provider once they finish processing.
|
|
172
|
-
for (const [id, session] of ctx.sessions) {
|
|
173
|
-
if (!session.isProcessing()) {
|
|
174
|
-
session.dispose();
|
|
175
|
-
ctx.sessions.delete(id);
|
|
176
|
-
} else {
|
|
177
|
-
session.markStale();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
ctx.updateConfigFingerprint();
|
|
182
|
-
|
|
183
|
-
ctx.send(socket, {
|
|
184
|
-
type: 'model_info',
|
|
185
|
-
model: config.model,
|
|
186
|
-
provider: config.provider,
|
|
187
|
-
});
|
|
188
|
-
} catch (err) {
|
|
189
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
190
|
-
ctx.send(socket, { type: 'error', message: `Failed to set model: ${message}` });
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function handleImageGenModelSet(
|
|
195
|
-
msg: ImageGenModelSetRequest,
|
|
196
|
-
_socket: net.Socket,
|
|
197
|
-
ctx: HandlerContext,
|
|
198
|
-
): void {
|
|
199
|
-
try {
|
|
200
|
-
const raw = loadRawConfig();
|
|
201
|
-
raw.imageGenModel = msg.model;
|
|
202
|
-
|
|
203
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
204
|
-
ctx.setSuppressConfigReload(true);
|
|
205
|
-
try {
|
|
206
|
-
saveRawConfig(raw);
|
|
207
|
-
} catch (err) {
|
|
208
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
212
|
-
|
|
213
|
-
ctx.updateConfigFingerprint();
|
|
214
|
-
log.info({ model: msg.model }, 'Image generation model updated');
|
|
215
|
-
} catch (err) {
|
|
216
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
217
|
-
log.error({ err }, `Failed to set image gen model: ${message}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function handleAddTrustRule(
|
|
222
|
-
msg: AddTrustRule,
|
|
223
|
-
_socket: net.Socket,
|
|
224
|
-
_ctx: HandlerContext,
|
|
225
|
-
): void {
|
|
226
|
-
try {
|
|
227
|
-
const hasMetadata = msg.allowHighRisk != null
|
|
228
|
-
|| msg.executionTarget != null;
|
|
229
|
-
|
|
230
|
-
addRule(
|
|
231
|
-
msg.toolName,
|
|
232
|
-
msg.pattern,
|
|
233
|
-
msg.scope,
|
|
234
|
-
msg.decision,
|
|
235
|
-
undefined, // priority — use default
|
|
236
|
-
hasMetadata
|
|
237
|
-
? {
|
|
238
|
-
allowHighRisk: msg.allowHighRisk,
|
|
239
|
-
executionTarget: msg.executionTarget,
|
|
240
|
-
}
|
|
241
|
-
: undefined,
|
|
242
|
-
);
|
|
243
|
-
log.info({ toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope, decision: msg.decision }, 'Trust rule added via client');
|
|
244
|
-
} catch (err) {
|
|
245
|
-
log.error({ err, toolName: msg.toolName, pattern: msg.pattern, scope: msg.scope }, 'Failed to add trust rule via client');
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function handleTrustRulesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
250
|
-
const rules = getAllRules();
|
|
251
|
-
ctx.send(socket, { type: 'trust_rules_list_response', rules });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function handleRemoveTrustRule(
|
|
255
|
-
msg: RemoveTrustRule,
|
|
256
|
-
_socket: net.Socket,
|
|
257
|
-
_ctx: HandlerContext,
|
|
258
|
-
): void {
|
|
259
|
-
try {
|
|
260
|
-
const removed = removeRule(msg.id);
|
|
261
|
-
if (!removed) {
|
|
262
|
-
log.warn({ id: msg.id }, 'Trust rule not found for removal');
|
|
263
|
-
} else {
|
|
264
|
-
log.info({ id: msg.id }, 'Trust rule removed via client');
|
|
265
|
-
}
|
|
266
|
-
} catch (err) {
|
|
267
|
-
log.error({ err }, 'Failed to remove trust rule');
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function handleUpdateTrustRule(
|
|
272
|
-
msg: UpdateTrustRule,
|
|
273
|
-
_socket: net.Socket,
|
|
274
|
-
_ctx: HandlerContext,
|
|
275
|
-
): void {
|
|
276
|
-
try {
|
|
277
|
-
updateRule(msg.id, {
|
|
278
|
-
tool: msg.tool,
|
|
279
|
-
pattern: msg.pattern,
|
|
280
|
-
scope: msg.scope,
|
|
281
|
-
decision: msg.decision,
|
|
282
|
-
priority: msg.priority,
|
|
283
|
-
});
|
|
284
|
-
log.info({ id: msg.id }, 'Trust rule updated via client');
|
|
285
|
-
} catch (err) {
|
|
286
|
-
log.error({ err }, 'Failed to update trust rule');
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function handleAcceptStarterBundle(
|
|
291
|
-
socket: net.Socket,
|
|
292
|
-
ctx: HandlerContext,
|
|
293
|
-
): void {
|
|
294
|
-
try {
|
|
295
|
-
const result = acceptStarterBundle();
|
|
296
|
-
ctx.send(socket, {
|
|
297
|
-
type: 'accept_starter_bundle_response',
|
|
298
|
-
accepted: result.accepted,
|
|
299
|
-
rulesAdded: result.rulesAdded,
|
|
300
|
-
alreadyAccepted: result.alreadyAccepted,
|
|
301
|
-
});
|
|
302
|
-
log.info({ rulesAdded: result.rulesAdded, alreadyAccepted: result.alreadyAccepted }, 'Starter bundle accepted via client');
|
|
303
|
-
} catch (err) {
|
|
304
|
-
log.error({ err }, 'Failed to accept starter bundle');
|
|
305
|
-
ctx.send(socket, { type: 'error', message: 'Failed to accept starter bundle' });
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function handleSchedulesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
310
|
-
const jobs = listSchedules();
|
|
311
|
-
ctx.send(socket, {
|
|
312
|
-
type: 'schedules_list_response',
|
|
313
|
-
schedules: jobs.map((j) => ({
|
|
314
|
-
id: j.id,
|
|
315
|
-
name: j.name,
|
|
316
|
-
enabled: j.enabled,
|
|
317
|
-
syntax: j.syntax,
|
|
318
|
-
expression: j.expression,
|
|
319
|
-
cronExpression: j.cronExpression,
|
|
320
|
-
timezone: j.timezone,
|
|
321
|
-
message: j.message,
|
|
322
|
-
nextRunAt: j.nextRunAt,
|
|
323
|
-
lastRunAt: j.lastRunAt,
|
|
324
|
-
lastStatus: j.lastStatus,
|
|
325
|
-
description: j.syntax === 'cron' ? describeCronExpression(j.cronExpression) : j.expression,
|
|
326
|
-
})),
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
export function handleScheduleToggle(
|
|
331
|
-
msg: ScheduleToggle,
|
|
332
|
-
socket: net.Socket,
|
|
333
|
-
ctx: HandlerContext,
|
|
334
|
-
): void {
|
|
335
|
-
try {
|
|
336
|
-
updateSchedule(msg.id, { enabled: msg.enabled });
|
|
337
|
-
log.info({ id: msg.id, enabled: msg.enabled }, 'Schedule toggled via client');
|
|
338
|
-
} catch (err) {
|
|
339
|
-
log.error({ err }, 'Failed to toggle schedule');
|
|
340
|
-
}
|
|
341
|
-
handleSchedulesList(socket, ctx);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
export function handleScheduleRemove(
|
|
345
|
-
msg: ScheduleRemove,
|
|
346
|
-
socket: net.Socket,
|
|
347
|
-
ctx: HandlerContext,
|
|
348
|
-
): void {
|
|
349
|
-
try {
|
|
350
|
-
const removed = deleteSchedule(msg.id);
|
|
351
|
-
if (!removed) {
|
|
352
|
-
log.warn({ id: msg.id }, 'Schedule not found for removal');
|
|
353
|
-
} else {
|
|
354
|
-
log.info({ id: msg.id }, 'Schedule removed via client');
|
|
355
|
-
}
|
|
356
|
-
} catch (err) {
|
|
357
|
-
log.error({ err }, 'Failed to remove schedule');
|
|
358
|
-
}
|
|
359
|
-
handleSchedulesList(socket, ctx);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
export function handleRemindersList(socket: net.Socket, ctx: HandlerContext): void {
|
|
363
|
-
const items = listReminders();
|
|
364
|
-
ctx.send(socket, {
|
|
365
|
-
type: 'reminders_list_response',
|
|
366
|
-
reminders: items.map((r) => ({
|
|
367
|
-
id: r.id,
|
|
368
|
-
label: r.label,
|
|
369
|
-
message: r.message,
|
|
370
|
-
fireAt: r.fireAt,
|
|
371
|
-
mode: r.mode,
|
|
372
|
-
status: r.status,
|
|
373
|
-
firedAt: r.firedAt,
|
|
374
|
-
createdAt: r.createdAt,
|
|
375
|
-
})),
|
|
376
|
-
});
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function handleReminderCancel(
|
|
380
|
-
msg: ReminderCancel,
|
|
381
|
-
socket: net.Socket,
|
|
382
|
-
ctx: HandlerContext,
|
|
383
|
-
): void {
|
|
384
|
-
try {
|
|
385
|
-
const cancelled = cancelReminder(msg.id);
|
|
386
|
-
if (!cancelled) {
|
|
387
|
-
log.warn({ id: msg.id }, 'Reminder not found or already fired/cancelled');
|
|
388
|
-
} else {
|
|
389
|
-
log.info({ id: msg.id }, 'Reminder cancelled via client');
|
|
390
|
-
}
|
|
391
|
-
} catch (err) {
|
|
392
|
-
log.error({ err }, 'Failed to cancel reminder');
|
|
393
|
-
}
|
|
394
|
-
handleRemindersList(socket, ctx);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export async function handleShareToSlack(
|
|
398
|
-
msg: ShareToSlackRequest,
|
|
399
|
-
socket: net.Socket,
|
|
400
|
-
ctx: HandlerContext,
|
|
401
|
-
): Promise<void> {
|
|
402
|
-
try {
|
|
403
|
-
const config = loadRawConfig();
|
|
404
|
-
const webhookUrl = config.slackWebhookUrl as string | undefined;
|
|
405
|
-
if (!webhookUrl) {
|
|
406
|
-
ctx.send(socket, {
|
|
407
|
-
type: 'share_to_slack_response',
|
|
408
|
-
success: false,
|
|
409
|
-
error: 'No Slack webhook URL configured. Set one in Settings.',
|
|
410
|
-
});
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const app = getApp(msg.appId);
|
|
415
|
-
if (!app) {
|
|
416
|
-
ctx.send(socket, {
|
|
417
|
-
type: 'share_to_slack_response',
|
|
418
|
-
success: false,
|
|
419
|
-
error: `App not found: ${msg.appId}`,
|
|
420
|
-
});
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
await postToSlackWebhook(
|
|
425
|
-
webhookUrl,
|
|
426
|
-
app.name,
|
|
427
|
-
app.description ?? '',
|
|
428
|
-
'\u{1F4F1}',
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
ctx.send(socket, { type: 'share_to_slack_response', success: true });
|
|
432
|
-
} catch (err) {
|
|
433
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
434
|
-
log.error({ err, appId: msg.appId }, 'Failed to share app to Slack');
|
|
435
|
-
ctx.send(socket, {
|
|
436
|
-
type: 'share_to_slack_response',
|
|
437
|
-
success: false,
|
|
438
|
-
error: message,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export function handleSlackWebhookConfig(
|
|
444
|
-
msg: SlackWebhookConfigRequest,
|
|
445
|
-
socket: net.Socket,
|
|
446
|
-
ctx: HandlerContext,
|
|
447
|
-
): void {
|
|
448
|
-
try {
|
|
449
|
-
const config = loadRawConfig();
|
|
450
|
-
if (msg.action === 'get') {
|
|
451
|
-
ctx.send(socket, {
|
|
452
|
-
type: 'slack_webhook_config_response',
|
|
453
|
-
webhookUrl: (config.slackWebhookUrl as string) ?? undefined,
|
|
454
|
-
success: true,
|
|
455
|
-
});
|
|
456
|
-
} else {
|
|
457
|
-
config.slackWebhookUrl = msg.webhookUrl ?? '';
|
|
458
|
-
saveRawConfig(config);
|
|
459
|
-
ctx.send(socket, {
|
|
460
|
-
type: 'slack_webhook_config_response',
|
|
461
|
-
success: true,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
} catch (err) {
|
|
465
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
466
|
-
log.error({ err }, 'Failed to handle Slack webhook config');
|
|
467
|
-
ctx.send(socket, {
|
|
468
|
-
type: 'slack_webhook_config_response',
|
|
469
|
-
success: false,
|
|
470
|
-
error: message,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function computeGatewayTarget(): string {
|
|
476
|
-
if (process.env.GATEWAY_INTERNAL_BASE_URL) {
|
|
477
|
-
return process.env.GATEWAY_INTERNAL_BASE_URL.replace(/\/+$/, '');
|
|
478
|
-
}
|
|
479
|
-
const portRaw = process.env.GATEWAY_PORT || '7830';
|
|
480
|
-
const port = Number(portRaw) || 7830;
|
|
481
|
-
return `http://127.0.0.1:${port}`;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
1
|
/**
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* URL changes, without requiring a gateway restart.
|
|
488
|
-
*/
|
|
489
|
-
function triggerGatewayReconcile(ingressPublicBaseUrl: string | undefined): void {
|
|
490
|
-
const gatewayBase = computeGatewayTarget();
|
|
491
|
-
const token = readHttpToken();
|
|
492
|
-
if (!token) {
|
|
493
|
-
log.debug('Skipping gateway reconcile trigger: no HTTP bearer token available');
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const url = `${gatewayBase}/internal/telegram/reconcile`;
|
|
498
|
-
const body = JSON.stringify({ ingressPublicBaseUrl: ingressPublicBaseUrl ?? '' });
|
|
499
|
-
|
|
500
|
-
fetch(url, {
|
|
501
|
-
method: 'POST',
|
|
502
|
-
headers: {
|
|
503
|
-
'Content-Type': 'application/json',
|
|
504
|
-
'Authorization': `Bearer ${token}`,
|
|
505
|
-
},
|
|
506
|
-
body,
|
|
507
|
-
signal: AbortSignal.timeout(5_000),
|
|
508
|
-
}).then((res) => {
|
|
509
|
-
if (res.ok) {
|
|
510
|
-
log.info('Gateway Telegram webhook reconcile triggered successfully');
|
|
511
|
-
} else {
|
|
512
|
-
log.warn({ status: res.status }, 'Gateway Telegram webhook reconcile returned non-OK status');
|
|
513
|
-
}
|
|
514
|
-
}).catch((err) => {
|
|
515
|
-
log.debug({ err }, 'Gateway Telegram webhook reconcile failed (gateway may not be running)');
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Best-effort Twilio webhook sync helper.
|
|
521
|
-
*
|
|
522
|
-
* Computes the voice, status-callback, and SMS webhook URLs from the current
|
|
523
|
-
* ingress config and pushes them to the Twilio IncomingPhoneNumber API.
|
|
2
|
+
* Config handler barrel — re-exports all config domain handlers and assembles
|
|
3
|
+
* the combined `configHandlers` dispatch map.
|
|
524
4
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
5
|
+
* Individual handlers live in domain-specific files:
|
|
6
|
+
* config-model.ts — Model selection (LLM + image gen)
|
|
7
|
+
* config-trust.ts — Trust rules (permissions allowlist)
|
|
8
|
+
* config-scheduling.ts — Schedules & reminders
|
|
9
|
+
* config-slack.ts — Slack webhook sharing
|
|
10
|
+
* config-ingress.ts — Public ingress URL & gateway reconciliation
|
|
11
|
+
* config-integrations.ts — Vercel API & Twitter integration
|
|
12
|
+
* config-telegram.ts — Telegram bot configuration
|
|
13
|
+
* config-twilio.ts — Twilio SMS/voice configuration
|
|
14
|
+
* config-channels.ts — Channel guardian & readiness
|
|
15
|
+
* config-tools.ts — Env vars, tool permission simulation, tool names
|
|
16
|
+
* config-parental.ts — Parental control PIN + content/tool restrictions
|
|
529
17
|
*/
|
|
530
|
-
async function syncTwilioWebhooks(
|
|
531
|
-
phoneNumber: string,
|
|
532
|
-
accountSid: string,
|
|
533
|
-
authToken: string,
|
|
534
|
-
ingressConfig: IngressConfig,
|
|
535
|
-
): Promise<{ success: boolean; warning?: string }> {
|
|
536
|
-
try {
|
|
537
|
-
const voiceUrl = getTwilioVoiceWebhookUrl(ingressConfig);
|
|
538
|
-
const statusCallbackUrl = getTwilioStatusCallbackUrl(ingressConfig);
|
|
539
|
-
const smsUrl = getTwilioSmsWebhookUrl(ingressConfig);
|
|
540
|
-
await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
|
|
541
|
-
voiceUrl,
|
|
542
|
-
statusCallbackUrl,
|
|
543
|
-
smsUrl,
|
|
544
|
-
});
|
|
545
|
-
log.info({ phoneNumber }, 'Twilio webhooks configured successfully');
|
|
546
|
-
return { success: true };
|
|
547
|
-
} catch (err) {
|
|
548
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
549
|
-
log.warn({ err, phoneNumber }, `Webhook configuration skipped: ${message}`);
|
|
550
|
-
return { success: false, warning: `Webhook configuration skipped: ${message}` };
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
export async function handleIngressConfig(
|
|
555
|
-
msg: IngressConfigRequest,
|
|
556
|
-
socket: net.Socket,
|
|
557
|
-
ctx: HandlerContext,
|
|
558
|
-
): Promise<void> {
|
|
559
|
-
const localGatewayTarget = computeGatewayTarget();
|
|
560
|
-
try {
|
|
561
|
-
if (msg.action === 'get') {
|
|
562
|
-
const raw = loadRawConfig();
|
|
563
|
-
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
564
|
-
const publicBaseUrl = (ingress.publicBaseUrl as string) ?? '';
|
|
565
|
-
// Backward compatibility: if `enabled` was never explicitly set,
|
|
566
|
-
// infer from whether a publicBaseUrl is configured so existing users
|
|
567
|
-
// who predate the toggle aren't silently disabled.
|
|
568
|
-
const enabled = (ingress.enabled as boolean | undefined) ?? (publicBaseUrl ? true : false);
|
|
569
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled, publicBaseUrl, localGatewayTarget, success: true });
|
|
570
|
-
} else if (msg.action === 'set') {
|
|
571
|
-
const value = (msg.publicBaseUrl ?? '').trim().replace(/\/+$/, '');
|
|
572
|
-
// Ensure we capture the original env value before any mutation below
|
|
573
|
-
getOriginalIngressEnv();
|
|
574
|
-
const raw = loadRawConfig();
|
|
575
|
-
|
|
576
|
-
// Update ingress.publicBaseUrl — this is the single source of truth for
|
|
577
|
-
// the canonical public ingress URL. The gateway receives this value via
|
|
578
|
-
// the INGRESS_PUBLIC_BASE_URL env var at spawn time (see hatch.ts).
|
|
579
|
-
// The gateway also validates Twilio signatures against forwarded public
|
|
580
|
-
// URL headers, so local tunnel updates generally apply without restarts.
|
|
581
|
-
const ingress = (raw?.ingress ?? {}) as Record<string, unknown>;
|
|
582
|
-
ingress.publicBaseUrl = value || undefined;
|
|
583
|
-
if (msg.enabled !== undefined) {
|
|
584
|
-
ingress.enabled = msg.enabled;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
588
|
-
ctx.setSuppressConfigReload(true);
|
|
589
|
-
try {
|
|
590
|
-
saveRawConfig({ ...raw, ingress });
|
|
591
|
-
} catch (err) {
|
|
592
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
593
|
-
throw err;
|
|
594
|
-
}
|
|
595
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
596
|
-
|
|
597
|
-
// Propagate to the gateway's process environment so it picks up the
|
|
598
|
-
// new URL when it is restarted. For the local-deployment path the
|
|
599
|
-
// gateway runs as a child process that inherited the assistant's env,
|
|
600
|
-
// so updating process.env here ensures the value is visible when the
|
|
601
|
-
// gateway is restarted (e.g. by the self-upgrade skill or a manual
|
|
602
|
-
// `pkill -f gateway`).
|
|
603
|
-
// Only export the URL when ingress is enabled; clearing it when
|
|
604
|
-
// disabled ensures the gateway stops accepting inbound webhooks.
|
|
605
|
-
const isEnabled = (ingress.enabled as boolean | undefined) ?? (value ? true : false);
|
|
606
|
-
if (value && isEnabled) {
|
|
607
|
-
process.env.INGRESS_PUBLIC_BASE_URL = value;
|
|
608
|
-
} else if (isEnabled && getOriginalIngressEnv() !== undefined) {
|
|
609
|
-
// Ingress is enabled but the user cleared the URL — fall back to the
|
|
610
|
-
// env var that was present when the process started.
|
|
611
|
-
process.env.INGRESS_PUBLIC_BASE_URL = getOriginalIngressEnv()!;
|
|
612
|
-
} else {
|
|
613
|
-
// Ingress is disabled or no URL is configured and no startup env var
|
|
614
|
-
// exists — remove the env var so the gateway stops accepting webhooks.
|
|
615
|
-
delete process.env.INGRESS_PUBLIC_BASE_URL;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled: isEnabled, publicBaseUrl: value, localGatewayTarget, success: true });
|
|
619
|
-
|
|
620
|
-
// Trigger immediate Telegram webhook reconcile on the gateway so
|
|
621
|
-
// that changing the ingress URL takes effect without a restart.
|
|
622
|
-
// Called unconditionally so the gateway clears its in-memory URL
|
|
623
|
-
// when ingress is disabled, preventing stale re-registration on
|
|
624
|
-
// credential rotation.
|
|
625
|
-
// Use the effective URL from process.env (which accounts for the
|
|
626
|
-
// fallback branch above) rather than the raw `value` from the UI.
|
|
627
|
-
const effectiveUrl = isEnabled ? process.env.INGRESS_PUBLIC_BASE_URL : undefined;
|
|
628
|
-
triggerGatewayReconcile(effectiveUrl);
|
|
629
|
-
|
|
630
|
-
// Best-effort Twilio webhook reconciliation: when ingress is being
|
|
631
|
-
// enabled/updated and Twilio numbers are assigned with valid credentials,
|
|
632
|
-
// push the new webhook URLs to Twilio so calls and SMS route correctly.
|
|
633
|
-
if (isEnabled && hasTwilioCredentials()) {
|
|
634
|
-
const currentConfig = loadRawConfig();
|
|
635
|
-
const smsConfig = (currentConfig?.sms ?? {}) as Record<string, unknown>;
|
|
636
|
-
const assignedNumbers = new Set<string>();
|
|
637
|
-
const legacyNumber = (smsConfig.phoneNumber as string) ?? '';
|
|
638
|
-
if (legacyNumber) assignedNumbers.add(legacyNumber);
|
|
639
|
-
|
|
640
|
-
const assistantPhoneNumbers = smsConfig.assistantPhoneNumbers;
|
|
641
|
-
if (assistantPhoneNumbers && typeof assistantPhoneNumbers === 'object' && !Array.isArray(assistantPhoneNumbers)) {
|
|
642
|
-
for (const number of Object.values(assistantPhoneNumbers as Record<string, unknown>)) {
|
|
643
|
-
if (typeof number === 'string' && number) {
|
|
644
|
-
assignedNumbers.add(number);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (assignedNumbers.size > 0) {
|
|
650
|
-
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
651
|
-
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
652
|
-
// Fire-and-forget: webhook sync failure must not block the ingress save.
|
|
653
|
-
// Reconcile every assigned number so assistant-scoped mappings do not
|
|
654
|
-
// retain stale Twilio webhook URLs after ingress URL changes.
|
|
655
|
-
for (const assignedNumber of assignedNumbers) {
|
|
656
|
-
syncTwilioWebhooks(assignedNumber, acctSid, acctToken, currentConfig as IngressConfig)
|
|
657
|
-
.catch(() => {
|
|
658
|
-
// Already logged inside syncTwilioWebhooks
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
} else {
|
|
664
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
|
|
665
|
-
}
|
|
666
|
-
} catch (err) {
|
|
667
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
668
|
-
ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: message });
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
export function handleVercelApiConfig(
|
|
673
|
-
msg: VercelApiConfigRequest,
|
|
674
|
-
socket: net.Socket,
|
|
675
|
-
ctx: HandlerContext,
|
|
676
|
-
): void {
|
|
677
|
-
try {
|
|
678
|
-
if (msg.action === 'get') {
|
|
679
|
-
const existing = getSecureKey('credential:vercel:api_token');
|
|
680
|
-
ctx.send(socket, {
|
|
681
|
-
type: 'vercel_api_config_response',
|
|
682
|
-
hasToken: !!existing,
|
|
683
|
-
success: true,
|
|
684
|
-
});
|
|
685
|
-
} else if (msg.action === 'set') {
|
|
686
|
-
if (!msg.apiToken) {
|
|
687
|
-
ctx.send(socket, {
|
|
688
|
-
type: 'vercel_api_config_response',
|
|
689
|
-
hasToken: false,
|
|
690
|
-
success: false,
|
|
691
|
-
error: 'apiToken is required for set action',
|
|
692
|
-
});
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
const stored = setSecureKey('credential:vercel:api_token', msg.apiToken);
|
|
696
|
-
if (!stored) {
|
|
697
|
-
ctx.send(socket, {
|
|
698
|
-
type: 'vercel_api_config_response',
|
|
699
|
-
hasToken: false,
|
|
700
|
-
success: false,
|
|
701
|
-
error: 'Failed to store API token in secure storage',
|
|
702
|
-
});
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
upsertCredentialMetadata('vercel', 'api_token', {
|
|
706
|
-
allowedTools: ['publish_page', 'unpublish_page'],
|
|
707
|
-
});
|
|
708
|
-
ctx.send(socket, {
|
|
709
|
-
type: 'vercel_api_config_response',
|
|
710
|
-
hasToken: true,
|
|
711
|
-
success: true,
|
|
712
|
-
});
|
|
713
|
-
} else {
|
|
714
|
-
deleteSecureKey('credential:vercel:api_token');
|
|
715
|
-
deleteCredentialMetadata('vercel', 'api_token');
|
|
716
|
-
ctx.send(socket, {
|
|
717
|
-
type: 'vercel_api_config_response',
|
|
718
|
-
hasToken: false,
|
|
719
|
-
success: true,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
} catch (err) {
|
|
723
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
724
|
-
log.error({ err }, 'Failed to handle Vercel API config');
|
|
725
|
-
ctx.send(socket, {
|
|
726
|
-
type: 'vercel_api_config_response',
|
|
727
|
-
hasToken: false,
|
|
728
|
-
success: false,
|
|
729
|
-
error: message,
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
export function handleTwitterIntegrationConfig(
|
|
735
|
-
msg: TwitterIntegrationConfigRequest,
|
|
736
|
-
socket: net.Socket,
|
|
737
|
-
ctx: HandlerContext,
|
|
738
|
-
): void {
|
|
739
|
-
try {
|
|
740
|
-
if (msg.action === 'get') {
|
|
741
|
-
const raw = loadRawConfig();
|
|
742
|
-
const mode = (raw.twitterIntegrationMode as 'local_byo' | 'managed' | undefined) ?? 'local_byo';
|
|
743
|
-
const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
|
|
744
|
-
const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
|
|
745
|
-
const localClientConfigured = !!getSecureKey('credential:integration:twitter:oauth_client_id');
|
|
746
|
-
const connected = !!getSecureKey('credential:integration:twitter:access_token');
|
|
747
|
-
const meta = getCredentialMetadata('integration:twitter', 'access_token');
|
|
748
|
-
ctx.send(socket, {
|
|
749
|
-
type: 'twitter_integration_config_response',
|
|
750
|
-
success: true,
|
|
751
|
-
mode,
|
|
752
|
-
managedAvailable: false,
|
|
753
|
-
localClientConfigured,
|
|
754
|
-
connected,
|
|
755
|
-
accountInfo: meta?.accountInfo ?? undefined,
|
|
756
|
-
strategy,
|
|
757
|
-
strategyConfigured,
|
|
758
|
-
});
|
|
759
|
-
} else if (msg.action === 'get_strategy') {
|
|
760
|
-
const raw = loadRawConfig();
|
|
761
|
-
const strategy = (raw.twitterOperationStrategy as 'oauth' | 'browser' | 'auto' | undefined) ?? 'auto';
|
|
762
|
-
const strategyConfigured = Object.prototype.hasOwnProperty.call(raw, 'twitterOperationStrategy');
|
|
763
|
-
ctx.send(socket, {
|
|
764
|
-
type: 'twitter_integration_config_response',
|
|
765
|
-
success: true,
|
|
766
|
-
managedAvailable: false,
|
|
767
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
768
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
769
|
-
strategy,
|
|
770
|
-
strategyConfigured,
|
|
771
|
-
});
|
|
772
|
-
} else if (msg.action === 'set_strategy') {
|
|
773
|
-
const valid = ['oauth', 'browser', 'auto'];
|
|
774
|
-
const value = msg.strategy;
|
|
775
|
-
if (!value || !valid.includes(value)) {
|
|
776
|
-
ctx.send(socket, {
|
|
777
|
-
type: 'twitter_integration_config_response',
|
|
778
|
-
success: false,
|
|
779
|
-
managedAvailable: false,
|
|
780
|
-
localClientConfigured: false,
|
|
781
|
-
connected: false,
|
|
782
|
-
error: `Invalid strategy value: ${String(value)}. Must be one of: ${valid.join(', ')}`,
|
|
783
|
-
});
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
const raw = loadRawConfig();
|
|
787
|
-
raw.twitterOperationStrategy = value;
|
|
788
|
-
saveRawConfig(raw);
|
|
789
|
-
ctx.send(socket, {
|
|
790
|
-
type: 'twitter_integration_config_response',
|
|
791
|
-
success: true,
|
|
792
|
-
managedAvailable: false,
|
|
793
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
794
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
795
|
-
strategy: value as 'oauth' | 'browser' | 'auto',
|
|
796
|
-
strategyConfigured: true,
|
|
797
|
-
});
|
|
798
|
-
} else if (msg.action === 'set_mode') {
|
|
799
|
-
const raw = loadRawConfig();
|
|
800
|
-
raw.twitterIntegrationMode = msg.mode ?? 'local_byo';
|
|
801
|
-
saveRawConfig(raw);
|
|
802
|
-
ctx.send(socket, {
|
|
803
|
-
type: 'twitter_integration_config_response',
|
|
804
|
-
success: true,
|
|
805
|
-
mode: msg.mode ?? 'local_byo',
|
|
806
|
-
managedAvailable: false,
|
|
807
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
808
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
809
|
-
});
|
|
810
|
-
} else if (msg.action === 'set_local_client') {
|
|
811
|
-
if (!msg.clientId) {
|
|
812
|
-
ctx.send(socket, {
|
|
813
|
-
type: 'twitter_integration_config_response',
|
|
814
|
-
success: false,
|
|
815
|
-
managedAvailable: false,
|
|
816
|
-
localClientConfigured: false,
|
|
817
|
-
connected: false,
|
|
818
|
-
error: 'clientId is required for set_local_client action',
|
|
819
|
-
});
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
const previousClientId = getSecureKey('credential:integration:twitter:oauth_client_id');
|
|
823
|
-
const storedId = setSecureKey('credential:integration:twitter:oauth_client_id', msg.clientId);
|
|
824
|
-
if (!storedId) {
|
|
825
|
-
ctx.send(socket, {
|
|
826
|
-
type: 'twitter_integration_config_response',
|
|
827
|
-
success: false,
|
|
828
|
-
managedAvailable: false,
|
|
829
|
-
localClientConfigured: false,
|
|
830
|
-
connected: false,
|
|
831
|
-
error: 'Failed to store client ID in secure storage',
|
|
832
|
-
});
|
|
833
|
-
return;
|
|
834
|
-
}
|
|
835
|
-
if (msg.clientSecret) {
|
|
836
|
-
const storedSecret = setSecureKey('credential:integration:twitter:oauth_client_secret', msg.clientSecret);
|
|
837
|
-
if (!storedSecret) {
|
|
838
|
-
// Roll back the client ID to its previous value to avoid inconsistent OAuth state
|
|
839
|
-
if (previousClientId) {
|
|
840
|
-
setSecureKey('credential:integration:twitter:oauth_client_id', previousClientId);
|
|
841
|
-
} else {
|
|
842
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_id');
|
|
843
|
-
}
|
|
844
|
-
ctx.send(socket, {
|
|
845
|
-
type: 'twitter_integration_config_response',
|
|
846
|
-
success: false,
|
|
847
|
-
managedAvailable: false,
|
|
848
|
-
localClientConfigured: !!previousClientId,
|
|
849
|
-
connected: false,
|
|
850
|
-
error: 'Failed to store client secret in secure storage',
|
|
851
|
-
});
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
} else {
|
|
855
|
-
// Clear any stale secret when updating client without a secret (e.g. switching to PKCE)
|
|
856
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_secret');
|
|
857
|
-
}
|
|
858
|
-
ctx.send(socket, {
|
|
859
|
-
type: 'twitter_integration_config_response',
|
|
860
|
-
success: true,
|
|
861
|
-
managedAvailable: false,
|
|
862
|
-
localClientConfigured: true,
|
|
863
|
-
connected: !!getSecureKey('credential:integration:twitter:access_token'),
|
|
864
|
-
});
|
|
865
|
-
} else if (msg.action === 'clear_local_client') {
|
|
866
|
-
// If connected, disconnect first
|
|
867
|
-
if (getSecureKey('credential:integration:twitter:access_token')) {
|
|
868
|
-
deleteSecureKey('credential:integration:twitter:access_token');
|
|
869
|
-
deleteSecureKey('credential:integration:twitter:refresh_token');
|
|
870
|
-
deleteCredentialMetadata('integration:twitter', 'access_token');
|
|
871
|
-
}
|
|
872
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_id');
|
|
873
|
-
deleteSecureKey('credential:integration:twitter:oauth_client_secret');
|
|
874
|
-
ctx.send(socket, {
|
|
875
|
-
type: 'twitter_integration_config_response',
|
|
876
|
-
success: true,
|
|
877
|
-
managedAvailable: false,
|
|
878
|
-
localClientConfigured: false,
|
|
879
|
-
connected: false,
|
|
880
|
-
});
|
|
881
|
-
} else if (msg.action === 'disconnect') {
|
|
882
|
-
deleteSecureKey('credential:integration:twitter:access_token');
|
|
883
|
-
deleteSecureKey('credential:integration:twitter:refresh_token');
|
|
884
|
-
deleteCredentialMetadata('integration:twitter', 'access_token');
|
|
885
|
-
ctx.send(socket, {
|
|
886
|
-
type: 'twitter_integration_config_response',
|
|
887
|
-
success: true,
|
|
888
|
-
managedAvailable: false,
|
|
889
|
-
localClientConfigured: !!getSecureKey('credential:integration:twitter:oauth_client_id'),
|
|
890
|
-
connected: false,
|
|
891
|
-
});
|
|
892
|
-
} else {
|
|
893
|
-
ctx.send(socket, {
|
|
894
|
-
type: 'twitter_integration_config_response',
|
|
895
|
-
success: false,
|
|
896
|
-
managedAvailable: false,
|
|
897
|
-
localClientConfigured: false,
|
|
898
|
-
connected: false,
|
|
899
|
-
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
} catch (err) {
|
|
903
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
904
|
-
log.error({ err }, 'Failed to handle Twitter integration config');
|
|
905
|
-
ctx.send(socket, {
|
|
906
|
-
type: 'twitter_integration_config_response',
|
|
907
|
-
success: false,
|
|
908
|
-
managedAvailable: false,
|
|
909
|
-
localClientConfigured: false,
|
|
910
|
-
connected: false,
|
|
911
|
-
error: message,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
export async function handleTelegramConfig(
|
|
917
|
-
msg: TelegramConfigRequest,
|
|
918
|
-
socket: net.Socket,
|
|
919
|
-
ctx: HandlerContext,
|
|
920
|
-
): Promise<void> {
|
|
921
|
-
try {
|
|
922
|
-
if (msg.action === 'get') {
|
|
923
|
-
const hasBotToken = !!getSecureKey('credential:telegram:bot_token');
|
|
924
|
-
const hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
925
|
-
const meta = getCredentialMetadata('telegram', 'bot_token');
|
|
926
|
-
const botUsername = meta?.accountInfo ?? undefined;
|
|
927
|
-
ctx.send(socket, {
|
|
928
|
-
type: 'telegram_config_response',
|
|
929
|
-
success: true,
|
|
930
|
-
hasBotToken,
|
|
931
|
-
botUsername,
|
|
932
|
-
connected: hasBotToken && hasWebhookSecret,
|
|
933
|
-
hasWebhookSecret,
|
|
934
|
-
});
|
|
935
|
-
} else if (msg.action === 'set') {
|
|
936
|
-
// Resolve token: prefer explicit msg.botToken, fall back to secure storage.
|
|
937
|
-
// Track provenance so we only rollback tokens that were freshly provided.
|
|
938
|
-
const isNewToken = !!msg.botToken;
|
|
939
|
-
const botToken = msg.botToken || getSecureKey('credential:telegram:bot_token');
|
|
940
|
-
if (!botToken) {
|
|
941
|
-
ctx.send(socket, {
|
|
942
|
-
type: 'telegram_config_response',
|
|
943
|
-
success: false,
|
|
944
|
-
hasBotToken: false,
|
|
945
|
-
connected: false,
|
|
946
|
-
hasWebhookSecret: false,
|
|
947
|
-
error: 'botToken is required for set action',
|
|
948
|
-
});
|
|
949
|
-
return;
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Validate token via Telegram getMe API
|
|
953
|
-
let botUsername: string;
|
|
954
|
-
try {
|
|
955
|
-
const res = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
|
956
|
-
if (!res.ok) {
|
|
957
|
-
const body = await res.text();
|
|
958
|
-
ctx.send(socket, {
|
|
959
|
-
type: 'telegram_config_response',
|
|
960
|
-
success: false,
|
|
961
|
-
hasBotToken: false,
|
|
962
|
-
connected: false,
|
|
963
|
-
hasWebhookSecret: false,
|
|
964
|
-
error: `Telegram API validation failed: ${body}`,
|
|
965
|
-
});
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const data = await res.json() as { ok: boolean; result?: { username?: string } };
|
|
969
|
-
if (!data.ok || !data.result?.username) {
|
|
970
|
-
ctx.send(socket, {
|
|
971
|
-
type: 'telegram_config_response',
|
|
972
|
-
success: false,
|
|
973
|
-
hasBotToken: false,
|
|
974
|
-
connected: false,
|
|
975
|
-
hasWebhookSecret: false,
|
|
976
|
-
error: 'Telegram API returned unexpected response',
|
|
977
|
-
});
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
botUsername = data.result.username;
|
|
981
|
-
} catch (err) {
|
|
982
|
-
const message = summarizeTelegramError(err);
|
|
983
|
-
ctx.send(socket, {
|
|
984
|
-
type: 'telegram_config_response',
|
|
985
|
-
success: false,
|
|
986
|
-
hasBotToken: false,
|
|
987
|
-
connected: false,
|
|
988
|
-
hasWebhookSecret: false,
|
|
989
|
-
error: `Failed to validate bot token: ${message}`,
|
|
990
|
-
});
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Store bot token securely
|
|
995
|
-
const stored = setSecureKey('credential:telegram:bot_token', botToken);
|
|
996
|
-
if (!stored) {
|
|
997
|
-
ctx.send(socket, {
|
|
998
|
-
type: 'telegram_config_response',
|
|
999
|
-
success: false,
|
|
1000
|
-
hasBotToken: false,
|
|
1001
|
-
connected: false,
|
|
1002
|
-
hasWebhookSecret: false,
|
|
1003
|
-
error: 'Failed to store bot token in secure storage',
|
|
1004
|
-
});
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// Store metadata with bot username
|
|
1009
|
-
upsertCredentialMetadata('telegram', 'bot_token', {
|
|
1010
|
-
accountInfo: botUsername,
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
// Ensure webhook secret exists (generate if missing)
|
|
1014
|
-
let hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
1015
|
-
if (!hasWebhookSecret) {
|
|
1016
|
-
const { randomUUID } = await import('node:crypto');
|
|
1017
|
-
const webhookSecret = randomUUID();
|
|
1018
|
-
const secretStored = setSecureKey('credential:telegram:webhook_secret', webhookSecret);
|
|
1019
|
-
if (secretStored) {
|
|
1020
|
-
upsertCredentialMetadata('telegram', 'webhook_secret', {});
|
|
1021
|
-
hasWebhookSecret = true;
|
|
1022
|
-
} else {
|
|
1023
|
-
// Only roll back the bot token if it was freshly provided.
|
|
1024
|
-
// When the token came from secure storage it was already valid
|
|
1025
|
-
// configuration; deleting it would destroy working state.
|
|
1026
|
-
if (isNewToken) {
|
|
1027
|
-
deleteSecureKey('credential:telegram:bot_token');
|
|
1028
|
-
deleteCredentialMetadata('telegram', 'bot_token');
|
|
1029
|
-
}
|
|
1030
|
-
ctx.send(socket, {
|
|
1031
|
-
type: 'telegram_config_response',
|
|
1032
|
-
success: false,
|
|
1033
|
-
hasBotToken: !isNewToken,
|
|
1034
|
-
connected: false,
|
|
1035
|
-
hasWebhookSecret: false,
|
|
1036
|
-
error: 'Failed to store webhook secret',
|
|
1037
|
-
});
|
|
1038
|
-
return;
|
|
1039
|
-
}
|
|
1040
|
-
} else {
|
|
1041
|
-
// Self-heal: ensure metadata exists even when the secret was
|
|
1042
|
-
// already present (covers previously lost/corrupted metadata).
|
|
1043
|
-
upsertCredentialMetadata('telegram', 'webhook_secret', {});
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
ctx.send(socket, {
|
|
1047
|
-
type: 'telegram_config_response',
|
|
1048
|
-
success: true,
|
|
1049
|
-
hasBotToken: true,
|
|
1050
|
-
botUsername,
|
|
1051
|
-
connected: true,
|
|
1052
|
-
hasWebhookSecret,
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
// Trigger gateway reconcile so the webhook registration updates immediately
|
|
1056
|
-
const effectiveUrl = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
1057
|
-
if (effectiveUrl) {
|
|
1058
|
-
triggerGatewayReconcile(effectiveUrl);
|
|
1059
|
-
}
|
|
1060
|
-
} else if (msg.action === 'clear') {
|
|
1061
|
-
// Deregister the Telegram webhook before deleting credentials.
|
|
1062
|
-
// The gateway reconcile short-circuits when credentials are absent,
|
|
1063
|
-
// so we must call the Telegram API directly while the token is still
|
|
1064
|
-
// available.
|
|
1065
|
-
const botToken = getSecureKey('credential:telegram:bot_token');
|
|
1066
|
-
if (botToken) {
|
|
1067
|
-
try {
|
|
1068
|
-
await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`);
|
|
1069
|
-
} catch (err) {
|
|
1070
|
-
log.warn(
|
|
1071
|
-
{ error: summarizeTelegramError(err) },
|
|
1072
|
-
'Failed to deregister Telegram webhook (proceeding with credential cleanup)',
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
deleteSecureKey('credential:telegram:bot_token');
|
|
1078
|
-
deleteCredentialMetadata('telegram', 'bot_token');
|
|
1079
|
-
deleteSecureKey('credential:telegram:webhook_secret');
|
|
1080
|
-
deleteCredentialMetadata('telegram', 'webhook_secret');
|
|
1081
|
-
|
|
1082
|
-
ctx.send(socket, {
|
|
1083
|
-
type: 'telegram_config_response',
|
|
1084
|
-
success: true,
|
|
1085
|
-
hasBotToken: false,
|
|
1086
|
-
connected: false,
|
|
1087
|
-
hasWebhookSecret: false,
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
// Trigger reconcile to deregister webhook
|
|
1091
|
-
const effectiveUrl = process.env.INGRESS_PUBLIC_BASE_URL;
|
|
1092
|
-
if (effectiveUrl) {
|
|
1093
|
-
triggerGatewayReconcile(effectiveUrl);
|
|
1094
|
-
}
|
|
1095
|
-
} else if (msg.action === 'set_commands') {
|
|
1096
|
-
const storedToken = getSecureKey('credential:telegram:bot_token');
|
|
1097
|
-
if (!storedToken) {
|
|
1098
|
-
ctx.send(socket, {
|
|
1099
|
-
type: 'telegram_config_response',
|
|
1100
|
-
success: false,
|
|
1101
|
-
hasBotToken: false,
|
|
1102
|
-
connected: false,
|
|
1103
|
-
hasWebhookSecret: false,
|
|
1104
|
-
error: 'Bot token not configured. Run set action first.',
|
|
1105
|
-
});
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const commands = msg.commands ?? [
|
|
1110
|
-
{ command: 'new', description: 'Start a new conversation' },
|
|
1111
|
-
{ command: 'guardian_verify', description: 'Verify your guardian identity' },
|
|
1112
|
-
];
|
|
1113
|
-
|
|
1114
|
-
try {
|
|
1115
|
-
const res = await fetch(`https://api.telegram.org/bot${storedToken}/setMyCommands`, {
|
|
1116
|
-
method: 'POST',
|
|
1117
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1118
|
-
body: JSON.stringify({ commands }),
|
|
1119
|
-
});
|
|
1120
|
-
if (!res.ok) {
|
|
1121
|
-
const body = await res.text();
|
|
1122
|
-
ctx.send(socket, {
|
|
1123
|
-
type: 'telegram_config_response',
|
|
1124
|
-
success: false,
|
|
1125
|
-
hasBotToken: true,
|
|
1126
|
-
connected: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1127
|
-
hasWebhookSecret: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1128
|
-
error: `Failed to set bot commands: ${body}`,
|
|
1129
|
-
});
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
} catch (err) {
|
|
1133
|
-
const message = summarizeTelegramError(err);
|
|
1134
|
-
ctx.send(socket, {
|
|
1135
|
-
type: 'telegram_config_response',
|
|
1136
|
-
success: false,
|
|
1137
|
-
hasBotToken: true,
|
|
1138
|
-
connected: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1139
|
-
hasWebhookSecret: !!getSecureKey('credential:telegram:webhook_secret'),
|
|
1140
|
-
error: `Failed to set bot commands: ${message}`,
|
|
1141
|
-
});
|
|
1142
|
-
return;
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const hasBotToken = !!getSecureKey('credential:telegram:bot_token');
|
|
1146
|
-
const hasWebhookSecret = !!getSecureKey('credential:telegram:webhook_secret');
|
|
1147
|
-
ctx.send(socket, {
|
|
1148
|
-
type: 'telegram_config_response',
|
|
1149
|
-
success: true,
|
|
1150
|
-
hasBotToken,
|
|
1151
|
-
connected: hasBotToken && hasWebhookSecret,
|
|
1152
|
-
hasWebhookSecret,
|
|
1153
|
-
});
|
|
1154
|
-
} else {
|
|
1155
|
-
ctx.send(socket, {
|
|
1156
|
-
type: 'telegram_config_response',
|
|
1157
|
-
success: false,
|
|
1158
|
-
hasBotToken: false,
|
|
1159
|
-
connected: false,
|
|
1160
|
-
hasWebhookSecret: false,
|
|
1161
|
-
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
1162
|
-
});
|
|
1163
|
-
}
|
|
1164
|
-
} catch (err) {
|
|
1165
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1166
|
-
log.error({ err }, 'Failed to handle Telegram config');
|
|
1167
|
-
ctx.send(socket, {
|
|
1168
|
-
type: 'telegram_config_response',
|
|
1169
|
-
success: false,
|
|
1170
|
-
hasBotToken: false,
|
|
1171
|
-
connected: false,
|
|
1172
|
-
hasWebhookSecret: false,
|
|
1173
|
-
error: message,
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
export async function handleTwilioConfig(
|
|
1179
|
-
msg: TwilioConfigRequest,
|
|
1180
|
-
socket: net.Socket,
|
|
1181
|
-
ctx: HandlerContext,
|
|
1182
|
-
): Promise<void> {
|
|
1183
|
-
try {
|
|
1184
|
-
if (msg.action === 'get') {
|
|
1185
|
-
const hasCredentials = hasTwilioCredentials();
|
|
1186
|
-
const raw = loadRawConfig();
|
|
1187
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1188
|
-
// When assistantId is provided, look up in assistantPhoneNumbers first,
|
|
1189
|
-
// fall back to the legacy phoneNumber field
|
|
1190
|
-
let phoneNumber: string;
|
|
1191
|
-
if (msg.assistantId) {
|
|
1192
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1193
|
-
phoneNumber = mapping[msg.assistantId] ?? (sms.phoneNumber as string) ?? '';
|
|
1194
|
-
} else {
|
|
1195
|
-
phoneNumber = (sms.phoneNumber as string) ?? '';
|
|
1196
|
-
}
|
|
1197
|
-
ctx.send(socket, {
|
|
1198
|
-
type: 'twilio_config_response',
|
|
1199
|
-
success: true,
|
|
1200
|
-
hasCredentials,
|
|
1201
|
-
phoneNumber: phoneNumber || undefined,
|
|
1202
|
-
});
|
|
1203
|
-
} else if (msg.action === 'set_credentials') {
|
|
1204
|
-
if (!msg.accountSid || !msg.authToken) {
|
|
1205
|
-
ctx.send(socket, {
|
|
1206
|
-
type: 'twilio_config_response',
|
|
1207
|
-
success: false,
|
|
1208
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1209
|
-
error: 'accountSid and authToken are required for set_credentials action',
|
|
1210
|
-
});
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Validate credentials by calling the Twilio API
|
|
1215
|
-
const authHeader = 'Basic ' + Buffer.from(`${msg.accountSid}:${msg.authToken}`).toString('base64');
|
|
1216
|
-
try {
|
|
1217
|
-
const res = await fetch(
|
|
1218
|
-
`https://api.twilio.com/2010-04-01/Accounts/${msg.accountSid}.json`,
|
|
1219
|
-
{
|
|
1220
|
-
method: 'GET',
|
|
1221
|
-
headers: { Authorization: authHeader },
|
|
1222
|
-
},
|
|
1223
|
-
);
|
|
1224
|
-
if (!res.ok) {
|
|
1225
|
-
const body = await res.text();
|
|
1226
|
-
ctx.send(socket, {
|
|
1227
|
-
type: 'twilio_config_response',
|
|
1228
|
-
success: false,
|
|
1229
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1230
|
-
error: `Twilio API validation failed (${res.status}): ${body}`,
|
|
1231
|
-
});
|
|
1232
|
-
return;
|
|
1233
|
-
}
|
|
1234
|
-
} catch (err) {
|
|
1235
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1236
|
-
ctx.send(socket, {
|
|
1237
|
-
type: 'twilio_config_response',
|
|
1238
|
-
success: false,
|
|
1239
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1240
|
-
error: `Failed to validate Twilio credentials: ${message}`,
|
|
1241
|
-
});
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// Store credentials securely
|
|
1246
|
-
const sidStored = setSecureKey('credential:twilio:account_sid', msg.accountSid);
|
|
1247
|
-
if (!sidStored) {
|
|
1248
|
-
ctx.send(socket, {
|
|
1249
|
-
type: 'twilio_config_response',
|
|
1250
|
-
success: false,
|
|
1251
|
-
hasCredentials: false,
|
|
1252
|
-
error: 'Failed to store Account SID in secure storage',
|
|
1253
|
-
});
|
|
1254
|
-
return;
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
const tokenStored = setSecureKey('credential:twilio:auth_token', msg.authToken);
|
|
1258
|
-
if (!tokenStored) {
|
|
1259
|
-
// Roll back the Account SID
|
|
1260
|
-
deleteSecureKey('credential:twilio:account_sid');
|
|
1261
|
-
ctx.send(socket, {
|
|
1262
|
-
type: 'twilio_config_response',
|
|
1263
|
-
success: false,
|
|
1264
|
-
hasCredentials: false,
|
|
1265
|
-
error: 'Failed to store Auth Token in secure storage',
|
|
1266
|
-
});
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
upsertCredentialMetadata('twilio', 'account_sid', {});
|
|
1271
|
-
upsertCredentialMetadata('twilio', 'auth_token', {});
|
|
1272
|
-
|
|
1273
|
-
ctx.send(socket, {
|
|
1274
|
-
type: 'twilio_config_response',
|
|
1275
|
-
success: true,
|
|
1276
|
-
hasCredentials: true,
|
|
1277
|
-
});
|
|
1278
|
-
} else if (msg.action === 'clear_credentials') {
|
|
1279
|
-
// Only clear authentication credentials (Account SID and Auth Token).
|
|
1280
|
-
// Preserve the phone number in both config (sms.phoneNumber) and secure
|
|
1281
|
-
// key (credential:twilio:phone_number) so that re-entering credentials
|
|
1282
|
-
// resumes working without needing to reassign the number.
|
|
1283
|
-
deleteSecureKey('credential:twilio:account_sid');
|
|
1284
|
-
deleteSecureKey('credential:twilio:auth_token');
|
|
1285
|
-
deleteCredentialMetadata('twilio', 'account_sid');
|
|
1286
|
-
deleteCredentialMetadata('twilio', 'auth_token');
|
|
1287
|
-
|
|
1288
|
-
ctx.send(socket, {
|
|
1289
|
-
type: 'twilio_config_response',
|
|
1290
|
-
success: true,
|
|
1291
|
-
hasCredentials: false,
|
|
1292
|
-
});
|
|
1293
|
-
} else if (msg.action === 'provision_number') {
|
|
1294
|
-
if (!hasTwilioCredentials()) {
|
|
1295
|
-
ctx.send(socket, {
|
|
1296
|
-
type: 'twilio_config_response',
|
|
1297
|
-
success: false,
|
|
1298
|
-
hasCredentials: false,
|
|
1299
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1300
|
-
});
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1305
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1306
|
-
const country = msg.country ?? 'US';
|
|
1307
|
-
|
|
1308
|
-
// Search for an available number
|
|
1309
|
-
const available = await searchAvailableNumbers(accountSid, authToken, country, msg.areaCode);
|
|
1310
|
-
if (available.length === 0) {
|
|
1311
|
-
ctx.send(socket, {
|
|
1312
|
-
type: 'twilio_config_response',
|
|
1313
|
-
success: false,
|
|
1314
|
-
hasCredentials: true,
|
|
1315
|
-
error: `No available phone numbers found for country=${country}${msg.areaCode ? ` areaCode=${msg.areaCode}` : ''}`,
|
|
1316
|
-
});
|
|
1317
|
-
return;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
// Purchase the first available number
|
|
1321
|
-
const purchased = await provisionPhoneNumber(accountSid, authToken, available[0].phoneNumber);
|
|
1322
|
-
|
|
1323
|
-
// Auto-assign: persist the purchased number in secure storage and config
|
|
1324
|
-
// (same persistence as assign_number for consistency)
|
|
1325
|
-
const phoneStored = setSecureKey('credential:twilio:phone_number', purchased.phoneNumber);
|
|
1326
|
-
if (!phoneStored) {
|
|
1327
|
-
ctx.send(socket, {
|
|
1328
|
-
type: 'twilio_config_response',
|
|
1329
|
-
success: false,
|
|
1330
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1331
|
-
phoneNumber: purchased.phoneNumber,
|
|
1332
|
-
error: `Phone number ${purchased.phoneNumber} was purchased but could not be saved. Use assign_number to assign it manually.`,
|
|
1333
|
-
});
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
const raw = loadRawConfig();
|
|
1338
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1339
|
-
// When assistantId is provided, only set the legacy global phoneNumber
|
|
1340
|
-
// if it's not already set — this prevents multi-assistant assignments
|
|
1341
|
-
// from clobbering each other's outbound SMS number.
|
|
1342
|
-
if (msg.assistantId) {
|
|
1343
|
-
if (!sms.phoneNumber) {
|
|
1344
|
-
sms.phoneNumber = purchased.phoneNumber;
|
|
1345
|
-
}
|
|
1346
|
-
} else {
|
|
1347
|
-
sms.phoneNumber = purchased.phoneNumber;
|
|
1348
|
-
}
|
|
1349
|
-
// When assistantId is provided, also persist into the per-assistant mapping
|
|
1350
|
-
if (msg.assistantId) {
|
|
1351
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1352
|
-
mapping[msg.assistantId] = purchased.phoneNumber;
|
|
1353
|
-
sms.assistantPhoneNumbers = mapping;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
1357
|
-
ctx.setSuppressConfigReload(true);
|
|
1358
|
-
try {
|
|
1359
|
-
saveRawConfig({ ...raw, sms });
|
|
1360
|
-
} catch (err) {
|
|
1361
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
1362
|
-
throw err;
|
|
1363
|
-
}
|
|
1364
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1365
|
-
|
|
1366
|
-
// Best-effort webhook configuration — non-fatal so the number is
|
|
1367
|
-
// still usable even if ingress isn't configured yet.
|
|
1368
|
-
const webhookResult = await syncTwilioWebhooks(
|
|
1369
|
-
purchased.phoneNumber,
|
|
1370
|
-
accountSid,
|
|
1371
|
-
authToken,
|
|
1372
|
-
loadRawConfig() as IngressConfig,
|
|
1373
|
-
);
|
|
1374
|
-
|
|
1375
|
-
ctx.send(socket, {
|
|
1376
|
-
type: 'twilio_config_response',
|
|
1377
|
-
success: true,
|
|
1378
|
-
hasCredentials: true,
|
|
1379
|
-
phoneNumber: purchased.phoneNumber,
|
|
1380
|
-
warning: webhookResult.warning,
|
|
1381
|
-
});
|
|
1382
|
-
} else if (msg.action === 'assign_number') {
|
|
1383
|
-
if (!msg.phoneNumber) {
|
|
1384
|
-
ctx.send(socket, {
|
|
1385
|
-
type: 'twilio_config_response',
|
|
1386
|
-
success: false,
|
|
1387
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1388
|
-
error: 'phoneNumber is required for assign_number action',
|
|
1389
|
-
});
|
|
1390
|
-
return;
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
// Persist the phone number in the secure credential store so the
|
|
1394
|
-
// active Twilio runtime can read it via credential:twilio:phone_number
|
|
1395
|
-
const phoneStored = setSecureKey('credential:twilio:phone_number', msg.phoneNumber);
|
|
1396
|
-
if (!phoneStored) {
|
|
1397
|
-
ctx.send(socket, {
|
|
1398
|
-
type: 'twilio_config_response',
|
|
1399
|
-
success: false,
|
|
1400
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1401
|
-
error: 'Failed to store phone number in secure storage',
|
|
1402
|
-
});
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
// Also persist in assistant config (non-secret) for the UI
|
|
1407
|
-
const raw = loadRawConfig();
|
|
1408
|
-
const sms = (raw?.sms ?? {}) as Record<string, unknown>;
|
|
1409
|
-
// When assistantId is provided, only set the legacy global phoneNumber
|
|
1410
|
-
// if it's not already set — this prevents multi-assistant assignments
|
|
1411
|
-
// from clobbering each other's outbound SMS number.
|
|
1412
|
-
if (msg.assistantId) {
|
|
1413
|
-
if (!sms.phoneNumber) {
|
|
1414
|
-
sms.phoneNumber = msg.phoneNumber;
|
|
1415
|
-
}
|
|
1416
|
-
} else {
|
|
1417
|
-
sms.phoneNumber = msg.phoneNumber;
|
|
1418
|
-
}
|
|
1419
|
-
// When assistantId is provided, also persist into the per-assistant mapping
|
|
1420
|
-
if (msg.assistantId) {
|
|
1421
|
-
const mapping = (sms.assistantPhoneNumbers as Record<string, string> | undefined) ?? {};
|
|
1422
|
-
mapping[msg.assistantId] = msg.phoneNumber;
|
|
1423
|
-
sms.assistantPhoneNumbers = mapping;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
const wasSuppressed = ctx.suppressConfigReload;
|
|
1427
|
-
ctx.setSuppressConfigReload(true);
|
|
1428
|
-
try {
|
|
1429
|
-
saveRawConfig({ ...raw, sms });
|
|
1430
|
-
} catch (err) {
|
|
1431
|
-
ctx.setSuppressConfigReload(wasSuppressed);
|
|
1432
|
-
throw err;
|
|
1433
|
-
}
|
|
1434
|
-
ctx.debounceTimers.schedule('__suppress_reset__', () => { ctx.setSuppressConfigReload(false); }, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
1435
|
-
|
|
1436
|
-
// Best-effort webhook configuration when credentials are available
|
|
1437
|
-
let webhookWarning: string | undefined;
|
|
1438
|
-
if (hasTwilioCredentials()) {
|
|
1439
|
-
const acctSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1440
|
-
const acctToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1441
|
-
const webhookResult = await syncTwilioWebhooks(
|
|
1442
|
-
msg.phoneNumber,
|
|
1443
|
-
acctSid,
|
|
1444
|
-
acctToken,
|
|
1445
|
-
loadRawConfig() as IngressConfig,
|
|
1446
|
-
);
|
|
1447
|
-
webhookWarning = webhookResult.warning;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
ctx.send(socket, {
|
|
1451
|
-
type: 'twilio_config_response',
|
|
1452
|
-
success: true,
|
|
1453
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1454
|
-
phoneNumber: msg.phoneNumber,
|
|
1455
|
-
warning: webhookWarning,
|
|
1456
|
-
});
|
|
1457
|
-
} else if (msg.action === 'list_numbers') {
|
|
1458
|
-
if (!hasTwilioCredentials()) {
|
|
1459
|
-
ctx.send(socket, {
|
|
1460
|
-
type: 'twilio_config_response',
|
|
1461
|
-
success: false,
|
|
1462
|
-
hasCredentials: false,
|
|
1463
|
-
error: 'Twilio credentials not configured. Set credentials first.',
|
|
1464
|
-
});
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
const accountSid = getSecureKey('credential:twilio:account_sid')!;
|
|
1469
|
-
const authToken = getSecureKey('credential:twilio:auth_token')!;
|
|
1470
|
-
const numbers = await listIncomingPhoneNumbers(accountSid, authToken);
|
|
1471
|
-
|
|
1472
|
-
ctx.send(socket, {
|
|
1473
|
-
type: 'twilio_config_response',
|
|
1474
|
-
success: true,
|
|
1475
|
-
hasCredentials: true,
|
|
1476
|
-
numbers,
|
|
1477
|
-
});
|
|
1478
|
-
} else {
|
|
1479
|
-
ctx.send(socket, {
|
|
1480
|
-
type: 'twilio_config_response',
|
|
1481
|
-
success: false,
|
|
1482
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1483
|
-
error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
} catch (err) {
|
|
1487
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1488
|
-
log.error({ err }, 'Failed to handle Twilio config');
|
|
1489
|
-
ctx.send(socket, {
|
|
1490
|
-
type: 'twilio_config_response',
|
|
1491
|
-
success: false,
|
|
1492
|
-
hasCredentials: hasTwilioCredentials(),
|
|
1493
|
-
error: message,
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
export function handleGuardianVerification(
|
|
1499
|
-
msg: GuardianVerificationRequest,
|
|
1500
|
-
socket: net.Socket,
|
|
1501
|
-
ctx: HandlerContext,
|
|
1502
|
-
): void {
|
|
1503
|
-
// Use the assistant ID from the request when available; fall back to
|
|
1504
|
-
// 'self' for backward compatibility with single-assistant mode.
|
|
1505
|
-
const assistantId = msg.assistantId ?? 'self';
|
|
1506
|
-
const channel = msg.channel ?? 'telegram';
|
|
1507
|
-
|
|
1508
|
-
try {
|
|
1509
|
-
if (msg.action === 'create_challenge') {
|
|
1510
|
-
const result = createVerificationChallenge(assistantId, channel, msg.sessionId);
|
|
1511
|
-
|
|
1512
|
-
ctx.send(socket, {
|
|
1513
|
-
type: 'guardian_verification_response',
|
|
1514
|
-
success: true,
|
|
1515
|
-
secret: result.secret,
|
|
1516
|
-
instruction: result.instruction,
|
|
1517
|
-
channel,
|
|
1518
|
-
});
|
|
1519
|
-
} else if (msg.action === 'status') {
|
|
1520
|
-
const binding = getGuardianBinding(assistantId, channel);
|
|
1521
|
-
ctx.send(socket, {
|
|
1522
|
-
type: 'guardian_verification_response',
|
|
1523
|
-
success: true,
|
|
1524
|
-
bound: binding !== null,
|
|
1525
|
-
guardianExternalUserId: binding?.guardianExternalUserId,
|
|
1526
|
-
channel,
|
|
1527
|
-
assistantId,
|
|
1528
|
-
guardianDeliveryChatId: binding?.guardianDeliveryChatId,
|
|
1529
|
-
});
|
|
1530
|
-
} else if (msg.action === 'revoke') {
|
|
1531
|
-
revokeGuardianBinding(assistantId, channel);
|
|
1532
|
-
ctx.send(socket, {
|
|
1533
|
-
type: 'guardian_verification_response',
|
|
1534
|
-
success: true,
|
|
1535
|
-
bound: false,
|
|
1536
|
-
channel,
|
|
1537
|
-
});
|
|
1538
|
-
} else {
|
|
1539
|
-
ctx.send(socket, {
|
|
1540
|
-
type: 'guardian_verification_response',
|
|
1541
|
-
success: false,
|
|
1542
|
-
error: `Unknown action: ${String(msg.action)}`,
|
|
1543
|
-
channel,
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
} catch (err) {
|
|
1547
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1548
|
-
log.error({ err }, 'Failed to handle guardian verification');
|
|
1549
|
-
ctx.send(socket, {
|
|
1550
|
-
type: 'guardian_verification_response',
|
|
1551
|
-
success: false,
|
|
1552
|
-
error: message,
|
|
1553
|
-
channel,
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
export function handleEnvVarsRequest(socket: net.Socket, ctx: HandlerContext): void {
|
|
1559
|
-
const vars: Record<string, string> = {};
|
|
1560
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
1561
|
-
if (value !== undefined) vars[key] = value;
|
|
1562
|
-
}
|
|
1563
|
-
ctx.send(socket, { type: 'env_vars_response', vars });
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
export async function handleToolPermissionSimulate(
|
|
1567
|
-
msg: ToolPermissionSimulateRequest,
|
|
1568
|
-
socket: net.Socket,
|
|
1569
|
-
ctx: HandlerContext,
|
|
1570
|
-
): Promise<void> {
|
|
1571
|
-
try {
|
|
1572
|
-
if (!msg.toolName || typeof msg.toolName !== 'string') {
|
|
1573
|
-
ctx.send(socket, {
|
|
1574
|
-
type: 'tool_permission_simulate_response',
|
|
1575
|
-
success: false,
|
|
1576
|
-
error: 'toolName is required',
|
|
1577
|
-
});
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
|
-
if (!msg.input || typeof msg.input !== 'object') {
|
|
1581
|
-
ctx.send(socket, {
|
|
1582
|
-
type: 'tool_permission_simulate_response',
|
|
1583
|
-
success: false,
|
|
1584
|
-
error: 'input is required and must be an object',
|
|
1585
|
-
});
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
const workingDir = msg.workingDir ?? process.cwd();
|
|
1590
|
-
|
|
1591
|
-
// Resolve execution target using manifest metadata or prefix heuristics.
|
|
1592
|
-
// resolveExecutionTarget handles unregistered tools via prefix fallback.
|
|
1593
|
-
const executionTarget = resolveExecutionTarget(msg.toolName);
|
|
1594
|
-
const policyContext = { executionTarget };
|
|
1595
|
-
|
|
1596
|
-
const riskLevel = await classifyRisk(msg.toolName, msg.input, workingDir);
|
|
1597
|
-
const result = await check(msg.toolName, msg.input, workingDir, policyContext);
|
|
1598
|
-
|
|
1599
|
-
// Private-thread override: promote allow → prompt for side-effect tools
|
|
1600
|
-
if (
|
|
1601
|
-
msg.forcePromptSideEffects
|
|
1602
|
-
&& result.decision === 'allow'
|
|
1603
|
-
&& isSideEffectTool(msg.toolName, msg.input)
|
|
1604
|
-
) {
|
|
1605
|
-
result.decision = 'prompt';
|
|
1606
|
-
result.reason = 'Private thread: side-effect tools require explicit approval';
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// Non-interactive override: convert prompt → deny
|
|
1610
|
-
if (msg.isInteractive === false && result.decision === 'prompt') {
|
|
1611
|
-
result.decision = 'deny';
|
|
1612
|
-
result.reason = 'Non-interactive session: no client to approve prompt';
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
// When decision is prompt, generate the full payload the UI needs
|
|
1616
|
-
let promptPayload: {
|
|
1617
|
-
allowlistOptions: Array<{ label: string; description: string; pattern: string }>;
|
|
1618
|
-
scopeOptions: Array<{ label: string; scope: string }>;
|
|
1619
|
-
persistentDecisionsAllowed: boolean;
|
|
1620
|
-
} | undefined;
|
|
1621
|
-
|
|
1622
|
-
if (result.decision === 'prompt') {
|
|
1623
|
-
const allowlistOptions = await generateAllowlistOptions(msg.toolName, msg.input);
|
|
1624
|
-
const scopeOptions = generateScopeOptions(workingDir, msg.toolName);
|
|
1625
|
-
const persistentDecisionsAllowed = !(
|
|
1626
|
-
msg.toolName === 'bash'
|
|
1627
|
-
&& msg.input.network_mode === 'proxied'
|
|
1628
|
-
);
|
|
1629
|
-
promptPayload = { allowlistOptions, scopeOptions, persistentDecisionsAllowed };
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
ctx.send(socket, {
|
|
1633
|
-
type: 'tool_permission_simulate_response',
|
|
1634
|
-
success: true,
|
|
1635
|
-
decision: result.decision,
|
|
1636
|
-
riskLevel,
|
|
1637
|
-
reason: result.reason,
|
|
1638
|
-
executionTarget,
|
|
1639
|
-
matchedRuleId: result.matchedRule?.id,
|
|
1640
|
-
promptPayload,
|
|
1641
|
-
});
|
|
1642
|
-
} catch (err) {
|
|
1643
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1644
|
-
log.error({ err }, 'Failed to simulate tool permission');
|
|
1645
|
-
ctx.send(socket, {
|
|
1646
|
-
type: 'tool_permission_simulate_response',
|
|
1647
|
-
success: false,
|
|
1648
|
-
error: message,
|
|
1649
|
-
});
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
export function handleToolNamesList(socket: net.Socket, ctx: HandlerContext): void {
|
|
1654
|
-
const tools = getAllTools();
|
|
1655
|
-
const names = tools.map((t) => t.name).sort((a, b) => a.localeCompare(b));
|
|
1656
|
-
const schemas: Record<string, import('../ipc-contract.js').ToolInputSchema> = {};
|
|
1657
|
-
for (const tool of tools) {
|
|
1658
|
-
try {
|
|
1659
|
-
const def = tool.getDefinition();
|
|
1660
|
-
schemas[tool.name] = def.input_schema as import('../ipc-contract.js').ToolInputSchema;
|
|
1661
|
-
} catch {
|
|
1662
|
-
// Skip tools whose definitions can't be resolved
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1665
|
-
ctx.send(socket, { type: 'tool_names_list_response', names, schemas });
|
|
1666
|
-
}
|
|
1667
18
|
|
|
1668
|
-
export
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
19
|
+
// Re-export individual handlers for direct import by tests and other modules
|
|
20
|
+
export { handleModelGet, handleModelSet, handleImageGenModelSet } from './config-model.js';
|
|
21
|
+
export { handleAddTrustRule, handleTrustRulesList, handleRemoveTrustRule, handleUpdateTrustRule, handleAcceptStarterBundle } from './config-trust.js';
|
|
22
|
+
export { handleSchedulesList, handleScheduleToggle, handleScheduleRemove, handleScheduleRunNow, handleRemindersList, handleReminderCancel } from './config-scheduling.js';
|
|
23
|
+
export { handleShareToSlack, handleSlackWebhookConfig } from './config-slack.js';
|
|
24
|
+
export { handleIngressConfig, computeGatewayTarget, triggerGatewayReconcile, syncTwilioWebhooks } from './config-ingress.js';
|
|
25
|
+
export { handleVercelApiConfig, handleTwitterIntegrationConfig } from './config-integrations.js';
|
|
26
|
+
export { handleTelegramConfig, summarizeTelegramError } from './config-telegram.js';
|
|
27
|
+
export { handleTwilioConfig } from './config-twilio.js';
|
|
28
|
+
export { handleGuardianVerification, handleChannelReadiness, getReadinessService } from './config-channels.js';
|
|
29
|
+
export { handleEnvVarsRequest, handleToolPermissionSimulate, handleToolNamesList } from './config-tools.js';
|
|
30
|
+
export { handleParentalControlGet, handleParentalControlVerifyPin, handleParentalControlSetPin, handleParentalControlUpdate } from './config-parental.js';
|
|
31
|
+
|
|
32
|
+
// Assemble the combined dispatch map from domain-specific handler groups
|
|
33
|
+
import { modelHandlers } from './config-model.js';
|
|
34
|
+
import { trustHandlers } from './config-trust.js';
|
|
35
|
+
import { schedulingHandlers } from './config-scheduling.js';
|
|
36
|
+
import { slackHandlers } from './config-slack.js';
|
|
37
|
+
import { ingressHandlers } from './config-ingress.js';
|
|
38
|
+
import { integrationHandlers } from './config-integrations.js';
|
|
39
|
+
import { telegramHandlers } from './config-telegram.js';
|
|
40
|
+
import { twilioHandlers } from './config-twilio.js';
|
|
41
|
+
import { channelHandlers } from './config-channels.js';
|
|
42
|
+
import { toolHandlers } from './config-tools.js';
|
|
43
|
+
import { parentalControlHandlers } from './config-parental.js';
|
|
44
|
+
|
|
45
|
+
export const configHandlers = {
|
|
46
|
+
...modelHandlers,
|
|
47
|
+
...trustHandlers,
|
|
48
|
+
...schedulingHandlers,
|
|
49
|
+
...slackHandlers,
|
|
50
|
+
...ingressHandlers,
|
|
51
|
+
...integrationHandlers,
|
|
52
|
+
...telegramHandlers,
|
|
53
|
+
...twilioHandlers,
|
|
54
|
+
...channelHandlers,
|
|
55
|
+
...toolHandlers,
|
|
56
|
+
...parentalControlHandlers,
|
|
57
|
+
};
|