@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,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rolling container CPU sampler.
|
|
3
|
+
*
|
|
4
|
+
* Tracks CPU usage over a rolling window so consumers (e.g. /v1/health) report
|
|
5
|
+
* near-real-time utilization instead of a lifetime average (total CPU time /
|
|
6
|
+
* total uptime). The sampler starts on module import and runs on an unref'd
|
|
7
|
+
* interval, so it never prevents process exit.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
import { getIsPlatform } from "../config/env-registry.js";
|
|
13
|
+
import { getContainerCpuCores } from "./cgroup-cpu.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Read the container's CPU usage from cgroup accounting files.
|
|
17
|
+
*
|
|
18
|
+
* Returns total CPU microseconds consumed by the container since boot.
|
|
19
|
+
* We use the delta between two samples to compute percentage.
|
|
20
|
+
*/
|
|
21
|
+
function getContainerCpuUsageUs(): number | null {
|
|
22
|
+
// cgroups v2: cpu.stat has a "usage_usec" line.
|
|
23
|
+
try {
|
|
24
|
+
const stat = readFileSync("/sys/fs/cgroup/cpu.stat", "utf-8");
|
|
25
|
+
for (const line of stat.split("\n")) {
|
|
26
|
+
if (line.startsWith("usage_usec")) {
|
|
27
|
+
const val = parseInt(line.split(/\s+/)[1], 10);
|
|
28
|
+
if (!isNaN(val) && val > 0) {
|
|
29
|
+
return val;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
/* not available */
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// cgroups v1: cpuacct.usage is in nanoseconds.
|
|
38
|
+
try {
|
|
39
|
+
const ns = parseInt(
|
|
40
|
+
readFileSync("/sys/fs/cgroup/cpuacct/cpuacct.usage", "utf-8").trim(),
|
|
41
|
+
10,
|
|
42
|
+
);
|
|
43
|
+
if (!isNaN(ns) && ns > 0) {
|
|
44
|
+
return ns / 1000;
|
|
45
|
+
} // convert ns → µs
|
|
46
|
+
} catch {
|
|
47
|
+
/* not available */
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const CPU_SAMPLE_INTERVAL_MS = 5_000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Sample this process's cumulative CPU time, returning null when the
|
|
57
|
+
* underlying syscall fails.
|
|
58
|
+
*/
|
|
59
|
+
function sampleProcessCpuUsage(): NodeJS.CpuUsage | null {
|
|
60
|
+
try {
|
|
61
|
+
return process.cpuUsage();
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convert a CPU-time delta into a percent of the container's full allocation,
|
|
69
|
+
* rounded to 2 decimal places. Callers must guard `numCores > 0` and
|
|
70
|
+
* `elapsedMs > 0`; a zero divisor yields a non-finite result.
|
|
71
|
+
*/
|
|
72
|
+
export function computeCpuPercent(
|
|
73
|
+
deltaCpuUs: number,
|
|
74
|
+
elapsedMs: number,
|
|
75
|
+
numCores: number,
|
|
76
|
+
): number {
|
|
77
|
+
const deltaCpuMs = deltaCpuUs / 1000;
|
|
78
|
+
return Math.round((deltaCpuMs / (elapsedMs * numCores)) * 10000) / 100;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Recent per-tick percents are retained so consumers with a coarser cadence
|
|
82
|
+
// (the resource-pressure guard samples every 30s) can average the 5s windows
|
|
83
|
+
// inside their own interval instead of reading one instantaneous value; a
|
|
84
|
+
// workload that spikes in phase with the coarser cadence would otherwise
|
|
85
|
+
// alias into a sustained-high reading.
|
|
86
|
+
const RECENT_SAMPLE_RETENTION_MS = 5 * 60 * 1000;
|
|
87
|
+
|
|
88
|
+
let _lastProcessCpuUsage: NodeJS.CpuUsage | null = sampleProcessCpuUsage();
|
|
89
|
+
let _lastCgroupCpuUs: number | null = getContainerCpuUsageUs();
|
|
90
|
+
let _lastCpuTime: number = Date.now();
|
|
91
|
+
let _cachedCpuPercent = 0;
|
|
92
|
+
// Whether the sampler has ever computed a real delta. Until then the cache
|
|
93
|
+
// is a placeholder 0 (still warming up, or neither cgroup counters nor
|
|
94
|
+
// process.cpuUsage() deltas are readable), not a measurement.
|
|
95
|
+
let _hasCpuSample = false;
|
|
96
|
+
let _recentSamples: { at: number; percent: number; elapsedMs: number }[] = [];
|
|
97
|
+
|
|
98
|
+
function setCachedCpuPercent(
|
|
99
|
+
percent: number,
|
|
100
|
+
at: number,
|
|
101
|
+
elapsedMs: number,
|
|
102
|
+
): void {
|
|
103
|
+
_cachedCpuPercent = percent;
|
|
104
|
+
_hasCpuSample = true;
|
|
105
|
+
_recentSamples.push({ at, percent, elapsedMs });
|
|
106
|
+
const cutoff = at - RECENT_SAMPLE_RETENTION_MS;
|
|
107
|
+
while (_recentSamples.length > 0 && _recentSamples[0].at < cutoff) {
|
|
108
|
+
_recentSamples.shift();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runCpuSamplerTick(now: number): void {
|
|
113
|
+
const elapsedMs = now - _lastCpuTime;
|
|
114
|
+
if (elapsedMs <= 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const numCores = getContainerCpuCores();
|
|
119
|
+
if (numCores <= 0) {
|
|
120
|
+
_lastCpuTime = now;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Always sample process-level CPU so the baseline stays fresh. This
|
|
125
|
+
// prevents a spike if the platform cgroup path later falls back to
|
|
126
|
+
// process.cpuUsage() after cgroup stats were previously available.
|
|
127
|
+
const newProcessUsage = sampleProcessCpuUsage();
|
|
128
|
+
const processDeltaUs =
|
|
129
|
+
newProcessUsage !== null && _lastProcessCpuUsage !== null
|
|
130
|
+
? newProcessUsage.user -
|
|
131
|
+
_lastProcessCpuUsage.user +
|
|
132
|
+
(newProcessUsage.system - _lastProcessCpuUsage.system)
|
|
133
|
+
: null;
|
|
134
|
+
if (newProcessUsage !== null) {
|
|
135
|
+
_lastProcessCpuUsage = newProcessUsage;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (getIsPlatform()) {
|
|
139
|
+
// In platform mode, prefer cgroup-level CPU usage so we see the full
|
|
140
|
+
// container footprint, not just this process.
|
|
141
|
+
const cgroupUs = getContainerCpuUsageUs();
|
|
142
|
+
if (cgroupUs !== null && _lastCgroupCpuUs !== null) {
|
|
143
|
+
setCachedCpuPercent(
|
|
144
|
+
computeCpuPercent(cgroupUs - _lastCgroupCpuUs, elapsedMs, numCores),
|
|
145
|
+
now,
|
|
146
|
+
elapsedMs,
|
|
147
|
+
);
|
|
148
|
+
} else if (processDeltaUs !== null) {
|
|
149
|
+
// cgroup CPU stats unavailable (e.g. gVisor) – fall back to process-level.
|
|
150
|
+
setCachedCpuPercent(
|
|
151
|
+
computeCpuPercent(processDeltaUs, elapsedMs, numCores),
|
|
152
|
+
now,
|
|
153
|
+
elapsedMs,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
_lastCgroupCpuUs = cgroupUs;
|
|
157
|
+
} else if (processDeltaUs !== null) {
|
|
158
|
+
// Non-platform: use process.cpuUsage() (accurate for single-process mode).
|
|
159
|
+
setCachedCpuPercent(
|
|
160
|
+
computeCpuPercent(processDeltaUs, elapsedMs, numCores),
|
|
161
|
+
now,
|
|
162
|
+
elapsedMs,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_lastCpuTime = now;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Kick off the background sampler. unref() so it never prevents process exit.
|
|
170
|
+
setInterval(() => {
|
|
171
|
+
runCpuSamplerTick(Date.now());
|
|
172
|
+
}, CPU_SAMPLE_INTERVAL_MS).unref();
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Near-real-time CPU utilization as a percent of the container's full
|
|
176
|
+
* allocation, refreshed every {@link CPU_SAMPLE_INTERVAL_MS}.
|
|
177
|
+
*
|
|
178
|
+
* Reports 0 until the sampler computes its first delta; callers that must
|
|
179
|
+
* distinguish "idle" from "no data yet" use
|
|
180
|
+
* {@link getCachedContainerCpuPercentOrNull}.
|
|
181
|
+
*/
|
|
182
|
+
export function getCachedContainerCpuPercent(): number {
|
|
183
|
+
return _cachedCpuPercent;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Like {@link getCachedContainerCpuPercent}, but null until the sampler has
|
|
188
|
+
* computed at least one real delta, so warm-up and unreadable CPU accounting
|
|
189
|
+
* read as "signal unavailable" instead of a genuine-looking 0%.
|
|
190
|
+
*/
|
|
191
|
+
export function getCachedContainerCpuPercentOrNull(): number | null {
|
|
192
|
+
return _hasCpuSample ? _cachedCpuPercent : null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Duration-weighted mean of per-tick percents over the window starting at
|
|
197
|
+
* `windowStartMs`. Each tick's percent covers the wall time its delta
|
|
198
|
+
* spanned (ending at `at`), so a delayed tick must weigh proportionally
|
|
199
|
+
* more than an on-schedule one, and only the part of a tick's interval
|
|
200
|
+
* that overlaps the window may contribute: a long delayed tick ending just
|
|
201
|
+
* inside the window would otherwise import load that predates it.
|
|
202
|
+
*/
|
|
203
|
+
export function computeDurationWeightedMeanPercent(
|
|
204
|
+
samples: readonly { at: number; percent: number; elapsedMs: number }[],
|
|
205
|
+
windowStartMs: number,
|
|
206
|
+
): number | null {
|
|
207
|
+
let weightedSum = 0;
|
|
208
|
+
let totalOverlapMs = 0;
|
|
209
|
+
for (const sample of samples) {
|
|
210
|
+
const intervalStart = sample.at - sample.elapsedMs;
|
|
211
|
+
const overlapMs = sample.at - Math.max(intervalStart, windowStartMs);
|
|
212
|
+
if (overlapMs > 0) {
|
|
213
|
+
weightedSum += sample.percent * overlapMs;
|
|
214
|
+
totalOverlapMs += overlapMs;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (totalOverlapMs <= 0) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
return Math.round((weightedSum / totalOverlapMs) * 100) / 100;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Duration-weighted mean of the per-tick percents recorded within the
|
|
225
|
+
* trailing `windowMs`, rounded to 2 decimal places, or null when no tick
|
|
226
|
+
* overlaps the window. Coarser-cadence consumers use this instead of the
|
|
227
|
+
* instantaneous cache so a short spike aligned with their cadence cannot
|
|
228
|
+
* read as sustained load.
|
|
229
|
+
*/
|
|
230
|
+
export function getAverageContainerCpuPercentOrNull(
|
|
231
|
+
windowMs: number,
|
|
232
|
+
): number | null {
|
|
233
|
+
return computeDurationWeightedMeanPercent(
|
|
234
|
+
_recentSamples,
|
|
235
|
+
Date.now() - windowMs,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function __resetContainerCpuSamplerForTests(): void {
|
|
240
|
+
_lastProcessCpuUsage = sampleProcessCpuUsage();
|
|
241
|
+
_lastCgroupCpuUs = getContainerCpuUsageUs();
|
|
242
|
+
_lastCpuTime = Date.now();
|
|
243
|
+
_cachedCpuPercent = 0;
|
|
244
|
+
_hasCpuSample = false;
|
|
245
|
+
_recentSamples = [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function __runContainerCpuSamplerTickForTests(nowMs: number): void {
|
|
249
|
+
runCpuSamplerTick(nowMs);
|
|
250
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
readdirSync,
|
|
23
23
|
readFileSync,
|
|
24
24
|
readSync,
|
|
25
|
+
renameSync,
|
|
25
26
|
statSync,
|
|
26
27
|
unlinkSync,
|
|
27
28
|
writeFileSync,
|
|
@@ -42,26 +43,151 @@ function getCacheDir(): string {
|
|
|
42
43
|
return join(tmpdir(), "vellum-optimized-images");
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Structural completeness check for a JPEG payload: SOI marker (FF D8) at the
|
|
48
|
+
* head and EOI marker (FF D9) at the tail. Every cache entry and every sips
|
|
49
|
+
* output is a JPEG, so a payload failing this is truncated or empty (the
|
|
50
|
+
* signature of a torn cache write) and must never reach a provider, which
|
|
51
|
+
* rejects it with a 400 that wedges the conversation (the corrupt block is
|
|
52
|
+
* resent on every subsequent turn).
|
|
53
|
+
*/
|
|
54
|
+
export function isCompleteJpeg(bytes: Uint8Array): boolean {
|
|
55
|
+
return (
|
|
56
|
+
bytes.length >= 4 &&
|
|
57
|
+
bytes[0] === 0xff &&
|
|
58
|
+
bytes[1] === 0xd8 &&
|
|
59
|
+
bytes[bytes.length - 2] === 0xff &&
|
|
60
|
+
bytes[bytes.length - 1] === 0xd9
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Structural JPEG truncation check for payloads of arbitrary origin: walk the
|
|
66
|
+
* marker segments from SOI and report whether a terminal EOI is reached.
|
|
67
|
+
* Unlike {@link isCompleteJpeg} (exact tail framing, for sips output and
|
|
68
|
+
* cache entries, which always end on EOI), this tolerates encoders that
|
|
69
|
+
* append padding or metadata after the EOI.
|
|
70
|
+
*
|
|
71
|
+
* A raw byte search for FF D9 is not enough: length-delimited APP/COM
|
|
72
|
+
* segments (an EXIF thumbnail is itself a complete embedded JPEG) may contain
|
|
73
|
+
* FF D9, so a truncated file with an intact metadata prefix would pass.
|
|
74
|
+
* Walking segment boundaries skips those payloads entirely; only an EOI at
|
|
75
|
+
* the top level of the marker stream counts. Entropy-coded scan data is
|
|
76
|
+
* traversed byte-wise, where FF is stuffed as FF 00 and restart markers
|
|
77
|
+
* (D0-D7) continue the scan, so a marker byte there is unambiguous.
|
|
78
|
+
*
|
|
79
|
+
* The EOI only counts after at least one frame header (SOF) and one scan
|
|
80
|
+
* (SOS) have been seen: a degenerate payload such as bare SOI+EOI carries no
|
|
81
|
+
* image data and providers reject it.
|
|
82
|
+
*/
|
|
83
|
+
export function hasValidJpegStructure(bytes: Uint8Array): boolean {
|
|
84
|
+
if (bytes.length < 4 || bytes[0] !== 0xff || bytes[1] !== 0xd8) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
let sawFrame = false;
|
|
88
|
+
let sawScan = false;
|
|
89
|
+
let i = 2;
|
|
90
|
+
while (i + 1 < bytes.length) {
|
|
91
|
+
if (bytes[i] !== 0xff) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
// FF fill bytes before a marker are legal padding.
|
|
95
|
+
let j = i + 1;
|
|
96
|
+
while (j < bytes.length && bytes[j] === 0xff) {
|
|
97
|
+
j++;
|
|
98
|
+
}
|
|
99
|
+
if (j >= bytes.length) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const marker = bytes[j];
|
|
103
|
+
i = j + 1;
|
|
104
|
+
if (marker === 0xd9) {
|
|
105
|
+
return sawFrame && sawScan;
|
|
106
|
+
}
|
|
107
|
+
// SOF0-SOF15 occupy C0-CF, excluding DHT (C4), JPG (C8), and DAC (CC).
|
|
108
|
+
if (
|
|
109
|
+
marker >= 0xc0 &&
|
|
110
|
+
marker <= 0xcf &&
|
|
111
|
+
marker !== 0xc4 &&
|
|
112
|
+
marker !== 0xc8 &&
|
|
113
|
+
marker !== 0xcc
|
|
114
|
+
) {
|
|
115
|
+
sawFrame = true;
|
|
116
|
+
}
|
|
117
|
+
// Standalone markers carry no length field: repeated SOI, TEM, RST0-7.
|
|
118
|
+
if (
|
|
119
|
+
marker === 0xd8 ||
|
|
120
|
+
marker === 0x01 ||
|
|
121
|
+
(marker >= 0xd0 && marker <= 0xd7)
|
|
122
|
+
) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (i + 1 >= bytes.length) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const segmentLength = (bytes[i] << 8) | bytes[i + 1];
|
|
129
|
+
if (segmentLength < 2) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
i += segmentLength;
|
|
133
|
+
if (marker === 0xda) {
|
|
134
|
+
sawScan = true;
|
|
135
|
+
// SOS: entropy-coded data follows the header. Scan to the next real
|
|
136
|
+
// marker; FF 00 (stuffed data byte) and FF D0-D7 (restart) stay inside
|
|
137
|
+
// the scan.
|
|
138
|
+
while (i + 1 < bytes.length) {
|
|
139
|
+
if (
|
|
140
|
+
bytes[i] === 0xff &&
|
|
141
|
+
bytes[i + 1] !== 0x00 &&
|
|
142
|
+
!(bytes[i + 1] >= 0xd0 && bytes[i + 1] <= 0xd7)
|
|
143
|
+
) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
i++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
45
153
|
function readFromCache(key: string): Buffer | null {
|
|
154
|
+
const cachePath = join(getCacheDir(), `${key}.jpg`);
|
|
46
155
|
try {
|
|
47
|
-
const cachePath = join(getCacheDir(), `${key}.jpg`);
|
|
48
156
|
if (!existsSync(cachePath)) {
|
|
49
157
|
return null;
|
|
50
158
|
}
|
|
51
|
-
|
|
159
|
+
const bytes = readFileSync(cachePath) as Buffer;
|
|
160
|
+
if (!isCompleteJpeg(bytes)) {
|
|
161
|
+
// Poisoned entry (torn write, disk full, etc.): drop it so the caller
|
|
162
|
+
// re-converts and re-caches.
|
|
163
|
+
unlinkSync(cachePath);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return bytes;
|
|
52
167
|
} catch {
|
|
53
168
|
return null;
|
|
54
169
|
}
|
|
55
170
|
}
|
|
56
171
|
|
|
57
172
|
function writeToCache(key: string, convertedBytes: Buffer): void {
|
|
173
|
+
const dir = getCacheDir();
|
|
174
|
+
// Write-then-rename so concurrent readers (the cache dir is shared across
|
|
175
|
+
// daemon processes) never observe a partially written entry; rename within
|
|
176
|
+
// the same directory is atomic. The pid+uuid suffix keeps concurrent
|
|
177
|
+
// writers of the same key off each other's temp file.
|
|
178
|
+
const tmpPath = join(dir, `${key}.${process.pid}-${uuid().slice(0, 8)}.tmp`);
|
|
58
179
|
try {
|
|
59
|
-
const dir = getCacheDir();
|
|
60
180
|
mkdirSync(dir, { recursive: true });
|
|
61
|
-
writeFileSync(
|
|
181
|
+
writeFileSync(tmpPath, convertedBytes);
|
|
182
|
+
renameSync(tmpPath, join(dir, `${key}.jpg`));
|
|
62
183
|
evictIfNeeded(dir);
|
|
63
184
|
} catch {
|
|
64
185
|
// Cache write failure is non-fatal.
|
|
186
|
+
try {
|
|
187
|
+
unlinkSync(tmpPath);
|
|
188
|
+
} catch {
|
|
189
|
+
/* ignore */
|
|
190
|
+
}
|
|
65
191
|
}
|
|
66
192
|
}
|
|
67
193
|
|
|
@@ -141,7 +267,14 @@ async function runSips(
|
|
|
141
267
|
if (proc.exitCode !== 0) {
|
|
142
268
|
return null;
|
|
143
269
|
}
|
|
144
|
-
|
|
270
|
+
const out = readFileSync(outPath) as Buffer;
|
|
271
|
+
// A zero-exit sips can still leave a truncated file (disk full, timeout
|
|
272
|
+
// kill racing the write). Returning it would poison the cache and every
|
|
273
|
+
// downstream provider call. Treat it as a failed conversion instead.
|
|
274
|
+
if (!isCompleteJpeg(out)) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
145
278
|
} catch {
|
|
146
279
|
return null;
|
|
147
280
|
} finally {
|
|
@@ -206,14 +339,22 @@ const HEIF_FTYP_BRANDS = new Set([
|
|
|
206
339
|
"msf1",
|
|
207
340
|
]);
|
|
208
341
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
342
|
+
// Brands whose canonical MIME is `image/heic`; the remaining HEIF brands
|
|
343
|
+
// (`heif`, `mif1`, `msf1`) are `image/heif`.
|
|
344
|
+
const HEIC_FTYP_BRANDS = new Set([
|
|
345
|
+
"heic",
|
|
346
|
+
"heix",
|
|
347
|
+
"hevc",
|
|
348
|
+
"hevx",
|
|
349
|
+
"heim",
|
|
350
|
+
"heis",
|
|
351
|
+
"hevm",
|
|
352
|
+
"hevs",
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
function heifFtypBrand(bytes: Uint8Array): string | null {
|
|
215
356
|
if (bytes.length < 12) {
|
|
216
|
-
return
|
|
357
|
+
return null;
|
|
217
358
|
}
|
|
218
359
|
// ISO BMFF layout: bytes 4-8 are "ftyp", bytes 8-12 the major brand.
|
|
219
360
|
if (
|
|
@@ -222,7 +363,7 @@ export function isHeifImage(bytes: Uint8Array): boolean {
|
|
|
222
363
|
bytes[6] !== 0x79 || // y
|
|
223
364
|
bytes[7] !== 0x70 // p
|
|
224
365
|
) {
|
|
225
|
-
return
|
|
366
|
+
return null;
|
|
226
367
|
}
|
|
227
368
|
const brand = String.fromCharCode(
|
|
228
369
|
bytes[8]!,
|
|
@@ -230,7 +371,30 @@ export function isHeifImage(bytes: Uint8Array): boolean {
|
|
|
230
371
|
bytes[10]!,
|
|
231
372
|
bytes[11]!,
|
|
232
373
|
);
|
|
233
|
-
return HEIF_FTYP_BRANDS.has(brand);
|
|
374
|
+
return HEIF_FTYP_BRANDS.has(brand) ? brand : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Content-based HEIF/HEIC detection. MIME metadata is unreliable here:
|
|
379
|
+
* Chromium reports an empty `file.type` for `.heic`, which clients coerce to
|
|
380
|
+
* `application/octet-stream`.
|
|
381
|
+
*/
|
|
382
|
+
export function isHeifImage(bytes: Uint8Array): boolean {
|
|
383
|
+
return heifFtypBrand(bytes) !== null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Canonical HEIF MIME type for bytes whose container brand names one, and null
|
|
388
|
+
* for everything else. For providers that read HEIF directly and key on the
|
|
389
|
+
* declared media type (Gemini accepts `image/heic` and `image/heif`:
|
|
390
|
+
* https://ai.google.dev/gemini-api/docs/image-understanding).
|
|
391
|
+
*/
|
|
392
|
+
export function heifImageMimeType(bytes: Uint8Array): string | null {
|
|
393
|
+
const brand = heifFtypBrand(bytes);
|
|
394
|
+
if (!brand) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
return HEIC_FTYP_BRANDS.has(brand) ? "image/heic" : "image/heif";
|
|
234
398
|
}
|
|
235
399
|
|
|
236
400
|
/**
|
package/src/util/platform.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { SEEDS } from "@vellumai/environments";
|
|
6
6
|
|
|
@@ -24,13 +24,17 @@ const VELLUM_ROOT = join(homedir(), ".vellum");
|
|
|
24
24
|
*/
|
|
25
25
|
export function vellumRoot(): string {
|
|
26
26
|
const override = getWorkspaceDirOverride();
|
|
27
|
+
let root = VELLUM_ROOT;
|
|
27
28
|
if (override) {
|
|
28
29
|
const parent = dirname(override);
|
|
29
30
|
if (parent !== "/") {
|
|
30
|
-
|
|
31
|
+
root = parent;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
// Same containment rule as getWorkspaceDir(): root-derived paths (protected
|
|
35
|
+
// dir, .env) must stay ephemeral in test processes too.
|
|
36
|
+
assertTestPathIsEphemeral(root);
|
|
37
|
+
return root;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export function isMacOS(): boolean {
|
|
@@ -334,19 +338,98 @@ export function getProcPidPath(name: string): string {
|
|
|
334
338
|
return join(getProcDir(name), `${name}.pid`);
|
|
335
339
|
}
|
|
336
340
|
|
|
341
|
+
// --- Live-workspace guard for test processes --------------------------------
|
|
342
|
+
//
|
|
343
|
+
// A test process must never resolve the workspace (or the vellum root) to a
|
|
344
|
+
// real, non-temp directory: production code exercised by a test would then
|
|
345
|
+
// read and destructively write live state. The tmpdir redirection normally
|
|
346
|
+
// comes from the bunfig.toml test preload, but bun only loads bunfig from the
|
|
347
|
+
// cwd, so `bun test` run from any other directory (for example a source
|
|
348
|
+
// checkout inside a deployed container's workspace) skips the preload and
|
|
349
|
+
// inherits the ambient VELLUM_WORKSPACE_DIR. The containment assertion
|
|
350
|
+
// therefore lives here, in production code, where it fires no matter how the
|
|
351
|
+
// test process was launched.
|
|
352
|
+
//
|
|
353
|
+
// Containment logic mirrors src/__tests__/assert-not-live-db.ts, which cannot
|
|
354
|
+
// be imported here (production code must not depend on test machinery).
|
|
355
|
+
|
|
356
|
+
/** Lazily computed: is this process a `bun test` run? */
|
|
357
|
+
let isTestProcess: boolean | undefined;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Resolve symlinks in the deepest existing ancestor of `p`, then re-append
|
|
361
|
+
* the not-yet-created tail. This keeps the containment check honest both for
|
|
362
|
+
* paths under a symlinked temp root (macOS /var/folders) and for symlinks
|
|
363
|
+
* that point outside it, whether or not the leaf exists yet.
|
|
364
|
+
*/
|
|
365
|
+
function canonicalizeForWorkspaceGuard(p: string): string {
|
|
366
|
+
let cur = p;
|
|
367
|
+
const tail: string[] = [];
|
|
368
|
+
for (;;) {
|
|
369
|
+
try {
|
|
370
|
+
return join(realpathSync(cur), ...tail);
|
|
371
|
+
} catch {
|
|
372
|
+
const parent = dirname(cur);
|
|
373
|
+
if (parent === cur) {
|
|
374
|
+
return p;
|
|
375
|
+
}
|
|
376
|
+
tail.unshift(basename(cur));
|
|
377
|
+
cur = parent;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function assertTestPathIsEphemeral(dir: string): void {
|
|
383
|
+
isTestProcess ??=
|
|
384
|
+
process.env.NODE_ENV === "test" ||
|
|
385
|
+
process.env.BUN_TEST === "1" ||
|
|
386
|
+
// `bun test` sets NODE_ENV=test only when unset; Bun.main being the test
|
|
387
|
+
// file itself is the backstop signal that survives a preset NODE_ENV.
|
|
388
|
+
(typeof Bun !== "undefined" &&
|
|
389
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/.test(Bun.main));
|
|
390
|
+
if (!isTestProcess) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
// Escape hatch for the rare intentional run against a real workspace,
|
|
394
|
+
// shared with assertTestDbIsIsolated() in persistence/db-connection.ts.
|
|
395
|
+
// Deliberately NOT added to tools/terminal/safe-env.ts: a daemon-level
|
|
396
|
+
// opt-out must not propagate into agent-spawned shells and disarm the
|
|
397
|
+
// guard for tests run from there.
|
|
398
|
+
if (process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS === "1") {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const tmpRoot = canonicalizeForWorkspaceGuard(tmpdir());
|
|
402
|
+
const resolved = canonicalizeForWorkspaceGuard(dir);
|
|
403
|
+
if (resolved !== tmpRoot && !resolved.startsWith(tmpRoot + sep)) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
[
|
|
406
|
+
`Refusing to use ${dir} (resolves to ${resolved}) in a test process: it is not under the temp directory (${tmpRoot}).`,
|
|
407
|
+
"",
|
|
408
|
+
"Tests must only touch an ephemeral workspace; a real one would expose",
|
|
409
|
+
"live assistant state to destructive test fixtures. This usually means",
|
|
410
|
+
"`bun test` ran from a cwd without the repo bunfig.toml, so the test",
|
|
411
|
+
"preload that redirects VELLUM_WORKSPACE_DIR to a tmpdir never loaded.",
|
|
412
|
+
"Run tests from the assistant package root, or set",
|
|
413
|
+
"VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS=1 to bypass deliberately.",
|
|
414
|
+
].join("\n"),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
337
419
|
/**
|
|
338
420
|
* Returns the workspace root for user-facing state.
|
|
339
421
|
*
|
|
340
422
|
* When the VELLUM_WORKSPACE_DIR env var is set, returns that value (used in
|
|
341
423
|
* containerized deployments where the workspace is a separate volume).
|
|
342
424
|
* Otherwise falls back to ~/.vellum/workspace.
|
|
425
|
+
*
|
|
426
|
+
* In test processes the resolved directory must live under `os.tmpdir()`
|
|
427
|
+
* (see the live-workspace guard above); anything else throws.
|
|
343
428
|
*/
|
|
344
429
|
export function getWorkspaceDir(): string {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
return join(VELLUM_ROOT, "workspace");
|
|
430
|
+
const dir = getWorkspaceDirOverride() ?? join(VELLUM_ROOT, "workspace");
|
|
431
|
+
assertTestPathIsEphemeral(dir);
|
|
432
|
+
return dir;
|
|
350
433
|
}
|
|
351
434
|
|
|
352
435
|
/**
|