@vellumai/assistant 0.6.4 → 0.6.5
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/.prettierignore +5 -0
- package/ARCHITECTURE.md +32 -36
- package/Dockerfile +12 -0
- package/README.md +3 -4
- package/bun.lock +8 -3
- package/docs/architecture/integrations.md +1 -20
- package/docs/architecture/security.md +16 -16
- package/docs/error-handling.md +111 -0
- package/docs/skills.md +10 -10
- package/docs/stt-provider-onboarding.md +2 -1
- package/knip.json +9 -2
- package/node_modules/@vellumai/ces-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
- package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
- package/node_modules/@vellumai/credential-storage/package.json +2 -2
- package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
- package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
- package/node_modules/@vellumai/egress-proxy/package.json +2 -2
- package/openapi.yaml +123 -11
- package/package.json +6 -3
- package/scripts/generate-openapi.ts +50 -11
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
- package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
- package/src/__tests__/agent-loop.test.ts +112 -1
- package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
- package/src/__tests__/anthropic-provider.test.ts +171 -2
- package/src/__tests__/approval-cascade.test.ts +31 -10
- package/src/__tests__/approval-routes-http.test.ts +134 -10
- package/src/__tests__/assistant-attachments.test.ts +44 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
- package/src/__tests__/browser-skill-endstate.test.ts +51 -182
- package/src/__tests__/btw-routes.test.ts +47 -1
- package/src/__tests__/call-controller.test.ts +1 -2
- package/src/__tests__/call-site-routing-provider.test.ts +214 -0
- package/src/__tests__/catalog-cache.test.ts +27 -4
- package/src/__tests__/channel-approval-routes.test.ts +4 -4
- package/src/__tests__/channel-reply-delivery.test.ts +300 -2
- package/src/__tests__/checker.test.ts +428 -501
- package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
- package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/config-analysis.test.ts +11 -28
- package/src/__tests__/config-loader-backfill.test.ts +174 -0
- package/src/__tests__/config-loader-corrupt.test.ts +183 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
- package/src/__tests__/config-schema-cmd.test.ts +11 -5
- package/src/__tests__/config-schema.test.ts +427 -114
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +72 -73
- package/src/__tests__/contacts-write.test.ts +4 -4
- package/src/__tests__/context-token-estimator.test.ts +191 -1
- package/src/__tests__/context-window-manager.test.ts +530 -2
- package/src/__tests__/conversation-abort-tool-results.test.ts +30 -16
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +61 -17
- package/src/__tests__/conversation-agent-loop.test.ts +412 -82
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +30 -9
- package/src/__tests__/conversation-error.test.ts +37 -6
- package/src/__tests__/conversation-history-web-search.test.ts +6 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +36 -0
- package/src/__tests__/conversation-lifecycle.test.ts +336 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +30 -16
- package/src/__tests__/conversation-process-callsite.test.ts +306 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -16
- package/src/__tests__/conversation-queue.test.ts +41 -26
- package/src/__tests__/conversation-routes-disk-view.test.ts +29 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +2735 -55
- package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
- package/src/__tests__/conversation-skill-tools.test.ts +12 -146
- package/src/__tests__/conversation-slash-queue.test.ts +34 -19
- package/src/__tests__/conversation-slash-unknown.test.ts +30 -16
- package/src/__tests__/conversation-speed-override.test.ts +30 -11
- package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
- package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -2
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
- package/src/__tests__/conversation-unread-route.test.ts +2 -2
- package/src/__tests__/conversation-usage.test.ts +3 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
- package/src/__tests__/conversation-workspace-injection.test.ts +43 -15
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +44 -16
- package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
- package/src/__tests__/credential-security-invariants.test.ts +3 -0
- package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
- package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
- package/src/__tests__/credential-vault-unit.test.ts +135 -19
- package/src/__tests__/credentials-cli.test.ts +1 -9
- package/src/__tests__/cross-provider-web-search.test.ts +84 -0
- package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
- package/src/__tests__/delete-propagation.test.ts +437 -0
- package/src/__tests__/dm-backfill.test.ts +417 -0
- package/src/__tests__/dm-persistence.test.ts +227 -0
- package/src/__tests__/edit-propagation.test.ts +280 -0
- package/src/__tests__/ephemeral-permissions.test.ts +93 -3
- package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
- package/src/__tests__/estimator-calibration.test.ts +213 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +26 -7
- package/src/__tests__/file-write-tool.test.ts +151 -1
- package/src/__tests__/filing-service.test.ts +255 -0
- package/src/__tests__/gemini-provider.test.ts +0 -3
- package/src/__tests__/guardian-grant-minting.test.ts +8 -0
- package/src/__tests__/headless-browser-interactions.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +96 -15
- package/src/__tests__/host-shell-tool.test.ts +124 -18
- package/src/__tests__/http-user-message-parity.test.ts +29 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
- package/src/__tests__/intent-routing.test.ts +1 -40
- package/src/__tests__/llm-catalog-parity.test.ts +174 -0
- package/src/__tests__/llm-context-normalization.test.ts +121 -0
- package/src/__tests__/llm-resolver.test.ts +214 -0
- package/src/__tests__/llm-schema.test.ts +223 -0
- package/src/__tests__/managed-proxy-context.test.ts +6 -2
- package/src/__tests__/messaging-skill-split.test.ts +3 -34
- package/src/__tests__/migration-import-from-url.test.ts +684 -0
- package/src/__tests__/model-intents.test.ts +9 -83
- package/src/__tests__/notification-decision-fallback.test.ts +0 -10
- package/src/__tests__/notification-decision-identity.test.ts +0 -9
- package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
- package/src/__tests__/oauth-store.test.ts +10 -7
- package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
- package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
- package/src/__tests__/openai-provider.test.ts +7 -0
- package/src/__tests__/openai-responses-provider.test.ts +396 -0
- package/src/__tests__/openrouter-provider-only.test.ts +135 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
- package/src/__tests__/permission-mode.test.ts +16 -0
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/persona-resolver.test.ts +13 -13
- package/src/__tests__/pkb-autoinject.test.ts +37 -1
- package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
- package/src/__tests__/pricing.test.ts +50 -3
- package/src/__tests__/profiler-routes.test.ts +1 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
- package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
- package/src/__tests__/provider-error-scenarios.test.ts +135 -6
- package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
- package/src/__tests__/provider-registry-ollama.test.ts +1 -2
- package/src/__tests__/proxy-approval-callback.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +560 -0
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
- package/src/__tests__/risk-classifier-parity.test.ts +230 -0
- package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
- package/src/__tests__/secret-ingress-http.test.ts +28 -0
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
- package/src/__tests__/secret-scanner-executor.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +29 -1
- package/src/__tests__/server-history-render.test.ts +31 -0
- package/src/__tests__/shell-parser-property.test.ts +13 -13
- package/src/__tests__/skill-cache-store.test.ts +182 -0
- package/src/__tests__/skills.test.ts +19 -33
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
- package/src/__tests__/slack-skill.test.ts +3 -8
- package/src/__tests__/starter-bundle.test.ts +35 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
- package/src/__tests__/suggestion-routes.test.ts +160 -3
- package/src/__tests__/system-prompt.test.ts +22 -35
- package/src/__tests__/task-runner.test.ts +3 -1
- package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
- package/src/__tests__/thread-backfill.test.ts +941 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
- package/src/__tests__/tool-executor.test.ts +60 -94
- package/src/__tests__/trust-store.test.ts +442 -109
- package/src/__tests__/update-bulletin-job.test.ts +389 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
- package/src/__tests__/voice-session-bridge.test.ts +39 -0
- package/src/__tests__/volume-security-guard.test.ts +3 -2
- package/src/__tests__/web-search-history.test.ts +337 -0
- package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
- package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
- package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
- package/src/__tests__/workspace-policy.test.ts +1 -13
- package/src/acp/client-handler.ts +1 -2
- package/src/agent/loop.ts +209 -17
- package/src/avatar/resvg-lazy.test.ts +136 -0
- package/src/avatar/resvg-lazy.ts +82 -9
- package/src/avatar/traits-png-sync.ts +21 -1
- package/src/browser/__tests__/operations.test.ts +163 -0
- package/src/browser/identifiers.ts +51 -0
- package/src/browser/operations.ts +660 -0
- package/src/browser/types.ts +81 -0
- package/src/calls/guardian-question-copy.ts +2 -2
- package/src/calls/telephony-stt-routing.ts +1 -1
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/AGENTS.md +1 -1
- package/src/cli/commands/__tests__/attachment.test.ts +438 -0
- package/src/cli/commands/__tests__/browser.test.ts +554 -0
- package/src/cli/commands/__tests__/cache.test.ts +623 -0
- package/src/cli/commands/__tests__/email-list.test.ts +6 -0
- package/src/cli/commands/__tests__/email-send.test.ts +93 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
- package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
- package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
- package/src/cli/commands/__tests__/task.test.ts +913 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
- package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
- package/src/cli/commands/__tests__/ui.test.ts +1215 -0
- package/src/cli/commands/__tests__/watchers.test.ts +716 -0
- package/src/cli/commands/attachment.ts +182 -0
- package/src/cli/commands/browser.ts +350 -0
- package/src/cli/commands/cache.ts +341 -0
- package/src/cli/commands/completions.ts +0 -3
- package/src/cli/commands/config.ts +6 -6
- package/src/cli/commands/conversations-import.ts +347 -0
- package/src/cli/commands/conversations.ts +14 -1
- package/src/cli/commands/email.ts +234 -194
- package/src/cli/commands/image-generation.ts +300 -0
- package/src/cli/commands/inference.ts +200 -0
- package/src/cli/commands/memory.ts +127 -17
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
- package/src/cli/commands/stt.ts +339 -0
- package/src/cli/commands/task.ts +795 -0
- package/src/cli/commands/trust.ts +50 -19
- package/src/cli/commands/tts.ts +273 -0
- package/src/cli/commands/ui.ts +670 -0
- package/src/cli/commands/watchers.ts +509 -0
- package/src/cli/lib/daemon-credential-client.ts +0 -19
- package/src/cli/program.ts +23 -4
- package/src/cli.ts +0 -37
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
- package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +8 -1
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-tool-registry.ts +0 -175
- package/src/config/env.ts +7 -2
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/llm-resolver.ts +128 -0
- package/src/config/loader.ts +194 -10
- package/src/config/raw-config-utils.ts +30 -2
- package/src/config/sanitize-for-transfer.ts +35 -0
- package/src/config/schema.ts +30 -41
- package/src/config/schemas/analysis.ts +3 -22
- package/src/config/schemas/calls.ts +0 -4
- package/src/config/schemas/filing.ts +2 -7
- package/src/config/schemas/heartbeat.ts +0 -5
- package/src/config/schemas/inference.ts +3 -23
- package/src/config/schemas/llm.ts +318 -0
- package/src/config/schemas/memory-processing.ts +1 -9
- package/src/config/schemas/notifications.ts +4 -11
- package/src/config/schemas/platform.ts +3 -9
- package/src/config/schemas/security.ts +33 -0
- package/src/config/schemas/services.ts +9 -4
- package/src/config/schemas/stt.ts +1 -0
- package/src/config/schemas/tts.ts +53 -0
- package/src/config/schemas/updates.ts +1 -1
- package/src/config/schemas/workspace-git.ts +3 -40
- package/src/config/skills.ts +2 -2
- package/src/context/__tests__/compact-prompt.test.ts +45 -0
- package/src/context/__tests__/microcompact.test.ts +805 -0
- package/src/context/estimator-calibration.ts +136 -0
- package/src/context/microcompact.ts +443 -0
- package/src/context/prompts/compact.md +12 -0
- package/src/context/token-estimator.ts +61 -3
- package/src/context/window-manager.ts +229 -25
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/executable-discovery.ts +19 -8
- package/src/credential-execution/process-manager.test.ts +109 -0
- package/src/credential-execution/process-manager.ts +65 -2
- package/src/daemon/approval-generators.ts +29 -4
- package/src/daemon/assistant-attachments.ts +24 -13
- package/src/daemon/classifier.ts +2 -2
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/context-overflow-reducer.ts +4 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +79 -12
- package/src/daemon/conversation-agent-loop.ts +462 -80
- package/src/daemon/conversation-attachments.ts +2 -6
- package/src/daemon/conversation-error.ts +36 -1
- package/src/daemon/conversation-lifecycle.ts +30 -6
- package/src/daemon/conversation-messaging.ts +73 -4
- package/src/daemon/conversation-process.ts +10 -4
- package/src/daemon/conversation-queue-manager.ts +3 -0
- package/src/daemon/conversation-runtime-assembly.ts +760 -29
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +389 -1
- package/src/daemon/conversation-tool-setup.ts +10 -5
- package/src/daemon/conversation-usage.ts +1 -1
- package/src/daemon/conversation.ts +118 -30
- package/src/daemon/external-skills-bootstrap.ts +41 -0
- package/src/daemon/guardian-action-generators.ts +34 -14
- package/src/daemon/handlers/config-model.test.ts +86 -0
- package/src/daemon/handlers/config-model.ts +54 -12
- package/src/daemon/handlers/conversations.ts +9 -2
- package/src/daemon/handlers/shared.ts +39 -11
- package/src/daemon/handlers/skills.ts +2 -2
- package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
- package/src/daemon/lifecycle.ts +76 -14
- package/src/daemon/message-types/conversations.ts +14 -0
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/trust.ts +0 -2
- package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
- package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
- package/src/daemon/pkb-context-tracker.test.ts +169 -0
- package/src/daemon/pkb-context-tracker.ts +125 -0
- package/src/daemon/pkb-reminder-builder.test.ts +70 -0
- package/src/daemon/pkb-reminder-builder.ts +31 -0
- package/src/daemon/providers-setup.ts +6 -0
- package/src/daemon/server.ts +117 -9
- package/src/daemon/tool-side-effects.ts +0 -9
- package/src/daemon/watch-handler.ts +4 -4
- package/src/daemon/web-search-history.ts +126 -0
- package/src/events/domain-events.ts +0 -1
- package/src/filing/filing-service.ts +9 -10
- package/src/heartbeat/heartbeat-service.ts +76 -28
- package/src/home/__tests__/feed-scheduler.test.ts +39 -11
- package/src/home/__tests__/rollup-producer.test.ts +44 -0
- package/src/home/assistant-feed-authoring.ts +4 -0
- package/src/home/emit-feed-event.ts +4 -0
- package/src/home/feed-scheduler.ts +20 -4
- package/src/home/feed-types.ts +56 -2
- package/src/home/relationship-state-writer.ts +2 -2
- package/src/home/rollup-producer.ts +34 -5
- package/src/home/suggested-prompts.ts +101 -0
- package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
- package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
- package/src/ipc/__tests__/socket-path.test.ts +73 -0
- package/src/ipc/__tests__/task-ipc.test.ts +577 -0
- package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
- package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
- package/src/ipc/cli-client.ts +2 -1
- package/src/ipc/cli-server.ts +26 -8
- package/src/ipc/gateway-client.ts +4 -4
- package/src/ipc/routes/attachment.ts +114 -0
- package/src/ipc/routes/browser-context.ts +61 -0
- package/src/ipc/routes/browser.ts +96 -0
- package/src/ipc/routes/cache.ts +96 -0
- package/src/ipc/routes/index.ts +17 -1
- package/src/ipc/routes/task-queue.ts +226 -0
- package/src/ipc/routes/task.ts +173 -0
- package/src/ipc/routes/ui-request.ts +50 -0
- package/src/ipc/routes/watcher.ts +203 -0
- package/src/ipc/socket-path.ts +100 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
- package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
- package/src/memory/admin.ts +18 -0
- package/src/memory/conversation-analyze-job.ts +14 -13
- package/src/memory/conversation-attention-store.ts +13 -6
- package/src/memory/conversation-crud.ts +103 -3
- package/src/memory/conversation-group-migration.ts +38 -6
- package/src/memory/conversation-title-service.ts +7 -4
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-backend.ts +1 -1
- package/src/memory/graph/compaction.ts +299 -0
- package/src/memory/graph/consolidation.ts +4 -4
- package/src/memory/graph/conversation-graph-memory.ts +89 -29
- package/src/memory/graph/extraction.test.ts +272 -2
- package/src/memory/graph/extraction.ts +173 -51
- package/src/memory/graph/graph-search.test.ts +92 -0
- package/src/memory/graph/graph-search.ts +4 -1
- package/src/memory/graph/narrative.ts +2 -2
- package/src/memory/graph/pattern-scan.ts +2 -2
- package/src/memory/graph/retriever.test.ts +459 -0
- package/src/memory/graph/retriever.ts +230 -48
- package/src/memory/graph/store.ts +41 -0
- package/src/memory/graph/tool-handlers.ts +27 -0
- package/src/memory/graph/tools.ts +6 -1
- package/src/memory/indexer.ts +5 -5
- package/src/memory/job-handlers/conversation-starters.ts +23 -20
- package/src/memory/job-handlers/summarization.ts +2 -2
- package/src/memory/job-utils.ts +7 -1
- package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
- package/src/memory/jobs/embed-pkb-file.ts +54 -0
- package/src/memory/jobs-store.ts +44 -3
- package/src/memory/jobs-worker.ts +4 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
- package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-index.test.ts +368 -0
- package/src/memory/pkb/pkb-index.ts +255 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
- package/src/memory/pkb/pkb-reconcile.ts +148 -0
- package/src/memory/pkb/pkb-search.test.ts +438 -0
- package/src/memory/pkb/pkb-search.ts +137 -0
- package/src/memory/pkb/types.ts +53 -0
- package/src/memory/qdrant-client.ts +122 -1
- package/src/memory/slack-thread-store.ts +37 -0
- package/src/messaging/providers/gmail/adapter.ts +6 -16
- package/src/messaging/providers/gmail/client.ts +22 -0
- package/src/messaging/providers/gmail/types.ts +7 -0
- package/src/messaging/providers/slack/adapter.ts +14 -2
- package/src/messaging/providers/slack/backfill.test.ts +257 -0
- package/src/messaging/providers/slack/backfill.ts +101 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
- package/src/messaging/providers/slack/message-metadata.ts +123 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
- package/src/messaging/providers/slack/render-transcript.ts +443 -0
- package/src/messaging/style-analyzer.ts +5 -2
- package/src/notifications/README.md +9 -5
- package/src/notifications/decision-engine.ts +3 -9
- package/src/notifications/preference-extractor.ts +2 -6
- package/src/oauth/oauth-store.ts +1 -0
- package/src/oauth/platform-connection.test.ts +47 -0
- package/src/oauth/platform-connection.ts +15 -5
- package/src/oauth/seed-providers.ts +4 -2
- package/src/permissions/approval-policy.test.ts +948 -0
- package/src/permissions/approval-policy.ts +257 -0
- package/src/permissions/bash-risk-classifier.test.ts +1208 -0
- package/src/permissions/bash-risk-classifier.ts +707 -0
- package/src/permissions/checker.ts +217 -708
- package/src/permissions/command-registry.test.ts +535 -0
- package/src/permissions/command-registry.ts +825 -0
- package/src/permissions/defaults.ts +26 -78
- package/src/permissions/file-risk-classifier.test.ts +535 -0
- package/src/permissions/file-risk-classifier.ts +274 -0
- package/src/permissions/risk-types.ts +205 -0
- package/src/permissions/secret-prompter.ts +53 -2
- package/src/permissions/skill-risk-classifier.test.ts +311 -0
- package/src/permissions/skill-risk-classifier.ts +214 -0
- package/src/permissions/trust-client.ts +52 -25
- package/src/permissions/trust-store-interface.ts +1 -6
- package/src/permissions/trust-store.ts +161 -62
- package/src/permissions/types.ts +23 -14
- package/src/permissions/web-risk-classifier.test.ts +170 -0
- package/src/permissions/web-risk-classifier.ts +89 -0
- package/src/permissions/workspace-policy.ts +1 -16
- package/src/platform/client.ts +19 -1
- package/src/prompts/persona-resolver.ts +3 -3
- package/src/prompts/system-prompt.ts +19 -20
- package/src/prompts/templates/SOUL.md +2 -2
- package/src/prompts/update-bulletin-job.ts +190 -0
- package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
- package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
- package/src/providers/__tests__/retry-callsite.test.ts +424 -0
- package/src/providers/anthropic/client.ts +183 -14
- package/src/providers/call-site-routing.ts +71 -0
- package/src/providers/gemini/client.ts +65 -2
- package/src/providers/managed-proxy/constants.ts +2 -1
- package/src/providers/model-catalog.ts +501 -33
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +57 -1
- package/src/providers/openai/responses-provider.ts +86 -9
- package/src/providers/openrouter/client.ts +76 -9
- package/src/providers/provider-env-vars.ts +56 -0
- package/src/providers/provider-send-message.ts +22 -5
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/registry.ts +19 -8
- package/src/providers/retry.ts +174 -39
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
- package/src/providers/speech-to-text/provider-catalog.ts +17 -0
- package/src/providers/speech-to-text/resolve.ts +7 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
- package/src/providers/speech-to-text/xai-realtime.ts +796 -0
- package/src/providers/speech-to-text/xai.test.ts +155 -0
- package/src/providers/speech-to-text/xai.ts +97 -0
- package/src/providers/types.ts +93 -3
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +43 -2
- package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
- package/src/runtime/agent-wake.ts +63 -22
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/btw-sidechain.ts +13 -3
- package/src/runtime/channel-reply-delivery.ts +106 -2
- package/src/runtime/decision-token.ts +116 -0
- package/src/runtime/gateway-client.ts +2 -2
- package/src/runtime/http-router.ts +32 -0
- package/src/runtime/http-server.ts +52 -1
- package/src/runtime/http-types.ts +23 -1
- package/src/runtime/interactive-ui.ts +362 -0
- package/src/runtime/invite-instruction-generator.ts +2 -2
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
- package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
- package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
- package/src/runtime/migrations/gcs-signed-url.ts +162 -0
- package/src/runtime/migrations/vbundle-importer.ts +154 -9
- package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
- package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
- package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
- package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
- package/src/runtime/migrations/vbundle-validator.ts +15 -6
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
- package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
- package/src/runtime/routes/approval-routes.ts +12 -17
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
- package/src/runtime/routes/avatar-routes.ts +20 -4
- package/src/runtime/routes/btw-routes.ts +1 -4
- package/src/runtime/routes/conversation-management-routes.ts +20 -2
- package/src/runtime/routes/conversation-routes.ts +133 -27
- package/src/runtime/routes/debug-routes.ts +1 -1
- package/src/runtime/routes/diagnostics-routes.ts +6 -4
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/guardian-approval-interception.ts +33 -3
- package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
- package/src/runtime/routes/home-feed-routes.ts +120 -2
- package/src/runtime/routes/inbound-message-handler.ts +912 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
- package/src/runtime/routes/integrations/slack/channel.ts +25 -3
- package/src/runtime/routes/llm-context-normalization.ts +23 -1
- package/src/runtime/routes/migration-routes.ts +720 -124
- package/src/runtime/routes/settings-routes.ts +4 -2
- package/src/runtime/routes/trust-rules-routes.ts +30 -14
- package/src/runtime/routes/work-items-routes.test.ts +1 -1
- package/src/runtime/routes/work-items-routes.ts +3 -2
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
- package/src/runtime/services/analyze-conversation.ts +12 -16
- package/src/runtime/skill-route-registry.ts +28 -6
- package/src/schedule/scheduler.ts +8 -0
- package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
- package/src/security/__tests__/untrusted-content.test.ts +109 -0
- package/src/security/oauth2.ts +98 -35
- package/src/security/secure-keys.ts +7 -8
- package/src/security/token-manager.ts +27 -13
- package/src/security/untrusted-content.ts +102 -0
- package/src/skills/catalog-cache.ts +26 -7
- package/src/skills/catalog-install.ts +31 -3
- package/src/skills/skill-cache-store.ts +97 -0
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
- package/src/stt/daemon-batch-transcriber.ts +33 -0
- package/src/stt/stt-stream-session.ts +8 -1
- package/src/stt/types.ts +5 -1
- package/src/subagent/manager.ts +41 -13
- package/src/tasks/ephemeral-permissions.ts +9 -4
- package/src/telemetry/usage-telemetry-reporter.ts +27 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +45 -2
- package/src/tools/browser/browser-execution.ts +65 -38
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
- package/src/tools/credentials/tool-policy.ts +39 -5
- package/src/tools/credentials/vault.ts +9 -4
- package/src/tools/executor.ts +4 -0
- package/src/tools/filesystem/write.ts +52 -0
- package/src/tools/host-terminal/host-shell.ts +45 -5
- package/src/tools/memory/register.test.ts +185 -0
- package/src/tools/memory/register.ts +3 -1
- package/src/tools/network/web-fetch.ts +20 -10
- package/src/tools/network/web-search.ts +19 -4
- package/src/tools/permission-checker.ts +36 -15
- package/src/tools/policy-context.ts +25 -8
- package/src/tools/registry.ts +55 -3
- package/src/tools/side-effects.ts +0 -11
- package/src/tools/skills/execute.ts +2 -2
- package/src/tools/skills/sandbox-runner.ts +5 -2
- package/src/tools/terminal/backends/native.ts +51 -2
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/terminal/shell.ts +1 -0
- package/src/tools/tool-manifest.ts +6 -21
- package/src/tools/types.ts +12 -3
- package/src/tools/verification-control-plane-policy.ts +1 -1
- package/src/tts/__tests__/provider-adapters.test.ts +240 -13
- package/src/tts/provider-catalog.ts +18 -0
- package/src/tts/providers/index.ts +2 -0
- package/src/tts/providers/xai-provider.ts +224 -0
- package/src/tts/types.ts +46 -0
- package/src/types/tar-stream.d.ts +66 -0
- package/src/util/json.ts +17 -0
- package/src/util/platform.ts +2 -2
- package/src/util/pricing.ts +15 -5
- package/src/watcher/engine.ts +1 -1
- package/src/watcher/providers/google-calendar.ts +134 -8
- package/src/watcher/providers/outlook-calendar.ts +42 -2
- package/src/workspace/git-service.ts +23 -4
- package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
- package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
- package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
- package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
- package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
- package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/provider-commit-message-generator.ts +19 -38
- package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
- package/src/__tests__/gmail-archive-gate.test.ts +0 -246
- package/src/__tests__/gmail-preferences.test.ts +0 -117
- package/src/__tests__/outlook-attachments.test.ts +0 -301
- package/src/__tests__/outlook-automation-tools.test.ts +0 -425
- package/src/__tests__/outlook-categories.test.ts +0 -212
- package/src/__tests__/outlook-compose-tools.test.ts +0 -325
- package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
- package/src/__tests__/outlook-follow-up.test.ts +0 -196
- package/src/__tests__/outlook-trash.test.ts +0 -77
- package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
- package/src/__tests__/update-bulletin-format.test.ts +0 -181
- package/src/__tests__/update-bulletin-state.test.ts +0 -135
- package/src/__tests__/update-bulletin.test.ts +0 -478
- package/src/__tests__/update-template-contract.test.ts +0 -29
- package/src/cli/commands/doctor.ts +0 -341
- package/src/config/bundled-skills/browser/SKILL.md +0 -88
- package/src/config/bundled-skills/browser/TOOLS.json +0 -516
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
- package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
- package/src/config/bundled-skills/gmail/SKILL.md +0 -221
- package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
- package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
- package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
- package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/google-calendar/types.ts +0 -97
- package/src/config/bundled-skills/outlook/SKILL.md +0 -196
- package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
- package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
- package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
- package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
- package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
- package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
- package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
- package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
- package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
- package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
- package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
- package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
- package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
- package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
- package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
- package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
- package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
- package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
- package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
- package/src/config/bundled-skills/slack/SKILL.md +0 -108
- package/src/config/bundled-skills/tasks/SKILL.md +0 -37
- package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
- package/src/config/bundled-skills/tasks/icon.svg +0 -34
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
- package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
- package/src/config/bundled-skills/watcher/SKILL.md +0 -31
- package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
- package/src/prompts/templates/UPDATES.md +0 -50
- package/src/prompts/update-bulletin-format.ts +0 -85
- package/src/prompts/update-bulletin-state.ts +0 -58
- package/src/prompts/update-bulletin-template-path.ts +0 -13
- package/src/prompts/update-bulletin.ts +0 -139
- package/src/shared/provider-env-vars.ts +0 -19
- package/src/tools/watcher/create.ts +0 -86
- package/src/tools/watcher/delete.ts +0 -36
- package/src/tools/watcher/digest.ts +0 -54
- package/src/tools/watcher/list.ts +0 -83
- package/src/tools/watcher/update.ts +0 -71
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the streaming validator primitives:
|
|
3
|
+
* - `readAndValidateManifest` consumes the first tar entry and runs the
|
|
4
|
+
* full manifest validation pipeline.
|
|
5
|
+
* - `createHashVerifier` is a passthrough Transform that aborts with a
|
|
6
|
+
* typed error on digest/size mismatch.
|
|
7
|
+
*
|
|
8
|
+
* Happy-path fixtures are built with the existing `buildVBundle` helper.
|
|
9
|
+
* Negative-path fixtures (non-manifest first, oversize manifest, malformed
|
|
10
|
+
* JSON, schema fail, sha mismatch) are hand-constructed because
|
|
11
|
+
* `buildVBundle` always produces valid, manifest-first archives.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
16
|
+
import { pipeline } from "node:stream/promises";
|
|
17
|
+
import { gzipSync } from "node:zlib";
|
|
18
|
+
import { describe, expect, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
import { buildVBundle } from "../vbundle-builder.js";
|
|
21
|
+
import {
|
|
22
|
+
createHashVerifier,
|
|
23
|
+
readAndValidateManifest,
|
|
24
|
+
StreamingValidationError,
|
|
25
|
+
} from "../vbundle-streaming-validator.js";
|
|
26
|
+
import {
|
|
27
|
+
parseVBundleStream,
|
|
28
|
+
type StreamedTarEntry,
|
|
29
|
+
} from "../vbundle-tar-stream.js";
|
|
30
|
+
import { computeManifestSha256 } from "../vbundle-validator.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const BLOCK_SIZE = 512;
|
|
37
|
+
|
|
38
|
+
function readableFromBuffer(buf: Uint8Array): Readable {
|
|
39
|
+
return Readable.from([Buffer.from(buf)]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Minimal hand-rolled tar entry builder (ustar regular file). */
|
|
43
|
+
function buildTarEntry(name: string, data: Uint8Array): Uint8Array {
|
|
44
|
+
const encoder = new TextEncoder();
|
|
45
|
+
const nameBytes = encoder.encode(name);
|
|
46
|
+
if (nameBytes.length > 100) {
|
|
47
|
+
throw new Error(`test helper: name too long (${nameBytes.length} bytes)`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const header = new Uint8Array(BLOCK_SIZE);
|
|
51
|
+
header.set(nameBytes, 0);
|
|
52
|
+
|
|
53
|
+
const writeOctal = (offset: number, length: number, value: number) => {
|
|
54
|
+
const str = value.toString(8).padStart(length - 1, "0");
|
|
55
|
+
for (let i = 0; i < str.length; i++) {
|
|
56
|
+
header[offset + i] = str.charCodeAt(i);
|
|
57
|
+
}
|
|
58
|
+
header[offset + length - 1] = 0;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
writeOctal(100, 8, 0o644); // mode
|
|
62
|
+
writeOctal(108, 8, 0); // uid
|
|
63
|
+
writeOctal(116, 8, 0); // gid
|
|
64
|
+
writeOctal(124, 12, data.length); // size
|
|
65
|
+
writeOctal(136, 12, Math.floor(Date.now() / 1000)); // mtime
|
|
66
|
+
header[156] = "0".charCodeAt(0); // typeflag = regular file
|
|
67
|
+
|
|
68
|
+
const magic = encoder.encode("ustar\0");
|
|
69
|
+
header.set(magic, 257);
|
|
70
|
+
header[263] = "0".charCodeAt(0);
|
|
71
|
+
header[264] = "0".charCodeAt(0);
|
|
72
|
+
|
|
73
|
+
// Header checksum: sum of all bytes with the checksum field treated
|
|
74
|
+
// as 8 ASCII spaces.
|
|
75
|
+
let sum = 0;
|
|
76
|
+
for (let i = 0; i < BLOCK_SIZE; i++) {
|
|
77
|
+
sum += i >= 148 && i < 156 ? 0x20 : header[i];
|
|
78
|
+
}
|
|
79
|
+
writeOctal(148, 7, sum);
|
|
80
|
+
header[155] = 0x20;
|
|
81
|
+
|
|
82
|
+
// Pad data out to the next 512-byte boundary.
|
|
83
|
+
const remainder = data.length % BLOCK_SIZE;
|
|
84
|
+
const padded =
|
|
85
|
+
remainder === 0
|
|
86
|
+
? data
|
|
87
|
+
: (() => {
|
|
88
|
+
const out = new Uint8Array(data.length + (BLOCK_SIZE - remainder));
|
|
89
|
+
out.set(data, 0);
|
|
90
|
+
return out;
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
const entry = new Uint8Array(header.length + padded.length);
|
|
94
|
+
entry.set(header, 0);
|
|
95
|
+
entry.set(padded, header.length);
|
|
96
|
+
return entry;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Hand-build a gzipped tar archive from the given entries (no manifest injected). */
|
|
100
|
+
function buildRawVBundle(
|
|
101
|
+
entries: Array<{ name: string; data: Uint8Array }>,
|
|
102
|
+
): Uint8Array {
|
|
103
|
+
const parts: Uint8Array[] = entries.map((e) => buildTarEntry(e.name, e.data));
|
|
104
|
+
// End-of-archive marker: two zero blocks.
|
|
105
|
+
parts.push(new Uint8Array(BLOCK_SIZE * 2));
|
|
106
|
+
|
|
107
|
+
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
108
|
+
const tar = new Uint8Array(total);
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (const p of parts) {
|
|
111
|
+
tar.set(p, offset);
|
|
112
|
+
offset += p.length;
|
|
113
|
+
}
|
|
114
|
+
return gzipSync(tar);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Fetch the first entry of a streaming archive; drain+close the iterator after. */
|
|
118
|
+
async function firstEntryOf(
|
|
119
|
+
archive: Uint8Array,
|
|
120
|
+
): Promise<{ entry: StreamedTarEntry; drainRest: () => Promise<void> }> {
|
|
121
|
+
const iter = parseVBundleStream(readableFromBuffer(archive));
|
|
122
|
+
const first = await iter.next();
|
|
123
|
+
if (first.done || !first.value) {
|
|
124
|
+
throw new Error("archive contained no entries");
|
|
125
|
+
}
|
|
126
|
+
const drainRest = async () => {
|
|
127
|
+
for await (const rest of iter) {
|
|
128
|
+
rest.body.resume();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
return { entry: first.value, drainRest };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// readAndValidateManifest — happy path
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
describe("readAndValidateManifest — happy path", () => {
|
|
139
|
+
test("parses manifest and populates expected map from manifest.files", async () => {
|
|
140
|
+
const fileA = new TextEncoder().encode("alpha payload\n");
|
|
141
|
+
const fileB = new TextEncoder().encode("beta payload\n");
|
|
142
|
+
const { archive, manifest } = buildVBundle({
|
|
143
|
+
files: [
|
|
144
|
+
{ path: "workspace/a.txt", data: fileA },
|
|
145
|
+
{ path: "workspace/b.txt", data: fileB },
|
|
146
|
+
],
|
|
147
|
+
source: "test",
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
151
|
+
const result = await readAndValidateManifest(entry);
|
|
152
|
+
await drainRest();
|
|
153
|
+
|
|
154
|
+
expect(result.manifest.schema_version).toBe(manifest.schema_version);
|
|
155
|
+
expect(result.manifest.files).toHaveLength(2);
|
|
156
|
+
expect(result.manifest.manifest_sha256).toBe(manifest.manifest_sha256);
|
|
157
|
+
|
|
158
|
+
expect(result.expected.size).toBe(2);
|
|
159
|
+
const expectA = result.expected.get("workspace/a.txt");
|
|
160
|
+
expect(expectA?.size).toBe(fileA.length);
|
|
161
|
+
expect(expectA?.sha256).toBe(
|
|
162
|
+
manifest.files.find((f) => f.path === "workspace/a.txt")?.sha256,
|
|
163
|
+
);
|
|
164
|
+
const expectB = result.expected.get("workspace/b.txt");
|
|
165
|
+
expect(expectB?.size).toBe(fileB.length);
|
|
166
|
+
expect(expectB?.sha256).toBe(
|
|
167
|
+
manifest.files.find((f) => f.path === "workspace/b.txt")?.sha256,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// readAndValidateManifest — negative paths
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
describe("readAndValidateManifest — negative paths", () => {
|
|
177
|
+
test("throws manifest_not_first when first entry is not manifest.json", async () => {
|
|
178
|
+
const archive = buildRawVBundle([
|
|
179
|
+
{ name: "workspace/a.txt", data: new TextEncoder().encode("hello") },
|
|
180
|
+
]);
|
|
181
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
182
|
+
|
|
183
|
+
let err: StreamingValidationError | null = null;
|
|
184
|
+
try {
|
|
185
|
+
await readAndValidateManifest(entry);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
err = e as StreamingValidationError;
|
|
188
|
+
}
|
|
189
|
+
await drainRest();
|
|
190
|
+
|
|
191
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
192
|
+
expect(err?.code).toBe("manifest_not_first");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("throws manifest_too_large and fails fast before draining the whole body", async () => {
|
|
196
|
+
// Fake a tar entry whose body would emit 5 MiB if fully drained. The
|
|
197
|
+
// validator must destroy() the stream and throw the moment the running
|
|
198
|
+
// byte count crosses the 1 MiB cap — it must NOT keep pulling chunks.
|
|
199
|
+
//
|
|
200
|
+
// We count bytes emitted via a _read implementation, and after the
|
|
201
|
+
// throw assert both that destroy() fired and that far fewer than 5 MiB
|
|
202
|
+
// were ever pulled out of the stream.
|
|
203
|
+
const CHUNK = 512 * 1024; // 512 KiB
|
|
204
|
+
const TOTAL_CHUNKS = 10; // 5 MiB worth — way past the 1 MiB cap
|
|
205
|
+
let chunksEmitted = 0;
|
|
206
|
+
let bytesEmitted = 0;
|
|
207
|
+
const body = new Readable({
|
|
208
|
+
read() {
|
|
209
|
+
if (chunksEmitted >= TOTAL_CHUNKS) {
|
|
210
|
+
this.push(null);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const buf = Buffer.alloc(CHUNK, 0x20);
|
|
214
|
+
chunksEmitted += 1;
|
|
215
|
+
bytesEmitted += buf.length;
|
|
216
|
+
this.push(buf);
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const entry: StreamedTarEntry = {
|
|
221
|
+
header: {
|
|
222
|
+
name: "manifest.json",
|
|
223
|
+
size: CHUNK * TOTAL_CHUNKS,
|
|
224
|
+
type: "file",
|
|
225
|
+
},
|
|
226
|
+
body,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
let err: StreamingValidationError | null = null;
|
|
230
|
+
try {
|
|
231
|
+
await readAndValidateManifest(entry);
|
|
232
|
+
} catch (e) {
|
|
233
|
+
err = e as StreamingValidationError;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
237
|
+
expect(err?.code).toBe("manifest_too_large");
|
|
238
|
+
// Fail-fast assertions: destroy() was called, and we didn't drain past
|
|
239
|
+
// ~1 MiB + one chunk. If the validator had drained to EOF we'd see the
|
|
240
|
+
// full 5 MiB / 10 chunks here.
|
|
241
|
+
expect(body.destroyed).toBe(true);
|
|
242
|
+
expect(chunksEmitted).toBeLessThanOrEqual(3);
|
|
243
|
+
expect(bytesEmitted).toBeLessThan(2 * 1024 * 1024);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("throws manifest_malformed when manifest body is not valid JSON", async () => {
|
|
247
|
+
const archive = buildRawVBundle([
|
|
248
|
+
{ name: "manifest.json", data: new TextEncoder().encode("{not-json") },
|
|
249
|
+
]);
|
|
250
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
251
|
+
|
|
252
|
+
let err: StreamingValidationError | null = null;
|
|
253
|
+
try {
|
|
254
|
+
await readAndValidateManifest(entry);
|
|
255
|
+
} catch (e) {
|
|
256
|
+
err = e as StreamingValidationError;
|
|
257
|
+
}
|
|
258
|
+
await drainRest();
|
|
259
|
+
|
|
260
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
261
|
+
expect(err?.code).toBe("manifest_malformed");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("throws manifest_schema when a required field is missing", async () => {
|
|
265
|
+
// Valid JSON but missing `files`, `manifest_sha256`, etc.
|
|
266
|
+
const bogus = new TextEncoder().encode(
|
|
267
|
+
JSON.stringify({ schema_version: "1.0", created_at: "now" }),
|
|
268
|
+
);
|
|
269
|
+
const archive = buildRawVBundle([{ name: "manifest.json", data: bogus }]);
|
|
270
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
271
|
+
|
|
272
|
+
let err: StreamingValidationError | null = null;
|
|
273
|
+
try {
|
|
274
|
+
await readAndValidateManifest(entry);
|
|
275
|
+
} catch (e) {
|
|
276
|
+
err = e as StreamingValidationError;
|
|
277
|
+
}
|
|
278
|
+
await drainRest();
|
|
279
|
+
|
|
280
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
281
|
+
expect(err?.code).toBe("manifest_schema");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("throws manifest_sha256 when the declared digest doesn't match canonical JSON", async () => {
|
|
285
|
+
const badManifest = {
|
|
286
|
+
schema_version: "1.0",
|
|
287
|
+
created_at: new Date().toISOString(),
|
|
288
|
+
files: [],
|
|
289
|
+
// Deliberately wrong digest. The canonical hash of a 4-field manifest
|
|
290
|
+
// won't match this, regardless of ordering.
|
|
291
|
+
manifest_sha256:
|
|
292
|
+
"0000000000000000000000000000000000000000000000000000000000000000",
|
|
293
|
+
};
|
|
294
|
+
const archive = buildRawVBundle([
|
|
295
|
+
{
|
|
296
|
+
name: "manifest.json",
|
|
297
|
+
data: new TextEncoder().encode(JSON.stringify(badManifest)),
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
301
|
+
|
|
302
|
+
let err: StreamingValidationError | null = null;
|
|
303
|
+
try {
|
|
304
|
+
await readAndValidateManifest(entry);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
err = e as StreamingValidationError;
|
|
307
|
+
}
|
|
308
|
+
await drainRest();
|
|
309
|
+
|
|
310
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
311
|
+
expect(err?.code).toBe("manifest_sha256");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("throws manifest_duplicate_path when the same archive path appears twice", async () => {
|
|
315
|
+
const baseManifest = {
|
|
316
|
+
schema_version: "1.0",
|
|
317
|
+
created_at: new Date().toISOString(),
|
|
318
|
+
files: [
|
|
319
|
+
{
|
|
320
|
+
path: "workspace/a.txt",
|
|
321
|
+
sha256:
|
|
322
|
+
"1111111111111111111111111111111111111111111111111111111111111111",
|
|
323
|
+
size: 10,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
// Deliberately duplicate path — malicious bundle could exploit this
|
|
327
|
+
// to bypass per-entry integrity checks if we silently collapsed.
|
|
328
|
+
path: "workspace/a.txt",
|
|
329
|
+
sha256:
|
|
330
|
+
"2222222222222222222222222222222222222222222222222222222222222222",
|
|
331
|
+
size: 20,
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
manifest_sha256: "",
|
|
335
|
+
};
|
|
336
|
+
baseManifest.manifest_sha256 = computeManifestSha256(baseManifest);
|
|
337
|
+
|
|
338
|
+
const archive = buildRawVBundle([
|
|
339
|
+
{
|
|
340
|
+
name: "manifest.json",
|
|
341
|
+
data: new TextEncoder().encode(JSON.stringify(baseManifest)),
|
|
342
|
+
},
|
|
343
|
+
]);
|
|
344
|
+
const { entry, drainRest } = await firstEntryOf(archive);
|
|
345
|
+
|
|
346
|
+
let err: StreamingValidationError | null = null;
|
|
347
|
+
try {
|
|
348
|
+
await readAndValidateManifest(entry);
|
|
349
|
+
} catch (e) {
|
|
350
|
+
err = e as StreamingValidationError;
|
|
351
|
+
}
|
|
352
|
+
await drainRest();
|
|
353
|
+
|
|
354
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
355
|
+
expect(err?.code).toBe("manifest_duplicate_path");
|
|
356
|
+
expect(err?.message).toContain("workspace/a.txt");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
// createHashVerifier
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
describe("createHashVerifier — identity + integrity", () => {
|
|
365
|
+
const payload = Buffer.from(
|
|
366
|
+
"the quick brown fox jumps over the lazy dog".repeat(100),
|
|
367
|
+
"utf8",
|
|
368
|
+
);
|
|
369
|
+
// Precomputed digest for the payload above.
|
|
370
|
+
const payloadSha = createHash("sha256").update(payload).digest("hex");
|
|
371
|
+
|
|
372
|
+
test("is an identity Transform for correct inputs", async () => {
|
|
373
|
+
const verifier = createHashVerifier({
|
|
374
|
+
sha256: payloadSha,
|
|
375
|
+
size: payload.length,
|
|
376
|
+
archivePath: "workspace/ok.txt",
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Feed the payload in two chunks to exercise multi-call _transform.
|
|
380
|
+
const half = payload.length >>> 1;
|
|
381
|
+
const source = Readable.from([
|
|
382
|
+
payload.subarray(0, half),
|
|
383
|
+
payload.subarray(half),
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
const collected: Buffer[] = [];
|
|
387
|
+
verifier.on("data", (chunk: Buffer) => collected.push(chunk));
|
|
388
|
+
|
|
389
|
+
await pipeline(source, verifier);
|
|
390
|
+
|
|
391
|
+
const out = Buffer.concat(collected);
|
|
392
|
+
expect(out.length).toBe(payload.length);
|
|
393
|
+
expect(out.equals(payload)).toBe(true);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("errors with code entry_hash on digest mismatch", async () => {
|
|
397
|
+
const verifier = createHashVerifier({
|
|
398
|
+
sha256:
|
|
399
|
+
// Wrong digest.
|
|
400
|
+
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
401
|
+
size: payload.length,
|
|
402
|
+
archivePath: "workspace/bad-hash.txt",
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
let err: StreamingValidationError | null = null;
|
|
406
|
+
try {
|
|
407
|
+
await pipeline(
|
|
408
|
+
Readable.from([payload]),
|
|
409
|
+
verifier,
|
|
410
|
+
async function* (source: AsyncIterable<Buffer>) {
|
|
411
|
+
// Drain the transform so _flush runs.
|
|
412
|
+
for await (const _chunk of source) {
|
|
413
|
+
// discard
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
} catch (e) {
|
|
418
|
+
err = e as StreamingValidationError;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
422
|
+
expect(err?.code).toBe("entry_hash");
|
|
423
|
+
expect(err?.archivePath).toBe("workspace/bad-hash.txt");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("errors with code entry_size on byte-count mismatch", async () => {
|
|
427
|
+
// Right digest, wrong declared size.
|
|
428
|
+
const verifier = createHashVerifier({
|
|
429
|
+
sha256: payloadSha,
|
|
430
|
+
size: payload.length + 1,
|
|
431
|
+
archivePath: "workspace/bad-size.txt",
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let err: StreamingValidationError | null = null;
|
|
435
|
+
try {
|
|
436
|
+
await pipeline(
|
|
437
|
+
Readable.from([payload]),
|
|
438
|
+
verifier,
|
|
439
|
+
async function* (source: AsyncIterable<Buffer>) {
|
|
440
|
+
for await (const _chunk of source) {
|
|
441
|
+
// discard
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
} catch (e) {
|
|
446
|
+
err = e as StreamingValidationError;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
expect(err).toBeInstanceOf(StreamingValidationError);
|
|
450
|
+
expect(err?.code).toBe("entry_size");
|
|
451
|
+
expect(err?.archivePath).toBe("workspace/bad-size.txt");
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `parseVBundleStream` — the streaming tar reader for `.vbundle`
|
|
3
|
+
* archives.
|
|
4
|
+
*
|
|
5
|
+
* Covered:
|
|
6
|
+
* - Happy path: 3-file archive yields entries in order with correct names/sizes.
|
|
7
|
+
* - Manifest-first invariant: first entry is `manifest.json`.
|
|
8
|
+
* - Truncated gzip mid-stream: generator throws.
|
|
9
|
+
* - Valid gzip but malformed tar payload: generator throws.
|
|
10
|
+
* - Early termination (break in for-await loop): upstream source is destroyed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Readable } from "node:stream";
|
|
14
|
+
import { gzipSync } from "node:zlib";
|
|
15
|
+
import { describe, expect, test } from "bun:test";
|
|
16
|
+
|
|
17
|
+
import { buildVBundle } from "../vbundle-builder.js";
|
|
18
|
+
import {
|
|
19
|
+
parseVBundleStream,
|
|
20
|
+
type StreamedTarEntry,
|
|
21
|
+
} from "../vbundle-tar-stream.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
async function collectBody(entry: StreamedTarEntry): Promise<Uint8Array> {
|
|
28
|
+
const chunks: Buffer[] = [];
|
|
29
|
+
for await (const chunk of entry.body) {
|
|
30
|
+
chunks.push(chunk as Buffer);
|
|
31
|
+
}
|
|
32
|
+
return new Uint8Array(Buffer.concat(chunks));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readableFromBuffer(buf: Uint8Array): Readable {
|
|
36
|
+
// `Readable.from(Uint8Array)` iterates byte-by-byte (each element becomes
|
|
37
|
+
// a chunk), which is not what we want here. Wrap in an array so the whole
|
|
38
|
+
// buffer arrives as a single chunk — closer to how the HTTP client will
|
|
39
|
+
// feed bytes in production.
|
|
40
|
+
return Readable.from([Buffer.from(buf)]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Build a minimal vbundle archive with the given extra files (plus manifest). */
|
|
44
|
+
function buildMinimalVBundle(
|
|
45
|
+
extraFiles: { path: string; data: Uint8Array }[],
|
|
46
|
+
): Uint8Array {
|
|
47
|
+
const { archive } = buildVBundle({
|
|
48
|
+
files: extraFiles,
|
|
49
|
+
source: "test",
|
|
50
|
+
description: "test bundle",
|
|
51
|
+
});
|
|
52
|
+
return archive;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Happy path
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
describe("parseVBundleStream — happy path", () => {
|
|
60
|
+
test("yields entries in order with correct names and sizes", async () => {
|
|
61
|
+
const fileA = new TextEncoder().encode("alpha\n");
|
|
62
|
+
const fileB = new TextEncoder().encode("beta beta\n");
|
|
63
|
+
const fileC = new TextEncoder().encode("gamma gamma gamma\n");
|
|
64
|
+
|
|
65
|
+
const archive = buildMinimalVBundle([
|
|
66
|
+
{ path: "workspace/a.txt", data: fileA },
|
|
67
|
+
{ path: "workspace/b.txt", data: fileB },
|
|
68
|
+
{ path: "workspace/c.txt", data: fileC },
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const source = readableFromBuffer(archive);
|
|
72
|
+
const seen: { name: string; size: number; body: Uint8Array }[] = [];
|
|
73
|
+
|
|
74
|
+
for await (const entry of parseVBundleStream(source)) {
|
|
75
|
+
const body = await collectBody(entry);
|
|
76
|
+
seen.push({
|
|
77
|
+
name: entry.header.name,
|
|
78
|
+
size: entry.header.size,
|
|
79
|
+
body,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// manifest.json is emitted first, then the 3 files in insertion order.
|
|
84
|
+
expect(seen.length).toBe(4);
|
|
85
|
+
expect(seen[0]?.name).toBe("manifest.json");
|
|
86
|
+
expect(seen[1]?.name).toBe("workspace/a.txt");
|
|
87
|
+
expect(seen[2]?.name).toBe("workspace/b.txt");
|
|
88
|
+
expect(seen[3]?.name).toBe("workspace/c.txt");
|
|
89
|
+
|
|
90
|
+
// Sizes in the header match the body lengths.
|
|
91
|
+
expect(seen[1]?.size).toBe(fileA.length);
|
|
92
|
+
expect(seen[1]?.body.length).toBe(fileA.length);
|
|
93
|
+
expect(seen[2]?.size).toBe(fileB.length);
|
|
94
|
+
expect(seen[2]?.body.length).toBe(fileB.length);
|
|
95
|
+
expect(seen[3]?.size).toBe(fileC.length);
|
|
96
|
+
expect(seen[3]?.body.length).toBe(fileC.length);
|
|
97
|
+
|
|
98
|
+
// Body contents round-trip correctly.
|
|
99
|
+
expect(new TextDecoder().decode(seen[1]?.body)).toBe("alpha\n");
|
|
100
|
+
expect(new TextDecoder().decode(seen[2]?.body)).toBe("beta beta\n");
|
|
101
|
+
expect(new TextDecoder().decode(seen[3]?.body)).toBe("gamma gamma gamma\n");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Manifest-first invariant
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
describe("parseVBundleStream — manifest-first", () => {
|
|
110
|
+
test("first entry is manifest.json", async () => {
|
|
111
|
+
const archive = buildMinimalVBundle([
|
|
112
|
+
{ path: "z-last.txt", data: new TextEncoder().encode("zzz") },
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const iter = parseVBundleStream(readableFromBuffer(archive));
|
|
116
|
+
const first = await iter.next();
|
|
117
|
+
expect(first.done).toBe(false);
|
|
118
|
+
expect(first.value?.header.name).toBe("manifest.json");
|
|
119
|
+
expect(first.value?.header.type).toBe("file");
|
|
120
|
+
|
|
121
|
+
// Drain the manifest body so the iterator can advance, then finish.
|
|
122
|
+
if (first.value) await collectBody(first.value);
|
|
123
|
+
// Exhaust the iterator to release resources. Drain each body so the
|
|
124
|
+
// extractor can advance. (We don't assert on the remaining entries here.)
|
|
125
|
+
for await (const entry of iter) {
|
|
126
|
+
await collectBody(entry);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Error handling
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
describe("parseVBundleStream — errors", () => {
|
|
136
|
+
test("throws on truncated gzip mid-stream", async () => {
|
|
137
|
+
const archive = buildMinimalVBundle([
|
|
138
|
+
{ path: "workspace/a.txt", data: new TextEncoder().encode("hello") },
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
// Lop off the trailing bytes of the gzip member so the decoder fails
|
|
142
|
+
// partway through.
|
|
143
|
+
const truncated = archive.subarray(0, Math.max(32, archive.length - 20));
|
|
144
|
+
const source = readableFromBuffer(new Uint8Array(truncated));
|
|
145
|
+
|
|
146
|
+
let threw = false;
|
|
147
|
+
try {
|
|
148
|
+
for await (const entry of parseVBundleStream(source)) {
|
|
149
|
+
// Drain whatever the decoder can produce before the error hits.
|
|
150
|
+
try {
|
|
151
|
+
await collectBody(entry);
|
|
152
|
+
} catch {
|
|
153
|
+
// Body streams may error mid-drain; that also counts as a failure
|
|
154
|
+
// in the outer iterator on the next advance.
|
|
155
|
+
threw = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
threw = true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
expect(threw).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("throws on valid gzip wrapping malformed tar bytes", async () => {
|
|
167
|
+
// Gzipped junk — valid gzip member, but the inflated bytes aren't a tar.
|
|
168
|
+
const junk = new TextEncoder().encode("this is not a tar archive payload");
|
|
169
|
+
// Pad out so the extractor has enough bytes to attempt a header parse.
|
|
170
|
+
const padded = new Uint8Array(1024);
|
|
171
|
+
padded.set(junk, 0);
|
|
172
|
+
const gz = gzipSync(padded);
|
|
173
|
+
const source = readableFromBuffer(new Uint8Array(gz));
|
|
174
|
+
|
|
175
|
+
let threw = false;
|
|
176
|
+
try {
|
|
177
|
+
for await (const entry of parseVBundleStream(source)) {
|
|
178
|
+
try {
|
|
179
|
+
await collectBody(entry);
|
|
180
|
+
} catch {
|
|
181
|
+
threw = true;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
threw = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expect(threw).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Early-termination cleanup
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
describe("parseVBundleStream — cleanup", () => {
|
|
198
|
+
test("destroys upstream source when caller breaks out of the loop", async () => {
|
|
199
|
+
const archive = buildMinimalVBundle([
|
|
200
|
+
{ path: "workspace/a.txt", data: new TextEncoder().encode("hello") },
|
|
201
|
+
{ path: "workspace/b.txt", data: new TextEncoder().encode("world") },
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
const source = readableFromBuffer(archive);
|
|
205
|
+
let destroyCalls = 0;
|
|
206
|
+
const originalDestroy = source.destroy.bind(source);
|
|
207
|
+
source.destroy = ((err?: Error) => {
|
|
208
|
+
destroyCalls += 1;
|
|
209
|
+
return originalDestroy(err);
|
|
210
|
+
}) as typeof source.destroy;
|
|
211
|
+
|
|
212
|
+
for await (const entry of parseVBundleStream(source)) {
|
|
213
|
+
// Consume just the first entry (manifest.json), then bail.
|
|
214
|
+
await collectBody(entry);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expect(destroyCalls).toBeGreaterThan(0);
|
|
219
|
+
// Source should now be destroyed (no dangling listeners keeping it alive).
|
|
220
|
+
expect(source.destroyed).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|