@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa
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/.env.example +2 -0
- package/ARCHITECTURE.md +26 -3
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +38 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +161 -17
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +54 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +9 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +14 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/process-message.ts +5 -0
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/tool-approval-handler.ts +9 -1
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -51,10 +51,10 @@ const PROVIDER_MODEL_INTENTS: Record<string, Record<ModelIntent, string>> = {
|
|
|
51
51
|
},
|
|
52
52
|
fireworks: {
|
|
53
53
|
balanced: "accounts/fireworks/models/minimax-m3",
|
|
54
|
-
"cost-optimized": "accounts/fireworks/models/deepseek-
|
|
55
|
-
"latency-optimized": "accounts/fireworks/models/deepseek-
|
|
56
|
-
"quality-optimized": "accounts/fireworks/models/kimi-
|
|
57
|
-
"vision-optimized": "accounts/fireworks/models/kimi-
|
|
54
|
+
"cost-optimized": "accounts/fireworks/models/deepseek-v4p1-flash",
|
|
55
|
+
"latency-optimized": "accounts/fireworks/models/deepseek-v4p1-flash",
|
|
56
|
+
"quality-optimized": "accounts/fireworks/models/kimi-k3",
|
|
57
|
+
"vision-optimized": "accounts/fireworks/models/kimi-k3",
|
|
58
58
|
},
|
|
59
59
|
openrouter: {
|
|
60
60
|
balanced: "anthropic/claude-sonnet-4.6",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
extractOverflowTokensFromMessage,
|
|
40
40
|
} from "../types.js";
|
|
41
41
|
import {
|
|
42
|
+
boundUnparseableToolArgsRaw,
|
|
42
43
|
isUnparseableToolArgs,
|
|
43
44
|
wrapUnparseableToolArgs,
|
|
44
45
|
} from "../unparseable-tool-args.js";
|
|
@@ -212,6 +213,12 @@ export interface OpenAIChatCompletionsProviderOptions {
|
|
|
212
213
|
* (Fireworks, Together) keep sending `none` / forced choices. Enabled for
|
|
213
214
|
* the generic `openai-compatible` adapter, whose upstream is unknown. */
|
|
214
215
|
omitToolChoiceWhenReasoning?: boolean;
|
|
216
|
+
/** Send `tool_choice: "auto"` whenever tools are offered and the caller
|
|
217
|
+
* left the choice unspecified. Strict OpenAI-compatible upstreams default
|
|
218
|
+
* an unspecified `tool_choice` to "none" (io.net documents exactly that),
|
|
219
|
+
* which would leave the model unable to call any tool; OpenAI's own
|
|
220
|
+
* default is "auto", so this option only exists for upstreams that differ. */
|
|
221
|
+
defaultToolChoiceAuto?: boolean;
|
|
215
222
|
/** Wire field for the output-token limit. OpenAI and OpenAI-compatible
|
|
216
223
|
* backends use `max_completion_tokens`. OpenRouter defaults to
|
|
217
224
|
* `max_tokens` because its parameter router matches that key on
|
|
@@ -864,6 +871,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
864
871
|
private coerceObjectArgsToJsonString: boolean;
|
|
865
872
|
private salvageXmlToolCalls: boolean;
|
|
866
873
|
private omitToolChoiceWhenReasoning: boolean;
|
|
874
|
+
private defaultToolChoiceAuto: boolean;
|
|
867
875
|
private outputTokenLimitField: "max_completion_tokens" | "max_tokens";
|
|
868
876
|
|
|
869
877
|
constructor(
|
|
@@ -896,6 +904,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
896
904
|
options.salvageXmlToolCalls ?? shouldSalvageXmlToolCalls(model);
|
|
897
905
|
this.omitToolChoiceWhenReasoning =
|
|
898
906
|
options.omitToolChoiceWhenReasoning ?? false;
|
|
907
|
+
this.defaultToolChoiceAuto = options.defaultToolChoiceAuto ?? false;
|
|
899
908
|
this.outputTokenLimitField =
|
|
900
909
|
options.outputTokenLimitField ?? "max_completion_tokens";
|
|
901
910
|
}
|
|
@@ -958,6 +967,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
958
967
|
maxTokens;
|
|
959
968
|
}
|
|
960
969
|
|
|
970
|
+
// Per-conversation prompt-cache key (OpenAI Chat Completions
|
|
971
|
+
// `prompt_cache_key`). `RetryProvider` stamps it from the durable
|
|
972
|
+
// conversation id for providers in PROMPT_CACHE_KEY_PROVIDERS; omitted
|
|
973
|
+
// from the wire when absent.
|
|
974
|
+
const promptCacheKey = configObj?.promptCacheKey;
|
|
975
|
+
if (typeof promptCacheKey === "string" && promptCacheKey.length > 0) {
|
|
976
|
+
params.prompt_cache_key = promptCacheKey;
|
|
977
|
+
}
|
|
978
|
+
|
|
961
979
|
// Profile-scoped token biasing (e.g. the `suppress-cjk` preset). Resolved
|
|
962
980
|
// and gated to this provider family upstream in `RetryProvider`.
|
|
963
981
|
if (logitBias) {
|
|
@@ -1028,7 +1046,12 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1028
1046
|
// wire. Catalog providers that honor `none` / forced choices still
|
|
1029
1047
|
// receive them; the generic openai-compatible adapter drops every
|
|
1030
1048
|
// explicit value in thinking mode via `omitToolChoiceWhenReasoning`.
|
|
1031
|
-
const
|
|
1049
|
+
const callerToolChoice = mapNeutralToolChoice(configObj?.tool_choice);
|
|
1050
|
+
const toolChoice =
|
|
1051
|
+
callerToolChoice ??
|
|
1052
|
+
(this.defaultToolChoiceAuto
|
|
1053
|
+
? ("auto" as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption)
|
|
1054
|
+
: undefined);
|
|
1032
1055
|
xmlToolCallSalvageEnabled =
|
|
1033
1056
|
this.salvageXmlToolCalls && toolChoice !== "none";
|
|
1034
1057
|
if (toolChoice !== undefined) {
|
|
@@ -1149,6 +1172,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1149
1172
|
id: string;
|
|
1150
1173
|
name: string;
|
|
1151
1174
|
args: string;
|
|
1175
|
+
inspectableArgs?: string;
|
|
1152
1176
|
providerMetadata?: ToolUseContent["providerMetadata"];
|
|
1153
1177
|
}
|
|
1154
1178
|
>();
|
|
@@ -1450,6 +1474,7 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1450
1474
|
input = JSON.parse(tc.args);
|
|
1451
1475
|
} catch {
|
|
1452
1476
|
input = wrapUnparseableToolArgs(tc.args);
|
|
1477
|
+
tc.inspectableArgs = boundUnparseableToolArgsRaw(tc.args);
|
|
1453
1478
|
}
|
|
1454
1479
|
const objectKeys = coercedObjectKeys.get(tc.name);
|
|
1455
1480
|
if (objectKeys && !isUnparseableToolArgs(input)) {
|
|
@@ -1457,6 +1482,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1457
1482
|
input = decoded.failedKey
|
|
1458
1483
|
? wrapUnparseableToolArgs(tc.args)
|
|
1459
1484
|
: decoded.input;
|
|
1485
|
+
if (decoded.failedKey) {
|
|
1486
|
+
tc.inspectableArgs = boundUnparseableToolArgsRaw(tc.args);
|
|
1487
|
+
}
|
|
1460
1488
|
}
|
|
1461
1489
|
content.push({
|
|
1462
1490
|
type: "tool_use",
|
|
@@ -1484,7 +1512,10 @@ export class OpenAIChatCompletionsProvider implements Provider {
|
|
|
1484
1512
|
{
|
|
1485
1513
|
id: tc.id,
|
|
1486
1514
|
type: "function",
|
|
1487
|
-
function: {
|
|
1515
|
+
function: {
|
|
1516
|
+
name: tc.name,
|
|
1517
|
+
arguments: tc.inspectableArgs ?? tc.args,
|
|
1518
|
+
},
|
|
1488
1519
|
},
|
|
1489
1520
|
thoughtSignatureFromToolUseMetadata(
|
|
1490
1521
|
tc.providerMetadata,
|
package/src/providers/retry.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
shouldSkipPrimary,
|
|
36
36
|
tryAcquireRecoveryProbe,
|
|
37
37
|
} from "./fallback-breaker.js";
|
|
38
|
+
import { resolveFireworksRequestHeaders } from "./fireworks/client.js";
|
|
38
39
|
import { resolveLogitBiasPreset } from "./inference/logit-bias.js";
|
|
39
40
|
import { MALFORMED_TOOL_CALL_MESSAGE } from "./malformed-tool-call.js";
|
|
40
41
|
import {
|
|
@@ -77,10 +78,16 @@ const USAGE_ATTRIBUTION_HEADER_NAMES = {
|
|
|
77
78
|
subagentSpawnMode: "X-Vellum-Subagent-Spawn-Mode",
|
|
78
79
|
} as const;
|
|
79
80
|
|
|
80
|
-
/** Providers whose transports consume `promptCacheKey` (OpenAI Responses
|
|
81
|
-
* `prompt_cache_key`); `RetryProvider` derives it from
|
|
82
|
-
* these only.
|
|
83
|
-
|
|
81
|
+
/** Providers whose transports consume `promptCacheKey` (OpenAI Responses /
|
|
82
|
+
* Chat Completions `prompt_cache_key`); `RetryProvider` derives it from
|
|
83
|
+
* `selectionSeed` for these only. `openai-compatible` is included so
|
|
84
|
+
* self-hosted bridges (e.g. a Claude Code shim) receive a stable
|
|
85
|
+
* per-conversation key and can pin server-side sessions / prompt caches to it. */
|
|
86
|
+
const PROMPT_CACHE_KEY_PROVIDERS = new Set([
|
|
87
|
+
"openai",
|
|
88
|
+
"openrouter",
|
|
89
|
+
"openai-compatible",
|
|
90
|
+
]);
|
|
84
91
|
|
|
85
92
|
/** Providers that support the `effort` config (extended thinking / reasoning). */
|
|
86
93
|
const EFFORT_SUPPORTED_PROVIDERS = new Set([
|
|
@@ -681,6 +688,19 @@ function normalizeSendMessageOptions(
|
|
|
681
688
|
nextConfig.requestHeaders = resolveOpenCodeRequestHeaders(conversationId);
|
|
682
689
|
}
|
|
683
690
|
|
|
691
|
+
if (providerName === "fireworks") {
|
|
692
|
+
// Covers BYOK Fireworks and the Vellum-managed Fireworks upstream, which
|
|
693
|
+
// both dispatch through `FireworksProvider`. Keyless calls send no header.
|
|
694
|
+
const sessionKey = [config.selectionSeed, config.conversationId].find(
|
|
695
|
+
(value): value is string =>
|
|
696
|
+
typeof value === "string" && value.trim().length > 0,
|
|
697
|
+
);
|
|
698
|
+
const headers = resolveFireworksRequestHeaders(sessionKey);
|
|
699
|
+
if (Object.keys(headers).length > 0) {
|
|
700
|
+
nextConfig.requestHeaders = headers;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
684
704
|
// `overrideProfile`, `forceOverrideProfile`, `selectionSeed`,
|
|
685
705
|
// `conversationId`, and `nativeWebSearchSentinel` are routing/resolution-time
|
|
686
706
|
// concerns (consumed by the resolver below, `CallSiteRoutingProvider`'s
|
|
@@ -689,6 +709,7 @@ function normalizeSendMessageOptions(
|
|
|
689
709
|
// (after the `openai` promptCacheKey copy above) so they never leak into
|
|
690
710
|
// provider request bodies even when callers set them without a `callSite`.
|
|
691
711
|
delete nextConfig.overrideProfile;
|
|
712
|
+
delete nextConfig.overrideProfileOrigin;
|
|
692
713
|
delete nextConfig.forceOverrideProfile;
|
|
693
714
|
delete nextConfig.selectionSeed;
|
|
694
715
|
delete nextConfig.conversationId;
|
|
@@ -703,6 +724,7 @@ function normalizeSendMessageOptions(
|
|
|
703
724
|
const attribution = resolveUsageAttribution({
|
|
704
725
|
callSite: config.callSite,
|
|
705
726
|
overrideProfile: config.overrideProfile,
|
|
727
|
+
overrideProfileOrigin: config.overrideProfileOrigin,
|
|
706
728
|
forceOverrideProfile: config.forceOverrideProfile,
|
|
707
729
|
selectionSeed: config.selectionSeed,
|
|
708
730
|
});
|
|
@@ -912,6 +934,36 @@ function normalizeSendMessageOptions(
|
|
|
912
934
|
}
|
|
913
935
|
}
|
|
914
936
|
|
|
937
|
+
// Adaptive-thinking-only Claude models (Fable, Opus 5.5, Sonnet 5.5) cannot
|
|
938
|
+
// turn reasoning off, so stripping `thinking` below does not clear the
|
|
939
|
+
// forced-tool conflict and Anthropic 400s the request. Downgrade the forced
|
|
940
|
+
// choice to `auto` so the call goes out. The tool is no longer guaranteed:
|
|
941
|
+
// forced-tool call sites already treat a missing tool call as a failed
|
|
942
|
+
// result, which is what the 400 gave them on every request.
|
|
943
|
+
const forcedToolModel =
|
|
944
|
+
typeof nextConfig.model === "string" ? nextConfig.model : "";
|
|
945
|
+
const forcedToolChoice = nextConfig.tool_choice as
|
|
946
|
+
| Record<string, unknown>
|
|
947
|
+
| undefined;
|
|
948
|
+
if (
|
|
949
|
+
forcedToolChoice != null &&
|
|
950
|
+
(forcedToolChoice.type === "tool" || forcedToolChoice.type === "any") &&
|
|
951
|
+
isAdaptiveThinkingOnlyModel(forcedToolModel) &&
|
|
952
|
+
targetsAnthropicWire(providerName, forcedToolModel)
|
|
953
|
+
) {
|
|
954
|
+
log.warn(
|
|
955
|
+
{
|
|
956
|
+
providerName,
|
|
957
|
+
callSite: config.callSite,
|
|
958
|
+
model: forcedToolModel,
|
|
959
|
+
droppedToolChoice: forcedToolChoice,
|
|
960
|
+
},
|
|
961
|
+
"Downgrading forced `tool_choice` to `auto` because this model always " +
|
|
962
|
+
"reasons and rejects forced tool use.",
|
|
963
|
+
);
|
|
964
|
+
nextConfig.tool_choice = { type: "auto" };
|
|
965
|
+
}
|
|
966
|
+
|
|
915
967
|
// Anthropic (and the gateways fronting Anthropic) rejects requests that
|
|
916
968
|
// combine extended thinking with forced tool use (`tool_choice.type` of
|
|
917
969
|
// `"tool"` or `"any"`). Strip thinking when both are present so the
|
|
@@ -197,6 +197,16 @@ export const SEARCH_PROVIDER_CATALOG: readonly SearchProviderCatalogEntry[] = [
|
|
|
197
197
|
fallbackOrder: 9,
|
|
198
198
|
privacyPolicyUrl: "https://exa.ai/privacy-policy",
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
id: "serply",
|
|
202
|
+
displayName: "Serply",
|
|
203
|
+
kind: "byok",
|
|
204
|
+
apiKeyPrefix: "Serply API key...",
|
|
205
|
+
envVar: "SERPLY_API_KEY",
|
|
206
|
+
secretKey: "serply",
|
|
207
|
+
fallbackOrder: 10,
|
|
208
|
+
privacyPolicyUrl: "https://serply.io/privacy",
|
|
209
|
+
},
|
|
200
210
|
];
|
|
201
211
|
|
|
202
212
|
/** Provider ids accepted by the web-search config schema. */
|
package/src/providers/types.ts
CHANGED
|
@@ -346,6 +346,13 @@ export interface SendMessageConfig {
|
|
|
346
346
|
* silently fall through.
|
|
347
347
|
*/
|
|
348
348
|
overrideProfile?: string;
|
|
349
|
+
/**
|
|
350
|
+
* Who chose `overrideProfile`: `"auto"` when the Auto profile's router
|
|
351
|
+
* picked it for the turn. Usage attribution reports it as the profile
|
|
352
|
+
* source so a routed turn is not counted as a user pin. A routing-time
|
|
353
|
+
* concern only; stripped before any provider wire request.
|
|
354
|
+
*/
|
|
355
|
+
overrideProfileOrigin?: "auto";
|
|
349
356
|
/**
|
|
350
357
|
* When true, the resolver floats `overrideProfile` above the call-site
|
|
351
358
|
* layers (named site profile + call-site override) for non-main-agent call
|
|
@@ -405,8 +412,9 @@ export interface SendMessageConfig {
|
|
|
405
412
|
usageAttributionHeaders?: Record<string, string>;
|
|
406
413
|
/**
|
|
407
414
|
* Per-request HTTP headers merged onto the transport. `RetryProvider`
|
|
408
|
-
* stamps these for providers that need
|
|
409
|
-
* `x-opencode-session` / `x-opencode-request
|
|
415
|
+
* stamps these for providers that need per-conversation routing headers
|
|
416
|
+
* (OpenCode `x-opencode-session` / `x-opencode-request`, Fireworks
|
|
417
|
+
* `x-session-affinity`). Provider clients pass
|
|
410
418
|
* them through SDK request options only and must never include this
|
|
411
419
|
* object in provider JSON request bodies.
|
|
412
420
|
*/
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { safeStringSlice } from "../util/unicode.js";
|
|
17
|
+
|
|
17
18
|
const UNPARSEABLE_TOOL_ARGS_KEY = "_raw";
|
|
19
|
+
const UNPARSEABLE_TOOL_ARGS_PREVIEW_LIMIT = 200;
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Message emitted by the Anthropic SDK's stream accumulator when a tool_use
|
|
@@ -30,6 +32,32 @@ export function wrapUnparseableToolArgs(raw: string): Record<string, unknown> {
|
|
|
30
32
|
return { [UNPARSEABLE_TOOL_ARGS_KEY]: raw };
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Bound an unparseable marker before it joins history, events, or audit logs.
|
|
37
|
+
* The prefix is enough to diagnose where JSON broke without retaining a
|
|
38
|
+
* multi-megabyte partial tool call in every downstream copy.
|
|
39
|
+
*/
|
|
40
|
+
export function boundUnparseableToolArgs(
|
|
41
|
+
input: Record<string, unknown>,
|
|
42
|
+
): Record<string, unknown> {
|
|
43
|
+
if (!isUnparseableToolArgs(input)) {
|
|
44
|
+
return input;
|
|
45
|
+
}
|
|
46
|
+
const raw = input[UNPARSEABLE_TOOL_ARGS_KEY];
|
|
47
|
+
const bounded = boundUnparseableToolArgsRaw(raw);
|
|
48
|
+
return bounded === raw ? input : wrapUnparseableToolArgs(bounded);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Bound raw malformed arguments for diagnostic copies that retain the provider
|
|
53
|
+
* response shape rather than the internal `_raw` marker shape.
|
|
54
|
+
*/
|
|
55
|
+
export function boundUnparseableToolArgsRaw(raw: string): string {
|
|
56
|
+
return raw.length > UNPARSEABLE_TOOL_ARGS_PREVIEW_LIMIT
|
|
57
|
+
? `${safeStringSlice(raw, 0, UNPARSEABLE_TOOL_ARGS_PREVIEW_LIMIT)}…`
|
|
58
|
+
: raw;
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
/**
|
|
34
62
|
* Detect input produced by {@link wrapUnparseableToolArgs}: exactly one key,
|
|
35
63
|
* the marker key, holding the raw argument string. The exact-shape check
|
|
@@ -56,11 +84,7 @@ export function unparseableToolArgsMessage(
|
|
|
56
84
|
toolName: string,
|
|
57
85
|
raw: string,
|
|
58
86
|
): string {
|
|
59
|
-
const
|
|
60
|
-
const preview =
|
|
61
|
-
raw.length > PREVIEW_LIMIT
|
|
62
|
-
? `${safeStringSlice(raw, 0, PREVIEW_LIMIT)}…`
|
|
63
|
-
: raw;
|
|
87
|
+
const preview = boundUnparseableToolArgsRaw(raw);
|
|
64
88
|
return (
|
|
65
89
|
`Error: the arguments for "${toolName}" were not valid JSON — the argument stream was malformed or truncated, so the tool was NOT executed. ` +
|
|
66
90
|
`Received: ${preview || "(empty)"}\n` +
|
|
@@ -77,9 +77,11 @@ export class UsageTrackingProvider implements Provider {
|
|
|
77
77
|
const attribution = resolveUsageAttribution({
|
|
78
78
|
callSite: config.callSite,
|
|
79
79
|
overrideProfile: appliedOverride,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// A rerouted serve ran on the backup the wrapper chose, not on the
|
|
81
|
+
// router's pick, so the origin applies only to the original route.
|
|
82
|
+
...(response.actualInferenceProfile === undefined
|
|
83
|
+
? { overrideProfileOrigin: config.overrideProfileOrigin }
|
|
84
|
+
: { forceOverrideProfile: true }),
|
|
83
85
|
});
|
|
84
86
|
const providerName = response.actualProvider ?? this.inner.name;
|
|
85
87
|
const pricingUsage = buildPricingUsageFromResponse(
|
|
@@ -438,6 +438,29 @@ export class AssistantEventHub {
|
|
|
438
438
|
return undefined;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Whether `connectionId` is still an active client subscription, optionally
|
|
443
|
+
* required to belong to `clientId`.
|
|
444
|
+
*/
|
|
445
|
+
hasActiveClientConnection(
|
|
446
|
+
connectionId: string,
|
|
447
|
+
clientId?: string,
|
|
448
|
+
): boolean {
|
|
449
|
+
for (const entry of this.subscribers) {
|
|
450
|
+
if (entry.connectionId !== connectionId) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (!entry.active || entry.type !== "client") {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
if (clientId != null && entry.clientId !== clientId) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
441
464
|
/**
|
|
442
465
|
* Return the verified actor principal id captured at SSE subscription time
|
|
443
466
|
* for the given client, or `undefined` if the client is unknown or
|
|
@@ -68,11 +68,6 @@ describe("route policy coverage", () => {
|
|
|
68
68
|
// handler calls `assertPlaygroundEnabled()`, so the surface
|
|
69
69
|
// is invisible in prod regardless of policy.
|
|
70
70
|
//
|
|
71
|
-
// GROUP C — pre-existing latent unprotected on main (the prior
|
|
72
|
-
// registry had no entry for these endpoints, so `enforcePolicy`
|
|
73
|
-
// returned allowed). Migration preserves behavior. Triage these
|
|
74
|
-
// and assign real policies in a follow-up PR:
|
|
75
|
-
// - integrations/a2a/invite/accept
|
|
76
71
|
const INTENTIONALLY_UNPROTECTED = new Set([
|
|
77
72
|
// A — design-intentional
|
|
78
73
|
"health",
|
|
@@ -87,8 +82,6 @@ describe("route policy coverage", () => {
|
|
|
87
82
|
"playground/seed-conversation",
|
|
88
83
|
"playground/seeded-conversations",
|
|
89
84
|
"playground/seeded-conversations/:id",
|
|
90
|
-
// C — pre-existing latent unprotected (follow-up audit owed)
|
|
91
|
-
"integrations/a2a/invite/accept",
|
|
92
85
|
]);
|
|
93
86
|
|
|
94
87
|
const unprotectedFound: string[] = [];
|
|
@@ -223,7 +223,7 @@ describe("enforcePolicy", () => {
|
|
|
223
223
|
// An oauth_proxy_v1 grant is printed for a user to export into a stock
|
|
224
224
|
// third-party CLI's environment, so it sits outside this install's trust
|
|
225
225
|
// boundary. `policy: null` admits any valid token, which for this grant means
|
|
226
|
-
// mutating routes it was never minted for
|
|
226
|
+
// mutating routes it was never minted for.
|
|
227
227
|
// ---------------------------------------------------------------------------
|
|
228
228
|
|
|
229
229
|
/** The context an exported OAuth proxy grant produces. */
|
|
@@ -3,6 +3,7 @@ import type { MessageAudience } from "@vellumai/gateway-client";
|
|
|
3
3
|
import { stripVellumLinks } from "../daemon/assistant-attachments.js";
|
|
4
4
|
import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
|
|
5
5
|
import { renderHistoryContent } from "../daemon/handlers/shared.js";
|
|
6
|
+
import { DEFAULT_LOCALE } from "../i18n/index.js";
|
|
6
7
|
import { editChannelMessage } from "../messaging/providers/index.js";
|
|
7
8
|
import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
|
|
8
9
|
import {
|
|
@@ -344,6 +345,7 @@ function readPersistedAssistantReply(msg: PersistedMessage): {
|
|
|
344
345
|
undefined,
|
|
345
346
|
undefined,
|
|
346
347
|
msg.metadata,
|
|
348
|
+
DEFAULT_LOCALE,
|
|
347
349
|
);
|
|
348
350
|
|
|
349
351
|
const linked = getAttachmentMetadataForMessage(msg.id);
|
|
@@ -75,9 +75,8 @@ function parseTrustRuntimeContext(value: unknown): TrustContext | undefined {
|
|
|
75
75
|
if (!rawSourceChannel || !isChannelId(rawSourceChannel)) {
|
|
76
76
|
return undefined;
|
|
77
77
|
}
|
|
78
|
-
const sourceChannel = rawSourceChannel;
|
|
79
78
|
return {
|
|
80
|
-
sourceChannel,
|
|
79
|
+
sourceChannel: rawSourceChannel,
|
|
81
80
|
trustClass,
|
|
82
81
|
guardianChatId:
|
|
83
82
|
typeof raw.guardianChatId === "string" ? raw.guardianChatId : undefined,
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
ConversationCreateOptions,
|
|
13
13
|
SlackInboundMessageMetadata,
|
|
14
14
|
} from "../daemon/handlers/shared.js";
|
|
15
|
+
import type { SupportedLocale } from "../i18n/index.js";
|
|
15
16
|
|
|
16
17
|
// Re-export so route modules (background-dispatch, etc.) can pull the type
|
|
17
18
|
// from the runtime barrel without reaching into daemon internals.
|
|
@@ -110,6 +111,8 @@ export interface RuntimeMessageConversationOptions {
|
|
|
110
111
|
sourceChannel?: ChannelId;
|
|
111
112
|
/** Originating interface (e.g. "cli", "web"). Defaults to "web". */
|
|
112
113
|
sourceInterface?: InterfaceId;
|
|
114
|
+
/** Locale for daemon-authored text emitted during this turn. */
|
|
115
|
+
locale?: SupportedLocale;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
export type MessageProcessor = (
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
computeLegacyManifestSha256,
|
|
32
32
|
computeManifestChecksum,
|
|
33
|
+
MANIFEST_MAX_BYTES,
|
|
33
34
|
} from "../vbundle-validator.js";
|
|
34
35
|
import { defaultV1Options } from "./v1-test-helpers.js";
|
|
35
36
|
|
|
@@ -202,15 +203,16 @@ describe("readAndValidateManifest — negative paths", () => {
|
|
|
202
203
|
});
|
|
203
204
|
|
|
204
205
|
test("throws manifest_too_large and fails fast before draining the whole body", async () => {
|
|
205
|
-
// Fake a tar entry whose body would emit
|
|
206
|
-
// validator must destroy() the stream and throw the moment the
|
|
207
|
-
// byte count crosses
|
|
206
|
+
// Fake a tar entry whose body would emit 10x the cap if fully drained.
|
|
207
|
+
// The validator must destroy() the stream and throw the moment the
|
|
208
|
+
// running byte count crosses MANIFEST_MAX_BYTES; it must NOT keep
|
|
209
|
+
// pulling chunks.
|
|
208
210
|
//
|
|
209
211
|
// We count bytes emitted via a _read implementation, and after the
|
|
210
|
-
// throw assert both that destroy() fired and that far
|
|
211
|
-
//
|
|
212
|
-
const CHUNK =
|
|
213
|
-
const TOTAL_CHUNKS =
|
|
212
|
+
// throw assert both that destroy() fired and that far less than the
|
|
213
|
+
// full body was ever pulled out of the stream.
|
|
214
|
+
const CHUNK = MANIFEST_MAX_BYTES / 2;
|
|
215
|
+
const TOTAL_CHUNKS = 20;
|
|
214
216
|
let chunksEmitted = 0;
|
|
215
217
|
let bytesEmitted = 0;
|
|
216
218
|
const body = new Readable({
|
|
@@ -245,11 +247,34 @@ describe("readAndValidateManifest — negative paths", () => {
|
|
|
245
247
|
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
246
248
|
expect(err?.code).toBe("manifest_too_large");
|
|
247
249
|
// Fail-fast assertions: destroy() was called, and we didn't drain past
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
+
// the cap + one chunk. If the validator had drained to EOF we'd see all
|
|
251
|
+
// 20 chunks here.
|
|
250
252
|
expect(body.destroyed).toBe(true);
|
|
251
253
|
expect(chunksEmitted).toBeLessThanOrEqual(3);
|
|
252
|
-
expect(bytesEmitted).toBeLessThan(2 *
|
|
254
|
+
expect(bytesEmitted).toBeLessThan(2 * MANIFEST_MAX_BYTES);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("throws bundle_too_many_entries when manifest.contents exceeds maxEntries", async () => {
|
|
258
|
+
const { archive } = buildVBundle({
|
|
259
|
+
files: [
|
|
260
|
+
{ path: "data/db/assistant.db", data: new Uint8Array() },
|
|
261
|
+
{ path: "workspace/a.txt", data: new TextEncoder().encode("a") },
|
|
262
|
+
{ path: "workspace/b.txt", data: new TextEncoder().encode("b") },
|
|
263
|
+
],
|
|
264
|
+
...defaultV1Options(),
|
|
265
|
+
});
|
|
266
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
267
|
+
|
|
268
|
+
let err: StreamingValidationError | null = null;
|
|
269
|
+
try {
|
|
270
|
+
await readAndValidateManifest(entry, { maxEntries: 2 });
|
|
271
|
+
} catch (e) {
|
|
272
|
+
err = e as StreamingValidationError;
|
|
273
|
+
}
|
|
274
|
+
await drainRest();
|
|
275
|
+
|
|
276
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
277
|
+
expect(err?.code).toBe("bundle_too_many_entries");
|
|
253
278
|
});
|
|
254
279
|
|
|
255
280
|
test("throws manifest_malformed when manifest body is not valid JSON", async () => {
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
canonicalizeJson,
|
|
20
20
|
computeLegacyManifestSha256,
|
|
21
21
|
computeManifestChecksum,
|
|
22
|
+
MANIFEST_MAX_BYTES,
|
|
23
|
+
MANIFEST_MAX_ENTRIES,
|
|
22
24
|
ManifestSchema,
|
|
23
25
|
validateVBundle,
|
|
24
26
|
} from "../vbundle-validator.js";
|
|
@@ -174,6 +176,52 @@ describe("ManifestSchema — v1 acceptance", () => {
|
|
|
174
176
|
});
|
|
175
177
|
});
|
|
176
178
|
|
|
179
|
+
describe("validateVBundle: streaming manifest limits", () => {
|
|
180
|
+
test("rejects a manifest over MANIFEST_MAX_BYTES with MANIFEST_TOO_LARGE", () => {
|
|
181
|
+
const oversized = new Uint8Array(MANIFEST_MAX_BYTES + 1).fill(0x20);
|
|
182
|
+
const archive = gzipSync(
|
|
183
|
+
tarArchive([{ name: "manifest.json", data: oversized }]),
|
|
184
|
+
);
|
|
185
|
+
const result = validateVBundle(archive, { enforceStreamingLimits: true });
|
|
186
|
+
expect(result.is_valid).toBe(false);
|
|
187
|
+
expect(result.errors.map((e) => e.code)).toEqual(["MANIFEST_TOO_LARGE"]);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("rejects a manifest declaring more than MANIFEST_MAX_ENTRIES with MANIFEST_TOO_MANY_ENTRIES", () => {
|
|
191
|
+
const skeleton = v1Skeleton();
|
|
192
|
+
const dbEntry = (skeleton.contents as Array<Record<string, unknown>>)[0]!;
|
|
193
|
+
const extra = Array.from({ length: MANIFEST_MAX_ENTRIES }, (_, i) => ({
|
|
194
|
+
...dbEntry,
|
|
195
|
+
path: `workspace/${i}`,
|
|
196
|
+
}));
|
|
197
|
+
const archive = gzipTarOf(
|
|
198
|
+
withChecksum({ ...skeleton, contents: [dbEntry, ...extra] }),
|
|
199
|
+
);
|
|
200
|
+
const result = validateVBundle(archive, { enforceStreamingLimits: true });
|
|
201
|
+
expect(result.is_valid).toBe(false);
|
|
202
|
+
expect(result.errors.map((e) => e.code)).toEqual([
|
|
203
|
+
"MANIFEST_TOO_MANY_ENTRIES",
|
|
204
|
+
]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("does not apply the limits unless enforceStreamingLimits is set", () => {
|
|
208
|
+
const skeleton = v1Skeleton();
|
|
209
|
+
const dbEntry = (skeleton.contents as Array<Record<string, unknown>>)[0]!;
|
|
210
|
+
const extra = Array.from({ length: MANIFEST_MAX_ENTRIES }, (_, i) => ({
|
|
211
|
+
...dbEntry,
|
|
212
|
+
path: `workspace/${i}`,
|
|
213
|
+
}));
|
|
214
|
+
const archive = gzipTarOf(
|
|
215
|
+
withChecksum({ ...skeleton, contents: [dbEntry, ...extra] }),
|
|
216
|
+
[{ name: "data/db/assistant.db", data: DB_BYTES }],
|
|
217
|
+
);
|
|
218
|
+
const result = validateVBundle(archive);
|
|
219
|
+
expect(result.errors.map((e) => e.code)).not.toContain(
|
|
220
|
+
"MANIFEST_TOO_MANY_ENTRIES",
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
177
225
|
describe("ManifestSchema — v1 rejection", () => {
|
|
178
226
|
test("rejects schema_version !== 1", () => {
|
|
179
227
|
const manifest = withChecksum({ ...v1Skeleton(), schema_version: 2 });
|
|
@@ -31,9 +31,11 @@ import { sanitizeConfigForTransfer } from "../../config/sanitize-for-transfer.js
|
|
|
31
31
|
import { trackDaemonActivity } from "../../daemon/activity-trail.js";
|
|
32
32
|
import { getLogger } from "../../util/logger.js";
|
|
33
33
|
import type { VBundleOriginMode } from "./origin-mode.js";
|
|
34
|
-
import
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
import {
|
|
35
|
+
MANIFEST_MAX_BYTES,
|
|
36
|
+
MANIFEST_MAX_ENTRIES,
|
|
37
|
+
type ManifestFileEntryType,
|
|
38
|
+
type ManifestType,
|
|
37
39
|
} from "./vbundle-validator.js";
|
|
38
40
|
|
|
39
41
|
// ---------------------------------------------------------------------------
|
|
@@ -400,6 +402,20 @@ function buildManifestObject(input: {
|
|
|
400
402
|
const checksum = sha256Hex(canonicalizeJson(manifestWithEmptyChecksum));
|
|
401
403
|
const manifest: ManifestType = { ...manifestWithEmptyChecksum, checksum };
|
|
402
404
|
const manifestData = new TextEncoder().encode(JSON.stringify(manifest));
|
|
405
|
+
if (
|
|
406
|
+
manifest.contents.length > MANIFEST_MAX_ENTRIES ||
|
|
407
|
+
manifestData.length > MANIFEST_MAX_BYTES
|
|
408
|
+
) {
|
|
409
|
+
getLogger("vbundle-builder").warn(
|
|
410
|
+
{
|
|
411
|
+
entries: manifest.contents.length,
|
|
412
|
+
manifestBytes: manifestData.length,
|
|
413
|
+
maxEntries: MANIFEST_MAX_ENTRIES,
|
|
414
|
+
maxBytes: MANIFEST_MAX_BYTES,
|
|
415
|
+
},
|
|
416
|
+
"Bundle manifest exceeds the streaming import limits; teleport and staged-path imports will reject this bundle",
|
|
417
|
+
);
|
|
418
|
+
}
|
|
403
419
|
return { manifest, manifestData };
|
|
404
420
|
}
|
|
405
421
|
|