@vellumai/assistant 0.11.5 → 0.11.6-dev.202608260020.27df966
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/AGENTS.md +5 -1
- package/ARCHITECTURE.md +1 -1
- package/docs/flux-turn-detection-spike.md +8 -7
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +56 -0
- package/node_modules/@vellumai/environments/src/shell.ts +65 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +18 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +0 -5
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +77 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +129 -61
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +495 -21
- package/package.json +2 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/compactor-call-site-logging.test.ts +61 -0
- package/src/__tests__/config-loader-backfill.test.ts +69 -0
- package/src/__tests__/config-schema.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +61 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-conversion.test.ts +38 -4
- package/src/__tests__/install-skill-routing.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/llm-resolver.test.ts +20 -0
- package/src/__tests__/llm-schema-fallback-profile.test.ts +359 -0
- package/src/__tests__/managed-fallback-dispatch.test.ts +430 -0
- package/src/__tests__/managed-profile-guard.test.ts +28 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +6 -8
- package/src/__tests__/persist-unsendable-image.test.ts +3 -3
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/platform.test.ts +19 -1
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/plugin-import-boundary-guard.test.ts +4 -0
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/voice-config-update.test.ts +43 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/__tests__/workspace-migration-147-rename-colliding-backup-profile-names.test.ts +480 -0
- package/src/__tests__/workspace-migration-148-strip-unsupported-fallback-profiles.test.ts +167 -0
- package/src/__tests__/workspace-migration-149-repoint-backup-profile-selections.test.ts +144 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +35 -3
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/backup/__tests__/paths.test.ts +6 -2
- package/src/backup/paths.ts +6 -4
- package/src/bundler/app-compiler.ts +2 -2
- package/src/bundler/compiler-tools.test.ts +36 -0
- package/src/bundler/compiler-tools.ts +54 -18
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +77 -1
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +11 -5
- package/src/config/bundled-skills/settings/tools/open-system-settings.test.ts +85 -0
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +33 -8
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +54 -3
- package/src/config/default-profile-catalog.ts +195 -10
- package/src/config/default-profile-names.ts +41 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +1 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +53 -8
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +4 -3
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/live-voice.ts +5 -3
- package/src/config/schemas/llm.ts +263 -10
- package/src/config/schemas/services.ts +30 -0
- package/src/config/schemas/stt.ts +29 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +18 -0
- package/src/context/compactor.ts +74 -1
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -0
- package/src/daemon/conversation-agent-loop.ts +40 -3
- package/src/daemon/conversation-error.ts +49 -2
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +8 -17
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +13 -4
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +285 -21
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +354 -0
- package/src/live-voice/__tests__/protocol.test.ts +169 -0
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +357 -4
- package/src/live-voice/live-voice-session.ts +240 -8
- package/src/live-voice/protocol.ts +157 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/file-descriptors.ts +39 -6
- package/src/monitoring/process-memory.ts +37 -5
- package/src/monitoring/resource-sampler.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +280 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/embedding-local.ts +13 -85
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugins/defaults/compaction/window-manager.ts +4 -0
- package/src/plugins/defaults/image-recovery/recover.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +125 -33
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-prompt.test.ts +21 -31
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +13 -9
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +31 -4
- package/src/plugins/defaults/memory/memory-retrospective-constants.ts +0 -11
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +140 -107
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +13 -13
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +2 -2
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +25 -18
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +225 -12
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +39 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +29 -2
- package/src/providers/openai/chat-completions-provider.ts +155 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +944 -68
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +42 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +136 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +120 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +97 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +78 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +159 -3
- package/src/providers/speech-to-text/resolve.ts +66 -15
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +31 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +176 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +108 -10
- package/src/runtime/routes/conversation-routes.ts +2 -2
- package/src/runtime/routes/credential-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +2 -2
- package/src/runtime/routes/inference-profiles-routes.ts +244 -38
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/stt-routes.ts +6 -1
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/worker.ts +6 -0
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/skills/available-skills.ts +10 -1
- package/src/skills/catalog-install.ts +19 -1
- package/src/skills/inline-command-runner.ts +6 -2
- package/src/skills/platform-compatibility.test.ts +49 -0
- package/src/skills/platform-compatibility.ts +65 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/types.ts +33 -3
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +142 -0
- package/src/telemetry/live-voice-funnel.ts +85 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/shared/filesystem/image-read.ts +1 -1
- package/src/tools/skills/find-similar-skills.ts +10 -2
- package/src/tools/skills/load.ts +14 -0
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/terminal/__tests__/safe-env.test.ts +64 -1
- package/src/tools/terminal/safe-env.ts +39 -9
- package/src/tools/terminal/shell.ts +12 -31
- package/src/tools/types.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +2 -2
- package/src/tools/workflows/run-workflow.test.ts +2 -1
- package/src/util/__tests__/file-use.test.ts +61 -0
- package/src/util/__tests__/process-table.test.ts +66 -0
- package/src/util/__tests__/process-tree.test.ts +46 -0
- package/src/util/errors.ts +1 -0
- package/src/util/file-use.ts +91 -0
- package/src/util/host-process.test.ts +17 -0
- package/src/util/host-process.ts +75 -0
- package/src/util/image-conversion.ts +39 -7
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/platform.ts +26 -6
- package/src/util/process-table.ts +249 -0
- package/src/util/process-tree.ts +85 -129
- package/src/util/provider-error-patterns.ts +20 -0
- package/src/util/spawn.ts +9 -2
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
- package/src/workspace/git-service.ts +4 -14
- package/src/workspace/migrations/147-rename-colliding-backup-profile-names.ts +360 -0
- package/src/workspace/migrations/148-strip-unsupported-fallback-profiles.ts +117 -0
- package/src/workspace/migrations/149-repoint-backup-profile-selections.ts +204 -0
- package/src/workspace/migrations/150-stt-flux-provider-to-model-family.ts +139 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +160 -0
- package/src/workspace/migrations/registry.ts +8 -0
package/AGENTS.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
For error handling conventions (throw vs result objects vs null), see [docs/error-handling.md](docs/error-handling.md).
|
|
4
4
|
|
|
5
|
-
Subdirectory-scoped rules live in local AGENTS.md files
|
|
5
|
+
Subdirectory-scoped rules live in local AGENTS.md files throughout `src/`, some
|
|
6
|
+
of them nested several levels down. Read the one nearest the code you are
|
|
7
|
+
changing plus any above it, and follow the nearest where two disagree.
|
|
8
|
+
`git ls-files ':(top)assistant/src/**/AGENTS.md'` lists them from anywhere in
|
|
9
|
+
the tree.
|
|
6
10
|
|
|
7
11
|
## Adding new environment variables
|
|
8
12
|
|
package/ARCHITECTURE.md
CHANGED
|
@@ -84,7 +84,7 @@ All HTTP API requests use a single `Authorization: Bearer <jwt>` header for auth
|
|
|
84
84
|
|
|
85
85
|
**Identity lifecycle:**
|
|
86
86
|
|
|
87
|
-
1. **Bootstrap (loopback-only,
|
|
87
|
+
1. **Bootstrap (loopback-only, desktop)**: On first launch, the desktop client calls `POST /v1/guardian/init` with `{ platform, deviceId }`. The endpoint is loopback-only and mints a JWT access token + refresh token pair. Returns `{ guardianPrincipalId, accessToken, accessTokenExpiresAt, refreshToken, refreshTokenExpiresAt, refreshAfter, isNew }`. The CLI obtains its bearer token during `hatch` and does not perform a separate bootstrap step.
|
|
88
88
|
|
|
89
89
|
2. **Refresh** — `POST /v1/guardian/refresh` accepts `{ refreshToken }` and returns a new access/refresh token pair. Single-use rotation with replay detection and family-based revocation.
|
|
90
90
|
|
|
@@ -59,7 +59,7 @@ If it does not, set the Deepgram key the ordinary way (client Settings, Speech-t
|
|
|
59
59
|
|
|
60
60
|
## 2. Enable Flux
|
|
61
61
|
|
|
62
|
-
> **
|
|
62
|
+
> **Putting a provider on the `flux` model family turns off batch transcription for the whole workspace, not just live voice.** `services.stt.provider` plus `services.stt.providers.<provider>.model` is the single source of truth for every STT route, and Flux is the only family in the catalog with no `daemon-batch` boundary. For as long as it is set, these all stop working: voice-message and inbound-attachment transcription, the `transcribe` skill, the `media-processing` skill's audio segments, `POST /v1/stt/transcribe`, and phone-call transcription. Each reports a message naming Flux as the cause rather than failing silently, but they do not fall back to `deepgram` on their own. **Set `services.stt.providers.deepgram.model` back to `nova-3` when the spike is over**, and do not run the spike on an assistant that is also taking calls or handling voice messages.
|
|
63
63
|
|
|
64
64
|
Two keys in `config.json`, which lives at `$VELLUM_WORKSPACE_DIR/config.json` (default `~/.vellum/workspace/config.json`):
|
|
65
65
|
|
|
@@ -67,7 +67,8 @@ Two keys in `config.json`, which lives at `$VELLUM_WORKSPACE_DIR/config.json` (d
|
|
|
67
67
|
{
|
|
68
68
|
"services": {
|
|
69
69
|
"stt": {
|
|
70
|
-
"provider": "deepgram
|
|
70
|
+
"provider": "deepgram",
|
|
71
|
+
"providers": { "deepgram": { "model": "flux" } }
|
|
71
72
|
}
|
|
72
73
|
},
|
|
73
74
|
"liveVoice": {
|
|
@@ -93,12 +94,12 @@ The rest of `liveVoice.flux` is optional and defaulted (`config/schemas/live-voi
|
|
|
93
94
|
|
|
94
95
|
## 3. Run the A/B
|
|
95
96
|
|
|
96
|
-
**Flip `turnEnd.enabled` between runs and leave
|
|
97
|
+
**Flip `turnEnd.enabled` between runs and leave the `flux` model family selected in both arms.** That holds the STT engine, the model, the socket, and the transcriber lifecycle constant, so the only thing that changes is which signal commits the turn.
|
|
97
98
|
|
|
98
|
-
- **Arm A (control):** `
|
|
99
|
-
- **Arm B (treatment):** `
|
|
99
|
+
- **Arm A (control):** `providers.deepgram.model: "flux"`, `turnEnd.enabled: false`. Flux transcribes; the four turn-detection events are ignored; the local silence boundary and the front-door hold path run exactly as they do today.
|
|
100
|
+
- **Arm B (treatment):** `providers.deepgram.model: "flux"`, `turnEnd.enabled: true`.
|
|
100
101
|
|
|
101
|
-
**Do not A/B by switching the
|
|
102
|
+
**Do not A/B by switching the model family between `nova-3` and `flux`.** That confounds two changes at once:
|
|
102
103
|
|
|
103
104
|
1. It swaps the STT model. `deepgram` runs `nova-2` (`DEFAULT_MODEL`, `deepgram-realtime.ts`), so any transcript-quality or first-partial difference lands in your latency numbers.
|
|
104
105
|
2. It swaps the transcriber lifecycle. `deepgram` implements `finalizeUtterance`, so the session adopts it as a persistent stream shared across the whole session (`sharedTranscriber`) and never tears it down between turns. Flux implements no `finalizeUtterance`, so every utterance owns its own `/v2/listen` socket and every release closes it. That per-turn socket churn is inside `roundTripMs` and `totalMs` on the Flux side and absent on the `deepgram` side, so a naive provider-swap A/B attributes it to turn detection. The next section is what it costs and why it cannot be removed.
|
|
@@ -230,7 +231,7 @@ Check these if you port the spike anywhere else, because a run that violates eit
|
|
|
230
231
|
|
|
231
232
|
## 8. Falling back
|
|
232
233
|
|
|
233
|
-
Set `liveVoice.flux.turnEnd.enabled` back to `false` (or delete the key) and restart. That is the whole rollback: the latch goes down, the turn-detection events become no-ops, and the silence-boundary path runs unchanged. You can leave
|
|
234
|
+
Set `liveVoice.flux.turnEnd.enabled` back to `false` (or delete the key) and restart. That is the whole rollback: the latch goes down, the turn-detection events become no-ops, and the silence-boundary path runs unchanged. You can leave the `flux` family selected or move back to `nova-3`; either is a working configuration.
|
|
234
235
|
|
|
235
236
|
Runtime fallback needs no action. A Flux stream that never emits `turn-end` is caught by the fail-open deadline and the utterance replays onto the silence path, so an outage degrades to today's behavior rather than to a hung turn.
|
|
236
237
|
|
|
@@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeHttpPublicBaseUrl,
|
|
5
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash,
|
|
5
6
|
normalizePublicBaseUrl,
|
|
7
|
+
parseRecordedAssistantId,
|
|
8
|
+
parseTunnelRecord,
|
|
9
|
+
trimmedNonEmptyString,
|
|
10
|
+
TUNNEL_PROVIDERS,
|
|
6
11
|
velayHostForPlatformHost,
|
|
7
12
|
} from "../ingress.js";
|
|
8
13
|
import {
|
|
@@ -85,6 +90,35 @@ describe("normalizeHttpPublicBaseUrl", () => {
|
|
|
85
90
|
});
|
|
86
91
|
});
|
|
87
92
|
|
|
93
|
+
describe("normalizeHttpPublicBaseUrlWithoutTrailingSlash", () => {
|
|
94
|
+
test("drops the root path its sibling always emits", () => {
|
|
95
|
+
expect(
|
|
96
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash("https://x.test"),
|
|
97
|
+
).toBe("https://x.test");
|
|
98
|
+
expect(
|
|
99
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash("https://x.test/"),
|
|
100
|
+
).toBe("https://x.test");
|
|
101
|
+
expect(
|
|
102
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(" https://x.test/api/// "),
|
|
103
|
+
).toBe("https://x.test/api");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("rejects everything its sibling rejects", () => {
|
|
107
|
+
for (const value of [
|
|
108
|
+
"",
|
|
109
|
+
" ",
|
|
110
|
+
"notaurl",
|
|
111
|
+
"ftp://x.test",
|
|
112
|
+
"https://x.test?a=b",
|
|
113
|
+
42,
|
|
114
|
+
]) {
|
|
115
|
+
expect(
|
|
116
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(value),
|
|
117
|
+
).toBeUndefined();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
describe("Twilio ingress helpers", () => {
|
|
89
123
|
test("resolves public base URL with fallback", () => {
|
|
90
124
|
expect(
|
|
@@ -123,3 +157,87 @@ describe("Twilio ingress helpers", () => {
|
|
|
123
157
|
});
|
|
124
158
|
});
|
|
125
159
|
});
|
|
160
|
+
|
|
161
|
+
describe("parseTunnelRecord", () => {
|
|
162
|
+
const TUNNEL_URL = "https://assistant-1.example.ts.net";
|
|
163
|
+
|
|
164
|
+
test("accepts every provider in the registry", () => {
|
|
165
|
+
for (const provider of TUNNEL_PROVIDERS) {
|
|
166
|
+
expect(
|
|
167
|
+
parseTunnelRecord({ provider, publicBaseUrl: TUNNEL_URL }),
|
|
168
|
+
).toEqual({ provider, publicBaseUrl: TUNNEL_URL });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("returns the URL in the shape its own validator produces", () => {
|
|
173
|
+
// Padding and trailing slashes are normalized away rather than handed
|
|
174
|
+
// back, so readers never re-normalize what they were given.
|
|
175
|
+
for (const publicBaseUrl of [
|
|
176
|
+
` ${TUNNEL_URL} `,
|
|
177
|
+
`${TUNNEL_URL}/`,
|
|
178
|
+
`${TUNNEL_URL}///`,
|
|
179
|
+
]) {
|
|
180
|
+
expect(
|
|
181
|
+
parseTunnelRecord({ provider: "ngrok", publicBaseUrl }),
|
|
182
|
+
).toEqual({ provider: "ngrok", publicBaseUrl: TUNNEL_URL });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("rejects values that are not a record", () => {
|
|
187
|
+
for (const value of [undefined, null, "nonsense", 42, []]) {
|
|
188
|
+
expect(parseTunnelRecord(value)).toBeNull();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects a record missing either field", () => {
|
|
193
|
+
expect(parseTunnelRecord({ provider: "ngrok" })).toBeNull();
|
|
194
|
+
expect(parseTunnelRecord({ publicBaseUrl: TUNNEL_URL })).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("rejects a provider outside the registry", () => {
|
|
198
|
+
// Readers render the provider into a `vellum tunnel --provider <name>`
|
|
199
|
+
// command, so a hand-edited config must not reach a terminal.
|
|
200
|
+
for (const provider of ["wireguard", "; rm -rf /", "", 42]) {
|
|
201
|
+
expect(
|
|
202
|
+
parseTunnelRecord({ provider, publicBaseUrl: TUNNEL_URL }),
|
|
203
|
+
).toBeNull();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("rejects a URL that is not absolute HTTP(S)", () => {
|
|
208
|
+
for (const publicBaseUrl of [
|
|
209
|
+
"",
|
|
210
|
+
" ",
|
|
211
|
+
"not-a-url",
|
|
212
|
+
"example.ts.net",
|
|
213
|
+
"ftp://x.test",
|
|
214
|
+
"https://one.ngrok.app?token=x",
|
|
215
|
+
42,
|
|
216
|
+
]) {
|
|
217
|
+
expect(
|
|
218
|
+
parseTunnelRecord({ provider: "ngrok", publicBaseUrl }),
|
|
219
|
+
).toBeNull();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("trimmedNonEmptyString", () => {
|
|
225
|
+
test("trims and rejects blank or non-string values", () => {
|
|
226
|
+
expect(trimmedNonEmptyString(" assistant-1 ")).toBe("assistant-1");
|
|
227
|
+
for (const value of ["", " ", undefined, null, 42]) {
|
|
228
|
+
expect(trimmedNonEmptyString(value)).toBeUndefined();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("parseRecordedAssistantId", () => {
|
|
234
|
+
test("trims a recorded id", () => {
|
|
235
|
+
expect(parseRecordedAssistantId(" assistant-1 ")).toBe("assistant-1");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("rejects blank and non-string values", () => {
|
|
239
|
+
for (const value of ["", " ", undefined, null, 42]) {
|
|
240
|
+
expect(parseRecordedAssistantId(value)).toBeNull();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|
package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts
CHANGED
|
@@ -48,3 +48,106 @@ export function normalizeHttpPublicBaseUrl(value: unknown): string | undefined {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The same validation as `normalizeHttpPublicBaseUrl`, minus the root path it
|
|
54
|
+
* always emits (`https://x` becomes `https://x/`). Callers that append a path
|
|
55
|
+
* or persist the result want this shape, so they neither join through a double
|
|
56
|
+
* slash nor re-strip what the normalizer just added.
|
|
57
|
+
*/
|
|
58
|
+
export function normalizeHttpPublicBaseUrlWithoutTrailingSlash(
|
|
59
|
+
value: unknown,
|
|
60
|
+
): string | undefined {
|
|
61
|
+
return normalizePublicBaseUrl(normalizeHttpPublicBaseUrl(value));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A trimmed string, or undefined when the value is not a non-blank string.
|
|
66
|
+
*
|
|
67
|
+
* Exported because the identity check in the daemon's tunnel probe compares an
|
|
68
|
+
* id recorded here against one an edge serves, and the two sides have to trim
|
|
69
|
+
* alike: a padded served id must read as a match, not as a different
|
|
70
|
+
* assistant.
|
|
71
|
+
*/
|
|
72
|
+
export function trimmedNonEmptyString(value: unknown): string | undefined {
|
|
73
|
+
if (typeof value !== "string") {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const trimmed = value.trim();
|
|
77
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Records `vellum tunnel` leaves in the workspace config's `ingress` section.
|
|
81
|
+
// The CLI writes them and the daemon reads them, and the two are separate
|
|
82
|
+
// build units that cannot import each other, so the key names, the provider
|
|
83
|
+
// allowlist, and the validation all live here rather than in a copy on each
|
|
84
|
+
// side.
|
|
85
|
+
|
|
86
|
+
/** Key under `ingress` holding the tunnel that most recently ran. */
|
|
87
|
+
export const INGRESS_LAST_TUNNEL_KEY = "lastTunnel";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Key under `ingress` holding a tunnel published for device pairing alone.
|
|
91
|
+
*
|
|
92
|
+
* A tailnet-only tunnel started while webhook integrations are configured
|
|
93
|
+
* cannot carry their callbacks, so it leaves `publicBaseUrl` (the callback
|
|
94
|
+
* base) as it is and records itself here. Readers that answer the pairing card
|
|
95
|
+
* prefer this address over `publicBaseUrl`; teardown drops the key.
|
|
96
|
+
*/
|
|
97
|
+
export const INGRESS_PAIRING_TUNNEL_KEY = "pairingTunnel";
|
|
98
|
+
|
|
99
|
+
/** Key under `ingress` holding the assistant id that tunnel fronted. */
|
|
100
|
+
export const INGRESS_ASSISTANT_ID_KEY = "assistantId";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The local tunnel providers the CLI can start, in the order they are offered.
|
|
104
|
+
* `vellum tunnel`'s accepted `--provider` values and the persisted-record
|
|
105
|
+
* validation below both derive from it.
|
|
106
|
+
*/
|
|
107
|
+
export const TUNNEL_PROVIDERS = ["ngrok", "cloudflare", "tailscale"] as const;
|
|
108
|
+
|
|
109
|
+
export type TunnelProviderName = (typeof TUNNEL_PROVIDERS)[number];
|
|
110
|
+
|
|
111
|
+
/** A recorded tunnel, kept after teardown so UIs can name the command to restart. */
|
|
112
|
+
export interface TunnelRecord {
|
|
113
|
+
provider: TunnelProviderName;
|
|
114
|
+
publicBaseUrl: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isTunnelProviderName(value: unknown): value is TunnelProviderName {
|
|
118
|
+
return (
|
|
119
|
+
typeof value === "string" &&
|
|
120
|
+
(TUNNEL_PROVIDERS as readonly string[]).includes(value)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parse an `ingress.lastTunnel` or `ingress.pairingTunnel` value, or null when
|
|
126
|
+
* it is absent or malformed.
|
|
127
|
+
*
|
|
128
|
+
* A hand-edited config must not hand callers an unusable address or a provider
|
|
129
|
+
* name no command accepts: the provider is checked against the allowlist
|
|
130
|
+
* because readers render it into a restart command, and the URL faces the same
|
|
131
|
+
* absolute HTTP(S) constraint as every other public base URL. The URL comes
|
|
132
|
+
* back in the shape the validator produced, so readers never re-normalize it.
|
|
133
|
+
*/
|
|
134
|
+
export function parseTunnelRecord(value: unknown): TunnelRecord | null {
|
|
135
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const { provider, publicBaseUrl } = value as Record<string, unknown>;
|
|
139
|
+
if (!isTunnelProviderName(provider)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const normalized =
|
|
143
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(publicBaseUrl);
|
|
144
|
+
if (normalized === undefined) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return { provider, publicBaseUrl: normalized };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Parse an `ingress.assistantId` value, or null when it is absent or blank. */
|
|
151
|
+
export function parseRecordedAssistantId(value: unknown): string | null {
|
|
152
|
+
return trimmedNonEmptyString(value) ?? null;
|
|
153
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildShellInvocation,
|
|
5
|
+
pathListDelimiter,
|
|
6
|
+
prependUniquePathEntries,
|
|
7
|
+
} from "./shell.js";
|
|
8
|
+
|
|
9
|
+
describe("buildShellInvocation", () => {
|
|
10
|
+
test("uses Bash on POSIX hosts", () => {
|
|
11
|
+
expect(buildShellInvocation("printf hello", "linux")).toEqual({
|
|
12
|
+
command: "bash",
|
|
13
|
+
args: ["-c", "--", "printf hello"],
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("uses encoded non-interactive PowerShell on Windows", () => {
|
|
18
|
+
const invocation = buildShellInvocation(
|
|
19
|
+
"Write-Output 'hello 世界'",
|
|
20
|
+
"win32",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(invocation.command).toBe("powershell.exe");
|
|
24
|
+
expect(invocation.args.slice(0, -1)).toEqual([
|
|
25
|
+
"-NoProfile",
|
|
26
|
+
"-NonInteractive",
|
|
27
|
+
"-ExecutionPolicy",
|
|
28
|
+
"Bypass",
|
|
29
|
+
"-EncodedCommand",
|
|
30
|
+
]);
|
|
31
|
+
const encoded = invocation.args.at(-1)!;
|
|
32
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf16le");
|
|
33
|
+
expect(decoded).toContain("[Console]::OutputEncoding");
|
|
34
|
+
expect(decoded).toContain("$global:LASTEXITCODE = 0");
|
|
35
|
+
expect(decoded).toContain("Write-Output 'hello 世界'");
|
|
36
|
+
expect(decoded).toContain("exit $__vellumNativeExitCode");
|
|
37
|
+
expect(decoded).toEndWith("exit 0");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("path list handling", () => {
|
|
42
|
+
test("uses the platform delimiter", () => {
|
|
43
|
+
expect(pathListDelimiter("win32")).toBe(";");
|
|
44
|
+
expect(pathListDelimiter("linux")).toBe(":");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("prepends Windows paths without splitting drive letters", () => {
|
|
48
|
+
expect(
|
|
49
|
+
prependUniquePathEntries(
|
|
50
|
+
"C:\\Windows\\System32;C:\\Tools",
|
|
51
|
+
["C:\\Tools", "C:\\Users\\Alice\\.bun\\bin"],
|
|
52
|
+
"win32",
|
|
53
|
+
),
|
|
54
|
+
).toBe("C:\\Users\\Alice\\.bun\\bin;C:\\Windows\\System32;C:\\Tools");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { delimiter } from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface ShellInvocation {
|
|
4
|
+
command: string;
|
|
5
|
+
args: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const WINDOWS_UTF8_PREAMBLE =
|
|
9
|
+
"try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}; " +
|
|
10
|
+
"$OutputEncoding = [System.Text.Encoding]::UTF8; " +
|
|
11
|
+
"$global:LASTEXITCODE = 0;";
|
|
12
|
+
|
|
13
|
+
const WINDOWS_EXIT_STATUS_SUFFIX = `
|
|
14
|
+
$__vellumCommandSucceeded = $?
|
|
15
|
+
$__vellumNativeExitCode = $LASTEXITCODE
|
|
16
|
+
if (-not $__vellumCommandSucceeded) {
|
|
17
|
+
if ($__vellumNativeExitCode -ne 0) { exit $__vellumNativeExitCode }
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
exit 0`;
|
|
21
|
+
|
|
22
|
+
export function buildShellInvocation(
|
|
23
|
+
command: string,
|
|
24
|
+
hostPlatform: NodeJS.Platform = process.platform,
|
|
25
|
+
windowsExecutable = "powershell.exe",
|
|
26
|
+
): ShellInvocation {
|
|
27
|
+
if (hostPlatform === "win32") {
|
|
28
|
+
return {
|
|
29
|
+
command: windowsExecutable,
|
|
30
|
+
args: [
|
|
31
|
+
"-NoProfile",
|
|
32
|
+
"-NonInteractive",
|
|
33
|
+
"-ExecutionPolicy",
|
|
34
|
+
"Bypass",
|
|
35
|
+
"-EncodedCommand",
|
|
36
|
+
Buffer.from(
|
|
37
|
+
`${WINDOWS_UTF8_PREAMBLE}\n${command}${WINDOWS_EXIT_STATUS_SUFFIX}`,
|
|
38
|
+
"utf16le",
|
|
39
|
+
).toString("base64"),
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { command: "bash", args: ["-c", "--", command] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function pathListDelimiter(
|
|
47
|
+
hostPlatform: NodeJS.Platform = process.platform,
|
|
48
|
+
): string {
|
|
49
|
+
return hostPlatform === process.platform
|
|
50
|
+
? delimiter
|
|
51
|
+
: hostPlatform === "win32"
|
|
52
|
+
? ";"
|
|
53
|
+
: ":";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function prependUniquePathEntries(
|
|
57
|
+
value: string | undefined,
|
|
58
|
+
entries: readonly string[],
|
|
59
|
+
hostPlatform: NodeJS.Platform = process.platform,
|
|
60
|
+
): string {
|
|
61
|
+
const separator = pathListDelimiter(hostPlatform);
|
|
62
|
+
const current = value?.split(separator).filter(Boolean) ?? [];
|
|
63
|
+
const missing = entries.filter((entry) => !current.includes(entry));
|
|
64
|
+
return [...missing, ...current].join(separator);
|
|
65
|
+
}
|
|
@@ -2,7 +2,12 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
normalizeHttpPublicBaseUrl,
|
|
5
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash,
|
|
5
6
|
normalizePublicBaseUrl,
|
|
7
|
+
parseRecordedAssistantId,
|
|
8
|
+
parseTunnelRecord,
|
|
9
|
+
trimmedNonEmptyString,
|
|
10
|
+
TUNNEL_PROVIDERS,
|
|
6
11
|
velayHostForPlatformHost,
|
|
7
12
|
} from "../ingress.js";
|
|
8
13
|
import {
|
|
@@ -85,6 +90,35 @@ describe("normalizeHttpPublicBaseUrl", () => {
|
|
|
85
90
|
});
|
|
86
91
|
});
|
|
87
92
|
|
|
93
|
+
describe("normalizeHttpPublicBaseUrlWithoutTrailingSlash", () => {
|
|
94
|
+
test("drops the root path its sibling always emits", () => {
|
|
95
|
+
expect(
|
|
96
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash("https://x.test"),
|
|
97
|
+
).toBe("https://x.test");
|
|
98
|
+
expect(
|
|
99
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash("https://x.test/"),
|
|
100
|
+
).toBe("https://x.test");
|
|
101
|
+
expect(
|
|
102
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(" https://x.test/api/// "),
|
|
103
|
+
).toBe("https://x.test/api");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("rejects everything its sibling rejects", () => {
|
|
107
|
+
for (const value of [
|
|
108
|
+
"",
|
|
109
|
+
" ",
|
|
110
|
+
"notaurl",
|
|
111
|
+
"ftp://x.test",
|
|
112
|
+
"https://x.test?a=b",
|
|
113
|
+
42,
|
|
114
|
+
]) {
|
|
115
|
+
expect(
|
|
116
|
+
normalizeHttpPublicBaseUrlWithoutTrailingSlash(value),
|
|
117
|
+
).toBeUndefined();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
88
122
|
describe("Twilio ingress helpers", () => {
|
|
89
123
|
test("resolves public base URL with fallback", () => {
|
|
90
124
|
expect(
|
|
@@ -123,3 +157,87 @@ describe("Twilio ingress helpers", () => {
|
|
|
123
157
|
});
|
|
124
158
|
});
|
|
125
159
|
});
|
|
160
|
+
|
|
161
|
+
describe("parseTunnelRecord", () => {
|
|
162
|
+
const TUNNEL_URL = "https://assistant-1.example.ts.net";
|
|
163
|
+
|
|
164
|
+
test("accepts every provider in the registry", () => {
|
|
165
|
+
for (const provider of TUNNEL_PROVIDERS) {
|
|
166
|
+
expect(
|
|
167
|
+
parseTunnelRecord({ provider, publicBaseUrl: TUNNEL_URL }),
|
|
168
|
+
).toEqual({ provider, publicBaseUrl: TUNNEL_URL });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("returns the URL in the shape its own validator produces", () => {
|
|
173
|
+
// Padding and trailing slashes are normalized away rather than handed
|
|
174
|
+
// back, so readers never re-normalize what they were given.
|
|
175
|
+
for (const publicBaseUrl of [
|
|
176
|
+
` ${TUNNEL_URL} `,
|
|
177
|
+
`${TUNNEL_URL}/`,
|
|
178
|
+
`${TUNNEL_URL}///`,
|
|
179
|
+
]) {
|
|
180
|
+
expect(
|
|
181
|
+
parseTunnelRecord({ provider: "ngrok", publicBaseUrl }),
|
|
182
|
+
).toEqual({ provider: "ngrok", publicBaseUrl: TUNNEL_URL });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("rejects values that are not a record", () => {
|
|
187
|
+
for (const value of [undefined, null, "nonsense", 42, []]) {
|
|
188
|
+
expect(parseTunnelRecord(value)).toBeNull();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("rejects a record missing either field", () => {
|
|
193
|
+
expect(parseTunnelRecord({ provider: "ngrok" })).toBeNull();
|
|
194
|
+
expect(parseTunnelRecord({ publicBaseUrl: TUNNEL_URL })).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("rejects a provider outside the registry", () => {
|
|
198
|
+
// Readers render the provider into a `vellum tunnel --provider <name>`
|
|
199
|
+
// command, so a hand-edited config must not reach a terminal.
|
|
200
|
+
for (const provider of ["wireguard", "; rm -rf /", "", 42]) {
|
|
201
|
+
expect(
|
|
202
|
+
parseTunnelRecord({ provider, publicBaseUrl: TUNNEL_URL }),
|
|
203
|
+
).toBeNull();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("rejects a URL that is not absolute HTTP(S)", () => {
|
|
208
|
+
for (const publicBaseUrl of [
|
|
209
|
+
"",
|
|
210
|
+
" ",
|
|
211
|
+
"not-a-url",
|
|
212
|
+
"example.ts.net",
|
|
213
|
+
"ftp://x.test",
|
|
214
|
+
"https://one.ngrok.app?token=x",
|
|
215
|
+
42,
|
|
216
|
+
]) {
|
|
217
|
+
expect(
|
|
218
|
+
parseTunnelRecord({ provider: "ngrok", publicBaseUrl }),
|
|
219
|
+
).toBeNull();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("trimmedNonEmptyString", () => {
|
|
225
|
+
test("trims and rejects blank or non-string values", () => {
|
|
226
|
+
expect(trimmedNonEmptyString(" assistant-1 ")).toBe("assistant-1");
|
|
227
|
+
for (const value of ["", " ", undefined, null, 42]) {
|
|
228
|
+
expect(trimmedNonEmptyString(value)).toBeUndefined();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("parseRecordedAssistantId", () => {
|
|
234
|
+
test("trims a recorded id", () => {
|
|
235
|
+
expect(parseRecordedAssistantId(" assistant-1 ")).toBe("assistant-1");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("rejects blank and non-string values", () => {
|
|
239
|
+
for (const value of ["", " ", undefined, null, 42]) {
|
|
240
|
+
expect(parseRecordedAssistantId(value)).toBeNull();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
});
|