@vellumai/assistant 0.11.4 → 0.11.5-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -171,7 +171,100 @@ describe("Slack edit propagation", () => {
|
|
|
171
171
|
expect(slackMeta!.editedAt!).toBeGreaterThanOrEqual(t0);
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
// Times out on the default budget: an unresolvable target pays the full
|
|
175
|
+
// `EDIT_LOOKUP_RETRIES` window before it is dropped.
|
|
176
|
+
test("an edit of a message the assistant never stored creates no conversation", async () => {
|
|
177
|
+
// Keying a conversation off the edit's own address made every edit of an
|
|
178
|
+
// unseen message mint one, permanently titled with the loading placeholder.
|
|
179
|
+
const db = getDb();
|
|
180
|
+
const countRows = (table: string): number =>
|
|
181
|
+
(
|
|
182
|
+
db.$client.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
|
|
183
|
+
n: number;
|
|
184
|
+
}
|
|
185
|
+
).n;
|
|
186
|
+
|
|
187
|
+
const resp = (await handleEditIntercept({
|
|
188
|
+
sourceChannel: "slack",
|
|
189
|
+
conversationExternalId: "C0NEVERSEEN",
|
|
190
|
+
externalMessageId: nextEditEventId(),
|
|
191
|
+
sourceMessageId: "1999.0001",
|
|
192
|
+
assistantId: "self",
|
|
193
|
+
content: "edited text",
|
|
194
|
+
})) as Record<string, unknown>;
|
|
195
|
+
|
|
196
|
+
expect(resp.accepted).toBe(true);
|
|
197
|
+
expect(resp.edited).toBe(false);
|
|
198
|
+
expect(countRows("conversations")).toBe(0);
|
|
199
|
+
expect(countRows("channel_inbound_events")).toBe(0);
|
|
200
|
+
expect(countRows("messages")).toBe(0);
|
|
201
|
+
}, 30_000);
|
|
202
|
+
|
|
203
|
+
test("an edit records against the edited message's conversation, not a new one", async () => {
|
|
204
|
+
const seeded = await seedSlackMessage({
|
|
205
|
+
conversationExternalId: "C0123CHANNEL",
|
|
206
|
+
channelTs: "1234.5678",
|
|
207
|
+
initialContent: "original text",
|
|
208
|
+
});
|
|
209
|
+
const db = getDb();
|
|
210
|
+
const conversationsBefore = (
|
|
211
|
+
db.$client.prepare("SELECT COUNT(*) AS n FROM conversations").get() as {
|
|
212
|
+
n: number;
|
|
213
|
+
}
|
|
214
|
+
).n;
|
|
215
|
+
|
|
216
|
+
await handleEditIntercept({
|
|
217
|
+
sourceChannel: "slack",
|
|
218
|
+
conversationExternalId: seeded.conversationExternalId,
|
|
219
|
+
externalMessageId: nextEditEventId(),
|
|
220
|
+
sourceMessageId: seeded.channelTs,
|
|
221
|
+
assistantId: "self",
|
|
222
|
+
content: "new text",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const conversationsAfter = (
|
|
226
|
+
db.$client.prepare("SELECT COUNT(*) AS n FROM conversations").get() as {
|
|
227
|
+
n: number;
|
|
228
|
+
}
|
|
229
|
+
).n;
|
|
230
|
+
expect(conversationsAfter).toBe(conversationsBefore);
|
|
231
|
+
|
|
232
|
+
const editEvents = db.$client
|
|
233
|
+
.prepare(
|
|
234
|
+
`SELECT conversation_id AS conversationId FROM channel_inbound_events
|
|
235
|
+
WHERE external_message_id LIKE 'edit-event-%'`,
|
|
236
|
+
)
|
|
237
|
+
.all() as Array<{ conversationId: string }>;
|
|
238
|
+
expect(editEvents).toHaveLength(1);
|
|
239
|
+
expect(editEvents[0].conversationId).toBe(seeded.conversationId);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a redelivered edit is answered without repeating the lookup window", async () => {
|
|
243
|
+
const seeded = await seedSlackMessage({
|
|
244
|
+
conversationExternalId: "C0123CHANNEL",
|
|
245
|
+
channelTs: "1234.5678",
|
|
246
|
+
initialContent: "original text",
|
|
247
|
+
});
|
|
248
|
+
const editEventId = nextEditEventId();
|
|
249
|
+
const send = () =>
|
|
250
|
+
handleEditIntercept({
|
|
251
|
+
sourceChannel: "slack",
|
|
252
|
+
conversationExternalId: seeded.conversationExternalId,
|
|
253
|
+
externalMessageId: editEventId,
|
|
254
|
+
sourceMessageId: seeded.channelTs,
|
|
255
|
+
assistantId: "self",
|
|
256
|
+
content: "new text",
|
|
257
|
+
}) as Promise<Record<string, unknown>>;
|
|
258
|
+
|
|
259
|
+
const first = await send();
|
|
260
|
+
const second = await send();
|
|
261
|
+
|
|
262
|
+
expect(first.duplicate).toBe(false);
|
|
263
|
+
expect(second.duplicate).toBe(true);
|
|
264
|
+
expect(second.eventId).toBe(first.eventId);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("a threaded edit stays in the edited message's conversation and preserves thread metadata", async () => {
|
|
175
268
|
const conversationExternalId = "C0123CHANNEL";
|
|
176
269
|
const threadTs = "1234.0000";
|
|
177
270
|
const seeded = await seedSlackMessage({
|
|
@@ -179,6 +272,14 @@ describe("Slack edit propagation", () => {
|
|
|
179
272
|
channelTs: "1234.5678",
|
|
180
273
|
initialContent: "original text",
|
|
181
274
|
});
|
|
275
|
+
const db = getDb();
|
|
276
|
+
const conversationCount = (): number =>
|
|
277
|
+
(
|
|
278
|
+
db.$client.prepare("SELECT COUNT(*) AS n FROM conversations").get() as {
|
|
279
|
+
n: number;
|
|
280
|
+
}
|
|
281
|
+
).n;
|
|
282
|
+
const before = conversationCount();
|
|
182
283
|
|
|
183
284
|
const resp = await handleEditIntercept({
|
|
184
285
|
sourceChannel: "slack",
|
|
@@ -191,10 +292,12 @@ describe("Slack edit propagation", () => {
|
|
|
191
292
|
});
|
|
192
293
|
|
|
193
294
|
expect((resp as Record<string, unknown>).accepted).toBe(true);
|
|
295
|
+
|
|
296
|
+
// The thread id names where the reply goes, not a conversation of the
|
|
297
|
+
// edit's own. Keying one off it is what minted a conversation per edit.
|
|
194
298
|
const threadedKey = `asst:self:slack:${conversationExternalId}:thread:${threadTs}`;
|
|
195
|
-
|
|
196
|
-
expect(
|
|
197
|
-
expect(editConversation!.conversationId).not.toBe(seeded.conversationId);
|
|
299
|
+
expect(getConversationByKey(threadedKey)).toBeNull();
|
|
300
|
+
expect(conversationCount()).toBe(before);
|
|
198
301
|
|
|
199
302
|
const after = readMessageRow(seeded.messageId);
|
|
200
303
|
const outer = JSON.parse(after.metadata!) as Record<string, unknown>;
|
|
@@ -54,6 +54,34 @@ describe("events client registration", () => {
|
|
|
54
54
|
ac.abort();
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
test("registers Windows with only its implemented host capabilities", () => {
|
|
58
|
+
const ac = new AbortController();
|
|
59
|
+
const hub = new AssistantEventHub();
|
|
60
|
+
|
|
61
|
+
handleSubscribeAssistantEvents(
|
|
62
|
+
{
|
|
63
|
+
headers: {
|
|
64
|
+
"x-vellum-client-id": "test-windows-001",
|
|
65
|
+
"x-vellum-interface-id": "windows",
|
|
66
|
+
},
|
|
67
|
+
abortSignal: ac.signal,
|
|
68
|
+
},
|
|
69
|
+
{ hub },
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const entry = hub.getClientById("test-windows-001");
|
|
73
|
+
expect(entry?.interfaceId).toBe("windows");
|
|
74
|
+
expect(entry?.capabilities).toEqual([
|
|
75
|
+
"host_bash",
|
|
76
|
+
"host_file",
|
|
77
|
+
"host_cu",
|
|
78
|
+
"host_browser",
|
|
79
|
+
"host_ui_snapshot",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
ac.abort();
|
|
83
|
+
});
|
|
84
|
+
|
|
57
85
|
test("skips registration when no headers are provided (backwards compat)", () => {
|
|
58
86
|
const ac = new AbortController();
|
|
59
87
|
const hub = new AssistantEventHub();
|
|
@@ -76,8 +76,10 @@ beforeEach(() => {
|
|
|
76
76
|
enqueueCalls.length = 0;
|
|
77
77
|
enqueueThrows = false;
|
|
78
78
|
// Reset to a stable tmp path so the sandbox tests (which don't use pkb/)
|
|
79
|
-
// deterministically land outside any configured PKB root.
|
|
80
|
-
|
|
79
|
+
// deterministically land outside any configured PKB root. A subdirectory
|
|
80
|
+
// rather than tmpdir() itself, so vellumRoot() (the parent) also stays
|
|
81
|
+
// inside the temp directory for the live-workspace guard.
|
|
82
|
+
process.env.VELLUM_WORKSPACE_DIR = join(tmpdir(), "file-write-tool-ws");
|
|
81
83
|
});
|
|
82
84
|
|
|
83
85
|
afterEach(() => {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a guardian's own gated-tool prompt is delivered.
|
|
3
|
+
*
|
|
4
|
+
* The prompt is raised by a turn the guardian is having, and that turn can be
|
|
5
|
+
* running in a room: a shared Slack channel, a Telegram group. The card
|
|
6
|
+
* carries the tool name and a preview of the command, so delivering it where
|
|
7
|
+
* the turn is shows both to everyone there. It goes to the guardian's own
|
|
8
|
+
* bound chat instead.
|
|
9
|
+
*
|
|
10
|
+
* The address is only half of it. A callback URL addresses the turn it came
|
|
11
|
+
* from, and a param left on one either fails the send or wins over the new
|
|
12
|
+
* address outright, which is indistinguishable from never having redirected.
|
|
13
|
+
* The assertions below are written against params the transports read today
|
|
14
|
+
* *and* against one nothing reads, because the guarantee is that everything is
|
|
15
|
+
* dropped rather than that a known list is.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, expect, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
resolveGuardianPromptDelivery,
|
|
22
|
+
stripTurnDestination,
|
|
23
|
+
} from "../approvals/guardian-channel-delivery.js";
|
|
24
|
+
|
|
25
|
+
import "./test-preload.js";
|
|
26
|
+
|
|
27
|
+
const ROOM = "C0SHARED123";
|
|
28
|
+
const GUARDIAN_CHAT = "D0GUARDIANDM";
|
|
29
|
+
|
|
30
|
+
describe("a prompt raised where the guardian is not alone", () => {
|
|
31
|
+
test("is addressed to the guardian's own chat", () => {
|
|
32
|
+
expect(
|
|
33
|
+
resolveGuardianPromptDelivery({
|
|
34
|
+
turnChatId: ROOM,
|
|
35
|
+
turnCallbackUrl: "https://gw.test/deliver/slack",
|
|
36
|
+
guardianChatId: GUARDIAN_CHAT,
|
|
37
|
+
}).chatId,
|
|
38
|
+
).toBe(GUARDIAN_CHAT);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("applies to any channel, including ones not built yet", () => {
|
|
42
|
+
// A Telegram group carries the same exposure as a shared Slack channel,
|
|
43
|
+
// and so will whatever is added next. The rule names no channel, so there
|
|
44
|
+
// is no per-channel list to keep in step with the transports.
|
|
45
|
+
for (const [turnChatId, guardianChatId] of [
|
|
46
|
+
["-1001234567890", "555001"],
|
|
47
|
+
["guild-channel-1", "dm-channel-1"],
|
|
48
|
+
["room@some-future-channel", "direct@some-future-channel"],
|
|
49
|
+
]) {
|
|
50
|
+
expect(
|
|
51
|
+
resolveGuardianPromptDelivery({
|
|
52
|
+
turnChatId,
|
|
53
|
+
turnCallbackUrl: "https://gw.test/deliver/whatever",
|
|
54
|
+
guardianChatId,
|
|
55
|
+
}).chatId,
|
|
56
|
+
).toBe(guardianChatId);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("a prompt raised in the guardian's own chat", () => {
|
|
62
|
+
const turnCallbackUrl = "https://gw.test/deliver/slack?threadTs=111.222";
|
|
63
|
+
|
|
64
|
+
test("does not move, and keeps the turn's callback intact", () => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveGuardianPromptDelivery({
|
|
67
|
+
turnChatId: GUARDIAN_CHAT,
|
|
68
|
+
turnCallbackUrl,
|
|
69
|
+
guardianChatId: GUARDIAN_CHAT,
|
|
70
|
+
}),
|
|
71
|
+
).toEqual({ chatId: GUARDIAN_CHAT, callbackUrl: turnCallbackUrl });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("does not move when no binding resolved", () => {
|
|
75
|
+
// The trust context falls back to the turn's own chat, so this is the same
|
|
76
|
+
// comparison as above: delivery is left exactly as it is today rather than
|
|
77
|
+
// dropped on the floor.
|
|
78
|
+
expect(
|
|
79
|
+
resolveGuardianPromptDelivery({
|
|
80
|
+
turnChatId: ROOM,
|
|
81
|
+
turnCallbackUrl,
|
|
82
|
+
guardianChatId: undefined,
|
|
83
|
+
}),
|
|
84
|
+
).toEqual({ chatId: ROOM, callbackUrl: turnCallbackUrl });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("a redirected delivery leaves the turn behind entirely", () => {
|
|
89
|
+
const redirected = (turnCallbackUrl: string) =>
|
|
90
|
+
resolveGuardianPromptDelivery({
|
|
91
|
+
turnChatId: ROOM,
|
|
92
|
+
turnCallbackUrl,
|
|
93
|
+
guardianChatId: GUARDIAN_CHAT,
|
|
94
|
+
}).callbackUrl;
|
|
95
|
+
|
|
96
|
+
test("drops params the transports read today", () => {
|
|
97
|
+
// threadTs: Slack's thread, absent from the new chat.
|
|
98
|
+
// threadId: a Telegram forum topic, and on Discord the destination itself
|
|
99
|
+
// (`sendTarget` returns it in place of the chatId).
|
|
100
|
+
// dm: changes how Discord reads the address.
|
|
101
|
+
// channel: the turn's own chat, which the gateway hangs on every Slack
|
|
102
|
+
// callback.
|
|
103
|
+
expect(
|
|
104
|
+
redirected(
|
|
105
|
+
"https://gw.test/deliver/slack?channel=C1&threadTs=1.2&threadId=9&dm=1&messageTs=3.4",
|
|
106
|
+
),
|
|
107
|
+
).toBe("https://gw.test/deliver/slack");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("drops a param nothing reads, which is the actual guarantee", () => {
|
|
111
|
+
// If this only dropped a known list, a transport that starts reading a new
|
|
112
|
+
// param would silently undo the redirect and no test would notice. The
|
|
113
|
+
// contract is that the callback keeps its channel and nothing else.
|
|
114
|
+
expect(redirected("https://gw.test/deliver/x?roomId=42")).toBe(
|
|
115
|
+
"https://gw.test/deliver/x",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("keeps the channel, which is how the transport is resolved", () => {
|
|
120
|
+
// `isDirectDelivery` picks a transport off the path, so the path has to
|
|
121
|
+
// survive for the redirect to be deliverable at all.
|
|
122
|
+
expect(redirected("https://gw.test/deliver/telegram?threadId=7")).toContain(
|
|
123
|
+
"/deliver/telegram",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("stripTurnDestination", () => {
|
|
129
|
+
test("returns relative or malformed urls untouched", () => {
|
|
130
|
+
// They carry no params, and `new URL` throws on them.
|
|
131
|
+
expect(stripTurnDestination("/deliver/slack")).toBe("/deliver/slack");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Regression tests for the guardian-verify-setup skill.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The behaviour under guard is proactive auto-check polling: after a code is
|
|
5
|
+
* delivered, the skill must poll for completion rather than leaving the user
|
|
6
|
+
* to ask whether it worked. Originally that existed only for voice, and these
|
|
7
|
+
* tests pinned the voice section. The three per-channel polling sections were
|
|
8
|
+
* later merged into one parameterised section, so the assertions here pin the
|
|
9
|
+
* behaviour in its shared form and cover every channel that polls.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { readFileSync } from "node:fs";
|
|
@@ -23,128 +27,170 @@ const SKILL_PATH = resolve(
|
|
|
23
27
|
|
|
24
28
|
const skillContent = readFileSync(SKILL_PATH, "utf-8");
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The channels that poll. Telegram is deliberately absent: it confirms through
|
|
32
|
+
* its own bot-driven flow, so polling it reports nothing.
|
|
33
|
+
*/
|
|
34
|
+
const POLLED_CHANNELS = ["phone", "slack", "discord", "email"] as const;
|
|
35
|
+
|
|
36
|
+
function section(from: string, to: string): string {
|
|
37
|
+
const body = skillContent.split(from)[1]?.split(to)[0];
|
|
38
|
+
// An empty slice would make every `toContain` below fail loudly rather than
|
|
39
|
+
// silently pass, which is the intent: a renamed heading must break this.
|
|
40
|
+
return body ?? "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pollingSection = () => section("## Auto-Check Polling", "## Step 6");
|
|
44
|
+
|
|
26
45
|
// ---------------------------------------------------------------------------
|
|
27
46
|
// Tests
|
|
28
47
|
// ---------------------------------------------------------------------------
|
|
29
48
|
|
|
30
|
-
describe("guardian-verify-setup skill
|
|
31
|
-
test("
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
49
|
+
describe("guardian-verify-setup skill: proactive auto-check polling", () => {
|
|
50
|
+
test("the polling section exists", () => {
|
|
51
|
+
expect(skillContent).toContain("## Auto-Check Polling");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("Step 3 sends every polled channel into the loop", () => {
|
|
55
|
+
const step3 = section("## Step 3", "## Step 4");
|
|
56
|
+
for (const channel of POLLED_CHANNELS) {
|
|
57
|
+
const bullet = step3
|
|
58
|
+
.split("\n")
|
|
59
|
+
.filter((line) =>
|
|
60
|
+
new RegExp(
|
|
61
|
+
`^\\s*-\\s+\\*\\*(Phone|Slack|Discord|Email)\\*\\*`,
|
|
62
|
+
"i",
|
|
63
|
+
).test(line),
|
|
64
|
+
)
|
|
65
|
+
.find((line) => new RegExp(channel, "i").test(line));
|
|
66
|
+
expect(bullet, `Step 3 has no bullet for ${channel}`).toBeDefined();
|
|
67
|
+
expect(bullet).toContain("auto-check polling loop");
|
|
68
|
+
}
|
|
37
69
|
});
|
|
38
70
|
|
|
39
|
-
test("
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
test("Step 4 resend sends every polled channel back into the loop", () => {
|
|
72
|
+
const resend = section("## Step 4", "## Step 5");
|
|
73
|
+
for (const channel of POLLED_CHANNELS) {
|
|
74
|
+
expect(
|
|
75
|
+
new RegExp(`\\*\\*${channel}\\*\\*`, "i").test(resend),
|
|
76
|
+
`Step 4 has no bullet for ${channel}`,
|
|
77
|
+
).toBe(true);
|
|
78
|
+
}
|
|
79
|
+
expect(resend).toContain("auto-check polling loop");
|
|
44
80
|
});
|
|
45
81
|
|
|
46
|
-
test("
|
|
47
|
-
|
|
82
|
+
test("the polling section names every polled channel and excludes Telegram", () => {
|
|
83
|
+
const body = pollingSection();
|
|
84
|
+
for (const channel of POLLED_CHANNELS) {
|
|
85
|
+
expect(body, `polling section omits ${channel}`).toContain(channel);
|
|
86
|
+
}
|
|
87
|
+
// Telegram may only appear as an exclusion, never as a channel to poll.
|
|
88
|
+
expect(body).toContain("Never Telegram");
|
|
48
89
|
});
|
|
49
90
|
|
|
50
|
-
test("polling
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect(
|
|
56
|
-
|
|
91
|
+
test("the polling command is guarded against an unset channel", () => {
|
|
92
|
+
const body = pollingSection();
|
|
93
|
+
// The command is parameterised, so the channel has to be assigned in the
|
|
94
|
+
// same block. An unset CHANNEL would call the CLI with an empty value.
|
|
95
|
+
expect(body).toContain('CHANNEL=""');
|
|
96
|
+
expect(body).toContain('if [ -z "$CHANNEL" ]');
|
|
97
|
+
expect(body).toContain(
|
|
98
|
+
'channel-verification-sessions status --channel "$CHANNEL" --json',
|
|
57
99
|
);
|
|
58
100
|
});
|
|
59
101
|
|
|
60
|
-
test("polling section
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(
|
|
102
|
+
test("the polling section states an interval and a timeout per channel", () => {
|
|
103
|
+
const body = pollingSection();
|
|
104
|
+
expect(body).toContain("15s");
|
|
105
|
+
expect(body).toContain("20s");
|
|
106
|
+
expect(body).toContain("2 minutes");
|
|
107
|
+
expect(body).toContain("3 minutes");
|
|
66
108
|
});
|
|
67
109
|
|
|
68
|
-
test("polling section
|
|
69
|
-
|
|
70
|
-
skillContent
|
|
71
|
-
.split("## Voice Auto-Check Polling")[1]
|
|
72
|
-
?.split("## Step 6")[0] ?? "";
|
|
73
|
-
expect(pollingSection).toContain("2 minutes");
|
|
110
|
+
test("the polling section checks for bound: true", () => {
|
|
111
|
+
expect(pollingSection()).toContain("bound: true");
|
|
74
112
|
});
|
|
75
113
|
|
|
76
|
-
test("
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
?.split("## Step 6")[0] ?? "";
|
|
81
|
-
expect(pollingSection).toContain("bound: true");
|
|
114
|
+
test("success is reported proactively, not on request", () => {
|
|
115
|
+
const body = pollingSection();
|
|
116
|
+
expect(body).toContain("success message");
|
|
117
|
+
expect(body).toContain("Do NOT require the user to ask");
|
|
82
118
|
});
|
|
83
119
|
|
|
84
|
-
test("
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
?.split("## Step 6")[0] ?? "";
|
|
89
|
-
expect(pollingSection).toContain("proactive success message");
|
|
120
|
+
test("timeout offers a resend rather than stopping silently", () => {
|
|
121
|
+
const body = pollingSection();
|
|
122
|
+
expect(body).toContain("timeout");
|
|
123
|
+
expect(body).toContain("resend");
|
|
90
124
|
});
|
|
91
125
|
|
|
92
|
-
test("
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
expect(pollingSection).toContain("timeout");
|
|
98
|
-
expect(pollingSection).toContain("resend");
|
|
126
|
+
test("the rebind guard survives, with its false-success reasoning", () => {
|
|
127
|
+
const body = pollingSection();
|
|
128
|
+
expect(body).toContain("Rebind guard");
|
|
129
|
+
expect(body).toContain("verificationSessionId");
|
|
130
|
+
expect(body).toContain("Non-rebind flows");
|
|
99
131
|
});
|
|
100
132
|
|
|
101
|
-
test("
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
133
|
+
test("no polled channel's Step 3 bullet tells the assistant to wait", () => {
|
|
134
|
+
const step3 = section("## Step 3", "## Step 4");
|
|
135
|
+
// Narrowed to the polled bullets: Telegram's "wait for the user to confirm
|
|
136
|
+
// they clicked the link" is a real instruction for its own bootstrap flow.
|
|
137
|
+
const bullets = step3
|
|
138
|
+
.split("\n")
|
|
139
|
+
.filter((line) =>
|
|
140
|
+
/^\s*-\s+\*\*(Phone|Slack|Discord|Email)\*\*/i.test(line),
|
|
141
|
+
);
|
|
142
|
+
expect(bullets.length).toBe(POLLED_CHANNELS.length);
|
|
143
|
+
for (const bullet of bullets) {
|
|
144
|
+
expect(bullet).not.toContain("wait for the user to confirm");
|
|
145
|
+
expect(bullet).not.toContain("ask the user if it worked");
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("guardian-verify-setup skill: channel coverage", () => {
|
|
151
|
+
test("every channel the CLI accepts is offered in Step 1", () => {
|
|
152
|
+
const step1 = section("## Step 1", "## Step 2");
|
|
153
|
+
for (const channel of [...POLLED_CHANNELS, "telegram"]) {
|
|
154
|
+
expect(step1, `Step 1 does not offer ${channel}`).toContain(
|
|
155
|
+
`**${channel}**`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("Step 2 collects a destination for every channel", () => {
|
|
161
|
+
const step2 = section("## Step 2", "## Step 3");
|
|
162
|
+
for (const channel of ["Phone", "Telegram", "Slack", "Discord", "Email"]) {
|
|
163
|
+
expect(step2, `Step 2 has no destination for ${channel}`).toContain(
|
|
164
|
+
`**${channel}**`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
116
167
|
});
|
|
117
168
|
|
|
118
|
-
test("
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
test("the missing-secret guardrail names only its exception", () => {
|
|
170
|
+
const guardrail = skillContent
|
|
171
|
+
.split("\n")
|
|
172
|
+
.find((line) => line.includes("Missing `secret` guardrail"));
|
|
173
|
+
expect(guardrail).toBeDefined();
|
|
174
|
+
// The rule covers every flow but one, so the line states that exception.
|
|
175
|
+
// Naming any other channel makes it an enumeration, and an enumeration
|
|
176
|
+
// drifts out of date the next time a channel is added.
|
|
177
|
+
expect(guardrail).toContain("except");
|
|
178
|
+
expect(guardrail).toContain("Telegram");
|
|
179
|
+
const lower = guardrail!.toLowerCase();
|
|
180
|
+
for (const channel of ["phone", "voice", "slack", "discord", "email"]) {
|
|
181
|
+
expect(lower, `guardrail enumerates ${channel}`).not.toContain(channel);
|
|
182
|
+
}
|
|
125
183
|
});
|
|
126
184
|
|
|
127
|
-
test(
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
const voiceAutoCheckSection =
|
|
132
|
-
skillContent
|
|
133
|
-
.split("## Voice Auto-Check Polling")[1]
|
|
134
|
-
?.split("## Step 6")[0] ?? "";
|
|
135
|
-
expect(voiceAutoCheckSection).toContain("Do NOT require the user to ask");
|
|
136
|
-
// The voice bullet in Step 3 should not instruct the assistant to wait
|
|
137
|
-
// for the user to confirm or ask if it worked. Narrow to just the voice
|
|
138
|
-
// bullet line to avoid false positives from Telegram's "wait for the
|
|
139
|
-
// user to confirm they clicked the link" which is unrelated to voice.
|
|
140
|
-
const step3Section =
|
|
141
|
-
skillContent.split("## Step 3")[1]?.split("## Step 4")[0] ?? "";
|
|
142
|
-
const voiceBullet = step3Section
|
|
185
|
+
test("the status and revoke guards allow every supported channel", () => {
|
|
186
|
+
// These enumerate valid CHANNEL values. A channel missing here reads to
|
|
187
|
+
// the assistant as unsupported at exactly the point a user asks for it.
|
|
188
|
+
const guards = skillContent
|
|
143
189
|
.split("\n")
|
|
144
|
-
.filter((line) =>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
190
|
+
.filter((line) => line.includes("MUST set to one of"));
|
|
191
|
+
expect(guards.length).toBeGreaterThanOrEqual(2);
|
|
192
|
+
for (const guard of guards) {
|
|
193
|
+
expect(guard).toContain("discord");
|
|
194
|
+
}
|
|
149
195
|
});
|
|
150
196
|
});
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* installIpcMock();
|
|
18
18
|
*
|
|
19
19
|
* mockIpcResponse("classify_risk",
|
|
20
|
-
* { risk: "low", reason: "ls", matchType: "
|
|
20
|
+
* { risk: "low", reason: "ls", matchType: "registry", scopeOptions: [] });
|
|
21
21
|
*
|
|
22
22
|
* const result = await classifyRisk("bash", { command: "ls" });
|
|
23
23
|
*/
|
|
@@ -36,12 +36,26 @@ export function mockIpcResponse(method: string, response: unknown): void {
|
|
|
36
36
|
responses.set(method, response);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/** Clear all registered responses. */
|
|
39
|
+
/** Clear all registered responses and captured params. */
|
|
40
40
|
export function clearIpcMocks(): void {
|
|
41
41
|
responses.clear();
|
|
42
|
+
lastParams.clear();
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
const lastParams = new Map<string, Record<string, unknown> | undefined>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The params of the most recent call to `method`, for asserting what the
|
|
49
|
+
* daemon sent rather than what it did with the answer.
|
|
50
|
+
*/
|
|
51
|
+
export function lastIpcParams(
|
|
52
|
+
method: string,
|
|
53
|
+
): Record<string, unknown> | undefined {
|
|
54
|
+
return lastParams.get(method);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleCall(method: string, params?: Record<string, unknown>): unknown {
|
|
58
|
+
lastParams.set(method, params);
|
|
45
59
|
const response = responses.get(method);
|
|
46
60
|
if (response !== undefined) {
|
|
47
61
|
return response;
|
|
@@ -57,11 +71,18 @@ function handleCall(method: string): unknown {
|
|
|
57
71
|
*/
|
|
58
72
|
export function installIpcMock(): void {
|
|
59
73
|
mock.module("@vellumai/gateway-client/ipc-client", () => ({
|
|
60
|
-
ipcCall: async (
|
|
74
|
+
ipcCall: async (
|
|
75
|
+
_socketPath: string,
|
|
76
|
+
method: string,
|
|
77
|
+
params?: Record<string, unknown>,
|
|
78
|
+
) => handleCall(method, params),
|
|
61
79
|
|
|
62
80
|
PersistentIpcClient: class MockPersistentIpcClient {
|
|
63
|
-
async call(
|
|
64
|
-
|
|
81
|
+
async call(
|
|
82
|
+
method: string,
|
|
83
|
+
params?: Record<string, unknown>,
|
|
84
|
+
): Promise<unknown> {
|
|
85
|
+
return handleCall(method, params);
|
|
65
86
|
}
|
|
66
87
|
destroy(): void {}
|
|
67
88
|
},
|