@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
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
2
|
import { dirname, join, resolve } from "node:path";
|
|
4
3
|
|
|
@@ -19,10 +18,6 @@ import { getSkillRoots } from "../skills/path-classifier.js";
|
|
|
19
18
|
import { computeTransitiveSkillVersionHash } from "../skills/transitive-version-hash.js";
|
|
20
19
|
import { computeSkillVersionHash } from "../skills/version-hash.js";
|
|
21
20
|
import type { ManifestOverride } from "../tools/execution-target.js";
|
|
22
|
-
import {
|
|
23
|
-
looksLikeHostPortShorthand,
|
|
24
|
-
looksLikePathOnlyInput,
|
|
25
|
-
} from "../tools/network/url-safety.js";
|
|
26
21
|
import { getTool, getToolOwner, resolveTool } from "../tools/registry.js";
|
|
27
22
|
import { resolveRealPath } from "../tools/shared/filesystem/path-policy.js";
|
|
28
23
|
import type { Tool } from "../tools/types.js";
|
|
@@ -46,9 +41,7 @@ import {
|
|
|
46
41
|
getAutoApproveThreshold,
|
|
47
42
|
refreshAutoApproveThreshold,
|
|
48
43
|
} from "./gateway-threshold-reader.js";
|
|
49
|
-
import type { RiskAssessment } from "./risk-types.js";
|
|
50
44
|
import {
|
|
51
|
-
type AllowlistOption,
|
|
52
45
|
type PermissionCheckResult,
|
|
53
46
|
type PolicyContext,
|
|
54
47
|
RiskLevel,
|
|
@@ -60,114 +53,19 @@ import {
|
|
|
60
53
|
resolveSandboxBase,
|
|
61
54
|
} from "./workspace-policy.js";
|
|
62
55
|
|
|
63
|
-
// ── Risk classification cache ────────────────────────────────────────────────
|
|
64
|
-
// classifyRisk() is called on every permission check and delegates to the
|
|
65
|
-
// gateway via IPC. Cache results keyed on
|
|
66
|
-
// (toolName, inputHash, workingDir, manifestOverride).
|
|
67
|
-
// Invalidated when trust rules change since risk classification for file tools
|
|
68
|
-
// depends on skill source path checks which reference config, but the core
|
|
69
|
-
// risk logic is input-deterministic.
|
|
70
|
-
/** The result of classifyRisk(): a risk level with an optional human-readable reason. */
|
|
71
|
-
export interface RiskClassification {
|
|
72
|
-
level: RiskLevel;
|
|
73
|
-
/** Human-readable explanation of why this risk level was assigned. */
|
|
74
|
-
reason?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
56
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
57
|
+
* One gateway classification as the daemon carries it: the `classify_risk`
|
|
58
|
+
* response with `risk` mapped onto the daemon's {@link RiskLevel} as `level`.
|
|
59
|
+
* Produced once per tool invocation by {@link classifyRisk} and handed down
|
|
60
|
+
* through `checkPermission` and {@link check}; the daemon keeps no memo of
|
|
61
|
+
* it, so a trust-rule, config, or skill change is reflected on the next call.
|
|
80
62
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
sandboxAutoApprove?: boolean;
|
|
88
|
-
/**
|
|
89
|
-
* Lexically-resolved path args from the gateway for bash sandbox
|
|
90
|
-
* auto-approve. Stored in the cache so the symlink escape check can be
|
|
91
|
-
* re-run on cache hits (symlink targets may change between calls).
|
|
92
|
-
*/
|
|
93
|
-
sandboxPathArgs?: string[];
|
|
94
|
-
/** Allowlist options from the gateway for generateAllowlistOptions(). */
|
|
95
|
-
allowlistOptions?: AllowlistOption[];
|
|
96
|
-
/** Resolved filesystem path arguments for directory-scoped rule matching. */
|
|
97
|
-
resolvedPaths?: string[];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const RISK_CACHE_MAX = 256;
|
|
101
|
-
const riskCache = new Map<string, RiskClassificationWithMeta>();
|
|
102
|
-
|
|
103
|
-
// ── Assessment cache ─────────────────────────────────────────────────────────
|
|
104
|
-
// Stores the full ClassificationResult from the gateway so that
|
|
105
|
-
// generateAllowlistOptions() can read gateway-produced allowlistOptions
|
|
106
|
-
// without re-classifying. Keyed on (toolName, inputHash) — a simpler key
|
|
107
|
-
// than the full risk cache since generateAllowlistOptions() does not receive
|
|
108
|
-
// workingDir or manifestOverride. Cleared alongside the risk cache.
|
|
109
|
-
const assessmentCache = new Map<string, RiskAssessment>();
|
|
110
|
-
|
|
111
|
-
function assessmentCacheKey(
|
|
112
|
-
toolName: string,
|
|
113
|
-
input: Record<string, unknown>,
|
|
114
|
-
): string {
|
|
115
|
-
const { reason: _reason, activity: _activity, ...cacheableInput } = input;
|
|
116
|
-
const inputJson = JSON.stringify(cacheableInput);
|
|
117
|
-
const hash = createHash("sha256").update(inputJson).digest("hex");
|
|
118
|
-
return `${toolName}\0${hash}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function riskCacheKey(
|
|
122
|
-
toolName: string,
|
|
123
|
-
input: Record<string, unknown>,
|
|
124
|
-
workingDir?: string,
|
|
125
|
-
manifestOverride?: ManifestOverride,
|
|
126
|
-
fsStateKey?: string,
|
|
127
|
-
): string {
|
|
128
|
-
// Strip `reason` and `activity` before computing the cache key — they are
|
|
129
|
-
// cosmetic status text that varies per invocation even for identical tool
|
|
130
|
-
// operations, causing unnecessary cache misses.
|
|
131
|
-
const { reason: _reason, activity: _activity, ...cacheableInput } = input;
|
|
132
|
-
const inputJson = JSON.stringify(cacheableInput);
|
|
133
|
-
const hash = createHash("sha256")
|
|
134
|
-
.update(inputJson)
|
|
135
|
-
.update("\0")
|
|
136
|
-
.update(workingDir ?? "")
|
|
137
|
-
.update("\0")
|
|
138
|
-
.update(manifestOverride ? JSON.stringify(manifestOverride) : "")
|
|
139
|
-
// For file tools, fold in the symlink-resolved target path(s). File risk
|
|
140
|
-
// depends on filesystem state (a symlink can be retargeted under a
|
|
141
|
-
// protected dir between calls), so the same raw input must miss the cache
|
|
142
|
-
// when its canonicalized target changes.
|
|
143
|
-
.update("\0")
|
|
144
|
-
.update(fsStateKey ?? "")
|
|
145
|
-
.digest("hex");
|
|
146
|
-
return `${toolName}\0${hash}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Compute the filesystem-state component of the risk cache key for file tools:
|
|
151
|
-
* the symlink-resolved target path(s). Returns `undefined` for non-file tools
|
|
152
|
-
* (whose risk does not depend on filesystem state).
|
|
153
|
-
*/
|
|
154
|
-
function fileToolFsStateKey(
|
|
155
|
-
toolName: string,
|
|
156
|
-
input: Record<string, unknown>,
|
|
157
|
-
workingDir?: string,
|
|
158
|
-
): string | undefined {
|
|
159
|
-
if (!FILE_TOOL_NAMES.has(toolName)) {
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
const resolved = resolveFileToolPaths(toolName, input, workingDir);
|
|
163
|
-
return `${resolved.resolvedPath ?? ""}\0${resolved.resolvedTransferDestPath ?? ""}\0${resolved.resolvedWorkingDir ?? ""}`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Clear the risk classification cache. Called when trust rules change. Exported for test setup. */
|
|
167
|
-
export function clearRiskCache(): void {
|
|
168
|
-
riskCache.clear();
|
|
169
|
-
assessmentCache.clear();
|
|
170
|
-
}
|
|
63
|
+
export type RiskClassificationWithMeta = Omit<
|
|
64
|
+
ClassifyRiskIpcResponse,
|
|
65
|
+
"risk"
|
|
66
|
+
> & {
|
|
67
|
+
level: RiskLevel;
|
|
68
|
+
};
|
|
171
69
|
|
|
172
70
|
// ── Approval policy singleton ────────────────────────────────────────────────
|
|
173
71
|
const defaultApprovalPolicy = new DefaultApprovalPolicy();
|
|
@@ -347,81 +245,20 @@ function computeTransitiveHashSafe(skillId: string): string | undefined {
|
|
|
347
245
|
}
|
|
348
246
|
}
|
|
349
247
|
|
|
350
|
-
function canonicalizeWebFetchUrl(parsed: URL): URL {
|
|
351
|
-
parsed.hash = "";
|
|
352
|
-
parsed.username = "";
|
|
353
|
-
parsed.password = "";
|
|
354
|
-
|
|
355
|
-
try {
|
|
356
|
-
// Normalize equivalent escaped paths (for example, "/%70rivate" -> "/private")
|
|
357
|
-
// so path-scoped trust rules cannot be bypassed via percent-encoding.
|
|
358
|
-
parsed.pathname = decodeURI(parsed.pathname);
|
|
359
|
-
} catch {
|
|
360
|
-
// Keep URL parser canonical form when decoding fails.
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (parsed.hostname.endsWith(".")) {
|
|
364
|
-
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return parsed;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function normalizeWebFetchUrl(rawUrl: string): URL | null {
|
|
371
|
-
const trimmed = rawUrl.trim();
|
|
372
|
-
if (!trimmed) {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (looksLikeHostPortShorthand(trimmed)) {
|
|
377
|
-
try {
|
|
378
|
-
return canonicalizeWebFetchUrl(new URL(`https://${trimmed}`));
|
|
379
|
-
} catch {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
const parsed = new URL(trimmed);
|
|
386
|
-
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
387
|
-
return canonicalizeWebFetchUrl(parsed);
|
|
388
|
-
}
|
|
389
|
-
return null;
|
|
390
|
-
} catch {
|
|
391
|
-
// Fall through.
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (looksLikePathOnlyInput(trimmed)) {
|
|
395
|
-
return null;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
|
|
399
|
-
return null;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
try {
|
|
403
|
-
return canonicalizeWebFetchUrl(new URL(`https://${trimmed}`));
|
|
404
|
-
} catch {
|
|
405
|
-
return null;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function escapeMinimatchLiteral(value: string): string {
|
|
410
|
-
return value.replace(/([\\*?[\]{}()!+@|])/g, "\\$1");
|
|
411
|
-
}
|
|
412
|
-
|
|
413
248
|
// ── IPC param builders ───────────────────────────────────────────────────────
|
|
414
|
-
// Build the
|
|
249
|
+
// Build the classify_risk request for each tool family. These resolve
|
|
415
250
|
// assistant-local context (file paths, skill metadata, etc.) before
|
|
416
251
|
// forwarding to the gateway.
|
|
417
252
|
|
|
418
253
|
import type {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
254
|
+
ClassifyRiskFileContext,
|
|
255
|
+
ClassifyRiskIpcParams,
|
|
256
|
+
ClassifyRiskIpcResponse,
|
|
257
|
+
ClassifyRiskSkillMetadata,
|
|
258
|
+
RiskLevelValue,
|
|
259
|
+
} from "@vellumai/gateway-client";
|
|
260
|
+
|
|
261
|
+
function buildFileContext(): ClassifyRiskFileContext {
|
|
425
262
|
const config = getConfig();
|
|
426
263
|
// Canonicalize the protected directories via realpath so that a symlinked
|
|
427
264
|
// component anywhere in their path still prefix-matches the canonicalized
|
|
@@ -479,16 +316,6 @@ function resolveClassificationPath(
|
|
|
479
316
|
return resolveRealPath(base);
|
|
480
317
|
}
|
|
481
318
|
|
|
482
|
-
const FILE_TOOL_NAMES = new Set([
|
|
483
|
-
"file_read",
|
|
484
|
-
"file_write",
|
|
485
|
-
"file_edit",
|
|
486
|
-
"host_file_read",
|
|
487
|
-
"host_file_write",
|
|
488
|
-
"host_file_edit",
|
|
489
|
-
"host_file_transfer",
|
|
490
|
-
]);
|
|
491
|
-
|
|
492
319
|
interface FileToolResolution {
|
|
493
320
|
filePath: string;
|
|
494
321
|
effectiveWorkingDir: string;
|
|
@@ -508,10 +335,8 @@ interface FileToolResolution {
|
|
|
508
335
|
|
|
509
336
|
/**
|
|
510
337
|
* Resolve the security-sensitive path(s) of a file tool invocation, including
|
|
511
|
-
* symlink canonicalization
|
|
512
|
-
*
|
|
513
|
-
* symlink targets, so the cache must key on the canonicalized path, not just
|
|
514
|
-
* the raw tool input.
|
|
338
|
+
* symlink canonicalization, for the IPC params: file risk depends on the
|
|
339
|
+
* symlink target, not the raw tool input.
|
|
515
340
|
*/
|
|
516
341
|
function resolveFileToolPaths(
|
|
517
342
|
toolName: string,
|
|
@@ -568,7 +393,9 @@ function resolveFileToolPaths(
|
|
|
568
393
|
};
|
|
569
394
|
}
|
|
570
395
|
|
|
571
|
-
function resolveSkillMetadata(
|
|
396
|
+
function resolveSkillMetadata(
|
|
397
|
+
selector: string,
|
|
398
|
+
): ClassifyRiskSkillMetadata | undefined {
|
|
572
399
|
const resolved = resolveSkillIdAndHash(selector);
|
|
573
400
|
if (!resolved) {
|
|
574
401
|
return undefined;
|
|
@@ -593,7 +420,7 @@ function buildClassifyRiskParams(
|
|
|
593
420
|
input: Record<string, unknown>,
|
|
594
421
|
workingDir?: string,
|
|
595
422
|
manifestOverride?: ManifestOverride,
|
|
596
|
-
):
|
|
423
|
+
): ClassifyRiskIpcParams {
|
|
597
424
|
// ── Bash/host_bash ──
|
|
598
425
|
if (toolName === "bash" || toolName === "host_bash") {
|
|
599
426
|
// Count credential references attached to this invocation.
|
|
@@ -683,7 +510,7 @@ function buildClassifyRiskParams(
|
|
|
683
510
|
// instead of hardcoding medium for unknown tools. When the tool is not in the
|
|
684
511
|
// registry but a manifestOverride provides a risk, use that instead.
|
|
685
512
|
const tool = getTool(toolName);
|
|
686
|
-
let registryDefaultRisk:
|
|
513
|
+
let registryDefaultRisk: RiskLevelValue | undefined;
|
|
687
514
|
if (tool) {
|
|
688
515
|
registryDefaultRisk =
|
|
689
516
|
tool.defaultRiskLevel === RiskLevel.Low
|
|
@@ -719,11 +546,9 @@ function riskStringToLevel(risk: string): RiskLevel {
|
|
|
719
546
|
* with symlink resolution. The gateway's lexical check cannot follow
|
|
720
547
|
* symlinks (no filesystem access), so the daemon resolves each path arg
|
|
721
548
|
* through {@link isPathWithinWorkspaceRoot} (which uses realpathSync) and
|
|
722
|
-
* revokes auto-approve if any escapes the workspace boundary.
|
|
723
|
-
*
|
|
724
|
-
*
|
|
725
|
-
* targets can change between invocations — a path that was safe on the
|
|
726
|
-
* first call may escape on the second if the symlink was retargeted.
|
|
549
|
+
* revokes auto-approve if any escapes the workspace boundary. Runs on every
|
|
550
|
+
* classification, so a symlink retargeted between two invocations of the
|
|
551
|
+
* same command is caught on the second.
|
|
727
552
|
*/
|
|
728
553
|
function applyBashSymlinkEscapeCheck(
|
|
729
554
|
result: RiskClassificationWithMeta,
|
|
@@ -755,31 +580,6 @@ export async function classifyRisk(
|
|
|
755
580
|
): Promise<RiskClassificationWithMeta> {
|
|
756
581
|
signal?.throwIfAborted();
|
|
757
582
|
|
|
758
|
-
// Check cache first.
|
|
759
|
-
const cacheKey = riskCacheKey(
|
|
760
|
-
toolName,
|
|
761
|
-
input,
|
|
762
|
-
workingDir,
|
|
763
|
-
manifestOverride,
|
|
764
|
-
fileToolFsStateKey(toolName, input, workingDir),
|
|
765
|
-
);
|
|
766
|
-
const cached = riskCache.get(cacheKey);
|
|
767
|
-
if (cached !== undefined) {
|
|
768
|
-
// LRU refresh
|
|
769
|
-
riskCache.delete(cacheKey);
|
|
770
|
-
riskCache.set(cacheKey, cached);
|
|
771
|
-
// Re-run the symlink escape check on cache hits: symlink targets can
|
|
772
|
-
// change between invocations, so a path that was safe when cached may
|
|
773
|
-
// now escape. Return a shallow copy so the cache entry is not mutated.
|
|
774
|
-
if (cached.sandboxPathArgs && cached.sandboxPathArgs.length > 0) {
|
|
775
|
-
const fresh = { ...cached };
|
|
776
|
-
applyBashSymlinkEscapeCheck(fresh, cached.sandboxPathArgs);
|
|
777
|
-
return fresh;
|
|
778
|
-
}
|
|
779
|
-
return cached;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
// ── Delegate to gateway via IPC ────────────────────────────────────────────
|
|
783
583
|
const ipcParams = buildClassifyRiskParams(
|
|
784
584
|
toolName,
|
|
785
585
|
input,
|
|
@@ -797,15 +597,10 @@ export async function classifyRisk(
|
|
|
797
597
|
);
|
|
798
598
|
}
|
|
799
599
|
|
|
600
|
+
const { risk, ...carried } = gatewayResult;
|
|
800
601
|
const result: RiskClassificationWithMeta = {
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
commandCandidates: gatewayResult.commandCandidates,
|
|
804
|
-
actionKeys: gatewayResult.actionKeys,
|
|
805
|
-
sandboxAutoApprove: gatewayResult.sandboxAutoApprove,
|
|
806
|
-
sandboxPathArgs: gatewayResult.sandboxPathArgs,
|
|
807
|
-
allowlistOptions: gatewayResult.allowlistOptions,
|
|
808
|
-
resolvedPaths: gatewayResult.resolvedPaths,
|
|
602
|
+
...carried,
|
|
603
|
+
level: riskStringToLevel(risk),
|
|
809
604
|
};
|
|
810
605
|
|
|
811
606
|
// ── Symlink escape check for bash sandbox auto-approve ───────────────
|
|
@@ -815,41 +610,8 @@ export async function classifyRisk(
|
|
|
815
610
|
// `ln -s /etc /workspace/escape`) would pass the lexical check and
|
|
816
611
|
// be auto-approved. Resolve the gateway-provided path args through
|
|
817
612
|
// symlinks here and revoke auto-approve if any escapes the workspace.
|
|
818
|
-
// The check is also re-run on cache hits (see above) because symlink
|
|
819
|
-
// targets can change between invocations.
|
|
820
613
|
applyBashSymlinkEscapeCheck(result, gatewayResult.sandboxPathArgs);
|
|
821
614
|
|
|
822
|
-
// Cache the result.
|
|
823
|
-
if (riskCache.size >= RISK_CACHE_MAX) {
|
|
824
|
-
const oldest = riskCache.keys().next().value;
|
|
825
|
-
if (oldest !== undefined) {
|
|
826
|
-
riskCache.delete(oldest);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
riskCache.set(cacheKey, result);
|
|
830
|
-
|
|
831
|
-
// Store a RiskAssessment-shaped entry in the assessment cache so that
|
|
832
|
-
// generateAllowlistOptions() can retrieve gateway-produced allowlistOptions
|
|
833
|
-
// and permission-checker.ts can populate riskScopeOptions for the Rule
|
|
834
|
-
// Editor Modal via cachedAssessment.scopeOptions.
|
|
835
|
-
const assessment: RiskAssessment = {
|
|
836
|
-
riskLevel: gatewayResult.risk === "unknown" ? "medium" : gatewayResult.risk,
|
|
837
|
-
reason: gatewayResult.reason,
|
|
838
|
-
scopeOptions: gatewayResult.scopeOptions ?? [],
|
|
839
|
-
matchType: gatewayResult.matchType ?? "unknown",
|
|
840
|
-
allowlistOptions: gatewayResult.allowlistOptions,
|
|
841
|
-
directoryScopeOptions: gatewayResult.directoryScopeOptions,
|
|
842
|
-
resolvedPaths: gatewayResult.resolvedPaths,
|
|
843
|
-
};
|
|
844
|
-
const aKey = assessmentCacheKey(toolName, input);
|
|
845
|
-
if (assessmentCache.size >= RISK_CACHE_MAX) {
|
|
846
|
-
const oldest = assessmentCache.keys().next().value;
|
|
847
|
-
if (oldest !== undefined) {
|
|
848
|
-
assessmentCache.delete(oldest);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
assessmentCache.set(aKey, assessment);
|
|
852
|
-
|
|
853
615
|
return result;
|
|
854
616
|
}
|
|
855
617
|
|
|
@@ -899,6 +661,14 @@ function isRetrospectiveSkillAuthoringGrant(
|
|
|
899
661
|
return false;
|
|
900
662
|
}
|
|
901
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Decide allow / prompt / deny for one tool invocation.
|
|
666
|
+
*
|
|
667
|
+
* `classification` is the invocation's gateway classification when the caller
|
|
668
|
+
* already holds it (the executor classifies once, before its gates, and hands
|
|
669
|
+
* it down through `checkPermission`); a caller without one gets a fresh
|
|
670
|
+
* classification of the same input here.
|
|
671
|
+
*/
|
|
902
672
|
export async function check(
|
|
903
673
|
toolName: string,
|
|
904
674
|
input: Record<string, unknown>,
|
|
@@ -906,6 +676,7 @@ export async function check(
|
|
|
906
676
|
policyContext?: PolicyContext,
|
|
907
677
|
manifestOverride?: ManifestOverride,
|
|
908
678
|
signal?: AbortSignal,
|
|
679
|
+
classification?: RiskClassificationWithMeta,
|
|
909
680
|
): Promise<PermissionCheckResult> {
|
|
910
681
|
signal?.throwIfAborted();
|
|
911
682
|
|
|
@@ -917,7 +688,7 @@ export async function check(
|
|
|
917
688
|
};
|
|
918
689
|
}
|
|
919
690
|
|
|
920
|
-
|
|
691
|
+
classification ??= await classifyRisk(
|
|
921
692
|
toolName,
|
|
922
693
|
input,
|
|
923
694
|
workingDir,
|
|
@@ -926,24 +697,7 @@ export async function check(
|
|
|
926
697
|
signal,
|
|
927
698
|
);
|
|
928
699
|
|
|
929
|
-
const { level:
|
|
930
|
-
|
|
931
|
-
// Inline-command ("dynamic") skill loads execute embedded shell at load time
|
|
932
|
-
// via child_process.spawn, outside the tool-approval pipeline that the
|
|
933
|
-
// auto-approve threshold governs. Treat an uncovered one as High so the
|
|
934
|
-
// standard threshold decides it like any other high-risk action: it runs at
|
|
935
|
-
// Full access (autoApproveUpTo "high") and prompts below it. A covering user
|
|
936
|
-
// trust rule arrives as matchType
|
|
937
|
-
// "user_rule" with the risk already lowered (the escape hatch), so leave it
|
|
938
|
-
// untouched. The gateway classifier is authoritative and also returns High;
|
|
939
|
-
// this local elevation is defense-in-depth for the gateway-unreachable or
|
|
940
|
-
// under-classified path. The separate non-interactive denial (no human to
|
|
941
|
-
// approve embedded shell) lives in tools/permission-checker.ts.
|
|
942
|
-
const risk =
|
|
943
|
-
isDynamicSkillLoadInvocation(toolName, input) &&
|
|
944
|
-
getCachedAssessment(toolName, input)?.matchType !== "user_rule"
|
|
945
|
-
? RiskLevel.High
|
|
946
|
-
: classifiedRisk;
|
|
700
|
+
const { level: risk, reason: riskReason } = classification;
|
|
947
701
|
|
|
948
702
|
// Use gateway-provided sandboxAutoApprove instead of evaluating locally.
|
|
949
703
|
const hasSandboxAutoApprove = classification.sandboxAutoApprove ?? false;
|
|
@@ -1021,276 +775,6 @@ export async function check(
|
|
|
1021
775
|
};
|
|
1022
776
|
}
|
|
1023
777
|
|
|
1024
|
-
const TOOL_DISPLAY_NAMES: Record<string, string> = {
|
|
1025
|
-
file_read: "file reads",
|
|
1026
|
-
file_write: "file writes",
|
|
1027
|
-
file_edit: "file edits",
|
|
1028
|
-
host_file_read: "host file reads",
|
|
1029
|
-
host_file_write: "host file writes",
|
|
1030
|
-
host_file_edit: "host file edits",
|
|
1031
|
-
host_file_transfer: "host file transfers",
|
|
1032
|
-
web_fetch: "URL fetches",
|
|
1033
|
-
network_request: "network requests",
|
|
1034
|
-
};
|
|
1035
|
-
|
|
1036
|
-
function friendlyBasename(filePath: string): string {
|
|
1037
|
-
const parts = filePath.split("/");
|
|
1038
|
-
return parts[parts.length - 1] || filePath;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function friendlyHostname(url: URL): string {
|
|
1042
|
-
return url.hostname.replace(/^www\./, "");
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// ── Per-tool allowlist option strategies ─────────────────────────────────────
|
|
1046
|
-
// Each strategy receives the tool name and raw input and returns allowlist
|
|
1047
|
-
// options. Adding support for a new tool type means adding a function here
|
|
1048
|
-
// and registering it in ALLOWLIST_STRATEGIES below.
|
|
1049
|
-
|
|
1050
|
-
type AllowlistStrategy = (
|
|
1051
|
-
toolName: string,
|
|
1052
|
-
input: Record<string, unknown>,
|
|
1053
|
-
) => Promise<AllowlistOption[]> | AllowlistOption[];
|
|
1054
|
-
|
|
1055
|
-
function fileAllowlistStrategy(
|
|
1056
|
-
toolName: string,
|
|
1057
|
-
input: Record<string, unknown>,
|
|
1058
|
-
): AllowlistOption[] {
|
|
1059
|
-
let filePath: string;
|
|
1060
|
-
if (toolName === "host_file_transfer") {
|
|
1061
|
-
// Use the host-side path: source_path for to_sandbox, dest_path for to_host.
|
|
1062
|
-
const direction = (input.direction as string) ?? "";
|
|
1063
|
-
filePath =
|
|
1064
|
-
direction === "to_sandbox"
|
|
1065
|
-
? ((input.source_path as string) ?? "")
|
|
1066
|
-
: ((input.dest_path as string) ?? "");
|
|
1067
|
-
} else {
|
|
1068
|
-
filePath =
|
|
1069
|
-
(input.path as string) ??
|
|
1070
|
-
(input.file_path as string) ??
|
|
1071
|
-
(input.dest_path as string) ??
|
|
1072
|
-
(input.source_path as string) ??
|
|
1073
|
-
"";
|
|
1074
|
-
}
|
|
1075
|
-
const toolLabel = TOOL_DISPLAY_NAMES[toolName] ?? toolName;
|
|
1076
|
-
const options: AllowlistOption[] = [];
|
|
1077
|
-
|
|
1078
|
-
// Patterns must match the "tool:path" format used by check()
|
|
1079
|
-
options.push({
|
|
1080
|
-
label: filePath,
|
|
1081
|
-
description: `This file only`,
|
|
1082
|
-
pattern: `${toolName}:${filePath}`,
|
|
1083
|
-
});
|
|
1084
|
-
|
|
1085
|
-
// Ancestor directory wildcards — walk up from immediate parent, stop at home dir or /
|
|
1086
|
-
const home = homedir();
|
|
1087
|
-
let dir = dirname(filePath);
|
|
1088
|
-
const maxLevels = 3;
|
|
1089
|
-
let levels = 0;
|
|
1090
|
-
while (dir && dir !== "/" && dir !== "." && levels < maxLevels) {
|
|
1091
|
-
const dirName = friendlyBasename(dir);
|
|
1092
|
-
options.push({
|
|
1093
|
-
label: `${dir}/**`,
|
|
1094
|
-
description: `Anything in ${dirName}/`,
|
|
1095
|
-
pattern: `${toolName}:${dir}/**`,
|
|
1096
|
-
});
|
|
1097
|
-
if (dir === home) {
|
|
1098
|
-
break;
|
|
1099
|
-
}
|
|
1100
|
-
const parent = dirname(dir);
|
|
1101
|
-
if (parent === dir) {
|
|
1102
|
-
break;
|
|
1103
|
-
}
|
|
1104
|
-
dir = parent;
|
|
1105
|
-
levels++;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
options.push({
|
|
1109
|
-
label: `${toolName}:*`,
|
|
1110
|
-
description: `All ${toolLabel}`,
|
|
1111
|
-
pattern: `${toolName}:*`,
|
|
1112
|
-
});
|
|
1113
|
-
return options;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
function urlAllowlistStrategy(
|
|
1117
|
-
toolName: string,
|
|
1118
|
-
input: Record<string, unknown>,
|
|
1119
|
-
): AllowlistOption[] {
|
|
1120
|
-
const rawUrl = getStringField(input, "url").trim();
|
|
1121
|
-
const normalized = normalizeWebFetchUrl(rawUrl);
|
|
1122
|
-
const exact = normalized?.href ?? rawUrl;
|
|
1123
|
-
|
|
1124
|
-
const options: AllowlistOption[] = [];
|
|
1125
|
-
if (exact) {
|
|
1126
|
-
options.push({
|
|
1127
|
-
label: exact,
|
|
1128
|
-
description: "This exact URL",
|
|
1129
|
-
pattern: `${toolName}:${escapeMinimatchLiteral(exact)}`,
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
if (normalized) {
|
|
1133
|
-
const host = friendlyHostname(normalized);
|
|
1134
|
-
options.push({
|
|
1135
|
-
label: `${normalized.origin}/*`,
|
|
1136
|
-
description: `Any page on ${host}`,
|
|
1137
|
-
pattern: `${toolName}:${escapeMinimatchLiteral(normalized.origin)}/*`,
|
|
1138
|
-
});
|
|
1139
|
-
}
|
|
1140
|
-
const toolLabel = TOOL_DISPLAY_NAMES[toolName] ?? toolName;
|
|
1141
|
-
// Use standalone "**" globstar — minimatch only treats ** as globstar when
|
|
1142
|
-
// it is its own path segment, so "${toolName}:*" would fail to match URL
|
|
1143
|
-
// candidates containing "/". The tool field is already filtered separately.
|
|
1144
|
-
options.push({
|
|
1145
|
-
label: `${toolName}:*`,
|
|
1146
|
-
description: `All ${toolLabel}`,
|
|
1147
|
-
pattern: `**`,
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
const seen = new Set<string>();
|
|
1151
|
-
return options.filter((o) => {
|
|
1152
|
-
if (seen.has(o.pattern)) {
|
|
1153
|
-
return false;
|
|
1154
|
-
}
|
|
1155
|
-
seen.add(o.pattern);
|
|
1156
|
-
return true;
|
|
1157
|
-
});
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
function managedSkillAllowlistStrategy(
|
|
1161
|
-
toolName: string,
|
|
1162
|
-
input: Record<string, unknown>,
|
|
1163
|
-
): AllowlistOption[] {
|
|
1164
|
-
const skillId = getStringField(input, "skill_id").trim();
|
|
1165
|
-
const toolLabel =
|
|
1166
|
-
toolName === "scaffold_managed_skill" ? "scaffold" : "delete";
|
|
1167
|
-
const options: AllowlistOption[] = [];
|
|
1168
|
-
if (skillId) {
|
|
1169
|
-
options.push({
|
|
1170
|
-
label: skillId,
|
|
1171
|
-
description: `This skill only`,
|
|
1172
|
-
pattern: `${toolName}:${skillId}`,
|
|
1173
|
-
});
|
|
1174
|
-
}
|
|
1175
|
-
options.push({
|
|
1176
|
-
label: `${toolName}:*`,
|
|
1177
|
-
description: `All managed skill ${toolLabel}s`,
|
|
1178
|
-
pattern: `${toolName}:*`,
|
|
1179
|
-
});
|
|
1180
|
-
return options;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function skillLoadAllowlistStrategy(
|
|
1184
|
-
_toolName: string,
|
|
1185
|
-
input: Record<string, unknown>,
|
|
1186
|
-
): AllowlistOption[] {
|
|
1187
|
-
const rawSelector = getStringField(input, "skill").trim();
|
|
1188
|
-
|
|
1189
|
-
if (rawSelector) {
|
|
1190
|
-
const resolved = resolveSkillIdAndHash(rawSelector);
|
|
1191
|
-
|
|
1192
|
-
if (resolved && hasInlineExpansions(resolved.id)) {
|
|
1193
|
-
const transitiveHash = computeTransitiveHashSafe(resolved.id);
|
|
1194
|
-
const options: AllowlistOption[] = [];
|
|
1195
|
-
if (transitiveHash) {
|
|
1196
|
-
options.push({
|
|
1197
|
-
label: `${resolved.id}@${transitiveHash}`,
|
|
1198
|
-
description: "This exact version (pinned)",
|
|
1199
|
-
pattern: `skill_load_dynamic:${resolved.id}@${transitiveHash}`,
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
options.push({
|
|
1203
|
-
label: resolved.id,
|
|
1204
|
-
description: "This skill (any version)",
|
|
1205
|
-
pattern: `skill_load_dynamic:${resolved.id}`,
|
|
1206
|
-
});
|
|
1207
|
-
return options;
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
if (resolved && resolved.versionHash) {
|
|
1211
|
-
return [
|
|
1212
|
-
{
|
|
1213
|
-
label: `${resolved.id}@${resolved.versionHash}`,
|
|
1214
|
-
description: "This exact version",
|
|
1215
|
-
pattern: `skill_load:${resolved.id}@${resolved.versionHash}`,
|
|
1216
|
-
},
|
|
1217
|
-
];
|
|
1218
|
-
}
|
|
1219
|
-
return [
|
|
1220
|
-
{
|
|
1221
|
-
label: rawSelector,
|
|
1222
|
-
description: "This skill",
|
|
1223
|
-
pattern: `skill_load:${rawSelector}`,
|
|
1224
|
-
},
|
|
1225
|
-
];
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
return [
|
|
1229
|
-
{
|
|
1230
|
-
label: "skill_load:*",
|
|
1231
|
-
description: "All skill loads",
|
|
1232
|
-
pattern: "skill_load:*",
|
|
1233
|
-
},
|
|
1234
|
-
];
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const ALLOWLIST_STRATEGIES: Record<string, AllowlistStrategy> = {
|
|
1238
|
-
file_read: fileAllowlistStrategy,
|
|
1239
|
-
file_write: fileAllowlistStrategy,
|
|
1240
|
-
file_edit: fileAllowlistStrategy,
|
|
1241
|
-
host_file_read: fileAllowlistStrategy,
|
|
1242
|
-
host_file_write: fileAllowlistStrategy,
|
|
1243
|
-
host_file_edit: fileAllowlistStrategy,
|
|
1244
|
-
host_file_transfer: fileAllowlistStrategy,
|
|
1245
|
-
web_fetch: urlAllowlistStrategy,
|
|
1246
|
-
network_request: urlAllowlistStrategy,
|
|
1247
|
-
scaffold_managed_skill: managedSkillAllowlistStrategy,
|
|
1248
|
-
delete_managed_skill: managedSkillAllowlistStrategy,
|
|
1249
|
-
skill_load: skillLoadAllowlistStrategy,
|
|
1250
|
-
};
|
|
1251
|
-
|
|
1252
|
-
export async function generateAllowlistOptions(
|
|
1253
|
-
toolName: string,
|
|
1254
|
-
input: Record<string, unknown>,
|
|
1255
|
-
signal?: AbortSignal,
|
|
1256
|
-
): Promise<AllowlistOption[]> {
|
|
1257
|
-
signal?.throwIfAborted();
|
|
1258
|
-
|
|
1259
|
-
// Use gateway-produced allowlist options from the assessment cache.
|
|
1260
|
-
// For bash/host_bash tools, these are always provided by the gateway.
|
|
1261
|
-
// For other tools that have classifier-produced options, use those too.
|
|
1262
|
-
const aKey = assessmentCacheKey(toolName, input);
|
|
1263
|
-
const cachedAssessment = assessmentCache.get(aKey);
|
|
1264
|
-
if (
|
|
1265
|
-
cachedAssessment?.allowlistOptions &&
|
|
1266
|
-
cachedAssessment.allowlistOptions.length > 0
|
|
1267
|
-
) {
|
|
1268
|
-
return cachedAssessment.allowlistOptions;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
// Fall back to the per-tool strategy function for non-bash tools
|
|
1272
|
-
// or when no cached assessment exists.
|
|
1273
|
-
if (Object.hasOwn(ALLOWLIST_STRATEGIES, toolName)) {
|
|
1274
|
-
return ALLOWLIST_STRATEGIES[toolName](toolName, input);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
return [{ label: "*", description: "Everything", pattern: "*" }];
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
/**
|
|
1281
|
-
* Retrieve a cached RiskAssessment for a given tool invocation.
|
|
1282
|
-
* Returns `undefined` when no classifier-backed assessment exists
|
|
1283
|
-
* (e.g. MCP tools, unknown tools that fall through to registry defaults).
|
|
1284
|
-
*/
|
|
1285
|
-
export function getCachedAssessment(
|
|
1286
|
-
toolName: string,
|
|
1287
|
-
input: Record<string, unknown>,
|
|
1288
|
-
): RiskAssessment | undefined {
|
|
1289
|
-
return assessmentCache.get(assessmentCacheKey(toolName, input));
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
// Directory-based scope only applies to filesystem and shell tools.
|
|
1293
|
-
// All other tools auto-use "everywhere" (the client handles this).
|
|
1294
778
|
export const SCOPE_AWARE_TOOLS = new Set([
|
|
1295
779
|
"bash",
|
|
1296
780
|
"host_bash",
|