@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
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-actor (same-user) binding check used by host proxies and result
|
|
3
|
+
* routes.
|
|
4
|
+
*
|
|
5
|
+
* Verifies that the submitting (source) actor's principal id matches the
|
|
6
|
+
* actor principal id captured for the target client at SSE subscription
|
|
7
|
+
* time. This is the authoritative gate that prevents cross-user
|
|
8
|
+
* execution and cross-user result submission across all three host-proxy
|
|
9
|
+
* capabilities (host_bash, host_file, host_cu).
|
|
10
|
+
*
|
|
11
|
+
* Two entry points map onto the two control-flow styles in the codebase:
|
|
12
|
+
* - {@link enforceSameActorOrErrorResult} for proxies — returns a
|
|
13
|
+
* tool-execution error result on rejection, `null` on success.
|
|
14
|
+
* - {@link enforceSameActorOrThrow} for HTTP/IPC route handlers —
|
|
15
|
+
* throws {@link ForbiddenError} on rejection so the route adapter
|
|
16
|
+
* maps it to HTTP 403.
|
|
17
|
+
*
|
|
18
|
+
* Both paths log a single structured warn line on rejection with the
|
|
19
|
+
* shape `{ sourceActorPrincipalId, targetClientId, targetActorPrincipalId,
|
|
20
|
+
* op, reason }` so that bash, file, and CU rejections render identically
|
|
21
|
+
* in the audit log.
|
|
22
|
+
*/
|
|
23
|
+
import type { HostProxyCapability } from "../../channels/types.js";
|
|
24
|
+
import { getLogger } from "../../util/logger.js";
|
|
25
|
+
import type { AssistantEventHub } from "../assistant-event-hub.js";
|
|
26
|
+
import { ForbiddenError } from "../routes/errors.js";
|
|
27
|
+
|
|
28
|
+
const log = getLogger("same-actor");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Canonical user-facing rejection message. Used by both the proxy and
|
|
32
|
+
* route paths so operators and auditors see identical wording regardless
|
|
33
|
+
* of whether the failure surfaced as a tool-execution result or an HTTP
|
|
34
|
+
* 403.
|
|
35
|
+
*/
|
|
36
|
+
const REJECTION_MESSAGE =
|
|
37
|
+
"Submitting actor does not match the target client's actor for this request. The targeted client's authenticated user must submit the result.";
|
|
38
|
+
|
|
39
|
+
/** OpenAPI 403 description for `*-result` endpoints, kept identical. */
|
|
40
|
+
export const SAME_ACTOR_FORBIDDEN_DESCRIPTION =
|
|
41
|
+
"Submitting client does not match the targeted client, or the submitting actor's principal does not match the target client's actor.";
|
|
42
|
+
|
|
43
|
+
/** Per-capability scope for the structured warn log entry. */
|
|
44
|
+
export type SameActorOp =
|
|
45
|
+
| "host_bash"
|
|
46
|
+
| "host_file"
|
|
47
|
+
| "host_cu"
|
|
48
|
+
| "host_transfer";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Args for the live-lookup variant: caller supplies the hub + target client
|
|
52
|
+
* id, and the helper looks up the target's actor principal in real time.
|
|
53
|
+
* Used at proxy request time (registration), where the SSE subscription is
|
|
54
|
+
* present by definition.
|
|
55
|
+
*/
|
|
56
|
+
export interface SameActorLiveArgs {
|
|
57
|
+
hub: Pick<AssistantEventHub, "getActorPrincipalIdForClient">;
|
|
58
|
+
sourceActorPrincipalId: string | undefined;
|
|
59
|
+
targetClientId: string;
|
|
60
|
+
op: SameActorOp;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Args for the persisted-value variant: caller supplies a target actor
|
|
65
|
+
* principal id captured at registration time. Used at result-submission
|
|
66
|
+
* time, where the SSE subscription may have briefly disconnected and the
|
|
67
|
+
* live hub lookup would falsely 403 a legitimate result.
|
|
68
|
+
*/
|
|
69
|
+
export interface SameActorPersistedArgs {
|
|
70
|
+
sourceActorPrincipalId: string | undefined;
|
|
71
|
+
targetActorPrincipalId: string | undefined;
|
|
72
|
+
targetClientId: string;
|
|
73
|
+
op: SameActorOp;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type SameActorArgs = SameActorLiveArgs;
|
|
77
|
+
|
|
78
|
+
type RejectionReason = "missing_source" | "missing_target" | "mismatch";
|
|
79
|
+
|
|
80
|
+
function isLive(
|
|
81
|
+
args: SameActorLiveArgs | SameActorPersistedArgs,
|
|
82
|
+
): args is SameActorLiveArgs {
|
|
83
|
+
return (args as SameActorLiveArgs).hub != null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Internal: returns the rejection reason or `undefined` when the source
|
|
88
|
+
* matches the target. Always logs on rejection so all callers share the
|
|
89
|
+
* same audit shape.
|
|
90
|
+
*/
|
|
91
|
+
function detectRejection(
|
|
92
|
+
args: SameActorLiveArgs | SameActorPersistedArgs,
|
|
93
|
+
): RejectionReason | undefined {
|
|
94
|
+
const { sourceActorPrincipalId, targetClientId, op } = args;
|
|
95
|
+
const targetActorPrincipalId = isLive(args)
|
|
96
|
+
? args.hub.getActorPrincipalIdForClient(targetClientId)
|
|
97
|
+
: args.targetActorPrincipalId;
|
|
98
|
+
|
|
99
|
+
let reason: RejectionReason | undefined;
|
|
100
|
+
if (sourceActorPrincipalId == null) {
|
|
101
|
+
reason = "missing_source";
|
|
102
|
+
} else if (targetActorPrincipalId == null) {
|
|
103
|
+
reason = "missing_target";
|
|
104
|
+
} else if (sourceActorPrincipalId !== targetActorPrincipalId) {
|
|
105
|
+
reason = "mismatch";
|
|
106
|
+
}
|
|
107
|
+
if (reason == null) return undefined;
|
|
108
|
+
|
|
109
|
+
log.warn(
|
|
110
|
+
{
|
|
111
|
+
sourceActorPrincipalId,
|
|
112
|
+
targetClientId,
|
|
113
|
+
targetActorPrincipalId,
|
|
114
|
+
op,
|
|
115
|
+
reason,
|
|
116
|
+
},
|
|
117
|
+
"Rejecting cross-user host proxy request",
|
|
118
|
+
);
|
|
119
|
+
return reason;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Route-flavored variant: throws {@link ForbiddenError} on rejection so
|
|
124
|
+
* the existing route adapter maps it to HTTP 403. Returns void on
|
|
125
|
+
* success.
|
|
126
|
+
*
|
|
127
|
+
* Accepts EITHER {@link SameActorLiveArgs} (live hub lookup, used at
|
|
128
|
+
* proxy registration time) OR {@link SameActorPersistedArgs} (compare
|
|
129
|
+
* against a value captured earlier, used at result-submission time so a
|
|
130
|
+
* brief SSE reconnect doesn't 403 a legitimate result).
|
|
131
|
+
*/
|
|
132
|
+
export function enforceSameActorOrThrow(
|
|
133
|
+
args: SameActorLiveArgs | SameActorPersistedArgs,
|
|
134
|
+
): void {
|
|
135
|
+
if (detectRejection(args) != null) {
|
|
136
|
+
throw new ForbiddenError(REJECTION_MESSAGE);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Proxy-flavored variant: returns a tool-execution-shaped error result
|
|
142
|
+
* on rejection (so the proxy can pass it directly back to the agent),
|
|
143
|
+
* or `null` on success. Always uses the live hub lookup — proxy
|
|
144
|
+
* registration runs while the target SSE subscription is active.
|
|
145
|
+
*/
|
|
146
|
+
export function enforceSameActorOrErrorResult(
|
|
147
|
+
args: SameActorLiveArgs,
|
|
148
|
+
): { content: string; isError: true } | null {
|
|
149
|
+
if (detectRejection(args) == null) return null;
|
|
150
|
+
return { content: REJECTION_MESSAGE, isError: true };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Result of attempting to auto-resolve a single same-user target client.
|
|
155
|
+
*
|
|
156
|
+
* - `match`: exactly one same-user client supports the capability. Use the
|
|
157
|
+
* returned clientId.
|
|
158
|
+
* - `none`: no same-user client supports the capability. Caller's choice
|
|
159
|
+
* how to handle (typically: fall through to no-target, which broadcasts
|
|
160
|
+
* to nobody when no clients are connected).
|
|
161
|
+
* - `ambiguous`: more than one same-user client supports the capability.
|
|
162
|
+
* Caller MUST refuse to silently broadcast across them; instead surface
|
|
163
|
+
* an error asking the caller to specify `target_client_id`.
|
|
164
|
+
*/
|
|
165
|
+
export type AutoResolveResult =
|
|
166
|
+
| { kind: "match"; clientId: string }
|
|
167
|
+
| { kind: "none" }
|
|
168
|
+
| { kind: "ambiguous" };
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Filter capable clients by `actorPrincipalId === sourcePrincipalId` and
|
|
172
|
+
* report whether exactly one matched, zero matched, or more than one
|
|
173
|
+
* matched.
|
|
174
|
+
*
|
|
175
|
+
* Used by host proxies to auto-resolve a target client when the caller
|
|
176
|
+
* did not specify one. Skipping when the caller has no principal keeps
|
|
177
|
+
* the same-user binding closed: an unauthenticated caller cannot
|
|
178
|
+
* piggyback on a connected user's session.
|
|
179
|
+
*
|
|
180
|
+
* Why three outcomes (vs. just `string | undefined`)? Earlier revisions
|
|
181
|
+
* collapsed `none` and `ambiguous` into `undefined`, which caused the
|
|
182
|
+
* proxy to fall through to an untargeted broadcast — fanning a single
|
|
183
|
+
* targeted-style request out across every same-user machine. Surfacing
|
|
184
|
+
* `ambiguous` separately lets the proxy reject with a clear "specify
|
|
185
|
+
* target_client_id" error instead.
|
|
186
|
+
*/
|
|
187
|
+
export function pickSameUserAutoResolve(args: {
|
|
188
|
+
hub: Pick<AssistantEventHub, "listClientsByCapability">;
|
|
189
|
+
capability: HostProxyCapability;
|
|
190
|
+
sourceActorPrincipalId: string | undefined;
|
|
191
|
+
}): AutoResolveResult {
|
|
192
|
+
const { hub, capability, sourceActorPrincipalId } = args;
|
|
193
|
+
if (sourceActorPrincipalId == null) return { kind: "none" };
|
|
194
|
+
const sameUser = hub
|
|
195
|
+
.listClientsByCapability(capability)
|
|
196
|
+
.filter((c) => c.actorPrincipalId === sourceActorPrincipalId);
|
|
197
|
+
if (sameUser.length === 0) return { kind: "none" };
|
|
198
|
+
if (sameUser.length === 1) {
|
|
199
|
+
return { kind: "match", clientId: sameUser[0].clientId };
|
|
200
|
+
}
|
|
201
|
+
return { kind: "ambiguous" };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Standard error result for proxies when {@link pickSameUserAutoResolve}
|
|
206
|
+
* returns `ambiguous`. Asks the caller to specify `target_client_id`.
|
|
207
|
+
*/
|
|
208
|
+
export function ambiguousSameUserError(capability: HostProxyCapability): {
|
|
209
|
+
content: string;
|
|
210
|
+
isError: true;
|
|
211
|
+
} {
|
|
212
|
+
return {
|
|
213
|
+
content: `Multiple ${capability} clients are connected for this user. Specify target_client_id to disambiguate. Run \`assistant clients list --capability ${capability}\` to see client IDs.`,
|
|
214
|
+
isError: true,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
parseChannelId,
|
|
8
8
|
parseInterfaceId,
|
|
9
9
|
} from "../channels/types.js";
|
|
10
|
+
import { getDiskPressureStatus } from "../daemon/disk-pressure-guard.js";
|
|
11
|
+
import { classifyDiskPressureTurnPolicy } from "../daemon/disk-pressure-policy.js";
|
|
10
12
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
11
13
|
import { updateDeliveredSegmentCount } from "../memory/delivery-channels.js";
|
|
12
|
-
import { linkMessage } from "../memory/delivery-crud.js";
|
|
14
|
+
import { clearPayload, linkMessage } from "../memory/delivery-crud.js";
|
|
13
15
|
import {
|
|
14
16
|
getRetryableEvents,
|
|
15
17
|
markProcessed,
|
|
@@ -18,10 +20,13 @@ import {
|
|
|
18
20
|
} from "../memory/delivery-status.js";
|
|
19
21
|
import { getLogger } from "../util/logger.js";
|
|
20
22
|
import { deliverReplyViaCallback } from "./channel-reply-delivery.js";
|
|
23
|
+
import { deliverChannelReply } from "./gateway-client.js";
|
|
21
24
|
import type { MessageProcessor } from "./http-types.js";
|
|
22
25
|
import { resolveRoutingStateFromRuntime } from "./trust-context-resolver.js";
|
|
23
26
|
|
|
24
27
|
const log = getLogger("runtime-http");
|
|
28
|
+
const DISK_PRESSURE_REMOTE_BLOCK_REPLY =
|
|
29
|
+
"Storage is critically low, so remote messages are ignored until the guardian frees enough space. Please try again later.";
|
|
25
30
|
|
|
26
31
|
function parseTrustRuntimeContext(value: unknown): TrustContext | undefined {
|
|
27
32
|
if (!value || typeof value !== "object") return undefined;
|
|
@@ -163,6 +168,65 @@ export async function sweepFailedEvents(
|
|
|
163
168
|
trustClass: "unknown",
|
|
164
169
|
};
|
|
165
170
|
|
|
171
|
+
const diskPressureDecision = classifyDiskPressureTurnPolicy(
|
|
172
|
+
getDiskPressureStatus(),
|
|
173
|
+
{
|
|
174
|
+
sourceChannel,
|
|
175
|
+
sourceInterface,
|
|
176
|
+
trustContext: {
|
|
177
|
+
sourceChannel: trustContext.sourceChannel,
|
|
178
|
+
trustClass: trustContext.trustClass,
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
if (diskPressureDecision.action === "block") {
|
|
183
|
+
clearPayload(event.id);
|
|
184
|
+
markProcessed(event.id);
|
|
185
|
+
log.info(
|
|
186
|
+
{
|
|
187
|
+
eventId: event.id,
|
|
188
|
+
conversationId: event.conversationId,
|
|
189
|
+
reason: diskPressureDecision.reason,
|
|
190
|
+
trustClass: trustContext.trustClass,
|
|
191
|
+
},
|
|
192
|
+
"Skipped channel retry during disk pressure cleanup mode",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const replyCallbackUrl =
|
|
196
|
+
typeof payload.replyCallbackUrl === "string"
|
|
197
|
+
? payload.replyCallbackUrl
|
|
198
|
+
: undefined;
|
|
199
|
+
const externalChatId =
|
|
200
|
+
typeof payload.externalChatId === "string"
|
|
201
|
+
? payload.externalChatId
|
|
202
|
+
: undefined;
|
|
203
|
+
if (replyCallbackUrl && externalChatId) {
|
|
204
|
+
const requesterExternalUserId =
|
|
205
|
+
trustContext.requesterExternalUserId ??
|
|
206
|
+
(typeof payload.senderExternalUserId === "string"
|
|
207
|
+
? payload.senderExternalUserId
|
|
208
|
+
: undefined);
|
|
209
|
+
const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
210
|
+
chatId: externalChatId,
|
|
211
|
+
text: DISK_PRESSURE_REMOTE_BLOCK_REPLY,
|
|
212
|
+
assistantId,
|
|
213
|
+
};
|
|
214
|
+
if (sourceChannel === "slack" && requesterExternalUserId) {
|
|
215
|
+
replyPayload.ephemeral = true;
|
|
216
|
+
replyPayload.user = requesterExternalUserId;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await deliverChannelReply(replyCallbackUrl, replyPayload);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
log.warn(
|
|
222
|
+
{ err, eventId: event.id, conversationId: event.conversationId },
|
|
223
|
+
"Failed to deliver disk pressure retry block reply",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
166
230
|
const metadataHintsRaw = sourceMetadata?.hints;
|
|
167
231
|
const metadataHints = Array.isArray(metadataHintsRaw)
|
|
168
232
|
? metadataHintsRaw.filter(
|
|
@@ -99,6 +99,13 @@ export interface GuardianReplyResult {
|
|
|
99
99
|
requestId?: string;
|
|
100
100
|
/** Detailed result from the canonical decision primitive (when a decision was attempted). */
|
|
101
101
|
canonicalResult?: CanonicalDecisionResult;
|
|
102
|
+
/** When a voice access request was approved, the contact that should be activated. */
|
|
103
|
+
activatedContact?: {
|
|
104
|
+
sourceChannel: string;
|
|
105
|
+
externalUserId: string;
|
|
106
|
+
externalChatId?: string;
|
|
107
|
+
displayName?: string;
|
|
108
|
+
};
|
|
102
109
|
/**
|
|
103
110
|
* When true, the caller should skip legacy approval interception for this
|
|
104
111
|
* message. Set by the invite handoff bypass so that "open invite flow"
|
|
@@ -686,6 +693,9 @@ async function applyDecision(
|
|
|
686
693
|
...(canonicalResult.resolverReplyText
|
|
687
694
|
? { replyText: canonicalResult.resolverReplyText }
|
|
688
695
|
: {}),
|
|
696
|
+
...(canonicalResult.activatedContact
|
|
697
|
+
? { activatedContact: canonicalResult.activatedContact }
|
|
698
|
+
: {}),
|
|
689
699
|
requestId,
|
|
690
700
|
canonicalResult,
|
|
691
701
|
};
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ChannelId } from "../channels/types.js";
|
|
15
|
+
import { isHttpAuthDisabled } from "../config/env.js";
|
|
15
16
|
import { findGuardianForChannel } from "../contacts/contact-store.js";
|
|
16
17
|
import type { TrustContext } from "../daemon/trust-context.js";
|
|
17
18
|
import { getLogger } from "../util/logger.js";
|
|
@@ -43,6 +44,52 @@ export function buildLocalAuthContext(conversationId: string): AuthContext {
|
|
|
43
44
|
};
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Look up the local vellum guardian's principalId from the contacts table.
|
|
49
|
+
*
|
|
50
|
+
* Returns `undefined` when no vellum guardian binding exists (e.g. fresh
|
|
51
|
+
* install before bootstrap). Callers should treat that case as
|
|
52
|
+
* "not yet available" and either fall back or proceed without a principalId.
|
|
53
|
+
*/
|
|
54
|
+
export function findLocalGuardianPrincipalId(): string | undefined {
|
|
55
|
+
return findGuardianForChannel("vellum")?.contact.principalId ?? undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Translate the synthetic dev-bypass actor principal to the real local
|
|
60
|
+
* guardian's principalId when running in `DISABLE_HTTP_AUTH=true` mode.
|
|
61
|
+
*
|
|
62
|
+
* The dev-bypass `AuthContext` (`runtime/auth/middleware.ts`) injects
|
|
63
|
+
* `"dev-bypass"` as the actor principal id for every request, but tool-side
|
|
64
|
+
* trust resolution (`resolveLocalTrustContext`) and SSE registration both
|
|
65
|
+
* carry the real local guardian principalId. Without this translation, every
|
|
66
|
+
* targeted host_bash/host_file/host_cu/host_transfer result POST mismatches
|
|
67
|
+
* the same-user check and is rejected with 403, and conversation/surface/
|
|
68
|
+
* guardian-action routes resolve trust against the wrong principal.
|
|
69
|
+
*
|
|
70
|
+
* Returns the input unchanged when:
|
|
71
|
+
* - HTTP auth is enabled (production / non-dev-bypass deployments), OR
|
|
72
|
+
* - the input is not literally `"dev-bypass"` (e.g. service tokens).
|
|
73
|
+
*
|
|
74
|
+
* Returns the local guardian principalId when both gates are true. Returns
|
|
75
|
+
* `undefined` when dev-bypass is set but no guardian binding has been created
|
|
76
|
+
* yet (e.g. fresh install before bootstrap); callers must treat this the
|
|
77
|
+
* same as a missing principal.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveActorPrincipalIdForLocalGuardian(
|
|
80
|
+
rawHeader: string | undefined,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
if (rawHeader !== "dev-bypass" || !isHttpAuthDisabled()) return rawHeader;
|
|
83
|
+
|
|
84
|
+
const guardianPrincipalId = findLocalGuardianPrincipalId();
|
|
85
|
+
if (guardianPrincipalId) return guardianPrincipalId;
|
|
86
|
+
|
|
87
|
+
log.warn(
|
|
88
|
+
"dev-bypass actor principal received but no vellum guardian binding found; returning undefined",
|
|
89
|
+
);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
/**
|
|
47
94
|
* Resolve the guardian runtime context for a local connection.
|
|
48
95
|
*
|
|
@@ -60,10 +107,8 @@ export function resolveLocalTrustContext(
|
|
|
60
107
|
): TrustContext {
|
|
61
108
|
const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
62
109
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (guardianResult && guardianResult.contact.principalId) {
|
|
66
|
-
const guardianPrincipalId = guardianResult.contact.principalId;
|
|
110
|
+
const guardianPrincipalId = findLocalGuardianPrincipalId();
|
|
111
|
+
if (guardianPrincipalId) {
|
|
67
112
|
const trustCtx = resolveTrustContext({
|
|
68
113
|
assistantId,
|
|
69
114
|
sourceChannel: "vellum",
|
|
@@ -97,13 +142,9 @@ export function resolveLocalTrustContext(
|
|
|
97
142
|
export function resolveLocalAuthContext(conversationId: string): AuthContext {
|
|
98
143
|
const authContext = buildLocalAuthContext(conversationId);
|
|
99
144
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
...authContext,
|
|
105
|
-
actorPrincipalId: guardianResult.contact.principalId,
|
|
106
|
-
};
|
|
145
|
+
const guardianPrincipalId = findLocalGuardianPrincipalId();
|
|
146
|
+
if (guardianPrincipalId) {
|
|
147
|
+
return { ...authContext, actorPrincipalId: guardianPrincipalId };
|
|
107
148
|
}
|
|
108
149
|
|
|
109
150
|
log.warn(
|
|
@@ -59,6 +59,14 @@ export interface PendingInteraction {
|
|
|
59
59
|
directResolve?: (decision: UserDecision) => void;
|
|
60
60
|
/** When set, the host_bash request should be routed to this specific client. */
|
|
61
61
|
targetClientId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Snapshot of `targetClientId`'s `actorPrincipalId` taken at registration
|
|
64
|
+
* time. Persisted so the result-route same-actor check compares against
|
|
65
|
+
* a stable value rather than the live hub — the target client's SSE
|
|
66
|
+
* subscription may have briefly disconnected between dispatch and result
|
|
67
|
+
* submission, which would otherwise 403 a legitimate result.
|
|
68
|
+
*/
|
|
69
|
+
targetActorPrincipalId?: string;
|
|
62
70
|
|
|
63
71
|
// -- RPC lifecycle (populated by host proxies) --
|
|
64
72
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the GET /v1/clients (list_clients) route.
|
|
3
|
+
*
|
|
4
|
+
* Validates the same-user filter applied to client listings:
|
|
5
|
+
* - Caller sees only clients owned by their `actorPrincipalId`.
|
|
6
|
+
* - Clients with no stored `actorPrincipalId` are filtered out (fail-closed).
|
|
7
|
+
* - Dev-bypass mode (`isHttpAuthDisabled()`) returns all clients.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
// ── Module mocks (must be set up before importing the route) ──────────────
|
|
13
|
+
|
|
14
|
+
let fakeHttpAuthDisabled = false;
|
|
15
|
+
|
|
16
|
+
mock.module("../../../config/env.js", () => ({
|
|
17
|
+
isHttpAuthDisabled: () => fakeHttpAuthDisabled,
|
|
18
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
mock.module("../../../util/logger.js", () => ({
|
|
22
|
+
getLogger: () =>
|
|
23
|
+
new Proxy({} as Record<string, unknown>, {
|
|
24
|
+
get: () => () => {},
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// ── Real imports (after mocks) ────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
import { assistantEventHub } from "../../assistant-event-hub.js";
|
|
31
|
+
import { ROUTES } from "../client-routes.js";
|
|
32
|
+
import type { RouteDefinition } from "../types.js";
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
mock.restore();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// ── Test helpers ──────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function findHandler(operationId: string): RouteDefinition["handler"] {
|
|
41
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
42
|
+
if (!route) throw new Error(`Route ${operationId} not found`);
|
|
43
|
+
return route.handler;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ListClientsResponse = {
|
|
47
|
+
clients: Array<{
|
|
48
|
+
clientId: string;
|
|
49
|
+
interfaceId: string;
|
|
50
|
+
capabilities: string[];
|
|
51
|
+
machineName?: string;
|
|
52
|
+
connectedAt: string;
|
|
53
|
+
lastActiveAt: string;
|
|
54
|
+
}>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function registerClient(args: {
|
|
58
|
+
clientId: string;
|
|
59
|
+
actorPrincipalId?: string;
|
|
60
|
+
}): void {
|
|
61
|
+
assistantEventHub.subscribe({
|
|
62
|
+
type: "client",
|
|
63
|
+
clientId: args.clientId,
|
|
64
|
+
interfaceId: "macos",
|
|
65
|
+
capabilities: ["host_bash", "host_file", "host_cu"],
|
|
66
|
+
actorPrincipalId: args.actorPrincipalId,
|
|
67
|
+
callback: () => {},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function clearHub(): void {
|
|
72
|
+
const ids = assistantEventHub.listClients().map((c) => c.clientId);
|
|
73
|
+
for (const id of ids) {
|
|
74
|
+
assistantEventHub.disposeClient(id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Tests ────────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
describe("list_clients route — same-user filter", () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
fakeHttpAuthDisabled = false;
|
|
83
|
+
clearHub();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("returns only clients owned by the calling actor", () => {
|
|
87
|
+
registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
88
|
+
registerClient({ clientId: "client-A2", actorPrincipalId: "user-A" });
|
|
89
|
+
registerClient({ clientId: "client-B1", actorPrincipalId: "user-B" });
|
|
90
|
+
|
|
91
|
+
const handler = findHandler("list_clients");
|
|
92
|
+
const result = handler({
|
|
93
|
+
headers: { "x-vellum-actor-principal-id": "user-A" },
|
|
94
|
+
}) as ListClientsResponse;
|
|
95
|
+
|
|
96
|
+
const ids = result.clients.map((c) => c.clientId).sort();
|
|
97
|
+
expect(ids).toEqual(["client-A1", "client-A2"]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("filters out cross-user clients when listing as a different user", () => {
|
|
101
|
+
registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
102
|
+
registerClient({ clientId: "client-B1", actorPrincipalId: "user-B" });
|
|
103
|
+
|
|
104
|
+
const handler = findHandler("list_clients");
|
|
105
|
+
const result = handler({
|
|
106
|
+
headers: { "x-vellum-actor-principal-id": "user-B" },
|
|
107
|
+
}) as ListClientsResponse;
|
|
108
|
+
|
|
109
|
+
const ids = result.clients.map((c) => c.clientId);
|
|
110
|
+
expect(ids).toEqual(["client-B1"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("filters out clients with no stored actorPrincipalId (fail-closed)", () => {
|
|
114
|
+
registerClient({
|
|
115
|
+
clientId: "client-noprincipal",
|
|
116
|
+
actorPrincipalId: undefined,
|
|
117
|
+
});
|
|
118
|
+
registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
119
|
+
|
|
120
|
+
const handler = findHandler("list_clients");
|
|
121
|
+
const result = handler({
|
|
122
|
+
headers: { "x-vellum-actor-principal-id": "user-A" },
|
|
123
|
+
}) as ListClientsResponse;
|
|
124
|
+
|
|
125
|
+
const ids = result.clients.map((c) => c.clientId);
|
|
126
|
+
expect(ids).toEqual(["client-A1"]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("filters out all clients when caller has no actorPrincipalId header (fail-closed)", () => {
|
|
130
|
+
registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
131
|
+
|
|
132
|
+
const handler = findHandler("list_clients");
|
|
133
|
+
const result = handler({}) as ListClientsResponse;
|
|
134
|
+
|
|
135
|
+
expect(result.clients).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("dev-bypass mode returns all clients regardless of actor", () => {
|
|
139
|
+
fakeHttpAuthDisabled = true;
|
|
140
|
+
registerClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
141
|
+
registerClient({ clientId: "client-B1", actorPrincipalId: "user-B" });
|
|
142
|
+
registerClient({
|
|
143
|
+
clientId: "client-noprincipal",
|
|
144
|
+
actorPrincipalId: undefined,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const handler = findHandler("list_clients");
|
|
148
|
+
const result = handler({
|
|
149
|
+
headers: { "x-vellum-actor-principal-id": "user-A" },
|
|
150
|
+
}) as ListClientsResponse;
|
|
151
|
+
|
|
152
|
+
const ids = result.clients.map((c) => c.clientId).sort();
|
|
153
|
+
expect(ids).toEqual(["client-A1", "client-B1", "client-noprincipal"]);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -10,7 +10,6 @@ mock.module("../../../util/logger.js", () => ({
|
|
|
10
10
|
import {
|
|
11
11
|
sampleConcepts as sharedSampleConcepts,
|
|
12
12
|
sampleConfig,
|
|
13
|
-
sampleSkills,
|
|
14
13
|
} from "../../../memory/__tests__/fixtures/memory-v2-activation-fixtures.js";
|
|
15
14
|
|
|
16
15
|
let rawConfigFixture: Record<string, unknown> = {};
|
|
@@ -36,7 +35,6 @@ import {
|
|
|
36
35
|
backfillMemoryV2ActivationMessageId,
|
|
37
36
|
type MemoryV2ConceptRowRecord,
|
|
38
37
|
type MemoryV2ConfigSnapshot,
|
|
39
|
-
type MemoryV2SkillRowRecord,
|
|
40
38
|
recordMemoryV2ActivationLog,
|
|
41
39
|
} from "../../../memory/memory-v2-activation-log-store.js";
|
|
42
40
|
import {
|
|
@@ -153,7 +151,6 @@ describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
|
|
|
153
151
|
turn: 4,
|
|
154
152
|
mode: "per-turn",
|
|
155
153
|
concepts: sampleConcepts,
|
|
156
|
-
skills: sampleSkills,
|
|
157
154
|
config: sampleConfig,
|
|
158
155
|
});
|
|
159
156
|
backfillMemoryV2ActivationMessageId(conversationId, messageId);
|
|
@@ -163,7 +160,6 @@ describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
|
|
|
163
160
|
turn: number;
|
|
164
161
|
mode: "context-load" | "per-turn";
|
|
165
162
|
concepts: MemoryV2ConceptRowRecord[];
|
|
166
|
-
skills: MemoryV2SkillRowRecord[];
|
|
167
163
|
config: MemoryV2ConfigSnapshot;
|
|
168
164
|
} | null;
|
|
169
165
|
memoryRecall: unknown;
|
|
@@ -173,7 +169,6 @@ describe("GET /v1/messages/:id/llm-context — memoryV2Activation", () => {
|
|
|
173
169
|
expect(body.memoryV2Activation!.turn).toBe(4);
|
|
174
170
|
expect(body.memoryV2Activation!.mode).toBe("per-turn");
|
|
175
171
|
expect(body.memoryV2Activation!.concepts).toEqual(sampleConcepts);
|
|
176
|
-
expect(body.memoryV2Activation!.skills).toEqual(sampleSkills);
|
|
177
172
|
expect(body.memoryV2Activation!.config).toEqual(sampleConfig);
|
|
178
173
|
// Backwards-compat: memoryRecall field still present.
|
|
179
174
|
expect(body).toHaveProperty("memoryRecall");
|
|
@@ -90,7 +90,7 @@ describe("setHeartbeatConfig handler", () => {
|
|
|
90
90
|
// invalidation + getConfig() read picked up the new on-disk state.
|
|
91
91
|
expect(result.success).toBe(true);
|
|
92
92
|
expect(result.enabled).toBe(true);
|
|
93
|
-
expect(result.intervalMs).toBe(
|
|
93
|
+
expect(result.intervalMs).toBe(30 * 60_000);
|
|
94
94
|
expect(result.activeHoursStart).toBe(8);
|
|
95
95
|
expect(result.activeHoursEnd).toBe(22);
|
|
96
96
|
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
|
|
10
10
|
import type { HostProxyCapability } from "../../channels/types.js";
|
|
11
|
+
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
11
12
|
import { datesToISO } from "../../util/json.js";
|
|
12
13
|
import { assistantEventHub } from "../assistant-event-hub.js";
|
|
13
14
|
import { NotFoundError } from "./errors.js";
|
|
@@ -33,7 +34,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
33
34
|
responseBody: z.object({
|
|
34
35
|
clients: z.array(z.object({}).passthrough()),
|
|
35
36
|
}),
|
|
36
|
-
handler: ({ queryParams }) => {
|
|
37
|
+
handler: ({ queryParams, headers }) => {
|
|
37
38
|
const capability = queryParams?.capability as
|
|
38
39
|
| HostProxyCapability
|
|
39
40
|
| undefined;
|
|
@@ -42,8 +43,25 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
42
43
|
? assistantEventHub.listClientsByCapability(capability)
|
|
43
44
|
: assistantEventHub.listClients();
|
|
44
45
|
|
|
46
|
+
// Defense-in-depth: filter the listing to clients owned by the calling
|
|
47
|
+
// actor so users cannot enumerate other users' connected client IDs.
|
|
48
|
+
// Clients with no stored `actorPrincipalId` (legacy SSE subscribers from
|
|
49
|
+
// before host-proxy-same-user, service-gateway tokens) are filtered out
|
|
50
|
+
// — fail-closed is the right default for this security boundary.
|
|
51
|
+
// Dev-bypass mode (DISABLE_HTTP_AUTH=true, mirroring
|
|
52
|
+
// require-bound-guardian.ts) preserves the previous "return all" behavior
|
|
53
|
+
// for platform-managed deployments where the platform handles auth.
|
|
54
|
+
const callerPrincipalId = headers?.["x-vellum-actor-principal-id"];
|
|
55
|
+
const filtered = isHttpAuthDisabled()
|
|
56
|
+
? clients
|
|
57
|
+
: clients.filter(
|
|
58
|
+
(c) =>
|
|
59
|
+
c.actorPrincipalId !== undefined &&
|
|
60
|
+
c.actorPrincipalId === callerPrincipalId,
|
|
61
|
+
);
|
|
62
|
+
|
|
45
63
|
return {
|
|
46
|
-
clients:
|
|
64
|
+
clients: filtered.map((c) =>
|
|
47
65
|
datesToISO({
|
|
48
66
|
clientId: c.clientId,
|
|
49
67
|
interfaceId: c.interfaceId,
|