@vellumai/assistant 0.11.4 → 0.11.5-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/feature-flag-registry.json +35 -19
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canonicalizeWebUrl,
|
|
5
|
+
looksLikeHostPortShorthand,
|
|
6
|
+
looksLikePathOnlyInput,
|
|
7
|
+
normalizeWebUrl,
|
|
8
|
+
} from "../url-normalization.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* These are the rules a trust rule is saved and matched under. A change here
|
|
12
|
+
* changes which saved rules still match, so each case states the target the
|
|
13
|
+
* two spellings must agree on.
|
|
14
|
+
*/
|
|
15
|
+
describe("normalizeWebUrl", () => {
|
|
16
|
+
test("keeps an ordinary https URL as-is", () => {
|
|
17
|
+
expect(normalizeWebUrl("https://example.com/docs/page")?.href).toBe(
|
|
18
|
+
"https://example.com/docs/page",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("drops the fragment: it never reaches the server", () => {
|
|
23
|
+
expect(normalizeWebUrl("https://example.com/docs#section")?.href).toBe(
|
|
24
|
+
"https://example.com/docs",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("drops userinfo so credentials cannot land in a saved rule", () => {
|
|
29
|
+
const credentialed = new URL("https://example.com/docs/page");
|
|
30
|
+
credentialed.username = "demo";
|
|
31
|
+
credentialed.password = ["c", "r", "e", "d", "1", "2", "3"].join("");
|
|
32
|
+
|
|
33
|
+
const normalized = normalizeWebUrl(credentialed.href);
|
|
34
|
+
expect(normalized?.href).toBe("https://example.com/docs/page");
|
|
35
|
+
expect(normalized?.username).toBe("");
|
|
36
|
+
expect(normalized?.password).toBe("");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("strips a trailing root dot from the hostname", () => {
|
|
40
|
+
expect(normalizeWebUrl("https://example.com./docs/page")?.href).toBe(
|
|
41
|
+
"https://example.com/docs/page",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("decodes escaped path segments so one path has one spelling", () => {
|
|
46
|
+
// Without this, a rule scoped to /private is bypassed by /%70rivate.
|
|
47
|
+
expect(normalizeWebUrl("https://example.com/%70rivate")?.href).toBe(
|
|
48
|
+
"https://example.com/private",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("reads scheme-less input as https", () => {
|
|
53
|
+
expect(normalizeWebUrl("example.com/docs")?.href).toBe(
|
|
54
|
+
"https://example.com/docs",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("reads host:port shorthand as an https origin, not a scheme", () => {
|
|
59
|
+
expect(normalizeWebUrl("example.com:8443/status")?.origin).toBe(
|
|
60
|
+
"https://example.com:8443",
|
|
61
|
+
);
|
|
62
|
+
expect(normalizeWebUrl("[2001:db8::1]:8443/status")?.origin).toBe(
|
|
63
|
+
"https://[2001:db8::1]:8443",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("rejects path-only input rather than coercing it to a host", () => {
|
|
68
|
+
for (const input of ["/etc/passwd", "./rel", "../up", "?q=1", "#frag"]) {
|
|
69
|
+
expect(normalizeWebUrl(input)).toBeNull();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects every non-http scheme", () => {
|
|
74
|
+
for (const input of [
|
|
75
|
+
"file:///etc/passwd",
|
|
76
|
+
"data:text/html,<script>",
|
|
77
|
+
"javascript:alert(1)",
|
|
78
|
+
"ftp://example.com/f",
|
|
79
|
+
]) {
|
|
80
|
+
expect(normalizeWebUrl(input)).toBeNull();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("rejects empty and whitespace-only input", () => {
|
|
85
|
+
expect(normalizeWebUrl("")).toBeNull();
|
|
86
|
+
expect(normalizeWebUrl(" ")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("trims surrounding whitespace", () => {
|
|
90
|
+
expect(normalizeWebUrl(" https://example.com/a ")?.href).toBe(
|
|
91
|
+
"https://example.com/a",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("returns null rather than throwing on an unparseable authority", () => {
|
|
96
|
+
expect(normalizeWebUrl("https://")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("canonicalizeWebUrl", () => {
|
|
101
|
+
test("keeps the parser's form when the path is not decodable, without throwing", () => {
|
|
102
|
+
// `%zz` and a truncated UTF-8 sequence are not valid escapes; the path
|
|
103
|
+
// must survive unchanged rather than the call throwing.
|
|
104
|
+
expect(canonicalizeWebUrl(new URL("https://example.com/100%zz")).href).toBe(
|
|
105
|
+
"https://example.com/100%zz",
|
|
106
|
+
);
|
|
107
|
+
expect(
|
|
108
|
+
canonicalizeWebUrl(new URL("https://example.com/%E0%A4%A")).href,
|
|
109
|
+
).toBe("https://example.com/%E0%A4%A");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("decodes an escaped percent, so `%25` and `%` are one path", () => {
|
|
113
|
+
expect(canonicalizeWebUrl(new URL("https://example.com/100%25")).href).toBe(
|
|
114
|
+
"https://example.com/100%",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("input shape predicates", () => {
|
|
120
|
+
test("host:port shorthand is recognized, scheme-prefixed input is not", () => {
|
|
121
|
+
expect(looksLikeHostPortShorthand("example.com:8443/x")).toBe(true);
|
|
122
|
+
expect(looksLikeHostPortShorthand("[2001:db8::1]:443")).toBe(true);
|
|
123
|
+
expect(looksLikeHostPortShorthand("https://example.com/x")).toBe(false);
|
|
124
|
+
expect(looksLikeHostPortShorthand("example.com/x")).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("path-only input is recognized", () => {
|
|
128
|
+
expect(looksLikePathOnlyInput("/abs")).toBe(true);
|
|
129
|
+
expect(looksLikePathOnlyInput("./rel")).toBe(true);
|
|
130
|
+
expect(looksLikePathOnlyInput("../up")).toBe(true);
|
|
131
|
+
expect(looksLikePathOnlyInput("?q=1")).toBe(true);
|
|
132
|
+
expect(looksLikePathOnlyInput("#frag")).toBe(true);
|
|
133
|
+
expect(looksLikePathOnlyInput("example.com/x")).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical URL normalization for web tool inputs, shared by the gateway's
|
|
3
|
+
* risk classifiers and the daemon's URL safety checks.
|
|
4
|
+
*
|
|
5
|
+
* A URL a model passes to `web_fetch` / `network_request` reaches more than
|
|
6
|
+
* one consumer, and they must agree on what counts as the same target: the
|
|
7
|
+
* gateway builds the trust-rule ladder from it, and anything that later
|
|
8
|
+
* compares a URL to a saved rule has to fold the same spellings together. Two
|
|
9
|
+
* normalizations would mean two answers, so it lives here rather than in
|
|
10
|
+
* either process.
|
|
11
|
+
*
|
|
12
|
+
* Data-only in spirit: no imports, no config, no I/O.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Whether a bare `host:port` (or `[v6]:port`) shorthand was written. */
|
|
16
|
+
export function looksLikeHostPortShorthand(value: string): boolean {
|
|
17
|
+
if (/^\[[0-9a-fA-F:.%]+\]:\d+(?:[/?#]|$)/.test(value)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return /^[^/?#@\s:]+:\d+(?:[/?#]|$)/.test(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether the input is a path, query, or fragment rather than a URL. */
|
|
24
|
+
export function looksLikePathOnlyInput(value: string): boolean {
|
|
25
|
+
return (
|
|
26
|
+
value.startsWith("/") ||
|
|
27
|
+
value.startsWith("./") ||
|
|
28
|
+
value.startsWith("../") ||
|
|
29
|
+
value.startsWith("?") ||
|
|
30
|
+
value.startsWith("#")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strip the parts of a URL that must not affect a trust decision, and fold
|
|
36
|
+
* the encodings that would otherwise let one URL wear two spellings.
|
|
37
|
+
*
|
|
38
|
+
* Percent-escaped path segments are decoded (`/%70rivate` and `/private` are
|
|
39
|
+
* one path, so a path-scoped rule cannot be bypassed by escaping), a trailing
|
|
40
|
+
* root dot is dropped from the hostname, and fragment and userinfo are
|
|
41
|
+
* removed: the fragment never reaches the server, and credentials in the
|
|
42
|
+
* authority must never end up in a saved rule.
|
|
43
|
+
*/
|
|
44
|
+
export function canonicalizeWebUrl(parsed: URL): URL {
|
|
45
|
+
parsed.hash = "";
|
|
46
|
+
parsed.username = "";
|
|
47
|
+
parsed.password = "";
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
parsed.pathname = decodeURI(parsed.pathname);
|
|
51
|
+
} catch {
|
|
52
|
+
// Keep the URL parser's canonical form when decoding fails.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (parsed.hostname.endsWith(".")) {
|
|
56
|
+
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse and canonicalize a web tool's `url` input, or `null` when it is not a
|
|
64
|
+
* URL this system will fetch.
|
|
65
|
+
*
|
|
66
|
+
* Accepts `https://host/path`, `http://…`, bare `host/path`, and `host:port`
|
|
67
|
+
* shorthand. Rejects path-only input and any other scheme (`file:`, `data:`,
|
|
68
|
+
* `javascript:`), so a non-http target can never acquire a web trust rule.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeWebUrl(rawUrl: string): URL | null {
|
|
71
|
+
const trimmed = rawUrl.trim();
|
|
72
|
+
if (!trimmed) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (looksLikeHostPortShorthand(trimmed)) {
|
|
77
|
+
try {
|
|
78
|
+
return canonicalizeWebUrl(new URL(`https://${trimmed}`));
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(trimmed);
|
|
86
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
87
|
+
return canonicalizeWebUrl(parsed);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
} catch {
|
|
91
|
+
// Not an absolute URL; fall through to the shorthand forms.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (looksLikePathOnlyInput(trimmed)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return canonicalizeWebUrl(new URL(`https://${trimmed}`));
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./remote-web-pairing": "./src/remote-web-pairing.ts",
|
|
15
15
|
"./twilio-ingress": "./src/twilio-ingress.ts",
|
|
16
16
|
"./trust-rules": "./src/trust-rules.ts",
|
|
17
|
+
"./url-normalization": "./src/url-normalization.ts",
|
|
17
18
|
"./handles": "./src/handles.ts",
|
|
18
19
|
"./rpc": "./src/rpc.ts",
|
|
19
20
|
"./attachment-naming": "./src/attachment-naming.ts",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
canonicalizeWebUrl,
|
|
5
|
+
looksLikeHostPortShorthand,
|
|
6
|
+
looksLikePathOnlyInput,
|
|
7
|
+
normalizeWebUrl,
|
|
8
|
+
} from "../url-normalization.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* These are the rules a trust rule is saved and matched under. A change here
|
|
12
|
+
* changes which saved rules still match, so each case states the target the
|
|
13
|
+
* two spellings must agree on.
|
|
14
|
+
*/
|
|
15
|
+
describe("normalizeWebUrl", () => {
|
|
16
|
+
test("keeps an ordinary https URL as-is", () => {
|
|
17
|
+
expect(normalizeWebUrl("https://example.com/docs/page")?.href).toBe(
|
|
18
|
+
"https://example.com/docs/page",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("drops the fragment: it never reaches the server", () => {
|
|
23
|
+
expect(normalizeWebUrl("https://example.com/docs#section")?.href).toBe(
|
|
24
|
+
"https://example.com/docs",
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("drops userinfo so credentials cannot land in a saved rule", () => {
|
|
29
|
+
const credentialed = new URL("https://example.com/docs/page");
|
|
30
|
+
credentialed.username = "demo";
|
|
31
|
+
credentialed.password = ["c", "r", "e", "d", "1", "2", "3"].join("");
|
|
32
|
+
|
|
33
|
+
const normalized = normalizeWebUrl(credentialed.href);
|
|
34
|
+
expect(normalized?.href).toBe("https://example.com/docs/page");
|
|
35
|
+
expect(normalized?.username).toBe("");
|
|
36
|
+
expect(normalized?.password).toBe("");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("strips a trailing root dot from the hostname", () => {
|
|
40
|
+
expect(normalizeWebUrl("https://example.com./docs/page")?.href).toBe(
|
|
41
|
+
"https://example.com/docs/page",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("decodes escaped path segments so one path has one spelling", () => {
|
|
46
|
+
// Without this, a rule scoped to /private is bypassed by /%70rivate.
|
|
47
|
+
expect(normalizeWebUrl("https://example.com/%70rivate")?.href).toBe(
|
|
48
|
+
"https://example.com/private",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("reads scheme-less input as https", () => {
|
|
53
|
+
expect(normalizeWebUrl("example.com/docs")?.href).toBe(
|
|
54
|
+
"https://example.com/docs",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("reads host:port shorthand as an https origin, not a scheme", () => {
|
|
59
|
+
expect(normalizeWebUrl("example.com:8443/status")?.origin).toBe(
|
|
60
|
+
"https://example.com:8443",
|
|
61
|
+
);
|
|
62
|
+
expect(normalizeWebUrl("[2001:db8::1]:8443/status")?.origin).toBe(
|
|
63
|
+
"https://[2001:db8::1]:8443",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("rejects path-only input rather than coercing it to a host", () => {
|
|
68
|
+
for (const input of ["/etc/passwd", "./rel", "../up", "?q=1", "#frag"]) {
|
|
69
|
+
expect(normalizeWebUrl(input)).toBeNull();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rejects every non-http scheme", () => {
|
|
74
|
+
for (const input of [
|
|
75
|
+
"file:///etc/passwd",
|
|
76
|
+
"data:text/html,<script>",
|
|
77
|
+
"javascript:alert(1)",
|
|
78
|
+
"ftp://example.com/f",
|
|
79
|
+
]) {
|
|
80
|
+
expect(normalizeWebUrl(input)).toBeNull();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("rejects empty and whitespace-only input", () => {
|
|
85
|
+
expect(normalizeWebUrl("")).toBeNull();
|
|
86
|
+
expect(normalizeWebUrl(" ")).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("trims surrounding whitespace", () => {
|
|
90
|
+
expect(normalizeWebUrl(" https://example.com/a ")?.href).toBe(
|
|
91
|
+
"https://example.com/a",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("returns null rather than throwing on an unparseable authority", () => {
|
|
96
|
+
expect(normalizeWebUrl("https://")).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("canonicalizeWebUrl", () => {
|
|
101
|
+
test("keeps the parser's form when the path is not decodable, without throwing", () => {
|
|
102
|
+
// `%zz` and a truncated UTF-8 sequence are not valid escapes; the path
|
|
103
|
+
// must survive unchanged rather than the call throwing.
|
|
104
|
+
expect(canonicalizeWebUrl(new URL("https://example.com/100%zz")).href).toBe(
|
|
105
|
+
"https://example.com/100%zz",
|
|
106
|
+
);
|
|
107
|
+
expect(
|
|
108
|
+
canonicalizeWebUrl(new URL("https://example.com/%E0%A4%A")).href,
|
|
109
|
+
).toBe("https://example.com/%E0%A4%A");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("decodes an escaped percent, so `%25` and `%` are one path", () => {
|
|
113
|
+
expect(canonicalizeWebUrl(new URL("https://example.com/100%25")).href).toBe(
|
|
114
|
+
"https://example.com/100%",
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("input shape predicates", () => {
|
|
120
|
+
test("host:port shorthand is recognized, scheme-prefixed input is not", () => {
|
|
121
|
+
expect(looksLikeHostPortShorthand("example.com:8443/x")).toBe(true);
|
|
122
|
+
expect(looksLikeHostPortShorthand("[2001:db8::1]:443")).toBe(true);
|
|
123
|
+
expect(looksLikeHostPortShorthand("https://example.com/x")).toBe(false);
|
|
124
|
+
expect(looksLikeHostPortShorthand("example.com/x")).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("path-only input is recognized", () => {
|
|
128
|
+
expect(looksLikePathOnlyInput("/abs")).toBe(true);
|
|
129
|
+
expect(looksLikePathOnlyInput("./rel")).toBe(true);
|
|
130
|
+
expect(looksLikePathOnlyInput("../up")).toBe(true);
|
|
131
|
+
expect(looksLikePathOnlyInput("?q=1")).toBe(true);
|
|
132
|
+
expect(looksLikePathOnlyInput("#frag")).toBe(true);
|
|
133
|
+
expect(looksLikePathOnlyInput("example.com/x")).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical URL normalization for web tool inputs, shared by the gateway's
|
|
3
|
+
* risk classifiers and the daemon's URL safety checks.
|
|
4
|
+
*
|
|
5
|
+
* A URL a model passes to `web_fetch` / `network_request` reaches more than
|
|
6
|
+
* one consumer, and they must agree on what counts as the same target: the
|
|
7
|
+
* gateway builds the trust-rule ladder from it, and anything that later
|
|
8
|
+
* compares a URL to a saved rule has to fold the same spellings together. Two
|
|
9
|
+
* normalizations would mean two answers, so it lives here rather than in
|
|
10
|
+
* either process.
|
|
11
|
+
*
|
|
12
|
+
* Data-only in spirit: no imports, no config, no I/O.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Whether a bare `host:port` (or `[v6]:port`) shorthand was written. */
|
|
16
|
+
export function looksLikeHostPortShorthand(value: string): boolean {
|
|
17
|
+
if (/^\[[0-9a-fA-F:.%]+\]:\d+(?:[/?#]|$)/.test(value)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return /^[^/?#@\s:]+:\d+(?:[/?#]|$)/.test(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Whether the input is a path, query, or fragment rather than a URL. */
|
|
24
|
+
export function looksLikePathOnlyInput(value: string): boolean {
|
|
25
|
+
return (
|
|
26
|
+
value.startsWith("/") ||
|
|
27
|
+
value.startsWith("./") ||
|
|
28
|
+
value.startsWith("../") ||
|
|
29
|
+
value.startsWith("?") ||
|
|
30
|
+
value.startsWith("#")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strip the parts of a URL that must not affect a trust decision, and fold
|
|
36
|
+
* the encodings that would otherwise let one URL wear two spellings.
|
|
37
|
+
*
|
|
38
|
+
* Percent-escaped path segments are decoded (`/%70rivate` and `/private` are
|
|
39
|
+
* one path, so a path-scoped rule cannot be bypassed by escaping), a trailing
|
|
40
|
+
* root dot is dropped from the hostname, and fragment and userinfo are
|
|
41
|
+
* removed: the fragment never reaches the server, and credentials in the
|
|
42
|
+
* authority must never end up in a saved rule.
|
|
43
|
+
*/
|
|
44
|
+
export function canonicalizeWebUrl(parsed: URL): URL {
|
|
45
|
+
parsed.hash = "";
|
|
46
|
+
parsed.username = "";
|
|
47
|
+
parsed.password = "";
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
parsed.pathname = decodeURI(parsed.pathname);
|
|
51
|
+
} catch {
|
|
52
|
+
// Keep the URL parser's canonical form when decoding fails.
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (parsed.hostname.endsWith(".")) {
|
|
56
|
+
parsed.hostname = parsed.hostname.replace(/\.+$/, "");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse and canonicalize a web tool's `url` input, or `null` when it is not a
|
|
64
|
+
* URL this system will fetch.
|
|
65
|
+
*
|
|
66
|
+
* Accepts `https://host/path`, `http://…`, bare `host/path`, and `host:port`
|
|
67
|
+
* shorthand. Rejects path-only input and any other scheme (`file:`, `data:`,
|
|
68
|
+
* `javascript:`), so a non-http target can never acquire a web trust rule.
|
|
69
|
+
*/
|
|
70
|
+
export function normalizeWebUrl(rawUrl: string): URL | null {
|
|
71
|
+
const trimmed = rawUrl.trim();
|
|
72
|
+
if (!trimmed) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (looksLikeHostPortShorthand(trimmed)) {
|
|
77
|
+
try {
|
|
78
|
+
return canonicalizeWebUrl(new URL(`https://${trimmed}`));
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const parsed = new URL(trimmed);
|
|
86
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
|
|
87
|
+
return canonicalizeWebUrl(parsed);
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
} catch {
|
|
91
|
+
// Not an absolute URL; fall through to the shorthand forms.
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (looksLikePathOnlyInput(trimmed)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return canonicalizeWebUrl(new URL(`https://${trimmed}`));
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -63,11 +63,16 @@ export type TrustRulesListIpcParams = z.infer<
|
|
|
63
63
|
typeof TrustRulesListIpcParamsSchema
|
|
64
64
|
>;
|
|
65
65
|
|
|
66
|
+
/** The three risk levels a trust rule can set and a registry default can carry. */
|
|
67
|
+
export const RISK_LEVEL_VALUES = ["low", "medium", "high"] as const;
|
|
68
|
+
export type RiskLevelValue = (typeof RISK_LEVEL_VALUES)[number];
|
|
69
|
+
export const RiskLevelValueSchema = z.enum(RISK_LEVEL_VALUES);
|
|
70
|
+
|
|
66
71
|
export const TrustRuleSchema = z.object({
|
|
67
72
|
id: z.string(),
|
|
68
73
|
tool: z.string(),
|
|
69
74
|
pattern: z.string(),
|
|
70
|
-
risk:
|
|
75
|
+
risk: RiskLevelValueSchema,
|
|
71
76
|
description: z.string(),
|
|
72
77
|
origin: z.enum(["default", "user_defined"]),
|
|
73
78
|
userModified: z.boolean(),
|
|
@@ -341,3 +346,163 @@ export const GetGuardianContactIpcResponseSchema = z.object({
|
|
|
341
346
|
export type GetGuardianContactIpcResponse = z.infer<
|
|
342
347
|
typeof GetGuardianContactIpcResponseSchema
|
|
343
348
|
>;
|
|
349
|
+
|
|
350
|
+
// ── classify_risk ────────────────────────────────────────────────────────────
|
|
351
|
+
// Risk classification is gateway-owned; the assistant sends one request per
|
|
352
|
+
// tool invocation and reads the whole answer back. The gateway validates the
|
|
353
|
+
// request against `ClassifyRiskIpcParamsSchema`; the assistant validates the
|
|
354
|
+
// response against `ClassifyRiskIpcResponseSchema` and fails closed on a
|
|
355
|
+
// mismatch.
|
|
356
|
+
|
|
357
|
+
/** A classified risk; `unknown` is the classifier's own "not in registry". */
|
|
358
|
+
export const ClassifiedRiskSchema = z.enum([...RISK_LEVEL_VALUES, "unknown"]);
|
|
359
|
+
export type ClassifiedRisk = z.infer<typeof ClassifiedRiskSchema>;
|
|
360
|
+
|
|
361
|
+
/** How a risk was determined: a user trust rule, the registry, or neither. */
|
|
362
|
+
export const RiskMatchTypeSchema = z.enum(["user_rule", "registry", "unknown"]);
|
|
363
|
+
export type RiskMatchType = z.infer<typeof RiskMatchTypeSchema>;
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* File classifier context the assistant pre-resolves (it owns the workspace
|
|
367
|
+
* filesystem) so the gateway can classify file tools without assistant path
|
|
368
|
+
* helpers. Directories are canonicalized (symlinks resolved) by the sender.
|
|
369
|
+
*/
|
|
370
|
+
export const ClassifyRiskFileContextSchema = z.object({
|
|
371
|
+
protectedDir: z.string(),
|
|
372
|
+
deprecatedDir: z.string(),
|
|
373
|
+
hooksDir: z.string(),
|
|
374
|
+
pluginsDir: z.string().optional(),
|
|
375
|
+
toolsDir: z.string().optional(),
|
|
376
|
+
routesDir: z.string().optional(),
|
|
377
|
+
workflowsDir: z.string().optional(),
|
|
378
|
+
/** Monitoring data dir: the sentinel lives here, writes are code-injection risk. */
|
|
379
|
+
monitoringDir: z.string().optional(),
|
|
380
|
+
actorTokenSigningKeyPath: z.string(),
|
|
381
|
+
skillSourceDirs: z.array(z.string()),
|
|
382
|
+
});
|
|
383
|
+
export type ClassifyRiskFileContext = z.infer<
|
|
384
|
+
typeof ClassifyRiskFileContextSchema
|
|
385
|
+
>;
|
|
386
|
+
|
|
387
|
+
/** Skill metadata the assistant pre-resolves for skill-load classification. */
|
|
388
|
+
export const ClassifyRiskSkillMetadataSchema = z.object({
|
|
389
|
+
skillId: z.string(),
|
|
390
|
+
selector: z.string(),
|
|
391
|
+
versionHash: z.string(),
|
|
392
|
+
transitiveHash: z.string().optional(),
|
|
393
|
+
hasInlineExpansions: z.boolean(),
|
|
394
|
+
isDynamic: z.boolean(),
|
|
395
|
+
});
|
|
396
|
+
export type ClassifyRiskSkillMetadata = z.infer<
|
|
397
|
+
typeof ClassifyRiskSkillMetadataSchema
|
|
398
|
+
>;
|
|
399
|
+
|
|
400
|
+
export const ClassifyRiskIpcParamsSchema = z.object({
|
|
401
|
+
tool: z.string().min(1),
|
|
402
|
+
command: z.string().optional(),
|
|
403
|
+
url: z.string().optional(),
|
|
404
|
+
path: z.string().optional(),
|
|
405
|
+
/**
|
|
406
|
+
* The file tool's target path with symlinks resolved by the assistant. The
|
|
407
|
+
* gateway's security escalation prefix checks use it so a symlink cannot
|
|
408
|
+
* mask a write into a protected directory; falls back to lexical resolution
|
|
409
|
+
* of `path` when absent.
|
|
410
|
+
*/
|
|
411
|
+
resolvedPath: z.string().optional(),
|
|
412
|
+
/**
|
|
413
|
+
* The sandbox file tool's working directory with symlinks resolved. Paired
|
|
414
|
+
* with `resolvedPath` for the workspace-boundary check so a symlinked
|
|
415
|
+
* workspace prefix (macOS `/var` → `/private/var`) does not read as an
|
|
416
|
+
* escape. Absent for host tools.
|
|
417
|
+
*/
|
|
418
|
+
resolvedWorkingDir: z.string().optional(),
|
|
419
|
+
skill: z.string().optional(),
|
|
420
|
+
mode: z.string().optional(),
|
|
421
|
+
script: z.string().optional(),
|
|
422
|
+
workingDir: z.string().optional(),
|
|
423
|
+
allowPrivateNetwork: z.boolean().optional(),
|
|
424
|
+
networkMode: z.string().optional(),
|
|
425
|
+
isContainerized: z.boolean().optional(),
|
|
426
|
+
workspaceRoot: z.string().optional(),
|
|
427
|
+
fileContext: ClassifyRiskFileContextSchema.optional(),
|
|
428
|
+
skillMetadata: ClassifyRiskSkillMetadataSchema.optional(),
|
|
429
|
+
/**
|
|
430
|
+
* The tool's registry default risk, for tools with no dedicated classifier.
|
|
431
|
+
* The gateway answers with it (`matchType: "registry"`); absent, the tool is
|
|
432
|
+
* unknown and answers `medium`.
|
|
433
|
+
*/
|
|
434
|
+
registryDefaultRisk: RiskLevelValueSchema.optional(),
|
|
435
|
+
/** Number of credential references attached to this tool invocation. */
|
|
436
|
+
credentialRefCount: z.number().int().nonnegative().optional(),
|
|
437
|
+
/**
|
|
438
|
+
* For `host_file_transfer` to_sandbox: the workspace-side destination path
|
|
439
|
+
* and the sandbox working directory it resolves against, so a transfer that
|
|
440
|
+
* lands an executable file in a code-injection sink (tools, routes, hooks,
|
|
441
|
+
* plugins, skills) escalates even though `path` carries the host-side source.
|
|
442
|
+
*/
|
|
443
|
+
transferSandboxDestPath: z.string().optional(),
|
|
444
|
+
transferSandboxWorkingDir: z.string().optional(),
|
|
445
|
+
/** `transferSandboxDestPath` with symlinks resolved by the assistant. */
|
|
446
|
+
resolvedTransferDestPath: z.string().optional(),
|
|
447
|
+
});
|
|
448
|
+
export type ClassifyRiskIpcParams = z.infer<typeof ClassifyRiskIpcParamsSchema>;
|
|
449
|
+
|
|
450
|
+
/** Regex ladder shown when the user saves a classification (display only). */
|
|
451
|
+
export const RiskPatternScopeOptionSchema = z.object({
|
|
452
|
+
pattern: z.string(),
|
|
453
|
+
label: z.string(),
|
|
454
|
+
});
|
|
455
|
+
export type RiskPatternScopeOption = z.infer<
|
|
456
|
+
typeof RiskPatternScopeOptionSchema
|
|
457
|
+
>;
|
|
458
|
+
|
|
459
|
+
/** Minimatch ladder for the "always allow" prompt option (what a rule matches). */
|
|
460
|
+
export const RiskAllowlistOptionSchema = z.object({
|
|
461
|
+
label: z.string(),
|
|
462
|
+
description: z.string(),
|
|
463
|
+
pattern: z.string(),
|
|
464
|
+
});
|
|
465
|
+
export type RiskAllowlistOption = z.infer<typeof RiskAllowlistOptionSchema>;
|
|
466
|
+
|
|
467
|
+
/** Directory ladder for filesystem operations; `scope` is a path glob or `everywhere`. */
|
|
468
|
+
export const RiskDirectoryScopeOptionSchema = z.object({
|
|
469
|
+
scope: z.string(),
|
|
470
|
+
label: z.string(),
|
|
471
|
+
});
|
|
472
|
+
export type RiskDirectoryScopeOption = z.infer<
|
|
473
|
+
typeof RiskDirectoryScopeOptionSchema
|
|
474
|
+
>;
|
|
475
|
+
|
|
476
|
+
export const RiskDangerousPatternSchema = z.object({
|
|
477
|
+
type: z.string(),
|
|
478
|
+
description: z.string(),
|
|
479
|
+
text: z.string(),
|
|
480
|
+
});
|
|
481
|
+
export type RiskDangerousPattern = z.infer<typeof RiskDangerousPatternSchema>;
|
|
482
|
+
|
|
483
|
+
export const ClassifyRiskIpcResponseSchema = z.object({
|
|
484
|
+
risk: ClassifiedRiskSchema,
|
|
485
|
+
reason: z.string(),
|
|
486
|
+
matchType: RiskMatchTypeSchema,
|
|
487
|
+
scopeOptions: z.array(RiskPatternScopeOptionSchema),
|
|
488
|
+
allowlistOptions: z.array(RiskAllowlistOptionSchema).optional(),
|
|
489
|
+
actionKeys: z.array(z.string()).optional(),
|
|
490
|
+
commandCandidates: z.array(z.string()).optional(),
|
|
491
|
+
dangerousPatterns: z.array(RiskDangerousPatternSchema).optional(),
|
|
492
|
+
opaqueConstructs: z.boolean().optional(),
|
|
493
|
+
isComplexSyntax: z.boolean().optional(),
|
|
494
|
+
sandboxAutoApprove: z.boolean().optional(),
|
|
495
|
+
/**
|
|
496
|
+
* Lexically-resolved path arguments from sandbox-auto-approve-eligible bash
|
|
497
|
+
* segments. The gateway has no filesystem access; the assistant resolves
|
|
498
|
+
* these through symlinks and revokes `sandboxAutoApprove` if any escapes
|
|
499
|
+
* the workspace.
|
|
500
|
+
*/
|
|
501
|
+
sandboxPathArgs: z.array(z.string()).optional(),
|
|
502
|
+
directoryScopeOptions: z.array(RiskDirectoryScopeOptionSchema).optional(),
|
|
503
|
+
/** Fully resolved filesystem path arguments, for directory-scoped rule matching. */
|
|
504
|
+
resolvedPaths: z.array(z.string()).optional(),
|
|
505
|
+
});
|
|
506
|
+
export type ClassifyRiskIpcResponse = z.infer<
|
|
507
|
+
typeof ClassifyRiskIpcResponseSchema
|
|
508
|
+
>;
|