@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
|
@@ -16,6 +16,7 @@ import { isHostProxyTransport } from "../daemon/message-types/conversations.js";
|
|
|
16
16
|
describe("supportsHostProxy (runtime)", () => {
|
|
17
17
|
test("no-arg form returns true for host-proxy interfaces", () => {
|
|
18
18
|
expect(supportsHostProxy("macos")).toBe(true);
|
|
19
|
+
expect(supportsHostProxy("windows")).toBe(true);
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
test("no-arg form returns false for interfaces without host-proxy support", () => {
|
|
@@ -42,13 +43,22 @@ describe("supportsHostProxy (runtime)", () => {
|
|
|
42
43
|
expect(supportsHostProxy("chrome-extension", "host_cu")).toBe(false);
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
test("capability form grants
|
|
46
|
+
test("capability form grants representative host capabilities to macOS", () => {
|
|
46
47
|
expect(supportsHostProxy("macos", "host_bash")).toBe(true);
|
|
47
48
|
expect(supportsHostProxy("macos", "host_file")).toBe(true);
|
|
48
49
|
expect(supportsHostProxy("macos", "host_cu")).toBe(true);
|
|
49
50
|
expect(supportsHostProxy("macos", "host_browser")).toBe(true);
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
test("capability form withholds unimplemented Windows app control", () => {
|
|
54
|
+
expect(supportsHostProxy("windows", "host_bash")).toBe(true);
|
|
55
|
+
expect(supportsHostProxy("windows", "host_file")).toBe(true);
|
|
56
|
+
expect(supportsHostProxy("windows", "host_cu")).toBe(true);
|
|
57
|
+
expect(supportsHostProxy("windows", "host_browser")).toBe(true);
|
|
58
|
+
expect(supportsHostProxy("windows", "host_ui_snapshot")).toBe(true);
|
|
59
|
+
expect(supportsHostProxy("windows", "host_app_control")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
52
62
|
test("capability form rejects everything for non-host-proxy interfaces", () => {
|
|
53
63
|
expect(supportsHostProxy("ios", "host_bash")).toBe(false);
|
|
54
64
|
expect(supportsHostProxy("cli", "host_file")).toBe(false);
|
|
@@ -678,6 +678,14 @@ describe("host_bash — spawn error handling", () => {
|
|
|
678
678
|
// ---------------------------------------------------------------------------
|
|
679
679
|
|
|
680
680
|
describe("host_bash — proxy delegation", () => {
|
|
681
|
+
// Under tmpdir so the live-workspace guard accepts it as a workspace.
|
|
682
|
+
const FAKE_INSTANCE_WS = join(
|
|
683
|
+
tmpdir(),
|
|
684
|
+
"vellum-instance",
|
|
685
|
+
".vellum",
|
|
686
|
+
"workspace",
|
|
687
|
+
);
|
|
688
|
+
|
|
681
689
|
const ROUTING_ENV_KEYS = [
|
|
682
690
|
"VELLUM_WORKSPACE_DIR",
|
|
683
691
|
"VELLUM_DATA_DIR",
|
|
@@ -850,6 +858,11 @@ describe("host_bash — proxy delegation", () => {
|
|
|
850
858
|
for (const key of ROUTING_ENV_KEYS) {
|
|
851
859
|
delete process.env[key];
|
|
852
860
|
}
|
|
861
|
+
// With VELLUM_WORKSPACE_DIR unset, loadConfig resolves the real ~/.vellum
|
|
862
|
+
// fallback (read-only here), so bypass the live-workspace guard for it.
|
|
863
|
+
const originalAllowRealWorkspace =
|
|
864
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
865
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
|
|
853
866
|
|
|
854
867
|
try {
|
|
855
868
|
const proxyResult: ToolExecutionResult = {
|
|
@@ -870,14 +883,20 @@ describe("host_bash — proxy delegation", () => {
|
|
|
870
883
|
__REVEAL_NONCE: expect.any(String),
|
|
871
884
|
});
|
|
872
885
|
} finally {
|
|
886
|
+
if (originalAllowRealWorkspace === undefined) {
|
|
887
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
888
|
+
} else {
|
|
889
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS =
|
|
890
|
+
originalAllowRealWorkspace;
|
|
891
|
+
}
|
|
873
892
|
restoreEnv(envSnapshot);
|
|
874
893
|
}
|
|
875
894
|
});
|
|
876
895
|
|
|
877
896
|
test("propagates daemon routing env vars to proxy for nested assistant CLI calls", async () => {
|
|
878
897
|
const envSnapshot = captureEnv(ROUTING_ENV_KEYS);
|
|
879
|
-
process.env.VELLUM_WORKSPACE_DIR =
|
|
880
|
-
process.env.VELLUM_DATA_DIR = "
|
|
898
|
+
process.env.VELLUM_WORKSPACE_DIR = FAKE_INSTANCE_WS;
|
|
899
|
+
process.env.VELLUM_DATA_DIR = join(FAKE_INSTANCE_WS, "data");
|
|
881
900
|
process.env.VELLUM_ENVIRONMENT = "local";
|
|
882
901
|
process.env.INTERNAL_GATEWAY_BASE_URL = "http://127.0.0.1:7830";
|
|
883
902
|
|
|
@@ -896,8 +915,8 @@ describe("host_bash — proxy delegation", () => {
|
|
|
896
915
|
expect(result).toBe(proxyResult);
|
|
897
916
|
expect(calls.length).toBe(1);
|
|
898
917
|
expect(calls[0].input.env).toEqual({
|
|
899
|
-
VELLUM_WORKSPACE_DIR:
|
|
900
|
-
VELLUM_DATA_DIR: "
|
|
918
|
+
VELLUM_WORKSPACE_DIR: FAKE_INSTANCE_WS,
|
|
919
|
+
VELLUM_DATA_DIR: join(FAKE_INSTANCE_WS, "data"),
|
|
901
920
|
VELLUM_ENVIRONMENT: "local",
|
|
902
921
|
INTERNAL_GATEWAY_BASE_URL: "http://127.0.0.1:7830",
|
|
903
922
|
__CONVERSATION_ID: "test-conversation",
|
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
* gated on darwin.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { existsSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { tmpdir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { beforeAll, describe, expect, test } from "bun:test";
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
convertImageToJpeg,
|
|
17
|
+
hasValidJpegStructure,
|
|
18
|
+
isCompleteJpeg,
|
|
16
19
|
isHeifImage,
|
|
17
20
|
jpegFilenameFor,
|
|
18
21
|
normalizeImageBase64,
|
|
@@ -131,6 +134,119 @@ describe("sniffImageMimeType", () => {
|
|
|
131
134
|
});
|
|
132
135
|
});
|
|
133
136
|
|
|
137
|
+
describe("isCompleteJpeg", () => {
|
|
138
|
+
test("accepts SOI...EOI framed payloads", () => {
|
|
139
|
+
expect(
|
|
140
|
+
isCompleteJpeg(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0xff, 0xd9])),
|
|
141
|
+
).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("rejects empty, truncated, and non-JPEG payloads", () => {
|
|
145
|
+
expect(isCompleteJpeg(Buffer.alloc(0))).toBe(false);
|
|
146
|
+
// Valid head, torn tail: the poisoned-cache signature.
|
|
147
|
+
expect(isCompleteJpeg(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]))).toBe(
|
|
148
|
+
false,
|
|
149
|
+
);
|
|
150
|
+
expect(isCompleteJpeg(PNG_1PX_BYTES)).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Mirrors the converter's cache-key derivation so the test can plant a
|
|
155
|
+
// poisoned entry at the exact path a conversion will consult.
|
|
156
|
+
function cacheKeyFor(bytes: Uint8Array, quality: number): string {
|
|
157
|
+
const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
|
|
158
|
+
return `${hash}-full-q${quality}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Minimal structurally valid JPEG: SOI, APP0 (len 4, 2 payload bytes),
|
|
162
|
+
// SOF0 (len 5, 3 payload bytes), SOS (len 3, 1 payload byte), entropy data
|
|
163
|
+
// with a stuffed FF, then EOI.
|
|
164
|
+
function minimalJpeg(extra: {
|
|
165
|
+
trailing?: Uint8Array;
|
|
166
|
+
dropEoi?: boolean;
|
|
167
|
+
}): Buffer {
|
|
168
|
+
const parts: Uint8Array[] = [
|
|
169
|
+
Buffer.from([0xff, 0xd8]), // SOI
|
|
170
|
+
Buffer.from([0xff, 0xe0, 0x00, 0x04, 0x4a, 0x46]), // APP0
|
|
171
|
+
Buffer.from([0xff, 0xc0, 0x00, 0x05, 0x08, 0x00, 0x01]), // SOF0
|
|
172
|
+
Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01]), // SOS header
|
|
173
|
+
Buffer.from([0x12, 0xff, 0x00, 0x34]), // entropy data with stuffed FF
|
|
174
|
+
];
|
|
175
|
+
if (!extra.dropEoi) {
|
|
176
|
+
parts.push(Buffer.from([0xff, 0xd9])); // EOI
|
|
177
|
+
}
|
|
178
|
+
if (extra.trailing) {
|
|
179
|
+
parts.push(extra.trailing);
|
|
180
|
+
}
|
|
181
|
+
return Buffer.concat(parts);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
describe("hasValidJpegStructure", () => {
|
|
185
|
+
test("accepts a JPEG with trailing bytes after the EOI marker", () => {
|
|
186
|
+
// Some encoders append padding or metadata past the EOI; the structural
|
|
187
|
+
// walk tolerates it where isCompleteJpeg's exact tail framing does not.
|
|
188
|
+
const padded = minimalJpeg({ trailing: Buffer.from([0x00, 0x00]) });
|
|
189
|
+
expect(hasValidJpegStructure(padded)).toBe(true);
|
|
190
|
+
expect(isCompleteJpeg(padded)).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("accepts restart markers inside entropy-coded data", () => {
|
|
194
|
+
const withRst = Buffer.concat([
|
|
195
|
+
Buffer.from([0xff, 0xd8]),
|
|
196
|
+
Buffer.from([0xff, 0xc0, 0x00, 0x05, 0x08, 0x00, 0x01]), // SOF0
|
|
197
|
+
Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01]),
|
|
198
|
+
Buffer.from([0x12, 0xff, 0xd0, 0x34]), // RST0 continues the scan
|
|
199
|
+
Buffer.from([0xff, 0xd9]),
|
|
200
|
+
]);
|
|
201
|
+
expect(hasValidJpegStructure(withRst)).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("rejects a truncated JPEG whose metadata segment contains FF D9", () => {
|
|
205
|
+
// An EXIF-style APP1 payload can embed a complete thumbnail JPEG, so an
|
|
206
|
+
// FF D9 pair appears inside the segment while the main image is torn. A
|
|
207
|
+
// raw byte search would accept this; the marker walk skips the segment
|
|
208
|
+
// payload and reports the truncation.
|
|
209
|
+
const tornWithThumbnail = Buffer.concat([
|
|
210
|
+
Buffer.from([0xff, 0xd8]), // SOI
|
|
211
|
+
Buffer.from([0xff, 0xe1, 0x00, 0x08]), // APP1, length 8
|
|
212
|
+
Buffer.from([0x45, 0x78, 0xff, 0xd9, 0x00, 0x00]), // payload with FF D9
|
|
213
|
+
Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01, 0x12, 0x34]), // torn scan
|
|
214
|
+
]);
|
|
215
|
+
expect(hasValidJpegStructure(tornWithThumbnail)).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("rejects truncated JPEGs and non-JPEG payloads", () => {
|
|
219
|
+
expect(hasValidJpegStructure(minimalJpeg({ dropEoi: true }))).toBe(false);
|
|
220
|
+
expect(
|
|
221
|
+
hasValidJpegStructure(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00])),
|
|
222
|
+
).toBe(false);
|
|
223
|
+
expect(hasValidJpegStructure(Buffer.alloc(0))).toBe(false);
|
|
224
|
+
expect(hasValidJpegStructure(PNG_1PX_BYTES)).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("rejects a top-level EOI with no frame or scan", () => {
|
|
228
|
+
// Bare SOI+EOI is structurally walkable but carries no image data;
|
|
229
|
+
// providers reject it, so the gate must too.
|
|
230
|
+
expect(hasValidJpegStructure(Buffer.from([0xff, 0xd8, 0xff, 0xd9]))).toBe(
|
|
231
|
+
false,
|
|
232
|
+
);
|
|
233
|
+
// A scan without a frame header is equally undecodable.
|
|
234
|
+
const scanNoFrame = Buffer.concat([
|
|
235
|
+
Buffer.from([0xff, 0xd8]),
|
|
236
|
+
Buffer.from([0xff, 0xda, 0x00, 0x03, 0x01, 0x12]),
|
|
237
|
+
Buffer.from([0xff, 0xd9]),
|
|
238
|
+
]);
|
|
239
|
+
expect(hasValidJpegStructure(scanNoFrame)).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("rejects a segment length that runs past the buffer", () => {
|
|
243
|
+
// A declared length larger than the remaining bytes is truncation, not a
|
|
244
|
+
// reason to scan beyond the buffer.
|
|
245
|
+
const overrun = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0xff, 0xff, 0x00]);
|
|
246
|
+
expect(hasValidJpegStructure(overrun)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
134
250
|
describe("normalizeImageBytes passthrough", () => {
|
|
135
251
|
test("non-HEIF bytes pass through untouched", async () => {
|
|
136
252
|
const result = await normalizeImageBytes("image/png", PNG_1PX_BYTES);
|
|
@@ -205,6 +321,9 @@ describe.skipIf(process.platform !== "darwin")(
|
|
|
205
321
|
const converted = await convertImageToJpeg(heicBytes);
|
|
206
322
|
expect(converted).not.toBeNull();
|
|
207
323
|
expect(startsWithJpegMagic(converted!)).toBe(true);
|
|
324
|
+
// A real encoder's output must pass the structural walk, or the
|
|
325
|
+
// compactor gate would drop every legitimately converted image.
|
|
326
|
+
expect(hasValidJpegStructure(converted!)).toBe(true);
|
|
208
327
|
});
|
|
209
328
|
|
|
210
329
|
test("conversion options produce distinct outputs (cache key isolation)", async () => {
|
|
@@ -228,6 +347,29 @@ describe.skipIf(process.platform !== "darwin")(
|
|
|
228
347
|
expect(first!.equals(second!)).toBe(true);
|
|
229
348
|
});
|
|
230
349
|
|
|
350
|
+
test("a poisoned (truncated) cache entry is discarded, not returned", async () => {
|
|
351
|
+
// Distinct quality so this test owns its cache key.
|
|
352
|
+
const quality = 77;
|
|
353
|
+
const cacheDir = join(tmpdir(), "vellum-optimized-images");
|
|
354
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
355
|
+
const poisonedPath = join(
|
|
356
|
+
cacheDir,
|
|
357
|
+
`${cacheKeyFor(heicBytes, quality)}.jpg`,
|
|
358
|
+
);
|
|
359
|
+
// A torn write: valid JPEG head, no EOI tail. readFromCache must
|
|
360
|
+
// discard it rather than return it as the converted image.
|
|
361
|
+
writeFileSync(poisonedPath, Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]));
|
|
362
|
+
|
|
363
|
+
const converted = await convertImageToJpeg(heicBytes, { quality });
|
|
364
|
+
expect(converted).not.toBeNull();
|
|
365
|
+
expect(isCompleteJpeg(converted!)).toBe(true);
|
|
366
|
+
|
|
367
|
+
// The poisoned entry was replaced by the re-converted result.
|
|
368
|
+
expect(existsSync(poisonedPath)).toBe(true);
|
|
369
|
+
const { readFileSync } = await import("node:fs");
|
|
370
|
+
expect(isCompleteJpeg(readFileSync(poisonedPath))).toBe(true);
|
|
371
|
+
});
|
|
372
|
+
|
|
231
373
|
test("normalizeImageBytes converts to a JPEG master", async () => {
|
|
232
374
|
const result = await normalizeImageBytes("image/heic", heicBytes);
|
|
233
375
|
expect(result.converted).toBe(true);
|
|
@@ -26,6 +26,7 @@ process.env.GATEWAY_SECURITY_DIR = join(testDir, "protected");
|
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
28
|
installIpcMock,
|
|
29
|
+
lastIpcParams,
|
|
29
30
|
mockIpcResponse,
|
|
30
31
|
} from "./helpers/gateway-classify-mock.js";
|
|
31
32
|
installIpcMock();
|
|
@@ -33,6 +34,7 @@ mockIpcResponse("classify_risk", {
|
|
|
33
34
|
risk: "low",
|
|
34
35
|
reason: "skill_load",
|
|
35
36
|
matchType: "unknown",
|
|
37
|
+
scopeOptions: [],
|
|
36
38
|
});
|
|
37
39
|
mockIpcResponse("get_global_thresholds", {
|
|
38
40
|
interactive: "low",
|
|
@@ -42,8 +44,7 @@ mockIpcResponse("get_global_thresholds", {
|
|
|
42
44
|
|
|
43
45
|
// ── Imports (after mocks) ─────────────────────────────────────────────────
|
|
44
46
|
|
|
45
|
-
import { check,
|
|
46
|
-
import { clearRiskCache } from "../permissions/checker.js";
|
|
47
|
+
import { check, classifyRisk } from "../permissions/checker.js";
|
|
47
48
|
import { _clearGlobalCacheForTesting } from "../permissions/gateway-threshold-reader.js";
|
|
48
49
|
import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
|
|
49
50
|
|
|
@@ -82,13 +83,34 @@ function writeDynamicSkill(
|
|
|
82
83
|
);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* What the gateway skill classifier answers for a load whose params carry
|
|
88
|
+
* `hasInlineExpansions` (gateway/src/risk/skill-risk-classifier.ts): High,
|
|
89
|
+
* registry-matched. The daemon does not elevate locally; the gateway is the
|
|
90
|
+
* one place a dynamic load becomes High.
|
|
91
|
+
*/
|
|
92
|
+
function mockDynamicSkillClassification(): void {
|
|
93
|
+
mockIpcResponse("classify_risk", {
|
|
94
|
+
risk: "high",
|
|
95
|
+
reason:
|
|
96
|
+
"Skill load with inline command expansions (executes shell commands at load time)",
|
|
97
|
+
matchType: "registry",
|
|
98
|
+
scopeOptions: [],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
// ── Tests ─────────────────────────────────────────────────────────────────
|
|
86
103
|
|
|
87
104
|
describe("inline-command skill_load permissions", () => {
|
|
88
105
|
beforeEach(() => {
|
|
89
|
-
clearRiskCache();
|
|
90
106
|
_clearGlobalCacheForTesting();
|
|
91
107
|
setOverridesForTesting({});
|
|
108
|
+
mockIpcResponse("classify_risk", {
|
|
109
|
+
risk: "low",
|
|
110
|
+
reason: "skill_load",
|
|
111
|
+
matchType: "unknown",
|
|
112
|
+
scopeOptions: [],
|
|
113
|
+
});
|
|
92
114
|
mockIpcResponse("get_global_thresholds", {
|
|
93
115
|
interactive: "low",
|
|
94
116
|
autonomous: "medium",
|
|
@@ -116,6 +138,7 @@ describe("inline-command skill_load permissions", () => {
|
|
|
116
138
|
test("an uncovered dynamic skill prompts below Full access", async () => {
|
|
117
139
|
ensureSkillsDir();
|
|
118
140
|
writeDynamicSkill("dynamic-prompt", "Dynamic Prompt Skill");
|
|
141
|
+
mockDynamicSkillClassification();
|
|
119
142
|
|
|
120
143
|
// interactive threshold "low" (beforeEach) is below the High risk of an
|
|
121
144
|
// inline-command load, so it prompts.
|
|
@@ -131,6 +154,7 @@ describe("inline-command skill_load permissions", () => {
|
|
|
131
154
|
test("an uncovered dynamic skill runs at Full access (high threshold)", async () => {
|
|
132
155
|
ensureSkillsDir();
|
|
133
156
|
writeDynamicSkill("dynamic-full", "Dynamic Full Access Skill");
|
|
157
|
+
mockDynamicSkillClassification();
|
|
134
158
|
mockIpcResponse("get_global_thresholds", {
|
|
135
159
|
interactive: "high",
|
|
136
160
|
autonomous: "high",
|
|
@@ -155,6 +179,7 @@ describe("inline-command skill_load permissions", () => {
|
|
|
155
179
|
risk: "low",
|
|
156
180
|
reason: "user rule: skill_load_dynamic:dynamic-covered",
|
|
157
181
|
matchType: "user_rule",
|
|
182
|
+
scopeOptions: [],
|
|
158
183
|
});
|
|
159
184
|
|
|
160
185
|
const result = await check(
|
|
@@ -163,17 +188,12 @@ describe("inline-command skill_load permissions", () => {
|
|
|
163
188
|
"/tmp",
|
|
164
189
|
);
|
|
165
190
|
expect(result.decision).toBe("allow");
|
|
166
|
-
|
|
167
|
-
mockIpcResponse("classify_risk", {
|
|
168
|
-
risk: "low",
|
|
169
|
-
reason: "skill_load",
|
|
170
|
-
matchType: "unknown",
|
|
171
|
-
});
|
|
172
191
|
});
|
|
173
192
|
|
|
174
193
|
test("dynamic skill prompts in strict mode (no matching rule)", async () => {
|
|
175
194
|
ensureSkillsDir();
|
|
176
195
|
writeDynamicSkill("dynamic-strict", "Dynamic Strict Skill");
|
|
196
|
+
mockDynamicSkillClassification();
|
|
177
197
|
mockIpcResponse("get_global_thresholds", {
|
|
178
198
|
interactive: "none",
|
|
179
199
|
autonomous: "none",
|
|
@@ -211,43 +231,34 @@ describe("inline-command skill_load permissions", () => {
|
|
|
211
231
|
|
|
212
232
|
// ── Allowlist options ────────────────────────────────────────────────
|
|
213
233
|
|
|
214
|
-
describe("
|
|
215
|
-
|
|
234
|
+
describe("skill metadata sent to the gateway", () => {
|
|
235
|
+
// The gateway picks the `skill_load_dynamic:` namespace off these flags,
|
|
236
|
+
// so what the daemon reads from disk decides which rule namespace a user
|
|
237
|
+
// can save into.
|
|
238
|
+
test("a skill with inline expansions is sent as dynamic", async () => {
|
|
216
239
|
ensureSkillsDir();
|
|
217
240
|
writeDynamicSkill("dynamic-opts", "Dynamic Opts Skill");
|
|
218
241
|
|
|
219
|
-
|
|
220
|
-
skill: "dynamic-opts",
|
|
221
|
-
});
|
|
242
|
+
await classifyRisk("skill_load", { skill: "dynamic-opts" });
|
|
222
243
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// Should have an any-version option
|
|
230
|
-
const anyVersionOption = options.find(
|
|
231
|
-
(o) => o.pattern === "skill_load_dynamic:dynamic-opts",
|
|
232
|
-
);
|
|
233
|
-
expect(anyVersionOption).toBeDefined();
|
|
234
|
-
expect(anyVersionOption!.description).toBe("This skill (any version)");
|
|
244
|
+
const metadata = lastIpcParams("classify_risk")?.skillMetadata as
|
|
245
|
+
| { hasInlineExpansions: boolean; isDynamic: boolean }
|
|
246
|
+
| undefined;
|
|
247
|
+
expect(metadata?.hasInlineExpansions).toBe(true);
|
|
248
|
+
expect(metadata?.isDynamic).toBe(true);
|
|
235
249
|
});
|
|
236
250
|
|
|
237
|
-
test("plain skill
|
|
251
|
+
test("a plain skill is not sent as dynamic", async () => {
|
|
238
252
|
ensureSkillsDir();
|
|
239
253
|
writePlainSkill("plain-opts", "Plain Opts Skill");
|
|
240
254
|
|
|
241
|
-
|
|
242
|
-
skill: "plain-opts",
|
|
243
|
-
});
|
|
255
|
+
await classifyRisk("skill_load", { skill: "plain-opts" });
|
|
244
256
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
257
|
+
const metadata = lastIpcParams("classify_risk")?.skillMetadata as
|
|
258
|
+
| { hasInlineExpansions: boolean; isDynamic: boolean }
|
|
259
|
+
| undefined;
|
|
260
|
+
expect(metadata?.hasInlineExpansions).toBe(false);
|
|
261
|
+
expect(metadata?.isDynamic).toBe(false);
|
|
251
262
|
});
|
|
252
263
|
});
|
|
253
264
|
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import type { SkillToolEntry } from "../config/skills.js";
|
|
6
|
+
import {
|
|
7
|
+
bundledToolInputAliases,
|
|
8
|
+
bundledToolInputRepairs,
|
|
9
|
+
} from "../tools/shared/input-repairs.js";
|
|
10
|
+
|
|
11
|
+
const TOOL = "scaffold_managed_skill";
|
|
12
|
+
const FIND_TOOL = "find_similar_skills";
|
|
13
|
+
|
|
14
|
+
/** The real manifest entries the repairs describe. */
|
|
15
|
+
function manifestEntry(name: string): SkillToolEntry {
|
|
16
|
+
const manifest = JSON.parse(
|
|
17
|
+
readFileSync(
|
|
18
|
+
join(
|
|
19
|
+
import.meta.dir,
|
|
20
|
+
"../config/bundled-skills/skill-management/TOOLS.json",
|
|
21
|
+
),
|
|
22
|
+
"utf-8",
|
|
23
|
+
),
|
|
24
|
+
) as { tools: SkillToolEntry[] };
|
|
25
|
+
const entry = manifest.tools.find((t) => t.name === name);
|
|
26
|
+
if (!entry) {
|
|
27
|
+
throw new Error(`${name} is missing from the skill-management TOOLS.json`);
|
|
28
|
+
}
|
|
29
|
+
return entry;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function declaredProperties(name: string): Record<string, { type?: string }> {
|
|
33
|
+
const schema = manifestEntry(name).input_schema as {
|
|
34
|
+
properties: Record<string, { type?: string }>;
|
|
35
|
+
};
|
|
36
|
+
return schema.properties;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("bundledToolInputRepairs: parameter aliases", () => {
|
|
40
|
+
test("reads `body` as the declared `body_markdown`", () => {
|
|
41
|
+
const result = bundledToolInputRepairs(TOOL, {
|
|
42
|
+
skill_id: "deploy",
|
|
43
|
+
body: "# Deploy\n",
|
|
44
|
+
});
|
|
45
|
+
expect(result).toEqual({ skill_id: "deploy", body_markdown: "# Deploy\n" });
|
|
46
|
+
expect("body" in result).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("never overwrites a call that spells the parameter correctly", () => {
|
|
50
|
+
const input = { body: "alias", body_markdown: "declared" };
|
|
51
|
+
const result = bundledToolInputRepairs(TOOL, input);
|
|
52
|
+
expect(result).toBe(input);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("leaves a tool with no alias table alone", () => {
|
|
56
|
+
const input = { body: "# Deploy\n" };
|
|
57
|
+
expect(bundledToolInputRepairs("file_write", input)).toBe(input);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test.each([TOOL, FIND_TOOL])(
|
|
61
|
+
"%s: no alias shadows a parameter the tool declares",
|
|
62
|
+
(tool) => {
|
|
63
|
+
const declared = declaredProperties(tool);
|
|
64
|
+
const aliases = bundledToolInputAliases(tool);
|
|
65
|
+
expect(aliases.length).toBeGreaterThan(0);
|
|
66
|
+
for (const alias of aliases) {
|
|
67
|
+
expect(declared[alias]).toBeUndefined();
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
test.each([TOOL, FIND_TOOL])(
|
|
73
|
+
"%s: every alias targets a parameter the tool declares",
|
|
74
|
+
(tool) => {
|
|
75
|
+
const declared = declaredProperties(tool);
|
|
76
|
+
for (const alias of bundledToolInputAliases(tool)) {
|
|
77
|
+
const result = bundledToolInputRepairs(tool, { [alias]: "value" });
|
|
78
|
+
const target = Object.keys(result).find((key) => key !== alias);
|
|
79
|
+
expect(target).toBeDefined();
|
|
80
|
+
expect(declared[target!]).toBeDefined();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
test.each([
|
|
86
|
+
["title", "name"],
|
|
87
|
+
["summary", "description"],
|
|
88
|
+
["content", "body_markdown"],
|
|
89
|
+
])("reads `%s` as `%s`", (alias, canonical) => {
|
|
90
|
+
const result = bundledToolInputRepairs(TOOL, { [alias]: "value" });
|
|
91
|
+
expect(result).toEqual({ [canonical]: "value" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test.each(["description", "query"])(
|
|
95
|
+
"find_similar_skills reads `%s` as `goal`",
|
|
96
|
+
(alias) => {
|
|
97
|
+
const result = bundledToolInputRepairs(FIND_TOOL, {
|
|
98
|
+
[alias]: "deploy staging",
|
|
99
|
+
});
|
|
100
|
+
expect(result).toEqual({ goal: "deploy staging" });
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("bundledToolInputRepairs: files written as a path-keyed map", () => {
|
|
106
|
+
test("reads string values as each file's contents", () => {
|
|
107
|
+
const result = bundledToolInputRepairs(TOOL, {
|
|
108
|
+
files: {
|
|
109
|
+
"references/failure-modes.md": "The retry loop stalls.",
|
|
110
|
+
"scripts/run.sh": "echo hi",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
expect(result.files).toEqual([
|
|
114
|
+
{
|
|
115
|
+
path: "references/failure-modes.md",
|
|
116
|
+
content: "The retry loop stalls.",
|
|
117
|
+
},
|
|
118
|
+
{ path: "scripts/run.sh", content: "echo hi" },
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("reads an entry that names content or copy_from", () => {
|
|
123
|
+
const result = bundledToolInputRepairs(TOOL, {
|
|
124
|
+
files: {
|
|
125
|
+
"references/notes.md": { content: "hi" },
|
|
126
|
+
"scripts/run.sh": { copy_from: "/tmp/run.sh" },
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
expect(result.files).toEqual([
|
|
130
|
+
{ path: "references/notes.md", content: "hi" },
|
|
131
|
+
{ path: "scripts/run.sh", copy_from: "/tmp/run.sh" },
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("leaves one entry spelling out its own fields, which is a file not a map", () => {
|
|
136
|
+
const input = { files: { path: "references/notes.md", content: "hi" } };
|
|
137
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("leaves the whole value when any entry is unreadable", () => {
|
|
141
|
+
const input = {
|
|
142
|
+
files: {
|
|
143
|
+
"references/notes.md": "hi",
|
|
144
|
+
"scripts/run.sh": { mode: 755 },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("leaves a map with a blank path", () => {
|
|
151
|
+
const input = { files: { " ": "hi" } };
|
|
152
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("leaves an empty object", () => {
|
|
156
|
+
const input = { files: {} };
|
|
157
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("an entry spelling out every declared file property is read as one file", () => {
|
|
161
|
+
const schema = manifestEntry(TOOL).input_schema as {
|
|
162
|
+
properties: {
|
|
163
|
+
files: { items: { properties: Record<string, unknown> } };
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const declared = Object.keys(schema.properties.files.items.properties);
|
|
167
|
+
expect(declared).toContain("path");
|
|
168
|
+
const entry = Object.fromEntries(declared.map((key) => [key, "value"]));
|
|
169
|
+
|
|
170
|
+
const input = { files: entry };
|
|
171
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("leaves files that already arrived as an array", () => {
|
|
175
|
+
const input = { files: [{ path: "references/notes.md", content: "hi" }] };
|
|
176
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("bundledToolInputRepairs: purity", () => {
|
|
181
|
+
test("returns the same object when there is nothing to repair", () => {
|
|
182
|
+
const input = { skill_id: "deploy", body_markdown: "# Deploy\n" };
|
|
183
|
+
expect(bundledToolInputRepairs(TOOL, input)).toBe(input);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("returns a new object and never mutates the original input", () => {
|
|
187
|
+
const input = {
|
|
188
|
+
body: "# Deploy\n",
|
|
189
|
+
files: { "references/notes.md": "hi" },
|
|
190
|
+
};
|
|
191
|
+
const snapshot = JSON.parse(JSON.stringify(input));
|
|
192
|
+
const result = bundledToolInputRepairs(TOOL, input);
|
|
193
|
+
expect(result).not.toBe(input);
|
|
194
|
+
expect(input).toEqual(snapshot);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("repairs an alias and a files map in the same call", () => {
|
|
198
|
+
const result = bundledToolInputRepairs(TOOL, {
|
|
199
|
+
body: "# Deploy\n",
|
|
200
|
+
files: { "references/notes.md": "hi" },
|
|
201
|
+
});
|
|
202
|
+
expect(result).toEqual({
|
|
203
|
+
body_markdown: "# Deploy\n",
|
|
204
|
+
files: [{ path: "references/notes.md", content: "hi" }],
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|