@vellumai/assistant 0.7.2 → 0.7.3
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/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -17,11 +17,20 @@ mock.module("../config/loader.js", () => ({
|
|
|
17
17
|
const sentMessages: unknown[] = [];
|
|
18
18
|
const sentMessageOptions: unknown[] = [];
|
|
19
19
|
let mockHasClient = false;
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
type MockClient = {
|
|
21
|
+
clientId: string;
|
|
22
|
+
capabilities: string[];
|
|
23
|
+
actorPrincipalId?: string;
|
|
24
|
+
};
|
|
25
|
+
let mockCapableClients: Array<MockClient> = [];
|
|
26
|
+
let mockClientRegistry: Map<string, MockClient> = new Map();
|
|
22
27
|
|
|
23
28
|
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
24
|
-
broadcastMessage: (
|
|
29
|
+
broadcastMessage: (
|
|
30
|
+
msg: unknown,
|
|
31
|
+
_conversationId?: string,
|
|
32
|
+
options?: unknown,
|
|
33
|
+
) => {
|
|
25
34
|
sentMessages.push(msg);
|
|
26
35
|
sentMessageOptions.push(options);
|
|
27
36
|
},
|
|
@@ -30,6 +39,8 @@ mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
|
30
39
|
cap === "host_bash" && mockHasClient ? { id: "mock-client" } : null,
|
|
31
40
|
listClientsByCapability: (_cap: string) => mockCapableClients,
|
|
32
41
|
getClientById: (clientId: string) => mockClientRegistry.get(clientId),
|
|
42
|
+
getActorPrincipalIdForClient: (clientId: string) =>
|
|
43
|
+
mockClientRegistry.get(clientId)?.actorPrincipalId,
|
|
33
44
|
},
|
|
34
45
|
}));
|
|
35
46
|
|
|
@@ -50,8 +61,15 @@ describe("HostBashProxy", () => {
|
|
|
50
61
|
proxy = new (HostBashProxy as any)();
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
function setupSingleClient(
|
|
54
|
-
|
|
64
|
+
function setupSingleClient(
|
|
65
|
+
clientId: string = "client-1",
|
|
66
|
+
actorPrincipalId: string = "user-A",
|
|
67
|
+
) {
|
|
68
|
+
const entry: MockClient = {
|
|
69
|
+
clientId,
|
|
70
|
+
capabilities: ["host_bash"],
|
|
71
|
+
actorPrincipalId,
|
|
72
|
+
};
|
|
55
73
|
mockCapableClients = [entry];
|
|
56
74
|
mockClientRegistry.set(clientId, entry);
|
|
57
75
|
}
|
|
@@ -60,6 +78,7 @@ describe("HostBashProxy", () => {
|
|
|
60
78
|
mockCapableClients = clientIds.map((id) => ({
|
|
61
79
|
clientId: id,
|
|
62
80
|
capabilities: ["host_bash"],
|
|
81
|
+
actorPrincipalId: "user-A",
|
|
63
82
|
}));
|
|
64
83
|
for (const entry of mockCapableClients) {
|
|
65
84
|
mockClientRegistry.set(entry.clientId, entry);
|
|
@@ -551,11 +570,13 @@ describe("HostBashProxy", () => {
|
|
|
551
570
|
describe("target client routing", () => {
|
|
552
571
|
test("auto-resolves when exactly one capable client is connected", async () => {
|
|
553
572
|
setup();
|
|
554
|
-
setupSingleClient("client-abc");
|
|
573
|
+
setupSingleClient("client-abc", "user-A");
|
|
555
574
|
|
|
556
575
|
const resultPromise = proxy.request(
|
|
557
576
|
{ command: "echo hello" },
|
|
558
577
|
"session-1",
|
|
578
|
+
undefined,
|
|
579
|
+
"user-A",
|
|
559
580
|
);
|
|
560
581
|
|
|
561
582
|
expect(sentMessages).toHaveLength(1);
|
|
@@ -580,15 +601,21 @@ describe("HostBashProxy", () => {
|
|
|
580
601
|
|
|
581
602
|
test("uses explicit targetClientId when it is valid", async () => {
|
|
582
603
|
setup();
|
|
583
|
-
setupSingleClient("client-abc");
|
|
604
|
+
setupSingleClient("client-abc", "user-A");
|
|
584
605
|
// Also register a second client so we're sure explicit targeting works
|
|
585
|
-
const entry2 = {
|
|
606
|
+
const entry2: MockClient = {
|
|
607
|
+
clientId: "client-xyz",
|
|
608
|
+
capabilities: ["host_bash"],
|
|
609
|
+
actorPrincipalId: "user-A",
|
|
610
|
+
};
|
|
586
611
|
mockCapableClients.push(entry2);
|
|
587
612
|
mockClientRegistry.set("client-xyz", entry2);
|
|
588
613
|
|
|
589
614
|
const resultPromise = proxy.request(
|
|
590
615
|
{ command: "echo hello", targetClientId: "client-abc" },
|
|
591
616
|
"session-1",
|
|
617
|
+
undefined,
|
|
618
|
+
"user-A",
|
|
592
619
|
);
|
|
593
620
|
|
|
594
621
|
expect(sentMessages).toHaveLength(1);
|
|
@@ -612,17 +639,21 @@ describe("HostBashProxy", () => {
|
|
|
612
639
|
|
|
613
640
|
test("returns error for explicit targetClientId that is not connected", async () => {
|
|
614
641
|
setup();
|
|
615
|
-
setupSingleClient("client-abc");
|
|
642
|
+
setupSingleClient("client-abc", "user-A");
|
|
616
643
|
|
|
617
644
|
const result = await proxy.request(
|
|
618
645
|
{ command: "echo hello", targetClientId: "client-unknown" },
|
|
619
646
|
"session-1",
|
|
647
|
+
undefined,
|
|
648
|
+
"user-A",
|
|
620
649
|
);
|
|
621
650
|
|
|
622
651
|
// Should return error without broadcasting
|
|
623
652
|
expect(result.isError).toBe(true);
|
|
624
653
|
expect(result.content).toContain("client-unknown");
|
|
625
|
-
expect(result.content).toContain(
|
|
654
|
+
expect(result.content).toContain(
|
|
655
|
+
"assistant clients list --capability host_bash",
|
|
656
|
+
);
|
|
626
657
|
expect(sentMessages).toHaveLength(0);
|
|
627
658
|
});
|
|
628
659
|
|
|
@@ -632,11 +663,14 @@ describe("HostBashProxy", () => {
|
|
|
632
663
|
mockClientRegistry.set("client-no-bash", {
|
|
633
664
|
clientId: "client-no-bash",
|
|
634
665
|
capabilities: [],
|
|
666
|
+
actorPrincipalId: "user-A",
|
|
635
667
|
});
|
|
636
668
|
|
|
637
669
|
const result = await proxy.request(
|
|
638
670
|
{ command: "echo hello", targetClientId: "client-no-bash" },
|
|
639
671
|
"session-1",
|
|
672
|
+
undefined,
|
|
673
|
+
"user-A",
|
|
640
674
|
);
|
|
641
675
|
|
|
642
676
|
expect(result.isError).toBe(true);
|
|
@@ -645,36 +679,23 @@ describe("HostBashProxy", () => {
|
|
|
645
679
|
expect(sentMessages).toHaveLength(0);
|
|
646
680
|
});
|
|
647
681
|
|
|
648
|
-
test("
|
|
682
|
+
test("rejects ambiguously when multiple same-user capable clients are connected and no targetClientId", async () => {
|
|
683
|
+
// Regression: previously fell through to untargeted broadcast, fanning
|
|
684
|
+
// a single targeted-style request out across every same-user machine.
|
|
649
685
|
setup();
|
|
650
686
|
setupMultipleClients(["client-1", "client-2", "client-3"]);
|
|
651
687
|
|
|
652
|
-
const
|
|
688
|
+
const result = await proxy.request(
|
|
653
689
|
{ command: "echo hello" },
|
|
654
690
|
"session-1",
|
|
691
|
+
undefined,
|
|
692
|
+
"user-A",
|
|
655
693
|
);
|
|
656
694
|
|
|
657
|
-
|
|
658
|
-
expect(
|
|
659
|
-
|
|
660
|
-
expect(
|
|
661
|
-
// No target client resolved — untargeted broadcast
|
|
662
|
-
expect(sent.targetClientId).toBeUndefined();
|
|
663
|
-
|
|
664
|
-
const opts = sentMessageOptions[0] as Record<string, unknown> | undefined;
|
|
665
|
-
expect(opts?.targetClientId).toBeUndefined();
|
|
666
|
-
|
|
667
|
-
// Manually resolve to clean up
|
|
668
|
-
const requestId = sent.requestId as string;
|
|
669
|
-
proxy.resolveResult(requestId, {
|
|
670
|
-
stdout: "hello\n",
|
|
671
|
-
stderr: "",
|
|
672
|
-
exitCode: 0,
|
|
673
|
-
timedOut: false,
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
const result = await resultPromise;
|
|
677
|
-
expect(result.isError).toBe(false);
|
|
695
|
+
expect(result.isError).toBe(true);
|
|
696
|
+
expect(result.content).toContain("target_client_id");
|
|
697
|
+
// No broadcast happened
|
|
698
|
+
expect(sentMessages).toHaveLength(0);
|
|
678
699
|
});
|
|
679
700
|
|
|
680
701
|
test("falls through to broadcast when zero capable clients (existing timeout path)", async () => {
|
|
@@ -707,13 +728,15 @@ describe("HostBashProxy", () => {
|
|
|
707
728
|
|
|
708
729
|
test("includes targetClientId in timeout error message when client was resolved", async () => {
|
|
709
730
|
setup();
|
|
710
|
-
setupSingleClient("client-mac");
|
|
731
|
+
setupSingleClient("client-mac", "user-A");
|
|
711
732
|
|
|
712
733
|
jest.useFakeTimers();
|
|
713
734
|
try {
|
|
714
735
|
const resultPromise = proxy.request(
|
|
715
736
|
{ command: "echo slow", timeout_seconds: 30 },
|
|
716
737
|
"session-1",
|
|
738
|
+
undefined,
|
|
739
|
+
"user-A",
|
|
717
740
|
);
|
|
718
741
|
|
|
719
742
|
// Proxy timeout = 33s; advance past it
|
|
@@ -727,4 +750,174 @@ describe("HostBashProxy", () => {
|
|
|
727
750
|
}
|
|
728
751
|
});
|
|
729
752
|
});
|
|
753
|
+
|
|
754
|
+
describe("same-user binding (sourceActorPrincipalId)", () => {
|
|
755
|
+
const SAME_USER_REJECTION =
|
|
756
|
+
"Submitting actor does not match the target client's actor for this request. The targeted client's authenticated user must submit the result.";
|
|
757
|
+
|
|
758
|
+
test("same-user targeted request succeeds", async () => {
|
|
759
|
+
setup();
|
|
760
|
+
setupSingleClient("client-abc", "user-A");
|
|
761
|
+
|
|
762
|
+
const resultPromise = proxy.request(
|
|
763
|
+
{ command: "echo hello", targetClientId: "client-abc" },
|
|
764
|
+
"session-1",
|
|
765
|
+
undefined,
|
|
766
|
+
"user-A",
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
expect(sentMessages).toHaveLength(1);
|
|
770
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
771
|
+
expect(sent.type).toBe("host_bash_request");
|
|
772
|
+
expect(sent.targetClientId).toBe("client-abc");
|
|
773
|
+
const requestId = sent.requestId as string;
|
|
774
|
+
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
775
|
+
|
|
776
|
+
proxy.resolveResult(requestId, {
|
|
777
|
+
stdout: "hello\n",
|
|
778
|
+
stderr: "",
|
|
779
|
+
exitCode: 0,
|
|
780
|
+
timedOut: false,
|
|
781
|
+
});
|
|
782
|
+
await resultPromise;
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("cross-user targeted request rejected", async () => {
|
|
786
|
+
setup();
|
|
787
|
+
setupSingleClient("client-abc", "user-A");
|
|
788
|
+
|
|
789
|
+
const result = await proxy.request(
|
|
790
|
+
{ command: "echo hello", targetClientId: "client-abc" },
|
|
791
|
+
"session-1",
|
|
792
|
+
undefined,
|
|
793
|
+
"user-B",
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
expect(result.isError).toBe(true);
|
|
797
|
+
expect(result.content).toBe(SAME_USER_REJECTION);
|
|
798
|
+
// No broadcast and no pending registration
|
|
799
|
+
expect(sentMessages).toHaveLength(0);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
test("target client missing actorPrincipalId rejected", async () => {
|
|
803
|
+
setup();
|
|
804
|
+
// Register a client without an actorPrincipalId (legacy/service-token).
|
|
805
|
+
const entry: MockClient = {
|
|
806
|
+
clientId: "client-abc",
|
|
807
|
+
capabilities: ["host_bash"],
|
|
808
|
+
};
|
|
809
|
+
mockCapableClients = [entry];
|
|
810
|
+
mockClientRegistry.set("client-abc", entry);
|
|
811
|
+
|
|
812
|
+
const result = await proxy.request(
|
|
813
|
+
{ command: "echo hello", targetClientId: "client-abc" },
|
|
814
|
+
"session-1",
|
|
815
|
+
undefined,
|
|
816
|
+
"user-A",
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
expect(result.isError).toBe(true);
|
|
820
|
+
expect(result.content).toBe(SAME_USER_REJECTION);
|
|
821
|
+
expect(sentMessages).toHaveLength(0);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("source missing actorPrincipalId rejected when targeting", async () => {
|
|
825
|
+
setup();
|
|
826
|
+
setupSingleClient("client-abc", "user-A");
|
|
827
|
+
|
|
828
|
+
const result = await proxy.request(
|
|
829
|
+
{ command: "echo hello", targetClientId: "client-abc" },
|
|
830
|
+
"session-1",
|
|
831
|
+
undefined,
|
|
832
|
+
undefined,
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
expect(result.isError).toBe(true);
|
|
836
|
+
expect(result.content).toBe(SAME_USER_REJECTION);
|
|
837
|
+
expect(sentMessages).toHaveLength(0);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("untargeted local flow unchanged when no auto-resolve match", async () => {
|
|
841
|
+
setup();
|
|
842
|
+
// No capable clients connected — untargeted path runs.
|
|
843
|
+
|
|
844
|
+
const resultPromise = proxy.request(
|
|
845
|
+
{ command: "echo hello" },
|
|
846
|
+
"session-1",
|
|
847
|
+
undefined,
|
|
848
|
+
"user-A",
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
expect(sentMessages).toHaveLength(1);
|
|
852
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
853
|
+
expect(sent.type).toBe("host_bash_request");
|
|
854
|
+
expect(sent.targetClientId).toBeUndefined();
|
|
855
|
+
|
|
856
|
+
const requestId = sent.requestId as string;
|
|
857
|
+
proxy.resolveResult(requestId, {
|
|
858
|
+
stdout: "hello\n",
|
|
859
|
+
stderr: "",
|
|
860
|
+
exitCode: 0,
|
|
861
|
+
timedOut: false,
|
|
862
|
+
});
|
|
863
|
+
await resultPromise;
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test("auto-resolve to same-user client succeeds", async () => {
|
|
867
|
+
setup();
|
|
868
|
+
setupSingleClient("client-abc", "user-A");
|
|
869
|
+
|
|
870
|
+
const resultPromise = proxy.request(
|
|
871
|
+
{ command: "echo hello" },
|
|
872
|
+
"session-1",
|
|
873
|
+
undefined,
|
|
874
|
+
"user-A",
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
expect(sentMessages).toHaveLength(1);
|
|
878
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
879
|
+
expect(sent.targetClientId).toBe("client-abc");
|
|
880
|
+
|
|
881
|
+
const requestId = sent.requestId as string;
|
|
882
|
+
proxy.resolveResult(requestId, {
|
|
883
|
+
stdout: "hello\n",
|
|
884
|
+
stderr: "",
|
|
885
|
+
exitCode: 0,
|
|
886
|
+
timedOut: false,
|
|
887
|
+
});
|
|
888
|
+
await resultPromise;
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
test("auto-resolve to different-user client falls through to untargeted", async () => {
|
|
892
|
+
setup();
|
|
893
|
+
// Single capable client owned by user-B; caller is user-A.
|
|
894
|
+
setupSingleClient("client-abc", "user-B");
|
|
895
|
+
|
|
896
|
+
const resultPromise = proxy.request(
|
|
897
|
+
{ command: "echo hello" },
|
|
898
|
+
"session-1",
|
|
899
|
+
undefined,
|
|
900
|
+
"user-A",
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// Auto-resolve must NOT pick the cross-user client; the untargeted
|
|
904
|
+
// broadcast path runs instead.
|
|
905
|
+
expect(sentMessages).toHaveLength(1);
|
|
906
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
907
|
+
expect(sent.type).toBe("host_bash_request");
|
|
908
|
+
expect(sent.targetClientId).toBeUndefined();
|
|
909
|
+
|
|
910
|
+
const opts = sentMessageOptions[0] as Record<string, unknown> | undefined;
|
|
911
|
+
expect(opts?.targetClientId).toBeUndefined();
|
|
912
|
+
|
|
913
|
+
const requestId = sent.requestId as string;
|
|
914
|
+
proxy.resolveResult(requestId, {
|
|
915
|
+
stdout: "hello\n",
|
|
916
|
+
stderr: "",
|
|
917
|
+
exitCode: 0,
|
|
918
|
+
timedOut: false,
|
|
919
|
+
});
|
|
920
|
+
await resultPromise;
|
|
921
|
+
});
|
|
922
|
+
});
|
|
730
923
|
});
|
|
@@ -35,7 +35,12 @@ mock.module("../runtime/pending-interactions.js", () => ({
|
|
|
35
35
|
|
|
36
36
|
interface ResolveCall {
|
|
37
37
|
requestId: string;
|
|
38
|
-
result: {
|
|
38
|
+
result: {
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
exitCode: number | null;
|
|
42
|
+
timedOut: boolean;
|
|
43
|
+
};
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
const resolveSpy: ResolveCall[] = [];
|
|
@@ -46,7 +51,12 @@ mock.module("../daemon/host-bash-proxy.js", () => ({
|
|
|
46
51
|
return {
|
|
47
52
|
resolveResult(
|
|
48
53
|
requestId: string,
|
|
49
|
-
result: {
|
|
54
|
+
result: {
|
|
55
|
+
stdout: string;
|
|
56
|
+
stderr: string;
|
|
57
|
+
exitCode: number | null;
|
|
58
|
+
timedOut: boolean;
|
|
59
|
+
},
|
|
50
60
|
) {
|
|
51
61
|
// resolveResult() internally calls pendingInteractions.resolve() in the real
|
|
52
62
|
// implementation; simulate that here so resolvedIds assertions still hold.
|
|
@@ -58,6 +68,16 @@ mock.module("../daemon/host-bash-proxy.js", () => ({
|
|
|
58
68
|
},
|
|
59
69
|
}));
|
|
60
70
|
|
|
71
|
+
// Stored actor-principal-id keyed by clientId, populated by tests.
|
|
72
|
+
const clientActorPrincipals = new Map<string, string>();
|
|
73
|
+
|
|
74
|
+
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
75
|
+
assistantEventHub: {
|
|
76
|
+
getActorPrincipalIdForClient: (clientId: string) =>
|
|
77
|
+
clientActorPrincipals.get(clientId),
|
|
78
|
+
},
|
|
79
|
+
}));
|
|
80
|
+
|
|
61
81
|
// ── Real imports (after mocks) ───────────────────────────────────────
|
|
62
82
|
|
|
63
83
|
import {
|
|
@@ -82,10 +102,19 @@ function registerPending(
|
|
|
82
102
|
requestId: string,
|
|
83
103
|
overrides: Partial<PendingInteraction> = {},
|
|
84
104
|
): void {
|
|
105
|
+
// Mirror the production proxy behavior: capture the target's actor
|
|
106
|
+
// principal at registration time so the result-route check compares
|
|
107
|
+
// against the persisted value rather than a live hub lookup.
|
|
108
|
+
const targetActorPrincipalId =
|
|
109
|
+
overrides.targetActorPrincipalId ??
|
|
110
|
+
(overrides.targetClientId
|
|
111
|
+
? clientActorPrincipals.get(overrides.targetClientId)
|
|
112
|
+
: undefined);
|
|
85
113
|
pendingStore.set(requestId, {
|
|
86
114
|
conversationId: "conv-1",
|
|
87
115
|
kind: "host_bash",
|
|
88
116
|
...overrides,
|
|
117
|
+
targetActorPrincipalId,
|
|
89
118
|
});
|
|
90
119
|
}
|
|
91
120
|
|
|
@@ -106,6 +135,7 @@ describe("handleHostBashResult", () => {
|
|
|
106
135
|
pendingStore.clear();
|
|
107
136
|
resolvedIds.length = 0;
|
|
108
137
|
resolveSpy.length = 0;
|
|
138
|
+
clientActorPrincipals.clear();
|
|
109
139
|
});
|
|
110
140
|
|
|
111
141
|
// ── Happy paths ────────────────────────────────────────────────────
|
|
@@ -142,11 +172,15 @@ describe("handleHostBashResult", () => {
|
|
|
142
172
|
describe("targeted request (targetClientId set)", () => {
|
|
143
173
|
test("accepts when x-vellum-client-id matches targetClientId", async () => {
|
|
144
174
|
const requestId = "req-targeted-match";
|
|
175
|
+
clientActorPrincipals.set("client-abc", "principal-1");
|
|
145
176
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
146
177
|
|
|
147
178
|
const result = await handleHostBashResult({
|
|
148
179
|
body: bashBody(requestId),
|
|
149
|
-
headers: {
|
|
180
|
+
headers: {
|
|
181
|
+
"x-vellum-client-id": "client-abc",
|
|
182
|
+
"x-vellum-actor-principal-id": "principal-1",
|
|
183
|
+
},
|
|
150
184
|
});
|
|
151
185
|
|
|
152
186
|
expect(result).toEqual({ accepted: true });
|
|
@@ -157,14 +191,145 @@ describe("handleHostBashResult", () => {
|
|
|
157
191
|
|
|
158
192
|
test("trims whitespace from x-vellum-client-id before comparing", async () => {
|
|
159
193
|
const requestId = "req-targeted-trim";
|
|
194
|
+
clientActorPrincipals.set("client-abc", "principal-1");
|
|
195
|
+
registerPending(requestId, { targetClientId: "client-abc" });
|
|
196
|
+
|
|
197
|
+
const result = await handleHostBashResult({
|
|
198
|
+
body: bashBody(requestId),
|
|
199
|
+
headers: {
|
|
200
|
+
"x-vellum-client-id": " client-abc ",
|
|
201
|
+
"x-vellum-actor-principal-id": "principal-1",
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result).toEqual({ accepted: true });
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── Same-user actor binding (defense-in-depth) ─────────────────────
|
|
210
|
+
|
|
211
|
+
describe("targeted request — actor principal binding", () => {
|
|
212
|
+
test("accepts when submitting actor matches target client's actor", async () => {
|
|
213
|
+
const requestId = "req-actor-match";
|
|
214
|
+
clientActorPrincipals.set("client-abc", "principal-shared");
|
|
215
|
+
registerPending(requestId, { targetClientId: "client-abc" });
|
|
216
|
+
|
|
217
|
+
const result = await handleHostBashResult({
|
|
218
|
+
body: bashBody(requestId),
|
|
219
|
+
headers: {
|
|
220
|
+
"x-vellum-client-id": "client-abc",
|
|
221
|
+
"x-vellum-actor-principal-id": "principal-shared",
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result).toEqual({ accepted: true });
|
|
226
|
+
expect(resolveSpy).toHaveLength(1);
|
|
227
|
+
expect(resolvedIds).toContain(requestId);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("throws ForbiddenError (403) when submitting actor does not match target client's actor", () => {
|
|
231
|
+
const requestId = "req-actor-mismatch";
|
|
232
|
+
clientActorPrincipals.set("client-abc", "principal-victim");
|
|
233
|
+
registerPending(requestId, { targetClientId: "client-abc" });
|
|
234
|
+
|
|
235
|
+
expect(() =>
|
|
236
|
+
handleHostBashResult({
|
|
237
|
+
body: bashBody(requestId),
|
|
238
|
+
headers: {
|
|
239
|
+
"x-vellum-client-id": "client-abc",
|
|
240
|
+
"x-vellum-actor-principal-id": "principal-attacker",
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
).toThrow(ForbiddenError);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("interaction is NOT resolved on cross-actor 403 (still pending)", () => {
|
|
247
|
+
const requestId = "req-actor-mismatch-stays";
|
|
248
|
+
clientActorPrincipals.set("client-abc", "principal-victim");
|
|
249
|
+
registerPending(requestId, { targetClientId: "client-abc" });
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
handleHostBashResult({
|
|
253
|
+
body: bashBody(requestId),
|
|
254
|
+
headers: {
|
|
255
|
+
"x-vellum-client-id": "client-abc",
|
|
256
|
+
"x-vellum-actor-principal-id": "principal-attacker",
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
} catch {
|
|
260
|
+
// expected
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
expect(resolvedIds).not.toContain(requestId);
|
|
264
|
+
expect(pendingStore.has(requestId)).toBe(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("throws ForbiddenError (403) when x-vellum-actor-principal-id header is missing entirely", () => {
|
|
268
|
+
const requestId = "req-actor-missing";
|
|
269
|
+
clientActorPrincipals.set("client-abc", "principal-victim");
|
|
270
|
+
registerPending(requestId, { targetClientId: "client-abc" });
|
|
271
|
+
|
|
272
|
+
expect(() =>
|
|
273
|
+
handleHostBashResult({
|
|
274
|
+
body: bashBody(requestId),
|
|
275
|
+
headers: { "x-vellum-client-id": "client-abc" },
|
|
276
|
+
}),
|
|
277
|
+
).toThrow(ForbiddenError);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("interaction is NOT resolved when submitting actor is missing (still pending)", () => {
|
|
281
|
+
const requestId = "req-actor-missing-stays";
|
|
282
|
+
clientActorPrincipals.set("client-abc", "principal-victim");
|
|
160
283
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
161
284
|
|
|
285
|
+
try {
|
|
286
|
+
handleHostBashResult({
|
|
287
|
+
body: bashBody(requestId),
|
|
288
|
+
headers: { "x-vellum-client-id": "client-abc" },
|
|
289
|
+
});
|
|
290
|
+
} catch {
|
|
291
|
+
// expected
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
expect(resolvedIds).not.toContain(requestId);
|
|
295
|
+
expect(pendingStore.has(requestId)).toBe(true);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("throws ForbiddenError (403) when target client has no stored actor principal", () => {
|
|
299
|
+
// Target client connected without a verified principal (e.g. legacy
|
|
300
|
+
// service token) — refuse the submission rather than silently allow
|
|
301
|
+
// any actor through.
|
|
302
|
+
const requestId = "req-actor-target-missing";
|
|
303
|
+
registerPending(requestId, { targetClientId: "client-abc" });
|
|
304
|
+
// Note: no entry in clientActorPrincipals for "client-abc".
|
|
305
|
+
|
|
306
|
+
expect(() =>
|
|
307
|
+
handleHostBashResult({
|
|
308
|
+
body: bashBody(requestId),
|
|
309
|
+
headers: {
|
|
310
|
+
"x-vellum-client-id": "client-abc",
|
|
311
|
+
"x-vellum-actor-principal-id": "principal-1",
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
).toThrow(ForbiddenError);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ── Untargeted-request behavior (regression for new check) ─────────
|
|
319
|
+
|
|
320
|
+
describe("untargeted request — actor principal check is skipped", () => {
|
|
321
|
+
test("accepts even when submitting actor is absent and no target client is set", async () => {
|
|
322
|
+
const requestId = "req-untargeted-no-actor";
|
|
323
|
+
registerPending(requestId);
|
|
324
|
+
|
|
162
325
|
const result = await handleHostBashResult({
|
|
163
326
|
body: bashBody(requestId),
|
|
164
|
-
headers: {
|
|
327
|
+
headers: {},
|
|
165
328
|
});
|
|
166
329
|
|
|
167
330
|
expect(result).toEqual({ accepted: true });
|
|
331
|
+
expect(resolveSpy).toHaveLength(1);
|
|
332
|
+
expect(resolvedIds).toContain(requestId);
|
|
168
333
|
});
|
|
169
334
|
});
|
|
170
335
|
|
|
@@ -175,9 +340,9 @@ describe("handleHostBashResult", () => {
|
|
|
175
340
|
const requestId = "req-targeted-no-header";
|
|
176
341
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
177
342
|
|
|
178
|
-
expect(() =>
|
|
179
|
-
|
|
180
|
-
)
|
|
343
|
+
expect(() => handleHostBashResult({ body: bashBody(requestId) })).toThrow(
|
|
344
|
+
BadRequestError,
|
|
345
|
+
);
|
|
181
346
|
});
|
|
182
347
|
|
|
183
348
|
test("throws BadRequestError (400) when header is empty string", () => {
|
|
@@ -267,9 +432,9 @@ describe("handleHostBashResult", () => {
|
|
|
267
432
|
});
|
|
268
433
|
|
|
269
434
|
test("throws BadRequestError when requestId is missing", () => {
|
|
270
|
-
expect(() =>
|
|
271
|
-
|
|
272
|
-
)
|
|
435
|
+
expect(() => handleHostBashResult({ body: { stdout: "x" } })).toThrow(
|
|
436
|
+
BadRequestError,
|
|
437
|
+
);
|
|
273
438
|
});
|
|
274
439
|
|
|
275
440
|
test("throws NotFoundError for unknown requestId", () => {
|
|
@@ -287,8 +452,8 @@ describe("handleHostBashResult", () => {
|
|
|
287
452
|
kind: "confirmation",
|
|
288
453
|
});
|
|
289
454
|
|
|
290
|
-
expect(() =>
|
|
291
|
-
|
|
292
|
-
)
|
|
455
|
+
expect(() => handleHostBashResult({ body: bashBody(requestId) })).toThrow(
|
|
456
|
+
ConflictError,
|
|
457
|
+
);
|
|
293
458
|
});
|
|
294
459
|
});
|