@vellumai/assistant 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +273 -10
- package/Dockerfile +2 -3
- package/bun.lock +5 -13
- package/docs/backup-troubleshooting.md +52 -0
- package/docs/browser-use-architecture-phase2.md +174 -0
- package/docs/stt-provider-onboarding.md +120 -0
- package/knip.json +12 -2
- package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
- package/node_modules/@vellumai/ces-contracts/package.json +3 -3
- package/openapi.yaml +982 -72
- package/package.json +4 -6
- package/scripts/generate-openapi.ts +0 -1
- package/scripts/test.sh +73 -18
- package/src/__tests__/agent-image-optimize.test.ts +28 -0
- package/src/__tests__/agent-loop.test.ts +123 -0
- package/src/__tests__/anthropic-provider.test.ts +263 -10
- package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
- package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
- package/src/__tests__/browser-fill-credential.test.ts +11 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
- package/src/__tests__/browser-skill-endstate.test.ts +31 -7
- package/src/__tests__/btw-routes.test.ts +7 -0
- package/src/__tests__/call-controller.test.ts +581 -20
- package/src/__tests__/catalog-files.test.ts +138 -0
- package/src/__tests__/channel-invite-transport.test.ts +2 -2
- package/src/__tests__/channel-readiness-routes.test.ts +16 -20
- package/src/__tests__/channel-readiness-service.test.ts +12 -7
- package/src/__tests__/checker.test.ts +157 -10
- package/src/__tests__/clawhub-files.test.ts +347 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
- package/src/__tests__/config-analysis.test.ts +100 -0
- package/src/__tests__/config-schema.test.ts +1013 -66
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
- package/src/__tests__/config-watcher.test.ts +43 -8
- package/src/__tests__/contact-store-user-file.test.ts +512 -0
- package/src/__tests__/contacts-write.test.ts +197 -0
- package/src/__tests__/context-window-manager.test.ts +88 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +98 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +135 -0
- package/src/__tests__/conversation-error.test.ts +70 -0
- package/src/__tests__/conversation-history-web-search.test.ts +11 -4
- package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
- package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
- package/src/__tests__/conversation-list-source.test.ts +145 -0
- package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +901 -60
- package/src/__tests__/conversation-routes-disk-view.test.ts +270 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +55 -0
- package/src/__tests__/conversation-skill-tools.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +33 -0
- package/src/__tests__/conversation-slash-queue.test.ts +89 -18
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-health-service.test.ts +352 -0
- package/src/__tests__/credential-security-invariants.test.ts +5 -3
- package/src/__tests__/credential-vault-unit.test.ts +379 -3
- package/src/__tests__/credentials-cli.test.ts +40 -16
- package/src/__tests__/cross-provider-web-search.test.ts +146 -35
- package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
- package/src/__tests__/device-id.test.ts +112 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
- package/src/__tests__/email-html-renderer.test.ts +71 -0
- package/src/__tests__/email-invite-adapter.test.ts +36 -32
- package/src/__tests__/emit-event-signal.test.ts +71 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +75 -8
- package/src/__tests__/fixtures/mock-chrome-extension.ts +11 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
- package/src/__tests__/gateway-only-guard.test.ts +0 -1
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
- package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
- package/src/__tests__/gmail-archive-gate.test.ts +246 -0
- package/src/__tests__/gmail-preferences.test.ts +117 -0
- package/src/__tests__/headless-browser-interactions.test.ts +43 -0
- package/src/__tests__/headless-browser-mode.test.ts +614 -0
- package/src/__tests__/headless-browser-navigate.test.ts +142 -5
- package/src/__tests__/headless-browser-read-tools.test.ts +11 -0
- package/src/__tests__/headless-browser-snapshot.test.ts +10 -0
- package/src/__tests__/heartbeat-service.test.ts +70 -17
- package/src/__tests__/home-state-routes.test.ts +162 -0
- package/src/__tests__/host-bash-proxy.test.ts +0 -5
- package/src/__tests__/host-browser-e2e-cloud.test.ts +138 -4
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +4 -4
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +103 -0
- package/src/__tests__/host-cu-proxy.test.ts +0 -5
- package/src/__tests__/identity-intro-cache.test.ts +40 -10
- package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
- package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
- package/src/__tests__/llm-context-normalization.test.ts +488 -0
- package/src/__tests__/llm-context-route-provider.test.ts +86 -5
- package/src/__tests__/llm-usage-store.test.ts +363 -0
- package/src/__tests__/media-stream-output.test.ts +555 -0
- package/src/__tests__/media-stream-parser.test.ts +374 -0
- package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
- package/src/__tests__/media-stream-stt-session.test.ts +588 -0
- package/src/__tests__/media-turn-detector.test.ts +440 -0
- package/src/__tests__/message-queue.test.ts +125 -0
- package/src/__tests__/migration-export-http.test.ts +6 -6
- package/src/__tests__/migration-import-commit-http.test.ts +8 -6
- package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
- package/src/__tests__/migration-validate-http.test.ts +3 -3
- package/src/__tests__/mock-gateway-ipc.ts +151 -0
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +2 -0
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/oauth-store.test.ts +85 -0
- package/src/__tests__/oauth2-gateway-transport.test.ts +249 -6
- package/src/__tests__/onboarding-template-contract.test.ts +6 -13
- package/src/__tests__/openai-provider.test.ts +176 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
- package/src/__tests__/openai-responses-provider.test.ts +1105 -0
- package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
- package/src/__tests__/outlook-unsubscribe.test.ts +31 -2
- package/src/__tests__/persona-resolver.test.ts +251 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
- package/src/__tests__/platform.test.ts +92 -1
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
- package/src/__tests__/pricing.test.ts +174 -0
- package/src/__tests__/qdrant-manager.test.ts +29 -8
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
- package/src/__tests__/relationship-state-contract.test.ts +175 -0
- package/src/__tests__/relay-server.test.ts +423 -5
- package/src/__tests__/search-skills-unified.test.ts +118 -0
- package/src/__tests__/secret-scanner-executor.test.ts +4 -0
- package/src/__tests__/secure-keys.test.ts +107 -0
- package/src/__tests__/send-endpoint-busy.test.ts +5 -1
- package/src/__tests__/sequence-store.test.ts +1 -1
- package/src/__tests__/server-history-render.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +201 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
- package/src/__tests__/skills-file-content-endpoint.test.ts +276 -145
- package/src/__tests__/skills-files-catalog-fallback.test.ts +381 -93
- package/src/__tests__/skills.test.ts +5 -2
- package/src/__tests__/skillssh-files.test.ts +446 -0
- package/src/__tests__/slack-block-formatting.test.ts +110 -0
- package/src/__tests__/slack-channel-config.test.ts +564 -1
- package/src/__tests__/stt-catalog-parity.test.ts +282 -0
- package/src/__tests__/stt-stream-session.test.ts +535 -0
- package/src/__tests__/system-prompt.test.ts +112 -26
- package/src/__tests__/telephony-stt-routing.test.ts +329 -0
- package/src/__tests__/terminal-tools.test.ts +18 -7
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
- package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +33 -24
- package/src/__tests__/tool-result-truncation.test.ts +36 -0
- package/src/__tests__/trust-store.test.ts +7 -1
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
- package/src/__tests__/tts-catalog-parity.test.ts +345 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
- package/src/__tests__/twilio-routes.test.ts +376 -0
- package/src/__tests__/unicode.test.ts +293 -0
- package/src/__tests__/update-bulletin-format.test.ts +59 -0
- package/src/__tests__/update-bulletin.test.ts +206 -5
- package/src/__tests__/usage-routes.test.ts +25 -4
- package/src/__tests__/user-reference.test.ts +46 -61
- package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
- package/src/__tests__/voice-config-update.test.ts +403 -0
- package/src/__tests__/voice-quality.test.ts +434 -19
- package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
- package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
- package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
- package/src/__tests__/workspace-migration-meets.test.ts +244 -0
- package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
- package/src/__tests__/workspace-policy.test.ts +2 -0
- package/src/agent/image-optimize.ts +24 -12
- package/src/agent/loop.ts +43 -3
- package/src/backup/__tests__/backup-key.test.ts +152 -0
- package/src/backup/__tests__/backup-worker.test.ts +767 -0
- package/src/backup/__tests__/list-snapshots.test.ts +87 -0
- package/src/backup/__tests__/local-writer.test.ts +218 -0
- package/src/backup/__tests__/offsite-writer.test.ts +641 -0
- package/src/backup/__tests__/paths.test.ts +300 -0
- package/src/backup/__tests__/restore.test.ts +498 -0
- package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
- package/src/backup/__tests__/stream-crypt.test.ts +228 -0
- package/src/backup/backup-key.ts +137 -0
- package/src/backup/backup-worker.ts +459 -0
- package/src/backup/list-snapshots.ts +147 -0
- package/src/backup/local-writer.ts +133 -0
- package/src/backup/offsite-writer.ts +222 -0
- package/src/backup/paths.ts +226 -0
- package/src/backup/restore.ts +322 -0
- package/src/backup/snapshot-lock.ts +431 -0
- package/src/backup/stream-crypt.ts +263 -0
- package/src/bundler/package-resolver.ts +4 -0
- package/src/calls/audio-store.ts +11 -5
- package/src/calls/call-controller.ts +226 -71
- package/src/calls/call-domain.ts +9 -0
- package/src/calls/call-speech-output.ts +190 -0
- package/src/calls/call-transport.ts +77 -0
- package/src/calls/media-stream-audio-transcode.ts +173 -0
- package/src/calls/media-stream-output.ts +660 -0
- package/src/calls/media-stream-parser.ts +300 -0
- package/src/calls/media-stream-protocol.ts +166 -0
- package/src/calls/media-stream-server.ts +592 -0
- package/src/calls/media-stream-stt-session.ts +460 -0
- package/src/calls/media-turn-detector.ts +230 -0
- package/src/calls/relay-server.ts +90 -75
- package/src/calls/resolve-call-tts-provider.ts +136 -0
- package/src/calls/telephony-stt-routing.ts +145 -0
- package/src/calls/tts-call-strategy.ts +161 -0
- package/src/calls/tts-text-sanitizer.ts +32 -16
- package/src/calls/twilio-routes.ts +281 -17
- package/src/calls/voice-quality.ts +78 -35
- package/src/calls/voice-session-bridge.ts +8 -1
- package/src/channels/types.ts +16 -0
- package/src/cli/__tests__/run-assistant-command.ts +11 -1
- package/src/cli/commands/__tests__/backup.test.ts +1165 -0
- package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
- package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
- package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
- package/src/cli/commands/__tests__/email-download.test.ts +16 -1
- package/src/cli/commands/__tests__/email-list.test.ts +22 -4
- package/src/cli/commands/__tests__/email-register.test.ts +4 -4
- package/src/cli/commands/__tests__/email-send.test.ts +37 -4
- package/src/cli/commands/__tests__/email-status.test.ts +5 -1
- package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
- package/src/cli/commands/backup.ts +993 -0
- package/src/cli/commands/conversations.ts +77 -0
- package/src/cli/commands/credentials.ts +0 -1
- package/src/cli/commands/domain.ts +210 -0
- package/src/cli/commands/email.ts +255 -3
- package/src/cli/commands/oauth/__tests__/connect.test.ts +12 -0
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -0
- package/src/cli/commands/oauth/mode.ts +12 -3
- package/src/cli/commands/oauth/providers.ts +15 -0
- package/src/cli/commands/oauth/shared.ts +2 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +4 -9
- package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
- package/src/cli/program.ts +30 -4
- package/src/config/__tests__/backup-schema.test.ts +134 -0
- package/src/config/assistant-feature-flags.ts +61 -62
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +37 -1
- package/src/config/bundled-skills/browser/SKILL.md +30 -5
- package/src/config/bundled-skills/browser/TOOLS.json +123 -0
- package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
- package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
- package/src/config/bundled-skills/contacts/SKILL.md +2 -2
- package/src/config/bundled-skills/gmail/SKILL.md +53 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
- package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
- package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
- package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
- package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
- package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
- package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
- package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
- package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
- package/src/config/bundled-skills/outlook/SKILL.md +2 -2
- package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
- package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
- package/src/config/bundled-skills/slack/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
- package/src/config/bundled-tool-registry.ts +8 -0
- package/src/config/env-registry.ts +24 -0
- package/src/config/env.ts +34 -10
- package/src/config/feature-flag-registry.json +46 -14
- package/src/config/loader.ts +26 -12
- package/src/config/schema.ts +35 -10
- package/src/config/schemas/__tests__/stt.test.ts +43 -0
- package/src/config/schemas/analysis.ts +51 -0
- package/src/config/schemas/backup.ts +72 -0
- package/src/config/schemas/calls.ts +1 -26
- package/src/config/schemas/elevenlabs.ts +0 -59
- package/src/config/schemas/filing.ts +47 -7
- package/src/config/schemas/heartbeat.ts +27 -5
- package/src/config/schemas/host-browser.ts +47 -1
- package/src/config/schemas/inference.ts +1 -1
- package/src/config/schemas/memory-lifecycle.ts +14 -2
- package/src/config/schemas/services.ts +44 -0
- package/src/config/schemas/stt.ts +59 -0
- package/src/config/schemas/tts.ts +230 -0
- package/src/config/schemas/updates.ts +14 -0
- package/src/config/skills.ts +4 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +56 -11
- package/src/contacts/contacts-write.ts +38 -1
- package/src/context/post-turn-tool-result-truncation.ts +3 -2
- package/src/context/tool-result-truncation.ts +2 -1
- package/src/context/window-manager.ts +45 -12
- package/src/credential-execution/executable-discovery.ts +12 -2
- package/src/credential-execution/process-manager.ts +33 -2
- package/src/credential-health/credential-health-service.ts +366 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
- package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +17 -8
- package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
- package/src/daemon/config-watcher.ts +99 -5
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +101 -24
- package/src/daemon/conversation-error.ts +11 -0
- package/src/daemon/conversation-history.ts +40 -6
- package/src/daemon/conversation-launch.ts +220 -0
- package/src/daemon/conversation-lifecycle.ts +59 -9
- package/src/daemon/conversation-messaging.ts +37 -3
- package/src/daemon/conversation-notifiers.ts +5 -0
- package/src/daemon/conversation-process.ts +581 -19
- package/src/daemon/conversation-queue-manager.ts +24 -0
- package/src/daemon/conversation-runtime-assembly.ts +11 -1
- package/src/daemon/conversation-slash.ts +36 -0
- package/src/daemon/conversation-surfaces.ts +94 -4
- package/src/daemon/conversation-tool-setup.ts +25 -0
- package/src/daemon/conversation-usage.ts +7 -4
- package/src/daemon/conversation.ts +86 -28
- package/src/daemon/handlers/config-slack-channel.ts +269 -94
- package/src/daemon/handlers/conversations.ts +4 -1
- package/src/daemon/handlers/shared.ts +22 -0
- package/src/daemon/handlers/skills.ts +321 -77
- package/src/daemon/host-browser-proxy.ts +2 -1
- package/src/daemon/lifecycle.ts +122 -25
- package/src/daemon/message-protocol.ts +6 -0
- package/src/daemon/message-types/conversations.ts +34 -1
- package/src/daemon/message-types/home.ts +40 -0
- package/src/daemon/message-types/meet.ts +143 -0
- package/src/daemon/message-types/messages.ts +14 -0
- package/src/daemon/message-types/schedules.ts +34 -2
- package/src/daemon/message-types/skills.ts +16 -0
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/daemon/server.ts +347 -2
- package/src/daemon/shutdown-handlers.ts +32 -4
- package/src/daemon/shutdown-registry.ts +40 -0
- package/src/daemon/tool-side-effects.ts +9 -0
- package/src/email/html-renderer.ts +76 -0
- package/src/heartbeat/heartbeat-service.ts +93 -7
- package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
- package/src/home/__tests__/emit-feed-event.test.ts +169 -0
- package/src/home/__tests__/feed-scheduler.test.ts +194 -0
- package/src/home/__tests__/feed-types.test.ts +275 -0
- package/src/home/__tests__/feed-writer.test.ts +688 -0
- package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
- package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
- package/src/home/__tests__/progress-formula.test.ts +213 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
- package/src/home/__tests__/rollup-producer.test.ts +398 -0
- package/src/home/assistant-feed-authoring.ts +124 -0
- package/src/home/emit-feed-event.ts +158 -0
- package/src/home/feed-scheduler.ts +247 -0
- package/src/home/feed-types.ts +181 -0
- package/src/home/feed-writer.ts +469 -0
- package/src/home/platform-gmail-digest.ts +163 -0
- package/src/home/progress-formula.ts +86 -0
- package/src/home/relationship-state-writer.ts +824 -0
- package/src/home/relationship-state.ts +143 -0
- package/src/home/rollup-producer.ts +384 -0
- package/src/hooks/runner.ts +7 -0
- package/src/inbound/platform-callback-registration.ts +12 -3
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/instrument.ts +1 -1
- package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
- package/src/ipc/cli-client.ts +151 -0
- package/src/ipc/cli-server.ts +234 -0
- package/src/ipc/gateway-client.ts +180 -0
- package/src/ipc/routes/index.ts +5 -0
- package/src/ipc/routes/wake-conversation.ts +19 -0
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
- package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
- package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
- package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
- package/src/memory/app-store.ts +1 -1
- package/src/memory/attachments-store.ts +70 -0
- package/src/memory/auto-analysis-enqueue.ts +127 -0
- package/src/memory/auto-analysis-guard.ts +27 -0
- package/src/memory/cleanup-schedule-state.ts +37 -0
- package/src/memory/conversation-analyze-job.ts +73 -0
- package/src/memory/conversation-crud.ts +99 -0
- package/src/memory/conversation-disk-view.ts +7 -0
- package/src/memory/conversation-group-migration.ts +34 -2
- package/src/memory/conversation-queries.ts +6 -5
- package/src/memory/db-init.ts +6 -0
- package/src/memory/db-maintenance.ts +108 -0
- package/src/memory/db.ts +1 -0
- package/src/memory/graph/conversation-graph-memory.ts +15 -0
- package/src/memory/graph/extraction.test.ts +23 -0
- package/src/memory/graph/extraction.ts +8 -0
- package/src/memory/graph/retriever.ts +27 -18
- package/src/memory/graph/scoring.test.ts +186 -0
- package/src/memory/graph/scoring.ts +31 -1
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/group-crud.ts +6 -1
- package/src/memory/indexer.ts +95 -16
- package/src/memory/job-handlers/cleanup.ts +11 -8
- package/src/memory/job-handlers/conversation-starters.ts +16 -10
- package/src/memory/jobs-store.ts +64 -4
- package/src/memory/jobs-worker.ts +22 -9
- package/src/memory/llm-usage-store.ts +92 -56
- package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
- package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
- package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/qdrant-manager.ts +43 -16
- package/src/memory/schema/conversations.ts +2 -0
- package/src/memory/schema/oauth.ts +3 -0
- package/src/memory/usage-buckets.ts +396 -0
- package/src/messaging/providers/gmail/client.ts +57 -6
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
- package/src/messaging/providers/slack/adapter.ts +143 -38
- package/src/messaging/providers/slack/client.ts +16 -0
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/notifications/decision-engine.ts +3 -3
- package/src/notifications/signal.ts +5 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -0
- package/src/oauth/byo-connection.test.ts +18 -1
- package/src/oauth/byo-connection.ts +3 -1
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.ts +6 -2
- package/src/oauth/connection.ts +2 -0
- package/src/oauth/oauth-store.ts +9 -0
- package/src/oauth/platform-connection.test.ts +98 -0
- package/src/oauth/platform-connection.ts +52 -31
- package/src/oauth/seed-providers.ts +7 -0
- package/src/permissions/checker.ts +16 -6
- package/src/permissions/defaults.ts +49 -1
- package/src/permissions/trust-store.ts +3 -3
- package/src/permissions/workspace-policy.ts +3 -0
- package/src/platform/client.test.ts +10 -0
- package/src/platform/sync-identity.ts +129 -0
- package/src/prompts/persona-resolver.ts +126 -2
- package/src/prompts/system-prompt.ts +59 -18
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/SOUL.md +3 -1
- package/src/prompts/templates/UPDATES.md +12 -0
- package/src/prompts/templates/channels/slack.md +20 -0
- package/src/prompts/update-bulletin-format.ts +26 -9
- package/src/prompts/update-bulletin.ts +34 -23
- package/src/prompts/user-reference.ts +20 -17
- package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
- package/src/providers/anthropic/client.ts +157 -61
- package/src/providers/fireworks/client.ts +2 -2
- package/src/providers/gemini/client.ts +9 -1
- package/src/providers/model-catalog.ts +6 -0
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/ollama/client.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +474 -0
- package/src/providers/openai/client.ts +25 -440
- package/src/providers/openai/responses-provider.ts +502 -0
- package/src/providers/openrouter/client.ts +101 -4
- package/src/providers/provider-secret-catalog.ts +139 -0
- package/src/providers/registry.ts +2 -2
- package/src/providers/retry.ts +14 -3
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
- package/src/providers/speech-to-text/deepgram.test.ts +332 -0
- package/src/providers/speech-to-text/deepgram.ts +115 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
- package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
- package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
- package/src/providers/speech-to-text/google-gemini.ts +101 -0
- package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
- package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
- package/src/providers/speech-to-text/openai-whisper.ts +63 -33
- package/src/providers/speech-to-text/provider-catalog.ts +306 -0
- package/src/providers/speech-to-text/resolve.ts +386 -6
- package/src/providers/types.ts +9 -0
- package/src/runtime/AGENTS.md +43 -1
- package/src/runtime/__tests__/agent-wake.test.ts +831 -0
- package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
- package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
- package/src/runtime/agent-wake.ts +512 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +40 -0
- package/src/runtime/auth/route-policy.ts +30 -5
- package/src/runtime/auth/token-service.ts +56 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/capability-tokens.ts +10 -10
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-invite-transports/email.ts +14 -6
- package/src/runtime/channel-readiness-service.ts +12 -22
- package/src/runtime/chrome-extension-registry.ts +38 -2
- package/src/runtime/http-server.ts +395 -10
- package/src/runtime/http-types.ts +6 -2
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +36 -0
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
- package/src/runtime/migrations/migration-transport.ts +1 -0
- package/src/runtime/migrations/migration-wizard.ts +1 -0
- package/src/runtime/migrations/vbundle-import-analyzer.ts +77 -1
- package/src/runtime/migrations/vbundle-importer.ts +34 -0
- package/src/runtime/pending-interactions.ts +0 -11
- package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
- package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
- package/src/runtime/routes/app-management-routes.ts +12 -18
- package/src/runtime/routes/attachment-routes.test.ts +9 -3
- package/src/runtime/routes/attachment-routes.ts +216 -17
- package/src/runtime/routes/backup-routes.ts +519 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +82 -23
- package/src/runtime/routes/btw-routes.ts +8 -6
- package/src/runtime/routes/contact-routes.test.ts +298 -0
- package/src/runtime/routes/contact-routes.ts +132 -5
- package/src/runtime/routes/conversation-analysis-routes.ts +22 -142
- package/src/runtime/routes/conversation-management-routes.ts +115 -0
- package/src/runtime/routes/conversation-routes.ts +367 -146
- package/src/runtime/routes/filing-routes.ts +93 -0
- package/src/runtime/routes/home-feed-routes.ts +334 -0
- package/src/runtime/routes/home-state-routes.ts +138 -0
- package/src/runtime/routes/host-browser-routes.ts +3 -14
- package/src/runtime/routes/identity-intro-cache.ts +7 -3
- package/src/runtime/routes/identity-routes.ts +3 -17
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
- package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
- package/src/runtime/routes/integrations/slack/channel.ts +11 -3
- package/src/runtime/routes/integrations/slack/share.ts +45 -7
- package/src/runtime/routes/llm-context-normalization.ts +303 -0
- package/src/runtime/routes/memory-item-routes.test.ts +3 -2
- package/src/runtime/routes/migration-routes.ts +40 -5
- package/src/runtime/routes/settings-routes.ts +22 -5
- package/src/runtime/routes/skills-routes.ts +76 -7
- package/src/runtime/routes/stt-routes.ts +233 -0
- package/src/runtime/routes/surface-action-routes.ts +41 -2
- package/src/runtime/routes/tts-routes.ts +108 -24
- package/src/runtime/routes/usage-routes.ts +30 -2
- package/src/runtime/routes/user-route-dispatcher.ts +50 -5
- package/src/runtime/routes/user-routes.ts +13 -1
- package/src/runtime/routes/work-items-routes.ts +8 -1
- package/src/runtime/runtime-mode.ts +33 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
- package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
- package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
- package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
- package/src/runtime/services/analyze-conversation.ts +344 -0
- package/src/runtime/services/analyze-deps-singleton.ts +32 -0
- package/src/runtime/services/auto-analysis-prompt.ts +55 -0
- package/src/runtime/skill-route-registry.ts +49 -0
- package/src/runtime/slack-block-formatting.ts +437 -10
- package/src/schedule/scheduler.ts +50 -0
- package/src/security/oauth2.ts +26 -4
- package/src/security/secure-keys.ts +25 -2
- package/src/security/token-manager.ts +8 -0
- package/src/sequence/engine.ts +23 -0
- package/src/sequence/types.ts +1 -1
- package/src/skills/catalog-files.ts +64 -2
- package/src/skills/category-inference.ts +122 -0
- package/src/skills/clawhub-files.ts +213 -0
- package/src/skills/clawhub.ts +84 -23
- package/src/skills/skill-file-provider.ts +40 -0
- package/src/skills/skillssh-files.ts +395 -0
- package/src/skills/skillssh-registry.ts +4 -4
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
- package/src/stt/__tests__/types.test.ts +89 -0
- package/src/stt/daemon-batch-transcriber.ts +195 -0
- package/src/stt/stt-stream-session.ts +499 -0
- package/src/stt/types.ts +330 -0
- package/src/stt/wav-encoder.test.ts +373 -0
- package/src/stt/wav-encoder.ts +175 -0
- package/src/subagent/manager.ts +38 -14
- package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
- package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
- package/src/tools/browser/browser-execution.ts +1163 -23
- package/src/tools/browser/browser-manager.ts +45 -0
- package/src/tools/browser/browser-mode-constants.ts +12 -0
- package/src/tools/browser/browser-mode.ts +92 -0
- package/src/tools/browser/browser-status-constants.ts +33 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +393 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +29 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1648 -32
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +264 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +183 -17
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +254 -21
- package/src/tools/browser/cdp-client/errors.ts +15 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +39 -16
- package/src/tools/browser/cdp-client/factory.ts +797 -87
- package/src/tools/browser/cdp-client/index.ts +16 -2
- package/src/tools/browser/cdp-client/types.ts +68 -0
- package/src/tools/credentials/vault.ts +35 -6
- package/src/tools/network/web-fetch.ts +5 -2
- package/src/tools/network/web-search.ts +5 -2
- package/src/tools/shared/shell-output.ts +3 -1
- package/src/tools/side-effects.ts +2 -0
- package/src/tools/skills/sandbox-runner.ts +3 -2
- package/src/tools/terminal/safe-env.ts +10 -2
- package/src/tools/terminal/shell.ts +15 -4
- package/src/tools/tool-manifest.ts +21 -0
- package/src/tools/types.ts +17 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tts/__tests__/provider-adapters.test.ts +834 -0
- package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
- package/src/tts/__tests__/provider-catalog.test.ts +183 -0
- package/src/tts/__tests__/provider-registry.test.ts +90 -0
- package/src/tts/provider-catalog.ts +201 -0
- package/src/tts/provider-registry.ts +73 -0
- package/src/tts/providers/deepgram-provider.ts +219 -0
- package/src/tts/providers/elevenlabs-provider.ts +211 -0
- package/src/tts/providers/fish-audio-provider.ts +183 -0
- package/src/tts/providers/index.ts +42 -0
- package/src/tts/providers/register-builtins.ts +130 -0
- package/src/tts/synthesize-text.ts +110 -0
- package/src/tts/tts-config-resolver.ts +78 -0
- package/src/tts/types.ts +153 -0
- package/src/types/onboarding-context.ts +7 -0
- package/src/util/abort-reasons.ts +58 -0
- package/src/util/device-id.ts +32 -16
- package/src/util/errors.ts +9 -1
- package/src/util/platform.ts +54 -10
- package/src/util/pricing.ts +66 -3
- package/src/util/spawn.ts +1 -1
- package/src/util/truncate.ts +4 -2
- package/src/util/unicode.ts +201 -0
- package/src/version.ts +19 -24
- package/src/watcher/engine.ts +23 -0
- package/src/watcher/watcher-store.ts +31 -0
- package/src/workspace/migrations/003-seed-device-id.ts +9 -3
- package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
- package/src/workspace/migrations/029-seed-pkb.ts +1 -1
- package/src/workspace/migrations/031-drop-user-md.ts +317 -0
- package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
- package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
- package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
- package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
- package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
- package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
- package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
- package/src/workspace/migrations/registry.ts +16 -0
- package/src/workspace/top-level-renderer.ts +13 -1
- package/src/workspace/turn-commit.ts +31 -0
- package/src/__tests__/email-cli.test.ts +0 -297
- package/src/__tests__/email-service-config-fallback.test.ts +0 -102
- package/src/cli/commands/browser-relay.ts +0 -466
- package/src/email/guardrails.ts +0 -221
- package/src/email/provider.ts +0 -117
- package/src/email/providers/agentmail.ts +0 -361
- package/src/email/providers/index.ts +0 -65
- package/src/email/service.ts +0 -384
- package/src/email/types.ts +0 -126
- package/src/prompts/templates/USER.md +0 -13
- package/src/providers/speech-to-text/types.ts +0 -17
- package/src/runtime/routes/browser-cdp-routes.ts +0 -229
|
@@ -14,12 +14,38 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
16
|
import { getWorkspaceRoutesDir } from "../../../util/platform.js";
|
|
17
|
+
import { AssistantEventHub } from "../../assistant-event-hub.js";
|
|
18
|
+
import type { UserRouteContext } from "../user-route-dispatcher.js";
|
|
17
19
|
import { UserRouteDispatcher } from "../user-route-dispatcher.js";
|
|
18
20
|
|
|
19
21
|
// ---------------------------------------------------------------------------
|
|
20
22
|
// Helpers
|
|
21
23
|
// ---------------------------------------------------------------------------
|
|
22
24
|
|
|
25
|
+
/** Build a minimal UserRouteContext for tests. */
|
|
26
|
+
function makeContext(
|
|
27
|
+
overrides?: Partial<UserRouteContext>,
|
|
28
|
+
): UserRouteContext {
|
|
29
|
+
return {
|
|
30
|
+
assistantEventHub: new AssistantEventHub(),
|
|
31
|
+
assistantId: "test-assistant",
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Create a dispatcher with a stub context and optional overrides. */
|
|
37
|
+
function makeDispatcher(
|
|
38
|
+
options?: Partial<{
|
|
39
|
+
handlerTimeoutMs: number;
|
|
40
|
+
context: UserRouteContext;
|
|
41
|
+
}>,
|
|
42
|
+
): UserRouteDispatcher {
|
|
43
|
+
return new UserRouteDispatcher({
|
|
44
|
+
context: options?.context ?? makeContext(),
|
|
45
|
+
...options,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
function makeRequest(
|
|
24
50
|
method: string,
|
|
25
51
|
path = "http://localhost/v1/x/test",
|
|
@@ -60,7 +86,7 @@ afterEach(() => {
|
|
|
60
86
|
|
|
61
87
|
describe("path traversal", () => {
|
|
62
88
|
test("rejects paths containing '..'", async () => {
|
|
63
|
-
const dispatcher =
|
|
89
|
+
const dispatcher = makeDispatcher();
|
|
64
90
|
const res = await dispatcher.dispatch("../etc/passwd", makeRequest("GET"));
|
|
65
91
|
expect(res.status).toBe(400);
|
|
66
92
|
const body = await readErrorBody(res);
|
|
@@ -69,7 +95,7 @@ describe("path traversal", () => {
|
|
|
69
95
|
});
|
|
70
96
|
|
|
71
97
|
test("rejects embedded '..' segments", async () => {
|
|
72
|
-
const dispatcher =
|
|
98
|
+
const dispatcher = makeDispatcher();
|
|
73
99
|
const res = await dispatcher.dispatch(
|
|
74
100
|
"foo/../../etc/passwd",
|
|
75
101
|
makeRequest("GET"),
|
|
@@ -84,7 +110,7 @@ describe("path traversal", () => {
|
|
|
84
110
|
|
|
85
111
|
describe("missing handler", () => {
|
|
86
112
|
test("returns 404 when no handler file exists", async () => {
|
|
87
|
-
const dispatcher =
|
|
113
|
+
const dispatcher = makeDispatcher();
|
|
88
114
|
const res = await dispatcher.dispatch("nonexistent", makeRequest("GET"));
|
|
89
115
|
expect(res.status).toBe(404);
|
|
90
116
|
const body = await readErrorBody(res);
|
|
@@ -106,7 +132,7 @@ describe("successful dispatch", () => {
|
|
|
106
132
|
}`,
|
|
107
133
|
);
|
|
108
134
|
|
|
109
|
-
const dispatcher =
|
|
135
|
+
const dispatcher = makeDispatcher();
|
|
110
136
|
const res = await dispatcher.dispatch("hello", makeRequest("GET"));
|
|
111
137
|
expect(res.status).toBe(200);
|
|
112
138
|
const body = await res.json();
|
|
@@ -121,7 +147,7 @@ describe("successful dispatch", () => {
|
|
|
121
147
|
}`,
|
|
122
148
|
);
|
|
123
149
|
|
|
124
|
-
const dispatcher =
|
|
150
|
+
const dispatcher = makeDispatcher();
|
|
125
151
|
const res = await dispatcher.dispatch("submit", makeRequest("POST"));
|
|
126
152
|
expect(res.status).toBe(201);
|
|
127
153
|
const body = await res.json();
|
|
@@ -136,7 +162,7 @@ describe("successful dispatch", () => {
|
|
|
136
162
|
}`,
|
|
137
163
|
);
|
|
138
164
|
|
|
139
|
-
const dispatcher =
|
|
165
|
+
const dispatcher = makeDispatcher();
|
|
140
166
|
const res = await dispatcher.dispatch("legacy", makeRequest("GET"));
|
|
141
167
|
expect(res.status).toBe(200);
|
|
142
168
|
const body = await res.json();
|
|
@@ -157,7 +183,7 @@ describe("index file convention", () => {
|
|
|
157
183
|
}`,
|
|
158
184
|
);
|
|
159
185
|
|
|
160
|
-
const dispatcher =
|
|
186
|
+
const dispatcher = makeDispatcher();
|
|
161
187
|
const res = await dispatcher.dispatch("my-app", makeRequest("GET"));
|
|
162
188
|
expect(res.status).toBe(200);
|
|
163
189
|
const body = await res.json();
|
|
@@ -172,7 +198,7 @@ describe("index file convention", () => {
|
|
|
172
198
|
}`,
|
|
173
199
|
);
|
|
174
200
|
|
|
175
|
-
const dispatcher =
|
|
201
|
+
const dispatcher = makeDispatcher();
|
|
176
202
|
const res = await dispatcher.dispatch("fallback-app", makeRequest("GET"));
|
|
177
203
|
expect(res.status).toBe(200);
|
|
178
204
|
const body = await res.json();
|
|
@@ -193,7 +219,7 @@ describe("index file convention", () => {
|
|
|
193
219
|
}`,
|
|
194
220
|
);
|
|
195
221
|
|
|
196
|
-
const dispatcher =
|
|
222
|
+
const dispatcher = makeDispatcher();
|
|
197
223
|
const res = await dispatcher.dispatch("dual", makeRequest("GET"));
|
|
198
224
|
expect(res.status).toBe(200);
|
|
199
225
|
const body = await res.json();
|
|
@@ -214,7 +240,7 @@ describe("method not allowed", () => {
|
|
|
214
240
|
}`,
|
|
215
241
|
);
|
|
216
242
|
|
|
217
|
-
const dispatcher =
|
|
243
|
+
const dispatcher = makeDispatcher();
|
|
218
244
|
const res = await dispatcher.dispatch("get-only", makeRequest("POST"));
|
|
219
245
|
expect(res.status).toBe(405);
|
|
220
246
|
expect(res.headers.get("Allow")).toBe("GET");
|
|
@@ -228,7 +254,7 @@ describe("method not allowed", () => {
|
|
|
228
254
|
export function DELETE(request) { return new Response("ok"); }`,
|
|
229
255
|
);
|
|
230
256
|
|
|
231
|
-
const dispatcher =
|
|
257
|
+
const dispatcher = makeDispatcher();
|
|
232
258
|
const res = await dispatcher.dispatch("multi", makeRequest("PUT"));
|
|
233
259
|
expect(res.status).toBe(405);
|
|
234
260
|
const allow = res.headers.get("Allow");
|
|
@@ -252,7 +278,7 @@ describe("handler timeout", () => {
|
|
|
252
278
|
);
|
|
253
279
|
|
|
254
280
|
// Use a very short timeout for testing
|
|
255
|
-
const dispatcher =
|
|
281
|
+
const dispatcher = makeDispatcher({ handlerTimeoutMs: 50 });
|
|
256
282
|
const res = await dispatcher.dispatch("slow", makeRequest("GET"));
|
|
257
283
|
expect(res.status).toBe(504);
|
|
258
284
|
const body = await readErrorBody(res);
|
|
@@ -274,7 +300,7 @@ describe("handler errors", () => {
|
|
|
274
300
|
}`,
|
|
275
301
|
);
|
|
276
302
|
|
|
277
|
-
const dispatcher =
|
|
303
|
+
const dispatcher = makeDispatcher();
|
|
278
304
|
const res = await dispatcher.dispatch("throws", makeRequest("GET"));
|
|
279
305
|
expect(res.status).toBe(500);
|
|
280
306
|
const body = await readErrorBody(res);
|
|
@@ -290,7 +316,7 @@ describe("handler errors", () => {
|
|
|
290
316
|
}`,
|
|
291
317
|
);
|
|
292
318
|
|
|
293
|
-
const dispatcher =
|
|
319
|
+
const dispatcher = makeDispatcher();
|
|
294
320
|
const res = await dispatcher.dispatch("rejects", makeRequest("GET"));
|
|
295
321
|
expect(res.status).toBe(500);
|
|
296
322
|
const body = await readErrorBody(res);
|
|
@@ -311,7 +337,7 @@ describe("mtime cache", () => {
|
|
|
311
337
|
}`,
|
|
312
338
|
);
|
|
313
339
|
|
|
314
|
-
const dispatcher =
|
|
340
|
+
const dispatcher = makeDispatcher();
|
|
315
341
|
|
|
316
342
|
// First request — version 1
|
|
317
343
|
const res1 = await dispatcher.dispatch("mutable", makeRequest("GET"));
|
|
@@ -349,7 +375,7 @@ describe("subdirectory routing", () => {
|
|
|
349
375
|
}`,
|
|
350
376
|
);
|
|
351
377
|
|
|
352
|
-
const dispatcher =
|
|
378
|
+
const dispatcher = makeDispatcher();
|
|
353
379
|
const res = await dispatcher.dispatch("api/v1/status", makeRequest("GET"));
|
|
354
380
|
expect(res.status).toBe(200);
|
|
355
381
|
const body = await res.json();
|
|
@@ -371,8 +397,113 @@ describe("description metadata", () => {
|
|
|
371
397
|
}`,
|
|
372
398
|
);
|
|
373
399
|
|
|
374
|
-
const dispatcher =
|
|
400
|
+
const dispatcher = makeDispatcher();
|
|
375
401
|
const res = await dispatcher.dispatch("with-meta", makeRequest("GET"));
|
|
376
402
|
expect(res.status).toBe(200);
|
|
377
403
|
});
|
|
378
404
|
});
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Context injection
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
describe("context injection", () => {
|
|
411
|
+
test("passes UserRouteContext as second argument to handler", async () => {
|
|
412
|
+
writeHandler(
|
|
413
|
+
"ctx-echo.ts",
|
|
414
|
+
`export function GET(request, context) {
|
|
415
|
+
return Response.json({
|
|
416
|
+
hasHub: typeof context.assistantEventHub?.publish === "function",
|
|
417
|
+
assistantId: context.assistantId,
|
|
418
|
+
});
|
|
419
|
+
}`,
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
const ctx = makeContext({ assistantId: "custom-id" });
|
|
423
|
+
const dispatcher = makeDispatcher({ context: ctx });
|
|
424
|
+
const res = await dispatcher.dispatch("ctx-echo", makeRequest("GET"));
|
|
425
|
+
expect(res.status).toBe(200);
|
|
426
|
+
const body = await res.json();
|
|
427
|
+
expect(body.hasHub).toBe(true);
|
|
428
|
+
expect(body.assistantId).toBe("custom-id");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("handler can publish events through injected hub", async () => {
|
|
432
|
+
writeHandler(
|
|
433
|
+
"ctx-publish.ts",
|
|
434
|
+
`export async function POST(request, context) {
|
|
435
|
+
const body = await request.json();
|
|
436
|
+
await context.assistantEventHub.publish({
|
|
437
|
+
id: "test-event-1",
|
|
438
|
+
assistantId: context.assistantId,
|
|
439
|
+
conversationId: body.conversationId,
|
|
440
|
+
emittedAt: new Date().toISOString(),
|
|
441
|
+
message: { type: "open_conversation", conversationId: body.conversationId },
|
|
442
|
+
});
|
|
443
|
+
return Response.json({ published: true });
|
|
444
|
+
}`,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
const hub = new AssistantEventHub();
|
|
448
|
+
const received: unknown[] = [];
|
|
449
|
+
hub.subscribe(
|
|
450
|
+
{ assistantId: "test-assistant" },
|
|
451
|
+
(event) => { received.push(event); },
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const ctx = makeContext({ assistantEventHub: hub });
|
|
455
|
+
const dispatcher = makeDispatcher({ context: ctx });
|
|
456
|
+
const req = new Request("http://localhost/v1/x/ctx-publish", {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: { "Content-Type": "application/json" },
|
|
459
|
+
body: JSON.stringify({ conversationId: "conv-123" }),
|
|
460
|
+
});
|
|
461
|
+
const res = await dispatcher.dispatch("ctx-publish", req);
|
|
462
|
+
expect(res.status).toBe(200);
|
|
463
|
+
const body = await res.json();
|
|
464
|
+
expect(body.published).toBe(true);
|
|
465
|
+
expect(received).toHaveLength(1);
|
|
466
|
+
expect((received[0] as { conversationId: string }).conversationId).toBe(
|
|
467
|
+
"conv-123",
|
|
468
|
+
);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("legacy handlers that ignore context still work", async () => {
|
|
472
|
+
writeHandler(
|
|
473
|
+
"no-ctx.ts",
|
|
474
|
+
`export function GET(request) {
|
|
475
|
+
return Response.json({ legacy: true });
|
|
476
|
+
}`,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
const dispatcher = makeDispatcher();
|
|
480
|
+
const res = await dispatcher.dispatch("no-ctx", makeRequest("GET"));
|
|
481
|
+
expect(res.status).toBe(200);
|
|
482
|
+
const body = await res.json();
|
|
483
|
+
expect(body.legacy).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("context is frozen — mutations throw in strict mode", async () => {
|
|
487
|
+
writeHandler(
|
|
488
|
+
"ctx-mutate.ts",
|
|
489
|
+
`export function GET(request, context) {
|
|
490
|
+
let threw = false;
|
|
491
|
+
try {
|
|
492
|
+
context.assistantId = "hacked";
|
|
493
|
+
} catch {
|
|
494
|
+
threw = true;
|
|
495
|
+
}
|
|
496
|
+
return Response.json({ threw, assistantId: context.assistantId });
|
|
497
|
+
}`,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const ctx = makeContext({ assistantId: "original" });
|
|
501
|
+
const dispatcher = makeDispatcher({ context: ctx });
|
|
502
|
+
const res = await dispatcher.dispatch("ctx-mutate", makeRequest("GET"));
|
|
503
|
+
expect(res.status).toBe(200);
|
|
504
|
+
const body = await res.json();
|
|
505
|
+
// Object.freeze makes property assignment throw in strict mode (ESM)
|
|
506
|
+
// and silently fail in sloppy mode — either way the value is unchanged.
|
|
507
|
+
expect(body.assistantId).toBe("original");
|
|
508
|
+
});
|
|
509
|
+
});
|
|
@@ -23,6 +23,7 @@ import { z } from "zod";
|
|
|
23
23
|
import { packageApp } from "../../bundler/app-bundler.js";
|
|
24
24
|
import { compileApp } from "../../bundler/app-compiler.js";
|
|
25
25
|
import { scanBundle } from "../../bundler/bundle-scanner.js";
|
|
26
|
+
import type { SignatureJson } from "../../bundler/bundle-signer.js";
|
|
26
27
|
import { verifyBundleSignature } from "../../bundler/signature-verifier.js";
|
|
27
28
|
import { compareSemver } from "../../daemon/handlers/shared.js";
|
|
28
29
|
import { defaultGallery } from "../../gallery/default-gallery.js";
|
|
@@ -39,11 +40,11 @@ import {
|
|
|
39
40
|
getApp,
|
|
40
41
|
getAppDirPath,
|
|
41
42
|
getAppPreview,
|
|
42
|
-
inlineDistAssets,
|
|
43
43
|
isMultifileApp,
|
|
44
44
|
listApps,
|
|
45
45
|
queryAppRecords,
|
|
46
46
|
resolveAppDir,
|
|
47
|
+
resolveEffectiveAppHtml,
|
|
47
48
|
updateApp,
|
|
48
49
|
updateAppRecord,
|
|
49
50
|
} from "../../memory/app-store.js";
|
|
@@ -583,16 +584,15 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
|
|
|
583
584
|
return httpError("BAD_REQUEST", "payload is not valid JSON", 400);
|
|
584
585
|
}
|
|
585
586
|
|
|
586
|
-
const signatureJson:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
};
|
|
587
|
+
const signatureJson: SignatureJson = {
|
|
588
|
+
algorithm: "ed25519",
|
|
589
|
+
signer: {
|
|
590
|
+
key_id: body.keyId,
|
|
591
|
+
display_name: "HTTP Signer",
|
|
592
|
+
},
|
|
593
|
+
content_hashes: contentHashes,
|
|
594
|
+
signature: body.signature,
|
|
595
|
+
};
|
|
596
596
|
return Response.json({ signed: true, signatureJson });
|
|
597
597
|
}
|
|
598
598
|
|
|
@@ -738,8 +738,6 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
|
|
|
738
738
|
return httpError("NOT_FOUND", `App not found: ${appId}`, 404);
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
-
let html = app.htmlDefinition;
|
|
742
|
-
|
|
743
741
|
if (isMultifileApp(app)) {
|
|
744
742
|
const appDir = getAppDirPath(appId);
|
|
745
743
|
const distIndex = join(appDir, "dist", "index.html");
|
|
@@ -752,12 +750,8 @@ export function appManagementRouteDefinitions(): RouteDefinition[] {
|
|
|
752
750
|
);
|
|
753
751
|
}
|
|
754
752
|
}
|
|
755
|
-
if (existsSync(distIndex)) {
|
|
756
|
-
html = inlineDistAssets(appDir, readFileSync(distIndex, "utf-8"));
|
|
757
|
-
} else {
|
|
758
|
-
html = `<p>App compilation failed. Edit a source file to trigger a rebuild.</p>`;
|
|
759
|
-
}
|
|
760
753
|
}
|
|
754
|
+
const html = resolveEffectiveAppHtml(app);
|
|
761
755
|
|
|
762
756
|
const { dirName } = resolveAppDir(app.id);
|
|
763
757
|
return Response.json({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mkdirSync,
|
|
3
3
|
mkdtempSync,
|
|
4
|
+
realpathSync,
|
|
4
5
|
rmSync,
|
|
5
6
|
symlinkSync,
|
|
6
7
|
writeFileSync,
|
|
@@ -9,10 +10,15 @@ import { tmpdir } from "node:os";
|
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
// realpathSync resolves the macOS /var → /private/var symlink so the paths
|
|
14
|
+
// match what resolveAllowedFileBackedAttachmentPath returns (it canonicalizes
|
|
15
|
+
// via realpathSync internally).
|
|
16
|
+
const testWorkspaceDir = realpathSync(
|
|
17
|
+
mkdtempSync(join(tmpdir(), "attachment-routes-workspace-")),
|
|
18
|
+
);
|
|
19
|
+
const testHomeDir = realpathSync(
|
|
20
|
+
mkdtempSync(join(tmpdir(), "attachment-routes-home-")),
|
|
14
21
|
);
|
|
15
|
-
const testHomeDir = mkdtempSync(join(tmpdir(), "attachment-routes-home-"));
|
|
16
22
|
|
|
17
23
|
const attachmentsDir = join(testWorkspaceDir, "data", "attachments");
|
|
18
24
|
const conversationsDir = join(testWorkspaceDir, "conversations");
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Route handlers for attachment upload, download, and deletion.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
copyFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} from "node:fs";
|
|
5
11
|
import { join, resolve, sep } from "node:path";
|
|
6
12
|
|
|
7
13
|
import { z } from "zod";
|
|
@@ -19,6 +25,9 @@ import type { RouteDefinition } from "../http-router.js";
|
|
|
19
25
|
/** 150 MB — base64-encoded 100 MB attachment ≈ 134 MB plus JSON wrapper overhead. */
|
|
20
26
|
const MAX_UPLOAD_BODY_BYTES = 150 * 1024 * 1024;
|
|
21
27
|
|
|
28
|
+
/** 100 MB — maximum file size for file-backed uploads (matches client memorySafetyLimit). */
|
|
29
|
+
const MAX_FILE_BACKED_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
30
|
+
|
|
22
31
|
function resolveCanonicalPath(filePath: string): string {
|
|
23
32
|
try {
|
|
24
33
|
return realpathSync(filePath);
|
|
@@ -88,7 +97,160 @@ export function resolveAllowedFileBackedAttachmentPath(
|
|
|
88
97
|
return null;
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
/** 100 MB — maximum file size for binary uploads (multipart / octet-stream). */
|
|
101
|
+
const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the standard JSON success response for an uploaded attachment.
|
|
105
|
+
*/
|
|
106
|
+
function attachmentResponse(
|
|
107
|
+
attachment: attachmentsStore.StoredAttachment,
|
|
108
|
+
): Response {
|
|
109
|
+
return Response.json({
|
|
110
|
+
id: attachment.id,
|
|
111
|
+
original_filename: attachment.originalFilename,
|
|
112
|
+
mime_type: attachment.mimeType,
|
|
113
|
+
size_bytes: attachment.sizeBytes,
|
|
114
|
+
kind: attachment.kind,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Content-Type dispatched upload handlers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handle multipart/form-data upload.
|
|
124
|
+
* Expects: "file" (Blob), "filename" (string), "mimeType" (string).
|
|
125
|
+
*/
|
|
126
|
+
async function handleMultipartUpload(req: Request): Promise<Response> {
|
|
127
|
+
// Pre-check Content-Length before parsing to reject oversized requests
|
|
128
|
+
// without buffering the full multipart body into memory. Binary uploads
|
|
129
|
+
// have no base64 overhead, so use the raw file size limit directly.
|
|
130
|
+
const contentLength = req.headers.get("content-length");
|
|
131
|
+
if (contentLength && Number(contentLength) > MAX_UPLOAD_BYTES) {
|
|
132
|
+
return httpError(
|
|
133
|
+
"BAD_REQUEST",
|
|
134
|
+
`File too large (limit: ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB)`,
|
|
135
|
+
413,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let formData: FormData;
|
|
140
|
+
try {
|
|
141
|
+
formData = await req.formData();
|
|
142
|
+
} catch {
|
|
143
|
+
return httpError("BAD_REQUEST", "Invalid multipart form data", 400);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const file = formData.get("file");
|
|
147
|
+
if (!file || !(file instanceof Blob)) {
|
|
148
|
+
return httpError(
|
|
149
|
+
"BAD_REQUEST",
|
|
150
|
+
'Multipart upload requires a "file" field',
|
|
151
|
+
400,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const filename = formData.get("filename");
|
|
156
|
+
if (!filename || typeof filename !== "string") {
|
|
157
|
+
return httpError("BAD_REQUEST", "filename field is required", 400);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const mimeType = formData.get("mimeType");
|
|
161
|
+
if (!mimeType || typeof mimeType !== "string") {
|
|
162
|
+
return httpError("BAD_REQUEST", "mimeType field is required", 400);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check file part size against the raw file limit (Content-Length may be
|
|
166
|
+
// absent or inaccurate, so this is the authoritative check).
|
|
167
|
+
if (file.size > MAX_UPLOAD_BYTES) {
|
|
168
|
+
return httpError(
|
|
169
|
+
"BAD_REQUEST",
|
|
170
|
+
`File is ${Math.round(file.size / (1024 * 1024))} MB which exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB upload limit`,
|
|
171
|
+
413,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const validation = validateAttachmentUpload(filename, mimeType);
|
|
176
|
+
if (!validation.ok) {
|
|
177
|
+
return httpError("UNPROCESSABLE_ENTITY", validation.error, 415);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
181
|
+
|
|
182
|
+
const attachment = attachmentsStore.uploadAttachmentFromBytes(
|
|
183
|
+
filename,
|
|
184
|
+
mimeType,
|
|
185
|
+
bytes,
|
|
186
|
+
);
|
|
187
|
+
return attachmentResponse(attachment);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Handle application/octet-stream upload.
|
|
192
|
+
* filename and mimeType come from URL query params.
|
|
193
|
+
*/
|
|
194
|
+
async function handleOctetStreamUpload(req: Request): Promise<Response> {
|
|
195
|
+
// Pre-check Content-Length before buffering to reject oversized requests
|
|
196
|
+
// without reading the full body into memory.
|
|
197
|
+
const contentLength = req.headers.get("content-length");
|
|
198
|
+
if (contentLength && Number(contentLength) > MAX_UPLOAD_BYTES) {
|
|
199
|
+
return httpError(
|
|
200
|
+
"BAD_REQUEST",
|
|
201
|
+
`File too large (limit: ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB)`,
|
|
202
|
+
413,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const url = new URL(req.url);
|
|
207
|
+
const filename = url.searchParams.get("filename");
|
|
208
|
+
if (!filename || typeof filename !== "string") {
|
|
209
|
+
return httpError(
|
|
210
|
+
"BAD_REQUEST",
|
|
211
|
+
"filename query parameter is required",
|
|
212
|
+
400,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const mimeType = url.searchParams.get("mimeType");
|
|
217
|
+
if (!mimeType || typeof mimeType !== "string") {
|
|
218
|
+
return httpError(
|
|
219
|
+
"BAD_REQUEST",
|
|
220
|
+
"mimeType query parameter is required",
|
|
221
|
+
400,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rawBody = await req.arrayBuffer();
|
|
226
|
+
// Post-read check (Content-Length may be absent or inaccurate).
|
|
227
|
+
if (rawBody.byteLength > MAX_UPLOAD_BYTES) {
|
|
228
|
+
return httpError(
|
|
229
|
+
"BAD_REQUEST",
|
|
230
|
+
`File is ${Math.round(rawBody.byteLength / (1024 * 1024))} MB which exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB upload limit`,
|
|
231
|
+
413,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const validation = validateAttachmentUpload(filename, mimeType);
|
|
236
|
+
if (!validation.ok) {
|
|
237
|
+
return httpError("UNPROCESSABLE_ENTITY", validation.error, 415);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const bytes = new Uint8Array(rawBody);
|
|
241
|
+
|
|
242
|
+
const attachment = attachmentsStore.uploadAttachmentFromBytes(
|
|
243
|
+
filename,
|
|
244
|
+
mimeType,
|
|
245
|
+
bytes,
|
|
246
|
+
);
|
|
247
|
+
return attachmentResponse(attachment);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Handle application/json upload (existing behaviour — base64 or file-path).
|
|
252
|
+
*/
|
|
253
|
+
async function handleJsonUpload(req: Request): Promise<Response> {
|
|
92
254
|
const rawBody = await req.arrayBuffer();
|
|
93
255
|
if (rawBody.byteLength > MAX_UPLOAD_BODY_BYTES) {
|
|
94
256
|
return httpError(
|
|
@@ -124,17 +286,45 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
|
|
|
124
286
|
|
|
125
287
|
// File-backed upload: when filePath is provided and data is empty/missing,
|
|
126
288
|
// register the attachment by path reference instead of requiring base64 data.
|
|
127
|
-
// This supports
|
|
128
|
-
// client
|
|
289
|
+
// This supports:
|
|
290
|
+
// 1. Desktop client file-picker uploads — the file is copied into the
|
|
291
|
+
// workspace attachments directory so it passes the directory allowlist.
|
|
292
|
+
// 2. Retry of file-backed attachments (e.g. recordings) where the client
|
|
293
|
+
// no longer holds the inline data but the file still exists on disk.
|
|
129
294
|
if (filePath && typeof filePath === "string" && (!data || data === "")) {
|
|
130
|
-
|
|
295
|
+
let resolvedPath = resolveAllowedFileBackedAttachmentPath(filePath);
|
|
296
|
+
|
|
297
|
+
// If the file isn't in an allowed directory, copy it into the workspace
|
|
298
|
+
// attachments directory. This handles desktop client file-picker uploads
|
|
299
|
+
// where the source file lives in an arbitrary user directory (e.g.
|
|
300
|
+
// ~/Desktop, ~/Downloads). The copy lands in the allowlisted workspace
|
|
301
|
+
// directory, preserving the security model.
|
|
131
302
|
if (!resolvedPath) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
"filePath
|
|
135
|
-
|
|
303
|
+
const canonicalSource = resolveCanonicalPath(filePath);
|
|
304
|
+
if (!existsSync(canonicalSource)) {
|
|
305
|
+
return httpError("BAD_REQUEST", "filePath does not exist on disk", 400);
|
|
306
|
+
}
|
|
307
|
+
const sourceSize = statSync(canonicalSource).size;
|
|
308
|
+
if (sourceSize > MAX_FILE_BACKED_UPLOAD_BYTES) {
|
|
309
|
+
const sizeMB = Math.round(sourceSize / (1024 * 1024));
|
|
310
|
+
return httpError(
|
|
311
|
+
"BAD_REQUEST",
|
|
312
|
+
`File is ${sizeMB} MB which exceeds the ${MAX_FILE_BACKED_UPLOAD_BYTES / (1024 * 1024)} MB upload limit`,
|
|
313
|
+
413,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
const workspaceAttachmentsDir = join(
|
|
317
|
+
getWorkspaceDir(),
|
|
318
|
+
"data",
|
|
319
|
+
"attachments",
|
|
136
320
|
);
|
|
321
|
+
mkdirSync(workspaceAttachmentsDir, { recursive: true });
|
|
322
|
+
const destFilename = `${Date.now()}-${filename.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
323
|
+
const destPath = join(workspaceAttachmentsDir, destFilename);
|
|
324
|
+
copyFileSync(canonicalSource, destPath);
|
|
325
|
+
resolvedPath = resolveCanonicalPath(destPath);
|
|
137
326
|
}
|
|
327
|
+
|
|
138
328
|
if (!existsSync(resolvedPath)) {
|
|
139
329
|
return httpError("BAD_REQUEST", "filePath does not exist on disk", 400);
|
|
140
330
|
}
|
|
@@ -168,13 +358,22 @@ export async function handleUploadAttachment(req: Request): Promise<Response> {
|
|
|
168
358
|
}
|
|
169
359
|
}
|
|
170
360
|
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
361
|
+
return attachmentResponse(attachment);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function handleUploadAttachment(req: Request): Promise<Response> {
|
|
365
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
366
|
+
|
|
367
|
+
if (contentType.includes("multipart/form-data")) {
|
|
368
|
+
return handleMultipartUpload(req);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (contentType.includes("application/octet-stream")) {
|
|
372
|
+
return handleOctetStreamUpload(req);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Default: JSON+base64 (existing behaviour)
|
|
376
|
+
return handleJsonUpload(req);
|
|
178
377
|
}
|
|
179
378
|
|
|
180
379
|
export async function handleDeleteAttachment(req: Request): Promise<Response> {
|
|
@@ -355,7 +554,7 @@ export function attachmentRouteDefinitions(): RouteDefinition[] {
|
|
|
355
554
|
method: "POST",
|
|
356
555
|
summary: "Upload attachment",
|
|
357
556
|
description:
|
|
358
|
-
"Upload an attachment
|
|
557
|
+
"Upload an attachment. Supports application/json (base64 data or file path reference), multipart/form-data (file + filename + mimeType fields), and application/octet-stream (raw bytes with filename and mimeType query params).",
|
|
359
558
|
tags: ["attachments"],
|
|
360
559
|
requestBody: z.object({
|
|
361
560
|
filename: z.string(),
|