@vellumai/assistant 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +32 -49
- package/Dockerfile +1 -0
- package/README.md +1 -2
- package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
- package/bun.lock +26 -26
- package/docs/architecture/security.md +20 -0
- package/docs/plugins.md +7 -9
- package/knip.json +1 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
- package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
- package/node_modules/@vellumai/twilio-client/package.json +18 -0
- package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
- package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
- package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
- package/openapi.yaml +565 -12
- package/package.json +6 -3
- package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
- package/src/__tests__/app-bundler.test.ts +170 -1
- package/src/__tests__/app-control-flow.test.ts +374 -0
- package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
- package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
- package/src/__tests__/app-executors.test.ts +30 -43
- package/src/__tests__/approval-routes-http.test.ts +23 -6
- package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
- package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
- package/src/__tests__/assistant-event-hub.test.ts +109 -2
- package/src/__tests__/assistant-event.test.ts +10 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
- package/src/__tests__/background-shell-host-bash.test.ts +14 -15
- package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
- package/src/__tests__/btw-routes.test.ts +13 -4
- package/src/__tests__/call-controller.test.ts +49 -1
- package/src/__tests__/call-domain.test.ts +0 -2
- package/src/__tests__/call-routes-http.test.ts +0 -2
- package/src/__tests__/channel-readiness-service.test.ts +59 -1
- package/src/__tests__/checker.test.ts +3 -4
- package/src/__tests__/config-loader-backfill.test.ts +90 -155
- package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-set-platform-guard.test.ts +48 -4
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-lifecycle.test.ts +36 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-slash-commands.test.ts +0 -4
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
- package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
- package/src/__tests__/credentials-cli.test.ts +5 -12
- package/src/__tests__/cu-unified-flow.test.ts +185 -23
- package/src/__tests__/daemon-credential-client.test.ts +101 -19
- package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
- package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +718 -1
- package/src/__tests__/helpers/call-route-handler.ts +7 -1
- package/src/__tests__/host-app-control-proxy.test.ts +602 -0
- package/src/__tests__/host-app-control-routes.test.ts +263 -0
- package/src/__tests__/host-bash-proxy.test.ts +246 -47
- package/src/__tests__/host-bash-routes.test.ts +294 -0
- package/src/__tests__/host-browser-proxy.test.ts +24 -22
- package/src/__tests__/host-browser-routes.test.ts +39 -13
- package/src/__tests__/host-cu-proxy.test.ts +41 -52
- package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
- package/src/__tests__/host-file-edit-tool.test.ts +47 -1
- package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
- package/src/__tests__/host-file-proxy.test.ts +37 -43
- package/src/__tests__/host-file-read-tool.test.ts +17 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
- package/src/__tests__/host-file-write-tool.test.ts +42 -1
- package/src/__tests__/host-proxy-base.test.ts +312 -0
- package/src/__tests__/host-shell-tool.test.ts +22 -4
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
- package/src/__tests__/host-transfer-proxy.test.ts +121 -22
- package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/identity-intro-cache.test.ts +29 -0
- package/src/__tests__/identity-routes.test.ts +103 -1
- package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
- package/src/__tests__/inline-command-runner.test.ts +0 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
- package/src/__tests__/integration-status.test.ts +85 -5
- package/src/__tests__/intent-routing.test.ts +0 -1
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
- package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/mcp-auth-routes.test.ts +197 -0
- package/src/__tests__/mcp-cli.test.ts +338 -2
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
- package/src/__tests__/migration-import-commit-http.test.ts +108 -2
- package/src/__tests__/mock-gateway-ipc.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +0 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
- package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/public-ingress-urls.test.ts +97 -0
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/retry-backoff.test.ts +87 -0
- package/src/__tests__/runtime-events-sse.test.ts +10 -6
- package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
- package/src/__tests__/schedule-retry.test.ts +715 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +43 -41
- package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
- package/src/__tests__/skill-load-inline-command.test.ts +0 -51
- package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
- package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
- package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
- package/src/__tests__/slack-channel-config.test.ts +9 -14
- package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/telegram-config.test.ts +0 -1
- package/src/__tests__/test-preload.ts +8 -0
- package/src/__tests__/tool-approval-handler.test.ts +3 -4
- package/src/__tests__/tool-audit-listener.test.ts +48 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +3 -16
- package/src/__tests__/twilio-routes.test.ts +3 -5
- package/src/__tests__/twilio-validation.test.ts +93 -0
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
- package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
- package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
- package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
- package/src/backup/__tests__/paths.test.ts +0 -22
- package/src/backup/__tests__/restore.test.ts +51 -151
- package/src/backup/paths.ts +2 -18
- package/src/backup/restore.ts +107 -231
- package/src/bundler/app-bundler.ts +51 -3
- package/src/calls/relay-server.ts +4 -44
- package/src/calls/twilio-config.ts +2 -17
- package/src/calls/twilio-rest.ts +33 -105
- package/src/calls/twilio-routes.ts +11 -12
- package/src/channels/types.ts +8 -7
- package/src/cli/commands/__tests__/backup.test.ts +6 -277
- package/src/cli/commands/__tests__/gateway.test.ts +288 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
- package/src/cli/commands/backup.ts +6 -331
- package/src/cli/commands/clients.ts +36 -37
- package/src/cli/commands/contacts.ts +73 -0
- package/src/cli/commands/conversations.ts +2 -5
- package/src/cli/commands/credentials.ts +15 -7
- package/src/cli/commands/domain.ts +66 -15
- package/src/cli/commands/gateway.ts +183 -0
- package/src/cli/commands/keys.ts +9 -6
- package/src/cli/commands/mcp.ts +116 -156
- package/src/cli/commands/memory-v2.ts +296 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
- package/src/cli/commands/platform/disconnect.ts +5 -4
- package/src/cli/commands/platform/index.ts +0 -18
- package/src/cli/lib/daemon-credential-client.ts +110 -28
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +67 -10
- package/src/config/bundled-skills/acp/SKILL.md +6 -0
- package/src/config/bundled-skills/acp/TOOLS.json +1 -22
- package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
- package/src/config/bundled-skills/app-control/SKILL.md +75 -0
- package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
- package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
- package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
- package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
- package/src/config/bundled-skills/document/TOOLS.json +0 -8
- package/src/config/bundled-skills/followups/TOOLS.json +0 -12
- package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
- package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
- package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
- package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
- package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
- package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
- package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
- package/src/config/bundled-skills/settings/SKILL.md +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +0 -12
- package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
- package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
- package/src/config/bundled-skills/subagent/SKILL.md +6 -2
- package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
- package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
- package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
- package/src/config/bundled-tool-registry.ts +21 -0
- package/src/config/env-registry.ts +0 -2
- package/src/config/env.ts +19 -12
- package/src/config/feature-flag-registry.json +21 -133
- package/src/config/loader.ts +73 -99
- package/src/config/sanitize-for-transfer.ts +2 -0
- package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
- package/src/config/schemas/calls.ts +0 -9
- package/src/config/schemas/heartbeat.ts +63 -0
- package/src/config/schemas/ingress.ts +10 -6
- package/src/config/schemas/llm.ts +5 -10
- package/src/config/schemas/memory-lifecycle.ts +77 -24
- package/src/config/schemas/memory-v2.ts +48 -4
- package/src/config/schemas/platform.ts +6 -0
- package/src/config/schemas/services.ts +1 -15
- package/src/config/schemas/skills.ts +0 -6
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/contacts/contact-store.ts +0 -30
- package/src/contacts/contacts-write.ts +0 -27
- package/src/context/window-manager.ts +1 -2
- package/src/credential-execution/feature-gates.ts +10 -10
- package/src/credential-execution/process-manager.ts +12 -41
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
- package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
- package/src/daemon/config-watcher.ts +4 -3
- package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
- package/src/daemon/conversation-agent-loop.ts +32 -28
- package/src/daemon/conversation-lifecycle.ts +8 -1
- package/src/daemon/conversation-process.ts +16 -11
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +125 -4
- package/src/daemon/conversation-tool-setup.ts +16 -55
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/doordash-steps.ts +1 -1
- package/src/daemon/handlers/shared.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +293 -0
- package/src/daemon/host-bash-proxy.ts +84 -74
- package/src/daemon/host-browser-proxy.ts +67 -82
- package/src/daemon/host-cu-proxy.ts +81 -86
- package/src/daemon/host-file-proxy.ts +93 -69
- package/src/daemon/host-proxy-base.ts +294 -0
- package/src/daemon/host-proxy-preactivation.ts +82 -0
- package/src/daemon/host-transfer-proxy.ts +247 -129
- package/src/daemon/lifecycle.ts +115 -117
- package/src/daemon/message-protocol.ts +3 -8
- package/src/daemon/message-types/contacts.ts +23 -1
- package/src/daemon/message-types/conversations.ts +11 -8
- package/src/daemon/message-types/host-app-control.ts +150 -0
- package/src/daemon/message-types/host-bash.ts +4 -0
- package/src/daemon/message-types/host-cu.ts +2 -0
- package/src/daemon/message-types/host-file.ts +4 -0
- package/src/daemon/message-types/host-transfer.ts +3 -0
- package/src/daemon/message-types/schedules.ts +8 -3
- package/src/daemon/message-types/skills.ts +2 -2
- package/src/daemon/process-message.ts +18 -1
- package/src/daemon/shutdown-handlers.ts +0 -3
- package/src/daemon/tool-setup-types.ts +51 -0
- package/src/daemon/tool-side-effects.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
- package/src/heartbeat/heartbeat-run-store.ts +236 -0
- package/src/heartbeat/heartbeat-service.ts +280 -49
- package/src/home/__tests__/post-connect-feed.test.ts +99 -0
- package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
- package/src/home/__tests__/suggested-prompts.test.ts +89 -0
- package/src/home/post-connect-feed.ts +68 -0
- package/src/home/relationship-state-writer.ts +17 -92
- package/src/home/suggested-prompts.ts +46 -10
- package/src/inbound/public-ingress-urls.ts +32 -34
- package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
- package/src/ipc/assistant-server.ts +14 -1
- package/src/ipc/cli-client.ts +32 -1
- package/src/live-voice/live-voice-metrics.ts +10 -10
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
- package/src/mcp/mcp-auth-orchestrator.ts +213 -0
- package/src/mcp/mcp-auth-state.ts +133 -0
- package/src/mcp/mcp-oauth-provider.ts +19 -0
- package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
- package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
- package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
- package/src/memory/anisotropy.test.ts +247 -0
- package/src/memory/anisotropy.ts +443 -0
- package/src/memory/auto-analysis-constants.ts +17 -0
- package/src/memory/auto-analysis-guard.ts +5 -15
- package/src/memory/canonical-guardian-store.ts +7 -7
- package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
- package/src/memory/context-search/agent-protocol.ts +6 -6
- package/src/memory/context-search/agent-runner.ts +32 -7
- package/src/memory/context-search/sources/memory-v2.ts +17 -5
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-key-store.ts +2 -15
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +9 -21
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
- package/src/memory/graph/conversation-graph-memory.ts +1 -24
- package/src/memory/graph/graph-search.ts +8 -0
- package/src/memory/graph/retriever.ts +28 -0
- package/src/memory/graph/tools.ts +1 -1
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
- package/src/memory/jobs/embed-concept-page.ts +28 -2
- package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
- package/src/memory/jobs-store.ts +66 -22
- package/src/memory/jobs-worker.ts +112 -63
- package/src/memory/memory-v2-activation-log-store.ts +1 -1
- package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
- package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-search.ts +7 -0
- package/src/memory/qdrant-client.ts +50 -20
- package/src/memory/schema/infrastructure.ts +15 -0
- package/src/memory/search/semantic.ts +7 -0
- package/src/memory/sparse-tokenize.ts +49 -0
- package/src/memory/v2/__tests__/activation.test.ts +77 -95
- package/src/memory/v2/__tests__/injection.test.ts +43 -21
- package/src/memory/v2/__tests__/sim.test.ts +166 -6
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/memory/v2/__tests__/static-context.test.ts +0 -1
- package/src/memory/v2/activation.ts +69 -88
- package/src/memory/v2/consolidation-job.ts +3 -5
- package/src/memory/v2/constants.ts +7 -0
- package/src/memory/v2/injection.ts +86 -53
- package/src/memory/v2/prompts/consolidation.ts +312 -91
- package/src/memory/v2/qdrant.ts +99 -1
- package/src/memory/v2/sim.ts +126 -16
- package/src/memory/v2/skill-qdrant.ts +12 -3
- package/src/memory/v2/skill-store.ts +16 -1
- package/src/memory/v2/sparse-bm25.ts +245 -0
- package/src/memory/v2/static-context.ts +6 -5
- package/src/messaging/providers/gmail/types.ts +0 -49
- package/src/messaging/providers/slack/adapter.ts +1 -31
- package/src/messaging/providers/slack/types.ts +0 -32
- package/src/notifications/README.md +10 -10
- package/src/notifications/broadcaster.ts +1 -1
- package/src/notifications/guardian-question-mode.ts +5 -5
- package/src/oauth/connect-orchestrator.ts +4 -0
- package/src/oauth/credential-token-resolver.ts +1 -3
- package/src/oauth/manual-token-connection.ts +0 -4
- package/src/outbound-proxy/index.ts +1 -37
- package/src/outbound-proxy/logging.ts +1 -1
- package/src/outbound-proxy/policy.ts +6 -5
- package/src/outbound-proxy/router.ts +2 -1
- package/src/permissions/approval-policy.test.ts +6 -275
- package/src/permissions/approval-policy.ts +0 -51
- package/src/permissions/checker.test.ts +0 -1
- package/src/permissions/checker.ts +3 -17
- package/src/permissions/gateway-threshold-reader.ts +2 -0
- package/src/permissions/prompter.ts +34 -1
- package/src/permissions/secret-prompter.ts +6 -2
- package/src/prompts/bootstrap-cleanup.ts +27 -0
- package/src/prompts/system-prompt.ts +3 -18
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/providers/speech-to-text/provider-catalog.ts +7 -8
- package/src/runtime/assistant-event-hub.ts +118 -96
- package/src/runtime/assistant-event.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
- package/src/runtime/auth/middleware.ts +0 -96
- package/src/runtime/auth/route-policy.ts +19 -0
- package/src/runtime/btw-sidechain.ts +2 -3
- package/src/runtime/channel-invite-transport.ts +2 -48
- package/src/runtime/channel-invite-transports/email.ts +1 -1
- package/src/runtime/channel-invite-transports/slack.ts +1 -1
- package/src/runtime/channel-invite-transports/telegram.ts +1 -1
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
- package/src/runtime/channel-invite-types.ts +54 -0
- package/src/runtime/channel-readiness-service.ts +32 -13
- package/src/runtime/http-server.ts +3 -329
- package/src/runtime/http-types.ts +0 -5
- package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
- package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
- package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
- package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
- package/src/runtime/migrations/migration-transport.ts +7 -7
- package/src/runtime/migrations/vbundle-builder.ts +327 -60
- package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
- package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
- package/src/runtime/migrations/vbundle-importer.ts +245 -68
- package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
- package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
- package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
- package/src/runtime/migrations/vbundle-validator.ts +114 -0
- package/src/runtime/pending-interactions.ts +35 -9
- package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
- package/src/runtime/routes/approval-interception-types.ts +13 -0
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
- package/src/runtime/routes/backup-routes.ts +15 -38
- package/src/runtime/routes/btw-routes.ts +14 -37
- package/src/runtime/routes/client-routes.ts +1 -0
- package/src/runtime/routes/contact-prompt-routes.ts +183 -0
- package/src/runtime/routes/conversation-query-routes.ts +36 -1
- package/src/runtime/routes/conversation-routes.ts +30 -13
- package/src/runtime/routes/document-pdf-renderer.ts +165 -0
- package/src/runtime/routes/documents-routes.ts +30 -0
- package/src/runtime/routes/errors.ts +19 -4
- package/src/runtime/routes/events-routes.ts +12 -6
- package/src/runtime/routes/gateway-log-routes.ts +79 -0
- package/src/runtime/routes/guardian-approval-interception.ts +2 -8
- package/src/runtime/routes/heartbeat-routes.ts +103 -38
- package/src/runtime/routes/host-app-control-routes.ts +134 -0
- package/src/runtime/routes/host-bash-routes.ts +36 -6
- package/src/runtime/routes/host-browser-routes.ts +108 -13
- package/src/runtime/routes/host-cu-routes.ts +44 -14
- package/src/runtime/routes/host-file-routes.ts +33 -10
- package/src/runtime/routes/host-transfer-routes.ts +64 -24
- package/src/runtime/routes/http-adapter.ts +1 -0
- package/src/runtime/routes/identity-intro-cache.ts +30 -0
- package/src/runtime/routes/identity-routes.ts +15 -43
- package/src/runtime/routes/inbound-message-handler.ts +1 -9
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/mcp-auth-routes.ts +132 -0
- package/src/runtime/routes/memory-item-routes.ts +10 -12
- package/src/runtime/routes/memory-v2-routes.ts +441 -1
- package/src/runtime/routes/migration-routes.ts +96 -0
- package/src/runtime/routes/schedule-routes.ts +7 -0
- package/src/runtime/verification-templates.ts +4 -7
- package/src/schedule/integration-status.ts +66 -2
- package/src/schedule/recurrence-engine.ts +4 -1
- package/src/schedule/retry-backoff.ts +18 -0
- package/src/schedule/retry-policy.ts +82 -0
- package/src/schedule/schedule-recovery.ts +64 -0
- package/src/schedule/schedule-store.ts +106 -2
- package/src/schedule/scheduler-types.ts +25 -0
- package/src/schedule/scheduler.ts +63 -38
- package/src/security/oauth-callback-registry.ts +8 -0
- package/src/sequence/analytics.ts +5 -5
- package/src/sequence/engine.ts +1 -1
- package/src/skills/catalog-files.ts +2 -8
- package/src/skills/include-graph.ts +5 -5
- package/src/skills/remote-skill-policy.ts +5 -5
- package/src/skills/skill-file-provider.ts +1 -1
- package/src/skills/skill-file-types.ts +13 -0
- package/src/skills/skillssh-audit-types.ts +28 -0
- package/src/skills/skillssh-registry.ts +8 -21
- package/src/telemetry/types.ts +2 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
- package/src/tools/apps/executors.ts +56 -69
- package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
- package/src/tools/browser/cdp-client/factory.ts +23 -24
- package/src/tools/browser/cdp-client/index.ts +1 -14
- package/src/tools/computer-use/definitions.ts +42 -20
- package/src/tools/executor.ts +2 -0
- package/src/tools/host-filesystem/edit.ts +26 -0
- package/src/tools/host-filesystem/read.ts +26 -0
- package/src/tools/host-filesystem/transfer.ts +31 -1
- package/src/tools/host-filesystem/write.ts +26 -0
- package/src/tools/host-terminal/host-shell.ts +58 -0
- package/src/tools/schedule/create.ts +6 -0
- package/src/tools/schedule/list.ts +2 -0
- package/src/tools/schedule/update.ts +10 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
- package/src/tools/shared/filesystem/path-policy.ts +25 -1
- package/src/tools/skills/load.ts +0 -32
- package/src/tools/tool-approval-handler.ts +1 -5
- package/src/tools/types.ts +4 -0
- package/src/usage/pricing.ts +1 -1
- package/src/workspace/hatched-date.ts +86 -0
- package/src/workspace/migrations/003-seed-device-id.ts +1 -1
- package/src/workspace/migrations/006-services-config.ts +8 -5
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
- package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
- package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
- package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
- package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
- package/src/workspace/migrations/AGENTS.md +1 -1
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
- package/src/workspace/migrations/utils.ts +21 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
- package/src/__tests__/twilio-rest.test.ts +0 -34
- package/src/backup/__tests__/backup-key.test.ts +0 -152
- package/src/backup/__tests__/backup-worker.test.ts +0 -782
- package/src/backup/__tests__/offsite-writer.test.ts +0 -641
- package/src/backup/__tests__/stream-crypt.test.ts +0 -228
- package/src/backup/backup-key.ts +0 -137
- package/src/backup/backup-worker.ts +0 -472
- package/src/backup/offsite-writer.ts +0 -222
- package/src/backup/stream-crypt.ts +0 -263
- package/src/daemon/message-types/pairing.ts +0 -58
- package/src/outbound-proxy/config.ts +0 -20
- package/src/outbound-proxy/health.ts +0 -18
- package/src/outbound-proxy/types.ts +0 -150
- package/src/runtime/capability-tokens.ts +0 -190
- package/src/signals/mcp-reload.ts +0 -18
|
@@ -1,641 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `writeOffsiteSnapshotToOne`, `writeOffsiteSnapshotToAll`, and
|
|
3
|
-
* `pruneOffsiteSnapshotsInAll`. All tests run against a temp directory so
|
|
4
|
-
* the real `~/.vellum/` tree is never touched.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { randomBytes } from "node:crypto";
|
|
8
|
-
import {
|
|
9
|
-
chmodSync,
|
|
10
|
-
existsSync,
|
|
11
|
-
mkdirSync,
|
|
12
|
-
mkdtempSync,
|
|
13
|
-
readFileSync,
|
|
14
|
-
rmSync,
|
|
15
|
-
writeFileSync,
|
|
16
|
-
} from "node:fs";
|
|
17
|
-
import { tmpdir } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
20
|
-
|
|
21
|
-
import type { BackupDestination } from "../../config/schema.js";
|
|
22
|
-
import { listSnapshotsInDir } from "../list-snapshots.js";
|
|
23
|
-
import {
|
|
24
|
-
pruneOffsiteSnapshotsInAll,
|
|
25
|
-
writeOffsiteSnapshotToAll,
|
|
26
|
-
writeOffsiteSnapshotToOne,
|
|
27
|
-
} from "../offsite-writer.js";
|
|
28
|
-
import { decryptFile } from "../stream-crypt.js";
|
|
29
|
-
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Fixtures
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
let ROOT: string;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
ROOT = mkdtempSync(join(tmpdir(), "vellum-offsite-writer-"));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
try {
|
|
42
|
-
rmSync(ROOT, { recursive: true, force: true });
|
|
43
|
-
} catch {
|
|
44
|
-
// best-effort
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
/** Write a fake local snapshot file and return its absolute path. */
|
|
49
|
-
function seedLocalSnapshot(payload: Buffer | string): string {
|
|
50
|
-
const path = join(ROOT, "local.vbundle");
|
|
51
|
-
writeFileSync(path, payload);
|
|
52
|
-
return path;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Absolute path helper for tests that need to construct destinations. */
|
|
56
|
-
function subPath(...segments: string[]): string {
|
|
57
|
-
return join(ROOT, ...segments);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// writeOffsiteSnapshotToOne
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
describe("writeOffsiteSnapshotToOne", () => {
|
|
65
|
-
const NOW = new Date("2026-04-11T15:30:45Z");
|
|
66
|
-
|
|
67
|
-
test("returns skipped=parent-missing when the parent directory does not exist", async () => {
|
|
68
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
69
|
-
// Parent "does/not/exist" is not created.
|
|
70
|
-
const destination: BackupDestination = {
|
|
71
|
-
path: subPath("does", "not", "exist", "backups"),
|
|
72
|
-
encrypt: true,
|
|
73
|
-
};
|
|
74
|
-
const key = randomBytes(32);
|
|
75
|
-
|
|
76
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
77
|
-
localSnapshotPath,
|
|
78
|
-
destination,
|
|
79
|
-
key,
|
|
80
|
-
NOW,
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
expect(result.skipped).toBe("parent-missing");
|
|
84
|
-
expect(result.entry).toBeNull();
|
|
85
|
-
expect(result.error).toBeUndefined();
|
|
86
|
-
expect(result.destination).toEqual(destination);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("encrypt=true writes a .vbundle.enc that round-trips through decryptFile", async () => {
|
|
90
|
-
const plaintext = randomBytes(2048);
|
|
91
|
-
const localSnapshotPath = seedLocalSnapshot(plaintext);
|
|
92
|
-
|
|
93
|
-
// Parent exists; destination directory itself does not yet.
|
|
94
|
-
const parent = subPath("icloud");
|
|
95
|
-
mkdirSync(parent, { recursive: true });
|
|
96
|
-
const destination: BackupDestination = {
|
|
97
|
-
path: join(parent, "backups"),
|
|
98
|
-
encrypt: true,
|
|
99
|
-
};
|
|
100
|
-
const key = randomBytes(32);
|
|
101
|
-
|
|
102
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
103
|
-
localSnapshotPath,
|
|
104
|
-
destination,
|
|
105
|
-
key,
|
|
106
|
-
NOW,
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
expect(result.error).toBeUndefined();
|
|
110
|
-
expect(result.skipped).toBeUndefined();
|
|
111
|
-
expect(result.entry).not.toBeNull();
|
|
112
|
-
expect(result.entry!.filename).toBe("backup-20260411-153045-000.vbundle.enc");
|
|
113
|
-
expect(result.entry!.encrypted).toBe(true);
|
|
114
|
-
expect(result.entry!.createdAt).toBe(NOW);
|
|
115
|
-
expect(result.entry!.path).toBe(
|
|
116
|
-
join(destination.path, "backup-20260411-153045-000.vbundle.enc"),
|
|
117
|
-
);
|
|
118
|
-
expect(existsSync(result.entry!.path)).toBe(true);
|
|
119
|
-
|
|
120
|
-
// Round-trip through decryptFile to confirm the ciphertext actually
|
|
121
|
-
// decrypts to the original bytes.
|
|
122
|
-
const roundTripPath = subPath("roundtrip.bin");
|
|
123
|
-
await decryptFile(result.entry!.path, roundTripPath, key);
|
|
124
|
-
const decoded = readFileSync(roundTripPath);
|
|
125
|
-
expect(decoded.equals(plaintext)).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("encrypt=false writes a plaintext .vbundle that byte-equals the source", async () => {
|
|
129
|
-
const plaintext = randomBytes(4096);
|
|
130
|
-
const localSnapshotPath = seedLocalSnapshot(plaintext);
|
|
131
|
-
|
|
132
|
-
const parent = subPath("external-ssd");
|
|
133
|
-
mkdirSync(parent, { recursive: true });
|
|
134
|
-
const destination: BackupDestination = {
|
|
135
|
-
path: join(parent, "vellum-backups"),
|
|
136
|
-
encrypt: false,
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
140
|
-
localSnapshotPath,
|
|
141
|
-
destination,
|
|
142
|
-
null, // no key needed for plaintext
|
|
143
|
-
NOW,
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
expect(result.error).toBeUndefined();
|
|
147
|
-
expect(result.skipped).toBeUndefined();
|
|
148
|
-
expect(result.entry).not.toBeNull();
|
|
149
|
-
expect(result.entry!.filename).toBe("backup-20260411-153045-000.vbundle");
|
|
150
|
-
expect(result.entry!.encrypted).toBe(false);
|
|
151
|
-
expect(result.entry!.sizeBytes).toBe(plaintext.length);
|
|
152
|
-
|
|
153
|
-
// Byte-equal check against the source file.
|
|
154
|
-
const written = readFileSync(result.entry!.path);
|
|
155
|
-
expect(written.equals(plaintext)).toBe(true);
|
|
156
|
-
|
|
157
|
-
// No stray .tmp sibling left behind.
|
|
158
|
-
expect(existsSync(`${result.entry!.path}.tmp`)).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("bootstraps intermediate directories under the iCloud Drive safe ancestor on first run", async () => {
|
|
162
|
-
// Redirect HOME so `getICloudDriveRoot()` resolves inside our temp ROOT.
|
|
163
|
-
// This is the key regression: on first install only the iCloud Drive root
|
|
164
|
-
// exists; `VellumAssistant/backups/` needs to be created by the writer.
|
|
165
|
-
const ORIGINAL_HOME = process.env.HOME;
|
|
166
|
-
process.env.HOME = ROOT;
|
|
167
|
-
try {
|
|
168
|
-
const iCloudRoot = join(
|
|
169
|
-
ROOT,
|
|
170
|
-
"Library",
|
|
171
|
-
"Mobile Documents",
|
|
172
|
-
"com~apple~CloudDocs",
|
|
173
|
-
);
|
|
174
|
-
mkdirSync(iCloudRoot, { recursive: true });
|
|
175
|
-
// Destination is two levels below the safe ancestor — neither of the
|
|
176
|
-
// intermediate dirs exists yet.
|
|
177
|
-
const destinationPath = join(iCloudRoot, "VellumAssistant", "backups");
|
|
178
|
-
expect(existsSync(destinationPath)).toBe(false);
|
|
179
|
-
|
|
180
|
-
const plaintext = randomBytes(512);
|
|
181
|
-
const localSnapshotPath = seedLocalSnapshot(plaintext);
|
|
182
|
-
const destination: BackupDestination = {
|
|
183
|
-
path: destinationPath,
|
|
184
|
-
encrypt: true,
|
|
185
|
-
};
|
|
186
|
-
const key = randomBytes(32);
|
|
187
|
-
|
|
188
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
189
|
-
localSnapshotPath,
|
|
190
|
-
destination,
|
|
191
|
-
key,
|
|
192
|
-
NOW,
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
expect(result.skipped).toBeUndefined();
|
|
196
|
-
expect(result.error).toBeUndefined();
|
|
197
|
-
expect(result.entry).not.toBeNull();
|
|
198
|
-
expect(existsSync(destinationPath)).toBe(true);
|
|
199
|
-
expect(existsSync(result.entry!.path)).toBe(true);
|
|
200
|
-
} finally {
|
|
201
|
-
if (ORIGINAL_HOME === undefined) {
|
|
202
|
-
delete process.env.HOME;
|
|
203
|
-
} else {
|
|
204
|
-
process.env.HOME = ORIGINAL_HOME;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("iCloud default path is still skipped when the iCloud Drive root is missing", async () => {
|
|
210
|
-
// Same shape as the previous test, but we do NOT create the iCloud Drive
|
|
211
|
-
// root — simulates iCloud Drive disabled. The destination must stay
|
|
212
|
-
// skipped rather than bootstrapping the tree under an arbitrary location.
|
|
213
|
-
const ORIGINAL_HOME = process.env.HOME;
|
|
214
|
-
process.env.HOME = ROOT;
|
|
215
|
-
try {
|
|
216
|
-
const iCloudRoot = join(
|
|
217
|
-
ROOT,
|
|
218
|
-
"Library",
|
|
219
|
-
"Mobile Documents",
|
|
220
|
-
"com~apple~CloudDocs",
|
|
221
|
-
);
|
|
222
|
-
expect(existsSync(iCloudRoot)).toBe(false);
|
|
223
|
-
|
|
224
|
-
const destinationPath = join(iCloudRoot, "VellumAssistant", "backups");
|
|
225
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
226
|
-
const destination: BackupDestination = {
|
|
227
|
-
path: destinationPath,
|
|
228
|
-
encrypt: true,
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
232
|
-
localSnapshotPath,
|
|
233
|
-
destination,
|
|
234
|
-
randomBytes(32),
|
|
235
|
-
NOW,
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
expect(result.skipped).toBe("parent-missing");
|
|
239
|
-
expect(result.entry).toBeNull();
|
|
240
|
-
expect(result.error).toBeUndefined();
|
|
241
|
-
expect(existsSync(destinationPath)).toBe(false);
|
|
242
|
-
// Critical: no intermediate directories were materialized.
|
|
243
|
-
expect(existsSync(join(iCloudRoot, "VellumAssistant"))).toBe(false);
|
|
244
|
-
} finally {
|
|
245
|
-
if (ORIGINAL_HOME === undefined) {
|
|
246
|
-
delete process.env.HOME;
|
|
247
|
-
} else {
|
|
248
|
-
process.env.HOME = ORIGINAL_HOME;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test("permissions error on mkdir is surfaced as result.error rather than thrown", async () => {
|
|
254
|
-
// Root directory exists but is not writable, so `mkdir -p destination` fails
|
|
255
|
-
// with EACCES. The writer must catch it and surface via `error` to keep
|
|
256
|
-
// a broken destination from poisoning the others.
|
|
257
|
-
const readOnlyParent = subPath("read-only");
|
|
258
|
-
mkdirSync(readOnlyParent, { recursive: true });
|
|
259
|
-
chmodSync(readOnlyParent, 0o500); // r-x only: stat works, mkdir fails
|
|
260
|
-
try {
|
|
261
|
-
const destination: BackupDestination = {
|
|
262
|
-
path: join(readOnlyParent, "backups"),
|
|
263
|
-
encrypt: false,
|
|
264
|
-
};
|
|
265
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
266
|
-
|
|
267
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
268
|
-
localSnapshotPath,
|
|
269
|
-
destination,
|
|
270
|
-
null,
|
|
271
|
-
NOW,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
expect(result.entry).toBeNull();
|
|
275
|
-
expect(result.skipped).toBeUndefined();
|
|
276
|
-
expect(result.error).toBeDefined();
|
|
277
|
-
expect(result.error).toMatch(/EACCES|permission/i);
|
|
278
|
-
} finally {
|
|
279
|
-
// Restore writable perms so the afterEach rmSync can clean up.
|
|
280
|
-
chmodSync(readOnlyParent, 0o700);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test("encrypt=true with key=null returns an error (caught internally, not thrown)", async () => {
|
|
285
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
286
|
-
|
|
287
|
-
const parent = subPath("icloud");
|
|
288
|
-
mkdirSync(parent, { recursive: true });
|
|
289
|
-
const destination: BackupDestination = {
|
|
290
|
-
path: join(parent, "backups"),
|
|
291
|
-
encrypt: true,
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
295
|
-
localSnapshotPath,
|
|
296
|
-
destination,
|
|
297
|
-
null, // missing key despite encrypt=true
|
|
298
|
-
NOW,
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
expect(result.entry).toBeNull();
|
|
302
|
-
expect(result.skipped).toBeUndefined();
|
|
303
|
-
expect(result.error).toBeDefined();
|
|
304
|
-
expect(result.error).toContain("encryption");
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
// writeOffsiteSnapshotToAll
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
|
|
312
|
-
describe("writeOffsiteSnapshotToAll", () => {
|
|
313
|
-
const NOW = new Date("2026-04-11T15:30:45Z");
|
|
314
|
-
|
|
315
|
-
test("empty destinations returns [] immediately", async () => {
|
|
316
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
317
|
-
const result = await writeOffsiteSnapshotToAll(
|
|
318
|
-
localSnapshotPath,
|
|
319
|
-
[],
|
|
320
|
-
null,
|
|
321
|
-
NOW,
|
|
322
|
-
);
|
|
323
|
-
expect(result).toEqual([]);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test("multi-destination: encrypted + plaintext writes succeed with correct extensions", async () => {
|
|
327
|
-
const plaintext = randomBytes(1024);
|
|
328
|
-
const localSnapshotPath = seedLocalSnapshot(plaintext);
|
|
329
|
-
|
|
330
|
-
const parentA = subPath("icloud");
|
|
331
|
-
const parentB = subPath("external-ssd");
|
|
332
|
-
mkdirSync(parentA, { recursive: true });
|
|
333
|
-
mkdirSync(parentB, { recursive: true });
|
|
334
|
-
|
|
335
|
-
const destinations: BackupDestination[] = [
|
|
336
|
-
{ path: join(parentA, "backups"), encrypt: true },
|
|
337
|
-
{ path: join(parentB, "vellum-backups"), encrypt: false },
|
|
338
|
-
];
|
|
339
|
-
const key = randomBytes(32);
|
|
340
|
-
|
|
341
|
-
const results = await writeOffsiteSnapshotToAll(
|
|
342
|
-
localSnapshotPath,
|
|
343
|
-
destinations,
|
|
344
|
-
key,
|
|
345
|
-
NOW,
|
|
346
|
-
);
|
|
347
|
-
|
|
348
|
-
expect(results).toHaveLength(2);
|
|
349
|
-
|
|
350
|
-
// Encrypted destination
|
|
351
|
-
expect(results[0].destination).toEqual(destinations[0]);
|
|
352
|
-
expect(results[0].entry).not.toBeNull();
|
|
353
|
-
expect(results[0].entry!.filename).toBe("backup-20260411-153045-000.vbundle.enc");
|
|
354
|
-
expect(results[0].entry!.encrypted).toBe(true);
|
|
355
|
-
expect(results[0].skipped).toBeUndefined();
|
|
356
|
-
expect(results[0].error).toBeUndefined();
|
|
357
|
-
|
|
358
|
-
// Plaintext destination
|
|
359
|
-
expect(results[1].destination).toEqual(destinations[1]);
|
|
360
|
-
expect(results[1].entry).not.toBeNull();
|
|
361
|
-
expect(results[1].entry!.filename).toBe("backup-20260411-153045-000.vbundle");
|
|
362
|
-
expect(results[1].entry!.encrypted).toBe(false);
|
|
363
|
-
expect(results[1].skipped).toBeUndefined();
|
|
364
|
-
expect(results[1].error).toBeUndefined();
|
|
365
|
-
|
|
366
|
-
// Plaintext copy is byte-equal to source.
|
|
367
|
-
expect(readFileSync(results[1].entry!.path).equals(plaintext)).toBe(true);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("one destination with a missing parent is skipped while the other succeeds", async () => {
|
|
371
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
372
|
-
|
|
373
|
-
const parentA = subPath("icloud");
|
|
374
|
-
mkdirSync(parentA, { recursive: true });
|
|
375
|
-
|
|
376
|
-
const destinations: BackupDestination[] = [
|
|
377
|
-
{ path: join(parentA, "backups"), encrypt: false }, // OK
|
|
378
|
-
{ path: subPath("missing", "mount", "backups"), encrypt: false }, // parent missing
|
|
379
|
-
];
|
|
380
|
-
|
|
381
|
-
const results = await writeOffsiteSnapshotToAll(
|
|
382
|
-
localSnapshotPath,
|
|
383
|
-
destinations,
|
|
384
|
-
null,
|
|
385
|
-
NOW,
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
expect(results).toHaveLength(2);
|
|
389
|
-
|
|
390
|
-
// A succeeded.
|
|
391
|
-
expect(results[0].entry).not.toBeNull();
|
|
392
|
-
expect(results[0].skipped).toBeUndefined();
|
|
393
|
-
expect(results[0].error).toBeUndefined();
|
|
394
|
-
expect(existsSync(results[0].entry!.path)).toBe(true);
|
|
395
|
-
|
|
396
|
-
// B skipped (parent missing).
|
|
397
|
-
expect(results[1].entry).toBeNull();
|
|
398
|
-
expect(results[1].skipped).toBe("parent-missing");
|
|
399
|
-
expect(results[1].error).toBeUndefined();
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
test("one destination throwing (dest.path is a file) reports error while the other still succeeds", async () => {
|
|
403
|
-
const localSnapshotPath = seedLocalSnapshot("payload");
|
|
404
|
-
|
|
405
|
-
const parentA = subPath("icloud");
|
|
406
|
-
const parentB = subPath("broken");
|
|
407
|
-
mkdirSync(parentA, { recursive: true });
|
|
408
|
-
mkdirSync(parentB, { recursive: true });
|
|
409
|
-
|
|
410
|
-
// Force B's `destination.path` to be a file, which makes the mkdir +
|
|
411
|
-
// write paths throw (a file exists where the destination directory
|
|
412
|
-
// should be).
|
|
413
|
-
const brokenDestPath = join(parentB, "not-a-dir");
|
|
414
|
-
writeFileSync(brokenDestPath, "I am a file, not a directory");
|
|
415
|
-
|
|
416
|
-
const destinations: BackupDestination[] = [
|
|
417
|
-
{ path: join(parentA, "backups"), encrypt: false },
|
|
418
|
-
{ path: brokenDestPath, encrypt: false },
|
|
419
|
-
];
|
|
420
|
-
|
|
421
|
-
const results = await writeOffsiteSnapshotToAll(
|
|
422
|
-
localSnapshotPath,
|
|
423
|
-
destinations,
|
|
424
|
-
null,
|
|
425
|
-
NOW,
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
expect(results).toHaveLength(2);
|
|
429
|
-
|
|
430
|
-
// A still succeeded.
|
|
431
|
-
expect(results[0].entry).not.toBeNull();
|
|
432
|
-
expect(results[0].error).toBeUndefined();
|
|
433
|
-
|
|
434
|
-
// B failed with an error (not a skip — its parent exists).
|
|
435
|
-
expect(results[1].entry).toBeNull();
|
|
436
|
-
expect(results[1].skipped).toBeUndefined();
|
|
437
|
-
expect(results[1].error).toBeDefined();
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
test("writes are sequential: after each iteration the corresponding destination has exactly one file", async () => {
|
|
441
|
-
// We verify order by having each destination's mtime reflect the order
|
|
442
|
-
// of writes. A simpler check: by the time writeOffsiteSnapshotToAll
|
|
443
|
-
// returns, both files exist; and each intermediate result is fully
|
|
444
|
-
// formed (not a promise-like stub). Sequential ordering is also enforced
|
|
445
|
-
// at compile time by the for...of + await structure.
|
|
446
|
-
const plaintext = randomBytes(256);
|
|
447
|
-
const localSnapshotPath = seedLocalSnapshot(plaintext);
|
|
448
|
-
|
|
449
|
-
const parents = [subPath("p0"), subPath("p1"), subPath("p2")];
|
|
450
|
-
for (const p of parents) mkdirSync(p, { recursive: true });
|
|
451
|
-
const destinations: BackupDestination[] = parents.map((p) => ({
|
|
452
|
-
path: join(p, "dst"),
|
|
453
|
-
encrypt: false,
|
|
454
|
-
}));
|
|
455
|
-
|
|
456
|
-
const results = await writeOffsiteSnapshotToAll(
|
|
457
|
-
localSnapshotPath,
|
|
458
|
-
destinations,
|
|
459
|
-
null,
|
|
460
|
-
new Date("2026-04-11T15:30:45Z"),
|
|
461
|
-
);
|
|
462
|
-
|
|
463
|
-
expect(results).toHaveLength(3);
|
|
464
|
-
// Ordering by destination.path matches the input, which is only
|
|
465
|
-
// guaranteed if we loop sequentially (Promise.all would also preserve
|
|
466
|
-
// order, but then interleaved destination failures could interact).
|
|
467
|
-
for (let i = 0; i < destinations.length; i++) {
|
|
468
|
-
expect(results[i].destination.path).toBe(destinations[i].path);
|
|
469
|
-
expect(results[i].entry).not.toBeNull();
|
|
470
|
-
const listed = await listSnapshotsInDir(destinations[i].path);
|
|
471
|
-
expect(listed).toHaveLength(1);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
test("only plaintext destinations with key=null succeeds (no key required)", async () => {
|
|
476
|
-
const plaintext = randomBytes(512);
|
|
477
|
-
const localSnapshotPath = seedLocalSnapshot(plaintext);
|
|
478
|
-
|
|
479
|
-
const parentA = subPath("a");
|
|
480
|
-
const parentB = subPath("b");
|
|
481
|
-
mkdirSync(parentA, { recursive: true });
|
|
482
|
-
mkdirSync(parentB, { recursive: true });
|
|
483
|
-
|
|
484
|
-
const destinations: BackupDestination[] = [
|
|
485
|
-
{ path: join(parentA, "dst"), encrypt: false },
|
|
486
|
-
{ path: join(parentB, "dst"), encrypt: false },
|
|
487
|
-
];
|
|
488
|
-
|
|
489
|
-
const results = await writeOffsiteSnapshotToAll(
|
|
490
|
-
localSnapshotPath,
|
|
491
|
-
destinations,
|
|
492
|
-
null,
|
|
493
|
-
new Date("2026-04-11T15:30:45Z"),
|
|
494
|
-
);
|
|
495
|
-
|
|
496
|
-
expect(results).toHaveLength(2);
|
|
497
|
-
for (const r of results) {
|
|
498
|
-
expect(r.entry).not.toBeNull();
|
|
499
|
-
expect(r.error).toBeUndefined();
|
|
500
|
-
expect(r.skipped).toBeUndefined();
|
|
501
|
-
expect(readFileSync(r.entry!.path).equals(plaintext)).toBe(true);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
// ---------------------------------------------------------------------------
|
|
507
|
-
// pruneOffsiteSnapshotsInAll
|
|
508
|
-
// ---------------------------------------------------------------------------
|
|
509
|
-
|
|
510
|
-
describe("pruneOffsiteSnapshotsInAll", () => {
|
|
511
|
-
/**
|
|
512
|
-
* Seed `count` timestamped backup files into `dir`. Ascending hours mean
|
|
513
|
-
* file index N is the Nth-oldest, so the last seeded file is the newest.
|
|
514
|
-
* Alternates `.vbundle` / `.vbundle.enc` when `mixed` is true.
|
|
515
|
-
*/
|
|
516
|
-
function seed(
|
|
517
|
-
dir: string,
|
|
518
|
-
count: number,
|
|
519
|
-
mixed = false,
|
|
520
|
-
): string[] {
|
|
521
|
-
mkdirSync(dir, { recursive: true });
|
|
522
|
-
const names: string[] = [];
|
|
523
|
-
for (let i = 0; i < count; i++) {
|
|
524
|
-
const hour = i.toString().padStart(2, "0");
|
|
525
|
-
const ext = mixed && i % 2 === 0 ? ".vbundle.enc" : ".vbundle";
|
|
526
|
-
const name = `backup-20260411-${hour}0000${ext}`;
|
|
527
|
-
writeFileSync(join(dir, name), `payload ${i}`);
|
|
528
|
-
names.push(name);
|
|
529
|
-
}
|
|
530
|
-
return names;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
test("two destinations with 10 files each, retention 7: each keeps its own 7 newest", async () => {
|
|
534
|
-
const parentA = subPath("a");
|
|
535
|
-
const parentB = subPath("b");
|
|
536
|
-
mkdirSync(parentA, { recursive: true });
|
|
537
|
-
mkdirSync(parentB, { recursive: true });
|
|
538
|
-
|
|
539
|
-
const dirA = join(parentA, "dst");
|
|
540
|
-
const dirB = join(parentB, "dst");
|
|
541
|
-
const seededA = seed(dirA, 10);
|
|
542
|
-
const seededB = seed(dirB, 10);
|
|
543
|
-
|
|
544
|
-
const destinations: BackupDestination[] = [
|
|
545
|
-
{ path: dirA, encrypt: false },
|
|
546
|
-
{ path: dirB, encrypt: false },
|
|
547
|
-
];
|
|
548
|
-
|
|
549
|
-
const results = await pruneOffsiteSnapshotsInAll(destinations, 7);
|
|
550
|
-
|
|
551
|
-
expect(results).toHaveLength(2);
|
|
552
|
-
|
|
553
|
-
// A: 7 newest kept, 3 oldest deleted.
|
|
554
|
-
expect(results[0].destination).toEqual(destinations[0]);
|
|
555
|
-
expect(results[0].kept).toHaveLength(7);
|
|
556
|
-
expect(results[0].deleted).toHaveLength(3);
|
|
557
|
-
expect(results[0].skipped).toBeUndefined();
|
|
558
|
-
// Filesystem matches.
|
|
559
|
-
const remainingA = await listSnapshotsInDir(dirA);
|
|
560
|
-
expect(remainingA).toHaveLength(7);
|
|
561
|
-
// Oldest three (indexes 0..2) are gone.
|
|
562
|
-
for (const name of seededA.slice(0, 3)) {
|
|
563
|
-
expect(existsSync(join(dirA, name))).toBe(false);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// B: same, independently of A.
|
|
567
|
-
expect(results[1].destination).toEqual(destinations[1]);
|
|
568
|
-
expect(results[1].kept).toHaveLength(7);
|
|
569
|
-
expect(results[1].deleted).toHaveLength(3);
|
|
570
|
-
expect(results[1].skipped).toBeUndefined();
|
|
571
|
-
const remainingB = await listSnapshotsInDir(dirB);
|
|
572
|
-
expect(remainingB).toHaveLength(7);
|
|
573
|
-
for (const name of seededB.slice(0, 3)) {
|
|
574
|
-
expect(existsSync(join(dirB, name))).toBe(false);
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
test("a destination with a missing parent returns skipped=true for that entry only", async () => {
|
|
579
|
-
const parentA = subPath("a");
|
|
580
|
-
mkdirSync(parentA, { recursive: true });
|
|
581
|
-
const dirA = join(parentA, "dst");
|
|
582
|
-
seed(dirA, 5);
|
|
583
|
-
|
|
584
|
-
const destinations: BackupDestination[] = [
|
|
585
|
-
{ path: dirA, encrypt: false },
|
|
586
|
-
// Parent does not exist.
|
|
587
|
-
{ path: subPath("missing", "mount", "dst"), encrypt: false },
|
|
588
|
-
];
|
|
589
|
-
|
|
590
|
-
const results = await pruneOffsiteSnapshotsInAll(destinations, 3);
|
|
591
|
-
|
|
592
|
-
expect(results).toHaveLength(2);
|
|
593
|
-
|
|
594
|
-
// A pruned normally.
|
|
595
|
-
expect(results[0].skipped).toBeUndefined();
|
|
596
|
-
expect(results[0].kept).toHaveLength(3);
|
|
597
|
-
expect(results[0].deleted).toHaveLength(2);
|
|
598
|
-
|
|
599
|
-
// B is skipped — parent missing.
|
|
600
|
-
expect(results[1].skipped).toBe(true);
|
|
601
|
-
expect(results[1].kept).toEqual([]);
|
|
602
|
-
expect(results[1].deleted).toEqual([]);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test("mixed .vbundle and .vbundle.enc files in one directory are pruned as a single pool ordered by timestamp", async () => {
|
|
606
|
-
const parent = subPath("a");
|
|
607
|
-
mkdirSync(parent, { recursive: true });
|
|
608
|
-
const dir = join(parent, "dst");
|
|
609
|
-
// 10 files alternating .vbundle.enc (even indexes) and .vbundle (odd).
|
|
610
|
-
const seeded = seed(dir, 10, /* mixed */ true);
|
|
611
|
-
|
|
612
|
-
const destinations: BackupDestination[] = [
|
|
613
|
-
{ path: dir, encrypt: true }, // encrypt flag is unrelated to prune logic
|
|
614
|
-
];
|
|
615
|
-
|
|
616
|
-
const results = await pruneOffsiteSnapshotsInAll(destinations, 4);
|
|
617
|
-
expect(results).toHaveLength(1);
|
|
618
|
-
|
|
619
|
-
const { kept, deleted } = results[0];
|
|
620
|
-
// 4 newest kept, 6 oldest deleted — pool treats both extensions the same.
|
|
621
|
-
expect(kept).toHaveLength(4);
|
|
622
|
-
expect(deleted).toHaveLength(6);
|
|
623
|
-
|
|
624
|
-
// Newest four are indexes 6..9 (ascending hours, so 09,08,07,06 newest-first).
|
|
625
|
-
const expectedKeptNames = seeded.slice(-4).reverse();
|
|
626
|
-
expect(kept.map((e) => e.filename)).toEqual(expectedKeptNames);
|
|
627
|
-
|
|
628
|
-
// Deleted six are indexes 0..5, returned in the original newest-first
|
|
629
|
-
// sort from listSnapshotsInDir: 05, 04, 03, 02, 01, 00.
|
|
630
|
-
const expectedDeletedNames = seeded.slice(0, 6).reverse();
|
|
631
|
-
expect(deleted.map((e) => e.filename)).toEqual(expectedDeletedNames);
|
|
632
|
-
|
|
633
|
-
// Filesystem check: mixed extensions gone for indexes 0..5.
|
|
634
|
-
for (const name of expectedDeletedNames) {
|
|
635
|
-
expect(existsSync(join(dir, name))).toBe(false);
|
|
636
|
-
}
|
|
637
|
-
// Kept files still on disk.
|
|
638
|
-
const remaining = await listSnapshotsInDir(dir);
|
|
639
|
-
expect(remaining.map((e) => e.filename)).toEqual(expectedKeptNames);
|
|
640
|
-
});
|
|
641
|
-
});
|