@vellumai/assistant 0.3.5 → 0.3.7
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/README.md +51 -0
- 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 +18 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +338 -1
- package/src/__tests__/approval-conversation-turn.test.ts +214 -0
- package/src/__tests__/browser-manager.test.ts +1 -0
- package/src/__tests__/call-conversation-messages.test.ts +130 -0
- package/src/__tests__/call-orchestrator.test.ts +752 -271
- 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 +5 -0
- package/src/__tests__/call-store.test.ts +3 -0
- package/src/__tests__/channel-approval-routes.test.ts +1260 -85
- package/src/__tests__/channel-approval.test.ts +37 -0
- package/src/__tests__/channel-approvals.test.ts +4 -65
- package/src/__tests__/channel-guardian.test.ts +556 -0
- package/src/__tests__/channel-readiness-service.test.ts +74 -7
- 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 +12 -7
- 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 +6 -2
- package/src/__tests__/db-migration-rollback.test.ts +752 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -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 +126 -0
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +228 -1
- package/src/__tests__/memory-upsert-concurrency.test.ts +828 -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 +2 -0
- package/src/__tests__/run-orchestrator.test.ts +20 -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 +237 -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 +2 -1
- 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 +141 -21
- 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 +45 -29
- 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 +106 -5
- package/src/calls/call-orchestrator.ts +252 -54
- 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 +7 -5
- package/src/calls/twilio-provider.ts +6 -4
- package/src/calls/twilio-rest.ts +40 -15
- package/src/calls/twilio-routes.ts +60 -45
- 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/macos-automation/icon.svg +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +72 -95
- package/src/config/bundled-skills/media-processing/TOOLS.json +57 -147
- 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 +7 -9
- package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
- package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +88 -253
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +22 -153
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +28 -51
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +35 -270
- package/src/config/bundled-skills/messaging/SKILL.md +12 -2
- 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/phone-calls/SKILL.md +86 -21
- 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 +27 -3
- package/src/config/env-registry.ts +169 -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 +157 -1138
- 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 +107 -56
- 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 +0 -7
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
- package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +112 -14
- 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 +254 -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 -2463
- package/src/daemon/handlers/diagnostics.ts +1 -1
- package/src/daemon/handlers/dictation.ts +4 -6
- package/src/daemon/handlers/documents.ts +18 -32
- package/src/daemon/handlers/index.ts +9 -0
- package/src/daemon/handlers/misc.ts +3 -5
- package/src/daemon/handlers/pairing.ts +98 -0
- package/src/daemon/handlers/sessions.ts +74 -5
- package/src/daemon/handlers/shared.ts +3 -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 +2 -2
- 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 +321 -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 +62 -0
- package/src/daemon/ipc-contract-inventory.ts +55 -29
- package/src/daemon/ipc-contract.ts +227 -2527
- package/src/daemon/ipc-protocol.ts +1 -1
- package/src/daemon/ipc-validate.ts +7 -0
- package/src/daemon/lifecycle.ts +97 -379
- package/src/daemon/pairing-store.ts +177 -0
- package/src/daemon/providers-setup.ts +43 -0
- package/src/daemon/ride-shotgun-handler.ts +67 -2
- package/src/daemon/server.ts +60 -44
- package/src/daemon/session-agent-loop-handlers.ts +421 -0
- package/src/daemon/session-agent-loop.ts +113 -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 +98 -4
- package/src/daemon/session-runtime-assembly.ts +149 -15
- package/src/daemon/session-surfaces.ts +26 -4
- package/src/daemon/session-tool-setup.ts +28 -30
- package/src/daemon/session-workspace.ts +1 -1
- package/src/daemon/session.ts +24 -1
- 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 +3 -1
- 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 +200 -1
- 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 +121 -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 +11 -42
- package/src/memory/job-handlers/summarization.ts +32 -26
- package/src/memory/job-utils.ts +3 -10
- package/src/memory/jobs-store.ts +6 -9
- package/src/memory/jobs-worker.ts +51 -36
- 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 +12 -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 +163 -47
- package/src/memory/schema-migration.ts +25 -984
- package/src/memory/schema.ts +130 -7
- package/src/memory/search/entity.ts +10 -19
- package/src/memory/search/lexical.ts +81 -52
- package/src/memory/search/ranking.ts +21 -22
- package/src/memory/search/semantic.ts +157 -19
- 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/providers/sms/adapter.ts +3 -6
- 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 +126 -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 +115 -5
- package/src/runtime/assistant-event-hub.ts +3 -1
- package/src/runtime/channel-approval-parser.ts +36 -2
- package/src/runtime/channel-approvals.ts +0 -21
- package/src/runtime/channel-guardian-service.ts +48 -7
- package/src/runtime/channel-readiness-service.ts +160 -34
- package/src/runtime/channel-readiness-types.ts +10 -4
- package/src/runtime/channel-retry-sweep.ts +184 -0
- package/src/runtime/guardian-context-resolver.ts +108 -0
- package/src/runtime/http-server.ts +289 -745
- package/src/runtime/http-types.ts +56 -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 +49 -6
- 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 -1634
- 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 +144 -0
- package/src/runtime/routes/run-routes.ts +15 -1
- package/src/runtime/run-orchestrator.ts +52 -34
- 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/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 +13 -1
- package/src/tools/browser/browser-manager.ts +119 -4
- package/src/tools/browser/network-recorder.ts +5 -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/executor.ts +80 -18
- 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 +7 -3
- 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/watcher/delete.ts +6 -0
- package/src/tools/weather/service.ts +1 -1
- package/src/twitter/client.ts +190 -24
- 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 +25 -18
- package/src/util/object.ts +3 -0
- package/src/util/platform.ts +72 -365
- 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/bundled-skills/media-processing/services/capability-registry.ts +0 -137
- package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +0 -280
- package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +0 -144
- package/src/config/bundled-skills/media-processing/services/feedback-store.ts +0 -136
- package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +0 -95
- package/src/config/bundled-skills/media-processing/services/timeline-service.ts +0 -267
- package/src/config/bundled-skills/media-processing/tools/detect-events.ts +0 -110
- package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +0 -235
- package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +0 -142
- package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +0 -150
- package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Optional HTTP server that exposes the canonical runtime API.
|
|
3
3
|
*
|
|
4
|
-
* Runs in the same process as the daemon.
|
|
5
|
-
*
|
|
4
|
+
* Runs in the same process as the daemon. Always started on the
|
|
5
|
+
* configured port (default: 7821).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readFileSync
|
|
9
|
-
import { resolve
|
|
10
|
-
import {
|
|
11
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
12
|
-
import { ConfigError, IngressBlockedError } from '../util/errors.js';
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
import { parseChannelId } from '../channels/types.js';
|
|
13
11
|
import { getLogger } from '../util/logger.js';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
import {
|
|
13
|
+
getGatewayInternalBaseUrl,
|
|
14
|
+
isHttpAuthDisabled,
|
|
15
|
+
getRuntimeGatewayOriginSecret,
|
|
16
|
+
} from '../config/env.js';
|
|
18
17
|
import type { RunOrchestrator } from './run-orchestrator.js';
|
|
19
18
|
|
|
20
19
|
// Route handlers — grouped by domain
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
handleListMessages,
|
|
23
22
|
handleSendMessage,
|
|
24
23
|
handleGetSuggestion,
|
|
24
|
+
handleSearchConversations,
|
|
25
25
|
} from './routes/conversation-routes.js';
|
|
26
26
|
import {
|
|
27
27
|
handleUploadAttachment,
|
|
@@ -44,12 +44,12 @@ import {
|
|
|
44
44
|
startGuardianExpirySweep,
|
|
45
45
|
stopGuardianExpirySweep,
|
|
46
46
|
} from './routes/channel-routes.js';
|
|
47
|
-
import
|
|
47
|
+
import {
|
|
48
|
+
startGuardianActionSweep,
|
|
49
|
+
stopGuardianActionSweep,
|
|
50
|
+
} from '../calls/guardian-action-sweep.js';
|
|
48
51
|
import * as conversationStore from '../memory/conversation-store.js';
|
|
49
52
|
import * as externalConversationStore from '../memory/external-conversation-store.js';
|
|
50
|
-
import * as attachmentsStore from '../memory/attachments-store.js';
|
|
51
|
-
import { renderHistoryContent } from '../daemon/handlers.js';
|
|
52
|
-
import { deliverChannelReply } from './gateway-client.js';
|
|
53
53
|
import {
|
|
54
54
|
handleServePage,
|
|
55
55
|
handleShareApp,
|
|
@@ -72,9 +72,45 @@ import {
|
|
|
72
72
|
} from '../calls/twilio-routes.js';
|
|
73
73
|
import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
|
|
74
74
|
import type { RelayWebSocketData } from '../calls/relay-server.js';
|
|
75
|
+
import { extensionRelayServer } from '../browser-extension-relay/server.js';
|
|
76
|
+
import type { BrowserRelayWebSocketData } from '../browser-extension-relay/server.js';
|
|
75
77
|
import { handleSubscribeAssistantEvents } from './routes/events-routes.js';
|
|
76
78
|
import { consumeCallback, consumeCallbackError } from '../security/oauth-callback-registry.js';
|
|
77
|
-
import
|
|
79
|
+
import { PairingStore } from '../daemon/pairing-store.js';
|
|
80
|
+
import type { ServerMessage } from '../daemon/ipc-contract.js';
|
|
81
|
+
import { assistantEventHub } from './assistant-event-hub.js';
|
|
82
|
+
import { buildAssistantEvent } from './assistant-event.js';
|
|
83
|
+
|
|
84
|
+
// Middleware
|
|
85
|
+
import {
|
|
86
|
+
verifyBearerToken,
|
|
87
|
+
isLoopbackHost,
|
|
88
|
+
isPrivateNetworkPeer,
|
|
89
|
+
isPrivateNetworkOrigin,
|
|
90
|
+
extractBearerToken,
|
|
91
|
+
} from './middleware/auth.js';
|
|
92
|
+
import { withErrorHandling } from './middleware/error-handler.js';
|
|
93
|
+
import {
|
|
94
|
+
TWILIO_WEBHOOK_RE,
|
|
95
|
+
TWILIO_GATEWAY_WEBHOOK_RE,
|
|
96
|
+
GATEWAY_SUBPATH_MAP,
|
|
97
|
+
GATEWAY_ONLY_BLOCKED_SUBPATHS,
|
|
98
|
+
validateTwilioWebhook,
|
|
99
|
+
cloneRequestWithBody,
|
|
100
|
+
} from './middleware/twilio-validation.js';
|
|
101
|
+
|
|
102
|
+
// Extracted route handlers
|
|
103
|
+
import {
|
|
104
|
+
handlePairingRegister,
|
|
105
|
+
handlePairingRequest,
|
|
106
|
+
handlePairingStatus,
|
|
107
|
+
} from './routes/pairing-routes.js';
|
|
108
|
+
import type { PairingHandlerContext } from './routes/pairing-routes.js';
|
|
109
|
+
import { handleHealth, handleGetIdentity } from './routes/identity-routes.js';
|
|
110
|
+
import { sweepFailedEvents } from './channel-retry-sweep.js';
|
|
111
|
+
|
|
112
|
+
// Re-export for consumers
|
|
113
|
+
export { isPrivateAddress } from './middleware/auth.js';
|
|
78
114
|
|
|
79
115
|
// Re-export shared types so existing consumers don't need to update imports
|
|
80
116
|
export type {
|
|
@@ -83,12 +119,16 @@ export type {
|
|
|
83
119
|
NonBlockingMessageProcessor,
|
|
84
120
|
RuntimeHttpServerOptions,
|
|
85
121
|
RuntimeAttachmentMetadata,
|
|
122
|
+
ApprovalCopyGenerator,
|
|
123
|
+
ApprovalConversationGenerator,
|
|
86
124
|
} from './http-types.js';
|
|
87
125
|
|
|
88
126
|
import type {
|
|
89
127
|
MessageProcessor,
|
|
90
128
|
NonBlockingMessageProcessor,
|
|
91
129
|
RuntimeHttpServerOptions,
|
|
130
|
+
ApprovalCopyGenerator,
|
|
131
|
+
ApprovalConversationGenerator,
|
|
92
132
|
} from './http-types.js';
|
|
93
133
|
|
|
94
134
|
const log = getLogger('runtime-http');
|
|
@@ -96,282 +136,9 @@ const log = getLogger('runtime-http');
|
|
|
96
136
|
const DEFAULT_PORT = 7821;
|
|
97
137
|
const DEFAULT_HOSTNAME = '127.0.0.1';
|
|
98
138
|
|
|
99
|
-
/**
|
|
100
|
-
function getGatewayBaseUrl(): string {
|
|
101
|
-
if (process.env.GATEWAY_INTERNAL_BASE_URL) {
|
|
102
|
-
return process.env.GATEWAY_INTERNAL_BASE_URL.replace(/\/+$/, '');
|
|
103
|
-
}
|
|
104
|
-
const port = Number(process.env.GATEWAY_PORT) || 7830;
|
|
105
|
-
return `http://127.0.0.1:${port}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Global hard cap on request body size (50 MB). Bun rejects larger payloads before they reach handlers. */
|
|
139
|
+
/** Global hard cap on request body size (50 MB). */
|
|
109
140
|
const MAX_REQUEST_BODY_BYTES = 50 * 1024 * 1024;
|
|
110
141
|
|
|
111
|
-
function parseGuardianRuntimeContext(value: unknown): GuardianRuntimeContext | undefined {
|
|
112
|
-
if (!value || typeof value !== 'object') return undefined;
|
|
113
|
-
const raw = value as Record<string, unknown>;
|
|
114
|
-
const actorRole = raw.actorRole;
|
|
115
|
-
if (
|
|
116
|
-
actorRole !== 'guardian'
|
|
117
|
-
&& actorRole !== 'non-guardian'
|
|
118
|
-
&& actorRole !== 'unverified_channel'
|
|
119
|
-
) {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
const sourceChannel = typeof raw.sourceChannel === 'string' && raw.sourceChannel.trim().length > 0
|
|
123
|
-
? raw.sourceChannel
|
|
124
|
-
: undefined;
|
|
125
|
-
if (!sourceChannel) return undefined;
|
|
126
|
-
const denialReason =
|
|
127
|
-
raw.denialReason === 'no_binding' || raw.denialReason === 'no_identity'
|
|
128
|
-
? raw.denialReason
|
|
129
|
-
: undefined;
|
|
130
|
-
return {
|
|
131
|
-
sourceChannel,
|
|
132
|
-
actorRole,
|
|
133
|
-
guardianChatId: typeof raw.guardianChatId === 'string' ? raw.guardianChatId : undefined,
|
|
134
|
-
guardianExternalUserId: typeof raw.guardianExternalUserId === 'string' ? raw.guardianExternalUserId : undefined,
|
|
135
|
-
requesterIdentifier: typeof raw.requesterIdentifier === 'string' ? raw.requesterIdentifier : undefined,
|
|
136
|
-
requesterExternalUserId: typeof raw.requesterExternalUserId === 'string' ? raw.requesterExternalUserId : undefined,
|
|
137
|
-
requesterChatId: typeof raw.requesterChatId === 'string' ? raw.requesterChatId : undefined,
|
|
138
|
-
denialReason,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
interface DiskSpaceInfo {
|
|
143
|
-
path: string;
|
|
144
|
-
totalMb: number;
|
|
145
|
-
usedMb: number;
|
|
146
|
-
freeMb: number;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function getDiskSpaceInfo(): DiskSpaceInfo | null {
|
|
150
|
-
try {
|
|
151
|
-
const baseDataDir = process.env.BASE_DATA_DIR?.trim();
|
|
152
|
-
const diskPath = baseDataDir && existsSync(baseDataDir) ? baseDataDir : '/';
|
|
153
|
-
const stats = statfsSync(diskPath);
|
|
154
|
-
const totalBytes = stats.bsize * stats.blocks;
|
|
155
|
-
const freeBytes = stats.bsize * stats.bavail;
|
|
156
|
-
const bytesToMb = (b: number) => Math.round((b / (1024 * 1024)) * 100) / 100;
|
|
157
|
-
return {
|
|
158
|
-
path: diskPath,
|
|
159
|
-
totalMb: bytesToMb(totalBytes),
|
|
160
|
-
usedMb: bytesToMb(totalBytes - freeBytes),
|
|
161
|
-
freeMb: bytesToMb(freeBytes),
|
|
162
|
-
};
|
|
163
|
-
} catch {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Regex to extract the Twilio webhook subpath from both top-level and
|
|
170
|
-
* assistant-scoped route shapes:
|
|
171
|
-
* /v1/calls/twilio/<subpath>
|
|
172
|
-
* /v1/assistants/<id>/calls/twilio/<subpath>
|
|
173
|
-
*/
|
|
174
|
-
const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Gateway-compatible Twilio webhook paths:
|
|
178
|
-
* /webhooks/twilio/<subpath>
|
|
179
|
-
*
|
|
180
|
-
* Maps gateway path segments to the internal subpath names used by the
|
|
181
|
-
* dispatcher below (e.g. "voice" -> "voice-webhook").
|
|
182
|
-
*/
|
|
183
|
-
const TWILIO_GATEWAY_WEBHOOK_RE = /^\/webhooks\/twilio\/(.+)$/;
|
|
184
|
-
const GATEWAY_SUBPATH_MAP: Record<string, string> = {
|
|
185
|
-
voice: 'voice-webhook',
|
|
186
|
-
status: 'status',
|
|
187
|
-
'connect-action': 'connect-action',
|
|
188
|
-
sms: 'sms',
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Direct Twilio webhook subpaths that are blocked in gateway_only mode.
|
|
193
|
-
* Includes all public-facing webhook paths (voice, status, connect-action, SMS)
|
|
194
|
-
* because the runtime must never serve as a direct ingress for external webhooks.
|
|
195
|
-
* Internal forwarding endpoints (gateway→runtime) are unaffected.
|
|
196
|
-
*/
|
|
197
|
-
const GATEWAY_ONLY_BLOCKED_SUBPATHS = new Set(['voice-webhook', 'status', 'connect-action', 'sms']);
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Check if a request origin is from a private/internal network address.
|
|
201
|
-
* Extracts the hostname from the Origin header and validates it against
|
|
202
|
-
* isPrivateAddress(), consistent with the isPrivateNetworkPeer check.
|
|
203
|
-
*/
|
|
204
|
-
function isPrivateNetworkOrigin(req: Request): boolean {
|
|
205
|
-
const origin = req.headers.get('origin');
|
|
206
|
-
// No origin header (e.g., server-initiated or same-origin) — allow
|
|
207
|
-
if (!origin) return true;
|
|
208
|
-
try {
|
|
209
|
-
const url = new URL(origin);
|
|
210
|
-
const host = url.hostname;
|
|
211
|
-
if (host === 'localhost') return true;
|
|
212
|
-
// URL.hostname wraps IPv6 addresses in brackets (e.g. "[::1]") — strip them
|
|
213
|
-
const rawHost = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
214
|
-
return isPrivateAddress(rawHost);
|
|
215
|
-
} catch {
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Check if a hostname is a loopback address.
|
|
222
|
-
*/
|
|
223
|
-
function isLoopbackHost(hostname: string): boolean {
|
|
224
|
-
return hostname === '127.0.0.1' || hostname === '::1' || hostname === 'localhost';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Check if the actual peer/remote address of a connection is from a
|
|
229
|
-
* private/internal network. Uses Bun's server.requestIP() to get the
|
|
230
|
-
* real peer address, which cannot be spoofed unlike the Origin header.
|
|
231
|
-
*
|
|
232
|
-
* Accepts loopback, RFC 1918 private IPv4, link-local, and RFC 4193
|
|
233
|
-
* unique-local IPv6 — including their IPv4-mapped IPv6 forms. This
|
|
234
|
-
* supports container/pod deployments (e.g. Kubernetes sidecars) where
|
|
235
|
-
* gateway and runtime communicate over pod-internal private IPs.
|
|
236
|
-
*/
|
|
237
|
-
function isPrivateNetworkPeer(server: { requestIP(req: Request): { address: string; family: string; port: number } | null }, req: Request): boolean {
|
|
238
|
-
const ip = server.requestIP(req);
|
|
239
|
-
if (!ip) return false;
|
|
240
|
-
return isPrivateAddress(ip.address);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* @internal Exported for testing.
|
|
245
|
-
*
|
|
246
|
-
* Determine whether an IP address string belongs to a private/internal
|
|
247
|
-
* network range:
|
|
248
|
-
* - Loopback: 127.0.0.0/8, ::1
|
|
249
|
-
* - RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
|
250
|
-
* - Link-local: 169.254.0.0/16
|
|
251
|
-
* - IPv6 unique local: fc00::/7 (fc00::–fdff::)
|
|
252
|
-
* - IPv4-mapped IPv6 variants of all of the above (::ffff:x.x.x.x)
|
|
253
|
-
*/
|
|
254
|
-
export function isPrivateAddress(addr: string): boolean {
|
|
255
|
-
// Handle IPv4-mapped IPv6 (e.g. ::ffff:10.0.0.1) — extract the IPv4 part
|
|
256
|
-
const v4Mapped = addr.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
|
|
257
|
-
const normalized = v4Mapped ? v4Mapped[1] : addr;
|
|
258
|
-
|
|
259
|
-
// IPv4 checks
|
|
260
|
-
if (normalized.includes('.')) {
|
|
261
|
-
const parts = normalized.split('.').map(Number);
|
|
262
|
-
if (parts.length !== 4 || parts.some(p => isNaN(p) || p < 0 || p > 255)) return false;
|
|
263
|
-
|
|
264
|
-
// Loopback: 127.0.0.0/8
|
|
265
|
-
if (parts[0] === 127) return true;
|
|
266
|
-
// 10.0.0.0/8
|
|
267
|
-
if (parts[0] === 10) return true;
|
|
268
|
-
// 172.16.0.0/12 (172.16.x.x – 172.31.x.x)
|
|
269
|
-
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
|
|
270
|
-
// 192.168.0.0/16
|
|
271
|
-
if (parts[0] === 192 && parts[1] === 168) return true;
|
|
272
|
-
// Link-local: 169.254.0.0/16
|
|
273
|
-
if (parts[0] === 169 && parts[1] === 254) return true;
|
|
274
|
-
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// IPv6 checks
|
|
279
|
-
const lower = normalized.toLowerCase();
|
|
280
|
-
// Loopback
|
|
281
|
-
if (lower === '::1') return true;
|
|
282
|
-
// Unique local: fc00::/7 (fc00:: through fdff::)
|
|
283
|
-
if (lower.startsWith('fc') || lower.startsWith('fd')) return true;
|
|
284
|
-
// Link-local: fe80::/10
|
|
285
|
-
if (lower.startsWith('fe80')) return true;
|
|
286
|
-
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Validate a Twilio webhook request's X-Twilio-Signature header.
|
|
292
|
-
*
|
|
293
|
-
* Returns the raw body text on success so callers can reconstruct the Request
|
|
294
|
-
* for downstream handlers (which also need to read the body).
|
|
295
|
-
* Returns a 403 Response if signature validation fails.
|
|
296
|
-
*
|
|
297
|
-
* Fail-closed: if the auth token is not configured, the request is rejected
|
|
298
|
-
* with 403 rather than silently skipping validation. An explicit local-dev
|
|
299
|
-
* bypass is available via TWILIO_WEBHOOK_VALIDATION_DISABLED=true.
|
|
300
|
-
*/
|
|
301
|
-
async function validateTwilioWebhook(
|
|
302
|
-
req: Request,
|
|
303
|
-
): Promise<{ body: string } | Response> {
|
|
304
|
-
const rawBody = await req.text();
|
|
305
|
-
|
|
306
|
-
// Allow explicit local-dev bypass — must be exactly "true"
|
|
307
|
-
if (process.env.TWILIO_WEBHOOK_VALIDATION_DISABLED === 'true') {
|
|
308
|
-
log.warn('Twilio webhook signature validation explicitly disabled via TWILIO_WEBHOOK_VALIDATION_DISABLED');
|
|
309
|
-
return { body: rawBody };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const authToken = TwilioConversationRelayProvider.getAuthToken();
|
|
313
|
-
|
|
314
|
-
// Fail-closed: reject if no auth token is configured
|
|
315
|
-
if (!authToken) {
|
|
316
|
-
log.error('Twilio auth token not configured — rejecting webhook request (fail-closed)');
|
|
317
|
-
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const signature = req.headers.get('x-twilio-signature');
|
|
321
|
-
if (!signature) {
|
|
322
|
-
log.warn('Twilio webhook request missing X-Twilio-Signature header');
|
|
323
|
-
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Parse form-urlencoded body into key-value params for signature computation
|
|
327
|
-
const params: Record<string, string> = {};
|
|
328
|
-
const formData = new URLSearchParams(rawBody);
|
|
329
|
-
for (const [key, value] of formData.entries()) {
|
|
330
|
-
params[key] = value;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Reconstruct the public-facing URL that Twilio signed against.
|
|
334
|
-
// Behind proxies/gateways, req.url is the local server URL (e.g.
|
|
335
|
-
// http://127.0.0.1:7821/...) which differs from the public URL Twilio
|
|
336
|
-
// used to compute the HMAC-SHA1 signature.
|
|
337
|
-
let publicBaseUrl: string | undefined;
|
|
338
|
-
try {
|
|
339
|
-
publicBaseUrl = getPublicBaseUrl(loadConfig());
|
|
340
|
-
} catch {
|
|
341
|
-
// No webhook base URL configured — fall back to using req.url as-is
|
|
342
|
-
}
|
|
343
|
-
const parsedUrl = new URL(req.url);
|
|
344
|
-
const publicUrl = publicBaseUrl
|
|
345
|
-
? publicBaseUrl + parsedUrl.pathname + parsedUrl.search
|
|
346
|
-
: req.url;
|
|
347
|
-
|
|
348
|
-
const isValid = TwilioConversationRelayProvider.verifyWebhookSignature(
|
|
349
|
-
publicUrl,
|
|
350
|
-
params,
|
|
351
|
-
signature,
|
|
352
|
-
authToken,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
if (!isValid) {
|
|
356
|
-
log.warn('Twilio webhook signature validation failed');
|
|
357
|
-
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return { body: rawBody };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Re-create a Request with the same method, headers, and URL but with a
|
|
365
|
-
* pre-read body string so downstream handlers can call req.text() again.
|
|
366
|
-
*/
|
|
367
|
-
function cloneRequestWithBody(original: Request, body: string): Request {
|
|
368
|
-
return new Request(original.url, {
|
|
369
|
-
method: original.method,
|
|
370
|
-
headers: original.headers,
|
|
371
|
-
body,
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
142
|
export class RuntimeHttpServer {
|
|
376
143
|
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
377
144
|
private port: number;
|
|
@@ -380,11 +147,15 @@ export class RuntimeHttpServer {
|
|
|
380
147
|
private processMessage?: MessageProcessor;
|
|
381
148
|
private persistAndProcessMessage?: NonBlockingMessageProcessor;
|
|
382
149
|
private runOrchestrator?: RunOrchestrator;
|
|
150
|
+
private approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
151
|
+
private approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
383
152
|
private interfacesDir: string | null;
|
|
384
153
|
private suggestionCache = new Map<string, string>();
|
|
385
154
|
private suggestionInFlight = new Map<string, Promise<string | null>>();
|
|
386
155
|
private retrySweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
387
156
|
private sweepInProgress = false;
|
|
157
|
+
private pairingStore = new PairingStore();
|
|
158
|
+
private pairingBroadcast?: (msg: ServerMessage) => void;
|
|
388
159
|
|
|
389
160
|
constructor(options: RuntimeHttpServerOptions = {}) {
|
|
390
161
|
this.port = options.port ?? DEFAULT_PORT;
|
|
@@ -393,6 +164,8 @@ export class RuntimeHttpServer {
|
|
|
393
164
|
this.processMessage = options.processMessage;
|
|
394
165
|
this.persistAndProcessMessage = options.persistAndProcessMessage;
|
|
395
166
|
this.runOrchestrator = options.runOrchestrator;
|
|
167
|
+
this.approvalCopyGenerator = options.approvalCopyGenerator;
|
|
168
|
+
this.approvalConversationGenerator = options.approvalConversationGenerator;
|
|
396
169
|
this.interfacesDir = options.interfacesDir ?? null;
|
|
397
170
|
}
|
|
398
171
|
|
|
@@ -401,30 +174,78 @@ export class RuntimeHttpServer {
|
|
|
401
174
|
return this.server?.port ?? this.port;
|
|
402
175
|
}
|
|
403
176
|
|
|
177
|
+
/** Expose the pairing store so the daemon server can wire IPC handlers. */
|
|
178
|
+
getPairingStore(): PairingStore {
|
|
179
|
+
return this.pairingStore;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Set a callback for broadcasting IPC messages (wired by daemon server). */
|
|
183
|
+
setPairingBroadcast(fn: (msg: ServerMessage) => void): void {
|
|
184
|
+
this.pairingBroadcast = fn;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private get pairingContext(): PairingHandlerContext {
|
|
188
|
+
const ipcBroadcast = this.pairingBroadcast;
|
|
189
|
+
return {
|
|
190
|
+
pairingStore: this.pairingStore,
|
|
191
|
+
bearerToken: this.bearerToken,
|
|
192
|
+
pairingBroadcast: ipcBroadcast
|
|
193
|
+
? (msg) => {
|
|
194
|
+
// Broadcast to IPC socket clients (local Unix socket)
|
|
195
|
+
ipcBroadcast(msg);
|
|
196
|
+
// Also publish to the event hub so HTTP/SSE clients (e.g. macOS
|
|
197
|
+
// app with localHttpEnabled) receive pairing approval requests.
|
|
198
|
+
void assistantEventHub.publish(buildAssistantEvent('self', msg));
|
|
199
|
+
}
|
|
200
|
+
: undefined,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
404
204
|
async start(): Promise<void> {
|
|
405
|
-
|
|
205
|
+
type AllWebSocketData = RelayWebSocketData | BrowserRelayWebSocketData;
|
|
206
|
+
this.server = Bun.serve<AllWebSocketData>({
|
|
406
207
|
port: this.port,
|
|
407
208
|
hostname: this.hostname,
|
|
408
209
|
maxRequestBodySize: MAX_REQUEST_BODY_BYTES,
|
|
409
210
|
fetch: (req, server) => this.handleRequest(req, server),
|
|
410
211
|
websocket: {
|
|
411
212
|
open(ws) {
|
|
412
|
-
const
|
|
213
|
+
const data = ws.data as AllWebSocketData;
|
|
214
|
+
if ('wsType' in data && data.wsType === 'browser-relay') {
|
|
215
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
216
|
+
extensionRelayServer.handleOpen(ws as any);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const callSessionId = (data as RelayWebSocketData).callSessionId;
|
|
413
220
|
log.info({ callSessionId }, 'ConversationRelay WebSocket opened');
|
|
414
221
|
if (callSessionId) {
|
|
415
|
-
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
223
|
+
const connection = new RelayConnection(ws as any, callSessionId);
|
|
416
224
|
activeRelayConnections.set(callSessionId, connection);
|
|
417
225
|
}
|
|
418
226
|
},
|
|
419
227
|
message(ws, message) {
|
|
420
|
-
const
|
|
228
|
+
const data = ws.data as AllWebSocketData;
|
|
229
|
+
const raw = typeof message === 'string' ? message : new TextDecoder().decode(message);
|
|
230
|
+
if ('wsType' in data && data.wsType === 'browser-relay') {
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
+
extensionRelayServer.handleMessage(ws as any, raw);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const callSessionId = (data as RelayWebSocketData).callSessionId;
|
|
421
236
|
if (callSessionId) {
|
|
422
237
|
const connection = activeRelayConnections.get(callSessionId);
|
|
423
|
-
connection?.handleMessage(
|
|
238
|
+
connection?.handleMessage(raw);
|
|
424
239
|
}
|
|
425
240
|
},
|
|
426
241
|
close(ws, code, reason) {
|
|
427
|
-
const
|
|
242
|
+
const data = ws.data as AllWebSocketData;
|
|
243
|
+
if ('wsType' in data && data.wsType === 'browser-relay') {
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
+
extensionRelayServer.handleClose(ws as any, code, reason?.toString());
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const callSessionId = (data as RelayWebSocketData).callSessionId;
|
|
428
249
|
log.info({ callSessionId, code, reason: reason?.toString() }, 'ConversationRelay WebSocket closed');
|
|
429
250
|
if (callSessionId) {
|
|
430
251
|
const connection = activeRelayConnections.get(callSessionId);
|
|
@@ -436,34 +257,38 @@ export class RuntimeHttpServer {
|
|
|
436
257
|
},
|
|
437
258
|
});
|
|
438
259
|
|
|
439
|
-
// Sweep failed channel inbound events for retry every 30 seconds
|
|
440
260
|
if (this.processMessage) {
|
|
261
|
+
const pm = this.processMessage;
|
|
262
|
+
const bt = this.bearerToken;
|
|
441
263
|
this.retrySweepTimer = setInterval(() => {
|
|
442
264
|
if (this.sweepInProgress) return;
|
|
443
265
|
this.sweepInProgress = true;
|
|
444
|
-
|
|
266
|
+
sweepFailedEvents(pm, bt).finally(() => { this.sweepInProgress = false; });
|
|
445
267
|
}, 30_000);
|
|
446
268
|
}
|
|
447
269
|
|
|
448
|
-
// Start proactive guardian approval expiry sweep whenever orchestrator
|
|
449
|
-
// support is available. Guardian approvals can be created even when the
|
|
450
|
-
// generic channel-approval UX flag is disabled.
|
|
451
270
|
if (this.runOrchestrator) {
|
|
452
|
-
startGuardianExpirySweep(this.runOrchestrator,
|
|
271
|
+
startGuardianExpirySweep(this.runOrchestrator, getGatewayInternalBaseUrl(), this.bearerToken, this.approvalCopyGenerator);
|
|
453
272
|
log.info('Guardian approval expiry sweep started');
|
|
454
273
|
}
|
|
455
274
|
|
|
456
|
-
|
|
275
|
+
startGuardianActionSweep(getGatewayInternalBaseUrl(), this.bearerToken);
|
|
276
|
+
log.info('Guardian action expiry sweep started');
|
|
277
|
+
|
|
457
278
|
log.info('Running in gateway-only ingress mode. Direct webhook routes disabled.');
|
|
458
279
|
if (!isLoopbackHost(this.hostname)) {
|
|
459
280
|
log.warn('RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.');
|
|
460
281
|
}
|
|
461
282
|
|
|
283
|
+
this.pairingStore.start();
|
|
284
|
+
|
|
462
285
|
log.info({ port: this.actualPort, hostname: this.hostname, auth: !!this.bearerToken }, 'Runtime HTTP server listening');
|
|
463
286
|
}
|
|
464
287
|
|
|
465
288
|
async stop(): Promise<void> {
|
|
289
|
+
this.pairingStore.stop();
|
|
466
290
|
stopGuardianExpirySweep();
|
|
291
|
+
stopGuardianActionSweep();
|
|
467
292
|
if (this.retrySweepTimer) {
|
|
468
293
|
clearInterval(this.retrySweepTimer);
|
|
469
294
|
this.retrySweepTimer = null;
|
|
@@ -475,104 +300,51 @@ export class RuntimeHttpServer {
|
|
|
475
300
|
}
|
|
476
301
|
}
|
|
477
302
|
|
|
478
|
-
/**
|
|
479
|
-
* Constant-time comparison of two bearer tokens to prevent timing attacks.
|
|
480
|
-
*/
|
|
481
|
-
private verifyToken(provided: string): boolean {
|
|
482
|
-
const expected = this.bearerToken!;
|
|
483
|
-
const a = Buffer.from(provided);
|
|
484
|
-
const b = Buffer.from(expected);
|
|
485
|
-
if (a.length !== b.length) return false;
|
|
486
|
-
return timingSafeEqual(a, b);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
303
|
private async handleRequest(req: Request, server: ReturnType<typeof Bun.serve>): Promise<Response> {
|
|
490
304
|
const url = new URL(req.url);
|
|
491
305
|
const path = url.pathname;
|
|
492
306
|
|
|
493
|
-
// Health checks are unauthenticated — they expose no sensitive data.
|
|
494
307
|
if (path === '/healthz' && req.method === 'GET') {
|
|
495
|
-
return
|
|
308
|
+
return handleHealth();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// WebSocket upgrade for the Chrome extension browser relay.
|
|
312
|
+
if (path === '/v1/browser-relay' && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
313
|
+
return this.handleBrowserRelayUpgrade(req, server);
|
|
496
314
|
}
|
|
497
315
|
|
|
498
316
|
// WebSocket upgrade for ConversationRelay — before auth check because
|
|
499
317
|
// Twilio WebSocket connections don't use bearer tokens.
|
|
500
318
|
if (path.startsWith('/v1/calls/relay') && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
501
|
-
|
|
502
|
-
// Primary check: actual peer address (cannot be spoofed) — accepts loopback
|
|
503
|
-
// and RFC 1918/4193 private addresses to support container deployments.
|
|
504
|
-
// Secondary check: Origin header (defense in depth).
|
|
505
|
-
if (!isPrivateNetworkPeer(server, req) || !isPrivateNetworkOrigin(req)) {
|
|
506
|
-
return Response.json(
|
|
507
|
-
{ error: 'Direct relay access disabled — only private network peers allowed', code: 'GATEWAY_ONLY' },
|
|
508
|
-
{ status: 403 },
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const wsUrl = new URL(req.url);
|
|
513
|
-
const callSessionId = wsUrl.searchParams.get('callSessionId');
|
|
514
|
-
if (!callSessionId) {
|
|
515
|
-
return new Response('Missing callSessionId', { status: 400 });
|
|
516
|
-
}
|
|
517
|
-
const upgraded = server.upgrade(req, { data: { callSessionId } });
|
|
518
|
-
if (!upgraded) {
|
|
519
|
-
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
520
|
-
}
|
|
521
|
-
// Bun handles the response after a successful upgrade.
|
|
522
|
-
// The RelayConnection is created in the websocket.open handler.
|
|
523
|
-
return undefined as unknown as Response;
|
|
319
|
+
return this.handleRelayUpgrade(req, server);
|
|
524
320
|
}
|
|
525
321
|
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
// Validates X-Twilio-Signature to prevent unauthorized access. ──
|
|
531
|
-
const twilioMatch = path.match(TWILIO_WEBHOOK_RE);
|
|
532
|
-
const gatewayTwilioMatch = !twilioMatch ? path.match(TWILIO_GATEWAY_WEBHOOK_RE) : null;
|
|
533
|
-
const resolvedTwilioSubpath = twilioMatch
|
|
534
|
-
? twilioMatch[1]
|
|
535
|
-
: gatewayTwilioMatch
|
|
536
|
-
? GATEWAY_SUBPATH_MAP[gatewayTwilioMatch[1]]
|
|
537
|
-
: null;
|
|
538
|
-
if (resolvedTwilioSubpath && req.method === 'POST') {
|
|
539
|
-
const twilioSubpath = resolvedTwilioSubpath;
|
|
540
|
-
|
|
541
|
-
// Block direct Twilio webhook routes — must go through the gateway
|
|
542
|
-
if (GATEWAY_ONLY_BLOCKED_SUBPATHS.has(twilioSubpath)) {
|
|
543
|
-
return Response.json(
|
|
544
|
-
{ error: 'Direct webhook access disabled. Use the gateway.', code: 'GATEWAY_ONLY' },
|
|
545
|
-
{ status: 410 },
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Validate Twilio request signature before dispatching
|
|
550
|
-
const validation = await validateTwilioWebhook(req);
|
|
551
|
-
if (validation instanceof Response) return validation;
|
|
552
|
-
|
|
553
|
-
// Reconstruct request so handlers can read the body
|
|
554
|
-
const validatedReq = cloneRequestWithBody(req, validation.body);
|
|
322
|
+
// Twilio webhook endpoints — before auth check because Twilio
|
|
323
|
+
// webhook POSTs don't include bearer tokens.
|
|
324
|
+
const twilioResponse = await this.handleTwilioWebhook(req, path);
|
|
325
|
+
if (twilioResponse) return twilioResponse;
|
|
555
326
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (twilioSubpath === 'connect-action') {
|
|
563
|
-
return await handleConnectAction(validatedReq);
|
|
564
|
-
}
|
|
327
|
+
// Pairing endpoints (unauthenticated, secret-gated)
|
|
328
|
+
if (path === '/v1/pairing/request' && req.method === 'POST') {
|
|
329
|
+
return await handlePairingRequest(req, this.pairingContext);
|
|
330
|
+
}
|
|
331
|
+
if (path === '/v1/pairing/status' && req.method === 'GET') {
|
|
332
|
+
return handlePairingStatus(url, this.pairingContext);
|
|
565
333
|
}
|
|
566
334
|
|
|
567
335
|
// Require bearer token when configured
|
|
568
|
-
if ((
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
if (!token || !this.verifyToken(token)) {
|
|
336
|
+
if (!isHttpAuthDisabled() && this.bearerToken) {
|
|
337
|
+
const token = extractBearerToken(req);
|
|
338
|
+
if (!token || !verifyBearerToken(token, this.bearerToken)) {
|
|
572
339
|
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
573
340
|
}
|
|
574
341
|
}
|
|
575
342
|
|
|
343
|
+
// Pairing registration (bearer-authenticated)
|
|
344
|
+
if (path === '/v1/pairing/register' && req.method === 'POST') {
|
|
345
|
+
return await handlePairingRegister(req, this.pairingContext);
|
|
346
|
+
}
|
|
347
|
+
|
|
576
348
|
// Serve shareable app pages
|
|
577
349
|
const pagesMatch = path.match(/^\/pages\/([^/]+)$/);
|
|
578
350
|
if (pagesMatch && req.method === 'GET') {
|
|
@@ -584,11 +356,9 @@ export class RuntimeHttpServer {
|
|
|
584
356
|
}
|
|
585
357
|
}
|
|
586
358
|
|
|
587
|
-
//
|
|
359
|
+
// Cloud sharing endpoints
|
|
588
360
|
if (path === '/v1/apps/share' && req.method === 'POST') {
|
|
589
|
-
try {
|
|
590
|
-
return await handleShareApp(req);
|
|
591
|
-
} catch (err) {
|
|
361
|
+
try { return await handleShareApp(req); } catch (err) {
|
|
592
362
|
log.error({ err }, 'Runtime HTTP handler error sharing app');
|
|
593
363
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
594
364
|
}
|
|
@@ -598,17 +368,13 @@ export class RuntimeHttpServer {
|
|
|
598
368
|
if (sharedTokenMatch) {
|
|
599
369
|
const shareToken = sharedTokenMatch[1];
|
|
600
370
|
if (req.method === 'GET') {
|
|
601
|
-
try {
|
|
602
|
-
return handleDownloadSharedApp(shareToken);
|
|
603
|
-
} catch (err) {
|
|
371
|
+
try { return handleDownloadSharedApp(shareToken); } catch (err) {
|
|
604
372
|
log.error({ err, shareToken }, 'Runtime HTTP handler error downloading shared app');
|
|
605
373
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
606
374
|
}
|
|
607
375
|
}
|
|
608
376
|
if (req.method === 'DELETE') {
|
|
609
|
-
try {
|
|
610
|
-
return handleDeleteSharedApp(shareToken);
|
|
611
|
-
} catch (err) {
|
|
377
|
+
try { return handleDeleteSharedApp(shareToken); } catch (err) {
|
|
612
378
|
log.error({ err, shareToken }, 'Runtime HTTP handler error deleting shared app');
|
|
613
379
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
614
380
|
}
|
|
@@ -617,27 +383,21 @@ export class RuntimeHttpServer {
|
|
|
617
383
|
|
|
618
384
|
const sharedMetadataMatch = path.match(/^\/v1\/apps\/shared\/([^/]+)\/metadata$/);
|
|
619
385
|
if (sharedMetadataMatch && req.method === 'GET') {
|
|
620
|
-
try {
|
|
621
|
-
return handleGetSharedAppMetadata(sharedMetadataMatch[1]);
|
|
622
|
-
} catch (err) {
|
|
386
|
+
try { return handleGetSharedAppMetadata(sharedMetadataMatch[1]); } catch (err) {
|
|
623
387
|
log.error({ err, shareToken: sharedMetadataMatch[1] }, 'Runtime HTTP handler error getting shared app metadata');
|
|
624
388
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
625
389
|
}
|
|
626
390
|
}
|
|
627
391
|
|
|
628
|
-
//
|
|
392
|
+
// Secret management endpoint
|
|
629
393
|
if (path === '/v1/secrets' && req.method === 'POST') {
|
|
630
|
-
try {
|
|
631
|
-
return await handleAddSecret(req);
|
|
632
|
-
} catch (err) {
|
|
394
|
+
try { return await handleAddSecret(req); } catch (err) {
|
|
633
395
|
log.error({ err }, 'Runtime HTTP handler error adding secret');
|
|
634
396
|
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
635
397
|
}
|
|
636
398
|
}
|
|
637
399
|
|
|
638
400
|
// New assistant-less runtime routes: /v1/<endpoint>
|
|
639
|
-
// These supersede the legacy /v1/assistants/:assistantId/... shape.
|
|
640
|
-
// Paths already handled above (/v1/apps/..., /v1/secrets) will never reach here.
|
|
641
401
|
const newRouteMatch = path.match(/^\/v1\/(?!assistants\/)(.+)$/);
|
|
642
402
|
if (newRouteMatch) {
|
|
643
403
|
return this.dispatchEndpoint(newRouteMatch[1], req, url);
|
|
@@ -655,10 +415,85 @@ export class RuntimeHttpServer {
|
|
|
655
415
|
return this.dispatchEndpoint(endpoint, req, url, assistantId);
|
|
656
416
|
}
|
|
657
417
|
|
|
418
|
+
private handleBrowserRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
|
|
419
|
+
if (!isLoopbackHost(new URL(req.url).hostname) && !isPrivateNetworkPeer(server, req)) {
|
|
420
|
+
return Response.json(
|
|
421
|
+
{ error: 'Browser relay only accepts connections from localhost', code: 'LOCALHOST_ONLY' },
|
|
422
|
+
{ status: 403 },
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if ((process.env.DISABLE_HTTP_AUTH ?? '').toLowerCase() !== 'true' && this.bearerToken) {
|
|
427
|
+
const wsUrl = new URL(req.url);
|
|
428
|
+
const token = wsUrl.searchParams.get('token');
|
|
429
|
+
if (!token || !verifyBearerToken(token, this.bearerToken)) {
|
|
430
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const connectionId = crypto.randomUUID();
|
|
435
|
+
const upgraded = server.upgrade(req, {
|
|
436
|
+
data: { wsType: 'browser-relay', connectionId } satisfies BrowserRelayWebSocketData,
|
|
437
|
+
});
|
|
438
|
+
if (!upgraded) {
|
|
439
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
440
|
+
}
|
|
441
|
+
return undefined as unknown as Response;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private handleRelayUpgrade(req: Request, server: ReturnType<typeof Bun.serve>): Response {
|
|
445
|
+
if (!isPrivateNetworkPeer(server, req) || !isPrivateNetworkOrigin(req)) {
|
|
446
|
+
return Response.json(
|
|
447
|
+
{ error: 'Direct relay access disabled — only private network peers allowed', code: 'GATEWAY_ONLY' },
|
|
448
|
+
{ status: 403 },
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const wsUrl = new URL(req.url);
|
|
453
|
+
const callSessionId = wsUrl.searchParams.get('callSessionId');
|
|
454
|
+
if (!callSessionId) {
|
|
455
|
+
return new Response('Missing callSessionId', { status: 400 });
|
|
456
|
+
}
|
|
457
|
+
const upgraded = server.upgrade(req, { data: { callSessionId } });
|
|
458
|
+
if (!upgraded) {
|
|
459
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
460
|
+
}
|
|
461
|
+
return undefined as unknown as Response;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private async handleTwilioWebhook(req: Request, path: string): Promise<Response | null> {
|
|
465
|
+
const twilioMatch = path.match(TWILIO_WEBHOOK_RE);
|
|
466
|
+
const gatewayTwilioMatch = !twilioMatch ? path.match(TWILIO_GATEWAY_WEBHOOK_RE) : null;
|
|
467
|
+
const resolvedTwilioSubpath = twilioMatch
|
|
468
|
+
? twilioMatch[1]
|
|
469
|
+
: gatewayTwilioMatch
|
|
470
|
+
? GATEWAY_SUBPATH_MAP[gatewayTwilioMatch[1]]
|
|
471
|
+
: null;
|
|
472
|
+
if (!resolvedTwilioSubpath || req.method !== 'POST') return null;
|
|
473
|
+
|
|
474
|
+
const twilioSubpath = resolvedTwilioSubpath;
|
|
475
|
+
|
|
476
|
+
if (GATEWAY_ONLY_BLOCKED_SUBPATHS.has(twilioSubpath)) {
|
|
477
|
+
return Response.json(
|
|
478
|
+
{ error: 'Direct webhook access disabled. Use the gateway.', code: 'GATEWAY_ONLY' },
|
|
479
|
+
{ status: 410 },
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const validation = await validateTwilioWebhook(req);
|
|
484
|
+
if (validation instanceof Response) return validation;
|
|
485
|
+
|
|
486
|
+
const validatedReq = cloneRequestWithBody(req, validation.body);
|
|
487
|
+
|
|
488
|
+
if (twilioSubpath === 'voice-webhook') return await handleVoiceWebhook(validatedReq);
|
|
489
|
+
if (twilioSubpath === 'status') return await handleStatusCallback(validatedReq);
|
|
490
|
+
if (twilioSubpath === 'connect-action') return await handleConnectAction(validatedReq);
|
|
491
|
+
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
658
495
|
/**
|
|
659
496
|
* Dispatch a request to the appropriate endpoint handler.
|
|
660
|
-
* Used by both the new assistant-less routes (/v1/<endpoint>) and the
|
|
661
|
-
* legacy assistant-scoped routes (/v1/assistants/:assistantId/<endpoint>).
|
|
662
497
|
*/
|
|
663
498
|
private async dispatchEndpoint(
|
|
664
499
|
endpoint: string,
|
|
@@ -666,9 +501,21 @@ export class RuntimeHttpServer {
|
|
|
666
501
|
url: URL,
|
|
667
502
|
assistantId: string = 'self',
|
|
668
503
|
): Promise<Response> {
|
|
669
|
-
|
|
670
|
-
if (endpoint === 'health' && req.method === 'GET')
|
|
671
|
-
|
|
504
|
+
return withErrorHandling(endpoint, async () => {
|
|
505
|
+
if (endpoint === 'health' && req.method === 'GET') return handleHealth();
|
|
506
|
+
|
|
507
|
+
if (endpoint === 'browser-relay/status' && req.method === 'GET') {
|
|
508
|
+
return Response.json(extensionRelayServer.getStatus());
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (endpoint === 'browser-relay/command' && req.method === 'POST') {
|
|
512
|
+
try {
|
|
513
|
+
const body = await req.json() as Record<string, unknown>;
|
|
514
|
+
const resp = await extensionRelayServer.sendCommand(body as Omit<import('../browser-extension-relay/protocol.js').ExtensionCommand, 'id'>);
|
|
515
|
+
return Response.json(resp);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
return Response.json({ success: false, error: err instanceof Error ? err.message : String(err) }, { status: 500 });
|
|
518
|
+
}
|
|
672
519
|
}
|
|
673
520
|
|
|
674
521
|
if (endpoint === 'conversations' && req.method === 'GET') {
|
|
@@ -682,6 +529,7 @@ export class RuntimeHttpServer {
|
|
|
682
529
|
return Response.json({
|
|
683
530
|
sessions: conversations.map((c) => {
|
|
684
531
|
const binding = bindings.get(c.id);
|
|
532
|
+
const originChannel = parseChannelId(c.originChannel);
|
|
685
533
|
return {
|
|
686
534
|
id: c.id,
|
|
687
535
|
title: c.title ?? 'Untitled',
|
|
@@ -696,15 +544,15 @@ export class RuntimeHttpServer {
|
|
|
696
544
|
username: binding.username,
|
|
697
545
|
},
|
|
698
546
|
} : {}),
|
|
547
|
+
...(originChannel ? { conversationOriginChannel: originChannel } : {}),
|
|
699
548
|
};
|
|
700
549
|
}),
|
|
701
550
|
hasMore: offset + conversations.length < totalCount,
|
|
702
551
|
});
|
|
703
552
|
}
|
|
704
553
|
|
|
705
|
-
if (endpoint === 'messages' && req.method === 'GET')
|
|
706
|
-
|
|
707
|
-
}
|
|
554
|
+
if (endpoint === 'messages' && req.method === 'GET') return handleListMessages(url, this.interfacesDir);
|
|
555
|
+
if (endpoint === 'search' && req.method === 'GET') return handleSearchConversations(url);
|
|
708
556
|
|
|
709
557
|
if (endpoint === 'messages' && req.method === 'POST') {
|
|
710
558
|
return await handleSendMessage(req, {
|
|
@@ -713,19 +561,11 @@ export class RuntimeHttpServer {
|
|
|
713
561
|
});
|
|
714
562
|
}
|
|
715
563
|
|
|
716
|
-
if (endpoint === 'attachments' && req.method === 'POST')
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
if (endpoint === 'attachments' && req.method === 'DELETE') {
|
|
721
|
-
return await handleDeleteAttachment(req);
|
|
722
|
-
}
|
|
564
|
+
if (endpoint === 'attachments' && req.method === 'POST') return await handleUploadAttachment(req);
|
|
565
|
+
if (endpoint === 'attachments' && req.method === 'DELETE') return await handleDeleteAttachment(req);
|
|
723
566
|
|
|
724
|
-
// Match attachments/:attachmentId
|
|
725
567
|
const attachmentMatch = endpoint.match(/^attachments\/([^/]+)$/);
|
|
726
|
-
if (attachmentMatch && req.method === 'GET')
|
|
727
|
-
return handleGetAttachment(attachmentMatch[1]);
|
|
728
|
-
}
|
|
568
|
+
if (attachmentMatch && req.method === 'GET') return handleGetAttachment(attachmentMatch[1]);
|
|
729
569
|
|
|
730
570
|
if (endpoint === 'suggestion' && req.method === 'GET') {
|
|
731
571
|
return await handleGetSuggestion(url, {
|
|
@@ -735,388 +575,93 @@ export class RuntimeHttpServer {
|
|
|
735
575
|
}
|
|
736
576
|
|
|
737
577
|
if (endpoint === 'runs' && req.method === 'POST') {
|
|
738
|
-
if (!this.runOrchestrator) {
|
|
739
|
-
return Response.json({ error: 'Run orchestration not configured' }, { status: 503 });
|
|
740
|
-
}
|
|
578
|
+
if (!this.runOrchestrator) return Response.json({ error: 'Run orchestration not configured' }, { status: 503 });
|
|
741
579
|
return await handleCreateRun(req, this.runOrchestrator);
|
|
742
580
|
}
|
|
743
581
|
|
|
744
|
-
// Match runs/:runId, runs/:runId/decision, runs/:runId/trust-rule, runs/:runId/secret
|
|
745
582
|
const runsMatch = endpoint.match(/^runs\/([^/]+)(\/decision|\/trust-rule|\/secret)?$/);
|
|
746
583
|
if (runsMatch) {
|
|
747
|
-
if (!this.runOrchestrator) {
|
|
748
|
-
return Response.json({ error: 'Run orchestration not configured' }, { status: 503 });
|
|
749
|
-
}
|
|
584
|
+
if (!this.runOrchestrator) return Response.json({ error: 'Run orchestration not configured' }, { status: 503 });
|
|
750
585
|
const runId = runsMatch[1];
|
|
751
|
-
if (runsMatch[2] === '/decision' && req.method === 'POST')
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
if (runsMatch[2] === '/secret' && req.method === 'POST') {
|
|
755
|
-
return await handleRunSecret(runId, req, this.runOrchestrator);
|
|
756
|
-
}
|
|
586
|
+
if (runsMatch[2] === '/decision' && req.method === 'POST') return await handleRunDecision(runId, req, this.runOrchestrator);
|
|
587
|
+
if (runsMatch[2] === '/secret' && req.method === 'POST') return await handleRunSecret(runId, req, this.runOrchestrator);
|
|
757
588
|
if (runsMatch[2] === '/trust-rule' && req.method === 'POST') {
|
|
758
589
|
const run = this.runOrchestrator.getRun(runId);
|
|
759
|
-
if (!run) {
|
|
760
|
-
return Response.json({ error: 'Run not found' }, { status: 404 });
|
|
761
|
-
}
|
|
590
|
+
if (!run) return Response.json({ error: 'Run not found' }, { status: 404 });
|
|
762
591
|
return await handleAddTrustRule(runId, req);
|
|
763
592
|
}
|
|
764
|
-
if (req.method === 'GET')
|
|
765
|
-
return handleGetRun(runId, this.runOrchestrator);
|
|
766
|
-
}
|
|
593
|
+
if (req.method === 'GET') return handleGetRun(runId, this.runOrchestrator);
|
|
767
594
|
}
|
|
768
595
|
|
|
769
596
|
const interfacesMatch = endpoint.match(/^interfaces\/(.+)$/);
|
|
770
|
-
if (interfacesMatch && req.method === 'GET')
|
|
771
|
-
return this.handleGetInterface(interfacesMatch[1]);
|
|
772
|
-
}
|
|
597
|
+
if (interfacesMatch && req.method === 'GET') return this.handleGetInterface(interfacesMatch[1]);
|
|
773
598
|
|
|
774
|
-
if (endpoint === 'channels/conversation' && req.method === 'DELETE')
|
|
775
|
-
return await handleDeleteConversation(req, assistantId);
|
|
776
|
-
}
|
|
599
|
+
if (endpoint === 'channels/conversation' && req.method === 'DELETE') return await handleDeleteConversation(req, assistantId);
|
|
777
600
|
|
|
778
601
|
if (endpoint === 'channels/inbound' && req.method === 'POST') {
|
|
779
|
-
const gatewayOriginSecret =
|
|
780
|
-
return await handleChannelInbound(req, this.processMessage, this.bearerToken, this.runOrchestrator, assistantId, gatewayOriginSecret);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
if (endpoint === 'channels/delivery-ack' && req.method === 'POST') {
|
|
784
|
-
return await handleChannelDeliveryAck(req);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
if (endpoint === 'channels/dead-letters' && req.method === 'GET') {
|
|
788
|
-
return handleListDeadLetters();
|
|
602
|
+
const gatewayOriginSecret = getRuntimeGatewayOriginSecret();
|
|
603
|
+
return await handleChannelInbound(req, this.processMessage, this.bearerToken, this.runOrchestrator, assistantId, gatewayOriginSecret, this.approvalCopyGenerator, this.approvalConversationGenerator);
|
|
789
604
|
}
|
|
790
605
|
|
|
791
|
-
if (endpoint === 'channels/
|
|
792
|
-
|
|
793
|
-
|
|
606
|
+
if (endpoint === 'channels/delivery-ack' && req.method === 'POST') return await handleChannelDeliveryAck(req);
|
|
607
|
+
if (endpoint === 'channels/dead-letters' && req.method === 'GET') return handleListDeadLetters();
|
|
608
|
+
if (endpoint === 'channels/replay' && req.method === 'POST') return await handleReplayDeadLetters(req);
|
|
794
609
|
|
|
795
|
-
|
|
796
|
-
if (endpoint === 'calls/start' && req.method === 'POST') {
|
|
797
|
-
return await handleStartCall(req, assistantId);
|
|
798
|
-
}
|
|
610
|
+
if (endpoint === 'calls/start' && req.method === 'POST') return await handleStartCall(req, assistantId);
|
|
799
611
|
|
|
800
|
-
// Match calls/:callSessionId and calls/:callSessionId/cancel, calls/:callSessionId/answer, calls/:callSessionId/instruction
|
|
801
612
|
const callsMatch = endpoint.match(/^calls\/([^/]+?)(\/cancel|\/answer|\/instruction)?$/);
|
|
802
613
|
if (callsMatch) {
|
|
803
614
|
const callSessionId = callsMatch[1];
|
|
804
|
-
// Skip known sub-paths that are handled elsewhere (twilio, relay)
|
|
805
615
|
if (callSessionId !== 'twilio' && callSessionId !== 'relay' && callSessionId !== 'start') {
|
|
806
|
-
if (callsMatch[2] === '/cancel' && req.method === 'POST')
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
if (callsMatch[2]
|
|
810
|
-
return await handleAnswerCall(req, callSessionId);
|
|
811
|
-
}
|
|
812
|
-
if (callsMatch[2] === '/instruction' && req.method === 'POST') {
|
|
813
|
-
return await handleInstructionCall(req, callSessionId);
|
|
814
|
-
}
|
|
815
|
-
if (!callsMatch[2] && req.method === 'GET') {
|
|
816
|
-
return handleGetCallStatus(callSessionId);
|
|
817
|
-
}
|
|
616
|
+
if (callsMatch[2] === '/cancel' && req.method === 'POST') return await handleCancelCall(req, callSessionId);
|
|
617
|
+
if (callsMatch[2] === '/answer' && req.method === 'POST') return await handleAnswerCall(req, callSessionId);
|
|
618
|
+
if (callsMatch[2] === '/instruction' && req.method === 'POST') return await handleInstructionCall(req, callSessionId);
|
|
619
|
+
if (!callsMatch[2] && req.method === 'GET') return handleGetCallStatus(callSessionId);
|
|
818
620
|
}
|
|
819
621
|
}
|
|
820
622
|
|
|
821
|
-
//
|
|
822
|
-
// These accept JSON payloads from the gateway (which already validated
|
|
823
|
-
// the Twilio signature) and reconstruct requests for the existing
|
|
824
|
-
// Twilio route handlers.
|
|
623
|
+
// Internal Twilio forwarding endpoints (gateway -> runtime)
|
|
825
624
|
if (endpoint === 'internal/twilio/voice-webhook' && req.method === 'POST') {
|
|
826
|
-
const json = await req.json() as { params: Record<string, string>; originalUrl?: string };
|
|
625
|
+
const json = await req.json() as { params: Record<string, string>; originalUrl?: string; assistantId?: string };
|
|
827
626
|
const formBody = new URLSearchParams(json.params).toString();
|
|
828
|
-
// Reconstruct request URL: keep the original URL query string (callSessionId)
|
|
829
627
|
const reconstructedUrl = json.originalUrl ?? req.url;
|
|
830
|
-
const fakeReq = new Request(reconstructedUrl, {
|
|
831
|
-
|
|
832
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
833
|
-
body: formBody,
|
|
834
|
-
});
|
|
835
|
-
return await handleVoiceWebhook(fakeReq);
|
|
628
|
+
const fakeReq = new Request(reconstructedUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
|
|
629
|
+
return await handleVoiceWebhook(fakeReq, json.assistantId);
|
|
836
630
|
}
|
|
837
631
|
|
|
838
632
|
if (endpoint === 'internal/twilio/status' && req.method === 'POST') {
|
|
839
633
|
const json = await req.json() as { params: Record<string, string> };
|
|
840
634
|
const formBody = new URLSearchParams(json.params).toString();
|
|
841
|
-
const fakeReq = new Request(req.url, {
|
|
842
|
-
method: 'POST',
|
|
843
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
844
|
-
body: formBody,
|
|
845
|
-
});
|
|
635
|
+
const fakeReq = new Request(req.url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
|
|
846
636
|
return await handleStatusCallback(fakeReq);
|
|
847
637
|
}
|
|
848
638
|
|
|
849
639
|
if (endpoint === 'internal/twilio/connect-action' && req.method === 'POST') {
|
|
850
640
|
const json = await req.json() as { params: Record<string, string> };
|
|
851
641
|
const formBody = new URLSearchParams(json.params).toString();
|
|
852
|
-
const fakeReq = new Request(req.url, {
|
|
853
|
-
method: 'POST',
|
|
854
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
855
|
-
body: formBody,
|
|
856
|
-
});
|
|
642
|
+
const fakeReq = new Request(req.url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formBody });
|
|
857
643
|
return await handleConnectAction(fakeReq);
|
|
858
644
|
}
|
|
859
645
|
|
|
860
|
-
if (endpoint === 'identity' && req.method === 'GET')
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
if (endpoint === 'events' && req.method === 'GET') {
|
|
865
|
-
return handleSubscribeAssistantEvents(req, url);
|
|
866
|
-
}
|
|
646
|
+
if (endpoint === 'identity' && req.method === 'GET') return handleGetIdentity();
|
|
647
|
+
if (endpoint === 'events' && req.method === 'GET') return handleSubscribeAssistantEvents(req, url);
|
|
867
648
|
|
|
868
|
-
//
|
|
649
|
+
// Internal OAuth callback endpoint (gateway -> runtime)
|
|
869
650
|
if (endpoint === 'internal/oauth/callback' && req.method === 'POST') {
|
|
870
651
|
const json = await req.json() as { state: string; code?: string; error?: string };
|
|
871
|
-
if (!json.state) {
|
|
872
|
-
return Response.json({ error: 'Missing state parameter' }, { status: 400 });
|
|
873
|
-
}
|
|
652
|
+
if (!json.state) return Response.json({ error: 'Missing state parameter' }, { status: 400 });
|
|
874
653
|
if (json.error) {
|
|
875
654
|
const consumed = consumeCallbackError(json.state, json.error);
|
|
876
|
-
return consumed
|
|
877
|
-
? Response.json({ ok: true })
|
|
878
|
-
: Response.json({ error: 'Unknown state' }, { status: 404 });
|
|
655
|
+
return consumed ? Response.json({ ok: true }) : Response.json({ error: 'Unknown state' }, { status: 404 });
|
|
879
656
|
}
|
|
880
657
|
if (json.code) {
|
|
881
658
|
const consumed = consumeCallback(json.state, json.code);
|
|
882
|
-
return consumed
|
|
883
|
-
? Response.json({ ok: true })
|
|
884
|
-
: Response.json({ error: 'Unknown state' }, { status: 404 });
|
|
659
|
+
return consumed ? Response.json({ ok: true }) : Response.json({ error: 'Unknown state' }, { status: 404 });
|
|
885
660
|
}
|
|
886
661
|
return Response.json({ error: 'Missing code or error parameter' }, { status: 400 });
|
|
887
662
|
}
|
|
888
663
|
|
|
889
664
|
return Response.json({ error: 'Not found', source: 'runtime' }, { status: 404 });
|
|
890
|
-
} catch (err) {
|
|
891
|
-
if (err instanceof IngressBlockedError) {
|
|
892
|
-
log.warn({ endpoint, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
|
|
893
|
-
return Response.json({ error: err.message, code: err.code }, { status: 422 });
|
|
894
|
-
}
|
|
895
|
-
if (err instanceof ConfigError) {
|
|
896
|
-
log.warn({ err, endpoint }, 'Runtime HTTP config error');
|
|
897
|
-
return Response.json({ error: err.message, code: err.code }, { status: 422 });
|
|
898
|
-
}
|
|
899
|
-
log.error({ err, endpoint }, 'Runtime HTTP handler error');
|
|
900
|
-
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
901
|
-
return Response.json({ error: message }, { status: 500 });
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Periodically retry failed channel inbound events that have passed
|
|
907
|
-
* their exponential backoff delay.
|
|
908
|
-
*/
|
|
909
|
-
private async sweepFailedEvents(): Promise<void> {
|
|
910
|
-
if (!this.processMessage) return;
|
|
911
|
-
|
|
912
|
-
const events = channelDeliveryStore.getRetryableEvents();
|
|
913
|
-
if (events.length === 0) return;
|
|
914
|
-
|
|
915
|
-
log.info({ count: events.length }, 'Retrying failed channel inbound events');
|
|
916
|
-
|
|
917
|
-
for (const event of events) {
|
|
918
|
-
if (!event.rawPayload) {
|
|
919
|
-
// No payload stored — can't replay, move to dead letter
|
|
920
|
-
channelDeliveryStore.recordProcessingFailure(
|
|
921
|
-
event.id,
|
|
922
|
-
new Error('No raw payload stored for replay'),
|
|
923
|
-
);
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
let payload: Record<string, unknown>;
|
|
928
|
-
try {
|
|
929
|
-
payload = JSON.parse(event.rawPayload) as Record<string, unknown>;
|
|
930
|
-
} catch {
|
|
931
|
-
channelDeliveryStore.recordProcessingFailure(
|
|
932
|
-
event.id,
|
|
933
|
-
new Error('Failed to parse stored raw payload'),
|
|
934
|
-
);
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
const content = typeof payload.content === 'string' ? payload.content.trim() : '';
|
|
939
|
-
const attachmentIds = Array.isArray(payload.attachmentIds) ? payload.attachmentIds as string[] : undefined;
|
|
940
|
-
const sourceChannel = payload.sourceChannel as string;
|
|
941
|
-
const sourceMetadata = payload.sourceMetadata as Record<string, unknown> | undefined;
|
|
942
|
-
const assistantId = typeof payload.assistantId === 'string'
|
|
943
|
-
? payload.assistantId
|
|
944
|
-
: undefined;
|
|
945
|
-
const guardianContext = parseGuardianRuntimeContext(payload.guardianCtx);
|
|
946
|
-
|
|
947
|
-
const metadataHintsRaw = sourceMetadata?.hints;
|
|
948
|
-
const metadataHints = Array.isArray(metadataHintsRaw)
|
|
949
|
-
? metadataHintsRaw.filter((h): h is string => typeof h === 'string' && h.trim().length > 0)
|
|
950
|
-
: [];
|
|
951
|
-
const metadataUxBrief = typeof sourceMetadata?.uxBrief === 'string' && sourceMetadata.uxBrief.trim().length > 0
|
|
952
|
-
? sourceMetadata.uxBrief.trim()
|
|
953
|
-
: undefined;
|
|
954
|
-
|
|
955
|
-
try {
|
|
956
|
-
const { messageId: userMessageId } = await this.processMessage(
|
|
957
|
-
event.conversationId,
|
|
958
|
-
content,
|
|
959
|
-
attachmentIds,
|
|
960
|
-
{
|
|
961
|
-
transport: {
|
|
962
|
-
channelId: sourceChannel,
|
|
963
|
-
hints: metadataHints.length > 0 ? metadataHints : undefined,
|
|
964
|
-
uxBrief: metadataUxBrief,
|
|
965
|
-
},
|
|
966
|
-
assistantId,
|
|
967
|
-
guardianContext,
|
|
968
|
-
},
|
|
969
|
-
);
|
|
970
|
-
channelDeliveryStore.linkMessage(event.id, userMessageId);
|
|
971
|
-
channelDeliveryStore.markProcessed(event.id);
|
|
972
|
-
log.info({ eventId: event.id }, 'Successfully replayed failed channel event');
|
|
973
|
-
|
|
974
|
-
const replyCallbackUrl = typeof payload.replyCallbackUrl === 'string'
|
|
975
|
-
? payload.replyCallbackUrl
|
|
976
|
-
: undefined;
|
|
977
|
-
if (replyCallbackUrl) {
|
|
978
|
-
const externalChatId = typeof payload.externalChatId === 'string'
|
|
979
|
-
? payload.externalChatId
|
|
980
|
-
: undefined;
|
|
981
|
-
if (externalChatId) {
|
|
982
|
-
await this.deliverReplyViaCallback(
|
|
983
|
-
event.conversationId,
|
|
984
|
-
externalChatId,
|
|
985
|
-
replyCallbackUrl,
|
|
986
|
-
assistantId,
|
|
987
|
-
);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
} catch (err) {
|
|
991
|
-
log.error({ err, eventId: event.id }, 'Retry failed for channel event');
|
|
992
|
-
channelDeliveryStore.recordProcessingFailure(event.id, err);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
private async deliverReplyViaCallback(
|
|
998
|
-
conversationId: string,
|
|
999
|
-
externalChatId: string,
|
|
1000
|
-
callbackUrl: string,
|
|
1001
|
-
assistantId?: string,
|
|
1002
|
-
): Promise<void> {
|
|
1003
|
-
const msgs = conversationStore.getMessages(conversationId);
|
|
1004
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1005
|
-
if (msgs[i].role === 'assistant') {
|
|
1006
|
-
let parsed: unknown;
|
|
1007
|
-
try { parsed = JSON.parse(msgs[i].content); } catch { parsed = msgs[i].content; }
|
|
1008
|
-
const rendered = renderHistoryContent(parsed);
|
|
1009
|
-
|
|
1010
|
-
const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id);
|
|
1011
|
-
const replyAttachments = linked.map((a) => ({
|
|
1012
|
-
id: a.id,
|
|
1013
|
-
filename: a.originalFilename,
|
|
1014
|
-
mimeType: a.mimeType,
|
|
1015
|
-
sizeBytes: a.sizeBytes,
|
|
1016
|
-
kind: a.kind,
|
|
1017
|
-
}));
|
|
1018
|
-
|
|
1019
|
-
if (rendered.text || replyAttachments.length > 0) {
|
|
1020
|
-
await deliverChannelReply(callbackUrl, {
|
|
1021
|
-
chatId: externalChatId,
|
|
1022
|
-
text: rendered.text || undefined,
|
|
1023
|
-
attachments: replyAttachments.length > 0 ? replyAttachments : undefined,
|
|
1024
|
-
assistantId,
|
|
1025
|
-
}, this.bearerToken);
|
|
1026
|
-
}
|
|
1027
|
-
break;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
private handleGetIdentity(): Response {
|
|
1033
|
-
const identityPath = getWorkspacePromptPath('IDENTITY.md');
|
|
1034
|
-
if (!existsSync(identityPath)) {
|
|
1035
|
-
return Response.json({ error: 'IDENTITY.md not found' }, { status: 404 });
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
const content = readFileSync(identityPath, 'utf-8');
|
|
1039
|
-
const fields: Record<string, string> = {};
|
|
1040
|
-
for (const line of content.split('\n')) {
|
|
1041
|
-
const trimmed = line.trim();
|
|
1042
|
-
const lower = trimmed.toLowerCase();
|
|
1043
|
-
const extract = (prefix: string): string | null => {
|
|
1044
|
-
if (!lower.startsWith(prefix)) return null;
|
|
1045
|
-
return trimmed.split(':**').pop()?.trim() ?? null;
|
|
1046
|
-
};
|
|
1047
|
-
|
|
1048
|
-
const name = extract('- **name:**');
|
|
1049
|
-
if (name) { fields.name = name; continue; }
|
|
1050
|
-
const role = extract('- **role:**');
|
|
1051
|
-
if (role) { fields.role = role; continue; }
|
|
1052
|
-
const personality = extract('- **personality:**') ?? extract('- **vibe:**');
|
|
1053
|
-
if (personality) { fields.personality = personality; continue; }
|
|
1054
|
-
const emoji = extract('- **emoji:**');
|
|
1055
|
-
if (emoji) { fields.emoji = emoji; continue; }
|
|
1056
|
-
const home = extract('- **home:**');
|
|
1057
|
-
if (home) { fields.home = home; continue; }
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// Read version from package.json
|
|
1061
|
-
let version: string | undefined;
|
|
1062
|
-
try {
|
|
1063
|
-
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '../../package.json');
|
|
1064
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
1065
|
-
version = pkg.version;
|
|
1066
|
-
} catch {
|
|
1067
|
-
// ignore
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
// Read createdAt from IDENTITY.md file birthtime
|
|
1071
|
-
let createdAt: string | undefined;
|
|
1072
|
-
try {
|
|
1073
|
-
const stats = statSync(identityPath);
|
|
1074
|
-
createdAt = stats.birthtime.toISOString();
|
|
1075
|
-
} catch {
|
|
1076
|
-
// ignore
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
// Read lockfile for assistantId, cloud, and originSystem
|
|
1080
|
-
let assistantId: string | undefined;
|
|
1081
|
-
let cloud: string | undefined;
|
|
1082
|
-
let originSystem: string | undefined;
|
|
1083
|
-
try {
|
|
1084
|
-
const lockData = readLockfile();
|
|
1085
|
-
const assistants = lockData?.assistants as Array<Record<string, unknown>> | undefined;
|
|
1086
|
-
if (assistants && assistants.length > 0) {
|
|
1087
|
-
// Use the most recently hatched assistant
|
|
1088
|
-
const sorted = [...assistants].sort((a, b) => {
|
|
1089
|
-
const dateA = new Date(a.hatchedAt as string || 0).getTime();
|
|
1090
|
-
const dateB = new Date(b.hatchedAt as string || 0).getTime();
|
|
1091
|
-
return dateB - dateA;
|
|
1092
|
-
});
|
|
1093
|
-
const latest = sorted[0];
|
|
1094
|
-
assistantId = latest.assistantId as string | undefined;
|
|
1095
|
-
cloud = latest.cloud as string | undefined;
|
|
1096
|
-
originSystem = cloud === 'local' ? 'local' : cloud;
|
|
1097
|
-
}
|
|
1098
|
-
} catch {
|
|
1099
|
-
// ignore — lockfile may not exist
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
return Response.json({
|
|
1103
|
-
name: fields.name ?? '',
|
|
1104
|
-
role: fields.role ?? '',
|
|
1105
|
-
personality: fields.personality ?? '',
|
|
1106
|
-
emoji: fields.emoji ?? '',
|
|
1107
|
-
home: fields.home ?? '',
|
|
1108
|
-
version,
|
|
1109
|
-
assistantId,
|
|
1110
|
-
createdAt,
|
|
1111
|
-
originSystem,
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
private handleHealth(): Response {
|
|
1116
|
-
return Response.json({
|
|
1117
|
-
status: 'healthy',
|
|
1118
|
-
timestamp: new Date().toISOString(),
|
|
1119
|
-
disk: getDiskSpaceInfo(),
|
|
1120
665
|
});
|
|
1121
666
|
}
|
|
1122
667
|
|
|
@@ -1125,7 +670,6 @@ export class RuntimeHttpServer {
|
|
|
1125
670
|
return Response.json({ error: 'Interface not found' }, { status: 404 });
|
|
1126
671
|
}
|
|
1127
672
|
const fullPath = resolve(this.interfacesDir, interfacePath);
|
|
1128
|
-
// Enforce directory boundary so prefix-sibling paths (e.g. "interfaces-other/") are rejected
|
|
1129
673
|
if (
|
|
1130
674
|
(fullPath !== this.interfacesDir && !fullPath.startsWith(this.interfacesDir + '/')) ||
|
|
1131
675
|
!existsSync(fullPath)
|