@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
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardian/approval routes: approval interception, approval prompt delivery,
|
|
3
|
+
* and guardian expiry sweep.
|
|
4
|
+
*/
|
|
5
|
+
import type { ChannelId } from '../../channels/types.js';
|
|
6
|
+
import { getPendingConfirmationsByConversation } from '../../memory/runs-store.js';
|
|
7
|
+
import { getLogger } from '../../util/logger.js';
|
|
8
|
+
import {
|
|
9
|
+
getPendingApprovalByRunAndGuardianChat,
|
|
10
|
+
getAllPendingApprovalsByGuardianChat,
|
|
11
|
+
getPendingApprovalForRun,
|
|
12
|
+
getUnresolvedApprovalForRun,
|
|
13
|
+
getExpiredPendingApprovals,
|
|
14
|
+
updateApprovalDecision,
|
|
15
|
+
} from '../../memory/channel-guardian-store.js';
|
|
16
|
+
import { deliverChannelReply, deliverApprovalPrompt } from '../gateway-client.js';
|
|
17
|
+
import { parseApprovalDecision } from '../channel-approval-parser.js';
|
|
18
|
+
import {
|
|
19
|
+
getChannelApprovalPrompt,
|
|
20
|
+
handleChannelDecision,
|
|
21
|
+
channelSupportsRichApprovalUI,
|
|
22
|
+
} from '../channel-approvals.js';
|
|
23
|
+
import { runApprovalConversationTurn } from '../approval-conversation-turn.js';
|
|
24
|
+
import type {
|
|
25
|
+
ApprovalDecisionResult,
|
|
26
|
+
ApprovalUIMetadata,
|
|
27
|
+
ChannelApprovalPrompt,
|
|
28
|
+
} from '../channel-approval-types.js';
|
|
29
|
+
import type { RunOrchestrator } from '../run-orchestrator.js';
|
|
30
|
+
import type {
|
|
31
|
+
ApprovalCopyGenerator,
|
|
32
|
+
ApprovalConversationGenerator,
|
|
33
|
+
ApprovalConversationContext,
|
|
34
|
+
} from '../http-types.js';
|
|
35
|
+
import { composeApprovalMessageGenerative } from '../approval-message-composer.js';
|
|
36
|
+
import type { ApprovalMessageContext } from '../approval-message-composer.js';
|
|
37
|
+
import {
|
|
38
|
+
type GuardianContext,
|
|
39
|
+
parseCallbackData,
|
|
40
|
+
requiredDecisionKeywords,
|
|
41
|
+
buildGuardianDenyContext,
|
|
42
|
+
} from './channel-route-shared.js';
|
|
43
|
+
import { schedulePostDecisionDelivery } from './channel-delivery-routes.js';
|
|
44
|
+
|
|
45
|
+
const log = getLogger('runtime-http');
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Approval prompt delivery
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
interface DeliverGeneratedApprovalPromptParams {
|
|
52
|
+
replyCallbackUrl: string;
|
|
53
|
+
chatId: string;
|
|
54
|
+
sourceChannel: ChannelId;
|
|
55
|
+
assistantId: string;
|
|
56
|
+
bearerToken?: string;
|
|
57
|
+
prompt: ChannelApprovalPrompt;
|
|
58
|
+
uiMetadata: ApprovalUIMetadata;
|
|
59
|
+
messageContext: ApprovalMessageContext;
|
|
60
|
+
approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Deliver approval prompts with best-available UX:
|
|
65
|
+
* 1) Rich UI (buttons) when supported
|
|
66
|
+
* 2) Plain-text fallback if rich delivery fails
|
|
67
|
+
* 3) Plain-text path for channels without rich UI
|
|
68
|
+
*/
|
|
69
|
+
export async function deliverGeneratedApprovalPrompt(params: DeliverGeneratedApprovalPromptParams): Promise<boolean> {
|
|
70
|
+
const {
|
|
71
|
+
replyCallbackUrl,
|
|
72
|
+
chatId,
|
|
73
|
+
sourceChannel,
|
|
74
|
+
assistantId,
|
|
75
|
+
bearerToken,
|
|
76
|
+
prompt,
|
|
77
|
+
uiMetadata,
|
|
78
|
+
messageContext,
|
|
79
|
+
approvalCopyGenerator,
|
|
80
|
+
} = params;
|
|
81
|
+
const keywords = requiredDecisionKeywords(uiMetadata.actions);
|
|
82
|
+
|
|
83
|
+
if (channelSupportsRichApprovalUI(sourceChannel)) {
|
|
84
|
+
const richText = await composeApprovalMessageGenerative(
|
|
85
|
+
{ ...messageContext, channel: sourceChannel, richUi: true },
|
|
86
|
+
{ fallbackText: prompt.promptText },
|
|
87
|
+
approvalCopyGenerator,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await deliverApprovalPrompt(
|
|
92
|
+
replyCallbackUrl,
|
|
93
|
+
chatId,
|
|
94
|
+
richText,
|
|
95
|
+
uiMetadata,
|
|
96
|
+
assistantId,
|
|
97
|
+
bearerToken,
|
|
98
|
+
);
|
|
99
|
+
return true;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
log.error(
|
|
102
|
+
{ err, chatId, sourceChannel },
|
|
103
|
+
'Failed to deliver rich approval prompt, attempting plain-text fallback',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const plainTextFallback = await composeApprovalMessageGenerative(
|
|
108
|
+
{ ...messageContext, channel: sourceChannel, richUi: false },
|
|
109
|
+
{ fallbackText: prompt.plainTextFallback, requiredKeywords: keywords },
|
|
110
|
+
approvalCopyGenerator,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Embed the run reference so plain-text replies can disambiguate when
|
|
114
|
+
// multiple approvals are pending for the same guardian chat.
|
|
115
|
+
const taggedFallback = `${plainTextFallback}\n[ref:${uiMetadata.runId}]`;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
119
|
+
chatId,
|
|
120
|
+
text: taggedFallback,
|
|
121
|
+
assistantId,
|
|
122
|
+
}, bearerToken);
|
|
123
|
+
return true;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
log.error(
|
|
126
|
+
{ err, chatId, sourceChannel },
|
|
127
|
+
'Failed to deliver plain-text fallback approval prompt',
|
|
128
|
+
);
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const plainText = await composeApprovalMessageGenerative(
|
|
134
|
+
{ ...messageContext, channel: sourceChannel, richUi: false },
|
|
135
|
+
{ fallbackText: prompt.plainTextFallback, requiredKeywords: keywords },
|
|
136
|
+
approvalCopyGenerator,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Embed the run reference for disambiguation in multi-pending scenarios.
|
|
140
|
+
const taggedPlainText = `${plainText}\n[ref:${uiMetadata.runId}]`;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
144
|
+
chatId,
|
|
145
|
+
text: taggedPlainText,
|
|
146
|
+
assistantId,
|
|
147
|
+
}, bearerToken);
|
|
148
|
+
return true;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
log.error({ err, chatId, sourceChannel }, 'Failed to deliver plain-text approval prompt');
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Approval interception
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
export interface ApprovalInterceptionParams {
|
|
160
|
+
conversationId: string;
|
|
161
|
+
callbackData?: string;
|
|
162
|
+
content: string;
|
|
163
|
+
externalChatId: string;
|
|
164
|
+
sourceChannel: ChannelId;
|
|
165
|
+
senderExternalUserId?: string;
|
|
166
|
+
replyCallbackUrl: string;
|
|
167
|
+
bearerToken?: string;
|
|
168
|
+
orchestrator: RunOrchestrator;
|
|
169
|
+
guardianCtx: GuardianContext;
|
|
170
|
+
assistantId: string;
|
|
171
|
+
approvalCopyGenerator?: ApprovalCopyGenerator;
|
|
172
|
+
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ApprovalInterceptionResult {
|
|
176
|
+
handled: boolean;
|
|
177
|
+
type?: 'decision_applied' | 'assistant_turn' | 'guardian_decision_applied' | 'stale_ignored';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check for pending approvals and handle inbound messages accordingly.
|
|
182
|
+
*
|
|
183
|
+
* Returns `{ handled: true }` when the message was consumed by the approval
|
|
184
|
+
* flow (either as a decision or a reminder), so the caller should NOT proceed
|
|
185
|
+
* to normal message processing.
|
|
186
|
+
*
|
|
187
|
+
* When the sender is a guardian responding from their chat, also checks for
|
|
188
|
+
* pending guardian approval requests and routes the decision accordingly.
|
|
189
|
+
*/
|
|
190
|
+
export async function handleApprovalInterception(
|
|
191
|
+
params: ApprovalInterceptionParams,
|
|
192
|
+
): Promise<ApprovalInterceptionResult> {
|
|
193
|
+
const {
|
|
194
|
+
conversationId,
|
|
195
|
+
callbackData,
|
|
196
|
+
content,
|
|
197
|
+
externalChatId,
|
|
198
|
+
sourceChannel,
|
|
199
|
+
senderExternalUserId,
|
|
200
|
+
replyCallbackUrl,
|
|
201
|
+
bearerToken,
|
|
202
|
+
orchestrator,
|
|
203
|
+
guardianCtx,
|
|
204
|
+
assistantId,
|
|
205
|
+
approvalCopyGenerator,
|
|
206
|
+
approvalConversationGenerator,
|
|
207
|
+
} = params;
|
|
208
|
+
|
|
209
|
+
// ── Guardian approval decision path ──
|
|
210
|
+
// When the sender is the guardian and there's a pending guardian approval
|
|
211
|
+
// request targeting this chat, the message might be a decision on behalf
|
|
212
|
+
// of a non-guardian requester.
|
|
213
|
+
if (
|
|
214
|
+
guardianCtx.actorRole === 'guardian' &&
|
|
215
|
+
senderExternalUserId
|
|
216
|
+
) {
|
|
217
|
+
// Callback/button path: deterministic and takes priority.
|
|
218
|
+
let callbackDecision: ApprovalDecisionResult | null = null;
|
|
219
|
+
if (callbackData) {
|
|
220
|
+
callbackDecision = parseCallbackData(callbackData);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// When a callback button provides a run ID, use the scoped lookup so
|
|
224
|
+
// the decision resolves to exactly the right approval even when
|
|
225
|
+
// multiple approvals target the same guardian chat.
|
|
226
|
+
let guardianApproval = callbackDecision?.runId
|
|
227
|
+
? getPendingApprovalByRunAndGuardianChat(callbackDecision.runId, sourceChannel, externalChatId, assistantId)
|
|
228
|
+
: null;
|
|
229
|
+
|
|
230
|
+
// When the scoped lookup didn't resolve an approval (either because
|
|
231
|
+
// there was no callback or the runId pointed to a stale/expired run),
|
|
232
|
+
// fall back to checking all pending approvals for this guardian chat.
|
|
233
|
+
if (!guardianApproval && callbackDecision) {
|
|
234
|
+
const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
|
|
235
|
+
if (allPending.length === 1) {
|
|
236
|
+
guardianApproval = allPending[0];
|
|
237
|
+
} else if (allPending.length > 1) {
|
|
238
|
+
// The callback targeted a stale/expired run but the guardian has other
|
|
239
|
+
// pending approvals. Inform them the clicked approval is no longer valid.
|
|
240
|
+
try {
|
|
241
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
242
|
+
scenario: 'guardian_disambiguation',
|
|
243
|
+
pendingCount: allPending.length,
|
|
244
|
+
channel: sourceChannel,
|
|
245
|
+
}, {}, approvalCopyGenerator);
|
|
246
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
247
|
+
chatId: externalChatId,
|
|
248
|
+
text: staleText,
|
|
249
|
+
assistantId,
|
|
250
|
+
}, bearerToken);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
log.error({ err, externalChatId }, 'Failed to deliver stale callback disambiguation notice');
|
|
253
|
+
}
|
|
254
|
+
return { handled: true, type: 'stale_ignored' };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// For plain-text messages (no callback), check if there are any pending
|
|
259
|
+
// approvals for this guardian chat to route through the conversation engine.
|
|
260
|
+
if (!guardianApproval && !callbackDecision) {
|
|
261
|
+
const allPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
|
|
262
|
+
if (allPending.length === 1) {
|
|
263
|
+
guardianApproval = allPending[0];
|
|
264
|
+
} else if (allPending.length > 1) {
|
|
265
|
+
// Multiple pending — pick the first approval matching this sender as
|
|
266
|
+
// primary context. The conversation engine sees all matching approvals
|
|
267
|
+
// via pendingApprovals and can disambiguate.
|
|
268
|
+
guardianApproval = allPending.find(a => a.guardianExternalUserId === senderExternalUserId) ?? allPending[0];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (guardianApproval) {
|
|
273
|
+
// Validate that the sender is the specific guardian who was assigned
|
|
274
|
+
// this approval request. This is a defense-in-depth check — the
|
|
275
|
+
// actorRole check above already verifies the sender is a guardian,
|
|
276
|
+
// but this catches edge cases like binding rotation between request
|
|
277
|
+
// creation and decision.
|
|
278
|
+
if (senderExternalUserId !== guardianApproval.guardianExternalUserId) {
|
|
279
|
+
log.warn(
|
|
280
|
+
{ externalChatId, senderExternalUserId, expectedGuardian: guardianApproval.guardianExternalUserId },
|
|
281
|
+
'Non-guardian sender attempted to act on guardian approval request',
|
|
282
|
+
);
|
|
283
|
+
try {
|
|
284
|
+
const mismatchText = await composeApprovalMessageGenerative({
|
|
285
|
+
scenario: 'guardian_identity_mismatch',
|
|
286
|
+
channel: sourceChannel,
|
|
287
|
+
}, {}, approvalCopyGenerator);
|
|
288
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
289
|
+
chatId: externalChatId,
|
|
290
|
+
text: mismatchText,
|
|
291
|
+
assistantId,
|
|
292
|
+
}, bearerToken);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian identity rejection notice');
|
|
295
|
+
}
|
|
296
|
+
return { handled: true, type: 'guardian_decision_applied' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (callbackDecision) {
|
|
300
|
+
// approve_always is not available for guardian approvals — guardians
|
|
301
|
+
// should not be able to permanently allowlist tools on behalf of the
|
|
302
|
+
// requester. Downgrade to approve_once.
|
|
303
|
+
if (callbackDecision.action === 'approve_always') {
|
|
304
|
+
callbackDecision = { ...callbackDecision, action: 'approve_once' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Apply the decision to the underlying run using the requester's
|
|
308
|
+
// conversation context
|
|
309
|
+
const result = handleChannelDecision(
|
|
310
|
+
guardianApproval.conversationId,
|
|
311
|
+
callbackDecision,
|
|
312
|
+
orchestrator,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (result.applied) {
|
|
316
|
+
// Update the guardian approval request record only when the decision
|
|
317
|
+
// was actually applied. If the run was already resolved (race with
|
|
318
|
+
// expiry sweep or concurrent callback), skip to avoid inconsistency.
|
|
319
|
+
const approvalStatus = callbackDecision.action === 'reject' ? 'denied' as const : 'approved' as const;
|
|
320
|
+
updateApprovalDecision(guardianApproval.id, {
|
|
321
|
+
status: approvalStatus,
|
|
322
|
+
decidedByExternalUserId: senderExternalUserId,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Notify the requester's chat about the outcome with the tool name
|
|
326
|
+
const outcomeText = await composeApprovalMessageGenerative({
|
|
327
|
+
scenario: 'guardian_decision_outcome',
|
|
328
|
+
decision: callbackDecision.action === 'reject' ? 'denied' : 'approved',
|
|
329
|
+
toolName: guardianApproval.toolName,
|
|
330
|
+
channel: sourceChannel,
|
|
331
|
+
}, {}, approvalCopyGenerator);
|
|
332
|
+
try {
|
|
333
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
334
|
+
chatId: guardianApproval.requesterChatId,
|
|
335
|
+
text: outcomeText,
|
|
336
|
+
assistantId,
|
|
337
|
+
}, bearerToken);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
log.error({ err, conversationId: guardianApproval.conversationId }, 'Failed to notify requester of guardian decision');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Schedule post-decision delivery to the requester's chat in case
|
|
343
|
+
// the original poll has already exited.
|
|
344
|
+
if (result.runId) {
|
|
345
|
+
schedulePostDecisionDelivery(
|
|
346
|
+
orchestrator,
|
|
347
|
+
result.runId,
|
|
348
|
+
guardianApproval.conversationId,
|
|
349
|
+
guardianApproval.requesterChatId,
|
|
350
|
+
replyCallbackUrl,
|
|
351
|
+
bearerToken,
|
|
352
|
+
assistantId,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return { handled: true, type: 'guardian_decision_applied' };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Race condition: callback arrived after run was already resolved.
|
|
359
|
+
return { handled: true, type: 'stale_ignored' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── Conversational engine for guardian plain-text messages ──
|
|
363
|
+
// Gather all pending guardian approvals for this chat so the engine
|
|
364
|
+
// can handle disambiguation when multiple are pending.
|
|
365
|
+
const allGuardianPending = getAllPendingApprovalsByGuardianChat(sourceChannel, externalChatId, assistantId);
|
|
366
|
+
// Only present approvals that belong to this sender so the engine
|
|
367
|
+
// does not offer disambiguation for requests assigned to a rotated
|
|
368
|
+
// guardian the sender cannot act on.
|
|
369
|
+
const senderPending = allGuardianPending.filter(a => a.guardianExternalUserId === senderExternalUserId);
|
|
370
|
+
const effectivePending = senderPending.length > 0 ? senderPending : allGuardianPending;
|
|
371
|
+
if (effectivePending.length > 0 && approvalConversationGenerator && content) {
|
|
372
|
+
const guardianAllowedActions = ['approve_once', 'reject'];
|
|
373
|
+
const engineContext: ApprovalConversationContext = {
|
|
374
|
+
toolName: guardianApproval.toolName,
|
|
375
|
+
allowedActions: guardianAllowedActions,
|
|
376
|
+
role: 'guardian',
|
|
377
|
+
pendingApprovals: effectivePending.map((a) => ({ runId: a.runId, toolName: a.toolName })),
|
|
378
|
+
userMessage: content,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const engineResult = await runApprovalConversationTurn(engineContext, approvalConversationGenerator);
|
|
382
|
+
|
|
383
|
+
if (engineResult.disposition === 'keep_pending') {
|
|
384
|
+
// Non-decision follow-up (clarification, disambiguation, etc.)
|
|
385
|
+
try {
|
|
386
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
387
|
+
chatId: externalChatId,
|
|
388
|
+
text: engineResult.replyText,
|
|
389
|
+
assistantId,
|
|
390
|
+
}, bearerToken);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
log.error({ err, conversationId: guardianApproval.conversationId }, 'Failed to deliver guardian conversation reply');
|
|
393
|
+
}
|
|
394
|
+
return { handled: true, type: 'assistant_turn' };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Decision-bearing disposition from the engine
|
|
398
|
+
let decisionAction = engineResult.disposition as 'approve_once' | 'approve_always' | 'reject';
|
|
399
|
+
|
|
400
|
+
// Belt-and-suspenders: guardians cannot approve_always even if the
|
|
401
|
+
// engine returns it (the engine's allowedActions validation should
|
|
402
|
+
// already prevent this, but enforce it here too).
|
|
403
|
+
if (decisionAction === 'approve_always') {
|
|
404
|
+
decisionAction = 'approve_once';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Resolve the target approval: use targetRunId from the engine if
|
|
408
|
+
// provided, otherwise use the single guardian approval.
|
|
409
|
+
const targetApproval = engineResult.targetRunId
|
|
410
|
+
? allGuardianPending.find((a) => a.runId === engineResult.targetRunId) ?? guardianApproval
|
|
411
|
+
: guardianApproval;
|
|
412
|
+
|
|
413
|
+
// Re-validate guardian identity against the resolved target. The
|
|
414
|
+
// engine may select a different pending approval (via targetRunId)
|
|
415
|
+
// that was assigned to a different guardian. Without this check a
|
|
416
|
+
// currently bound guardian could act on a request assigned to a
|
|
417
|
+
// previous guardian after a binding rotation.
|
|
418
|
+
if (senderExternalUserId !== targetApproval.guardianExternalUserId) {
|
|
419
|
+
log.warn(
|
|
420
|
+
{ externalChatId, senderExternalUserId, expectedGuardian: targetApproval.guardianExternalUserId, targetRunId: engineResult.targetRunId },
|
|
421
|
+
'Guardian identity mismatch on engine-selected target approval',
|
|
422
|
+
);
|
|
423
|
+
try {
|
|
424
|
+
const mismatchText = await composeApprovalMessageGenerative({
|
|
425
|
+
scenario: 'guardian_identity_mismatch',
|
|
426
|
+
channel: sourceChannel,
|
|
427
|
+
}, {}, approvalCopyGenerator);
|
|
428
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
429
|
+
chatId: externalChatId,
|
|
430
|
+
text: mismatchText,
|
|
431
|
+
assistantId,
|
|
432
|
+
}, bearerToken);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian identity mismatch notice for engine target');
|
|
435
|
+
}
|
|
436
|
+
return { handled: true, type: 'guardian_decision_applied' };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const engineDecision: ApprovalDecisionResult = {
|
|
440
|
+
action: decisionAction,
|
|
441
|
+
source: 'plain_text',
|
|
442
|
+
...(engineResult.targetRunId ? { runId: engineResult.targetRunId } : {}),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const result = handleChannelDecision(
|
|
446
|
+
targetApproval.conversationId,
|
|
447
|
+
engineDecision,
|
|
448
|
+
orchestrator,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (result.applied) {
|
|
452
|
+
// Update the guardian approval request record only when the decision
|
|
453
|
+
// was actually applied. If the run was already resolved (race with
|
|
454
|
+
// expiry sweep or concurrent callback), skip to avoid inconsistency.
|
|
455
|
+
const approvalStatus = decisionAction === 'reject' ? 'denied' as const : 'approved' as const;
|
|
456
|
+
updateApprovalDecision(targetApproval.id, {
|
|
457
|
+
status: approvalStatus,
|
|
458
|
+
decidedByExternalUserId: senderExternalUserId,
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Notify the requester's chat about the outcome
|
|
462
|
+
const outcomeText = await composeApprovalMessageGenerative({
|
|
463
|
+
scenario: 'guardian_decision_outcome',
|
|
464
|
+
decision: decisionAction === 'reject' ? 'denied' : 'approved',
|
|
465
|
+
toolName: targetApproval.toolName,
|
|
466
|
+
channel: sourceChannel,
|
|
467
|
+
}, {}, approvalCopyGenerator);
|
|
468
|
+
try {
|
|
469
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
470
|
+
chatId: targetApproval.requesterChatId,
|
|
471
|
+
text: outcomeText,
|
|
472
|
+
assistantId,
|
|
473
|
+
}, bearerToken);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
log.error({ err, conversationId: targetApproval.conversationId }, 'Failed to notify requester of guardian decision');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Schedule post-decision delivery to the requester's chat
|
|
479
|
+
if (result.runId) {
|
|
480
|
+
schedulePostDecisionDelivery(
|
|
481
|
+
orchestrator,
|
|
482
|
+
result.runId,
|
|
483
|
+
targetApproval.conversationId,
|
|
484
|
+
targetApproval.requesterChatId,
|
|
485
|
+
replyCallbackUrl,
|
|
486
|
+
bearerToken,
|
|
487
|
+
assistantId,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Deliver the engine's reply to the guardian
|
|
492
|
+
try {
|
|
493
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
494
|
+
chatId: externalChatId,
|
|
495
|
+
text: engineResult.replyText,
|
|
496
|
+
assistantId,
|
|
497
|
+
}, bearerToken);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
log.error({ err, conversationId: targetApproval.conversationId }, 'Failed to deliver guardian decision reply');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return { handled: true, type: 'guardian_decision_applied' };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Race condition: run was already resolved. Deliver a stale notice
|
|
506
|
+
// instead of the engine's optimistic reply.
|
|
507
|
+
try {
|
|
508
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
509
|
+
scenario: 'approval_already_resolved',
|
|
510
|
+
channel: sourceChannel,
|
|
511
|
+
}, {}, approvalCopyGenerator);
|
|
512
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
513
|
+
chatId: externalChatId,
|
|
514
|
+
text: staleText,
|
|
515
|
+
assistantId,
|
|
516
|
+
}, bearerToken);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
log.error({ err, conversationId: targetApproval.conversationId }, 'Failed to deliver stale guardian approval notice');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return { handled: true, type: 'stale_ignored' };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ── Legacy fallback when no conversational engine is available ──
|
|
525
|
+
// Use the deterministic parser to handle guardian plain-text so that
|
|
526
|
+
// simple yes/no replies still work when the engine is not injected.
|
|
527
|
+
if (content && !approvalConversationGenerator) {
|
|
528
|
+
const legacyGuardianDecision = parseApprovalDecision(content);
|
|
529
|
+
if (legacyGuardianDecision) {
|
|
530
|
+
// Guardians cannot approve_always — downgrade to approve_once.
|
|
531
|
+
if (legacyGuardianDecision.action === 'approve_always') {
|
|
532
|
+
legacyGuardianDecision.action = 'approve_once';
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Resolve the target approval: when a [ref:<runId>] tag is
|
|
536
|
+
// present, look up the specific pending approval by that runId
|
|
537
|
+
// so the decision applies to the correct conversation even when
|
|
538
|
+
// multiple guardian approvals are pending.
|
|
539
|
+
let targetLegacyApproval = guardianApproval;
|
|
540
|
+
if (legacyGuardianDecision.runId) {
|
|
541
|
+
const resolvedByRun = getPendingApprovalByRunAndGuardianChat(
|
|
542
|
+
legacyGuardianDecision.runId,
|
|
543
|
+
sourceChannel,
|
|
544
|
+
externalChatId,
|
|
545
|
+
assistantId,
|
|
546
|
+
);
|
|
547
|
+
if (!resolvedByRun) {
|
|
548
|
+
// The referenced run doesn't match any pending guardian
|
|
549
|
+
// approval — it may have expired or already been resolved.
|
|
550
|
+
try {
|
|
551
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
552
|
+
scenario: 'guardian_disambiguation',
|
|
553
|
+
channel: sourceChannel,
|
|
554
|
+
}, {}, approvalCopyGenerator);
|
|
555
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
556
|
+
chatId: externalChatId,
|
|
557
|
+
text: staleText,
|
|
558
|
+
assistantId,
|
|
559
|
+
}, bearerToken);
|
|
560
|
+
} catch (err) {
|
|
561
|
+
log.error({ err, externalChatId }, 'Failed to deliver stale approval notice (legacy path)');
|
|
562
|
+
}
|
|
563
|
+
return { handled: true, type: 'stale_ignored' };
|
|
564
|
+
}
|
|
565
|
+
targetLegacyApproval = resolvedByRun;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Re-validate guardian identity against the resolved target.
|
|
569
|
+
// The default guardianApproval was already checked, but a
|
|
570
|
+
// runId-resolved approval may belong to a different guardian.
|
|
571
|
+
if (senderExternalUserId !== targetLegacyApproval.guardianExternalUserId) {
|
|
572
|
+
log.warn(
|
|
573
|
+
{ externalChatId, senderExternalUserId, expectedGuardian: targetLegacyApproval.guardianExternalUserId, runId: legacyGuardianDecision.runId },
|
|
574
|
+
'Guardian identity mismatch on legacy run-ref resolved target approval',
|
|
575
|
+
);
|
|
576
|
+
try {
|
|
577
|
+
const mismatchText = await composeApprovalMessageGenerative({
|
|
578
|
+
scenario: 'guardian_identity_mismatch',
|
|
579
|
+
channel: sourceChannel,
|
|
580
|
+
}, {}, approvalCopyGenerator);
|
|
581
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
582
|
+
chatId: externalChatId,
|
|
583
|
+
text: mismatchText,
|
|
584
|
+
assistantId,
|
|
585
|
+
}, bearerToken);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
log.error({ err, externalChatId }, 'Failed to deliver guardian identity mismatch notice (legacy path)');
|
|
588
|
+
}
|
|
589
|
+
return { handled: true, type: 'guardian_decision_applied' };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const result = handleChannelDecision(
|
|
593
|
+
targetLegacyApproval.conversationId,
|
|
594
|
+
legacyGuardianDecision,
|
|
595
|
+
orchestrator,
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (result.applied) {
|
|
599
|
+
const approvalStatus = legacyGuardianDecision.action === 'reject' ? 'denied' as const : 'approved' as const;
|
|
600
|
+
updateApprovalDecision(targetLegacyApproval.id, {
|
|
601
|
+
status: approvalStatus,
|
|
602
|
+
decidedByExternalUserId: senderExternalUserId,
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// Notify the requester's chat about the outcome
|
|
606
|
+
const outcomeText = await composeApprovalMessageGenerative({
|
|
607
|
+
scenario: 'guardian_decision_outcome',
|
|
608
|
+
decision: legacyGuardianDecision.action === 'reject' ? 'denied' : 'approved',
|
|
609
|
+
toolName: targetLegacyApproval.toolName,
|
|
610
|
+
channel: sourceChannel,
|
|
611
|
+
}, {}, approvalCopyGenerator);
|
|
612
|
+
try {
|
|
613
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
614
|
+
chatId: targetLegacyApproval.requesterChatId,
|
|
615
|
+
text: outcomeText,
|
|
616
|
+
assistantId,
|
|
617
|
+
}, bearerToken);
|
|
618
|
+
} catch (err) {
|
|
619
|
+
log.error({ err, conversationId: targetLegacyApproval.conversationId }, 'Failed to notify requester of guardian decision (legacy path)');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (result.runId) {
|
|
623
|
+
schedulePostDecisionDelivery(
|
|
624
|
+
orchestrator,
|
|
625
|
+
result.runId,
|
|
626
|
+
targetLegacyApproval.conversationId,
|
|
627
|
+
targetLegacyApproval.requesterChatId,
|
|
628
|
+
replyCallbackUrl,
|
|
629
|
+
bearerToken,
|
|
630
|
+
assistantId,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { handled: true, type: 'guardian_decision_applied' };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Race condition: run was already resolved. Deliver stale notice.
|
|
638
|
+
try {
|
|
639
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
640
|
+
scenario: 'approval_already_resolved',
|
|
641
|
+
channel: sourceChannel,
|
|
642
|
+
}, {}, approvalCopyGenerator);
|
|
643
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
644
|
+
chatId: externalChatId,
|
|
645
|
+
text: staleText,
|
|
646
|
+
assistantId,
|
|
647
|
+
}, bearerToken);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
log.error({ err, conversationId: targetLegacyApproval.conversationId }, 'Failed to deliver stale guardian legacy fallback notice');
|
|
650
|
+
}
|
|
651
|
+
return { handled: true, type: 'stale_ignored' };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// No decision could be parsed — send a generic reminder to the guardian
|
|
655
|
+
try {
|
|
656
|
+
const reminderText = await composeApprovalMessageGenerative({
|
|
657
|
+
scenario: 'reminder_prompt',
|
|
658
|
+
toolName: guardianApproval.toolName,
|
|
659
|
+
channel: sourceChannel,
|
|
660
|
+
}, {}, approvalCopyGenerator);
|
|
661
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
662
|
+
chatId: externalChatId,
|
|
663
|
+
text: reminderText,
|
|
664
|
+
assistantId,
|
|
665
|
+
}, bearerToken);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
log.error({ err, conversationId: guardianApproval.conversationId }, 'Failed to deliver guardian reminder (legacy path)');
|
|
668
|
+
}
|
|
669
|
+
return { handled: true, type: 'assistant_turn' };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// No content and no engine — nothing to do, fall through to standard
|
|
673
|
+
// approval interception below.
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// ── Standard approval interception (existing flow) ──
|
|
678
|
+
const pendingPrompt = getChannelApprovalPrompt(conversationId);
|
|
679
|
+
if (!pendingPrompt) return { handled: false };
|
|
680
|
+
|
|
681
|
+
// When the sender is from an unverified channel, auto-deny any pending
|
|
682
|
+
// confirmation and block self-approval.
|
|
683
|
+
if (guardianCtx.actorRole === 'unverified_channel') {
|
|
684
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
685
|
+
if (pending.length > 0) {
|
|
686
|
+
const denyResult = handleChannelDecision(
|
|
687
|
+
conversationId,
|
|
688
|
+
{ action: 'reject', source: 'plain_text' },
|
|
689
|
+
orchestrator,
|
|
690
|
+
buildGuardianDenyContext(
|
|
691
|
+
pending[0].toolName,
|
|
692
|
+
guardianCtx.denialReason ?? 'no_binding',
|
|
693
|
+
sourceChannel,
|
|
694
|
+
),
|
|
695
|
+
);
|
|
696
|
+
if (denyResult.applied && denyResult.runId) {
|
|
697
|
+
schedulePostDecisionDelivery(
|
|
698
|
+
orchestrator,
|
|
699
|
+
denyResult.runId,
|
|
700
|
+
conversationId,
|
|
701
|
+
externalChatId,
|
|
702
|
+
replyCallbackUrl,
|
|
703
|
+
bearerToken,
|
|
704
|
+
assistantId,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return { handled: true, type: 'decision_applied' };
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// When the sender is a non-guardian and there's a pending guardian approval
|
|
712
|
+
// for this conversation's run, block self-approval. The non-guardian must
|
|
713
|
+
// wait for the guardian to decide.
|
|
714
|
+
if (guardianCtx.actorRole === 'non-guardian') {
|
|
715
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
716
|
+
if (pending.length > 0) {
|
|
717
|
+
const guardianApprovalForRun = getPendingApprovalForRun(pending[0].runId);
|
|
718
|
+
if (guardianApprovalForRun) {
|
|
719
|
+
// Allow the requester to cancel their own pending guardian request.
|
|
720
|
+
// Only reject/cancel is permitted — self-approval is still blocked.
|
|
721
|
+
if (content) {
|
|
722
|
+
let requesterCancelIntent = false;
|
|
723
|
+
let cancelReplyText: string | undefined;
|
|
724
|
+
let requesterFollowupReplyText: string | undefined;
|
|
725
|
+
|
|
726
|
+
// Interpret requester follow-ups through the conversation engine so
|
|
727
|
+
// "nevermind/cancel" resolves naturally while clarifying questions
|
|
728
|
+
// remain conversational turns.
|
|
729
|
+
if (approvalConversationGenerator) {
|
|
730
|
+
const cancelContext: ApprovalConversationContext = {
|
|
731
|
+
toolName: pending[0].toolName,
|
|
732
|
+
allowedActions: ['reject'],
|
|
733
|
+
role: 'requester',
|
|
734
|
+
pendingApprovals: pending.map(p => ({ runId: p.runId, toolName: p.toolName })),
|
|
735
|
+
userMessage: content,
|
|
736
|
+
};
|
|
737
|
+
const cancelResult = await runApprovalConversationTurn(cancelContext, approvalConversationGenerator);
|
|
738
|
+
if (cancelResult.disposition === 'reject') {
|
|
739
|
+
requesterCancelIntent = true;
|
|
740
|
+
cancelReplyText = cancelResult.replyText;
|
|
741
|
+
} else if (cancelResult.disposition === 'keep_pending') {
|
|
742
|
+
requesterFollowupReplyText = cancelResult.replyText;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (requesterCancelIntent) {
|
|
747
|
+
const rejectDecision: ApprovalDecisionResult = {
|
|
748
|
+
action: 'reject',
|
|
749
|
+
source: 'plain_text',
|
|
750
|
+
};
|
|
751
|
+
const cancelApplyResult = handleChannelDecision(conversationId, rejectDecision, orchestrator);
|
|
752
|
+
if (cancelApplyResult.applied) {
|
|
753
|
+
updateApprovalDecision(guardianApprovalForRun.id, {
|
|
754
|
+
status: 'denied',
|
|
755
|
+
decidedByExternalUserId: senderExternalUserId,
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// Notify requester
|
|
759
|
+
const replyText = cancelReplyText ?? await composeApprovalMessageGenerative({
|
|
760
|
+
scenario: 'requester_cancel',
|
|
761
|
+
toolName: pending[0].toolName,
|
|
762
|
+
channel: sourceChannel,
|
|
763
|
+
}, {}, approvalCopyGenerator);
|
|
764
|
+
try {
|
|
765
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
766
|
+
chatId: externalChatId,
|
|
767
|
+
text: replyText,
|
|
768
|
+
assistantId,
|
|
769
|
+
}, bearerToken);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
log.error({ err, conversationId }, 'Failed to deliver requester cancel notice');
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Notify guardian that the request was cancelled
|
|
775
|
+
try {
|
|
776
|
+
const guardianNotice = await composeApprovalMessageGenerative({
|
|
777
|
+
scenario: 'guardian_decision_outcome',
|
|
778
|
+
decision: 'denied',
|
|
779
|
+
toolName: pending[0].toolName,
|
|
780
|
+
channel: sourceChannel,
|
|
781
|
+
}, {}, approvalCopyGenerator);
|
|
782
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
783
|
+
chatId: guardianApprovalForRun.guardianChatId,
|
|
784
|
+
text: guardianNotice,
|
|
785
|
+
assistantId,
|
|
786
|
+
}, bearerToken);
|
|
787
|
+
} catch (err) {
|
|
788
|
+
log.error({ err, conversationId }, 'Failed to notify guardian of requester cancellation');
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (cancelApplyResult.runId) {
|
|
792
|
+
schedulePostDecisionDelivery(
|
|
793
|
+
orchestrator, cancelApplyResult.runId, conversationId, externalChatId,
|
|
794
|
+
replyCallbackUrl, bearerToken, assistantId,
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
return { handled: true, type: 'decision_applied' };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// Race condition: approval was already resolved elsewhere.
|
|
801
|
+
try {
|
|
802
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
803
|
+
scenario: 'approval_already_resolved',
|
|
804
|
+
channel: sourceChannel,
|
|
805
|
+
}, {}, approvalCopyGenerator);
|
|
806
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
807
|
+
chatId: externalChatId,
|
|
808
|
+
text: staleText,
|
|
809
|
+
assistantId,
|
|
810
|
+
}, bearerToken);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
log.error({ err, conversationId }, 'Failed to deliver stale requester-cancel notice');
|
|
813
|
+
}
|
|
814
|
+
return { handled: true, type: 'stale_ignored' };
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (requesterFollowupReplyText) {
|
|
818
|
+
try {
|
|
819
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
820
|
+
chatId: externalChatId,
|
|
821
|
+
text: requesterFollowupReplyText,
|
|
822
|
+
assistantId,
|
|
823
|
+
}, bearerToken);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
log.error({ err, conversationId }, 'Failed to deliver requester follow-up reply while awaiting guardian');
|
|
826
|
+
}
|
|
827
|
+
return { handled: true, type: 'assistant_turn' };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Not a cancel intent — tell the requester their request is pending
|
|
832
|
+
try {
|
|
833
|
+
const pendingText = await composeApprovalMessageGenerative({
|
|
834
|
+
scenario: 'request_pending_guardian',
|
|
835
|
+
channel: sourceChannel,
|
|
836
|
+
}, {}, approvalCopyGenerator);
|
|
837
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
838
|
+
chatId: externalChatId,
|
|
839
|
+
text: pendingText,
|
|
840
|
+
assistantId,
|
|
841
|
+
}, bearerToken);
|
|
842
|
+
} catch (err) {
|
|
843
|
+
log.error({ err, conversationId }, 'Failed to deliver guardian-pending notice to requester');
|
|
844
|
+
}
|
|
845
|
+
return { handled: true, type: 'assistant_turn' };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Check for an expired-but-unresolved guardian approval. If the approval
|
|
849
|
+
// expired without a guardian decision, auto-deny the run and transition
|
|
850
|
+
// the approval to 'expired'. Without this, the requester could bypass
|
|
851
|
+
// guardian-only controls by simply waiting for the TTL to elapse.
|
|
852
|
+
const unresolvedApproval = getUnresolvedApprovalForRun(pending[0].runId);
|
|
853
|
+
if (unresolvedApproval) {
|
|
854
|
+
updateApprovalDecision(unresolvedApproval.id, { status: 'expired' });
|
|
855
|
+
|
|
856
|
+
// Auto-deny the underlying run so it does not remain actionable
|
|
857
|
+
const expiredDecision: ApprovalDecisionResult = {
|
|
858
|
+
action: 'reject',
|
|
859
|
+
source: 'plain_text',
|
|
860
|
+
};
|
|
861
|
+
handleChannelDecision(conversationId, expiredDecision, orchestrator);
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
const expiredText = await composeApprovalMessageGenerative({
|
|
865
|
+
scenario: 'guardian_expired_requester',
|
|
866
|
+
toolName: pending[0].toolName,
|
|
867
|
+
channel: sourceChannel,
|
|
868
|
+
}, {}, approvalCopyGenerator);
|
|
869
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
870
|
+
chatId: externalChatId,
|
|
871
|
+
text: expiredText,
|
|
872
|
+
assistantId,
|
|
873
|
+
}, bearerToken);
|
|
874
|
+
} catch (err) {
|
|
875
|
+
log.error({ err, conversationId }, 'Failed to deliver guardian-expiry notice to requester');
|
|
876
|
+
}
|
|
877
|
+
return { handled: true, type: 'decision_applied' };
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Try to extract a decision from callback data (button press) first.
|
|
883
|
+
// Callback/button path remains deterministic and takes priority.
|
|
884
|
+
if (callbackData) {
|
|
885
|
+
const cbDecision = parseCallbackData(callbackData);
|
|
886
|
+
if (cbDecision) {
|
|
887
|
+
// When the decision came from a callback button, validate that the embedded
|
|
888
|
+
// run ID matches the currently pending run. A stale button (from a previous
|
|
889
|
+
// approval prompt) must not apply to a different pending run.
|
|
890
|
+
if (cbDecision.runId) {
|
|
891
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
892
|
+
if (pending.length === 0 || pending[0].runId !== cbDecision.runId) {
|
|
893
|
+
log.warn(
|
|
894
|
+
{ conversationId, callbackRunId: cbDecision.runId, pendingRunId: pending[0]?.runId },
|
|
895
|
+
'Callback run ID does not match pending run, ignoring stale button press',
|
|
896
|
+
);
|
|
897
|
+
return { handled: true, type: 'stale_ignored' };
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const result = handleChannelDecision(conversationId, cbDecision, orchestrator);
|
|
902
|
+
|
|
903
|
+
if (result.applied) {
|
|
904
|
+
// Schedule a background poll for run terminal state and deliver the reply.
|
|
905
|
+
// This handles the case where the original poll in
|
|
906
|
+
// processChannelMessageWithApprovals has already exited due to timeout.
|
|
907
|
+
// The claimRunDelivery guard ensures at-most-once delivery when both
|
|
908
|
+
// pollers race to terminal state.
|
|
909
|
+
if (result.runId) {
|
|
910
|
+
schedulePostDecisionDelivery(
|
|
911
|
+
orchestrator,
|
|
912
|
+
result.runId,
|
|
913
|
+
conversationId,
|
|
914
|
+
externalChatId,
|
|
915
|
+
replyCallbackUrl,
|
|
916
|
+
bearerToken,
|
|
917
|
+
assistantId,
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
return { handled: true, type: 'decision_applied' };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Race condition: run was already resolved between the stale check
|
|
924
|
+
// above and the decision attempt.
|
|
925
|
+
return { handled: true, type: 'stale_ignored' };
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ── Conversational approval engine for plain-text messages ──
|
|
930
|
+
// Instead of deterministic keyword matching and reminder prompts, delegate
|
|
931
|
+
// to the conversational approval engine which can classify natural language
|
|
932
|
+
// and respond conversationally.
|
|
933
|
+
const pending = getPendingConfirmationsByConversation(conversationId);
|
|
934
|
+
if (pending.length > 0 && approvalConversationGenerator && content) {
|
|
935
|
+
const allowedActions = pendingPrompt.actions.map((a) => a.id);
|
|
936
|
+
const engineContext: ApprovalConversationContext = {
|
|
937
|
+
toolName: pending[0].toolName,
|
|
938
|
+
allowedActions,
|
|
939
|
+
role: 'requester',
|
|
940
|
+
pendingApprovals: pending.map((p) => ({ runId: p.runId, toolName: p.toolName })),
|
|
941
|
+
userMessage: content,
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const engineResult = await runApprovalConversationTurn(engineContext, approvalConversationGenerator);
|
|
945
|
+
|
|
946
|
+
if (engineResult.disposition === 'keep_pending') {
|
|
947
|
+
// Non-decision follow-up — deliver the engine's reply and keep the run pending
|
|
948
|
+
try {
|
|
949
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
950
|
+
chatId: externalChatId,
|
|
951
|
+
text: engineResult.replyText,
|
|
952
|
+
assistantId,
|
|
953
|
+
}, bearerToken);
|
|
954
|
+
} catch (err) {
|
|
955
|
+
log.error({ err, conversationId }, 'Failed to deliver approval conversation reply');
|
|
956
|
+
}
|
|
957
|
+
return { handled: true, type: 'assistant_turn' };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Decision-bearing disposition — map to ApprovalDecisionResult and apply
|
|
961
|
+
const decisionAction = engineResult.disposition as 'approve_once' | 'approve_always' | 'reject';
|
|
962
|
+
const engineDecision: ApprovalDecisionResult = {
|
|
963
|
+
action: decisionAction,
|
|
964
|
+
source: 'plain_text',
|
|
965
|
+
...(engineResult.targetRunId ? { runId: engineResult.targetRunId } : {}),
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const result = handleChannelDecision(conversationId, engineDecision, orchestrator);
|
|
969
|
+
|
|
970
|
+
if (result.applied) {
|
|
971
|
+
if (result.runId) {
|
|
972
|
+
schedulePostDecisionDelivery(
|
|
973
|
+
orchestrator,
|
|
974
|
+
result.runId,
|
|
975
|
+
conversationId,
|
|
976
|
+
externalChatId,
|
|
977
|
+
replyCallbackUrl,
|
|
978
|
+
bearerToken,
|
|
979
|
+
assistantId,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Deliver the engine's reply text to the user
|
|
984
|
+
try {
|
|
985
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
986
|
+
chatId: externalChatId,
|
|
987
|
+
text: engineResult.replyText,
|
|
988
|
+
assistantId,
|
|
989
|
+
}, bearerToken);
|
|
990
|
+
} catch (err) {
|
|
991
|
+
log.error({ err, conversationId }, 'Failed to deliver approval decision reply');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return { handled: true, type: 'decision_applied' };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Race condition: run was already resolved by expiry sweep or
|
|
998
|
+
// concurrent callback. Deliver a stale notice instead of the
|
|
999
|
+
// engine's optimistic reply.
|
|
1000
|
+
try {
|
|
1001
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
1002
|
+
scenario: 'approval_already_resolved',
|
|
1003
|
+
channel: sourceChannel,
|
|
1004
|
+
}, {}, approvalCopyGenerator);
|
|
1005
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1006
|
+
chatId: externalChatId,
|
|
1007
|
+
text: staleText,
|
|
1008
|
+
assistantId,
|
|
1009
|
+
}, bearerToken);
|
|
1010
|
+
} catch (err) {
|
|
1011
|
+
log.error({ err, conversationId }, 'Failed to deliver stale approval notice');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return { handled: true, type: 'stale_ignored' };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Fallback: no conversational generator available or no content — use
|
|
1018
|
+
// the legacy deterministic path as a safety net. This preserves backward
|
|
1019
|
+
// compatibility when the generator is not injected.
|
|
1020
|
+
if (content) {
|
|
1021
|
+
const legacyDecision = parseApprovalDecision(content);
|
|
1022
|
+
if (legacyDecision) {
|
|
1023
|
+
if (legacyDecision.runId) {
|
|
1024
|
+
if (pending.length === 0 || pending[0].runId !== legacyDecision.runId) {
|
|
1025
|
+
return { handled: true, type: 'stale_ignored' };
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const result = handleChannelDecision(conversationId, legacyDecision, orchestrator);
|
|
1029
|
+
if (result.applied) {
|
|
1030
|
+
if (result.runId) {
|
|
1031
|
+
schedulePostDecisionDelivery(
|
|
1032
|
+
orchestrator,
|
|
1033
|
+
result.runId,
|
|
1034
|
+
conversationId,
|
|
1035
|
+
externalChatId,
|
|
1036
|
+
replyCallbackUrl,
|
|
1037
|
+
bearerToken,
|
|
1038
|
+
assistantId,
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return { handled: true, type: 'decision_applied' };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Race condition: run was already resolved.
|
|
1045
|
+
try {
|
|
1046
|
+
const staleText = await composeApprovalMessageGenerative({
|
|
1047
|
+
scenario: 'approval_already_resolved',
|
|
1048
|
+
channel: sourceChannel,
|
|
1049
|
+
}, {}, approvalCopyGenerator);
|
|
1050
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1051
|
+
chatId: externalChatId,
|
|
1052
|
+
text: staleText,
|
|
1053
|
+
assistantId,
|
|
1054
|
+
}, bearerToken);
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
log.error({ err, conversationId }, 'Failed to deliver stale approval notice (legacy path)');
|
|
1057
|
+
}
|
|
1058
|
+
return { handled: true, type: 'stale_ignored' };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// No decision could be extracted and no conversational engine is available —
|
|
1063
|
+
// deliver a simple status reply rather than a reminder prompt.
|
|
1064
|
+
try {
|
|
1065
|
+
const statusText = await composeApprovalMessageGenerative({
|
|
1066
|
+
scenario: 'reminder_prompt',
|
|
1067
|
+
channel: sourceChannel,
|
|
1068
|
+
toolName: pending.length > 0 ? pending[0].toolName : undefined,
|
|
1069
|
+
}, {}, approvalCopyGenerator);
|
|
1070
|
+
await deliverChannelReply(replyCallbackUrl, {
|
|
1071
|
+
chatId: externalChatId,
|
|
1072
|
+
text: statusText,
|
|
1073
|
+
assistantId,
|
|
1074
|
+
}, bearerToken);
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
log.error({ err, conversationId }, 'Failed to deliver approval status reply');
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
return { handled: true, type: 'assistant_turn' };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// ---------------------------------------------------------------------------
|
|
1083
|
+
// Proactive guardian approval expiry sweep
|
|
1084
|
+
// ---------------------------------------------------------------------------
|
|
1085
|
+
|
|
1086
|
+
/** Interval at which the expiry sweep runs (60 seconds). */
|
|
1087
|
+
const GUARDIAN_EXPIRY_SWEEP_INTERVAL_MS = 60_000;
|
|
1088
|
+
|
|
1089
|
+
/** Timer handle for the expiry sweep so it can be stopped in tests. */
|
|
1090
|
+
let expirySweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Sweep expired guardian approval requests, auto-deny the underlying runs,
|
|
1094
|
+
* and notify both the requester and guardian. This runs proactively on a
|
|
1095
|
+
* timer so expired approvals are closed without waiting for follow-up
|
|
1096
|
+
* traffic from either party.
|
|
1097
|
+
*
|
|
1098
|
+
* Accepts a `gatewayBaseUrl` rather than a fixed delivery URL so that
|
|
1099
|
+
* each approval's notification is routed to the correct channel-specific
|
|
1100
|
+
* endpoint (e.g. `/deliver/telegram`, `/deliver/sms`).
|
|
1101
|
+
*/
|
|
1102
|
+
export function sweepExpiredGuardianApprovals(
|
|
1103
|
+
orchestrator: RunOrchestrator,
|
|
1104
|
+
gatewayBaseUrl: string,
|
|
1105
|
+
bearerToken?: string,
|
|
1106
|
+
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
1107
|
+
): void {
|
|
1108
|
+
const expired = getExpiredPendingApprovals();
|
|
1109
|
+
for (const approval of expired) {
|
|
1110
|
+
// Mark the approval as expired
|
|
1111
|
+
updateApprovalDecision(approval.id, { status: 'expired' });
|
|
1112
|
+
|
|
1113
|
+
// Auto-deny the underlying run
|
|
1114
|
+
const expiredDecision: ApprovalDecisionResult = {
|
|
1115
|
+
action: 'reject',
|
|
1116
|
+
source: 'plain_text',
|
|
1117
|
+
};
|
|
1118
|
+
handleChannelDecision(approval.conversationId, expiredDecision, orchestrator);
|
|
1119
|
+
|
|
1120
|
+
// Construct the per-channel delivery URL from the approval's channel
|
|
1121
|
+
const deliverUrl = `${gatewayBaseUrl}/deliver/${approval.channel}`;
|
|
1122
|
+
|
|
1123
|
+
// Notify the requester that the approval expired
|
|
1124
|
+
void (async () => {
|
|
1125
|
+
const requesterText = await composeApprovalMessageGenerative({
|
|
1126
|
+
scenario: 'guardian_expired_requester',
|
|
1127
|
+
toolName: approval.toolName,
|
|
1128
|
+
channel: approval.channel,
|
|
1129
|
+
}, {}, approvalCopyGenerator);
|
|
1130
|
+
await deliverChannelReply(deliverUrl, {
|
|
1131
|
+
chatId: approval.requesterChatId,
|
|
1132
|
+
text: requesterText,
|
|
1133
|
+
assistantId: approval.assistantId,
|
|
1134
|
+
}, bearerToken);
|
|
1135
|
+
})().catch((err) => {
|
|
1136
|
+
log.error({ err, runId: approval.runId }, 'Failed to notify requester of guardian approval expiry');
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
// Notify the guardian that the approval expired
|
|
1140
|
+
void (async () => {
|
|
1141
|
+
const guardianText = await composeApprovalMessageGenerative({
|
|
1142
|
+
scenario: 'guardian_expired_guardian',
|
|
1143
|
+
toolName: approval.toolName,
|
|
1144
|
+
requesterIdentifier: approval.requesterExternalUserId,
|
|
1145
|
+
channel: approval.channel,
|
|
1146
|
+
}, {}, approvalCopyGenerator);
|
|
1147
|
+
await deliverChannelReply(deliverUrl, {
|
|
1148
|
+
chatId: approval.guardianChatId,
|
|
1149
|
+
text: guardianText,
|
|
1150
|
+
assistantId: approval.assistantId,
|
|
1151
|
+
}, bearerToken);
|
|
1152
|
+
})().catch((err) => {
|
|
1153
|
+
log.error({ err, runId: approval.runId }, 'Failed to notify guardian of approval expiry');
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
log.info(
|
|
1157
|
+
{ runId: approval.runId, approvalId: approval.id },
|
|
1158
|
+
'Auto-denied expired guardian approval request',
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Start the periodic expiry sweep. Idempotent — calling it multiple times
|
|
1165
|
+
* re-uses the same timer.
|
|
1166
|
+
*/
|
|
1167
|
+
export function startGuardianExpirySweep(
|
|
1168
|
+
orchestrator: RunOrchestrator,
|
|
1169
|
+
gatewayBaseUrl: string,
|
|
1170
|
+
bearerToken?: string,
|
|
1171
|
+
approvalCopyGenerator?: ApprovalCopyGenerator,
|
|
1172
|
+
): void {
|
|
1173
|
+
if (expirySweepTimer) return;
|
|
1174
|
+
expirySweepTimer = setInterval(() => {
|
|
1175
|
+
try {
|
|
1176
|
+
sweepExpiredGuardianApprovals(orchestrator, gatewayBaseUrl, bearerToken, approvalCopyGenerator);
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
log.error({ err }, 'Guardian expiry sweep failed');
|
|
1179
|
+
}
|
|
1180
|
+
}, GUARDIAN_EXPIRY_SWEEP_INTERVAL_MS);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Stop the periodic expiry sweep. Used in tests and shutdown.
|
|
1185
|
+
*/
|
|
1186
|
+
export function stopGuardianExpirySweep(): void {
|
|
1187
|
+
if (expirySweepTimer) {
|
|
1188
|
+
clearInterval(expirySweepTimer);
|
|
1189
|
+
expirySweepTimer = null;
|
|
1190
|
+
}
|
|
1191
|
+
}
|