@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
|
@@ -43,8 +43,11 @@ let checkFnOverride:
|
|
|
43
43
|
/** Override for generateScopeOptions — when set, returns this value instead of the default. */
|
|
44
44
|
let scopeOptionsOverride: ScopeOption[] | undefined;
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Override for the classification `classifyRisk` returns; when unset the
|
|
48
|
+
* classifier answers low with no reason and no options.
|
|
49
|
+
*/
|
|
50
|
+
let classificationOverride:
|
|
48
51
|
| {
|
|
49
52
|
riskLevel: string;
|
|
50
53
|
reason: string;
|
|
@@ -61,7 +64,14 @@ let cachedAssessmentOverride:
|
|
|
61
64
|
|
|
62
65
|
mock.module("../permissions/checker.js", () => ({
|
|
63
66
|
isDynamicSkillLoadInvocation: () => false,
|
|
64
|
-
classifyRisk: async () => ({
|
|
67
|
+
classifyRisk: async () => ({
|
|
68
|
+
level: classificationOverride?.riskLevel ?? "low",
|
|
69
|
+
reason: classificationOverride?.reason,
|
|
70
|
+
scopeOptions: classificationOverride?.scopeOptions ?? [],
|
|
71
|
+
allowlistOptions: classificationOverride?.allowlistOptions,
|
|
72
|
+
directoryScopeOptions: classificationOverride?.directoryScopeOptions,
|
|
73
|
+
matchType: classificationOverride?.matchType ?? "unknown",
|
|
74
|
+
}),
|
|
65
75
|
check: async (
|
|
66
76
|
toolName: string,
|
|
67
77
|
input: Record<string, unknown>,
|
|
@@ -77,12 +87,8 @@ mock.module("../permissions/checker.js", () => ({
|
|
|
77
87
|
}
|
|
78
88
|
return { decision: "allow", reason: "allowed" };
|
|
79
89
|
},
|
|
80
|
-
generateAllowlistOptions: () => [
|
|
81
|
-
{ label: "exact", description: "exact", pattern: "exact" },
|
|
82
|
-
],
|
|
83
90
|
generateScopeOptions: () =>
|
|
84
91
|
scopeOptionsOverride ?? [{ label: "/tmp", scope: "/tmp" }],
|
|
85
|
-
getCachedAssessment: () => cachedAssessmentOverride,
|
|
86
92
|
}));
|
|
87
93
|
|
|
88
94
|
// Mock every export so downstream test files that dynamically import modules
|
|
@@ -167,7 +173,7 @@ describe("ToolExecutor allowedToolNames gating", () => {
|
|
|
167
173
|
getAllToolsOverride = undefined;
|
|
168
174
|
checkResultOverride = undefined;
|
|
169
175
|
checkFnOverride = undefined;
|
|
170
|
-
|
|
176
|
+
classificationOverride = undefined;
|
|
171
177
|
});
|
|
172
178
|
|
|
173
179
|
test("executes normally when allowedToolNames is not set", async () => {
|
|
@@ -385,7 +391,7 @@ describe("ToolExecutor policy context plumbing", () => {
|
|
|
385
391
|
getToolOverride = undefined;
|
|
386
392
|
checkResultOverride = undefined;
|
|
387
393
|
checkFnOverride = undefined;
|
|
388
|
-
|
|
394
|
+
classificationOverride = undefined;
|
|
389
395
|
});
|
|
390
396
|
|
|
391
397
|
test("passes PolicyContext with executionTarget for skill-origin tools", async () => {
|
|
@@ -604,7 +610,7 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
|
|
|
604
610
|
getToolOverride = undefined;
|
|
605
611
|
checkResultOverride = undefined;
|
|
606
612
|
checkFnOverride = undefined;
|
|
607
|
-
|
|
613
|
+
classificationOverride = undefined;
|
|
608
614
|
promptCalled = false;
|
|
609
615
|
});
|
|
610
616
|
|
|
@@ -931,11 +937,30 @@ describe("integration regressions — prompt payload (PR 11)", () => {
|
|
|
931
937
|
getToolOverride = undefined;
|
|
932
938
|
});
|
|
933
939
|
|
|
934
|
-
test("shell command prompt payload
|
|
940
|
+
test("shell command prompt payload carries the classifier's ladder and the scope options", async () => {
|
|
935
941
|
checkResultOverride = {
|
|
936
942
|
decision: "prompt",
|
|
937
943
|
reason: "Medium risk: requires approval",
|
|
938
944
|
};
|
|
945
|
+
// The ladder is the gateway's; the daemon passes it through untouched.
|
|
946
|
+
classificationOverride = {
|
|
947
|
+
riskLevel: "medium",
|
|
948
|
+
reason: "npm install",
|
|
949
|
+
scopeOptions: [],
|
|
950
|
+
allowlistOptions: [
|
|
951
|
+
{
|
|
952
|
+
label: "npm install",
|
|
953
|
+
description: "Exact command",
|
|
954
|
+
pattern: "npm install",
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
label: "action:npm",
|
|
958
|
+
description: "Any npm command",
|
|
959
|
+
pattern: "action:npm",
|
|
960
|
+
},
|
|
961
|
+
],
|
|
962
|
+
matchType: "registry",
|
|
963
|
+
};
|
|
939
964
|
|
|
940
965
|
let capturedAllowlist: AllowlistOption[] | undefined;
|
|
941
966
|
let capturedScopes: ScopeOption[] | undefined;
|
|
@@ -963,13 +988,11 @@ describe("integration regressions — prompt payload (PR 11)", () => {
|
|
|
963
988
|
makeContext({ forcePromptSideEffects: true }),
|
|
964
989
|
);
|
|
965
990
|
|
|
966
|
-
//
|
|
967
|
-
expect(capturedAllowlist).
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
expect(capturedAllowlist![0]).toHaveProperty("label");
|
|
972
|
-
expect(capturedAllowlist![0]).toHaveProperty("description");
|
|
991
|
+
// The prompt offers exactly what the classifier produced, in order.
|
|
992
|
+
expect(capturedAllowlist?.map((o) => o.pattern)).toEqual([
|
|
993
|
+
"npm install",
|
|
994
|
+
"action:npm",
|
|
995
|
+
]);
|
|
973
996
|
|
|
974
997
|
// Verify scope options are also passed
|
|
975
998
|
expect(capturedScopes).toBeDefined();
|
|
@@ -989,11 +1012,11 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
989
1012
|
getToolOverride = undefined;
|
|
990
1013
|
checkResultOverride = undefined;
|
|
991
1014
|
checkFnOverride = undefined;
|
|
992
|
-
|
|
1015
|
+
classificationOverride = undefined;
|
|
993
1016
|
});
|
|
994
1017
|
|
|
995
1018
|
test("auto-approved tool result includes risk metadata when classifier assessment exists", async () => {
|
|
996
|
-
|
|
1019
|
+
classificationOverride = {
|
|
997
1020
|
riskLevel: "medium",
|
|
998
1021
|
reason: "Writes to a file outside the workspace",
|
|
999
1022
|
scopeOptions: [
|
|
@@ -1019,8 +1042,10 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1019
1042
|
]);
|
|
1020
1043
|
});
|
|
1021
1044
|
|
|
1022
|
-
test("tool result
|
|
1023
|
-
//
|
|
1045
|
+
test("tool result carries the classification's risk metadata even when the classifier produced no options", async () => {
|
|
1046
|
+
// classificationOverride is unset: the classifier answered low with no
|
|
1047
|
+
// reason and no options, which is what a tool with no dedicated classifier
|
|
1048
|
+
// (registry default risk) looks like.
|
|
1024
1049
|
const executor = new ToolExecutor(makePrompter());
|
|
1025
1050
|
const result = await executor.execute(
|
|
1026
1051
|
"file_read",
|
|
@@ -1029,9 +1054,9 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1029
1054
|
);
|
|
1030
1055
|
|
|
1031
1056
|
expect(result.isError).toBe(false);
|
|
1032
|
-
expect(result.riskLevel).
|
|
1033
|
-
expect(result.
|
|
1034
|
-
expect(result.
|
|
1057
|
+
expect(result.riskLevel).toBe("low");
|
|
1058
|
+
expect(result.riskScopeOptions).toEqual([]);
|
|
1059
|
+
expect(result.riskAllowlistOptions).toBeUndefined();
|
|
1035
1060
|
});
|
|
1036
1061
|
|
|
1037
1062
|
test("denied tool result includes risk metadata", async () => {
|
|
@@ -1039,7 +1064,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1039
1064
|
decision: "deny",
|
|
1040
1065
|
reason: "Blocked by deny rule",
|
|
1041
1066
|
};
|
|
1042
|
-
|
|
1067
|
+
classificationOverride = {
|
|
1043
1068
|
riskLevel: "high",
|
|
1044
1069
|
reason: "Recursive force delete",
|
|
1045
1070
|
scopeOptions: [{ pattern: "bash:rm -rf*", label: "rm -rf commands" }],
|
|
@@ -1066,7 +1091,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1066
1091
|
decision: "prompt",
|
|
1067
1092
|
reason: "Medium risk: requires approval",
|
|
1068
1093
|
};
|
|
1069
|
-
|
|
1094
|
+
classificationOverride = {
|
|
1070
1095
|
riskLevel: "medium",
|
|
1071
1096
|
reason: "Package manager installation",
|
|
1072
1097
|
scopeOptions: [
|
|
@@ -1091,7 +1116,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1091
1116
|
});
|
|
1092
1117
|
|
|
1093
1118
|
test("tool result includes riskDirectoryScopeOptions when classifier emits directoryScopeOptions", async () => {
|
|
1094
|
-
|
|
1119
|
+
classificationOverride = {
|
|
1095
1120
|
riskLevel: "medium",
|
|
1096
1121
|
reason: "Writes to file in workspace",
|
|
1097
1122
|
scopeOptions: [
|
|
@@ -1124,7 +1149,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1124
1149
|
});
|
|
1125
1150
|
|
|
1126
1151
|
test("tool result omits riskDirectoryScopeOptions when classifier does not emit directoryScopeOptions", async () => {
|
|
1127
|
-
|
|
1152
|
+
classificationOverride = {
|
|
1128
1153
|
riskLevel: "low",
|
|
1129
1154
|
reason: "Read-only operation",
|
|
1130
1155
|
scopeOptions: [],
|
|
@@ -1144,7 +1169,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1144
1169
|
});
|
|
1145
1170
|
|
|
1146
1171
|
test("riskScopeOptions and riskDirectoryScopeOptions are independent — one does not clobber the other", async () => {
|
|
1147
|
-
|
|
1172
|
+
classificationOverride = {
|
|
1148
1173
|
riskLevel: "medium",
|
|
1149
1174
|
reason: "Filesystem write",
|
|
1150
1175
|
scopeOptions: [
|
|
@@ -1170,7 +1195,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1170
1195
|
});
|
|
1171
1196
|
|
|
1172
1197
|
test("auto-approved tool result includes riskAllowlistOptions when classifier emits them (Minimatch-glob shape for save path)", async () => {
|
|
1173
|
-
|
|
1198
|
+
classificationOverride = {
|
|
1174
1199
|
riskLevel: "medium",
|
|
1175
1200
|
reason: "Reads workspace files",
|
|
1176
1201
|
// Display ladder (regex shape — not for save).
|
|
@@ -1222,7 +1247,7 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1222
1247
|
});
|
|
1223
1248
|
|
|
1224
1249
|
test("riskAllowlistOptions is undefined when classifier did not produce allowlist (e.g. web-risk classifier)", async () => {
|
|
1225
|
-
|
|
1250
|
+
classificationOverride = {
|
|
1226
1251
|
riskLevel: "low",
|
|
1227
1252
|
reason: "GET request to public URL",
|
|
1228
1253
|
scopeOptions: [
|
|
@@ -1248,8 +1273,8 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1248
1273
|
expect(result.riskAllowlistOptions).toBeUndefined();
|
|
1249
1274
|
});
|
|
1250
1275
|
|
|
1251
|
-
test("riskAllowlistOptions is undefined when
|
|
1252
|
-
//
|
|
1276
|
+
test("riskAllowlistOptions is undefined when the classifier produced none", async () => {
|
|
1277
|
+
// classificationOverride is unset: no allowlist options on the answer.
|
|
1253
1278
|
const executor = new ToolExecutor(makePrompter());
|
|
1254
1279
|
const result = await executor.execute(
|
|
1255
1280
|
"file_read",
|
|
@@ -1258,13 +1283,13 @@ describe("ToolExecutionResult includes risk metadata from classifier assessment"
|
|
|
1258
1283
|
);
|
|
1259
1284
|
|
|
1260
1285
|
expect(result.isError).toBe(false);
|
|
1261
|
-
expect(result.riskScopeOptions).
|
|
1286
|
+
expect(result.riskScopeOptions).toEqual([]);
|
|
1262
1287
|
expect(result.riskAllowlistOptions).toBeUndefined();
|
|
1263
1288
|
});
|
|
1264
1289
|
|
|
1265
1290
|
test("denied tool result still carries riskAllowlistOptions for the rule editor save path", async () => {
|
|
1266
1291
|
checkResultOverride = { decision: "deny", reason: "Blocked by deny rule" };
|
|
1267
|
-
|
|
1292
|
+
classificationOverride = {
|
|
1268
1293
|
riskLevel: "high",
|
|
1269
1294
|
reason: "Recursive force delete",
|
|
1270
1295
|
scopeOptions: [{ pattern: "^rm\\s+-rf", label: "rm -rf *" }],
|
|
@@ -1334,7 +1359,7 @@ describe("ToolExecutor thrown-value rendering", () => {
|
|
|
1334
1359
|
getAllToolsOverride = undefined;
|
|
1335
1360
|
checkResultOverride = undefined;
|
|
1336
1361
|
checkFnOverride = undefined;
|
|
1337
|
-
|
|
1362
|
+
classificationOverride = undefined;
|
|
1338
1363
|
});
|
|
1339
1364
|
|
|
1340
1365
|
function throwingTool(name: string, thrown: unknown): void {
|
|
@@ -1406,7 +1431,7 @@ describe("ToolExecutor input-schema validation gate", () => {
|
|
|
1406
1431
|
getAllToolsOverride = undefined;
|
|
1407
1432
|
checkResultOverride = undefined;
|
|
1408
1433
|
checkFnOverride = undefined;
|
|
1409
|
-
|
|
1434
|
+
classificationOverride = undefined;
|
|
1410
1435
|
});
|
|
1411
1436
|
|
|
1412
1437
|
/**
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { CredentialMetadata } from "../tools/credentials/metadata-store.js";
|
|
4
|
+
import {
|
|
5
|
+
BROWSER_FILL_CAPABILITY,
|
|
6
|
+
isToolAllowed,
|
|
7
|
+
serverUseDenialReason,
|
|
8
|
+
} from "../tools/credentials/tool-policy.js";
|
|
4
9
|
|
|
5
10
|
describe("isToolAllowed", () => {
|
|
6
11
|
// ── Allow cases ─────────────────────────────────────────────────────
|
|
@@ -72,3 +77,94 @@ describe("isToolAllowed", () => {
|
|
|
72
77
|
).toBe(false);
|
|
73
78
|
});
|
|
74
79
|
});
|
|
80
|
+
|
|
81
|
+
describe("serverUseDenialReason", () => {
|
|
82
|
+
function metadata(
|
|
83
|
+
overrides: Partial<CredentialMetadata> = {},
|
|
84
|
+
): CredentialMetadata {
|
|
85
|
+
return {
|
|
86
|
+
credentialId: "cred-1",
|
|
87
|
+
service: "acp",
|
|
88
|
+
field: "claude_oauth_token",
|
|
89
|
+
allowedTools: ["acp_spawn"],
|
|
90
|
+
allowedDomains: [],
|
|
91
|
+
createdAt: 0,
|
|
92
|
+
updatedAt: 0,
|
|
93
|
+
...overrides,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("denies when metadata is missing", () => {
|
|
98
|
+
expect(
|
|
99
|
+
serverUseDenialReason(
|
|
100
|
+
undefined,
|
|
101
|
+
"acp_spawn",
|
|
102
|
+
"acp",
|
|
103
|
+
"claude_oauth_token",
|
|
104
|
+
),
|
|
105
|
+
).toBe("No credential found for acp/claude_oauth_token");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("denies fail-closed when allowedTools is empty", () => {
|
|
109
|
+
const reason = serverUseDenialReason(
|
|
110
|
+
metadata({ allowedTools: [] }),
|
|
111
|
+
"acp_spawn",
|
|
112
|
+
"acp",
|
|
113
|
+
"claude_oauth_token",
|
|
114
|
+
);
|
|
115
|
+
expect(reason).toContain(
|
|
116
|
+
'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token.',
|
|
117
|
+
);
|
|
118
|
+
expect(reason).toContain("No tools are currently allowed");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("denies when allowedTools omits the requesting tool", () => {
|
|
122
|
+
const reason = serverUseDenialReason(
|
|
123
|
+
metadata({ allowedTools: ["bash", "web_fetch"] }),
|
|
124
|
+
"acp_spawn",
|
|
125
|
+
"acp",
|
|
126
|
+
"claude_oauth_token",
|
|
127
|
+
);
|
|
128
|
+
expect(reason).toContain(
|
|
129
|
+
'Tool "acp_spawn" is not allowed to use credential acp/claude_oauth_token.',
|
|
130
|
+
);
|
|
131
|
+
expect(reason).toContain("Allowed tools: bash, web_fetch.");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("allows when a legacy alias canonicalizes to the requesting tool", () => {
|
|
135
|
+
expect(
|
|
136
|
+
serverUseDenialReason(
|
|
137
|
+
metadata({ allowedTools: ["browser_fill_credential"] }),
|
|
138
|
+
BROWSER_FILL_CAPABILITY,
|
|
139
|
+
"acp",
|
|
140
|
+
"claude_oauth_token",
|
|
141
|
+
),
|
|
142
|
+
).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("denies a domain-restricted credential", () => {
|
|
146
|
+
expect(
|
|
147
|
+
serverUseDenialReason(
|
|
148
|
+
metadata({ allowedDomains: ["example.com", "example.org"] }),
|
|
149
|
+
"acp_spawn",
|
|
150
|
+
"acp",
|
|
151
|
+
"claude_oauth_token",
|
|
152
|
+
),
|
|
153
|
+
).toBe(
|
|
154
|
+
"Credential acp/claude_oauth_token has domain restrictions " +
|
|
155
|
+
"(example.com, example.org) and cannot be used server-side. " +
|
|
156
|
+
"Remove domain restrictions or use a separate credential without domain policy.",
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("allows fully-permitted metadata", () => {
|
|
161
|
+
expect(
|
|
162
|
+
serverUseDenialReason(
|
|
163
|
+
metadata(),
|
|
164
|
+
"acp_spawn",
|
|
165
|
+
"acp",
|
|
166
|
+
"claude_oauth_token",
|
|
167
|
+
),
|
|
168
|
+
).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -1231,8 +1231,9 @@ describe("(g) access_request resolver: requester code delivery", () => {
|
|
|
1231
1231
|
expect(requesterCodeReply!.payload.text).toContain(
|
|
1232
1232
|
"your access request was approved",
|
|
1233
1233
|
);
|
|
1234
|
-
// The code DM is durable,
|
|
1235
|
-
|
|
1234
|
+
// The code DM is durable, so it carries no audience: restricting it
|
|
1235
|
+
// would cost durability without hiding it from anyone.
|
|
1236
|
+
expect(requesterCodeReply!.payload.audience).toBeUndefined();
|
|
1236
1237
|
// threadTs (the guardian's channel thread) is stripped for the DM.
|
|
1237
1238
|
expect(requesterCodeReply!.url).not.toContain("threadTs");
|
|
1238
1239
|
|
|
@@ -1411,8 +1412,10 @@ describe("(g) access_request resolver: requester code delivery", () => {
|
|
|
1411
1412
|
);
|
|
1412
1413
|
expect(courier).toBeDefined();
|
|
1413
1414
|
expect(courier!.payload.chatId).toBe("C_SHARED_CHANNEL");
|
|
1414
|
-
expect(courier!.payload.
|
|
1415
|
-
|
|
1415
|
+
expect(courier!.payload.audience).toEqual({
|
|
1416
|
+
kind: "oneReader",
|
|
1417
|
+
userId: REQUESTER_UID,
|
|
1418
|
+
});
|
|
1416
1419
|
});
|
|
1417
1420
|
|
|
1418
1421
|
test("guardian-facing reply uses the requester's display name, not the raw ID", async () => {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getCachedUserTools,
|
|
29
29
|
resetPluginCacheForTests,
|
|
30
30
|
} from "../plugins/mtime-cache.js";
|
|
31
|
+
import { resetPluginRegistryForTests } from "../plugins/registry.js";
|
|
31
32
|
import { loadUserPlugins } from "../plugins/user-loader.js";
|
|
32
33
|
|
|
33
34
|
// Isolate every run under its own tempdir so parallel test files (and
|
|
@@ -71,6 +72,7 @@ function clearPluginsDir(): void {
|
|
|
71
72
|
describe("user plugin loader", () => {
|
|
72
73
|
beforeEach(() => {
|
|
73
74
|
resetPluginCacheForTests();
|
|
75
|
+
resetPluginRegistryForTests();
|
|
74
76
|
clearPluginsDir();
|
|
75
77
|
});
|
|
76
78
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
coerceArrayShapes,
|
|
4
5
|
coerceStringBooleans,
|
|
5
6
|
validateInputAgainstSchema,
|
|
6
7
|
} from "../skills/validate-input.js";
|
|
@@ -450,20 +451,255 @@ describe("coerceStringBooleans", () => {
|
|
|
450
451
|
});
|
|
451
452
|
});
|
|
452
453
|
|
|
453
|
-
describe("
|
|
454
|
-
|
|
454
|
+
describe("coerceArrayShapes", () => {
|
|
455
|
+
const schema = {
|
|
456
|
+
type: "object",
|
|
457
|
+
properties: {
|
|
458
|
+
activation_hints: { type: "array", items: { type: "string" } },
|
|
459
|
+
files: {
|
|
460
|
+
type: "array",
|
|
461
|
+
items: {
|
|
462
|
+
type: "object",
|
|
463
|
+
properties: {
|
|
464
|
+
path: { type: "string" },
|
|
465
|
+
content: { type: "string" },
|
|
466
|
+
copy_from: { type: "string" },
|
|
467
|
+
},
|
|
468
|
+
required: ["path"],
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
steps: {
|
|
472
|
+
type: "array",
|
|
473
|
+
items: {
|
|
474
|
+
type: "object",
|
|
475
|
+
properties: {
|
|
476
|
+
action: { type: "string" },
|
|
477
|
+
require_approval: { type: "boolean" },
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
name: { type: "string" },
|
|
482
|
+
tags: { type: ["array", "null"] },
|
|
483
|
+
untyped: { type: "array" },
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
describe("a string holding the JSON text of the array", () => {
|
|
488
|
+
test("decodes an array of strings", () => {
|
|
489
|
+
const result = coerceArrayShapes(
|
|
490
|
+
{ activation_hints: '["deploy staging","roll back"]' },
|
|
491
|
+
schema,
|
|
492
|
+
);
|
|
493
|
+
expect(result.activation_hints).toEqual(["deploy staging", "roll back"]);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("decodes an array of objects", () => {
|
|
497
|
+
const result = coerceArrayShapes(
|
|
498
|
+
{ files: '[{"path":"references/notes.md","content":"hi"}]' },
|
|
499
|
+
schema,
|
|
500
|
+
);
|
|
501
|
+
expect(result.files).toEqual([
|
|
502
|
+
{ path: "references/notes.md", content: "hi" },
|
|
503
|
+
]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("decodes for an array property that declares no item type", () => {
|
|
507
|
+
const result = coerceArrayShapes({ untyped: '["a"]' }, schema);
|
|
508
|
+
expect(result.untyped).toEqual(["a"]);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe("a single element where a list was expected", () => {
|
|
513
|
+
test("wraps a bare phrase for a string-item array", () => {
|
|
514
|
+
const result = coerceArrayShapes(
|
|
515
|
+
{ activation_hints: "user asks to deploy staging" },
|
|
516
|
+
schema,
|
|
517
|
+
);
|
|
518
|
+
expect(result.activation_hints).toEqual(["user asks to deploy staging"]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("wraps the decoded text of a JSON-quoted phrase", () => {
|
|
522
|
+
const result = coerceArrayShapes(
|
|
523
|
+
{ activation_hints: '"user asks to deploy staging"' },
|
|
524
|
+
schema,
|
|
525
|
+
);
|
|
526
|
+
expect(result.activation_hints).toEqual(["user asks to deploy staging"]);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test("wraps one object that spells out the item schema's properties", () => {
|
|
530
|
+
const result = coerceArrayShapes(
|
|
531
|
+
{ files: { path: "references/notes.md", content: "hi" } },
|
|
532
|
+
schema,
|
|
533
|
+
);
|
|
534
|
+
expect(result.files).toEqual([
|
|
535
|
+
{ path: "references/notes.md", content: "hi" },
|
|
536
|
+
]);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("leaves an object keyed by something the item schema does not name", () => {
|
|
540
|
+
const input = { files: { "references/notes.md": "hi" } };
|
|
541
|
+
const result = coerceArrayShapes(input, schema);
|
|
542
|
+
expect(result).toBe(input);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("leaves a bare phrase for an object-item array", () => {
|
|
546
|
+
const input = { files: "references/notes.md" };
|
|
547
|
+
const result = coerceArrayShapes(input, schema);
|
|
548
|
+
expect(result).toBe(input);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("leaves a bare phrase when the array declares no item type", () => {
|
|
552
|
+
const input = { untyped: "one thing" };
|
|
553
|
+
expect(coerceArrayShapes(input, schema)).toBe(input);
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe("shapes left for the validator to reject", () => {
|
|
558
|
+
test.each([
|
|
559
|
+
['["deploy staging"', "a truncated array"],
|
|
560
|
+
['{"path":"references/notes.md"', "a truncated object"],
|
|
561
|
+
["", "an empty string"],
|
|
562
|
+
[" ", "blank text"],
|
|
563
|
+
])("leaves %p (%s)", (raw) => {
|
|
564
|
+
const input = { activation_hints: raw };
|
|
565
|
+
const result = coerceArrayShapes(input, schema);
|
|
566
|
+
expect(result).toBe(input);
|
|
567
|
+
const validation = validateInputAgainstSchema(
|
|
568
|
+
"scaffold_managed_skill",
|
|
569
|
+
result,
|
|
570
|
+
schema,
|
|
571
|
+
);
|
|
572
|
+
expect(validation.ok).toBe(false);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("repaired input passes validation and preserves intent", () => {
|
|
577
|
+
const coerced = coerceArrayShapes(
|
|
578
|
+
{
|
|
579
|
+
activation_hints: "user asks to deploy staging",
|
|
580
|
+
files: { path: "references/notes.md", content: "hi" },
|
|
581
|
+
name: "x",
|
|
582
|
+
},
|
|
583
|
+
schema,
|
|
584
|
+
);
|
|
585
|
+
expect(
|
|
586
|
+
validateInputAgainstSchema("scaffold_managed_skill", coerced, schema),
|
|
587
|
+
).toEqual({ ok: true });
|
|
588
|
+
expect(coerced.activation_hints).toEqual(["user asks to deploy staging"]);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe("elements an array gains here", () => {
|
|
592
|
+
test("are coerced against the item schema when decoded", () => {
|
|
593
|
+
const result = coerceArrayShapes(
|
|
594
|
+
{ steps: '[{"action":"send","require_approval":"false"}]' },
|
|
595
|
+
schema,
|
|
596
|
+
);
|
|
597
|
+
expect(result.steps).toEqual([
|
|
598
|
+
{ action: "send", require_approval: false },
|
|
599
|
+
]);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("are coerced against the item schema when wrapped", () => {
|
|
603
|
+
const result = coerceArrayShapes(
|
|
604
|
+
{ steps: { action: "send", require_approval: "true" } },
|
|
605
|
+
schema,
|
|
606
|
+
);
|
|
607
|
+
expect(result.steps).toEqual([
|
|
608
|
+
{ action: "send", require_approval: true },
|
|
609
|
+
]);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("leave non-object elements alone", () => {
|
|
613
|
+
const result = coerceArrayShapes({ steps: '["send",7]' }, schema);
|
|
614
|
+
expect(result.steps).toEqual(["send", 7]);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("still reports a wrong element type after repair", () => {
|
|
619
|
+
const coerced = coerceArrayShapes(
|
|
620
|
+
{ activation_hints: '["deploy staging",7]' },
|
|
621
|
+
schema,
|
|
622
|
+
);
|
|
623
|
+
const validation = validateInputAgainstSchema(
|
|
624
|
+
"scaffold_managed_skill",
|
|
625
|
+
coerced,
|
|
626
|
+
schema,
|
|
627
|
+
);
|
|
628
|
+
expect(validation.ok).toBe(false);
|
|
629
|
+
if (validation.ok) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
expect(validation.errors).toContain("activation_hints[1] must be a string");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("does not touch non-array-typed or union-typed properties", () => {
|
|
636
|
+
const input = { name: '["x"]', tags: '["x"]' };
|
|
637
|
+
const result = coerceArrayShapes(input, schema);
|
|
638
|
+
expect(result).toBe(input);
|
|
639
|
+
expect(result.name).toBe('["x"]');
|
|
640
|
+
expect(result.tags).toBe('["x"]');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test("does not touch real arrays or other types", () => {
|
|
644
|
+
const input = { activation_hints: ["deploy staging"] };
|
|
645
|
+
expect(coerceArrayShapes(input, schema)).toBe(input);
|
|
646
|
+
const inputNum = { activation_hints: 7 };
|
|
647
|
+
expect(coerceArrayShapes(inputNum, schema)).toBe(inputNum);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test("returns the same object when there is nothing to repair", () => {
|
|
651
|
+
const input = { name: "x" };
|
|
652
|
+
expect(coerceArrayShapes(input, schema)).toBe(input);
|
|
653
|
+
expect(coerceArrayShapes(input, undefined)).toBe(input);
|
|
654
|
+
expect(coerceArrayShapes(input, { type: "object" })).toBe(input);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
test("returns a new object and never mutates the original input", () => {
|
|
658
|
+
const input = { activation_hints: '["deploy staging"]', name: "x" };
|
|
659
|
+
const snapshot = JSON.parse(JSON.stringify(input));
|
|
660
|
+
const result = coerceArrayShapes(input, schema);
|
|
661
|
+
expect(result).not.toBe(input);
|
|
662
|
+
expect(input).toEqual(snapshot);
|
|
663
|
+
expect(result.name).toBe("x");
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
describe("validateInputAgainstSchema: array shape error message", () => {
|
|
668
|
+
const schema = {
|
|
669
|
+
type: "object",
|
|
670
|
+
properties: {
|
|
671
|
+
activation_hints: { type: "array", items: { type: "string" } },
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
test("points at the JSON array form when a string is passed", () => {
|
|
455
676
|
const result = validateInputAgainstSchema(
|
|
456
|
-
"
|
|
457
|
-
{
|
|
458
|
-
|
|
677
|
+
"scaffold_managed_skill",
|
|
678
|
+
{ activation_hints: "[deploy staging" },
|
|
679
|
+
schema,
|
|
459
680
|
);
|
|
460
681
|
expect(result.ok).toBe(false);
|
|
461
682
|
if (result.ok) {
|
|
462
683
|
return;
|
|
463
684
|
}
|
|
464
|
-
expect(result.errors).
|
|
465
|
-
"
|
|
685
|
+
expect(result.errors).toEqual([
|
|
686
|
+
"activation_hints must be an array: pass a JSON array, not a string",
|
|
687
|
+
]);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("names an object when an object is passed", () => {
|
|
691
|
+
const result = validateInputAgainstSchema(
|
|
692
|
+
"scaffold_managed_skill",
|
|
693
|
+
{ activation_hints: { first: "deploy staging" } },
|
|
694
|
+
schema,
|
|
466
695
|
);
|
|
696
|
+
expect(result.ok).toBe(false);
|
|
697
|
+
if (result.ok) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
expect(result.errors).toEqual([
|
|
701
|
+
"activation_hints must be an array: pass a JSON array, not an object",
|
|
702
|
+
]);
|
|
467
703
|
});
|
|
468
704
|
});
|
|
469
705
|
|