@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,472 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Periodic backup worker.
|
|
3
|
-
*
|
|
4
|
-
* Drives the backup pipeline on a 5-minute tick interval. On each tick it
|
|
5
|
-
* checks whether `config.enabled` is true and whether enough time has passed
|
|
6
|
-
* since the last successful run; if so, it builds a workspace vbundle,
|
|
7
|
-
* writes it to the local backup directory, mirrors it to every configured
|
|
8
|
-
* offsite destination, applies retention to each pool, and records the run
|
|
9
|
-
* timestamp in the memory checkpoint store.
|
|
10
|
-
*
|
|
11
|
-
* The public surface is intentionally split into three layers:
|
|
12
|
-
*
|
|
13
|
-
* - `startBackupWorker` — installs the `setInterval` and returns a handle
|
|
14
|
-
* with `stop()` and `runOnce()`. Must never throw during startup (daemon
|
|
15
|
-
* startup philosophy); any failure during setup falls back to a no-op
|
|
16
|
-
* handle and logs the error.
|
|
17
|
-
* - `runBackupTick` — the pure tick body. Gates on `enabled` + interval +
|
|
18
|
-
* mutex, then delegates to `performBackup`. Propagates errors so callers
|
|
19
|
-
* (tests, the interval wrapper) can observe failures.
|
|
20
|
-
* - `createSnapshotNow` — manual-trigger variant. Bypasses the enabled and
|
|
21
|
-
* interval checks, but still honors the concurrency mutex (so a manual
|
|
22
|
-
* trigger will reject with "snapshot in progress" if one is in flight).
|
|
23
|
-
*
|
|
24
|
-
* Everything that touches real daemon state (DB, workspace, filesystem) is
|
|
25
|
-
* injected through the `BackupDeps` shape so tests can drive the whole
|
|
26
|
-
* surface against temp directories with tiny fake bundles.
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { hostname } from "node:os";
|
|
30
|
-
import { Database } from "bun:sqlite";
|
|
31
|
-
|
|
32
|
-
import { getConfig } from "../config/loader.js";
|
|
33
|
-
import type { BackupConfig } from "../config/schema.js";
|
|
34
|
-
import { getAssistantName } from "../daemon/identity-helpers.js";
|
|
35
|
-
import {
|
|
36
|
-
getMemoryCheckpoint as realGetMemoryCheckpoint,
|
|
37
|
-
setMemoryCheckpoint as realSetMemoryCheckpoint,
|
|
38
|
-
} from "../memory/checkpoints.js";
|
|
39
|
-
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
40
|
-
import type { VBundleOriginMode } from "../runtime/migrations/origin-mode.js";
|
|
41
|
-
import type { StreamExportVBundleResult } from "../runtime/migrations/vbundle-builder.js";
|
|
42
|
-
import { streamExportVBundle as realStreamExportVBundle } from "../runtime/migrations/vbundle-builder.js";
|
|
43
|
-
import { getDaemonRuntimeMode } from "../runtime/runtime-mode.js";
|
|
44
|
-
import { getLogger } from "../util/logger.js";
|
|
45
|
-
import { getDbPath, getWorkspaceDir } from "../util/platform.js";
|
|
46
|
-
import { APP_VERSION } from "../version.js";
|
|
47
|
-
import { ensureBackupKey as realEnsureBackupKey } from "./backup-key.js";
|
|
48
|
-
import type { SnapshotEntry } from "./list-snapshots.js";
|
|
49
|
-
import { pruneLocalSnapshots, writeLocalSnapshot } from "./local-writer.js";
|
|
50
|
-
import type { OffsiteWriteResult } from "./offsite-writer.js";
|
|
51
|
-
import {
|
|
52
|
-
pruneOffsiteSnapshotsInAll,
|
|
53
|
-
writeOffsiteSnapshotToAll,
|
|
54
|
-
} from "./offsite-writer.js";
|
|
55
|
-
import {
|
|
56
|
-
getBackupKeyPath,
|
|
57
|
-
getLocalBackupsDir,
|
|
58
|
-
resolveOffsiteDestinations,
|
|
59
|
-
} from "./paths.js";
|
|
60
|
-
import { acquireSnapshotLock, getSnapshotLockPath } from "./snapshot-lock.js";
|
|
61
|
-
|
|
62
|
-
const log = getLogger("backup-worker");
|
|
63
|
-
|
|
64
|
-
/** Memory checkpoint key for the last successful backup run timestamp. */
|
|
65
|
-
const LAST_RUN_CHECKPOINT_KEY = "backup:last_run_at";
|
|
66
|
-
|
|
67
|
-
/** Default tick interval — fires every 5 minutes, gated by interval check. */
|
|
68
|
-
const TICK_INTERVAL_MS = 5 * 60 * 1000;
|
|
69
|
-
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Public types
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Result of a single backup run. `offsite` is an array with one entry per
|
|
76
|
-
* destination so callers can inspect per-destination success / skip / error
|
|
77
|
-
* status — a single missing offsite volume does not poison the whole run.
|
|
78
|
-
*/
|
|
79
|
-
export interface BackupRunResult {
|
|
80
|
-
local: SnapshotEntry;
|
|
81
|
-
offsite: OffsiteWriteResult[];
|
|
82
|
-
durationMs: number;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Opaque handle returned by `startBackupWorker`. Callers drive the worker
|
|
87
|
-
* exclusively through this handle; the underlying timer and mutex state are
|
|
88
|
-
* module-scoped implementation details.
|
|
89
|
-
*/
|
|
90
|
-
export interface BackupWorkerHandle {
|
|
91
|
-
stop(): void;
|
|
92
|
-
runOnce(): Promise<BackupRunResult | null>;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Dependency injection bag for `runBackupTick` / `createSnapshotNow`.
|
|
97
|
-
*
|
|
98
|
-
* In production the defaults wire up the real DB, workspace, and memory
|
|
99
|
-
* checkpoint store. Tests inject fakes so they can drive the worker
|
|
100
|
-
* against temp directories with in-memory checkpoint state.
|
|
101
|
-
*/
|
|
102
|
-
export interface BackupDeps {
|
|
103
|
-
streamExportVBundle?: (
|
|
104
|
-
options: Parameters<typeof realStreamExportVBundle>[0],
|
|
105
|
-
) => Promise<StreamExportVBundleResult>;
|
|
106
|
-
getMemoryCheckpoint?: (key: string) => string | null;
|
|
107
|
-
setMemoryCheckpoint?: (key: string, value: string) => void;
|
|
108
|
-
ensureBackupKey?: (path: string) => Promise<Buffer>;
|
|
109
|
-
/** Override for the workspace directory (tests). */
|
|
110
|
-
workspaceDir?: string;
|
|
111
|
-
/** Override for the local backup directory (tests). */
|
|
112
|
-
localDir?: string;
|
|
113
|
-
/** Override for the backup key file path (tests). */
|
|
114
|
-
backupKeyPath?: string;
|
|
115
|
-
/**
|
|
116
|
-
* Override for the cross-process snapshot lock file path (tests). Defaults
|
|
117
|
-
* to `getSnapshotLockPath()` in production. Tests that drive multiple
|
|
118
|
-
* parallel runs against temp directories inject a path under their fixture
|
|
119
|
-
* root so they don't collide with each other or with the real daemon.
|
|
120
|
-
*/
|
|
121
|
-
snapshotLockPath?: string;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
// Concurrency mutex (two layers)
|
|
126
|
-
// ---------------------------------------------------------------------------
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* In-memory mutex flag — the first line of defense. Protects the current
|
|
130
|
-
* process from racing itself:
|
|
131
|
-
* - A scheduled tick that fires while a manual run is in flight skips silently.
|
|
132
|
-
* - A manual run that starts while a scheduled tick is running throws so the
|
|
133
|
-
* user can decide how to react (retry, wait, surface the conflict).
|
|
134
|
-
*
|
|
135
|
-
* The in-process flag is module-scoped rather than tied to the handle
|
|
136
|
-
* returned by `startBackupWorker` — the daemon may start the worker once at
|
|
137
|
-
* boot and also call `createSnapshotNow` from other code paths, and both must
|
|
138
|
-
* see the same concurrency state.
|
|
139
|
-
*
|
|
140
|
-
* Beyond the in-process flag, both entry points also acquire a cross-process
|
|
141
|
-
* file lock at `getSnapshotLockPath()` so a CLI `vellum backup create` run
|
|
142
|
-
* cannot race the daemon's periodic tick — they would otherwise hold
|
|
143
|
-
* independent copies of this module-scoped flag. See `./snapshot-lock.ts`.
|
|
144
|
-
* The in-process flag stays as a fast path so same-process conflicts skip
|
|
145
|
-
* the filesystem entirely.
|
|
146
|
-
*/
|
|
147
|
-
let snapshotInProgress = false;
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// Core pipeline body
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* The shared body that both `runBackupTick` and `createSnapshotNow` call
|
|
155
|
-
* after their gating checks pass. Does not touch the mutex — callers are
|
|
156
|
-
* responsible for acquiring it.
|
|
157
|
-
*
|
|
158
|
-
* Pipeline:
|
|
159
|
-
* 1. Resolve offsite destinations (iCloud default if config did not
|
|
160
|
-
* specify an explicit array).
|
|
161
|
-
* 2. Load the backup key only if at least one destination needs it.
|
|
162
|
-
* Plaintext-only setups never touch the key file.
|
|
163
|
-
* 3. Stream the workspace into a temp .vbundle file, passing a WAL
|
|
164
|
-
* checkpoint callback so the exported DB has every committed row.
|
|
165
|
-
* 4. Move the temp file into the local backup directory (rename).
|
|
166
|
-
* After this point the temp file no longer exists, so we must not
|
|
167
|
-
* call the `cleanup()` callback on success.
|
|
168
|
-
* 5. Mirror the local file to every offsite destination (sequential).
|
|
169
|
-
* 6. Apply retention to the local pool and every offsite pool.
|
|
170
|
-
*/
|
|
171
|
-
async function performBackup(
|
|
172
|
-
config: BackupConfig,
|
|
173
|
-
now: Date,
|
|
174
|
-
deps: BackupDeps,
|
|
175
|
-
): Promise<BackupRunResult> {
|
|
176
|
-
const streamExport = deps.streamExportVBundle ?? realStreamExportVBundle;
|
|
177
|
-
const ensureKey = deps.ensureBackupKey ?? realEnsureBackupKey;
|
|
178
|
-
const workspaceDir = deps.workspaceDir ?? getWorkspaceDir();
|
|
179
|
-
const localDir = deps.localDir ?? getLocalBackupsDir(config.localDirectory);
|
|
180
|
-
const backupKeyPath = deps.backupKeyPath ?? getBackupKeyPath();
|
|
181
|
-
|
|
182
|
-
const startTimestamp = Date.now();
|
|
183
|
-
|
|
184
|
-
const destinations = config.offsite.enabled
|
|
185
|
-
? resolveOffsiteDestinations(config.offsite.destinations)
|
|
186
|
-
: [];
|
|
187
|
-
const needsKey = destinations.some((d) => d.encrypt);
|
|
188
|
-
const key: Buffer | null = needsKey ? await ensureKey(backupKeyPath) : null;
|
|
189
|
-
|
|
190
|
-
// Build the vbundle into a temp file. Pass a WAL checkpoint callback that
|
|
191
|
-
// mirrors the pattern in `handleMigrationExport`: open a fresh Database
|
|
192
|
-
// handle, run PRAGMA wal_checkpoint(TRUNCATE), close it. Any failure is
|
|
193
|
-
// best-effort — the export still proceeds with whatever is on disk.
|
|
194
|
-
//
|
|
195
|
-
// The backup worker bundles credentials by design (its purpose is local
|
|
196
|
-
// recovery), so `secretsRedacted: false`. Backups run locally on the host
|
|
197
|
-
// machine; managed deployments delegate to the platform. Hardcoding to
|
|
198
|
-
// self-hosted ensures the resulting bundle satisfies the v1 schema's
|
|
199
|
-
// refine for managed/secrets_redacted — `getOriginMode()` would return
|
|
200
|
-
// "managed" in a managed deployment, producing a non-restorable bundle.
|
|
201
|
-
const originMode: VBundleOriginMode =
|
|
202
|
-
getDaemonRuntimeMode() === "docker"
|
|
203
|
-
? "self-hosted-remote"
|
|
204
|
-
: "self-hosted-local";
|
|
205
|
-
const result = await streamExport({
|
|
206
|
-
workspaceDir,
|
|
207
|
-
assistant: {
|
|
208
|
-
id: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
209
|
-
name: getAssistantName() ?? "Assistant",
|
|
210
|
-
runtime_version: APP_VERSION,
|
|
211
|
-
},
|
|
212
|
-
origin: {
|
|
213
|
-
mode: originMode,
|
|
214
|
-
hostname: hostname(),
|
|
215
|
-
},
|
|
216
|
-
compatibility: {
|
|
217
|
-
min_runtime_version: APP_VERSION,
|
|
218
|
-
max_runtime_version: null,
|
|
219
|
-
},
|
|
220
|
-
exportOptions: {
|
|
221
|
-
include_logs: true,
|
|
222
|
-
include_browser_state: false,
|
|
223
|
-
include_memory_vectors: false,
|
|
224
|
-
},
|
|
225
|
-
secretsRedacted: false,
|
|
226
|
-
checkpoint: () => {
|
|
227
|
-
const dbPath = getDbPath();
|
|
228
|
-
try {
|
|
229
|
-
const db = new Database(dbPath);
|
|
230
|
-
try {
|
|
231
|
-
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
232
|
-
} finally {
|
|
233
|
-
db.close();
|
|
234
|
-
}
|
|
235
|
-
} catch (err) {
|
|
236
|
-
log.warn(
|
|
237
|
-
{ err },
|
|
238
|
-
"WAL checkpoint failed — proceeding with backup without checkpoint",
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const { tempPath, cleanup } = result;
|
|
245
|
-
|
|
246
|
-
// `writeLocalSnapshot` moves (renames) the temp file to its final
|
|
247
|
-
// location. On success the temp file no longer exists, so we MUST NOT
|
|
248
|
-
// call `cleanup()` afterwards — it would try to unlink a missing path.
|
|
249
|
-
// On failure we still need to unlink the temp file to avoid leaks.
|
|
250
|
-
let localResult: SnapshotEntry;
|
|
251
|
-
try {
|
|
252
|
-
localResult = await writeLocalSnapshot(tempPath, localDir, now);
|
|
253
|
-
} catch (err) {
|
|
254
|
-
try {
|
|
255
|
-
await cleanup();
|
|
256
|
-
} catch {
|
|
257
|
-
// best-effort
|
|
258
|
-
}
|
|
259
|
-
throw err;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const offsiteResults = await writeOffsiteSnapshotToAll(
|
|
263
|
-
localResult.path,
|
|
264
|
-
destinations,
|
|
265
|
-
key,
|
|
266
|
-
now,
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// Apply retention to both pools. Retention is per-destination so a
|
|
270
|
-
// missing offsite volume doesn't skew the local pool's retention count.
|
|
271
|
-
await pruneLocalSnapshots(localDir, config.retention);
|
|
272
|
-
await pruneOffsiteSnapshotsInAll(destinations, config.retention);
|
|
273
|
-
|
|
274
|
-
log.info(
|
|
275
|
-
{
|
|
276
|
-
localPath: localResult.path,
|
|
277
|
-
offsite: offsiteResults.map((r) => ({
|
|
278
|
-
path: r.destination.path,
|
|
279
|
-
status: r.entry ? "ok" : r.skipped ? "skipped" : "error",
|
|
280
|
-
reason: r.skipped ?? r.error,
|
|
281
|
-
})),
|
|
282
|
-
},
|
|
283
|
-
"Backup snapshot complete",
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
local: localResult,
|
|
288
|
-
offsite: offsiteResults,
|
|
289
|
-
durationMs: Date.now() - startTimestamp,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
// Public entry points
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Pure tick body for the scheduled backup worker. Runs the enabled + interval
|
|
299
|
-
* gates, acquires the mutex, delegates to `performBackup`, and records the
|
|
300
|
-
* last-run timestamp on success. Returns `null` if any gate rejects the run.
|
|
301
|
-
*
|
|
302
|
-
* Errors from `performBackup` propagate — the `setInterval` caller wraps this
|
|
303
|
-
* in a try/catch that logs and swallows, so daemon startup and steady-state
|
|
304
|
-
* ticks never crash the process.
|
|
305
|
-
*/
|
|
306
|
-
export async function runBackupTick(
|
|
307
|
-
config: BackupConfig,
|
|
308
|
-
now: Date,
|
|
309
|
-
deps: BackupDeps = {},
|
|
310
|
-
): Promise<BackupRunResult | null> {
|
|
311
|
-
if (config.enabled !== true) return null;
|
|
312
|
-
|
|
313
|
-
const getCheckpoint = deps.getMemoryCheckpoint ?? realGetMemoryCheckpoint;
|
|
314
|
-
const setCheckpoint = deps.setMemoryCheckpoint ?? realSetMemoryCheckpoint;
|
|
315
|
-
const lockPath = deps.snapshotLockPath ?? getSnapshotLockPath();
|
|
316
|
-
|
|
317
|
-
const lastRunRaw = getCheckpoint(LAST_RUN_CHECKPOINT_KEY);
|
|
318
|
-
if (lastRunRaw != null) {
|
|
319
|
-
const lastRunMs = Number.parseInt(lastRunRaw, 10);
|
|
320
|
-
if (!Number.isNaN(lastRunMs)) {
|
|
321
|
-
const intervalMs = config.intervalHours * 3600 * 1000;
|
|
322
|
-
if (now.getTime() - lastRunMs < intervalMs) {
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// A manual snapshot in flight inside this process wins — the scheduled
|
|
329
|
-
// tick silently defers and will reconsider on the next interval.
|
|
330
|
-
if (snapshotInProgress) return null;
|
|
331
|
-
snapshotInProgress = true;
|
|
332
|
-
|
|
333
|
-
// Acquire the cross-process lock after the in-process fast path passes.
|
|
334
|
-
// If another process (a CLI `vellum backup create`, say) holds the lock,
|
|
335
|
-
// the scheduled tick defers silently — same semantics as when the
|
|
336
|
-
// in-process flag is set — so a concurrent CLI run does not spam warning
|
|
337
|
-
// logs on every 5-minute tick.
|
|
338
|
-
let release: (() => Promise<void>) | null = null;
|
|
339
|
-
try {
|
|
340
|
-
release = await acquireSnapshotLock(lockPath);
|
|
341
|
-
} catch (err) {
|
|
342
|
-
snapshotInProgress = false;
|
|
343
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
344
|
-
if (message.startsWith("snapshot in progress")) {
|
|
345
|
-
return null;
|
|
346
|
-
}
|
|
347
|
-
throw err;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
const result = await performBackup(config, now, deps);
|
|
352
|
-
setCheckpoint(LAST_RUN_CHECKPOINT_KEY, String(now.getTime()));
|
|
353
|
-
return result;
|
|
354
|
-
} finally {
|
|
355
|
-
try {
|
|
356
|
-
await release();
|
|
357
|
-
} catch {
|
|
358
|
-
// release is best-effort; logged internally
|
|
359
|
-
}
|
|
360
|
-
snapshotInProgress = false;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Manual-trigger variant of the backup pipeline. Bypasses the `enabled` and
|
|
366
|
-
* interval checks so users can force a snapshot regardless of schedule, but
|
|
367
|
-
* still honors the concurrency mutex — a second concurrent caller throws
|
|
368
|
-
* with "snapshot in progress".
|
|
369
|
-
*
|
|
370
|
-
* Does NOT update the last-run checkpoint on success: manual snapshots are
|
|
371
|
-
* an escape hatch and should not reset the automatic cadence.
|
|
372
|
-
*/
|
|
373
|
-
export async function createSnapshotNow(
|
|
374
|
-
config: BackupConfig,
|
|
375
|
-
now: Date,
|
|
376
|
-
deps: BackupDeps = {},
|
|
377
|
-
): Promise<BackupRunResult> {
|
|
378
|
-
const lockPath = deps.snapshotLockPath ?? getSnapshotLockPath();
|
|
379
|
-
|
|
380
|
-
// Fast path: in-process flag. Catches same-process races without touching
|
|
381
|
-
// the filesystem. The thrown message matches the cross-process variant's
|
|
382
|
-
// prefix so downstream consumers (HTTP 409, CLI error output) can test for
|
|
383
|
-
// "snapshot in progress" uniformly.
|
|
384
|
-
if (snapshotInProgress) {
|
|
385
|
-
throw new Error("snapshot in progress");
|
|
386
|
-
}
|
|
387
|
-
snapshotInProgress = true;
|
|
388
|
-
|
|
389
|
-
// Cross-process lock: the source of truth when a CLI invocation and the
|
|
390
|
-
// daemon worker could both be alive. On conflict, acquireSnapshotLock
|
|
391
|
-
// throws "snapshot in progress (locked by pid N)". We reset the
|
|
392
|
-
// in-process flag before rethrowing so subsequent local attempts aren't
|
|
393
|
-
// permanently blocked by a failed acquisition.
|
|
394
|
-
let release: (() => Promise<void>) | null = null;
|
|
395
|
-
try {
|
|
396
|
-
release = await acquireSnapshotLock(lockPath);
|
|
397
|
-
} catch (err) {
|
|
398
|
-
snapshotInProgress = false;
|
|
399
|
-
throw err;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
return await performBackup(config, now, deps);
|
|
404
|
-
} finally {
|
|
405
|
-
try {
|
|
406
|
-
await release();
|
|
407
|
-
} catch {
|
|
408
|
-
// release is best-effort; logged internally
|
|
409
|
-
}
|
|
410
|
-
snapshotInProgress = false;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// ---------------------------------------------------------------------------
|
|
415
|
-
// Scheduler
|
|
416
|
-
// ---------------------------------------------------------------------------
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* A no-op handle used when `startBackupWorker` fails to install its timer.
|
|
420
|
-
* Returning a live handle even on failure lets callers follow the normal
|
|
421
|
-
* `.stop()` cleanup path unconditionally.
|
|
422
|
-
*/
|
|
423
|
-
const NOOP_HANDLE: BackupWorkerHandle = {
|
|
424
|
-
stop: () => {},
|
|
425
|
-
runOnce: async () => null,
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Install the periodic backup worker.
|
|
430
|
-
*
|
|
431
|
-
* Schedules a `setInterval` tick every 5 minutes and returns a handle with
|
|
432
|
-
* `stop()` and `runOnce()`. `runOnce()` invokes the tick body synchronously
|
|
433
|
-
* (bypassing the interval) so callers can drive a backup from code without
|
|
434
|
-
* waiting up to 5 minutes for the next tick.
|
|
435
|
-
*
|
|
436
|
-
* Daemon startup philosophy: this function must never throw. Any unexpected
|
|
437
|
-
* error during setup logs and returns a no-op handle so the caller's startup
|
|
438
|
-
* sequence proceeds unperturbed.
|
|
439
|
-
*/
|
|
440
|
-
export function startBackupWorker(): BackupWorkerHandle {
|
|
441
|
-
try {
|
|
442
|
-
const timer = setInterval(() => {
|
|
443
|
-
void (async () => {
|
|
444
|
-
try {
|
|
445
|
-
const config = getConfig();
|
|
446
|
-
await runBackupTick(config.backup, new Date());
|
|
447
|
-
} catch (err) {
|
|
448
|
-
log.warn({ err }, "Backup worker tick failed");
|
|
449
|
-
}
|
|
450
|
-
})();
|
|
451
|
-
}, TICK_INTERVAL_MS);
|
|
452
|
-
|
|
453
|
-
// Non-blocking: the process may exit even if the timer is still armed.
|
|
454
|
-
(timer as NodeJS.Timeout).unref?.();
|
|
455
|
-
|
|
456
|
-
let stopped = false;
|
|
457
|
-
return {
|
|
458
|
-
stop(): void {
|
|
459
|
-
if (stopped) return;
|
|
460
|
-
stopped = true;
|
|
461
|
-
clearInterval(timer);
|
|
462
|
-
},
|
|
463
|
-
async runOnce(): Promise<BackupRunResult | null> {
|
|
464
|
-
const config = getConfig();
|
|
465
|
-
return runBackupTick(config.backup, new Date());
|
|
466
|
-
},
|
|
467
|
-
};
|
|
468
|
-
} catch (err) {
|
|
469
|
-
log.warn({ err }, "Failed to start backup worker — continuing without it");
|
|
470
|
-
return NOOP_HANDLE;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Offsite snapshot writer with per-destination encryption.
|
|
3
|
-
*
|
|
4
|
-
* "Offsite" destinations are any location outside the local backup directory
|
|
5
|
-
* where the user wants a redundant copy of a just-written local snapshot.
|
|
6
|
-
* Canonical examples: iCloud Drive, an external SSD, a network share.
|
|
7
|
-
*
|
|
8
|
-
* Per-destination `encrypt` flag:
|
|
9
|
-
* - `encrypt: true` → AES-256-GCM stream-encrypt into `.vbundle.enc`.
|
|
10
|
-
* - `encrypt: false` → plaintext copy into `.vbundle`. Intended for volumes
|
|
11
|
-
* where the user controls physical access (e.g. an external SSD).
|
|
12
|
-
*
|
|
13
|
-
* Each destination is written independently and sequentially, so one bad
|
|
14
|
-
* destination cannot poison the others: a missing iCloud mount or a broken
|
|
15
|
-
* external drive surfaces as a per-destination `skipped` or `error` in the
|
|
16
|
-
* returned array while every other destination still gets its copy.
|
|
17
|
-
*
|
|
18
|
-
* The helpers are pure with respect to daemon state — they operate on an
|
|
19
|
-
* explicit `localSnapshotPath`, `destinations`, `key`, and `now` so tests can
|
|
20
|
-
* drive the whole surface against temp directories.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { copyFile, mkdir, rename, stat } from "node:fs/promises";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
|
|
26
|
-
import type { BackupDestination } from "../config/schema.js";
|
|
27
|
-
import {
|
|
28
|
-
pruneDir,
|
|
29
|
-
type SnapshotEntry,
|
|
30
|
-
} from "./list-snapshots.js";
|
|
31
|
-
import { deriveSafeAncestor, formatBackupFilename } from "./paths.js";
|
|
32
|
-
import { encryptFile } from "./stream-crypt.js";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Result of writing a single offsite destination.
|
|
36
|
-
*
|
|
37
|
-
* Exactly one of `entry`, `skipped`, or `error` is meaningful:
|
|
38
|
-
* - `entry` non-null → the write succeeded.
|
|
39
|
-
* - `skipped: "parent-missing"` → the destination's safe ancestor does not
|
|
40
|
-
* exist (e.g. iCloud Drive not enabled, external volume unplugged). Not an
|
|
41
|
-
* error — the write is simply deferred until the volume is back. The
|
|
42
|
-
* ancestor is derived by `deriveSafeAncestor`: for iCloud Drive or
|
|
43
|
-
* `/Volumes/<name>/...` paths it is a well-known mount root, which lets us
|
|
44
|
-
* bootstrap intermediate directories on first run; for arbitrary
|
|
45
|
-
* user-configured paths it falls back to the immediate parent.
|
|
46
|
-
* - `error` set → an unexpected failure while writing. Surfaced as a string
|
|
47
|
-
* so callers can log without serializing an `Error` object.
|
|
48
|
-
*
|
|
49
|
-
* `destination` always preserves the full config record (path + encrypt) so
|
|
50
|
-
* callers can correlate a result with the destination that produced it.
|
|
51
|
-
*/
|
|
52
|
-
export interface OffsiteWriteResult {
|
|
53
|
-
destination: BackupDestination;
|
|
54
|
-
entry: SnapshotEntry | null;
|
|
55
|
-
skipped?: "parent-missing";
|
|
56
|
-
error?: string;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Write a local snapshot to a single offsite destination.
|
|
61
|
-
*
|
|
62
|
-
* Behavior:
|
|
63
|
-
* - If the destination's safe ancestor (see `deriveSafeAncestor`) does not
|
|
64
|
-
* exist → returns `{ destination, entry: null, skipped: "parent-missing" }`.
|
|
65
|
-
* The offsite volume is (temporarily) unavailable; the caller should not
|
|
66
|
-
* treat this as an error.
|
|
67
|
-
* - Otherwise `mkdir -p` the destination directory (mode `0o700`). This
|
|
68
|
-
* bootstraps any intermediate directories between the safe ancestor and
|
|
69
|
-
* the destination (e.g. creating `VellumAssistant/backups/` under iCloud
|
|
70
|
-
* Drive on first run).
|
|
71
|
-
* - If `destination.encrypt === true`, stream-encrypts via `encryptFile`
|
|
72
|
-
* with the provided `key` and writes `.vbundle.enc`. A missing `key`
|
|
73
|
-
* here is a programmer error, but per the plan we still catch it rather
|
|
74
|
-
* than throwing — a broken destination must never poison the others.
|
|
75
|
-
* - If `destination.encrypt === false`, copies the local snapshot into a
|
|
76
|
-
* `.tmp` sibling and renames into place (atomic; cross-filesystem safe
|
|
77
|
-
* because `copyFile` handles the copy itself).
|
|
78
|
-
*
|
|
79
|
-
* On any unexpected throw, returns `{ destination, entry: null, error: msg }`.
|
|
80
|
-
*/
|
|
81
|
-
export async function writeOffsiteSnapshotToOne(
|
|
82
|
-
localSnapshotPath: string,
|
|
83
|
-
destination: BackupDestination,
|
|
84
|
-
key: Buffer | null,
|
|
85
|
-
now: Date,
|
|
86
|
-
): Promise<OffsiteWriteResult> {
|
|
87
|
-
try {
|
|
88
|
-
// Ancestor-missing probe: if the destination's derived "safe ancestor"
|
|
89
|
-
// does not exist we treat the destination as temporarily unavailable
|
|
90
|
-
// rather than auto-creating a deep tree we have no reason to own. The
|
|
91
|
-
// ancestor is a well-known mount root (iCloud Drive, /Volumes/<name>)
|
|
92
|
-
// for recognized path shapes, or the immediate parent otherwise. That
|
|
93
|
-
// way an unplugged external drive still skips cleanly while the default
|
|
94
|
-
// iCloud destination can bootstrap its intermediate folders on first run.
|
|
95
|
-
try {
|
|
96
|
-
await stat(deriveSafeAncestor(destination.path));
|
|
97
|
-
} catch (err) {
|
|
98
|
-
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
99
|
-
return { destination, entry: null, skipped: "parent-missing" };
|
|
100
|
-
}
|
|
101
|
-
throw err;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// `destination.path` itself may not exist yet — create it now that we
|
|
105
|
-
// know its parent is reachable.
|
|
106
|
-
await mkdir(destination.path, { recursive: true, mode: 0o700 });
|
|
107
|
-
|
|
108
|
-
const filename = formatBackupFilename(now, {
|
|
109
|
-
encrypted: destination.encrypt,
|
|
110
|
-
});
|
|
111
|
-
const outputPath = join(destination.path, filename);
|
|
112
|
-
|
|
113
|
-
if (destination.encrypt) {
|
|
114
|
-
// Programmer-contract: the caller must have ensured a key exists if any
|
|
115
|
-
// destination is encrypted. We still route this through the catch block
|
|
116
|
-
// so a single broken destination cannot poison the others.
|
|
117
|
-
if (key == null) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
"Offsite destination requires encryption but no key was provided",
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
await encryptFile(localSnapshotPath, outputPath, key);
|
|
123
|
-
} else {
|
|
124
|
-
// Atomic plaintext copy: write into a sibling `.tmp` then rename into
|
|
125
|
-
// place. `copyFile` handles cross-filesystem copies, so we don't need
|
|
126
|
-
// the EXDEV fallback dance that `writeLocalSnapshot` uses for a
|
|
127
|
-
// same-device rename.
|
|
128
|
-
const tempPath = `${outputPath}.tmp`;
|
|
129
|
-
await copyFile(localSnapshotPath, tempPath);
|
|
130
|
-
await rename(tempPath, outputPath);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const stats = await stat(outputPath);
|
|
134
|
-
return {
|
|
135
|
-
destination,
|
|
136
|
-
entry: {
|
|
137
|
-
path: outputPath,
|
|
138
|
-
filename,
|
|
139
|
-
createdAt: now,
|
|
140
|
-
sizeBytes: stats.size,
|
|
141
|
-
encrypted: destination.encrypt,
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
} catch (err) {
|
|
145
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
-
return { destination, entry: null, error: message };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Write a local snapshot to every configured offsite destination, in order.
|
|
152
|
-
*
|
|
153
|
-
* Sequential by design: parallelizing wouldn't save meaningful wall-clock
|
|
154
|
-
* time (the dominant cost is filesystem IO on potentially-slow network
|
|
155
|
-
* volumes) and sequential writes make per-destination failures trivially
|
|
156
|
-
* observable. Empty array returns `[]` immediately without any stat/mkdir.
|
|
157
|
-
*
|
|
158
|
-
* Returns one `OffsiteWriteResult` per input destination, in the same order
|
|
159
|
-
* as `destinations`. Callers can `filter` the result to extract successes
|
|
160
|
-
* (`entry != null`), skips (`skipped === "parent-missing"`), or errors
|
|
161
|
-
* (`error != null`).
|
|
162
|
-
*/
|
|
163
|
-
export async function writeOffsiteSnapshotToAll(
|
|
164
|
-
localSnapshotPath: string,
|
|
165
|
-
destinations: BackupDestination[],
|
|
166
|
-
key: Buffer | null,
|
|
167
|
-
now: Date,
|
|
168
|
-
): Promise<OffsiteWriteResult[]> {
|
|
169
|
-
if (destinations.length === 0) return [];
|
|
170
|
-
|
|
171
|
-
const results: OffsiteWriteResult[] = [];
|
|
172
|
-
for (const destination of destinations) {
|
|
173
|
-
const result = await writeOffsiteSnapshotToOne(
|
|
174
|
-
localSnapshotPath,
|
|
175
|
-
destination,
|
|
176
|
-
key,
|
|
177
|
-
now,
|
|
178
|
-
);
|
|
179
|
-
results.push(result);
|
|
180
|
-
}
|
|
181
|
-
return results;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Apply retention to every configured offsite destination.
|
|
186
|
-
*
|
|
187
|
-
* Retention is applied **per destination** — each keeps its own newest
|
|
188
|
-
* `retention` snapshots independently. A `skipped: true` result means the
|
|
189
|
-
* destination's parent directory is missing (e.g. iCloud Drive disabled);
|
|
190
|
-
* callers should treat this as a transient unavailability rather than an
|
|
191
|
-
* empty directory.
|
|
192
|
-
*
|
|
193
|
-
* Mixed `.vbundle` and `.vbundle.enc` files in a single destination are
|
|
194
|
-
* treated as one pool ordered by parsed timestamp, so retention still holds
|
|
195
|
-
* if a destination's `encrypt` flag changes over its lifetime.
|
|
196
|
-
*/
|
|
197
|
-
export async function pruneOffsiteSnapshotsInAll(
|
|
198
|
-
destinations: BackupDestination[],
|
|
199
|
-
retention: number,
|
|
200
|
-
): Promise<
|
|
201
|
-
Array<{
|
|
202
|
-
destination: BackupDestination;
|
|
203
|
-
kept: SnapshotEntry[];
|
|
204
|
-
deleted: SnapshotEntry[];
|
|
205
|
-
skipped?: boolean;
|
|
206
|
-
}>
|
|
207
|
-
> {
|
|
208
|
-
const results: Array<{
|
|
209
|
-
destination: BackupDestination;
|
|
210
|
-
kept: SnapshotEntry[];
|
|
211
|
-
deleted: SnapshotEntry[];
|
|
212
|
-
skipped?: boolean;
|
|
213
|
-
}> = [];
|
|
214
|
-
for (const destination of destinations) {
|
|
215
|
-
const { kept, deleted, skipped } = await pruneDir(
|
|
216
|
-
destination.path,
|
|
217
|
-
retention,
|
|
218
|
-
);
|
|
219
|
-
results.push({ destination, kept, deleted, skipped });
|
|
220
|
-
}
|
|
221
|
-
return results;
|
|
222
|
-
}
|