@vellumai/assistant 0.11.4 → 0.11.5-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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__/config-loader-backfill.test.ts +19 -10
- 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__/managed-profile-guard.test.ts +4 -2
- 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/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- 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/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- 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/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- 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,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the live-workspace guard in util/platform.ts: a test process must
|
|
3
|
+
* never resolve the workspace to a directory outside os.tmpdir().
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { rmSync, symlinkSync } from "node:fs";
|
|
7
|
+
import { homedir, tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { getWorkspaceDir } from "../util/platform.js";
|
|
12
|
+
|
|
13
|
+
const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
14
|
+
const originalAllowReal = process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
process.env.VELLUM_WORKSPACE_DIR = originalWorkspaceDir;
|
|
18
|
+
if (originalAllowReal === undefined) {
|
|
19
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
20
|
+
} else {
|
|
21
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = originalAllowReal;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("live-workspace guard", () => {
|
|
26
|
+
test("allows the preload's tmpdir workspace", () => {
|
|
27
|
+
expect(getWorkspaceDir()).toBe(process.env.VELLUM_WORKSPACE_DIR!);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("allows a not-yet-created nested path under tmpdir", () => {
|
|
31
|
+
const dir = join(
|
|
32
|
+
tmpdir(),
|
|
33
|
+
"vellum-guard-nonexistent",
|
|
34
|
+
"nested",
|
|
35
|
+
"workspace",
|
|
36
|
+
);
|
|
37
|
+
process.env.VELLUM_WORKSPACE_DIR = dir;
|
|
38
|
+
expect(getWorkspaceDir()).toBe(dir);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("refuses a non-tmpdir workspace in a test process", () => {
|
|
42
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
43
|
+
process.env.VELLUM_WORKSPACE_DIR = join(homedir(), "vellum-guard-live");
|
|
44
|
+
expect(() => getWorkspaceDir()).toThrow(/Refusing to use/);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("refuses a tmpdir symlink that resolves outside tmpdir", () => {
|
|
48
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
49
|
+
const link = join(tmpdir(), `vellum-guard-escape-${process.pid}`);
|
|
50
|
+
rmSync(link, { force: true });
|
|
51
|
+
symlinkSync(homedir(), link);
|
|
52
|
+
try {
|
|
53
|
+
process.env.VELLUM_WORKSPACE_DIR = link;
|
|
54
|
+
expect(() => getWorkspaceDir()).toThrow(/Refusing to use/);
|
|
55
|
+
} finally {
|
|
56
|
+
rmSync(link, { force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS=1 bypasses the guard", () => {
|
|
61
|
+
const dir = join(homedir(), "vellum-guard-live-optout");
|
|
62
|
+
process.env.VELLUM_WORKSPACE_DIR = dir;
|
|
63
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
|
|
64
|
+
expect(getWorkspaceDir()).toBe(dir);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("opt-out is per-call, not cached: same dir throws once the var clears", () => {
|
|
68
|
+
const dir = join(homedir(), "vellum-guard-live-optout-cleared");
|
|
69
|
+
process.env.VELLUM_WORKSPACE_DIR = dir;
|
|
70
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
|
|
71
|
+
expect(getWorkspaceDir()).toBe(dir);
|
|
72
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
73
|
+
expect(() => getWorkspaceDir()).toThrow(/Refusing to use/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1090,7 +1090,7 @@ describe("code-owned default profiles — wire view and write normalization", ()
|
|
|
1090
1090
|
});
|
|
1091
1091
|
const response = (await getRoute.handler({})) as Record<string, any>;
|
|
1092
1092
|
const wireBalanced = response.llm.profiles.balanced;
|
|
1093
|
-
expect(wireBalanced.model).toBe("
|
|
1093
|
+
expect(wireBalanced.model).toBe("accounts/fireworks/models/glm-5p2");
|
|
1094
1094
|
expect(wireBalanced.provider).toBe("vellum");
|
|
1095
1095
|
expect(wireBalanced.provider_connection).toBeUndefined();
|
|
1096
1096
|
expect(wireBalanced.status).toBe("disabled");
|
|
@@ -1337,7 +1337,9 @@ describe("code-owned default profiles — echoes over stale on-disk bodies", ()
|
|
|
1337
1337
|
});
|
|
1338
1338
|
const response = (await getRoute.handler({})) as Record<string, any>;
|
|
1339
1339
|
// The wire view serves catalog content, not the stale body.
|
|
1340
|
-
expect(response.llm.profiles.balanced.model).toBe(
|
|
1340
|
+
expect(response.llm.profiles.balanced.model).toBe(
|
|
1341
|
+
"accounts/fireworks/models/glm-5p2",
|
|
1342
|
+
);
|
|
1341
1343
|
const result = await patchRoute.handler({
|
|
1342
1344
|
body: { llm: { profiles: response.llm.profiles } },
|
|
1343
1345
|
});
|
|
@@ -12,7 +12,7 @@ mock.module("../config/env-registry.js", () => ({
|
|
|
12
12
|
getIsContainerized: () => false,
|
|
13
13
|
getIsPlatform: () => false,
|
|
14
14
|
isPlatformRemote: () => false,
|
|
15
|
-
getWorkspaceDirOverride: () =>
|
|
15
|
+
getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
|
|
16
16
|
getBackupDirOverride: () => undefined,
|
|
17
17
|
getBackupKeyPathOverride: () => undefined,
|
|
18
18
|
getCpuLimit: () => undefined,
|
|
@@ -12,7 +12,7 @@ mock.module("../config/env-registry.js", () => ({
|
|
|
12
12
|
getIsContainerized: () => false,
|
|
13
13
|
getIsPlatform: () => false,
|
|
14
14
|
isPlatformRemote: () => false,
|
|
15
|
-
getWorkspaceDirOverride: () =>
|
|
15
|
+
getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
|
|
16
16
|
getBackupDirOverride: () => undefined,
|
|
17
17
|
getBackupKeyPathOverride: () => undefined,
|
|
18
18
|
getCpuLimit: () => undefined,
|
|
@@ -12,7 +12,7 @@ mock.module("../config/env-registry.js", () => ({
|
|
|
12
12
|
getIsContainerized: () => false,
|
|
13
13
|
getIsPlatform: () => false,
|
|
14
14
|
isPlatformRemote: () => false,
|
|
15
|
-
getWorkspaceDirOverride: () =>
|
|
15
|
+
getWorkspaceDirOverride: () => process.env.VELLUM_WORKSPACE_DIR,
|
|
16
16
|
getBackupDirOverride: () => undefined,
|
|
17
17
|
getBackupKeyPathOverride: () => undefined,
|
|
18
18
|
getCpuLimit: () => undefined,
|
|
@@ -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
|