@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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canned access-denial copy and the plugin notice the gateway posts when a
|
|
3
|
+
* plugin inbound delivery clears verification but fails the admission floor.
|
|
4
|
+
*
|
|
5
|
+
* Built-in channels send this reply through the runtime's channel transport.
|
|
6
|
+
* A plugin channel has no such transport: the gateway does not hold the
|
|
7
|
+
* vendor send credentials, so the notice is how the plugin learns it must
|
|
8
|
+
* send the same line itself, without running a turn.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
import { AdmissionPolicySchema } from "./admission-policy-contract.js";
|
|
14
|
+
import { TrustClassSchema } from "./trust-verdict-contract.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Requester-facing reply when access is denied and there is no handshake
|
|
18
|
+
* and no guardian notification. Single source for this line: the runtime
|
|
19
|
+
* ACL composer and the plugin admission-denied notice both send it.
|
|
20
|
+
*/
|
|
21
|
+
export const ACCESS_DENIED_NOT_APPROVED_REPLY =
|
|
22
|
+
"Sorry, you haven't been approved to message this assistant.";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Plugin route the gateway posts the notice to, under
|
|
26
|
+
* `/v1/x/plugins/<plugin>/`. Not a public ingress path.
|
|
27
|
+
*/
|
|
28
|
+
export const PLUGIN_ADMISSION_DENIED_NOTICE_PATH = "notices/admission-denied";
|
|
29
|
+
|
|
30
|
+
export const PluginAdmissionDeniedNoticeSchema = z.object({
|
|
31
|
+
reason: z.literal("admission_floor"),
|
|
32
|
+
plugin: z.string().min(1),
|
|
33
|
+
ingressRoute: z.string().min(1),
|
|
34
|
+
admissionPolicy: AdmissionPolicySchema,
|
|
35
|
+
trustClass: TrustClassSchema,
|
|
36
|
+
/** Vendor chat id, without the plugin prefix the gate stores. */
|
|
37
|
+
conversationExternalId: z.string().min(1),
|
|
38
|
+
/** Vendor sender id, without the plugin prefix the gate stores. */
|
|
39
|
+
actorExternalId: z.string().min(1),
|
|
40
|
+
/** Vendor message id, without the plugin prefix the gate stores. */
|
|
41
|
+
externalMessageId: z.string().min(1),
|
|
42
|
+
replyText: z.string().min(1),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type PluginAdmissionDeniedNotice = z.infer<
|
|
46
|
+
typeof PluginAdmissionDeniedNoticeSchema
|
|
47
|
+
>;
|
|
@@ -78,6 +78,17 @@ export const TrustVerdictSchema = z.object({
|
|
|
78
78
|
policy: z.string().optional(),
|
|
79
79
|
verifiedAt: z.number().nullable().optional(),
|
|
80
80
|
memberDisplayName: z.string().optional(),
|
|
81
|
+
/**
|
|
82
|
+
* Contact-level auto-approve ceiling from the gateway `contacts` row.
|
|
83
|
+
* Present when a member channel resolves. Null means the contact has no
|
|
84
|
+
* explicit override (inherit the room / trust-class cascade).
|
|
85
|
+
* Same vocabulary as `RiskThreshold` (`none` | `low` | `medium` | `high`).
|
|
86
|
+
* Inlined here so this leaf contract does not import the permission matrix.
|
|
87
|
+
*/
|
|
88
|
+
autoApproveThreshold: z
|
|
89
|
+
.enum(["none", "low", "medium", "high"])
|
|
90
|
+
.nullable()
|
|
91
|
+
.optional(),
|
|
81
92
|
|
|
82
93
|
// Gateway-owned interaction telemetry (a trust signal, not an info field per
|
|
83
94
|
// the 2×2) — carried straight off the member `contact_channels` row.
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
normalizePublicBaseUrl,
|
|
7
7
|
parseRecordedAssistantId,
|
|
8
8
|
parseTunnelRecord,
|
|
9
|
+
selectRestartTunnelRecord,
|
|
9
10
|
trimmedNonEmptyString,
|
|
10
11
|
TUNNEL_PROVIDERS,
|
|
12
|
+
type TunnelRecord,
|
|
11
13
|
velayHostForPlatformHost,
|
|
12
14
|
} from "../ingress.js";
|
|
13
15
|
import {
|
|
@@ -221,6 +223,41 @@ describe("parseTunnelRecord", () => {
|
|
|
221
223
|
});
|
|
222
224
|
});
|
|
223
225
|
|
|
226
|
+
describe("selectRestartTunnelRecord", () => {
|
|
227
|
+
const PAIRING: TunnelRecord = {
|
|
228
|
+
provider: "tailscale",
|
|
229
|
+
publicBaseUrl: "https://assistant-1.example.ts.net",
|
|
230
|
+
};
|
|
231
|
+
const LAST: TunnelRecord = {
|
|
232
|
+
provider: "ngrok",
|
|
233
|
+
publicBaseUrl: "https://assistant-1.ngrok.app",
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
test("prefers the pairing tunnel over the last one that ran", () => {
|
|
237
|
+
// The pairing tunnel deliberately left the older public record beside it,
|
|
238
|
+
// so naming that one would point at an ingress nobody is using.
|
|
239
|
+
expect(
|
|
240
|
+
selectRestartTunnelRecord({ lastTunnel: LAST, pairingTunnel: PAIRING }),
|
|
241
|
+
).toEqual(PAIRING);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("falls back to the last tunnel and to null", () => {
|
|
245
|
+
expect(selectRestartTunnelRecord({ lastTunnel: LAST })).toEqual(LAST);
|
|
246
|
+
for (const ingress of [undefined, null, {}, "nonsense", 42]) {
|
|
247
|
+
expect(selectRestartTunnelRecord(ingress)).toBeNull();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("skips a malformed pairing record for a usable last one", () => {
|
|
252
|
+
expect(
|
|
253
|
+
selectRestartTunnelRecord({
|
|
254
|
+
lastTunnel: LAST,
|
|
255
|
+
pairingTunnel: { provider: "wireguard", publicBaseUrl: "nope" },
|
|
256
|
+
}),
|
|
257
|
+
).toEqual(LAST);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
224
261
|
describe("trimmedNonEmptyString", () => {
|
|
225
262
|
test("trims and rejects blank or non-string values", () => {
|
|
226
263
|
expect(trimmedNonEmptyString(" assistant-1 ")).toBe("assistant-1");
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildRemoteWebPairingUrl,
|
|
5
|
+
isDnsIndependentLoopbackUrl,
|
|
6
|
+
isLoopbackPublicUrl,
|
|
7
|
+
isPrivateNetworkPublicUrl,
|
|
8
|
+
isRetryablePairingReason,
|
|
9
|
+
pairingSessionSurvives,
|
|
10
|
+
parsePairingAddress,
|
|
11
|
+
parseRemoteWebPairingParams,
|
|
12
|
+
resolvePublicBaseUrl,
|
|
13
|
+
RETRYABLE_PAIRING_REASONS,
|
|
14
|
+
type PairingFailureReason,
|
|
15
|
+
type PublicBaseUrlRejection,
|
|
16
|
+
} from "../remote-web-pairing.js";
|
|
17
|
+
|
|
18
|
+
describe("parseRemoteWebPairingParams", () => {
|
|
19
|
+
test("reads snake_case fragment parameters", () => {
|
|
20
|
+
expect(
|
|
21
|
+
parseRemoteWebPairingParams(
|
|
22
|
+
"https://assistant.example.com/assistant/pair#device_code=device-1&user_code=ABCD-EFGH",
|
|
23
|
+
),
|
|
24
|
+
).toEqual({ deviceCode: "device-1", userCode: "ABCD-EFGH" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("reads camelCase query parameters", () => {
|
|
28
|
+
expect(
|
|
29
|
+
parseRemoteWebPairingParams(
|
|
30
|
+
"https://assistant.example.com/assistant/pair?deviceCode=device-2&userCode=WXYZ-1234",
|
|
31
|
+
),
|
|
32
|
+
).toEqual({ deviceCode: "device-2", userCode: "WXYZ-1234" });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("accepts a relative link", () => {
|
|
36
|
+
expect(
|
|
37
|
+
parseRemoteWebPairingParams("/assistant/pair#device_code=device-3"),
|
|
38
|
+
).toEqual({ deviceCode: "device-3", userCode: null });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("reports null when no codes are present", () => {
|
|
42
|
+
expect(
|
|
43
|
+
parseRemoteWebPairingParams("https://assistant.example.com/assistant"),
|
|
44
|
+
).toEqual({ deviceCode: null, userCode: null });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("parsePairingAddress", () => {
|
|
49
|
+
test("splits a pairing link into its base and device code", () => {
|
|
50
|
+
const link = buildRemoteWebPairingUrl({
|
|
51
|
+
verificationUri: "https://assistant.example.com/assistant/pair",
|
|
52
|
+
deviceCode: "device-1",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(parsePairingAddress(link)).toEqual({
|
|
56
|
+
ok: true,
|
|
57
|
+
publicBaseUrl: "https://assistant.example.com",
|
|
58
|
+
deviceCode: "device-1",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("accepts the device code in the query string", () => {
|
|
63
|
+
expect(
|
|
64
|
+
parsePairingAddress(
|
|
65
|
+
"https://assistant.example.com/assistant/pair?deviceCode=device-2",
|
|
66
|
+
),
|
|
67
|
+
).toEqual({
|
|
68
|
+
ok: true,
|
|
69
|
+
publicBaseUrl: "https://assistant.example.com",
|
|
70
|
+
deviceCode: "device-2",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("preserves a path prefix while dropping the app-route suffix", () => {
|
|
75
|
+
expect(
|
|
76
|
+
parsePairingAddress(
|
|
77
|
+
"https://host.example.com/assistant-123/assistant/pair#device_code=device-3",
|
|
78
|
+
),
|
|
79
|
+
).toEqual({
|
|
80
|
+
ok: true,
|
|
81
|
+
publicBaseUrl: "https://host.example.com/assistant-123",
|
|
82
|
+
deviceCode: "device-3",
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("drops a trailing app-route suffix from a bare address", () => {
|
|
87
|
+
expect(
|
|
88
|
+
parsePairingAddress("https://assistant.example.com/assistant/pair"),
|
|
89
|
+
).toEqual({
|
|
90
|
+
ok: true,
|
|
91
|
+
publicBaseUrl: "https://assistant.example.com",
|
|
92
|
+
deviceCode: null,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("accepts a bare address with no device code", () => {
|
|
97
|
+
expect(parsePairingAddress("https://assistant.example.com")).toEqual({
|
|
98
|
+
ok: true,
|
|
99
|
+
publicBaseUrl: "https://assistant.example.com",
|
|
100
|
+
deviceCode: null,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test.each<[string, PublicBaseUrlRejection]>([
|
|
105
|
+
["not a url", "unparseable"],
|
|
106
|
+
["https://localhost:3000", "loopback"],
|
|
107
|
+
["https://127.0.0.1:3000", "loopback"],
|
|
108
|
+
["http://assistant.example.com", "non-https"],
|
|
109
|
+
["https://login.tailscale.com/admin/invite/abc123", "service-website"],
|
|
110
|
+
])("rejects %s", (raw, reason) => {
|
|
111
|
+
expect(parsePairingAddress(raw)).toEqual({ ok: false, reason });
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("resolvePublicBaseUrl private-address containment", () => {
|
|
116
|
+
// A host POSTs to whatever address was pasted, so an IP literal aimed at the
|
|
117
|
+
// local network (or at 169.254.169.254, the cloud instance metadata
|
|
118
|
+
// endpoint) is a blind SSRF target and is refused before any request.
|
|
119
|
+
test.each([
|
|
120
|
+
["https://0.0.0.0", "0/8"],
|
|
121
|
+
["https://10.0.0.1", "10/8"],
|
|
122
|
+
["https://10.255.255.254:8443", "10/8 with a port"],
|
|
123
|
+
["https://169.254.169.254", "169.254/16 cloud metadata"],
|
|
124
|
+
["https://172.16.0.1", "172.16/12 low edge"],
|
|
125
|
+
["https://172.31.255.254", "172.16/12 high edge"],
|
|
126
|
+
["https://192.0.0.8", "192.0.0/24"],
|
|
127
|
+
["https://192.168.1.5", "192.168/16"],
|
|
128
|
+
["https://198.18.0.1", "198.18/15"],
|
|
129
|
+
["https://198.19.255.254", "198.18/15 high half"],
|
|
130
|
+
["https://224.0.0.1", "224/4 multicast"],
|
|
131
|
+
["https://240.0.0.1", "240/4 reserved"],
|
|
132
|
+
["https://255.255.255.255", "broadcast"],
|
|
133
|
+
// WHATWG URL canonicalizes these to a dotted-quad before the check.
|
|
134
|
+
["https://2130706433", "decimal-encoded 127.0.0.1"],
|
|
135
|
+
["https://0xa000001", "hex-encoded 10.0.0.1"],
|
|
136
|
+
["https://[::]", "IPv6 unspecified"],
|
|
137
|
+
["https://[fd00::1]", "fc00::/7 unique-local"],
|
|
138
|
+
["https://[fc00::5]", "fc00::/7 low edge"],
|
|
139
|
+
["https://[fe80::1]", "fe80::/10 link-local"],
|
|
140
|
+
["https://[ff02::1]", "ff00::/8 multicast"],
|
|
141
|
+
["https://[::ffff:10.0.0.1]", "IPv4-mapped private"],
|
|
142
|
+
["https://[::ffff:169.254.169.254]", "IPv4-mapped metadata"],
|
|
143
|
+
["https://[::192.168.1.5]", "IPv4-compatible private"],
|
|
144
|
+
])("refuses %s (%s)", (raw) => {
|
|
145
|
+
const result = resolvePublicBaseUrl(raw);
|
|
146
|
+
expect(result.ok).toBe(false);
|
|
147
|
+
if (result.ok) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// 127/8 and ::1 keep their more specific loopback reason.
|
|
151
|
+
expect(["private-address", "loopback"]).toContain(result.reason);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test.each([
|
|
155
|
+
"https://10.0.0.1",
|
|
156
|
+
"https://169.254.169.254",
|
|
157
|
+
"https://172.20.3.4",
|
|
158
|
+
"https://192.168.1.5",
|
|
159
|
+
"https://198.18.0.1",
|
|
160
|
+
"https://[fd00::1]",
|
|
161
|
+
"https://[fe80::1]",
|
|
162
|
+
"https://[::ffff:10.0.0.1]",
|
|
163
|
+
])("reports %s as private-address, not loopback", (raw) => {
|
|
164
|
+
expect(resolvePublicBaseUrl(raw)).toEqual({
|
|
165
|
+
ok: false,
|
|
166
|
+
reason: "private-address",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// A wildcard bind reaches a listener on the machine that dials it, so it
|
|
171
|
+
// gets the more specific loopback reason and the "points back at this
|
|
172
|
+
// machine" guidance that goes with it.
|
|
173
|
+
test.each([
|
|
174
|
+
["https://0.0.0.0", "IPv4 wildcard"],
|
|
175
|
+
["https://[::]", "IPv6 wildcard"],
|
|
176
|
+
["https://[::ffff:0.0.0.0]", "IPv4-mapped wildcard"],
|
|
177
|
+
["https://[::ffff:127.0.0.1]", "IPv4-mapped loopback"],
|
|
178
|
+
])("reports %s (%s) as loopback", (raw) => {
|
|
179
|
+
expect(isLoopbackPublicUrl(raw)).toBe(true);
|
|
180
|
+
expect(resolvePublicBaseUrl(raw)).toEqual({
|
|
181
|
+
ok: false,
|
|
182
|
+
reason: "loopback",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Tailscale hands out 100.64.0.0/10 CGNAT addresses and `*.ts.net` names are
|
|
187
|
+
// a supported pairing target, so neither the range nor any hostname is
|
|
188
|
+
// filtered. Names are never resolved (see isPrivateNetworkPublicUrl).
|
|
189
|
+
test.each([
|
|
190
|
+
"https://my-box.tail1234.ts.net",
|
|
191
|
+
"https://assistant.example.com",
|
|
192
|
+
"https://100.101.102.103",
|
|
193
|
+
"https://[2606:4700::1111]",
|
|
194
|
+
"https://172.32.0.1",
|
|
195
|
+
"https://192.0.1.1",
|
|
196
|
+
"https://198.20.0.1",
|
|
197
|
+
"https://223.255.255.255",
|
|
198
|
+
])("still resolves %s", (raw) => {
|
|
199
|
+
expect(resolvePublicBaseUrl(raw)).toEqual({ ok: true, url: raw });
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("isPrivateNetworkPublicUrl reports a plain hostname as public", () => {
|
|
203
|
+
expect(isPrivateNetworkPublicUrl("https://my-box.tail1234.ts.net")).toBe(
|
|
204
|
+
false,
|
|
205
|
+
);
|
|
206
|
+
expect(isPrivateNetworkPublicUrl("not a url")).toBe(false);
|
|
207
|
+
expect(isPrivateNetworkPublicUrl("https://192.168.0.1")).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("resolvePublicBaseUrl localhost normalization", () => {
|
|
212
|
+
// A terminal DNS root dot survives WHATWG parsing, and resolvers read the
|
|
213
|
+
// absolute name as loopback, so the dot is stripped before every host
|
|
214
|
+
// comparison. The whole `.localhost` namespace is reserved (RFC 6761) and
|
|
215
|
+
// resolves to loopback too.
|
|
216
|
+
test.each([
|
|
217
|
+
["https://localhost.", "absolute localhost name"],
|
|
218
|
+
["https://localhost.:3000", "absolute localhost name with a port"],
|
|
219
|
+
["https://LOCALHOST.", "uppercase absolute localhost name"],
|
|
220
|
+
["https://foo.localhost", "reserved .localhost namespace"],
|
|
221
|
+
["https://foo.localhost.", "absolute .localhost namespace"],
|
|
222
|
+
["https://a.b.localhost.", "nested .localhost namespace"],
|
|
223
|
+
["https://127.0.0.1.", "absolute IPv4 loopback literal"],
|
|
224
|
+
])("reports %s (%s) as loopback", (raw) => {
|
|
225
|
+
expect(isLoopbackPublicUrl(raw)).toBe(true);
|
|
226
|
+
expect(resolvePublicBaseUrl(raw)).toEqual({
|
|
227
|
+
ok: false,
|
|
228
|
+
reason: "loopback",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("reports an absolute private literal as private-address", () => {
|
|
233
|
+
expect(isPrivateNetworkPublicUrl("https://10.0.0.1.")).toBe(true);
|
|
234
|
+
expect(resolvePublicBaseUrl("https://10.0.0.1.")).toEqual({
|
|
235
|
+
ok: false,
|
|
236
|
+
reason: "private-address",
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("reports an absolute vendor-site name as service-website", () => {
|
|
241
|
+
expect(resolvePublicBaseUrl("https://login.tailscale.com.")).toEqual({
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: "service-website",
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test.each([
|
|
248
|
+
"https://my-box.tail1234.ts.net.",
|
|
249
|
+
"https://assistant.example.com.",
|
|
250
|
+
"https://localhostage.example.com",
|
|
251
|
+
"https://mylocalhost.example.com.",
|
|
252
|
+
])("still resolves %s", (raw) => {
|
|
253
|
+
expect(isLoopbackPublicUrl(raw)).toBe(false);
|
|
254
|
+
expect(resolvePublicBaseUrl(raw)).toEqual({ ok: true, url: raw });
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("isDnsIndependentLoopbackUrl", () => {
|
|
259
|
+
test.each([
|
|
260
|
+
["http://localhost:7830", "the exact name"],
|
|
261
|
+
["http://LOCALHOST.:7830", "the absolute, uppercase name"],
|
|
262
|
+
["http://127.0.0.1:7830", "an IPv4 loopback literal"],
|
|
263
|
+
["http://127.9.9.9", "the rest of 127/8"],
|
|
264
|
+
["http://[::1]:7830", "the IPv6 loopback literal"],
|
|
265
|
+
["http://[::ffff:127.0.0.1]", "an IPv4-mapped loopback literal"],
|
|
266
|
+
["http://0.0.0.0:7830", "the IPv4 wildcard bind"],
|
|
267
|
+
["http://[::]:7830", "the IPv6 wildcard bind"],
|
|
268
|
+
])("%s reaches this machine (%s)", (raw) => {
|
|
269
|
+
expect(isDnsIndependentLoopbackUrl(raw)).toBe(true);
|
|
270
|
+
expect(isLoopbackPublicUrl(raw)).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// RFC 6761 reserves the namespace and says resolvers should map it to
|
|
274
|
+
// loopback, but glibc does not by default, so the name is ordinary DNS and
|
|
275
|
+
// can answer with any address. The wide predicate still refuses it as
|
|
276
|
+
// loopback; the narrow one refuses to hand it a loopback privilege.
|
|
277
|
+
test.each([
|
|
278
|
+
["https://foo.localhost", "reserved .localhost namespace"],
|
|
279
|
+
["https://foo.localhost.", "absolute .localhost namespace"],
|
|
280
|
+
["https://a.b.localhost", "nested .localhost namespace"],
|
|
281
|
+
])("%s is loopback but not DNS-independent (%s)", (raw) => {
|
|
282
|
+
expect(isLoopbackPublicUrl(raw)).toBe(true);
|
|
283
|
+
expect(isDnsIndependentLoopbackUrl(raw)).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test.each([
|
|
287
|
+
"https://assistant.example.com",
|
|
288
|
+
"https://localhostage.example.com",
|
|
289
|
+
"https://10.0.0.5",
|
|
290
|
+
"not a url",
|
|
291
|
+
])("%s is not loopback at all", (raw) => {
|
|
292
|
+
expect(isDnsIndependentLoopbackUrl(raw)).toBe(false);
|
|
293
|
+
expect(isLoopbackPublicUrl(raw)).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
/** Every reason, so a classification table cannot silently miss one. */
|
|
298
|
+
const EVERY_PAIRING_REASON: PairingFailureReason[] = [
|
|
299
|
+
"invalid-address",
|
|
300
|
+
"unknown-session",
|
|
301
|
+
"expired",
|
|
302
|
+
"unreachable",
|
|
303
|
+
"gateway-retryable",
|
|
304
|
+
"gateway",
|
|
305
|
+
"import",
|
|
306
|
+
"import-precheck",
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
describe("isRetryablePairingReason", () => {
|
|
310
|
+
test.each([
|
|
311
|
+
// Nothing reached the assistant, so the session and code are untouched.
|
|
312
|
+
"unreachable",
|
|
313
|
+
// The assistant refused with a status that released the code.
|
|
314
|
+
"gateway-retryable",
|
|
315
|
+
] as const)("%s is worth another attempt", (reason) => {
|
|
316
|
+
expect(isRetryablePairingReason(reason)).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test.each([
|
|
320
|
+
"invalid-address",
|
|
321
|
+
"unknown-session",
|
|
322
|
+
"expired",
|
|
323
|
+
// The assistant answered with something unusable, past which the code is
|
|
324
|
+
// spent rather than released.
|
|
325
|
+
"gateway",
|
|
326
|
+
"import",
|
|
327
|
+
// Nothing was spent, but an identical attempt is refused identically:
|
|
328
|
+
// only a caller that changes the name it asked for gets past this.
|
|
329
|
+
"import-precheck",
|
|
330
|
+
] as const)("%s settles the attempt", (reason) => {
|
|
331
|
+
expect(isRetryablePairingReason(reason)).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("an absent reason settles the attempt", () => {
|
|
335
|
+
expect(isRetryablePairingReason(undefined)).toBe(false);
|
|
336
|
+
expect(isRetryablePairingReason(null)).toBe(false);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("the exported set is exactly the reasons worth another attempt", () => {
|
|
340
|
+
expect(
|
|
341
|
+
EVERY_PAIRING_REASON.filter(isRetryablePairingReason).sort(),
|
|
342
|
+
).toEqual(["gateway-retryable", "unreachable"]);
|
|
343
|
+
expect([...RETRYABLE_PAIRING_REASONS].sort()).toEqual([
|
|
344
|
+
"gateway-retryable",
|
|
345
|
+
"unreachable",
|
|
346
|
+
]);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe("pairingSessionSurvives", () => {
|
|
351
|
+
test.each([
|
|
352
|
+
// Nothing reached the assistant, so the session and code are untouched.
|
|
353
|
+
"unreachable",
|
|
354
|
+
// The assistant released the code before answering.
|
|
355
|
+
"gateway-retryable",
|
|
356
|
+
// The refusal happened before the exchange, so the code is unspent.
|
|
357
|
+
"import-precheck",
|
|
358
|
+
] as const)("%s leaves the session pollable", (reason) => {
|
|
359
|
+
expect(pairingSessionSurvives(reason)).toBe(true);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test.each([
|
|
363
|
+
"invalid-address",
|
|
364
|
+
"unknown-session",
|
|
365
|
+
"expired",
|
|
366
|
+
"gateway",
|
|
367
|
+
// The exchange spent the code; only the local write failed.
|
|
368
|
+
"import",
|
|
369
|
+
] as const)("%s leaves nothing to poll", (reason) => {
|
|
370
|
+
expect(pairingSessionSurvives(reason)).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("an absent reason leaves nothing to poll", () => {
|
|
374
|
+
expect(pairingSessionSurvives(undefined)).toBe(false);
|
|
375
|
+
expect(pairingSessionSurvives(null)).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("a pre-check refusal is the one reason the two axes disagree on", () => {
|
|
379
|
+
// A caller that reads retryability as "keep the session" throws away a
|
|
380
|
+
// device code that is still good, which is what this reason exists to
|
|
381
|
+
// prevent: nothing was spent, so the same handle completes the pairing
|
|
382
|
+
// once the caller supplies a name that is free.
|
|
383
|
+
const disagreeing = EVERY_PAIRING_REASON.filter(
|
|
384
|
+
(reason) =>
|
|
385
|
+
pairingSessionSurvives(reason) !== isRetryablePairingReason(reason),
|
|
386
|
+
);
|
|
387
|
+
expect(disagreeing).toEqual(["import-precheck"]);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
@@ -147,6 +147,28 @@ export function parseTunnelRecord(value: unknown): TunnelRecord | null {
|
|
|
147
147
|
return { provider, publicBaseUrl: normalized };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The tunnel a reader should offer to restart, given an `ingress` section, or
|
|
152
|
+
* null when it records none.
|
|
153
|
+
*
|
|
154
|
+
* A pairing-only record wins: it is written by a tunnel that deliberately left
|
|
155
|
+
* `publicBaseUrl` (and the `lastTunnel` beside it) pointing at an older public
|
|
156
|
+
* provider, so naming that older one would send the user to rebuild an ingress
|
|
157
|
+
* they are not using instead of the tunnel actually in front of them.
|
|
158
|
+
*/
|
|
159
|
+
export function selectRestartTunnelRecord(
|
|
160
|
+
ingress: unknown,
|
|
161
|
+
): TunnelRecord | null {
|
|
162
|
+
if (typeof ingress !== "object" || ingress === null) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const section = ingress as Record<string, unknown>;
|
|
166
|
+
return (
|
|
167
|
+
parseTunnelRecord(section[INGRESS_PAIRING_TUNNEL_KEY]) ??
|
|
168
|
+
parseTunnelRecord(section[INGRESS_LAST_TUNNEL_KEY])
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
150
172
|
/** Parse an `ingress.assistantId` value, or null when it is absent or blank. */
|
|
151
173
|
export function parseRecordedAssistantId(value: unknown): string | null {
|
|
152
174
|
return trimmedNonEmptyString(value) ?? null;
|