@vellumai/assistant 0.11.4 → 0.11.5-staging.1
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/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/feature-flag-registry.json +35 -19
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
unsendableImageCardText,
|
|
5
|
+
unsendableImageFilenames,
|
|
6
|
+
} from "../daemon/unsendable-image-notice.js";
|
|
7
|
+
import { createInlineAttachment } from "../persistence/attachments-store.js";
|
|
8
|
+
import {
|
|
9
|
+
addMessage,
|
|
10
|
+
createConversation,
|
|
11
|
+
} from "../persistence/conversation-crud.js";
|
|
12
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
13
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
14
|
+
import {
|
|
15
|
+
isUnsendableImageSource,
|
|
16
|
+
resolveMediaReferences,
|
|
17
|
+
UNSENDABLE_IMAGE_FORMAT_NOTE,
|
|
18
|
+
} from "../providers/media-resolve.js";
|
|
19
|
+
import type { ContentBlock, Message } from "../providers/types.js";
|
|
20
|
+
|
|
21
|
+
await initializeDb();
|
|
22
|
+
|
|
23
|
+
function resetTables() {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
db.run("DELETE FROM message_attachments");
|
|
26
|
+
db.run("DELETE FROM attachments");
|
|
27
|
+
db.run("DELETE FROM messages");
|
|
28
|
+
db.run("DELETE FROM conversations");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A 1x1 PNG: a real signature plus enough body to be a decodable image. */
|
|
32
|
+
const PNG_B64 =
|
|
33
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A HEIC head (ISO BMFF `ftyp` + the `heic` brand) under a `.png` name, which
|
|
37
|
+
* is what a photo renamed by hand or exported by iOS arrives as.
|
|
38
|
+
*/
|
|
39
|
+
const HEIC_B64 = Buffer.from([
|
|
40
|
+
0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63, 0x00,
|
|
41
|
+
0x00, 0x00, 0x00,
|
|
42
|
+
]).toString("base64");
|
|
43
|
+
|
|
44
|
+
/** A PNG whose signature bytes are damaged, as a partial upload leaves it. */
|
|
45
|
+
const CORRUPT_PNG_B64 = Buffer.from([
|
|
46
|
+
0x89, 0x50, 0x4e, 0x00, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,
|
|
47
|
+
]).toString("base64");
|
|
48
|
+
|
|
49
|
+
function userMessage(content: ContentBlock[]): Message {
|
|
50
|
+
return { role: "user", content };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inlineImage(mediaType: string, data: string): ContentBlock {
|
|
54
|
+
return {
|
|
55
|
+
type: "image",
|
|
56
|
+
source: { type: "base64", media_type: mediaType, data },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function storedImageRef(
|
|
61
|
+
filename: string,
|
|
62
|
+
mimeType: string,
|
|
63
|
+
dataBase64: string,
|
|
64
|
+
): Promise<ContentBlock> {
|
|
65
|
+
const conv = createConversation();
|
|
66
|
+
const stored = await createInlineAttachment(
|
|
67
|
+
conv.id,
|
|
68
|
+
conv.createdAt,
|
|
69
|
+
filename,
|
|
70
|
+
mimeType,
|
|
71
|
+
dataBase64,
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
type: "image",
|
|
75
|
+
source: {
|
|
76
|
+
type: "workspace_ref",
|
|
77
|
+
media_type: mimeType,
|
|
78
|
+
attachmentId: stored.id,
|
|
79
|
+
sizeBytes: stored.sizeBytes,
|
|
80
|
+
filename,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("resolveMediaReferences image validation", () => {
|
|
86
|
+
beforeEach(resetTables);
|
|
87
|
+
|
|
88
|
+
test("sends a valid inline image untouched", async () => {
|
|
89
|
+
// GIVEN a message carrying an inline PNG whose bytes match its declaration
|
|
90
|
+
const message = userMessage([inlineImage("image/png", PNG_B64)]);
|
|
91
|
+
|
|
92
|
+
// WHEN the send boundary resolves it
|
|
93
|
+
const [resolved] = await resolveMediaReferences([message]);
|
|
94
|
+
|
|
95
|
+
// THEN the block is unchanged, and the message is the same object (no
|
|
96
|
+
// allocation or disk read on the clean live-turn fast path)
|
|
97
|
+
expect(resolved).toBe(message);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("drops the unreadable image from a batch and keeps the valid ones", async () => {
|
|
101
|
+
// GIVEN a batch of a valid PNG, a HEIC photo declared as a PNG, and a
|
|
102
|
+
// truncated PNG: the shape of the eight-image upload that lost a whole turn
|
|
103
|
+
const message = userMessage([
|
|
104
|
+
{ type: "text", text: "what is in these?" },
|
|
105
|
+
inlineImage("image/png", PNG_B64),
|
|
106
|
+
inlineImage("image/png", HEIC_B64),
|
|
107
|
+
inlineImage("image/png", CORRUPT_PNG_B64),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// WHEN the send boundary resolves them
|
|
111
|
+
const [resolved] = await resolveMediaReferences([message]);
|
|
112
|
+
|
|
113
|
+
// THEN the valid image still goes to the model
|
|
114
|
+
expect(resolved.content[1]).toEqual(inlineImage("image/png", PNG_B64));
|
|
115
|
+
|
|
116
|
+
// AND each unreadable image is replaced by a note instead of failing the
|
|
117
|
+
// whole request
|
|
118
|
+
expect(resolved.content[2]).toEqual({
|
|
119
|
+
type: "text",
|
|
120
|
+
text: UNSENDABLE_IMAGE_FORMAT_NOTE,
|
|
121
|
+
});
|
|
122
|
+
expect(resolved.content[3]).toEqual({
|
|
123
|
+
type: "text",
|
|
124
|
+
text: UNSENDABLE_IMAGE_FORMAT_NOTE,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("keeps a HEIC photo for a provider that decodes HEIF", async () => {
|
|
129
|
+
/**
|
|
130
|
+
* Gemini reads image/heic and image/heif directly, so an untranscoded HEIC
|
|
131
|
+
* photo must reach it as an image rather than as an omission note.
|
|
132
|
+
*/
|
|
133
|
+
// GIVEN a HEIC photo declared as a PNG, as an untranscoded upload arrives
|
|
134
|
+
const message = userMessage([inlineImage("image/png", HEIC_B64)]);
|
|
135
|
+
|
|
136
|
+
// WHEN a provider that accepts HEIF resolves it
|
|
137
|
+
const [resolved] = await resolveMediaReferences([message], {
|
|
138
|
+
acceptsHeif: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// THEN the image survives, declared as the HEIF type its brand names
|
|
142
|
+
expect(resolved.content[0]).toEqual(inlineImage("image/heic", HEIC_B64));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("relabels an inline image whose declared type is not its actual type", async () => {
|
|
146
|
+
// GIVEN PNG bytes declared as JPEG, which every provider rejects outright
|
|
147
|
+
const message = userMessage([inlineImage("image/jpeg", PNG_B64)]);
|
|
148
|
+
|
|
149
|
+
// WHEN the send boundary resolves it
|
|
150
|
+
const [resolved] = await resolveMediaReferences([message]);
|
|
151
|
+
|
|
152
|
+
// THEN the declaration is corrected to what the bytes are, payload intact
|
|
153
|
+
expect(resolved.content[0]).toEqual(inlineImage("image/png", PNG_B64));
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("drops a stored image whose bytes are not a readable format", async () => {
|
|
157
|
+
// GIVEN a persisted attachment holding HEIC bytes under a .png name,
|
|
158
|
+
// referenced by a reloaded message
|
|
159
|
+
const block = await storedImageRef("photo.png", "image/png", HEIC_B64);
|
|
160
|
+
|
|
161
|
+
// WHEN the send boundary resolves the reference
|
|
162
|
+
const [resolved] = await resolveMediaReferences([userMessage([block])]);
|
|
163
|
+
|
|
164
|
+
// THEN it is replaced by the note rather than hydrated and sent
|
|
165
|
+
expect(resolved.content[0]).toEqual({
|
|
166
|
+
type: "text",
|
|
167
|
+
text: UNSENDABLE_IMAGE_FORMAT_NOTE,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("keeps the unavailable-attachment note for a reference with no bytes", async () => {
|
|
172
|
+
// GIVEN a reference to an attachment row that does not exist
|
|
173
|
+
const block: ContentBlock = {
|
|
174
|
+
type: "image",
|
|
175
|
+
source: {
|
|
176
|
+
type: "workspace_ref",
|
|
177
|
+
media_type: "image/png",
|
|
178
|
+
attachmentId: "missing-attachment",
|
|
179
|
+
sizeBytes: 1024,
|
|
180
|
+
filename: "gone.png",
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// WHEN the send boundary resolves it
|
|
185
|
+
const [resolved] = await resolveMediaReferences([userMessage([block])]);
|
|
186
|
+
|
|
187
|
+
// THEN an unreadable payload is reported as unavailable, not as a bad
|
|
188
|
+
// format: the file may simply have been collected
|
|
189
|
+
expect(resolved.content[0]).toEqual({
|
|
190
|
+
type: "text",
|
|
191
|
+
text: "[Attachment unavailable: image could not be loaded]",
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("validates images nested in a tool result", async () => {
|
|
196
|
+
// GIVEN a tool result carrying one readable and one corrupt image
|
|
197
|
+
const message = userMessage([
|
|
198
|
+
{
|
|
199
|
+
type: "tool_result",
|
|
200
|
+
tool_use_id: "tool-1",
|
|
201
|
+
content: "two screenshots",
|
|
202
|
+
contentBlocks: [
|
|
203
|
+
inlineImage("image/png", PNG_B64),
|
|
204
|
+
inlineImage("image/png", CORRUPT_PNG_B64),
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
// WHEN the send boundary resolves it
|
|
210
|
+
const [resolved] = await resolveMediaReferences([message]);
|
|
211
|
+
|
|
212
|
+
// THEN the tool_use/tool_result pairing survives with the bad image noted
|
|
213
|
+
const toolResult = resolved.content[0];
|
|
214
|
+
if (toolResult.type !== "tool_result") {
|
|
215
|
+
throw new Error(`expected tool_result, got ${toolResult.type}`);
|
|
216
|
+
}
|
|
217
|
+
expect(toolResult.contentBlocks).toEqual([
|
|
218
|
+
inlineImage("image/png", PNG_B64),
|
|
219
|
+
{ type: "text", text: UNSENDABLE_IMAGE_FORMAT_NOTE },
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("isUnsendableImageSource", () => {
|
|
225
|
+
beforeEach(resetTables);
|
|
226
|
+
|
|
227
|
+
test("reports a stored attachment the send boundary will drop", async () => {
|
|
228
|
+
// GIVEN one attachment holding a real PNG and one holding HEIC bytes
|
|
229
|
+
const good = await storedImageRef("good.png", "image/png", PNG_B64);
|
|
230
|
+
const bad = await storedImageRef("bad.png", "image/png", HEIC_B64);
|
|
231
|
+
if (good.type !== "image" || bad.type !== "image") {
|
|
232
|
+
throw new Error("expected image blocks");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// WHEN each source is checked
|
|
236
|
+
// THEN only the unreadable one is reported, matching what the boundary does
|
|
237
|
+
expect(isUnsendableImageSource(good.source)).toBe(false);
|
|
238
|
+
expect(isUnsendableImageSource(bad.source)).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("does not report a reference whose attachment is gone", () => {
|
|
242
|
+
// GIVEN a reference with no row behind it
|
|
243
|
+
// WHEN it is checked
|
|
244
|
+
// THEN it is not called unsendable: a missing payload is a separate
|
|
245
|
+
// failure, and naming it in the drop card would misinform the user
|
|
246
|
+
expect(
|
|
247
|
+
isUnsendableImageSource({
|
|
248
|
+
type: "workspace_ref",
|
|
249
|
+
media_type: "image/png",
|
|
250
|
+
attachmentId: "missing-attachment",
|
|
251
|
+
sizeBytes: 10,
|
|
252
|
+
filename: "gone.png",
|
|
253
|
+
}),
|
|
254
|
+
).toBe(false);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("unsendable-image notice", () => {
|
|
259
|
+
beforeEach(resetTables);
|
|
260
|
+
|
|
261
|
+
test("names only the attachments the model cannot see", async () => {
|
|
262
|
+
// GIVEN a message whose attachments are one valid PNG and one HEIC-as-PNG
|
|
263
|
+
const msgConv = createConversation();
|
|
264
|
+
await addMessage(msgConv.id, "user", "look at these");
|
|
265
|
+
const good = await storedImageRef("holiday.png", "image/png", PNG_B64);
|
|
266
|
+
const bad = await storedImageRef("beach.png", "image/png", HEIC_B64);
|
|
267
|
+
if (good.type !== "image" || bad.type !== "image") {
|
|
268
|
+
throw new Error("expected image blocks");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// WHEN the notice decides what to name
|
|
272
|
+
const filenames = unsendableImageFilenames([
|
|
273
|
+
{ filename: "holiday.png", source: good.source },
|
|
274
|
+
{ filename: "beach.png", source: bad.source },
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
// THEN only the dropped file is named, so the user knows which to convert
|
|
278
|
+
expect(filenames).toEqual(["beach.png"]);
|
|
279
|
+
|
|
280
|
+
// AND the card copy names it and says what to do about it
|
|
281
|
+
expect(unsendableImageCardText(filenames)).toBe(
|
|
282
|
+
"beach.png was not sent to the model: the file is not a PNG, JPEG, GIF, or WebP image. Convert it and attach it again to include it.",
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("posts no notice when every attachment is readable", async () => {
|
|
287
|
+
// GIVEN attachments that are all valid images
|
|
288
|
+
const good = await storedImageRef("one.png", "image/png", PNG_B64);
|
|
289
|
+
if (good.type !== "image") {
|
|
290
|
+
throw new Error("expected an image block");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// WHEN the notice decides what to name
|
|
294
|
+
// THEN there is nothing to say, so no card is written
|
|
295
|
+
expect(
|
|
296
|
+
unsendableImageFilenames([{ filename: "one.png", source: good.source }]),
|
|
297
|
+
).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("names every dropped file when a batch has several", () => {
|
|
301
|
+
// GIVEN two inline attachments whose bytes are unreadable
|
|
302
|
+
// WHEN the card copy is built for them
|
|
303
|
+
const text = unsendableImageCardText(["a.png", "b.png"]);
|
|
304
|
+
|
|
305
|
+
// THEN both are named in plural copy
|
|
306
|
+
expect(text).toBe(
|
|
307
|
+
"a.png, b.png were not sent to the model: the files are not PNG, JPEG, GIF, or WebP images. Convert them and attach them again to include them.",
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
reconcilePluginSourcesNow,
|
|
39
39
|
resetPluginCacheForTests,
|
|
40
40
|
} from "../plugins/mtime-cache.js";
|
|
41
|
+
import { resetPluginRegistryForTests } from "../plugins/registry.js";
|
|
41
42
|
import { getSourceVersionsPath } from "../plugins/source-versions.js";
|
|
42
43
|
import {
|
|
43
44
|
__resetRegistryForTesting,
|
|
@@ -157,6 +158,7 @@ beforeEach(() => {
|
|
|
157
158
|
// The registry pulls plugin tools via loadPluginTools(); clear its side
|
|
158
159
|
// (tools + pulled fingerprints) so registrations never leak across tests.
|
|
159
160
|
__resetRegistryForTesting();
|
|
161
|
+
resetPluginRegistryForTests();
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
afterAll(() => {
|
|
@@ -1,4 +1,28 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ── Mocks: declared before imports that depend on them ──────────────
|
|
4
|
+
|
|
5
|
+
let updateMessageContentShouldThrow = false;
|
|
6
|
+
|
|
7
|
+
const updateMessageContentMock = mock(
|
|
8
|
+
(_messageId: string, _content: string) => {
|
|
9
|
+
if (updateMessageContentShouldThrow) {
|
|
10
|
+
throw new Error("DB write failed");
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
mock.module("../persistence/conversation-crud.js", () => ({
|
|
16
|
+
updateMessageContent: updateMessageContentMock,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const messagesInvalidated: string[] = [];
|
|
20
|
+
|
|
21
|
+
mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
22
|
+
publishConversationMessagesChanged: (conversationId: string) => {
|
|
23
|
+
messagesInvalidated.push(conversationId);
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
2
26
|
|
|
3
27
|
import type { AssistantEvent } from "../api/index.js";
|
|
4
28
|
import { VellumAdapter } from "../notifications/adapters/macos.js";
|
|
@@ -156,3 +180,102 @@ describe("VellumAdapter remotePushDispatched pass-through", () => {
|
|
|
156
180
|
expect(intent.remotePushDispatched).toBeUndefined();
|
|
157
181
|
});
|
|
158
182
|
});
|
|
183
|
+
|
|
184
|
+
describe("VellumAdapter update", () => {
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
updateMessageContentMock.mockClear();
|
|
187
|
+
updateMessageContentShouldThrow = false;
|
|
188
|
+
messagesInvalidated.length = 0;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("rewrites the persisted conversation message with the new body", async () => {
|
|
192
|
+
const { adapter } = captureBroadcast();
|
|
193
|
+
|
|
194
|
+
const result = await adapter.update!(
|
|
195
|
+
{
|
|
196
|
+
deliveryId: "delivery-1",
|
|
197
|
+
destination: "vellum",
|
|
198
|
+
messageId: "msg-1",
|
|
199
|
+
conversationId: "conv-1",
|
|
200
|
+
},
|
|
201
|
+
{ title: "Daily Briefing", body: "Revised briefing text." },
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(result.success).toBe(true);
|
|
205
|
+
expect(result.messageId).toBe("msg-1");
|
|
206
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
|
|
207
|
+
expect(updateMessageContentMock.mock.calls[0]).toEqual([
|
|
208
|
+
"msg-1",
|
|
209
|
+
"Revised briefing text.",
|
|
210
|
+
]);
|
|
211
|
+
// A client holding the conversation open refetches on this tag alone.
|
|
212
|
+
expect(messagesInvalidated).toEqual(["conv-1"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("rewrites without a conversation to invalidate", async () => {
|
|
216
|
+
// Deliveries recorded before the pairing carried a conversation have none
|
|
217
|
+
// to name, and the rewrite still stands on its own.
|
|
218
|
+
const { adapter } = captureBroadcast();
|
|
219
|
+
|
|
220
|
+
const result = await adapter.update!(
|
|
221
|
+
{ deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
|
|
222
|
+
{ body: "Revised briefing text." },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(result.success).toBe(true);
|
|
226
|
+
expect(updateMessageContentMock).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(messagesInvalidated).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("leaves the message untouched on a title-only edit", async () => {
|
|
231
|
+
// The feed rewrites its summary only when the patch carries a body, so
|
|
232
|
+
// writing the title here would put the conversation out of step with the
|
|
233
|
+
// card it is supposed to match.
|
|
234
|
+
const { adapter } = captureBroadcast();
|
|
235
|
+
|
|
236
|
+
const result = await adapter.update!(
|
|
237
|
+
{ deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
|
|
238
|
+
{ title: "Daily Briefing" },
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(result.success).toBe(true);
|
|
242
|
+
expect(updateMessageContentMock).not.toHaveBeenCalled();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("writes an empty body through, matching the feed summary patch", async () => {
|
|
246
|
+
const { adapter } = captureBroadcast();
|
|
247
|
+
|
|
248
|
+
await adapter.update!(
|
|
249
|
+
{ deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
|
|
250
|
+
{ title: "Daily Briefing", body: "" },
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(updateMessageContentMock.mock.calls[0]![1]).toBe("");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("skips deliveries that persisted no conversation message", async () => {
|
|
257
|
+
const { adapter } = captureBroadcast();
|
|
258
|
+
|
|
259
|
+
const result = await adapter.update!(
|
|
260
|
+
{ deliveryId: "delivery-1", destination: "vellum", messageId: null },
|
|
261
|
+
{ body: "Revised briefing text." },
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(result.success).toBe(false);
|
|
265
|
+
expect(result.error).toContain("missing_message_id");
|
|
266
|
+
expect(updateMessageContentMock).not.toHaveBeenCalled();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("reports failure without throwing when the write fails", async () => {
|
|
270
|
+
const { adapter } = captureBroadcast();
|
|
271
|
+
updateMessageContentShouldThrow = true;
|
|
272
|
+
|
|
273
|
+
const result = await adapter.update!(
|
|
274
|
+
{ deliveryId: "delivery-1", destination: "vellum", messageId: "msg-1" },
|
|
275
|
+
{ body: "Revised briefing text." },
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(result.success).toBe(false);
|
|
279
|
+
expect(result.error).toBe("DB write failed");
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -348,6 +348,28 @@ describe("OpenAIProvider", () => {
|
|
|
348
348
|
}
|
|
349
349
|
});
|
|
350
350
|
|
|
351
|
+
test("ollama wrapper prefers an explicit baseURL over OLLAMA_BASE_URL", () => {
|
|
352
|
+
const previousBaseUrl = process.env.OLLAMA_BASE_URL;
|
|
353
|
+
try {
|
|
354
|
+
process.env.OLLAMA_BASE_URL = "http://127.0.0.1:11434/v1";
|
|
355
|
+
const ollama = new OllamaProvider("llama3.2", {
|
|
356
|
+
baseURL: "http://192.168.1.50:11434/v1",
|
|
357
|
+
});
|
|
358
|
+
expect(ollama.name).toBe("ollama");
|
|
359
|
+
expectOpenAIConstructorOptions({
|
|
360
|
+
apiKey: "ollama",
|
|
361
|
+
baseURL: "http://192.168.1.50:11434/v1",
|
|
362
|
+
timeout: DEFAULT_SDK_TIMEOUT_MS,
|
|
363
|
+
});
|
|
364
|
+
} finally {
|
|
365
|
+
if (previousBaseUrl !== undefined) {
|
|
366
|
+
process.env.OLLAMA_BASE_URL = previousBaseUrl;
|
|
367
|
+
} else {
|
|
368
|
+
delete process.env.OLLAMA_BASE_URL;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
351
373
|
test("ollama wrapper treats empty OLLAMA_BASE_URL as unset", () => {
|
|
352
374
|
const previousBaseUrl = process.env.OLLAMA_BASE_URL;
|
|
353
375
|
try {
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
FALLBACK_TURN_TRUST,
|
|
19
|
+
isPersonalMemoryAllowed,
|
|
20
|
+
} from "../daemon/trust-context.js";
|
|
18
21
|
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
19
22
|
import type { TrustClass } from "../runtime/actor-trust-resolver.js";
|
|
20
23
|
|
|
@@ -45,16 +48,29 @@ describe("isPersonalMemoryAllowed — no exposure to non-guardian channel actors
|
|
|
45
48
|
expect(isPersonalMemoryAllowed(slack("unverified_contact"))).toBe(false);
|
|
46
49
|
});
|
|
47
50
|
|
|
48
|
-
test("allows personal memory for the guardian and for local /
|
|
51
|
+
test("allows personal memory for the guardian and for local / native turns", () => {
|
|
49
52
|
expect(isPersonalMemoryAllowed(slack("guardian"))).toBe(true);
|
|
50
|
-
// Local/native turn
|
|
53
|
+
// Local/native turn: no actor bound at all. `resolveTrustClass` elevates
|
|
54
|
+
// this to guardian under a disabled-auth posture, so identity resolution
|
|
55
|
+
// admits it rather than the memory gate carving out an exception.
|
|
51
56
|
expect(isPersonalMemoryAllowed(undefined)).toBe(true);
|
|
52
|
-
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("blocks personal memory for the fail-closed fallback trust", () => {
|
|
60
|
+
// `{vellum, unknown}` is FALLBACK_TURN_TRUST, not an internal flow: it is
|
|
61
|
+
// what the daemon substitutes when no per-turn actor was captured. Its own
|
|
62
|
+
// contract is to bias to `unknown` "so a missing snapshot cannot
|
|
63
|
+
// accidentally grant elevated trust", and every other consumer treats it
|
|
64
|
+
// as low-trust and never guardian. Personal memory is elevated trust, so
|
|
65
|
+
// the gate has to agree with them.
|
|
66
|
+
expect(isPersonalMemoryAllowed(FALLBACK_TURN_TRUST)).toBe(false);
|
|
67
|
+
// And a resolved non-guardian is denied on the console for the same
|
|
68
|
+
// reason: a surface is not an actor.
|
|
53
69
|
expect(
|
|
54
70
|
isPersonalMemoryAllowed({
|
|
55
71
|
sourceChannel: "vellum",
|
|
56
|
-
trustClass: "
|
|
72
|
+
trustClass: "unverified_contact",
|
|
57
73
|
}),
|
|
58
|
-
).toBe(
|
|
74
|
+
).toBe(false);
|
|
59
75
|
});
|
|
60
76
|
});
|
|
@@ -44,12 +44,8 @@ mock.module("../permissions/checker.js", () => ({
|
|
|
44
44
|
}
|
|
45
45
|
return { decision: "allow", reason: "allowed" };
|
|
46
46
|
},
|
|
47
|
-
generateAllowlistOptions: () => [
|
|
48
|
-
{ label: "exact", description: "exact", pattern: "exact" },
|
|
49
|
-
],
|
|
50
47
|
generateScopeOptions: () =>
|
|
51
48
|
scopeOptionsOverride ?? [{ label: "/tmp", scope: "/tmp" }],
|
|
52
|
-
getCachedAssessment: () => undefined,
|
|
53
49
|
}));
|
|
54
50
|
|
|
55
51
|
// Mock every export so downstream test files that dynamically import modules
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
ensureDataDir,
|
|
@@ -23,6 +23,21 @@ import {
|
|
|
23
23
|
} from "../util/platform.js";
|
|
24
24
|
|
|
25
25
|
const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
26
|
+
|
|
27
|
+
// This file characterizes path resolution itself (including the ~/.vellum
|
|
28
|
+
// fallback and literal override paths) without reading or writing those
|
|
29
|
+
// locations, so the live-workspace guard is deliberately bypassed.
|
|
30
|
+
const originalAllowRealWorkspace =
|
|
31
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
32
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
if (originalAllowRealWorkspace === undefined) {
|
|
35
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
36
|
+
} else {
|
|
37
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS =
|
|
38
|
+
originalAllowRealWorkspace;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
26
41
|
const originalVellumEnvironment = process.env.VELLUM_ENVIRONMENT;
|
|
27
42
|
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
28
43
|
|
|
@@ -102,21 +102,28 @@ describe("resolveCredential", () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe("plugin scoping", () => {
|
|
105
|
-
test("allows a plugin to resolve a credential whose
|
|
106
|
-
seedCredential("
|
|
107
|
-
const value = await runInPluginContext("
|
|
108
|
-
resolveCredential("
|
|
105
|
+
test("allows a plugin to resolve a credential whose service matches its name", async () => {
|
|
106
|
+
seedCredential("imessage", "api_key", "comms-key");
|
|
107
|
+
const value = await runInPluginContext("imessage", () =>
|
|
108
|
+
resolveCredential("imessage/api_key"),
|
|
109
109
|
);
|
|
110
|
-
expect(value).toBe("
|
|
110
|
+
expect(value).toBe("comms-key");
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
test("blocks a plugin from resolving
|
|
113
|
+
test("blocks a plugin from resolving another service", async () => {
|
|
114
114
|
seedCredential("openai", "api_key", "sk-secret");
|
|
115
115
|
await expect(
|
|
116
116
|
runInPluginContext("acme", () => resolveCredential("openai/api_key")),
|
|
117
117
|
).rejects.toThrow(/out of scope/);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test("blocks a plugin from resolving another service even when the field matches its name", async () => {
|
|
121
|
+
seedCredential("openai", "acme", "scoped-secret");
|
|
122
|
+
await expect(
|
|
123
|
+
runInPluginContext("acme", () => resolveCredential("openai/acme")),
|
|
124
|
+
).rejects.toThrow(/out of scope/);
|
|
125
|
+
});
|
|
126
|
+
|
|
120
127
|
test("does not read the secure backend when out of scope", async () => {
|
|
121
128
|
seedCredential("openai", "api_key", "sk-secret");
|
|
122
129
|
getSpy.mockClear();
|
|
@@ -126,15 +133,17 @@ describe("resolveCredential", () => {
|
|
|
126
133
|
expect(getSpy).not.toHaveBeenCalled();
|
|
127
134
|
});
|
|
128
135
|
|
|
129
|
-
test("scoping applies
|
|
130
|
-
seedCredential("
|
|
131
|
-
seedCredential("
|
|
136
|
+
test("scoping applies to every field under the plugin's service", async () => {
|
|
137
|
+
seedCredential("imessage", "api_key", "comms-key");
|
|
138
|
+
seedCredential("imessage", "photon_project_id", "proj-id");
|
|
132
139
|
await expect(
|
|
133
|
-
runInPluginContext("
|
|
134
|
-
).resolves.toBe("
|
|
140
|
+
runInPluginContext("imessage", () => resolveCredential("imessage/api_key")),
|
|
141
|
+
).resolves.toBe("comms-key");
|
|
135
142
|
await expect(
|
|
136
|
-
runInPluginContext("
|
|
137
|
-
|
|
143
|
+
runInPluginContext("imessage", () =>
|
|
144
|
+
resolveCredential("imessage/photon_project_id"),
|
|
145
|
+
),
|
|
146
|
+
).resolves.toBe("proj-id");
|
|
138
147
|
});
|
|
139
148
|
});
|
|
140
149
|
});
|
|
@@ -136,9 +136,14 @@ describe("ensurePluginApiShim", () => {
|
|
|
136
136
|
`}));\n`,
|
|
137
137
|
);
|
|
138
138
|
|
|
139
|
+
// Explicit env spread: under bun test, a child's default env reflects the
|
|
140
|
+
// process-start environ, not later process.env mutations. Without this
|
|
141
|
+
// the child misses the preload's VELLUM_WORKSPACE_DIR and resolves the
|
|
142
|
+
// real ~/.vellum/workspace (refused by the live-workspace guard).
|
|
139
143
|
const result = spawnSync(process.execPath, ["run", probe], {
|
|
140
144
|
encoding: "utf8",
|
|
141
145
|
timeout: 30_000,
|
|
146
|
+
env: { ...process.env },
|
|
142
147
|
});
|
|
143
148
|
expect(result.status).toBe(0);
|
|
144
149
|
const lastLine = result.stdout.trim().split("\n").at(-1) ?? "";
|