@vellumai/assistant 0.4.56 → 0.5.0
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 +10 -10
- package/Dockerfile +3 -0
- package/README.md +11 -11
- package/docs/architecture/integrations.md +2 -2
- package/docs/architecture/memory.md +3 -4
- package/docs/credential-execution-service.md +13 -20
- package/node_modules/@vellumai/ces-contracts/src/error.ts +5 -4
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +7 -7
- package/src/__tests__/anthropic-provider.test.ts +172 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +15 -1
- package/src/__tests__/approval-cascade.test.ts +2 -2
- package/src/__tests__/approval-routes-http.test.ts +3 -4
- package/src/__tests__/asset-materialize-tool.test.ts +5 -5
- package/src/__tests__/asset-search-tool.test.ts +1 -1
- package/src/__tests__/assistant-attachments.test.ts +5 -5
- package/src/__tests__/assistant-events-sse-hardening.test.ts +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +50 -38
- package/src/__tests__/attachments-store.test.ts +2 -2
- package/src/__tests__/avatar-e2e.test.ts +5 -3
- package/src/__tests__/browser-skill-endstate.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +2 -2
- package/src/__tests__/callback-handoff-copy.test.ts +1 -1
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +158 -0
- package/src/__tests__/channel-readiness-routes.test.ts +0 -1
- package/src/__tests__/channel-readiness-service.test.ts +0 -1
- package/src/__tests__/checker.test.ts +31 -32
- package/src/__tests__/chrome-cdp.test.ts +47 -18
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -2
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +9 -18
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -4
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop.test.ts +11 -4
- package/src/__tests__/conversation-attachments.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -2
- package/src/__tests__/conversation-error.test.ts +33 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -1
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -4
- package/src/__tests__/conversation-provider-retry-repair.test.ts +4 -4
- package/src/__tests__/conversation-queue.test.ts +23 -14
- package/src/__tests__/conversation-routes-slash-commands.test.ts +3 -3
- package/src/__tests__/conversation-runtime-assembly.test.ts +204 -185
- package/src/__tests__/conversation-seed-composer.test.ts +1 -1
- package/src/__tests__/conversation-slash-queue.test.ts +4 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -4
- package/src/__tests__/conversation-starter-routes.test.ts +291 -0
- package/src/__tests__/conversation-wipe.test.ts +438 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -3
- package/src/__tests__/conversation-workspace-injection.test.ts +4 -5
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -5
- package/src/__tests__/credential-security-e2e.test.ts +20 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +227 -0
- package/src/__tests__/credentials-cli.test.ts +3 -0
- package/src/__tests__/date-context.test.ts +59 -377
- package/src/__tests__/drop-capability-card-state-migration.test.ts +169 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +11 -45
- package/src/__tests__/emit-signal-routing-intent.test.ts +3 -3
- package/src/__tests__/encrypted-store.test.ts +249 -15
- package/src/__tests__/ephemeral-permissions.test.ts +4 -5
- package/src/__tests__/event-bus.test.ts +3 -3
- package/src/__tests__/file-read-tool.test.ts +40 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +2 -2
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +4 -4
- package/src/__tests__/gemini-provider.test.ts +6 -9
- package/src/__tests__/guardian-binding-drift-heal.test.ts +128 -0
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/host-file-read-tool.test.ts +87 -0
- package/src/__tests__/host-shell-tool.test.ts +6 -6
- package/src/__tests__/http-user-message-parity.test.ts +2 -2
- package/src/__tests__/identity-intro-cache.test.ts +209 -0
- package/src/__tests__/intent-routing.test.ts +51 -99
- package/src/__tests__/invite-routes-http.test.ts +5 -0
- package/src/__tests__/list-messages-attachments.test.ts +1 -1
- package/src/__tests__/managed-proxy-context.test.ts +2 -5
- package/src/__tests__/managed-skill-lifecycle.test.ts +8 -8
- package/src/__tests__/media-generate-image.test.ts +32 -15
- package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +1 -1
- package/src/__tests__/memory-lifecycle-e2e.test.ts +24 -18
- package/src/__tests__/memory-recall-quality.test.ts +4 -3
- package/src/__tests__/memory-regressions.test.ts +86 -90
- package/src/__tests__/migration-cross-version-compatibility.test.ts +32 -32
- package/src/__tests__/migration-export-http.test.ts +26 -27
- package/src/__tests__/migration-import-commit-http.test.ts +165 -37
- package/src/__tests__/migration-import-preflight-http.test.ts +81 -20
- package/src/__tests__/migration-validate-http.test.ts +16 -16
- package/src/__tests__/model-intents.test.ts +2 -2
- package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +3 -3
- package/src/__tests__/notification-broadcaster.test.ts +1 -1
- package/src/__tests__/notification-decision-fallback.test.ts +2 -2
- package/src/__tests__/notification-decision-identity.test.ts +8 -9
- package/src/__tests__/notification-decision-strategy.test.ts +1 -1
- package/src/__tests__/notification-deep-link.test.ts +1 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-schedule-dedup.test.ts +7 -7
- package/src/__tests__/oauth-store.test.ts +1 -3
- package/src/__tests__/oauth2-gateway-transport.test.ts +6 -1
- package/src/__tests__/onboarding-template-contract.test.ts +23 -59
- package/src/__tests__/provider-error-scenarios.test.ts +154 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +2 -2
- package/src/__tests__/provider-managed-proxy-integration.test.ts +8 -9
- package/src/__tests__/provider-registry-ollama.test.ts +5 -2
- package/src/__tests__/qdrant-manager.test.ts +7 -7
- package/src/__tests__/ratelimit.test.ts +0 -74
- package/src/__tests__/recording-handler.test.ts +0 -1
- package/src/__tests__/require-fresh-approval.test.ts +1 -1
- package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
- package/src/__tests__/runtime-events-sse-parity.test.ts +1 -1
- package/src/__tests__/runtime-events-sse.test.ts +1 -1
- package/src/__tests__/scheduler-recurrence.test.ts +46 -2
- package/src/__tests__/schema-transforms.test.ts +114 -54
- package/src/__tests__/secret-onetime-send.test.ts +20 -0
- package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -2
- package/src/__tests__/secret-scanner-executor.test.ts +1 -2
- package/src/__tests__/send-endpoint-busy.test.ts +63 -4
- package/src/__tests__/send-notification-tool.test.ts +2 -2
- package/src/__tests__/shell-credential-ref.test.ts +0 -1
- package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -2
- package/src/__tests__/skill-memory.test.ts +549 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +1 -2
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -0
- package/src/__tests__/slack-channel-config.test.ts +109 -94
- package/src/__tests__/swarm-conversation-integration.test.ts +2 -2
- package/src/__tests__/swarm-recursion.test.ts +2 -2
- package/src/__tests__/swarm-tool.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +19 -66
- package/src/__tests__/telegram-config.test.ts +121 -0
- package/src/__tests__/terminal-tools.test.ts +1 -1
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +1 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +1 -1
- package/src/__tests__/trace-emitter.test.ts +8 -1
- package/src/__tests__/trust-store.test.ts +7 -8
- package/src/__tests__/twilio-routes.test.ts +1 -18
- package/src/__tests__/user-reference.test.ts +82 -2
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +196 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +3 -3
- package/src/avatar/ascii-renderer.ts +2 -2
- package/src/avatar/png-renderer.ts +2 -2
- package/src/avatar/resvg-lazy.ts +21 -0
- package/src/calls/guardian-dispatch.ts +1 -1
- package/src/calls/relay-access-wait.ts +2 -2
- package/src/calls/twilio-rest.ts +0 -248
- package/src/cli/AGENTS.md +5 -8
- package/src/cli/__tests__/notifications.test.ts +5 -5
- package/src/cli/commands/avatar.ts +64 -2
- package/src/cli/commands/conversations.ts +131 -1
- package/src/cli/commands/credentials.ts +2 -0
- package/src/cli/commands/notifications.ts +3 -3
- package/src/cli.ts +10 -0
- package/src/config/bundled-skills/acp/SKILL.md +5 -5
- package/src/config/bundled-skills/acp/TOOLS.json +6 -6
- package/src/config/bundled-skills/app-builder/SKILL.md +42 -42
- package/src/config/bundled-skills/app-builder/TOOLS.json +10 -10
- package/src/config/bundled-skills/browser/SKILL.md +15 -15
- package/src/config/bundled-skills/browser/TOOLS.json +14 -14
- package/src/config/bundled-skills/chatgpt-import/SKILL.md +2 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +1 -1
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/bundled-skills/claude-code/SKILL.md +5 -5
- package/src/config/bundled-skills/computer-use/SKILL.md +2 -2
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -15
- package/src/config/bundled-skills/contacts/SKILL.md +3 -3
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -4
- package/src/config/bundled-skills/document/SKILL.md +4 -4
- package/src/config/bundled-skills/document/TOOLS.json +2 -2
- package/src/config/bundled-skills/followups/TOOLS.json +3 -3
- package/src/config/bundled-skills/gmail/SKILL.md +32 -32
- package/src/config/bundled-skills/gmail/TOOLS.json +16 -16
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
- package/src/config/bundled-skills/google-calendar/SKILL.md +1 -1
- package/src/config/bundled-skills/google-calendar/TOOLS.json +5 -5
- package/src/config/bundled-skills/google-calendar/types.ts +1 -1
- package/src/config/bundled-skills/heartbeat/SKILL.md +43 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +3 -3
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -3
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +16 -12
- package/src/config/bundled-skills/media-processing/SKILL.md +40 -40
- package/src/config/bundled-skills/media-processing/TOOLS.json +8 -8
- package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +2 -2
- package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +1 -1
- package/src/config/bundled-skills/media-processing/services/gemini-map.ts +5 -5
- package/src/config/bundled-skills/media-processing/services/gemini-video.ts +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +2 -2
- package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +2 -2
- package/src/config/bundled-skills/media-processing/services/reduce.ts +3 -3
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +2 -2
- package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +1 -1
- package/src/config/bundled-skills/messaging/SKILL.md +29 -25
- package/src/config/bundled-skills/messaging/TOOLS.json +11 -11
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +1 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -1
- package/src/config/bundled-skills/notifications/SKILL.md +3 -3
- package/src/config/bundled-skills/notifications/TOOLS.json +2 -2
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +3 -3
- package/src/config/bundled-skills/orchestration/SKILL.md +1 -1
- package/src/config/bundled-skills/orchestration/TOOLS.json +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +18 -14
- package/src/config/bundled-skills/phone-calls/TOOLS.json +3 -3
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +2 -2
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +1 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +4 -4
- package/src/config/bundled-skills/schedule/SKILL.md +26 -26
- package/src/config/bundled-skills/schedule/TOOLS.json +5 -5
- package/src/config/bundled-skills/screen-watch/SKILL.md +3 -3
- package/src/config/bundled-skills/screen-watch/TOOLS.json +1 -1
- package/src/config/bundled-skills/sequences/SKILL.md +2 -2
- package/src/config/bundled-skills/sequences/TOOLS.json +10 -10
- package/src/config/bundled-skills/sequences/tools/sequence-analytics.ts +2 -2
- package/src/config/bundled-skills/sequences/tools/sequence-enroll.ts +2 -2
- package/src/config/bundled-skills/sequences/tools/sequence-enrollment-list.ts +1 -1
- package/src/config/bundled-skills/sequences/tools/sequence-get.ts +1 -1
- package/src/config/bundled-skills/sequences/tools/sequence-import.ts +3 -3
- package/src/config/bundled-skills/sequences/tools/sequence-list.ts +1 -1
- package/src/config/bundled-skills/sequences/tools/sequence-update.ts +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -1
- package/src/config/bundled-skills/skill-management/TOOLS.json +5 -5
- package/src/config/bundled-skills/skills-catalog/SKILL.md +84 -0
- package/src/config/bundled-skills/slack/SKILL.md +2 -2
- package/src/config/bundled-skills/slack/TOOLS.json +8 -8
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +3 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -5
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +9 -9
- package/src/config/bundled-skills/transcribe/SKILL.md +5 -5
- package/src/config/bundled-skills/transcribe/TOOLS.json +1 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +10 -10
- package/src/config/bundled-skills/watcher/SKILL.md +4 -4
- package/src/config/bundled-skills/watcher/TOOLS.json +5 -5
- package/src/config/feature-flag-registry.json +33 -17
- package/src/config/schemas/sandbox.ts +1 -1
- package/src/config/schemas/services.ts +13 -3
- package/src/config/schemas/timeouts.ts +0 -10
- package/src/contacts/contact-store.ts +63 -0
- package/src/contacts/contacts-write.ts +1 -1
- package/src/daemon/assistant-attachments.ts +2 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -2
- package/src/daemon/conversation-agent-loop.ts +7 -30
- package/src/daemon/conversation-error.ts +24 -0
- package/src/daemon/conversation-memory.ts +8 -7
- package/src/daemon/conversation-runtime-assembly.ts +141 -275
- package/src/daemon/conversation-slash.ts +7 -26
- package/src/daemon/conversation-surfaces.ts +14 -0
- package/src/daemon/conversation-tool-setup.ts +9 -8
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/date-context.ts +10 -83
- package/src/daemon/handlers/config-channels.ts +12 -2
- package/src/daemon/handlers/config-slack-channel.ts +7 -1
- package/src/daemon/handlers/config-telegram.ts +6 -1
- package/src/daemon/handlers/conversations.ts +2 -2
- package/src/daemon/handlers/skills.ts +4 -0
- package/src/daemon/lifecycle.ts +28 -4
- package/src/daemon/providers-setup.ts +1 -1
- package/src/daemon/server.ts +1 -5
- package/src/daemon/shutdown-handlers.ts +9 -3
- package/src/daemon/tool-side-effects.ts +40 -0
- package/src/daemon/trace-emitter.ts +26 -2
- package/src/events/domain-events.ts +1 -1
- package/src/events/tool-permission-telemetry-listener.ts +46 -0
- package/src/inbound/platform-callback-registration.ts +0 -18
- package/src/media/app-icon-generator.ts +15 -8
- package/src/media/avatar-router.ts +15 -8
- package/src/media/gemini-image-service.ts +125 -21
- package/src/memory/attachments-store.ts +3 -3
- package/src/memory/channel-verification-sessions.ts +6 -6
- package/src/memory/conversation-crud.ts +196 -1
- package/src/memory/{thread-starters-cadence.ts → conversation-starters-cadence.ts} +9 -42
- package/src/memory/conversation-title-service.ts +2 -3
- package/src/memory/db-init.ts +25 -1
- package/src/memory/invite-store.ts +4 -4
- package/src/memory/items-extractor.ts +4 -4
- package/src/memory/job-handlers/{thread-starters.ts → conversation-starters.ts} +123 -38
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/jobs-worker.ts +7 -5
- package/src/memory/lifecycle-events-store.ts +63 -0
- package/src/memory/migrations/172-rename-created-by-session-id.ts +27 -0
- package/src/memory/migrations/173-rename-source-session-id.ts +16 -0
- package/src/memory/migrations/174-rename-thread-starters-table.ts +52 -0
- package/src/memory/migrations/175-create-lifecycle-events.ts +15 -0
- package/src/memory/migrations/176-drop-capability-card-state.ts +36 -0
- package/src/memory/migrations/177-create-trace-events-table.ts +40 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +13 -0
- package/src/memory/retriever.test.ts +223 -96
- package/src/memory/retriever.ts +115 -138
- package/src/memory/schema/calls.ts +1 -1
- package/src/memory/schema/contacts.ts +1 -1
- package/src/memory/schema/infrastructure.ts +29 -0
- package/src/memory/schema/memory-core.ts +7 -17
- package/src/memory/schema/notifications.ts +1 -1
- package/src/memory/search/formatting.ts +23 -6
- package/src/memory/search/lexical.ts +2 -0
- package/src/memory/search/semantic.ts +2 -0
- package/src/memory/search/staleness.ts +5 -1
- package/src/memory/search/types.ts +4 -0
- package/src/memory/task-memory-cleanup.ts +96 -6
- package/src/memory/trace-event-store.ts +148 -0
- package/src/notifications/README.md +1 -1
- package/src/notifications/decision-engine.ts +45 -4
- package/src/notifications/emit-signal.ts +5 -4
- package/src/notifications/events-store.ts +4 -4
- package/src/notifications/signal.ts +1 -1
- package/src/oauth/manual-token-connection.ts +49 -25
- package/src/permissions/checker.ts +6 -5
- package/src/permissions/defaults.ts +4 -4
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +9 -90
- package/src/prompts/cache-boundary.ts +8 -0
- package/src/prompts/system-prompt.ts +105 -634
- package/src/prompts/templates/BOOTSTRAP.md +172 -33
- package/src/prompts/templates/IDENTITY.md +8 -24
- package/src/prompts/templates/SOUL.md +20 -41
- package/src/prompts/templates/USER.md +3 -19
- package/src/prompts/user-reference.ts +14 -16
- package/src/providers/anthropic/client.ts +51 -19
- package/src/providers/gemini/client.ts +6 -9
- package/src/providers/managed-proxy/constants.ts +1 -7
- package/src/providers/managed-proxy/context.ts +0 -1
- package/src/providers/model-intents.ts +5 -5
- package/src/providers/openai/client.ts +10 -1
- package/src/providers/openrouter/client.ts +1 -0
- package/src/providers/ratelimit.ts +0 -35
- package/src/providers/registry.ts +3 -5
- package/src/providers/retry.ts +18 -1
- package/src/runtime/access-request-helper.ts +16 -2
- package/src/runtime/auth/route-policy.ts +7 -0
- package/src/runtime/channel-verification-service.ts +1 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
- package/src/runtime/guardian-vellum-migration.ts +61 -1
- package/src/runtime/http-server.ts +8 -4
- package/src/runtime/migrations/vbundle-builder.ts +212 -32
- package/src/runtime/migrations/vbundle-import-analyzer.ts +74 -8
- package/src/runtime/migrations/vbundle-importer.ts +66 -1
- package/src/runtime/migrations/vbundle-validator.ts +17 -3
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +4 -4
- package/src/runtime/routes/attachment-routes.ts +2 -2
- package/src/runtime/routes/btw-routes.ts +93 -0
- package/src/runtime/routes/channel-verification-routes.ts +19 -2
- package/src/runtime/routes/conversation-management-routes.ts +55 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +49 -5
- package/src/runtime/routes/conversation-starter-routes.ts +207 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +13 -9
- package/src/runtime/routes/identity-intro-cache.ts +105 -0
- package/src/runtime/routes/identity-routes.ts +51 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -1
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +1 -1
- package/src/runtime/routes/migration-routes.ts +25 -13
- package/src/runtime/routes/secret-routes.ts +18 -0
- package/src/runtime/routes/settings-routes.ts +9 -9
- package/src/runtime/routes/telemetry-routes.ts +53 -0
- package/src/runtime/routes/trace-event-routes.ts +62 -0
- package/src/runtime/tool-grant-request-helper.ts +1 -1
- package/src/runtime/verification-outbound-actions.ts +47 -31
- package/src/security/encrypted-store.ts +262 -78
- package/src/skills/catalog-install.ts +10 -0
- package/src/skills/managed-store.ts +2 -0
- package/src/skills/skill-memory.ts +222 -0
- package/src/subagent/manager.ts +1 -4
- package/src/telemetry/types.ts +10 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +7 -2
- package/src/telemetry/usage-telemetry-reporter.ts +53 -4
- package/src/tools/AGENTS.md +11 -11
- package/src/tools/acp/spawn.ts +1 -1
- package/src/tools/apps/executors.ts +8 -8
- package/src/tools/apps/registry.ts +1 -1
- package/src/tools/assets/materialize.ts +6 -6
- package/src/tools/assets/search.ts +10 -10
- package/src/tools/browser/__tests__/auth-cache.test.ts +2 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +4 -4
- package/src/tools/browser/auth-detector.ts +6 -6
- package/src/tools/browser/browser-execution.ts +13 -13
- package/src/tools/browser/browser-manager.ts +3 -3
- package/src/tools/browser/chrome-cdp.ts +5 -5
- package/src/tools/browser/jit-auth.ts +2 -2
- package/src/tools/browser/network-recorder.test.ts +2 -2
- package/src/tools/browser/network-recorder.ts +3 -3
- package/src/tools/browser/runtime-check.ts +3 -3
- package/src/tools/claude-code/claude-code.ts +2 -2
- package/src/tools/computer-use/definitions.ts +18 -18
- package/src/tools/credential-execution/make-authenticated-request.ts +4 -4
- package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -3
- package/src/tools/credential-execution/run-authenticated-command.ts +4 -4
- package/src/tools/credentials/broker-types.ts +5 -5
- package/src/tools/credentials/broker.ts +15 -15
- package/src/tools/credentials/metadata-store.ts +2 -2
- package/src/tools/credentials/resolve.ts +1 -1
- package/src/tools/credentials/selection.ts +1 -1
- package/src/tools/credentials/tool-policy.ts +1 -1
- package/src/tools/credentials/vault.ts +115 -25
- package/src/tools/execution-target.ts +2 -2
- package/src/tools/executor.ts +7 -7
- package/src/tools/filesystem/edit.ts +2 -2
- package/src/tools/filesystem/read.ts +15 -4
- package/src/tools/filesystem/write.ts +1 -1
- package/src/tools/host-filesystem/edit.ts +2 -1
- package/src/tools/host-filesystem/read.ts +18 -1
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +9 -8
- package/src/tools/mcp/mcp-tool-factory.ts +7 -6
- package/src/tools/memory/definitions.ts +6 -5
- package/src/tools/memory/handlers.test.ts +1 -1
- package/src/tools/network/__tests__/web-search.test.ts +3 -3
- package/src/tools/network/domain-normalize.ts +2 -2
- package/src/tools/network/script-proxy/session-manager.ts +10 -10
- package/src/tools/network/web-fetch.ts +1 -1
- package/src/tools/network/web-search.ts +3 -3
- package/src/tools/permission-checker.ts +8 -8
- package/src/tools/registry.ts +7 -7
- package/src/tools/schedule/list.ts +2 -2
- package/src/tools/schema-transforms.ts +31 -21
- package/src/tools/secret-detection-handler.ts +1 -1
- package/src/tools/sensitive-output-placeholders.ts +1 -1
- package/src/tools/shared/filesystem/edit-engine.ts +1 -1
- package/src/tools/shared/filesystem/file-ops-service.ts +3 -3
- package/src/tools/shared/filesystem/image-read.ts +25 -5
- package/src/tools/shared/filesystem/path-policy.ts +2 -2
- package/src/tools/shared/shell-output.ts +1 -1
- package/src/tools/side-effects.ts +1 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/load.ts +3 -3
- package/src/tools/skills/sandbox-runner.ts +3 -3
- package/src/tools/subagent/read.ts +1 -1
- package/src/tools/subagent/spawn.ts +2 -2
- package/src/tools/swarm/delegate.ts +3 -3
- package/src/tools/system/request-permission.ts +5 -4
- package/src/tools/terminal/backends/native.ts +4 -4
- package/src/tools/terminal/parser.ts +6 -6
- package/src/tools/terminal/sandbox-diagnostics.ts +1 -1
- package/src/tools/terminal/shell.ts +16 -16
- package/src/tools/tool-approval-handler.ts +21 -12
- package/src/tools/tool-manifest.ts +4 -4
- package/src/tools/types.ts +3 -3
- package/src/tools/ui-surface/definitions.ts +9 -37
- package/src/tools/watcher/list.ts +1 -1
- package/src/util/logger.ts +7 -2
- package/src/util/pricing.ts +4 -0
- package/src/util/retry.ts +29 -1
- package/src/workspace/migrations/007-web-search-provider-rename.ts +37 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/cli-help-reference-sync.test.ts +0 -26
- package/src/__tests__/onboarding-starter-tasks.test.ts +0 -190
- package/src/cli/reference.ts +0 -38
- package/src/memory/job-handlers/capability-cards.ts +0 -420
- package/src/runtime/routes/thread-starter-routes.ts +0 -294
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import { migrateDropCapabilityCardState } from "../memory/migrations/176-drop-capability-card-state.js";
|
|
7
|
+
import * as schema from "../memory/schema.js";
|
|
8
|
+
|
|
9
|
+
function createTestDb() {
|
|
10
|
+
const sqlite = new Database(":memory:");
|
|
11
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
12
|
+
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
13
|
+
return drizzle(sqlite, { schema });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type TestDb = ReturnType<typeof createTestDb>;
|
|
17
|
+
|
|
18
|
+
function getRawSqlite(db: TestDb): Database {
|
|
19
|
+
return (db as unknown as { $client: Database }).$client;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createLegacyCapabilityCardTables(raw: Database) {
|
|
23
|
+
raw.exec(/*sql*/ `
|
|
24
|
+
CREATE TABLE memory_checkpoints (
|
|
25
|
+
key TEXT PRIMARY KEY,
|
|
26
|
+
value TEXT NOT NULL,
|
|
27
|
+
updated_at INTEGER NOT NULL
|
|
28
|
+
)
|
|
29
|
+
`);
|
|
30
|
+
|
|
31
|
+
raw.exec(/*sql*/ `
|
|
32
|
+
CREATE TABLE memory_jobs (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
type TEXT NOT NULL,
|
|
35
|
+
payload TEXT NOT NULL,
|
|
36
|
+
status TEXT NOT NULL,
|
|
37
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
deferrals INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
run_after INTEGER NOT NULL,
|
|
40
|
+
last_error TEXT,
|
|
41
|
+
started_at INTEGER,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
updated_at INTEGER NOT NULL
|
|
44
|
+
)
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
raw.exec(/*sql*/ `
|
|
48
|
+
CREATE TABLE conversation_starters (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
label TEXT NOT NULL,
|
|
51
|
+
prompt TEXT NOT NULL,
|
|
52
|
+
generation_batch INTEGER NOT NULL,
|
|
53
|
+
scope_id TEXT NOT NULL DEFAULT 'default',
|
|
54
|
+
card_type TEXT NOT NULL DEFAULT 'chip',
|
|
55
|
+
created_at INTEGER NOT NULL
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
|
|
59
|
+
raw.exec(/*sql*/ `
|
|
60
|
+
CREATE TABLE capability_card_categories (
|
|
61
|
+
scope_id TEXT NOT NULL,
|
|
62
|
+
category TEXT NOT NULL,
|
|
63
|
+
relevance REAL NOT NULL,
|
|
64
|
+
generation_batch INTEGER NOT NULL,
|
|
65
|
+
created_at INTEGER NOT NULL,
|
|
66
|
+
PRIMARY KEY (scope_id, category)
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("migrateDropCapabilityCardState", () => {
|
|
72
|
+
test("removes legacy capability-card rows, jobs, checkpoints, and table", () => {
|
|
73
|
+
const db = createTestDb();
|
|
74
|
+
const raw = getRawSqlite(db);
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
|
|
77
|
+
createLegacyCapabilityCardTables(raw);
|
|
78
|
+
|
|
79
|
+
raw.exec(/*sql*/ `
|
|
80
|
+
INSERT INTO conversation_starters (id, label, prompt, generation_batch, scope_id, card_type, created_at)
|
|
81
|
+
VALUES
|
|
82
|
+
('chip-1', 'Prep tomorrow', 'Prep tomorrow', 1, 'default', 'chip', ${now}),
|
|
83
|
+
('card-1', 'Do this first', 'Do this first', 1, 'default', 'card', ${now})
|
|
84
|
+
`);
|
|
85
|
+
raw.exec(/*sql*/ `
|
|
86
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, created_at, updated_at)
|
|
87
|
+
VALUES
|
|
88
|
+
('job-chip', 'generate_conversation_starters', '{}', 'pending', 0, 0, ${now}, ${now}, ${now}),
|
|
89
|
+
('job-card', 'generate_capability_cards', '{}', 'pending', 0, 0, ${now}, ${now}, ${now})
|
|
90
|
+
`);
|
|
91
|
+
raw.exec(/*sql*/ `
|
|
92
|
+
INSERT INTO memory_checkpoints (key, value, updated_at)
|
|
93
|
+
VALUES
|
|
94
|
+
('capability_cards:generation_batch', '7', ${now}),
|
|
95
|
+
('other_checkpoint', '1', ${now})
|
|
96
|
+
`);
|
|
97
|
+
raw.exec(/*sql*/ `
|
|
98
|
+
INSERT INTO capability_card_categories (scope_id, category, relevance, generation_batch, created_at)
|
|
99
|
+
VALUES ('default', 'communication', 0.9, 1, ${now})
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
migrateDropCapabilityCardState(db);
|
|
103
|
+
|
|
104
|
+
const starterRows = raw
|
|
105
|
+
.query(`SELECT id, card_type FROM conversation_starters ORDER BY id`)
|
|
106
|
+
.all() as Array<{ id: string; card_type: string }>;
|
|
107
|
+
expect(starterRows).toEqual([{ id: "chip-1", card_type: "chip" }]);
|
|
108
|
+
|
|
109
|
+
const jobTypes = raw
|
|
110
|
+
.query(`SELECT type FROM memory_jobs ORDER BY id`)
|
|
111
|
+
.all() as Array<{
|
|
112
|
+
type: string;
|
|
113
|
+
}>;
|
|
114
|
+
expect(jobTypes).toEqual([{ type: "generate_conversation_starters" }]);
|
|
115
|
+
|
|
116
|
+
const checkpointKeys = raw
|
|
117
|
+
.query(`SELECT key FROM memory_checkpoints ORDER BY key`)
|
|
118
|
+
.all() as Array<{ key: string }>;
|
|
119
|
+
expect(checkpointKeys.map((row) => row.key)).toEqual([
|
|
120
|
+
"migration_drop_capability_card_state_v1",
|
|
121
|
+
"other_checkpoint",
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const migrationCheckpoint = raw
|
|
125
|
+
.query(
|
|
126
|
+
`SELECT value FROM memory_checkpoints WHERE key = 'migration_drop_capability_card_state_v1'`,
|
|
127
|
+
)
|
|
128
|
+
.get() as { value: string } | null;
|
|
129
|
+
expect(migrationCheckpoint?.value).toBe("1");
|
|
130
|
+
|
|
131
|
+
const legacyTable = raw
|
|
132
|
+
.query(
|
|
133
|
+
`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'capability_card_categories'`,
|
|
134
|
+
)
|
|
135
|
+
.get();
|
|
136
|
+
expect(legacyTable).toBeNull();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("is idempotent when run more than once", () => {
|
|
140
|
+
const db = createTestDb();
|
|
141
|
+
const raw = getRawSqlite(db);
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
|
|
144
|
+
createLegacyCapabilityCardTables(raw);
|
|
145
|
+
raw.exec(/*sql*/ `
|
|
146
|
+
INSERT INTO conversation_starters (id, label, prompt, generation_batch, scope_id, card_type, created_at)
|
|
147
|
+
VALUES ('card-1', 'Do this first', 'Do this first', 1, 'default', 'card', ${now})
|
|
148
|
+
`);
|
|
149
|
+
raw.exec(/*sql*/ `
|
|
150
|
+
INSERT INTO memory_jobs (id, type, payload, status, attempts, deferrals, run_after, created_at, updated_at)
|
|
151
|
+
VALUES ('job-card', 'generate_capability_cards', '{}', 'pending', 0, 0, ${now}, ${now}, ${now})
|
|
152
|
+
`);
|
|
153
|
+
|
|
154
|
+
migrateDropCapabilityCardState(db);
|
|
155
|
+
migrateDropCapabilityCardState(db);
|
|
156
|
+
|
|
157
|
+
const starterCount = raw
|
|
158
|
+
.query(`SELECT COUNT(*) AS count FROM conversation_starters`)
|
|
159
|
+
.get() as { count: number };
|
|
160
|
+
expect(starterCount.count).toBe(0);
|
|
161
|
+
|
|
162
|
+
const jobCount = raw
|
|
163
|
+
.query(
|
|
164
|
+
`SELECT COUNT(*) AS count FROM memory_jobs WHERE type = 'generate_capability_cards'`,
|
|
165
|
+
)
|
|
166
|
+
.get() as { count: number };
|
|
167
|
+
expect(jobCount.count).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -53,7 +53,6 @@ mock.module("../util/logger.js", () => ({
|
|
|
53
53
|
|
|
54
54
|
mock.module("../config/loader.js", () => ({
|
|
55
55
|
getConfig: () => ({
|
|
56
|
-
sandbox: { enabled: false, backend: "native" },
|
|
57
56
|
assistantFeatureFlagValues: {
|
|
58
57
|
"feature_flags.browser.enabled": true,
|
|
59
58
|
},
|
|
@@ -66,9 +65,9 @@ mock.module("../config/loader.js", () => ({
|
|
|
66
65
|
"image-generation": {
|
|
67
66
|
mode: "your-own",
|
|
68
67
|
provider: "gemini",
|
|
69
|
-
model: "gemini-
|
|
68
|
+
model: "gemini-3.1-flash-image-preview",
|
|
70
69
|
},
|
|
71
|
-
"web-search": { mode: "your-own", provider: "
|
|
70
|
+
"web-search": { mode: "your-own", provider: "inference-provider-native" },
|
|
72
71
|
},
|
|
73
72
|
}),
|
|
74
73
|
loadConfig: () => ({}),
|
|
@@ -83,7 +82,7 @@ mock.module("../config/loader.js", () => ({
|
|
|
83
82
|
|
|
84
83
|
const { buildSystemPrompt } = await import("../prompts/system-prompt.js");
|
|
85
84
|
|
|
86
|
-
describe("Dynamic Skill Authoring Workflow
|
|
85
|
+
describe("Dynamic Skill Authoring Workflow moved to tool descriptions", () => {
|
|
87
86
|
beforeEach(() => {
|
|
88
87
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
89
88
|
});
|
|
@@ -94,36 +93,11 @@ describe("Dynamic Skill Authoring Workflow prompt section", () => {
|
|
|
94
93
|
}
|
|
95
94
|
});
|
|
96
95
|
|
|
97
|
-
test("
|
|
96
|
+
test("system prompt no longer contains Dynamic Skill Authoring section", () => {
|
|
98
97
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
99
98
|
const result = buildSystemPrompt();
|
|
100
|
-
expect(result).toContain("## Dynamic Skill Authoring Workflow");
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
test("workflow section mentions scaffold and delete tools and bun run workflow", () => {
|
|
104
|
-
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
105
|
-
const result = buildSystemPrompt();
|
|
106
|
-
expect(result).toContain("bun run /tmp/vellum-eval/snippet.ts");
|
|
107
|
-
expect(result).toContain("scaffold_managed_skill");
|
|
108
|
-
expect(result).toContain("delete_managed_skill");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("workflow section includes user confirmation warning", () => {
|
|
112
|
-
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
113
|
-
const result = buildSystemPrompt();
|
|
114
|
-
expect(result).toContain("explicit user confirmation");
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test("workflow section includes retry limit guidance", () => {
|
|
118
|
-
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
119
|
-
const result = buildSystemPrompt();
|
|
120
|
-
expect(result).toContain("3 attempts");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("workflow section includes conversation eviction note", () => {
|
|
124
|
-
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
125
|
-
const result = buildSystemPrompt();
|
|
126
|
-
expect(result).toContain("recreated session");
|
|
99
|
+
expect(result).not.toContain("## Dynamic Skill Authoring Workflow");
|
|
100
|
+
expect(result).not.toContain("### Community Skills Discovery");
|
|
127
101
|
});
|
|
128
102
|
|
|
129
103
|
test("prompt still includes available skills catalog when skills exist", () => {
|
|
@@ -138,8 +112,7 @@ describe("Dynamic Skill Authoring Workflow prompt section", () => {
|
|
|
138
112
|
|
|
139
113
|
const result = buildSystemPrompt();
|
|
140
114
|
expect(result).toContain("## Available Skills");
|
|
141
|
-
expect(result).toContain(
|
|
142
|
-
expect(result).toContain("## Dynamic Skill Authoring Workflow");
|
|
115
|
+
expect(result).toContain("**test-skill**");
|
|
143
116
|
});
|
|
144
117
|
|
|
145
118
|
test("prompt is additive with IDENTITY/SOUL/USER files", () => {
|
|
@@ -151,21 +124,14 @@ describe("Dynamic Skill Authoring Workflow prompt section", () => {
|
|
|
151
124
|
expect(result).toContain("Identity here");
|
|
152
125
|
expect(result).toContain("Soul here");
|
|
153
126
|
expect(result).toContain("User here");
|
|
154
|
-
expect(result).toContain("## Dynamic Skill Authoring Workflow");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("workflow section includes skill_load instruction", () => {
|
|
158
|
-
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
159
|
-
const result = buildSystemPrompt();
|
|
160
|
-
expect(result).toContain("skill_load");
|
|
161
127
|
});
|
|
162
128
|
|
|
163
|
-
test("browser skill has activation hints in
|
|
129
|
+
test("browser skill has activation hints in skills catalog instead of dedicated section", () => {
|
|
164
130
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
165
131
|
const result = buildSystemPrompt();
|
|
166
|
-
// Browser routing moved from dedicated section to
|
|
132
|
+
// Browser routing moved from dedicated section to inline hints in catalog bullet
|
|
167
133
|
expect(result).not.toContain("Browser Skill Prerequisite");
|
|
168
|
-
expect(result).toContain(
|
|
169
|
-
expect(result).toContain("
|
|
134
|
+
expect(result).toContain("**browser**");
|
|
135
|
+
expect(result).toContain("Load first if you need browser_* tools");
|
|
170
136
|
});
|
|
171
137
|
});
|
|
@@ -116,7 +116,7 @@ describe("emitNotificationSignal routing intent re-persistence", () => {
|
|
|
116
116
|
const result = await emitNotificationSignal({
|
|
117
117
|
sourceEventName: "schedule.notify",
|
|
118
118
|
sourceChannel: "scheduler",
|
|
119
|
-
|
|
119
|
+
sourceContextId: "rem-1",
|
|
120
120
|
attentionHints: {
|
|
121
121
|
requiresAction: true,
|
|
122
122
|
urgency: "high",
|
|
@@ -162,7 +162,7 @@ describe("emitNotificationSignal routing intent re-persistence", () => {
|
|
|
162
162
|
await emitNotificationSignal({
|
|
163
163
|
sourceEventName: "schedule.notify",
|
|
164
164
|
sourceChannel: "scheduler",
|
|
165
|
-
|
|
165
|
+
sourceContextId: "rem-2",
|
|
166
166
|
attentionHints: {
|
|
167
167
|
requiresAction: false,
|
|
168
168
|
urgency: "medium",
|
|
@@ -198,7 +198,7 @@ describe("emitNotificationSignal routing intent re-persistence", () => {
|
|
|
198
198
|
await emitNotificationSignal({
|
|
199
199
|
sourceEventName: "schedule.notify",
|
|
200
200
|
sourceChannel: "scheduler",
|
|
201
|
-
|
|
201
|
+
sourceContextId: "rem-3",
|
|
202
202
|
attentionHints: {
|
|
203
203
|
requiresAction: false,
|
|
204
204
|
urgency: "medium",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { createCipheriv, pbkdf2Sync, randomBytes } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
4
|
existsSync,
|
|
@@ -7,9 +7,10 @@ import {
|
|
|
7
7
|
readFileSync,
|
|
8
8
|
rmSync,
|
|
9
9
|
statSync,
|
|
10
|
+
unlinkSync,
|
|
10
11
|
writeFileSync,
|
|
11
12
|
} from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
+
import { hostname, tmpdir, userInfo } from "node:os";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
import {
|
|
15
16
|
afterAll,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
} from "bun:test";
|
|
24
25
|
|
|
25
26
|
// ---------------------------------------------------------------------------
|
|
26
|
-
// Mock only the logger (not platform
|
|
27
|
+
// Mock only the logger (not platform -- we use _setStorePath instead)
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
|
|
29
30
|
mock.module("../util/logger.js", () => ({
|
|
@@ -34,6 +35,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
34
35
|
}));
|
|
35
36
|
|
|
36
37
|
import {
|
|
38
|
+
_setStoreKeyPath,
|
|
37
39
|
_setStorePath,
|
|
38
40
|
deleteKey,
|
|
39
41
|
getKey,
|
|
@@ -50,6 +52,80 @@ const TEST_DIR = join(
|
|
|
50
52
|
`vellum-enc-test-${randomBytes(4).toString("hex")}`,
|
|
51
53
|
);
|
|
52
54
|
const STORE_PATH = join(TEST_DIR, "keys.enc");
|
|
55
|
+
const STORE_KEY_PATH = join(TEST_DIR, "store.key");
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Legacy v1 helpers (for migration tests)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
const ALGORITHM = "aes-256-gcm";
|
|
62
|
+
const KEY_LENGTH = 32;
|
|
63
|
+
const IV_LENGTH = 16;
|
|
64
|
+
const AUTH_TAG_LENGTH = 16;
|
|
65
|
+
const PBKDF2_ITERATIONS = process.env.BUN_TEST === "1" ? 1 : 100_000;
|
|
66
|
+
const SALT_LENGTH = 32;
|
|
67
|
+
|
|
68
|
+
/** Local copy of the legacy machine entropy derivation (the export was removed). */
|
|
69
|
+
function legacyMachineEntropy(): string {
|
|
70
|
+
const parts: string[] = [];
|
|
71
|
+
try {
|
|
72
|
+
parts.push(hostname());
|
|
73
|
+
} catch {
|
|
74
|
+
parts.push("unknown-host");
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
parts.push(userInfo().username);
|
|
78
|
+
} catch {
|
|
79
|
+
parts.push("unknown-user");
|
|
80
|
+
}
|
|
81
|
+
parts.push(process.platform);
|
|
82
|
+
parts.push(process.arch);
|
|
83
|
+
try {
|
|
84
|
+
parts.push(userInfo().homedir);
|
|
85
|
+
} catch {
|
|
86
|
+
parts.push("/tmp");
|
|
87
|
+
}
|
|
88
|
+
return parts.join(":");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function legacyDeriveKey(salt: Buffer): Buffer {
|
|
92
|
+
const entropy = legacyMachineEntropy();
|
|
93
|
+
return pbkdf2Sync(entropy, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha512");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function legacyEncrypt(
|
|
97
|
+
plaintext: string,
|
|
98
|
+
key: Buffer,
|
|
99
|
+
): { iv: string; tag: string; data: string } {
|
|
100
|
+
const iv = randomBytes(IV_LENGTH);
|
|
101
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, {
|
|
102
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
103
|
+
});
|
|
104
|
+
const encrypted = Buffer.concat([
|
|
105
|
+
cipher.update(plaintext, "utf-8"),
|
|
106
|
+
cipher.final(),
|
|
107
|
+
]);
|
|
108
|
+
const tag = cipher.getAuthTag();
|
|
109
|
+
return {
|
|
110
|
+
iv: iv.toString("hex"),
|
|
111
|
+
tag: tag.toString("hex"),
|
|
112
|
+
data: encrypted.toString("hex"),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Write a v1 store file directly (bypassing the module's writeStore). */
|
|
117
|
+
function writeV1Store(
|
|
118
|
+
path: string,
|
|
119
|
+
salt: Buffer,
|
|
120
|
+
entries: Record<string, { iv: string; tag: string; data: string }>,
|
|
121
|
+
): void {
|
|
122
|
+
const store = {
|
|
123
|
+
version: 1,
|
|
124
|
+
salt: salt.toString("hex"),
|
|
125
|
+
entries,
|
|
126
|
+
};
|
|
127
|
+
writeFileSync(path, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
128
|
+
}
|
|
53
129
|
|
|
54
130
|
// ---------------------------------------------------------------------------
|
|
55
131
|
// Tests
|
|
@@ -66,10 +142,12 @@ describe("encrypted-store", () => {
|
|
|
66
142
|
rmSync(join(TEST_DIR, entry), { recursive: true, force: true });
|
|
67
143
|
}
|
|
68
144
|
_setStorePath(STORE_PATH);
|
|
145
|
+
_setStoreKeyPath(STORE_KEY_PATH);
|
|
69
146
|
});
|
|
70
147
|
|
|
71
148
|
afterEach(() => {
|
|
72
149
|
_setStorePath(null);
|
|
150
|
+
_setStoreKeyPath(null);
|
|
73
151
|
});
|
|
74
152
|
|
|
75
153
|
afterAll(() => {
|
|
@@ -173,16 +251,15 @@ describe("encrypted-store", () => {
|
|
|
173
251
|
});
|
|
174
252
|
|
|
175
253
|
// -----------------------------------------------------------------------
|
|
176
|
-
// Store format
|
|
254
|
+
// Store format (v2)
|
|
177
255
|
// -----------------------------------------------------------------------
|
|
178
256
|
describe("store format", () => {
|
|
179
|
-
test("store file is valid JSON with version
|
|
257
|
+
test("store file is valid JSON with version 2 and entries (no salt)", () => {
|
|
180
258
|
setKey("test", "value");
|
|
181
259
|
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
182
260
|
const parsed = JSON.parse(raw);
|
|
183
|
-
expect(parsed.version).toBe(
|
|
184
|
-
expect(
|
|
185
|
-
expect(parsed.salt.length).toBe(64); // 32 bytes = 64 hex chars
|
|
261
|
+
expect(parsed.version).toBe(2);
|
|
262
|
+
expect(parsed.salt).toBeUndefined();
|
|
186
263
|
expect(typeof parsed.entries).toBe("object");
|
|
187
264
|
expect(parsed.entries.test).toBeDefined();
|
|
188
265
|
});
|
|
@@ -223,6 +300,163 @@ describe("encrypted-store", () => {
|
|
|
223
300
|
});
|
|
224
301
|
});
|
|
225
302
|
|
|
303
|
+
// -----------------------------------------------------------------------
|
|
304
|
+
// v2 format and store.key
|
|
305
|
+
// -----------------------------------------------------------------------
|
|
306
|
+
describe("v2 format and store.key", () => {
|
|
307
|
+
test("fresh store creates v2 format and store.key", () => {
|
|
308
|
+
setKey("test", "value");
|
|
309
|
+
|
|
310
|
+
// Verify keys.enc is v2 with no salt
|
|
311
|
+
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
312
|
+
const parsed = JSON.parse(raw);
|
|
313
|
+
expect(parsed.version).toBe(2);
|
|
314
|
+
expect(parsed.salt).toBeUndefined();
|
|
315
|
+
|
|
316
|
+
// Verify store.key exists with exactly 32 bytes and 0o600 perms
|
|
317
|
+
expect(existsSync(STORE_KEY_PATH)).toBe(true);
|
|
318
|
+
const keyBuf = readFileSync(STORE_KEY_PATH);
|
|
319
|
+
expect(keyBuf.length).toBe(32);
|
|
320
|
+
const mode = statSync(STORE_KEY_PATH).mode & 0o777;
|
|
321
|
+
expect(mode).toBe(0o600);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("v2 round-trip", () => {
|
|
325
|
+
// Set multiple values and read them back
|
|
326
|
+
setKey("key-a", "value-a");
|
|
327
|
+
setKey("key-b", "value-b");
|
|
328
|
+
setKey("key-c", "value-c");
|
|
329
|
+
|
|
330
|
+
expect(getKey("key-a")).toBe("value-a");
|
|
331
|
+
expect(getKey("key-b")).toBe("value-b");
|
|
332
|
+
expect(getKey("key-c")).toBe("value-c");
|
|
333
|
+
|
|
334
|
+
// Delete one and verify others remain
|
|
335
|
+
deleteKey("key-b");
|
|
336
|
+
expect(getKey("key-b")).toBeUndefined();
|
|
337
|
+
expect(getKey("key-a")).toBe("value-a");
|
|
338
|
+
expect(getKey("key-c")).toBe("value-c");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("v2 store without store.key returns undefined", () => {
|
|
342
|
+
setKey("test", "value");
|
|
343
|
+
// Delete store.key
|
|
344
|
+
unlinkSync(STORE_KEY_PATH);
|
|
345
|
+
expect(getKey("test")).toBeUndefined();
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// -----------------------------------------------------------------------
|
|
350
|
+
// v1 -> v2 migration
|
|
351
|
+
// -----------------------------------------------------------------------
|
|
352
|
+
describe("v1 to v2 migration", () => {
|
|
353
|
+
test("v1 to v2 migration preserves entries", () => {
|
|
354
|
+
// Create a v1 store using legacy encryption
|
|
355
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
356
|
+
const legacyKey = legacyDeriveKey(salt);
|
|
357
|
+
const entries: Record<string, { iv: string; tag: string; data: string }> =
|
|
358
|
+
{};
|
|
359
|
+
entries["api-key"] = legacyEncrypt("secret-123", legacyKey);
|
|
360
|
+
entries["other-key"] = legacyEncrypt("secret-456", legacyKey);
|
|
361
|
+
writeV1Store(STORE_PATH, salt, entries);
|
|
362
|
+
|
|
363
|
+
// Access via getKey -- should trigger migration
|
|
364
|
+
const val1 = getKey("api-key");
|
|
365
|
+
expect(val1).toBe("secret-123");
|
|
366
|
+
|
|
367
|
+
const val2 = getKey("other-key");
|
|
368
|
+
expect(val2).toBe("secret-456");
|
|
369
|
+
|
|
370
|
+
// Store should now be v2
|
|
371
|
+
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
372
|
+
const parsed = JSON.parse(raw);
|
|
373
|
+
expect(parsed.version).toBe(2);
|
|
374
|
+
expect(parsed.salt).toBeUndefined();
|
|
375
|
+
|
|
376
|
+
// store.key should exist
|
|
377
|
+
expect(existsSync(STORE_KEY_PATH)).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("migration is idempotent", () => {
|
|
381
|
+
// Create a v1 store
|
|
382
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
383
|
+
const legacyKey = legacyDeriveKey(salt);
|
|
384
|
+
const entries: Record<string, { iv: string; tag: string; data: string }> =
|
|
385
|
+
{};
|
|
386
|
+
entries["my-key"] = legacyEncrypt("my-value", legacyKey);
|
|
387
|
+
writeV1Store(STORE_PATH, salt, entries);
|
|
388
|
+
|
|
389
|
+
// First access triggers migration
|
|
390
|
+
const val1 = getKey("my-key");
|
|
391
|
+
expect(val1).toBe("my-value");
|
|
392
|
+
|
|
393
|
+
// Second access should work the same (already migrated)
|
|
394
|
+
const val2 = getKey("my-key");
|
|
395
|
+
expect(val2).toBe("my-value");
|
|
396
|
+
|
|
397
|
+
// Should still be v2
|
|
398
|
+
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
399
|
+
expect(JSON.parse(raw).version).toBe(2);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("migration skips corrupt entries", () => {
|
|
403
|
+
// Create a v1 store with one good entry and one tampered entry
|
|
404
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
405
|
+
const legacyKey = legacyDeriveKey(salt);
|
|
406
|
+
const entries: Record<string, { iv: string; tag: string; data: string }> =
|
|
407
|
+
{};
|
|
408
|
+
entries["good"] = legacyEncrypt("good-value", legacyKey);
|
|
409
|
+
entries["bad"] = legacyEncrypt("bad-value", legacyKey);
|
|
410
|
+
// Tamper with the bad entry
|
|
411
|
+
const badData = entries["bad"].data;
|
|
412
|
+
entries["bad"].data =
|
|
413
|
+
badData[0] === "0" ? "1" + badData.slice(1) : "0" + badData.slice(1);
|
|
414
|
+
writeV1Store(STORE_PATH, salt, entries);
|
|
415
|
+
|
|
416
|
+
// Trigger migration via getKey
|
|
417
|
+
const goodVal = getKey("good");
|
|
418
|
+
expect(goodVal).toBe("good-value");
|
|
419
|
+
|
|
420
|
+
// Bad entry should be gone (not migrated)
|
|
421
|
+
const badVal = getKey("bad");
|
|
422
|
+
expect(badVal).toBeUndefined();
|
|
423
|
+
|
|
424
|
+
// Store should be v2
|
|
425
|
+
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
426
|
+
const parsed = JSON.parse(raw);
|
|
427
|
+
expect(parsed.version).toBe(2);
|
|
428
|
+
expect(parsed.entries["good"]).toBeDefined();
|
|
429
|
+
expect(parsed.entries["bad"]).toBeUndefined();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("partial migration recovery", () => {
|
|
433
|
+
// Simulate crash between store.key write and store rewrite:
|
|
434
|
+
// write v1 store + store.key but leave store as v1
|
|
435
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
436
|
+
const legacyKey = legacyDeriveKey(salt);
|
|
437
|
+
const entries: Record<string, { iv: string; tag: string; data: string }> =
|
|
438
|
+
{};
|
|
439
|
+
entries["test-key"] = legacyEncrypt("test-value", legacyKey);
|
|
440
|
+
writeV1Store(STORE_PATH, salt, entries);
|
|
441
|
+
|
|
442
|
+
// Pre-write store.key (simulating partial migration)
|
|
443
|
+
const preKey = randomBytes(32);
|
|
444
|
+
writeFileSync(STORE_KEY_PATH, preKey, { mode: 0o600 });
|
|
445
|
+
|
|
446
|
+
// Migration should complete using the existing store.key
|
|
447
|
+
const val = getKey("test-key");
|
|
448
|
+
expect(val).toBe("test-value");
|
|
449
|
+
|
|
450
|
+
// Verify the store.key was reused (not regenerated)
|
|
451
|
+
const afterKey = readFileSync(STORE_KEY_PATH);
|
|
452
|
+
expect(afterKey.equals(preKey)).toBe(true);
|
|
453
|
+
|
|
454
|
+
// Store should now be v2
|
|
455
|
+
const raw = readFileSync(STORE_PATH, "utf-8");
|
|
456
|
+
expect(JSON.parse(raw).version).toBe(2);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
226
460
|
// -----------------------------------------------------------------------
|
|
227
461
|
// Error handling
|
|
228
462
|
// -----------------------------------------------------------------------
|
|
@@ -273,7 +507,9 @@ describe("encrypted-store", () => {
|
|
|
273
507
|
test("setKey creates directory if missing", () => {
|
|
274
508
|
// Point to a path in a non-existent subdirectory
|
|
275
509
|
const nestedPath = join(TEST_DIR, "sub", "dir", "keys.enc");
|
|
510
|
+
const nestedKeyPath = join(TEST_DIR, "sub", "dir", "store.key");
|
|
276
511
|
_setStorePath(nestedPath);
|
|
512
|
+
_setStoreKeyPath(nestedKeyPath);
|
|
277
513
|
const result = setKey("test", "value");
|
|
278
514
|
expect(result).toBe(true);
|
|
279
515
|
expect(getKey("test")).toBe("value");
|
|
@@ -311,7 +547,7 @@ describe("encrypted-store", () => {
|
|
|
311
547
|
setKey("test", "value");
|
|
312
548
|
// Loosen permissions
|
|
313
549
|
chmodSync(STORE_PATH, 0o644);
|
|
314
|
-
// Write again
|
|
550
|
+
// Write again -- should re-enforce 0600
|
|
315
551
|
setKey("test2", "value2");
|
|
316
552
|
const mode = statSync(STORE_PATH).mode & 0o777;
|
|
317
553
|
expect(mode).toBe(0o600);
|
|
@@ -352,16 +588,14 @@ describe("encrypted-store", () => {
|
|
|
352
588
|
expect(getKey("__proto__")).toBeUndefined();
|
|
353
589
|
});
|
|
354
590
|
|
|
355
|
-
test("
|
|
591
|
+
test("store.key is reused across set operations", () => {
|
|
356
592
|
setKey("key1", "val1");
|
|
357
|
-
const
|
|
358
|
-
const salt1 = JSON.parse(raw1).salt;
|
|
593
|
+
const key1 = readFileSync(STORE_KEY_PATH);
|
|
359
594
|
|
|
360
595
|
setKey("key2", "val2");
|
|
361
|
-
const
|
|
362
|
-
const salt2 = JSON.parse(raw2).salt;
|
|
596
|
+
const key2 = readFileSync(STORE_KEY_PATH);
|
|
363
597
|
|
|
364
|
-
expect(
|
|
598
|
+
expect(key1.equals(key2)).toBe(true);
|
|
365
599
|
});
|
|
366
600
|
});
|
|
367
601
|
});
|
|
@@ -328,20 +328,19 @@ describe("ephemeral-permissions", () => {
|
|
|
328
328
|
testConfig.permissions.mode = "workspace";
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
-
test("workspace mode
|
|
331
|
+
test("workspace mode auto-allows workspace-scoped file_write (low risk)", async () => {
|
|
332
332
|
const filePath = join(testDir, "workspace-test-file.txt");
|
|
333
333
|
const result = await check("file_write", { path: filePath }, testDir);
|
|
334
|
-
expect(result.decision).toBe("
|
|
335
|
-
expect(result.reason).toContain("medium risk");
|
|
334
|
+
expect(result.decision).toBe("allow");
|
|
336
335
|
});
|
|
337
336
|
|
|
338
|
-
test("workspace mode
|
|
337
|
+
test("workspace mode auto-allows file_write outside workspace (low risk)", async () => {
|
|
339
338
|
const result = await check(
|
|
340
339
|
"file_write",
|
|
341
340
|
{ path: "/etc/config" },
|
|
342
341
|
testDir,
|
|
343
342
|
);
|
|
344
|
-
expect(result.decision).toBe("
|
|
343
|
+
expect(result.decision).toBe("allow");
|
|
345
344
|
});
|
|
346
345
|
|
|
347
346
|
test("explicit deny rule overrides workspace mode auto-allow", async () => {
|