@vellumai/assistant 0.11.6 → 0.11.7-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +31 -19
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +3 -3
- package/docs/flux-turn-detection-spike.md +8 -7
- package/docs/runbook-trusted-contacts.md +14 -0
- package/docs/stt-provider-onboarding.md +8 -4
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-manifest/package.json +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +173 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/read.test.ts +114 -0
- package/node_modules/@vellumai/avatar-manifest/src/index.ts +29 -0
- package/node_modules/@vellumai/avatar-manifest/src/layout.ts +18 -0
- package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +156 -0
- package/node_modules/@vellumai/avatar-manifest/src/read.ts +109 -0
- package/node_modules/@vellumai/avatar-manifest/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/ces-client/src/http-credentials.ts +161 -0
- package/node_modules/@vellumai/ces-client/src/index.ts +1 -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 +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +32 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +82 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +56 -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 +5 -11
- package/node_modules/@vellumai/gateway-client/src/channel-permission-contract.ts +3 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +9 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +4 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +75 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +17 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +8 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +47 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +11 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/remote-web-pairing.test.ts +389 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +22 -0
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +508 -18
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +174 -0
- package/openapi.yaml +419 -55
- package/package.json +4 -1
- package/scripts/sync-llm-catalog.ts +6 -0
- package/src/__tests__/approval-card-edit-inbound-handoff.test.ts +199 -0
- package/src/__tests__/approval-card-edit.test.ts +153 -0
- package/src/__tests__/attachments-store-heic-normalize.test.ts +1 -1
- package/src/__tests__/available-skills.test.ts +54 -0
- package/src/__tests__/avatar-identity-sync.test.ts +5 -1
- package/src/__tests__/catalog-install-normalize.test.ts +9 -1
- package/src/__tests__/channel-policy.test.ts +1 -3
- package/src/__tests__/channel-readiness-discord.test.ts +9 -70
- 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 +2 -3
- package/src/__tests__/conversation-agent-loop.test.ts +238 -0
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +44 -0
- package/src/__tests__/conversation-evictor.test.ts +19 -0
- package/src/__tests__/conversation-fork-referential.test.ts +73 -63
- package/src/__tests__/conversation-fork-retrospective.test.ts +99 -74
- package/src/__tests__/conversation-runtime-assembly.test.ts +23 -0
- package/src/__tests__/conversation-slash.test.ts +9 -0
- package/src/__tests__/conversation-title-service.test.ts +97 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +16 -0
- package/src/__tests__/credential-broker-server-use.test.ts +0 -218
- package/src/__tests__/credential-broker.test.ts +43 -420
- package/src/__tests__/credential-record-write-through.test.ts +78 -0
- package/src/__tests__/credential-routes.test.ts +17 -1
- package/src/__tests__/credential-security-invariants.test.ts +8 -9
- package/src/__tests__/default-profile-catalog-fallback.test.ts +289 -0
- package/src/__tests__/delete-propagation.test.ts +92 -2
- package/src/__tests__/discord-callback-round-trip.test.ts +93 -0
- package/src/__tests__/discord-channel-config.test.ts +237 -0
- package/src/__tests__/edit-propagation.test.ts +42 -2
- package/src/__tests__/evict-conversations-for-reload.test.ts +51 -1
- package/src/__tests__/fallback-breaker.test.ts +1335 -0
- package/src/__tests__/gateway-threshold-reader-mock.ts +6 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +30 -0
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -2
- 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 +4 -0
- package/src/__tests__/llm-resolver.test.ts +74 -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__/managed-speech-defaults.test.ts +196 -1
- package/src/__tests__/mcp-cli.test.ts +23 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +43 -34
- package/src/__tests__/media-stream-stt-session.test.ts +2 -0
- package/src/__tests__/notification-decision-fallback.test.ts +106 -0
- package/src/__tests__/notification-discord-adapter.test.ts +162 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +95 -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.test.ts +19 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -1
- package/src/__tests__/provider-catalog-visibility.test.ts +17 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +8 -1
- package/src/__tests__/provider-usage-tracking.test.ts +56 -0
- package/src/__tests__/require-fresh-approval.test.ts +5 -2
- package/src/__tests__/retry-fallback-escalation.test.ts +1093 -0
- package/src/__tests__/schedule-retry.test.ts +183 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +6 -6
- package/src/__tests__/secret-routes-platform-proxy.test.ts +55 -0
- package/src/__tests__/skill-load-tool.test.ts +34 -0
- package/src/__tests__/skill-tool-factory.test.ts +6 -1
- package/src/__tests__/skill-tool-manifest.test.ts +20 -0
- package/src/__tests__/skills.test.ts +25 -0
- package/src/__tests__/subagent-manager-notify.test.ts +38 -0
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +146 -2
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +108 -2
- package/src/__tests__/tool-audit.test.ts +111 -40
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +228 -18
- package/src/__tests__/ui-shape-teaching.test.ts +1 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +6 -2
- package/src/__tests__/voice-config-update.test.ts +43 -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/loop.ts +31 -2
- package/src/approvals/guardian-card-withdrawal.ts +22 -0
- package/src/avatar/__tests__/ensure-raster.test.ts +291 -0
- package/src/avatar/avatar-manifest.ts +48 -100
- package/src/avatar/avatar-store.ts +7 -7
- package/src/avatar/ensure-raster.ts +146 -0
- package/src/avatar/resvg-lazy.test.ts +0 -1
- package/src/avatar/traits-png-sync.ts +9 -10
- package/src/background-wake/wake-intent-hooks.test.ts +0 -1
- 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/calls/__tests__/telephony-synthesis-language.test.ts +81 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +57 -0
- package/src/calls/media-stream-stt-session.ts +2 -1
- package/src/calls/telephony-synthesis-language.ts +13 -5
- package/src/calls/voice-session-bridge.ts +19 -4
- package/src/channels/config.ts +23 -12
- package/src/cli/AGENTS.md +3 -1
- package/src/cli/commands/backup.help.ts +10 -13
- package/src/cli/commands/contacts.help.ts +16 -4
- package/src/cli/commands/contacts.ts +5 -1
- package/src/cli/commands/credentials.help.ts +1 -1
- package/src/cli/commands/inference-providers.ts +18 -53
- package/src/cli/commands/mcp.help.ts +6 -5
- package/src/cli/commands/mcp.ts +1 -1
- package/src/cli/commands/memory/memory-retrospective.ts +1 -0
- package/src/cli/commands/stt.help.ts +2 -2
- package/src/config/__tests__/backup-schema.test.ts +1 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +83 -2
- package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +1 -1
- package/src/config/__tests__/memory-retrospective-schema.test.ts +12 -15
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -6
- package/src/config/bundled-skills/computer-use/TOOLS.json +9 -6
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +3 -1
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +16 -15
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +2 -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/bundled-skills/transcribe/tools/transcribe-media.ts +1 -1
- 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 +34 -2
- package/src/config/inference-profile-validation.ts +11 -2
- package/src/config/llm-resolver.ts +25 -0
- package/src/config/loader.ts +3 -0
- package/src/config/managed-speech-defaults.ts +160 -10
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +7 -6
- package/src/config/schemas/__tests__/stt.test.ts +94 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/channels.ts +32 -0
- package/src/config/schemas/live-voice.ts +13 -4
- package/src/config/schemas/llm.ts +263 -15
- package/src/config/schemas/mcp.ts +10 -2
- package/src/config/schemas/memory-retrospective.ts +1 -19
- package/src/config/schemas/services.ts +24 -0
- package/src/config/schemas/stt.ts +128 -5
- package/src/config/seed-inference-profiles.ts +32 -3
- package/src/config/skill-state.ts +5 -0
- package/src/config/skills.ts +21 -0
- package/src/contacts/notify-contacts-changed.ts +5 -2
- package/src/context/compactor.ts +74 -1
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +138 -1
- package/src/daemon/config-watcher.ts +2 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +27 -0
- package/src/daemon/conversation-agent-loop.ts +86 -10
- package/src/daemon/conversation-error.ts +25 -0
- package/src/daemon/conversation-evictor.ts +2 -4
- package/src/daemon/conversation-runtime-assembly.ts +19 -7
- package/src/daemon/conversation-skill-tools.ts +5 -1
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-store.ts +8 -1
- package/src/daemon/conversation-tool-setup.ts +37 -0
- package/src/daemon/conversation.ts +0 -4
- package/src/daemon/handlers/channel-config-result.ts +30 -0
- package/src/daemon/handlers/config-discord-channel.ts +301 -0
- package/src/daemon/handlers/config-ingress.ts +9 -0
- package/src/daemon/handlers/config-slack-channel.ts +13 -15
- package/src/daemon/handlers/config-telegram.ts +2 -5
- package/src/daemon/handlers/conversations.ts +0 -2
- package/src/daemon/handlers/skills.ts +25 -7
- package/src/daemon/lifecycle.ts +3 -1
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/tool-setup-types.ts +5 -3
- package/src/daemon/trust-context-types.test.ts +33 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +58 -4
- package/src/live-voice/__tests__/live-voice-managed-speech-fallback.test.ts +8 -3
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +73 -11
- package/src/live-voice/__tests__/live-voice-session-preflight.test.ts +148 -2
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +64 -0
- package/src/live-voice/__tests__/live-voice-stt.test.ts +3 -0
- package/src/live-voice/__tests__/live-voice-text-turn.test.ts +391 -0
- package/src/live-voice/__tests__/protocol.test.ts +224 -0
- package/src/live-voice/live-voice-credential-preflight.ts +5 -1
- package/src/live-voice/live-voice-session-manager.ts +39 -2
- package/src/live-voice/live-voice-session.ts +224 -9
- package/src/live-voice/protocol.ts +187 -1
- package/src/messaging/providers/discord/api.ts +2 -1
- package/src/messaging/providers/discord/withdraw.ts +62 -0
- package/src/messaging/read-provider-metadata.ts +32 -0
- package/src/monitoring/__tests__/file-descriptors.test.ts +32 -0
- package/src/monitoring/__tests__/process-memory.test.ts +34 -1
- package/src/monitoring/db-integrity-sample.ts +1 -0
- 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__/activity-failed-dedupe.test.ts +161 -0
- package/src/notifications/__tests__/background-failure-signal.test.ts +107 -0
- package/src/notifications/__tests__/connected-channels.test.ts +12 -0
- package/src/notifications/__tests__/copy-composer.test.ts +83 -0
- package/src/notifications/__tests__/destination-resolver.test.ts +38 -0
- package/src/notifications/activity-failed-dedupe.ts +95 -0
- package/src/notifications/adapters/discord.ts +132 -0
- package/src/notifications/adapters/shared.ts +16 -0
- package/src/notifications/adapters/slack.ts +6 -1
- package/src/notifications/adapters/telegram.ts +5 -8
- package/src/notifications/approval-card-data.ts +94 -1
- package/src/notifications/background-failure-signal.ts +116 -0
- package/src/notifications/broadcaster.ts +3 -9
- package/src/notifications/copy-composer.ts +41 -8
- package/src/notifications/decision-engine.ts +55 -0
- package/src/notifications/destination-resolver.ts +45 -5
- package/src/notifications/emit-signal.ts +19 -5
- package/src/notifications/guardian-question-mode.ts +57 -0
- package/src/notifications/types.ts +2 -8
- package/src/oauth/__tests__/seed-providers-managed.test.ts +18 -0
- package/src/oauth/connect-orchestrator.ts +16 -0
- package/src/oauth/seed-providers.ts +262 -0
- package/src/permissions/checker.test.ts +21 -0
- package/src/permissions/checker.ts +6 -0
- package/src/permissions/gateway-threshold-reader.test.ts +184 -0
- package/src/permissions/gateway-threshold-reader.ts +144 -10
- package/src/permissions/prompter.ts +0 -6
- package/src/permissions/types.ts +5 -4
- package/src/persistence/__tests__/bulk-write-gate.test.ts +0 -38
- package/src/persistence/bulk-write-gate.ts +9 -10
- package/src/persistence/conversation-crud.ts +88 -134
- package/src/persistence/conversation-title-service.ts +43 -29
- package/src/persistence/embeddings/embedding-local.ts +14 -85
- package/src/persistence/embeddings/embedding-runtime-manager.ts +1 -0
- package/src/persistence/lifecycle-events-store.test.ts +50 -0
- package/src/persistence/lifecycle-events-store.ts +75 -5
- package/src/platform/platform-patch-queue.test.ts +368 -0
- package/src/platform/platform-patch-queue.ts +190 -0
- package/src/platform/sync-avatar.test.ts +527 -0
- package/src/platform/sync-avatar.ts +209 -0
- package/src/platform/sync-identity.test.ts +75 -0
- package/src/platform/sync-identity.ts +17 -110
- package/src/plugin-api/model-profiles.test.ts +2 -2
- package/src/plugin-api/model-profiles.ts +7 -7
- package/src/plugin-api/transcription-session.ts +1 -1
- 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 +133 -34
- 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-fork-boundary.ts +27 -13
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +183 -113
- 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/substrate/__tests__/consolidation-job.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +4 -2
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +9 -42
- package/src/plugins/defaults/memory/v2/rerank-local.ts +1 -0
- package/src/plugins/defaults/memory/worker.ts +7 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +55 -0
- package/src/providers/__tests__/vellum-connection-routing.test.ts +21 -3
- package/src/providers/connection-resolution.ts +2 -17
- package/src/providers/fallback-breaker.ts +389 -0
- package/src/providers/inference/__tests__/adapter-factory-opencode.test.ts +92 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +16 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +12 -0
- package/src/providers/inference/adapter-factory.ts +246 -3
- package/src/providers/inference/auth.ts +15 -14
- package/src/providers/inference/chatgpt-subscription-auth.ts +70 -0
- package/src/providers/inference/chatgpt-subscription-credentials.ts +84 -0
- package/src/providers/inference/resolve-auth.ts +1 -1
- package/src/providers/media-resolve.ts +2 -3
- package/src/providers/model-catalog.ts +100 -0
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +22 -0
- package/src/providers/openai/__tests__/chat-template-flatten.test.ts +198 -0
- package/src/providers/openai/api-error-normalization.ts +13 -0
- package/src/providers/openai/chat-completions-provider.ts +84 -0
- package/src/providers/opencode/client.test.ts +150 -0
- package/src/providers/opencode/client.ts +61 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/provider-resolvability.ts +20 -0
- package/src/providers/provider-secret-catalog.ts +3 -2
- package/src/providers/retry.ts +956 -68
- 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 +316 -12
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +83 -3
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +132 -4
- package/src/providers/speech-to-text/provider-catalog.ts +191 -3
- package/src/providers/speech-to-text/resolve.ts +132 -24
- package/src/providers/speech-to-text/vellum-managed-flux-realtime.ts +296 -0
- package/src/providers/types.ts +39 -0
- package/src/providers/usage-tracking.ts +12 -1
- package/src/providers/vellum/client.ts +29 -0
- package/src/providers/vellum-model-routing.test.ts +2 -0
- package/src/providers/vellum-model-routing.ts +3 -3
- package/src/runtime/AGENTS.md +1 -2
- package/src/runtime/__tests__/agent-wake.test.ts +105 -3
- package/src/runtime/__tests__/background-job-runner.test.ts +89 -3
- package/src/runtime/agent-wake.ts +44 -5
- package/src/runtime/background-job-runner.ts +68 -50
- package/src/runtime/channel-readiness-service.ts +0 -39
- package/src/runtime/http-server.ts +12 -4
- package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +18 -18
- package/src/runtime/routes/__tests__/backup-routes.test.ts +9 -5
- package/src/runtime/routes/__tests__/chatgpt-subscription-auth-routes.test.ts +372 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +96 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +37 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +2 -1
- package/src/runtime/routes/__tests__/mcp-add-default-risk.test.ts +87 -0
- package/src/runtime/routes/acp-claude-auth-routes.ts +25 -54
- package/src/runtime/routes/approval-routes.ts +0 -12
- package/src/runtime/routes/avatar-routes.ts +17 -30
- package/src/runtime/routes/backup-routes.ts +2 -2
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +296 -113
- package/src/runtime/routes/contact-prompt-routes.ts +65 -6
- package/src/runtime/routes/contact-routes.ts +20 -15
- package/src/runtime/routes/conversation-query-routes.ts +98 -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 +12 -9
- package/src/runtime/routes/inbound-message-handler.ts +47 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +18 -13
- package/src/runtime/routes/inbound-stages/admission-policy.ts +2 -2
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +20 -2
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +9 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -1
- 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 +12 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +2 -2
- package/src/runtime/routes/inference-send-routes.ts +2 -2
- package/src/runtime/routes/ingress-status-routes.ts +2 -2
- package/src/runtime/routes/integrations/discord.ts +119 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +2 -2
- package/src/runtime/routes/mcp-auth-routes.ts +12 -6
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/oauth-pending-flows.ts +136 -0
- package/src/runtime/routes/secret-routes.ts +7 -0
- package/src/runtime/routes/stt-routes.ts +38 -6
- package/src/runtime/routes/wake-conversation-routes.ts +8 -1
- package/src/runtime/routes/watch-routes.ts +9 -5
- package/src/runtime/sync/resource-sync-events.ts +2 -0
- package/src/runtime/trust-verdict-consumer.ts +3 -3
- package/src/schedule/__tests__/worker-feature-flags.test.ts +58 -0
- package/src/schedule/retry-policy.ts +8 -8
- package/src/schedule/run-script.ts +14 -23
- package/src/schedule/schedule-recovery.ts +0 -1
- package/src/schedule/scheduler.ts +89 -51
- package/src/schedule/worker.ts +57 -0
- package/src/security/__tests__/openai-device-auth.test.ts +355 -0
- package/src/security/ces-rpc-record-backend.ts +123 -0
- package/src/security/oauth2.ts +7 -0
- package/src/security/openai-device-auth.ts +364 -0
- package/src/security/secure-keys.ts +15 -0
- 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/skills/tool-manifest.ts +20 -0
- package/src/stt/__tests__/roles.test.ts +152 -0
- package/src/stt/daemon-batch-transcriber.ts +4 -1
- package/src/stt/roles.ts +180 -0
- package/src/stt/stt-stream-session.ts +3 -2
- package/src/stt/types.ts +33 -3
- package/src/subagent/manager.ts +12 -0
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +34 -0
- package/src/telemetry/live-voice-funnel.ts +12 -2
- package/src/telemetry/telemetry-wire.generated.ts +6 -0
- package/src/telemetry/tool-audit.ts +83 -30
- package/src/telemetry/tool-executed-events-store.test.ts +0 -1
- package/src/telemetry/turn-outcome.ts +75 -4
- package/src/tools/client-os.ts +115 -0
- package/src/tools/computer-use/definitions.ts +11 -7
- package/src/tools/computer-use/skill-proxy-bridge.ts +2 -2
- package/src/tools/credentials/broker-types.ts +0 -71
- package/src/tools/credentials/broker.ts +6 -201
- package/src/tools/credentials/metadata-store.ts +65 -16
- package/src/tools/credentials/store.ts +2 -0
- package/src/tools/executor.ts +2 -8
- package/src/tools/host-shell.test.ts +75 -0
- package/src/tools/host-shell.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +15 -44
- package/src/tools/mcp/mcp-tool-factory.ts +44 -9
- package/src/tools/permission-checker.ts +73 -21
- package/src/tools/policy-context.ts +1 -0
- 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 +41 -6
- package/src/tools/skills/sandbox-runner.ts +8 -16
- package/src/tools/skills/skill-tool-factory.ts +1 -0
- 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/tool-approval-handler.ts +64 -10
- package/src/tools/tool-types.ts +10 -0
- package/src/tools/types.ts +19 -3
- package/src/tools/ui-surface/surface-shape-docs.ts +4 -3
- 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/platform.ts +32 -13
- 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/watch/__tests__/watch-retro.test.ts +48 -1
- package/src/watch/watch-retro.ts +46 -4
- 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/src/__tests__/permission-types.test.ts +0 -17
- package/src/channels/gateway-discord-admission.ts +0 -32
- package/src/persistence/fork-message-copy.ts +0 -239
- package/src/plugins/defaults/memory/__tests__/fork-message-copy.test.ts +0 -221
- package/src/security/__tests__/oauth2-device-code.test.ts +0 -431
- package/src/security/oauth2-device-code.ts +0 -312
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* `gateway/src/remote-web/pairing-challenge-store.ts`)
|
|
10
10
|
* - `POST /v1/remote-web/pairing-verification` approve by user code
|
|
11
11
|
* (`gateway/src/http/routes/remote-web-pairing-verification.ts`)
|
|
12
|
+
* (loopback-only)
|
|
12
13
|
* - `POST /v1/remote-web/pairing-token` poll + exchange device code
|
|
13
|
-
* (`gateway/src/http/routes/remote-web-pairing-token.ts`)
|
|
14
|
+
* (`gateway/src/http/routes/remote-web-pairing-token.ts`): a browser
|
|
15
|
+
* receives its refresh token as an `HttpOnly` cookie; a request that
|
|
16
|
+
* carries a `deviceId` receives a device-bound one in the body instead
|
|
14
17
|
* - `GET /v1/remote-web/pairing-requests` list pending challenges
|
|
15
18
|
* (loopback-only)
|
|
16
19
|
* - `POST /v1/remote-web/pairing-requests/approve` approve by request id
|
|
@@ -19,7 +22,8 @@
|
|
|
19
22
|
* (loopback-only)
|
|
20
23
|
*
|
|
21
24
|
* These shapes mirror those handlers' request/response bodies exactly so the
|
|
22
|
-
* gateway, the `vellum pair` CLI (`cli/src/commands/pair.ts`),
|
|
25
|
+
* gateway, the `vellum pair` CLI (`cli/src/commands/pair.ts`), the host-side
|
|
26
|
+
* pairing sessions (`packages/local-mode/src/pair.ts`), and the web SPA
|
|
23
27
|
* (`clients/web/src/lib/auth/remote-gateway-session.ts`) share one definition
|
|
24
28
|
* and cannot silently drift.
|
|
25
29
|
*
|
|
@@ -128,10 +132,57 @@ export interface RemoteWebPairingRequestDenyResponse {
|
|
|
128
132
|
status: "denied";
|
|
129
133
|
}
|
|
130
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Platforms a device-bound pairing exchange may declare for itself. The value
|
|
137
|
+
* is what the host's `Paired devices` list renders for the new pairing.
|
|
138
|
+
*/
|
|
139
|
+
export const REMOTE_WEB_PAIRING_PLATFORMS = [
|
|
140
|
+
"cli",
|
|
141
|
+
"desktop",
|
|
142
|
+
"ios",
|
|
143
|
+
"android",
|
|
144
|
+
] as const;
|
|
145
|
+
|
|
146
|
+
export type RemoteWebPairingPlatform =
|
|
147
|
+
(typeof REMOTE_WEB_PAIRING_PLATFORMS)[number];
|
|
148
|
+
|
|
149
|
+
/** Platform recorded when an exchange names none the gateway recognizes. */
|
|
150
|
+
export const DEFAULT_REMOTE_WEB_PAIRING_PLATFORM: RemoteWebPairingPlatform =
|
|
151
|
+
"desktop";
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A requested platform coerced to a known value. The pairing-token route is
|
|
155
|
+
* publicly reachable and the platform renders verbatim in the host's
|
|
156
|
+
* paired-devices list, so an unrecognized value never reaches the DB.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveRemoteWebPairingPlatform(
|
|
159
|
+
raw: unknown,
|
|
160
|
+
): RemoteWebPairingPlatform {
|
|
161
|
+
return (
|
|
162
|
+
REMOTE_WEB_PAIRING_PLATFORMS.find((platform) => platform === raw) ??
|
|
163
|
+
DEFAULT_REMOTE_WEB_PAIRING_PLATFORM
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
/** `POST /v1/remote-web/pairing-token` request body. */
|
|
132
168
|
export interface RemoteWebPairingTokenRequest {
|
|
133
169
|
/** The `deviceCode` from the challenge. */
|
|
134
170
|
deviceCode: string;
|
|
171
|
+
/**
|
|
172
|
+
* Client-generated device id, sent only by a trusted host completing the
|
|
173
|
+
* exchange for itself. It switches the gateway to a device-bound, per-device
|
|
174
|
+
* revocable credential whose refresh token comes back in the response body.
|
|
175
|
+
* Browsers omit it and keep the cookie delivery.
|
|
176
|
+
*/
|
|
177
|
+
deviceId?: string;
|
|
178
|
+
/** Platform to record for the device. Ignored without a `deviceId`. */
|
|
179
|
+
platform?: RemoteWebPairingPlatform;
|
|
180
|
+
/**
|
|
181
|
+
* Optional self-reported device name, for example "Vellum iOS app". The gateway
|
|
182
|
+
* stores it verbatim (capped) and never trusts it for authorization. Absent from
|
|
183
|
+
* clients that predate this field.
|
|
184
|
+
*/
|
|
185
|
+
clientReportedName?: string;
|
|
135
186
|
}
|
|
136
187
|
|
|
137
188
|
/**
|
|
@@ -148,8 +199,9 @@ export interface RemoteWebPairingTokenPendingResponse {
|
|
|
148
199
|
|
|
149
200
|
/**
|
|
150
201
|
* `POST /v1/remote-web/pairing-token` approved response body (200) — carries
|
|
151
|
-
* the minted
|
|
152
|
-
* of band as an `HttpOnly` cookie
|
|
202
|
+
* the minted session credentials. A browser exchange (no `deviceId`) receives
|
|
203
|
+
* its refresh token out of band as an `HttpOnly` cookie and so omits the two
|
|
204
|
+
* refresh fields below; a device-bound exchange receives it here instead.
|
|
153
205
|
*/
|
|
154
206
|
export interface RemoteWebPairingTokenApprovedResponse {
|
|
155
207
|
status: "approved";
|
|
@@ -160,6 +212,10 @@ export interface RemoteWebPairingTokenApprovedResponse {
|
|
|
160
212
|
refreshAfter: string;
|
|
161
213
|
guardianId: string;
|
|
162
214
|
assistantId: string;
|
|
215
|
+
/** Device-bound refresh token, present only for a `deviceId` exchange. */
|
|
216
|
+
refreshToken?: string;
|
|
217
|
+
/** ISO-8601 instant the device-bound refresh token expires. */
|
|
218
|
+
refreshTokenExpiresAt?: string;
|
|
163
219
|
}
|
|
164
220
|
|
|
165
221
|
/** Union of the two terminal `pairing-token` response bodies. */
|
|
@@ -169,7 +225,7 @@ export type RemoteWebPairingTokenResponse =
|
|
|
169
225
|
|
|
170
226
|
// ── Shared pairing URL helpers ──────────────────────────────────────────────
|
|
171
227
|
//
|
|
172
|
-
// Every surface that mints a pairing (the `vellum pair
|
|
228
|
+
// Every surface that mints a pairing (the `vellum pair` CLI, the web
|
|
173
229
|
// settings "Pair a device" card) must accept the same public URLs and build
|
|
174
230
|
// the same scannable links. The helpers are environment-neutral (WHATWG URL
|
|
175
231
|
// only) so both Node and browser callers share one implementation.
|
|
@@ -178,6 +234,7 @@ export type RemoteWebPairingTokenResponse =
|
|
|
178
234
|
export type PublicBaseUrlRejection =
|
|
179
235
|
| "unparseable"
|
|
180
236
|
| "loopback"
|
|
237
|
+
| "private-address"
|
|
181
238
|
| "non-https"
|
|
182
239
|
| "service-website";
|
|
183
240
|
|
|
@@ -204,6 +261,16 @@ export const TUNNEL_PROVIDER_WEBSITE_HOSTS = [
|
|
|
204
261
|
"www.cloudflare.com",
|
|
205
262
|
] as const;
|
|
206
263
|
|
|
264
|
+
/**
|
|
265
|
+
* A hostname with any trailing DNS root dot removed. `example.com.` and
|
|
266
|
+
* `example.com` name the same host, and a resolver reads `localhost.` as the
|
|
267
|
+
* loopback name, so every host comparison below runs on the stripped form.
|
|
268
|
+
* Bracketed IPv6 literals never carry one.
|
|
269
|
+
*/
|
|
270
|
+
function hostnameWithoutRootDot(hostname: string): string {
|
|
271
|
+
return hostname.replace(/\.+$/, "");
|
|
272
|
+
}
|
|
273
|
+
|
|
207
274
|
/**
|
|
208
275
|
* A URL whose exact host is a tunnel/ingress vendor's own website (see
|
|
209
276
|
* {@link TUNNEL_PROVIDER_WEBSITE_HOSTS}). Exact-host only: a user's real
|
|
@@ -214,7 +281,7 @@ export function isTunnelProviderWebsiteUrl(url: string): boolean {
|
|
|
214
281
|
let hostname: string;
|
|
215
282
|
try {
|
|
216
283
|
// WHATWG URL lowercases the hostname during parsing.
|
|
217
|
-
hostname = new URL(url).hostname;
|
|
284
|
+
hostname = hostnameWithoutRootDot(new URL(url).hostname);
|
|
218
285
|
} catch {
|
|
219
286
|
return false;
|
|
220
287
|
}
|
|
@@ -231,7 +298,7 @@ export function isTunnelProviderWebsiteUrl(url: string): boolean {
|
|
|
231
298
|
export function tunnelProviderWebsiteName(url: string): string | null {
|
|
232
299
|
let hostname: string;
|
|
233
300
|
try {
|
|
234
|
-
hostname = new URL(url).hostname;
|
|
301
|
+
hostname = hostnameWithoutRootDot(new URL(url).hostname);
|
|
235
302
|
} catch {
|
|
236
303
|
return null;
|
|
237
304
|
}
|
|
@@ -247,21 +314,223 @@ export function tunnelProviderWebsiteName(url: string): string | null {
|
|
|
247
314
|
}
|
|
248
315
|
|
|
249
316
|
/**
|
|
250
|
-
* A
|
|
251
|
-
*
|
|
317
|
+
* A URL's comparable host: root dot stripped and IPv6 brackets removed, or
|
|
318
|
+
* null when the value is not a parseable URL.
|
|
252
319
|
*/
|
|
253
|
-
|
|
320
|
+
function comparableHost(url: string): string | null {
|
|
254
321
|
try {
|
|
255
|
-
// WHATWG URL canonicalizes hostnames
|
|
256
|
-
|
|
257
|
-
return (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
/^127(?:\.\d{1,3}){3}$/.test(hostname)
|
|
322
|
+
// WHATWG URL canonicalizes hostnames: IPv6 loopback is always "[::1]", a
|
|
323
|
+
// bare "0" is "0.0.0.0", and encoded IPv4 literals become dotted quads.
|
|
324
|
+
return hostnameWithoutRootDot(new URL(url).hostname).replace(
|
|
325
|
+
/^\[|\]$/g,
|
|
326
|
+
"",
|
|
261
327
|
);
|
|
262
328
|
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Hosts that reach the dialing machine without a resolver having to agree:
|
|
335
|
+
* `localhost`, `127.x.x.x`, `[::1]`, and the wildcard binds `0.0.0.0` / `[::]`,
|
|
336
|
+
* in their dotted, hex, and IPv4-mapped spellings.
|
|
337
|
+
*/
|
|
338
|
+
function isDnsIndependentLoopbackHost(host: string): boolean {
|
|
339
|
+
return (
|
|
340
|
+
host === "localhost" ||
|
|
341
|
+
host === "::1" ||
|
|
342
|
+
host === "0:0:0:0:0:0:0:1" ||
|
|
343
|
+
/^127(?:\.\d{1,3}){3}$/.test(host) ||
|
|
344
|
+
// A wildcard host reaches a local listener when dialed, so it counts as
|
|
345
|
+
// local for the pairing and refresh-channel guards alike.
|
|
346
|
+
host === "0.0.0.0" ||
|
|
347
|
+
host === "0" ||
|
|
348
|
+
host === "::" ||
|
|
349
|
+
host === "0:0:0:0:0:0:0:0" ||
|
|
350
|
+
// IPv4-mapped loopback and wildcard, in dotted and hex encodings.
|
|
351
|
+
/^(?:0:0:0:0:0|:):ffff:127(?:\.\d{1,3}){3}$/.test(host) ||
|
|
352
|
+
/^(?:0:0:0:0:0|:):ffff:7f[0-9a-f]{2}:[0-9a-f]{1,4}$/.test(host) ||
|
|
353
|
+
/^(?:0:0:0:0:0|:):ffff:0\.0\.0\.0$/.test(host) ||
|
|
354
|
+
/^(?:0:0:0:0:0|:):ffff:0:0$/.test(host)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A URL that reaches a listener on the machine dialing it, judged wide: the
|
|
360
|
+
* literals {@link isDnsIndependentLoopbackHost} covers PLUS the whole reserved
|
|
361
|
+
* `localhost` namespace (RFC 6761, so `foo.localhost` counts). A pairing link
|
|
362
|
+
* that encodes one is unreachable from the scanning device, and a host that
|
|
363
|
+
* POSTs to one is calling a service on its own machine.
|
|
364
|
+
*
|
|
365
|
+
* This is the one loopback predicate the whole pairing path reads (the pasted
|
|
366
|
+
* address, and the gatewayUrl an import registers), so the same address class
|
|
367
|
+
* can never be refused by one entry point and accepted by another. Every read
|
|
368
|
+
* of it REFUSES the address it matches, which is why it is the wide one.
|
|
369
|
+
* A guard that GRANTS something to loopback reads
|
|
370
|
+
* {@link isDnsIndependentLoopbackUrl} instead.
|
|
371
|
+
*/
|
|
372
|
+
export function isLoopbackPublicUrl(url: string): boolean {
|
|
373
|
+
const host = comparableHost(url);
|
|
374
|
+
if (host === null) {
|
|
263
375
|
return false;
|
|
264
376
|
}
|
|
377
|
+
return host.endsWith(".localhost") || isDnsIndependentLoopbackHost(host);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* A URL that reaches this machine whatever the resolver does, judged narrow:
|
|
382
|
+
* exact `localhost` and the loopback/wildcard IP literals, and NOT the
|
|
383
|
+
* reserved `.localhost` namespace. RFC 6761 says a resolver should map that
|
|
384
|
+
* namespace to loopback, but glibc does not by default, so `evil.localhost`
|
|
385
|
+
* is an ordinary DNS name that can answer with any address at all.
|
|
386
|
+
*
|
|
387
|
+
* Read this wherever loopback EARNS a privilege a public host does not get,
|
|
388
|
+
* such as accepting a plaintext channel as confidential; read the wider
|
|
389
|
+
* {@link isLoopbackPublicUrl} wherever loopback is refused. Both directions
|
|
390
|
+
* then fail safe on a name that only might be local.
|
|
391
|
+
*/
|
|
392
|
+
export function isDnsIndependentLoopbackUrl(url: string): boolean {
|
|
393
|
+
const host = comparableHost(url);
|
|
394
|
+
return host !== null && isDnsIndependentLoopbackHost(host);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Canonical dotted-quad as its four octets, or null when it is not one. */
|
|
398
|
+
function parseIpv4Literal(hostname: string): number[] | null {
|
|
399
|
+
const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(hostname);
|
|
400
|
+
if (!match) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const octets = match.slice(1).map(Number);
|
|
404
|
+
return octets.every((octet) => octet <= 255) ? octets : null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Bracketed IPv6 literal as its eight 16-bit pieces, or null. */
|
|
408
|
+
function parseIpv6Literal(hostname: string): number[] | null {
|
|
409
|
+
if (!hostname.startsWith("[") || !hostname.endsWith("]")) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const halves = hostname.slice(1, -1).split("::");
|
|
413
|
+
if (halves.length > 2) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
const toPieces = (part: string): number[] | null => {
|
|
417
|
+
if (!part) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
const pieces: number[] = [];
|
|
421
|
+
const labels = part.split(":");
|
|
422
|
+
for (const [index, label] of labels.entries()) {
|
|
423
|
+
// A trailing dotted-quad is legal IPv6 text. The WHATWG serializer never
|
|
424
|
+
// emits one, but a hand-built literal can still reach this parser.
|
|
425
|
+
if (index === labels.length - 1 && label.includes(".")) {
|
|
426
|
+
const octets = parseIpv4Literal(label);
|
|
427
|
+
if (!octets) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
pieces.push(
|
|
431
|
+
((octets[0] ?? 0) << 8) | (octets[1] ?? 0),
|
|
432
|
+
((octets[2] ?? 0) << 8) | (octets[3] ?? 0),
|
|
433
|
+
);
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
if (!/^[0-9a-f]{1,4}$/i.test(label)) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
pieces.push(Number.parseInt(label, 16));
|
|
440
|
+
}
|
|
441
|
+
return pieces;
|
|
442
|
+
};
|
|
443
|
+
const head = toPieces(halves[0] ?? "");
|
|
444
|
+
const tail = halves.length === 2 ? toPieces(halves[1] ?? "") : [];
|
|
445
|
+
if (!head || !tail) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
if (halves.length === 1) {
|
|
449
|
+
return head.length === 8 ? head : null;
|
|
450
|
+
}
|
|
451
|
+
const gap = 8 - head.length - tail.length;
|
|
452
|
+
return gap >= 1 ? [...head, ...Array<number>(gap).fill(0), ...tail] : null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* IPv4 ranges that are not publicly routable, judged from the four octets of a
|
|
457
|
+
* canonical dotted-quad. Loopback (127/8) is included so the predicate is
|
|
458
|
+
* complete on its own; `resolvePublicBaseUrl` still reports it as `loopback`
|
|
459
|
+
* because it checks that first.
|
|
460
|
+
*
|
|
461
|
+
* 100.64.0.0/10 is DELIBERATELY ABSENT. Tailscale hands out CGNAT addresses
|
|
462
|
+
* from that range and a Tailscale endpoint is a supported pairing target.
|
|
463
|
+
*/
|
|
464
|
+
function isPrivateIpv4(octets: readonly number[]): boolean {
|
|
465
|
+
const a = octets[0] ?? 0;
|
|
466
|
+
const b = octets[1] ?? 0;
|
|
467
|
+
const c = octets[2] ?? 0;
|
|
468
|
+
return (
|
|
469
|
+
a === 0 || // 0/8 "this network"
|
|
470
|
+
a === 10 || // 10/8 private
|
|
471
|
+
a === 127 || // 127/8 loopback
|
|
472
|
+
(a === 169 && b === 254) || // 169.254/16 link-local, incl. cloud metadata
|
|
473
|
+
(a === 172 && b >= 16 && b <= 31) || // 172.16/12 private
|
|
474
|
+
(a === 192 && b === 0 && c === 0) || // 192.0.0/24 protocol assignments
|
|
475
|
+
(a === 192 && b === 168) || // 192.168/16 private
|
|
476
|
+
(a === 198 && (b === 18 || b === 19)) || // 198.18/15 benchmarking
|
|
477
|
+
a >= 224 // 224/4 multicast, 240/4 reserved, 255.255.255.255 broadcast
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* A URL whose hostname is an IP literal outside publicly routable space:
|
|
483
|
+
* private, loopback, link-local (which is where the cloud instance metadata
|
|
484
|
+
* endpoint 169.254.169.254 lives), multicast, or reserved, in either address
|
|
485
|
+
* family. Pairing POSTs to whatever address the user pasted, so a literal
|
|
486
|
+
* aimed at the host's own network is refused before any request is made.
|
|
487
|
+
*
|
|
488
|
+
* Only literals written directly in the address are checked. Hostnames are
|
|
489
|
+
* NEVER resolved and resolved addresses are NEVER filtered, deliberately:
|
|
490
|
+
* Tailscale endpoints (`*.ts.net`) are a supported pairing target and resolve
|
|
491
|
+
* into the 100.64.0.0/10 CGNAT range, so post-DNS filtering would refuse them
|
|
492
|
+
* outright. It would also be TOCTOU-prone, since a name can resolve
|
|
493
|
+
* differently between the check and the connection.
|
|
494
|
+
*/
|
|
495
|
+
export function isPrivateNetworkPublicUrl(url: string): boolean {
|
|
496
|
+
let hostname: string;
|
|
497
|
+
try {
|
|
498
|
+
// WHATWG URL canonicalizes IP literals: decimal, octal, and hex IPv4 forms
|
|
499
|
+
// collapse to a dotted-quad, and IPv6 to a bracketed lowercase literal.
|
|
500
|
+
hostname = hostnameWithoutRootDot(new URL(url).hostname);
|
|
501
|
+
} catch {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
const ipv4 = parseIpv4Literal(hostname);
|
|
505
|
+
if (ipv4) {
|
|
506
|
+
return isPrivateIpv4(ipv4);
|
|
507
|
+
}
|
|
508
|
+
const ipv6 = parseIpv6Literal(hostname);
|
|
509
|
+
if (!ipv6) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
// IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d) literals carry
|
|
513
|
+
// an IPv4 destination, so they are unwrapped and judged as IPv4.
|
|
514
|
+
const low = ipv6[5] ?? 0;
|
|
515
|
+
if (
|
|
516
|
+
ipv6.slice(0, 5).every((piece) => piece === 0) &&
|
|
517
|
+
(low === 0xffff || low === 0)
|
|
518
|
+
) {
|
|
519
|
+
const mapped = ipv6[6] ?? 0;
|
|
520
|
+
const trailing = ipv6[7] ?? 0;
|
|
521
|
+
return isPrivateIpv4([
|
|
522
|
+
mapped >> 8,
|
|
523
|
+
mapped & 0xff,
|
|
524
|
+
trailing >> 8,
|
|
525
|
+
trailing & 0xff,
|
|
526
|
+
]);
|
|
527
|
+
}
|
|
528
|
+
const top = ipv6[0] ?? 0;
|
|
529
|
+
return (
|
|
530
|
+
(top & 0xfe00) === 0xfc00 || // fc00::/7 unique-local
|
|
531
|
+
(top & 0xffc0) === 0xfe80 || // fe80::/10 link-local
|
|
532
|
+
(top & 0xff00) === 0xff00 // ff00::/8 multicast
|
|
533
|
+
);
|
|
265
534
|
}
|
|
266
535
|
|
|
267
536
|
/**
|
|
@@ -285,8 +554,13 @@ export function normalizePairingBaseUrl(value: string): string {
|
|
|
285
554
|
|
|
286
555
|
/**
|
|
287
556
|
* Resolve an address to the public https base URL to advertise in a pairing
|
|
288
|
-
* challenge, or report why it can't be used. Loopback
|
|
289
|
-
* refused with a specific reason callers
|
|
557
|
+
* challenge, or report why it can't be used. Loopback, private-network IP
|
|
558
|
+
* literals, and non-https links are refused with a specific reason callers
|
|
559
|
+
* turn into their own guidance.
|
|
560
|
+
*
|
|
561
|
+
* Hosts POST to this address during pairing, so the refusals are the SSRF
|
|
562
|
+
* containment for every pairing surface. See
|
|
563
|
+
* {@link isPrivateNetworkPublicUrl} for why resolved addresses are left alone.
|
|
290
564
|
*/
|
|
291
565
|
export function resolvePublicBaseUrl(raw: string): PublicBaseUrlResult {
|
|
292
566
|
let normalized: string;
|
|
@@ -298,6 +572,9 @@ export function resolvePublicBaseUrl(raw: string): PublicBaseUrlResult {
|
|
|
298
572
|
if (isLoopbackPublicUrl(normalized)) {
|
|
299
573
|
return { ok: false, reason: "loopback" };
|
|
300
574
|
}
|
|
575
|
+
if (isPrivateNetworkPublicUrl(normalized)) {
|
|
576
|
+
return { ok: false, reason: "private-address" };
|
|
577
|
+
}
|
|
301
578
|
if (isTunnelProviderWebsiteUrl(normalized)) {
|
|
302
579
|
return { ok: false, reason: "service-website" };
|
|
303
580
|
}
|
|
@@ -324,3 +601,216 @@ export function buildRemoteWebPairingUrl(
|
|
|
324
601
|
}).toString();
|
|
325
602
|
return url.toString();
|
|
326
603
|
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Base that makes a relative pairing link parseable. Only the query and
|
|
607
|
+
* fragment are read off the result, so it never reaches a caller.
|
|
608
|
+
*/
|
|
609
|
+
const RELATIVE_PAIRING_LINK_BASE = "https://pairing.invalid";
|
|
610
|
+
|
|
611
|
+
/** The device/user codes a pairing link can carry. */
|
|
612
|
+
export interface RemoteWebPairingParams {
|
|
613
|
+
deviceCode: string | null;
|
|
614
|
+
userCode: string | null;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function firstParam(
|
|
618
|
+
params: URLSearchParams,
|
|
619
|
+
...names: string[]
|
|
620
|
+
): string | null {
|
|
621
|
+
for (const name of names) {
|
|
622
|
+
const value = params.get(name)?.trim();
|
|
623
|
+
if (value) {
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Query parameters merged with fragment parameters, query winning on a clash.
|
|
632
|
+
* Pairing links carry the device code in the fragment so it never reaches the
|
|
633
|
+
* wire, but hand-assembled links put it in the query.
|
|
634
|
+
*/
|
|
635
|
+
function pairingLinkParams(url: URL): URLSearchParams {
|
|
636
|
+
const merged = new URLSearchParams(url.search);
|
|
637
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
638
|
+
for (const [key, value] of new URLSearchParams(hash)) {
|
|
639
|
+
if (!merged.has(key)) {
|
|
640
|
+
merged.set(key, value);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return merged;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Read the pairing codes off a link, accepting either casing (`device_code` /
|
|
648
|
+
* `deviceCode`) from either the query string or the fragment. Relative values
|
|
649
|
+
* are accepted so a router can pass `pathname + search + hash` straight in.
|
|
650
|
+
*/
|
|
651
|
+
export function parseRemoteWebPairingParams(
|
|
652
|
+
value: string | URL,
|
|
653
|
+
): RemoteWebPairingParams {
|
|
654
|
+
const url =
|
|
655
|
+
typeof value === "string"
|
|
656
|
+
? new URL(value, RELATIVE_PAIRING_LINK_BASE)
|
|
657
|
+
: value;
|
|
658
|
+
const params = pairingLinkParams(url);
|
|
659
|
+
return {
|
|
660
|
+
deviceCode: firstParam(params, "deviceCode", "device_code"),
|
|
661
|
+
userCode: firstParam(params, "userCode", "user_code"),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Outcome of {@link parsePairingAddress}. */
|
|
666
|
+
export type ParsePairingAddressResult =
|
|
667
|
+
| { ok: true; publicBaseUrl: string; deviceCode: string | null }
|
|
668
|
+
| { ok: false; reason: PublicBaseUrlRejection };
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* The inverse of {@link buildRemoteWebPairingUrl}: what a pasted pairing
|
|
672
|
+
* address means. A full pairing link yields its base plus the device code it
|
|
673
|
+
* carries; a bare `https://host` address yields a null device code, leaving
|
|
674
|
+
* the caller to mint its own challenge. The base goes through
|
|
675
|
+
* {@link resolvePublicBaseUrl}, so loopback, private-network literals,
|
|
676
|
+
* non-https, and tunnel-vendor websites are refused with the same reasons
|
|
677
|
+
* every other pairing surface reports.
|
|
678
|
+
*/
|
|
679
|
+
export function parsePairingAddress(raw: string): ParsePairingAddressResult {
|
|
680
|
+
const resolved = resolvePublicBaseUrl(raw);
|
|
681
|
+
if (!resolved.ok) {
|
|
682
|
+
return resolved;
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
ok: true,
|
|
686
|
+
publicBaseUrl: resolved.url,
|
|
687
|
+
deviceCode: parseRemoteWebPairingParams(raw).deviceCode,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* What a pairing attempt failed on, for callers picking recovery copy. Every
|
|
693
|
+
* pairing surface reports one of these: the host-side sessions in
|
|
694
|
+
* `@vellumai/local-mode`, the Electron bridge, the desktop connect dialog, and
|
|
695
|
+
* the `vellum connect import` CLI.
|
|
696
|
+
*/
|
|
697
|
+
export type PairingFailureReason =
|
|
698
|
+
/** The pasted address is not a usable assistant address. */
|
|
699
|
+
| "invalid-address"
|
|
700
|
+
/** No live session for this handle: it was cancelled or never existed. */
|
|
701
|
+
| "unknown-session"
|
|
702
|
+
/** The device code expired, was denied, or was already spent. */
|
|
703
|
+
| "expired"
|
|
704
|
+
/** The assistant could not be reached. */
|
|
705
|
+
| "unreachable"
|
|
706
|
+
/**
|
|
707
|
+
* The assistant refused the request with a status that left the device code
|
|
708
|
+
* exchangeable, so the same session is worth another attempt.
|
|
709
|
+
*/
|
|
710
|
+
| "gateway-retryable"
|
|
711
|
+
/**
|
|
712
|
+
* The assistant answered, but with a reply this device cannot use. The code
|
|
713
|
+
* behind it is spent or unknowable, so the attempt is settled.
|
|
714
|
+
*/
|
|
715
|
+
| "gateway"
|
|
716
|
+
/** The credentials arrived, but registering them locally was refused. */
|
|
717
|
+
| "import"
|
|
718
|
+
/**
|
|
719
|
+
* The local destination was refused BEFORE the exchange, so the one-time
|
|
720
|
+
* code is untouched and the session is still pollable. Only a caller that
|
|
721
|
+
* changes what it asked for (a different `name`) can get past it, so it is
|
|
722
|
+
* not worth another identical attempt.
|
|
723
|
+
*/
|
|
724
|
+
| "import-precheck";
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Every reason, mapped to whether another attempt against the same session can
|
|
728
|
+
* still succeed. Two classes leave the device code exchangeable host-side:
|
|
729
|
+
*
|
|
730
|
+
* - `unreachable`, the transport class (a thrown fetch, a timeout, a refused
|
|
731
|
+
* redirect, a body that errored mid-stream). Nothing reached the assistant,
|
|
732
|
+
* so the session and the code are untouched.
|
|
733
|
+
* - `gateway-retryable`, a non-200 the assistant answered with. The gateway
|
|
734
|
+
* releases the challenge before a repairable failure (a transient error, or
|
|
735
|
+
* the `GUARDIAN_REPAIR_REQUIRED` 503), so the same code stays exchangeable.
|
|
736
|
+
*
|
|
737
|
+
* Everything else ends the attempt. `invalid-address` and `unknown-session`
|
|
738
|
+
* cannot resolve by waiting; `expired` and `import` mean the one-time code is
|
|
739
|
+
* already spent; `gateway` means the assistant answered with something this
|
|
740
|
+
* device cannot use (an over-cap body, credentials it cannot persist, a 200
|
|
741
|
+
* that is not a pairing reply), past which the code is spent rather than
|
|
742
|
+
* released; and `import-precheck` refused the local destination before the
|
|
743
|
+
* exchange, so an identical attempt is refused identically.
|
|
744
|
+
*
|
|
745
|
+
* `import-precheck` is the one of those that leaves the SESSION alive, which
|
|
746
|
+
* is a separate question from whether to retry: see
|
|
747
|
+
* {@link pairingSessionSurvives}.
|
|
748
|
+
*
|
|
749
|
+
* The `satisfies` clause keeps the map exhaustive, so a new reason does not
|
|
750
|
+
* compile until it is classified here.
|
|
751
|
+
*/
|
|
752
|
+
const PAIRING_REASON_RETRYABLE = {
|
|
753
|
+
"invalid-address": false,
|
|
754
|
+
"unknown-session": false,
|
|
755
|
+
expired: false,
|
|
756
|
+
unreachable: true,
|
|
757
|
+
"gateway-retryable": true,
|
|
758
|
+
gateway: false,
|
|
759
|
+
import: false,
|
|
760
|
+
"import-precheck": false,
|
|
761
|
+
} satisfies Record<PairingFailureReason, boolean>;
|
|
762
|
+
|
|
763
|
+
/** The reasons {@link isRetryablePairingReason} accepts. */
|
|
764
|
+
export const RETRYABLE_PAIRING_REASONS: ReadonlySet<PairingFailureReason> =
|
|
765
|
+
new Set(
|
|
766
|
+
(Object.keys(PAIRING_REASON_RETRYABLE) as PairingFailureReason[]).filter(
|
|
767
|
+
(reason) => PAIRING_REASON_RETRYABLE[reason],
|
|
768
|
+
),
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Whether a refused pairing step is worth another attempt. An unlabelled
|
|
773
|
+
* failure reads as settled, so a host too old to name a reason ends the
|
|
774
|
+
* attempt rather than being spun against until the code expires.
|
|
775
|
+
*/
|
|
776
|
+
export function isRetryablePairingReason(
|
|
777
|
+
reason: PairingFailureReason | null | undefined,
|
|
778
|
+
): boolean {
|
|
779
|
+
return reason != null && RETRYABLE_PAIRING_REASONS.has(reason);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Every reason, mapped to whether the pairing session it came from is still
|
|
784
|
+
* live host-side. A caller holding the handle reads this to decide whether to
|
|
785
|
+
* release the session: releasing a live one throws away a device code that is
|
|
786
|
+
* still good and costs the user a fresh pairing link.
|
|
787
|
+
*
|
|
788
|
+
* This is deliberately NOT the retryable map. `import-precheck` is the reason
|
|
789
|
+
* the two disagree on: nothing was spent and the same handle still polls, but
|
|
790
|
+
* only once the caller changes the name it asked for, so a blind retry is
|
|
791
|
+
* futile while dropping the session is destructive.
|
|
792
|
+
*
|
|
793
|
+
* `invalid-address` never had a session to begin with, so it reads false the
|
|
794
|
+
* same way a dead one does: there is nothing to hold onto either way.
|
|
795
|
+
*/
|
|
796
|
+
const PAIRING_REASON_SESSION_LIVE = {
|
|
797
|
+
"invalid-address": false,
|
|
798
|
+
"unknown-session": false,
|
|
799
|
+
expired: false,
|
|
800
|
+
unreachable: true,
|
|
801
|
+
"gateway-retryable": true,
|
|
802
|
+
gateway: false,
|
|
803
|
+
import: false,
|
|
804
|
+
"import-precheck": true,
|
|
805
|
+
} satisfies Record<PairingFailureReason, boolean>;
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Whether the session behind a refused pairing step is still pollable. An
|
|
809
|
+
* unlabelled failure reads as settled, so a host too old to name a reason has
|
|
810
|
+
* its session released rather than left to time out.
|
|
811
|
+
*/
|
|
812
|
+
export function pairingSessionSurvives(
|
|
813
|
+
reason: PairingFailureReason | null | undefined,
|
|
814
|
+
): boolean {
|
|
815
|
+
return reason != null && PAIRING_REASON_SESSION_LIVE[reason] === true;
|
|
816
|
+
}
|