@vellumai/assistant 0.11.10 → 0.11.11-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +1 -1
  2. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  3. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  6. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +2 -1
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/plan-credit.test.ts +51 -0
  12. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  13. package/node_modules/@vellumai/service-contracts/src/plan-credit.ts +66 -0
  14. package/openapi.yaml +152 -55
  15. package/package.json +1 -1
  16. package/src/__tests__/anthropic-provider.test.ts +60 -0
  17. package/src/__tests__/config-get-vision-flag.test.ts +36 -0
  18. package/src/__tests__/conversation-error.test.ts +38 -0
  19. package/src/__tests__/copy-composer-tc-templates.test.ts +2 -2
  20. package/src/__tests__/guardian-action-service-scope.test.ts +53 -0
  21. package/src/__tests__/headless-browser-mode.test.ts +63 -13
  22. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  23. package/src/__tests__/heartbeat-service.test.ts +118 -27
  24. package/src/__tests__/host-app-control-proxy.test.ts +2 -2
  25. package/src/__tests__/host-app-control-routes.test.ts +41 -0
  26. package/src/__tests__/host-bash-routes.test.ts +30 -1
  27. package/src/__tests__/host-browser-routes.test.ts +34 -0
  28. package/src/__tests__/host-cu-routes-targeted.test.ts +29 -0
  29. package/src/__tests__/host-file-routes-targeted.test.ts +29 -0
  30. package/src/__tests__/host-proxy-base.test.ts +1 -0
  31. package/src/__tests__/host-transfer-routes-targeted.test.ts +86 -0
  32. package/src/__tests__/llm-schema.test.ts +32 -0
  33. package/src/__tests__/mcp-config-secret-boundary.test.ts +2 -0
  34. package/src/__tests__/memory-config-file-parity.test.ts +18 -0
  35. package/src/__tests__/memory-retrieval-hook.test.ts +15 -0
  36. package/src/__tests__/messaging-send-tool.test.ts +159 -13
  37. package/src/__tests__/messaging-shared.test.ts +19 -0
  38. package/src/__tests__/notification-events-source-context-probe.test.ts +211 -0
  39. package/src/__tests__/openai-responses-prompt-cache.test.ts +40 -6
  40. package/src/__tests__/outlook-messaging-provider.test.ts +9 -0
  41. package/src/__tests__/provider-catalog-visibility.test.ts +2 -2
  42. package/src/__tests__/schedule-routes.test.ts +30 -1
  43. package/src/__tests__/scheduler-result-notification.test.ts +209 -0
  44. package/src/__tests__/skills-install-extract.test.ts +26 -0
  45. package/src/__tests__/skills-install-staging.test.ts +11 -2
  46. package/src/api/constants/personality-sliders.test.ts +21 -0
  47. package/src/api/constants/personality-sliders.ts +64 -0
  48. package/src/api/index.ts +8 -0
  49. package/src/archive/ustar-size.test.ts +32 -0
  50. package/src/archive/ustar-size.ts +23 -0
  51. package/src/background-wake/next-wake.test.ts +13 -0
  52. package/src/background-wake/next-wake.ts +4 -2
  53. package/src/calls/voice-session-bridge.ts +26 -0
  54. package/src/cli/__tests__/catalog-search-help.test.ts +1 -0
  55. package/src/cli/commands/__tests__/plugins-search.test.ts +81 -0
  56. package/src/cli/commands/config.help.ts +3 -1
  57. package/src/cli/commands/memory/worker.ts +1 -1
  58. package/src/cli/commands/platform/__tests__/credits-format.test.ts +94 -5
  59. package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
  60. package/src/cli/commands/platform/credits-format.ts +57 -0
  61. package/src/cli/commands/platform/index.help.ts +19 -3
  62. package/src/cli/commands/plugins.help.ts +4 -1
  63. package/src/cli/commands/plugins.ts +2 -1
  64. package/src/cli/lib/bundled-marketplace.json +1 -1
  65. package/src/cli/lib/install-from-platform.ts +8 -10
  66. package/src/config/bundled-skills/messaging/SKILL.md +2 -0
  67. package/src/config/bundled-skills/messaging/TOOLS.json +4 -4
  68. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +85 -1
  69. package/src/config/bundled-skills/messaging/tools/shared.ts +6 -0
  70. package/src/config/bundled-skills/schedule/SKILL.md +9 -1
  71. package/src/config/bundled-skills/screen-annotation/SKILL.md +87 -20
  72. package/src/config/bundled-skills/screen-annotation/TOOLS.json +21 -5
  73. package/src/config/bundled-skills/screen-annotation/tools/screen-annotation.test.ts +67 -0
  74. package/src/config/feature-flag-registry.json +17 -1
  75. package/src/config/input-modalities.test.ts +71 -0
  76. package/src/config/input-modalities.ts +95 -0
  77. package/src/config/schemas/heartbeat.ts +3 -3
  78. package/src/config/schemas/llm.ts +13 -3
  79. package/src/config/schemas/memory-storage.test.ts +39 -0
  80. package/src/config/schemas/memory-storage.ts +21 -0
  81. package/src/context/token-estimator.ts +2 -2
  82. package/src/daemon/__tests__/embedding-reconcile.test.ts +77 -0
  83. package/src/daemon/__tests__/lifecycle-embedding-reconcile.test.ts +3 -0
  84. package/src/daemon/conversation-error.ts +20 -7
  85. package/src/daemon/embedding-reconcile.ts +64 -1
  86. package/src/daemon/handlers/config-embeddings.ts +31 -0
  87. package/src/daemon/host-app-control-proxy.ts +2 -0
  88. package/src/daemon/host-bash-proxy.ts +1 -0
  89. package/src/daemon/host-cu-proxy.ts +6 -6
  90. package/src/daemon/host-file-proxy.ts +6 -6
  91. package/src/daemon/host-proxy-base.ts +10 -9
  92. package/src/daemon/host-transfer-proxy.ts +21 -24
  93. package/src/daemon/mcp-reload-service.ts +6 -0
  94. package/src/daemon/providers-setup.ts +5 -26
  95. package/src/heartbeat/heartbeat-service.ts +72 -13
  96. package/src/live-voice/__tests__/live-voice-photo.test.ts +72 -0
  97. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +81 -0
  98. package/src/live-voice/live-voice-photo.ts +68 -4
  99. package/src/live-voice/live-voice-session.ts +36 -2
  100. package/src/live-voice/protocol.ts +98 -1
  101. package/src/mcp/__tests__/reload-signal-emission.test.ts +54 -0
  102. package/src/mcp/__tests__/reload-signal.test.ts +57 -0
  103. package/src/mcp/__tests__/startup.test.ts +150 -0
  104. package/src/mcp/reload-signal.ts +50 -0
  105. package/src/mcp/startup.ts +101 -0
  106. package/src/messaging/providers/outlook/adapter.ts +2 -13
  107. package/src/messaging/providers/outlook/client.ts +13 -1
  108. package/src/messaging/providers/outlook/types.ts +3 -0
  109. package/src/notifications/__tests__/decision-engine.test.ts +79 -6
  110. package/src/notifications/__tests__/home-feed-side-effect.test.ts +27 -0
  111. package/src/notifications/__tests__/schedule-result-producer.test.ts +397 -0
  112. package/src/notifications/copy-composer.ts +33 -11
  113. package/src/notifications/decision-engine.ts +48 -11
  114. package/src/notifications/events-store.ts +68 -2
  115. package/src/notifications/home-feed-side-effect.ts +15 -3
  116. package/src/notifications/schedule-result-producer.ts +275 -0
  117. package/src/notifications/signal.ts +5 -0
  118. package/src/oauth/seed-providers.ts +3 -2
  119. package/src/persistence/conversation-crud.ts +38 -0
  120. package/src/persistence/embeddings/embedding-backend.test.ts +162 -0
  121. package/src/persistence/embeddings/embedding-backend.ts +148 -11
  122. package/src/persistence/embeddings/embedding-openai.test.ts +113 -0
  123. package/src/persistence/embeddings/embedding-openai.ts +55 -4
  124. package/src/persistence/embeddings/embedding-types.test.ts +24 -0
  125. package/src/persistence/embeddings/embedding-types.ts +25 -1
  126. package/src/persistence/job-utils.ts +6 -2
  127. package/src/plugin-api/index.ts +2 -1
  128. package/src/plugin-api/vision-support.test.ts +81 -0
  129. package/src/plugin-api/vision-support.ts +57 -10
  130. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +8 -0
  131. package/src/plugins/defaults/memory/jobs/__tests__/embed-concept-page.test.ts +1 -0
  132. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +11 -3
  133. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +20 -0
  134. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +3 -1
  135. package/src/plugins/defaults/memory/substrate/__tests__/reembed-job.test.ts +1 -0
  136. package/src/plugins/defaults/memory/v2/__tests__/backfill-jobs.test.ts +1 -0
  137. package/src/plugins/defaults/memory/v3/__tests__/section-dense-store.test.ts +82 -0
  138. package/src/plugins/defaults/memory/v3/section-dense-store.ts +9 -27
  139. package/src/providers/__tests__/provider-secret-catalog.test.ts +4 -0
  140. package/src/providers/__tests__/xml-tool-call-salvage.test.ts +172 -0
  141. package/src/providers/anthropic/client.ts +9 -4
  142. package/src/providers/inline-audio-support.test.ts +34 -0
  143. package/src/providers/inline-audio-support.ts +27 -0
  144. package/src/providers/model-catalog.ts +22 -2
  145. package/src/providers/openai/__tests__/api-error-normalization.test.ts +50 -0
  146. package/src/providers/openai/__tests__/chat-completions-extra-body.test.ts +51 -0
  147. package/src/providers/openai/__tests__/chat-completions-provider-xml-salvage.test.ts +291 -0
  148. package/src/providers/openai/api-error-normalization.ts +18 -3
  149. package/src/providers/openai/chat-completions-provider.ts +113 -13
  150. package/src/providers/openai/responses-provider.ts +10 -8
  151. package/src/providers/provider-secret-catalog.ts +11 -1
  152. package/src/providers/vellum/client.ts +14 -1
  153. package/src/providers/vellum/personality-directions.test.ts +163 -0
  154. package/src/providers/vellum/personality-directions.ts +120 -0
  155. package/src/providers/xml-tool-call-salvage.ts +212 -0
  156. package/src/runtime/__tests__/background-job-runner.test.ts +42 -4
  157. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -6
  158. package/src/runtime/auth/__tests__/route-policy.test.ts +65 -0
  159. package/src/runtime/auth/same-actor.ts +29 -5
  160. package/src/runtime/background-job-runner.ts +38 -29
  161. package/src/runtime/guardian-action-service.ts +20 -1
  162. package/src/runtime/migrations/vbundle-validator.ts +3 -3
  163. package/src/runtime/pending-interactions.ts +4 -5
  164. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +24 -0
  165. package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +99 -5
  166. package/src/runtime/routes/conversation-query-routes.ts +53 -11
  167. package/src/runtime/routes/document-comments-routes.ts +8 -2
  168. package/src/runtime/routes/guardian-action-routes.ts +7 -0
  169. package/src/runtime/routes/heartbeat-routes.ts +41 -53
  170. package/src/runtime/routes/host-app-control-routes.ts +10 -36
  171. package/src/runtime/routes/host-bash-routes.ts +11 -45
  172. package/src/runtime/routes/host-browser-routes.ts +24 -0
  173. package/src/runtime/routes/host-cu-routes.ts +11 -45
  174. package/src/runtime/routes/host-file-routes.ts +11 -44
  175. package/src/runtime/routes/host-proxy-result-binding.ts +57 -0
  176. package/src/runtime/routes/host-transfer-routes.ts +33 -25
  177. package/src/runtime/routes/index.ts +0 -2
  178. package/src/runtime/routes/integrations/a2a.ts +12 -3
  179. package/src/runtime/routes/integrations/vercel.ts +13 -3
  180. package/src/runtime/routes/platform-routes.ts +121 -24
  181. package/src/runtime/routes/schedule-routes.ts +15 -1
  182. package/src/schedule/__tests__/schedule-timezone.test.ts +21 -3
  183. package/src/schedule/__tests__/worker-mcp-tools.test.ts +88 -0
  184. package/src/schedule/schedule-timezone.ts +25 -0
  185. package/src/schedule/scheduler.ts +19 -0
  186. package/src/schedule/worker.ts +119 -2
  187. package/src/skills/catalog-install.ts +40 -5
  188. package/src/tools/browser/__tests__/browser-status.test.ts +3 -0
  189. package/src/tools/browser/browser-execution.ts +117 -53
  190. package/src/tools/browser/browser-status-constants.ts +6 -6
  191. package/src/tools/capability-offer.test.ts +87 -0
  192. package/src/tools/capability-offer.ts +93 -0
  193. package/src/tools/terminal/__tests__/safe-env.test.ts +17 -0
  194. package/src/tools/terminal/safe-env.ts +3 -2
  195. package/src/util/provider-error-patterns.ts +34 -0
  196. package/src/watcher/__tests__/engine.test.ts +5 -2
  197. package/src/watcher/engine.ts +7 -4
  198. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +0 -487
  199. package/src/runtime/routes/sight-frame-routes.ts +0 -136
@@ -20,6 +20,7 @@ import { listHeartbeatRuns } from "../../heartbeat/heartbeat-run-store.js";
20
20
  import { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
21
21
  import { getConversation } from "../../persistence/conversation-crud.js";
22
22
  import { getUsageCostForConversationWindow } from "../../persistence/llm-usage-store.js";
23
+ import { resolveScheduleTimezone } from "../../schedule/schedule-timezone.js";
23
24
  import { readTextFileSync } from "../../util/fs.js";
24
25
  import { getLogger } from "../../util/logger.js";
25
26
  import { getWorkspacePromptPath } from "../../util/platform.js";
@@ -36,6 +37,41 @@ import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
36
37
 
37
38
  const log = getLogger("heartbeat-routes");
38
39
 
40
+ const HeartbeatConfigResponseSchema = z.object({
41
+ enabled: z.boolean(),
42
+ intervalMs: z.number(),
43
+ activeHoursStart: z.number().nullable(),
44
+ activeHoursEnd: z.number().nullable(),
45
+ cronExpression: z.string().nullable(),
46
+ timezone: z.string().nullable(),
47
+ effectiveTimezone: z
48
+ .string()
49
+ .nullable()
50
+ .describe(
51
+ "Timezone used for cron and active-hours evaluation after resolving heartbeat.timezone, then the user's configured or detected zone",
52
+ ),
53
+ nextRunAt: z.number().nullable(),
54
+ lastRunAt: z.number().nullable(),
55
+ success: z.boolean(),
56
+ });
57
+
58
+ function heartbeatConfigResponse() {
59
+ const heartbeat = getConfig().heartbeat;
60
+ const svc = HeartbeatService.getInstance();
61
+ return {
62
+ enabled: heartbeat.enabled,
63
+ intervalMs: heartbeat.intervalMs,
64
+ activeHoursStart: heartbeat.activeHoursStart ?? null,
65
+ activeHoursEnd: heartbeat.activeHoursEnd ?? null,
66
+ cronExpression: heartbeat.cronExpression ?? null,
67
+ timezone: heartbeat.timezone ?? null,
68
+ effectiveTimezone: resolveScheduleTimezone(heartbeat.timezone),
69
+ nextRunAt: svc?.nextRunAt ?? null,
70
+ lastRunAt: svc?.lastRunAt ?? null,
71
+ success: true,
72
+ };
73
+ }
74
+
39
75
  // ---------------------------------------------------------------------------
40
76
  // Handlers (transport-agnostic)
41
77
  // ---------------------------------------------------------------------------
@@ -196,32 +232,8 @@ export const ROUTES: RouteDefinition[] = [
196
232
  summary: "Get heartbeat config",
197
233
  description: "Return the current heartbeat schedule configuration.",
198
234
  tags: ["heartbeat"],
199
- responseBody: z.object({
200
- enabled: z.boolean(),
201
- intervalMs: z.number(),
202
- activeHoursStart: z.number().nullable(),
203
- activeHoursEnd: z.number().nullable(),
204
- cronExpression: z.string().nullable(),
205
- timezone: z.string().nullable(),
206
- nextRunAt: z.number().nullable(),
207
- lastRunAt: z.number().nullable(),
208
- success: z.boolean(),
209
- }),
210
- handler: async (_args: RouteHandlerArgs) => {
211
- const config = getConfig().heartbeat;
212
- const svc = HeartbeatService.getInstance();
213
- return {
214
- enabled: config.enabled,
215
- intervalMs: config.intervalMs,
216
- activeHoursStart: config.activeHoursStart ?? null,
217
- activeHoursEnd: config.activeHoursEnd ?? null,
218
- cronExpression: config.cronExpression ?? null,
219
- timezone: config.timezone ?? null,
220
- nextRunAt: svc?.nextRunAt ?? null,
221
- lastRunAt: svc?.lastRunAt ?? null,
222
- success: true,
223
- };
224
- },
235
+ responseBody: HeartbeatConfigResponseSchema,
236
+ handler: async (_args: RouteHandlerArgs) => heartbeatConfigResponse(),
225
237
  },
226
238
  {
227
239
  operationId: "updateHeartbeatConfig",
@@ -258,19 +270,9 @@ export const ROUTES: RouteDefinition[] = [
258
270
  .string()
259
271
  .nullable()
260
272
  .optional()
261
- .describe("Timezone for cron evaluation"),
262
- }),
263
- responseBody: z.object({
264
- enabled: z.boolean(),
265
- intervalMs: z.number(),
266
- activeHoursStart: z.number().nullable(),
267
- activeHoursEnd: z.number().nullable(),
268
- cronExpression: z.string().nullable(),
269
- timezone: z.string().nullable(),
270
- nextRunAt: z.number().nullable(),
271
- lastRunAt: z.number().nullable(),
272
- success: z.boolean(),
273
+ .describe("Timezone for cron and active-hours evaluation"),
273
274
  }),
275
+ responseBody: HeartbeatConfigResponseSchema,
274
276
  handler: async ({ body = {} }: RouteHandlerArgs) => {
275
277
  // Build a patch containing only the fields the caller actually set.
276
278
  // Writing back the full Zod-defaulted heartbeat object would bake
@@ -316,24 +318,10 @@ export const ROUTES: RouteDefinition[] = [
316
318
  throw new InternalError("Failed to save config");
317
319
  }
318
320
 
319
- // Read effective values back through the schema-defaulting loader so
320
- // callers that only set a subset of fields still see the resolved
321
- // (post-default) shape in the response.
322
- const heartbeat = getConfig().heartbeat;
323
321
  const svc = HeartbeatService.getInstance();
324
322
  svc?.reconfigure();
325
323
 
326
- return {
327
- enabled: heartbeat.enabled,
328
- intervalMs: heartbeat.intervalMs,
329
- activeHoursStart: heartbeat.activeHoursStart ?? null,
330
- activeHoursEnd: heartbeat.activeHoursEnd ?? null,
331
- cronExpression: heartbeat.cronExpression ?? null,
332
- timezone: heartbeat.timezone ?? null,
333
- nextRunAt: svc?.nextRunAt ?? null,
334
- lastRunAt: svc?.lastRunAt ?? null,
335
- success: true,
336
- };
324
+ return heartbeatConfigResponse();
337
325
  },
338
326
  },
339
327
  {
@@ -19,15 +19,10 @@ import type {
19
19
  HostAppControlResultPayload,
20
20
  HostAppControlState,
21
21
  } from "../../daemon/message-types/host-app-control.js";
22
- import { assistantEventHub } from "../assistant-event-hub.js";
23
22
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
24
- import {
25
- enforceSameActorOrThrow,
26
- SAME_ACTOR_FORBIDDEN_DESCRIPTION,
27
- } from "../auth/same-actor.js";
28
- import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
23
+ import { SAME_ACTOR_FORBIDDEN_DESCRIPTION } from "../auth/same-actor.js";
29
24
  import * as pendingInteractions from "../pending-interactions.js";
30
- import { BadRequestError, ForbiddenError } from "./errors.js";
25
+ import { assertHostProxyResultBinding } from "./host-proxy-result-binding.js";
31
26
  import { parseBody } from "./parse-body.js";
32
27
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
33
28
 
@@ -93,35 +88,14 @@ async function handleHostAppControlResult({ body, headers }: RouteHandlerArgs) {
93
88
  return { accepted: true };
94
89
  }
95
90
 
96
- // Same-actor binding: when the pending interaction has a targetClientId,
97
- // validate the submitting client matches and the actor principals align.
98
- // Mirrors host-browser / host-cu / host-bash result routes.
99
- if (peeked.targetClientId != null) {
100
- const headerMap = headers ?? {};
101
- const submittingClientId =
102
- headerMap["x-vellum-client-id"]?.trim() || undefined;
103
- if (!submittingClientId) {
104
- throw new BadRequestError(
105
- "x-vellum-client-id header is missing for a targeted host app-control request.",
106
- );
107
- }
108
- if (submittingClientId !== peeked.targetClientId) {
109
- throw new ForbiddenError(
110
- `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
111
- );
112
- }
113
- const submittingActorPrincipalId =
114
- await resolveActorPrincipalIdForLocalGuardian(
115
- headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
116
- );
117
- enforceSameActorOrThrow({
118
- sourceActorPrincipalId: submittingActorPrincipalId,
119
- targetActorPrincipalId: peeked.targetActorPrincipalId,
120
- targetClientId: peeked.targetClientId,
121
- op: "host_app_control",
122
- hubForMissingTarget: assistantEventHub,
123
- });
124
- }
91
+ await assertHostProxyResultBinding({
92
+ headers,
93
+ targetClientId: peeked.targetClientId,
94
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
95
+ op: "host_app_control",
96
+ missingClientIdMessage:
97
+ "x-vellum-client-id header is missing for a targeted host app-control request.",
98
+ });
125
99
 
126
100
  const interaction = pendingInteractions.resolve(requestId, "answered")!;
127
101
  const conversation = findConversation(interaction.conversationId);
@@ -7,20 +7,11 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
10
- import { assistantEventHub } from "../assistant-event-hub.js";
11
10
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
12
- import {
13
- enforceSameActorOrThrow,
14
- SAME_ACTOR_FORBIDDEN_DESCRIPTION,
15
- } from "../auth/same-actor.js";
16
- import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
11
+ import { SAME_ACTOR_FORBIDDEN_DESCRIPTION } from "../auth/same-actor.js";
17
12
  import * as pendingInteractions from "../pending-interactions.js";
18
- import {
19
- BadRequestError,
20
- ConflictError,
21
- ForbiddenError,
22
- NotFoundError,
23
- } from "./errors.js";
13
+ import { ConflictError, NotFoundError } from "./errors.js";
14
+ import { assertHostProxyResultBinding } from "./host-proxy-result-binding.js";
24
15
  import { parseBody } from "./parse-body.js";
25
16
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
26
17
 
@@ -59,13 +50,6 @@ async function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
59
50
  body,
60
51
  );
61
52
 
62
- const submittingClientId =
63
- headers?.["x-vellum-client-id"]?.trim() || undefined;
64
- const submittingActorPrincipalId =
65
- await resolveActorPrincipalIdForLocalGuardian(
66
- headers?.["x-vellum-actor-principal-id"]?.trim() || undefined,
67
- );
68
-
69
53
  const peeked = pendingInteractions.get(requestId);
70
54
  if (!peeked) {
71
55
  throw new NotFoundError("No pending interaction found for this requestId");
@@ -77,32 +61,14 @@ async function handleHostBashResult({ body, headers }: RouteHandlerArgs) {
77
61
  );
78
62
  }
79
63
 
80
- const { targetClientId } = peeked;
81
- if (targetClientId) {
82
- if (!submittingClientId) {
83
- throw new BadRequestError(
84
- "x-vellum-client-id header is required for targeted host bash requests",
85
- );
86
- }
87
- if (submittingClientId !== targetClientId) {
88
- throw new ForbiddenError(
89
- `Client "${submittingClientId}" is not the target for this request (expected "${targetClientId}"). The targeted client must submit the result.`,
90
- );
91
- }
92
-
93
- // Defense-in-depth on top of the client-id header binding above: the
94
- // submitting actor's principal must match the actor principal stored
95
- // for the target client at SSE subscription time. This prevents a
96
- // cross-user submission even when the attacker can guess or spoof the
97
- // target's client ID.
98
- enforceSameActorOrThrow({
99
- sourceActorPrincipalId: submittingActorPrincipalId,
100
- targetActorPrincipalId: peeked.targetActorPrincipalId,
101
- targetClientId,
102
- op: "host_bash",
103
- hubForMissingTarget: assistantEventHub,
104
- });
105
- }
64
+ await assertHostProxyResultBinding({
65
+ headers,
66
+ targetClientId: peeked.targetClientId,
67
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
68
+ op: "host_bash",
69
+ missingClientIdMessage:
70
+ "x-vellum-client-id header is required for targeted host bash requests",
71
+ });
106
72
 
107
73
  HostBashProxy.instance.resolveResult(requestId, {
108
74
  stdout: stdout ?? "",
@@ -149,6 +149,30 @@ export async function resolveHostBrowserResultByRequestId(
149
149
  message: err instanceof Error ? err.message : "Same-actor check failed",
150
150
  };
151
151
  }
152
+ } else if (peeked.targetActorPrincipalId) {
153
+ const headerMap = headers ?? {};
154
+ const submittingClientId =
155
+ headerMap["x-vellum-client-id"]?.trim() || undefined;
156
+ const submittingActorPrincipalId =
157
+ await resolveActorPrincipalIdForLocalGuardian(
158
+ headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
159
+ );
160
+ try {
161
+ enforceSameActorOrThrow({
162
+ sourceActorPrincipalId: submittingActorPrincipalId,
163
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
164
+ targetClientId: submittingClientId ?? "",
165
+ op: "host_browser",
166
+ hubForMissingTarget: assistantEventHub,
167
+ });
168
+ } catch (err) {
169
+ return {
170
+ ok: false,
171
+ code: "FORBIDDEN",
172
+ status: 403,
173
+ message: err instanceof Error ? err.message : "Same-actor check failed",
174
+ };
175
+ }
152
176
  }
153
177
 
154
178
  const normalizedContent = typeof content === "string" ? content : "";
@@ -7,20 +7,11 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { findConversation } from "../../daemon/conversation-registry.js";
10
- import { assistantEventHub } from "../assistant-event-hub.js";
11
10
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
12
- import {
13
- enforceSameActorOrThrow,
14
- SAME_ACTOR_FORBIDDEN_DESCRIPTION,
15
- } from "../auth/same-actor.js";
16
- import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
11
+ import { SAME_ACTOR_FORBIDDEN_DESCRIPTION } from "../auth/same-actor.js";
17
12
  import * as pendingInteractions from "../pending-interactions.js";
18
- import {
19
- BadRequestError,
20
- ConflictError,
21
- ForbiddenError,
22
- NotFoundError,
23
- } from "./errors.js";
13
+ import { ConflictError, NotFoundError } from "./errors.js";
14
+ import { assertHostProxyResultBinding } from "./host-proxy-result-binding.js";
24
15
  import { parseBody } from "./parse-body.js";
25
16
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
26
17
 
@@ -78,39 +69,14 @@ async function handleHostCuResult({ body, headers }: RouteHandlerArgs) {
78
69
  );
79
70
  }
80
71
 
81
- // Validate submitting client matches the targeted client (if any).
82
- if (peeked.targetClientId != null) {
83
- const headerMap = (headers as Record<string, string | undefined>) ?? {};
84
- const submittingClientId =
85
- headerMap["x-vellum-client-id"]?.trim() || undefined;
86
- if (!submittingClientId) {
87
- throw new BadRequestError(
88
- "x-vellum-client-id header is missing for a targeted host CU request.",
89
- );
90
- }
91
- if (submittingClientId !== peeked.targetClientId) {
92
- throw new ForbiddenError(
93
- `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
94
- );
95
- }
96
-
97
- // Defense-in-depth: require the submitting actor's principal id to match
98
- // the actor principal id captured when the target client opened its SSE
99
- // stream. This prevents a different authenticated user with knowledge of
100
- // both the requestId and target clientId from submitting a result on
101
- // behalf of the targeted client.
102
- const submittingActorPrincipalId =
103
- await resolveActorPrincipalIdForLocalGuardian(
104
- headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
105
- );
106
- enforceSameActorOrThrow({
107
- sourceActorPrincipalId: submittingActorPrincipalId,
108
- targetActorPrincipalId: peeked.targetActorPrincipalId,
109
- targetClientId: peeked.targetClientId,
110
- op: "host_cu",
111
- hubForMissingTarget: assistantEventHub,
112
- });
113
- }
72
+ await assertHostProxyResultBinding({
73
+ headers: headers as Record<string, string | undefined> | undefined,
74
+ targetClientId: peeked.targetClientId,
75
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
76
+ op: "host_cu",
77
+ missingClientIdMessage:
78
+ "x-vellum-client-id header is missing for a targeted host CU request.",
79
+ });
114
80
 
115
81
  // Conversation-agnostic observation (see `runtime/host-observe.ts`): the
116
82
  // request was raised outside any turn, so there is no conversation and no CU
@@ -7,20 +7,11 @@
7
7
  import { z } from "zod";
8
8
 
9
9
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
10
- import { assistantEventHub } from "../assistant-event-hub.js";
11
10
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
12
- import {
13
- enforceSameActorOrThrow,
14
- SAME_ACTOR_FORBIDDEN_DESCRIPTION,
15
- } from "../auth/same-actor.js";
16
- import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
11
+ import { SAME_ACTOR_FORBIDDEN_DESCRIPTION } from "../auth/same-actor.js";
17
12
  import * as pendingInteractions from "../pending-interactions.js";
18
- import {
19
- BadRequestError,
20
- ConflictError,
21
- ForbiddenError,
22
- NotFoundError,
23
- } from "./errors.js";
13
+ import { ConflictError, NotFoundError } from "./errors.js";
14
+ import { assertHostProxyResultBinding } from "./host-proxy-result-binding.js";
24
15
  import { parseBody } from "./parse-body.js";
25
16
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
26
17
 
@@ -69,38 +60,14 @@ async function handleHostFileResult({ body, headers }: RouteHandlerArgs) {
69
60
  );
70
61
  }
71
62
 
72
- // Validate submitting client matches the targeted client (if any).
73
- if (peeked.targetClientId != null) {
74
- const headerMap = (headers as Record<string, string | undefined>) ?? {};
75
- const submittingClientId =
76
- headerMap["x-vellum-client-id"]?.trim() || undefined;
77
- if (!submittingClientId) {
78
- throw new BadRequestError(
79
- "x-vellum-client-id header is missing for a targeted host file request.",
80
- );
81
- }
82
- if (submittingClientId !== peeked.targetClientId) {
83
- throw new ForbiddenError(
84
- `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}"). The targeted client must submit the result.`,
85
- );
86
- }
87
-
88
- // Defense-in-depth: also require the submitting actor's principal id to
89
- // match the actor that opened the target client's SSE stream. This blocks
90
- // cross-user submissions even if a different user somehow obtains the
91
- // target client id.
92
- const submittingActorPrincipalId =
93
- await resolveActorPrincipalIdForLocalGuardian(
94
- headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
95
- );
96
- enforceSameActorOrThrow({
97
- sourceActorPrincipalId: submittingActorPrincipalId,
98
- targetActorPrincipalId: peeked.targetActorPrincipalId,
99
- targetClientId: peeked.targetClientId,
100
- op: "host_file",
101
- hubForMissingTarget: assistantEventHub,
102
- });
103
- }
63
+ await assertHostProxyResultBinding({
64
+ headers: headers as Record<string, string | undefined> | undefined,
65
+ targetClientId: peeked.targetClientId,
66
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
67
+ op: "host_file",
68
+ missingClientIdMessage:
69
+ "x-vellum-client-id header is missing for a targeted host file request.",
70
+ });
104
71
 
105
72
  HostFileProxy.instance.resolve(requestId, {
106
73
  content: content ?? "",
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared binding checks for host-proxy result (and transfer content) routes.
3
+ *
4
+ * Targeted requests still require the submitting client id to match the
5
+ * pending `targetClientId`. When a request was dispatched untargeted, the
6
+ * pending interaction may still carry `targetActorPrincipalId` (the turn's
7
+ * source actor). In that case the same-actor check still runs so a different
8
+ * principal cannot forge stdout/stderr/exitCode for someone else's command.
9
+ *
10
+ * Identity-less local flows (neither target client nor source actor recorded)
11
+ * keep the open path.
12
+ */
13
+ import { assistantEventHub } from "../assistant-event-hub.js";
14
+ import {
15
+ enforceSameActorOrThrow,
16
+ type SameActorOp,
17
+ } from "../auth/same-actor.js";
18
+ import { resolveActorPrincipalIdForLocalGuardian } from "../local-actor-identity.js";
19
+ import { BadRequestError, ForbiddenError } from "./errors.js";
20
+
21
+ export async function assertHostProxyResultBinding(args: {
22
+ headers?: Record<string, string | undefined>;
23
+ targetClientId?: string;
24
+ targetActorPrincipalId?: string;
25
+ op: SameActorOp;
26
+ missingClientIdMessage: string;
27
+ }): Promise<void> {
28
+ const submittingClientId =
29
+ args.headers?.["x-vellum-client-id"]?.trim() || undefined;
30
+ const submittingActorPrincipalId =
31
+ await resolveActorPrincipalIdForLocalGuardian(
32
+ args.headers?.["x-vellum-actor-principal-id"]?.trim() || undefined,
33
+ );
34
+
35
+ if (args.targetClientId) {
36
+ if (!submittingClientId) {
37
+ throw new BadRequestError(args.missingClientIdMessage);
38
+ }
39
+ if (submittingClientId !== args.targetClientId) {
40
+ throw new ForbiddenError(
41
+ `Client "${submittingClientId}" is not the target for this request (expected "${args.targetClientId}"). The targeted client must submit the result.`,
42
+ );
43
+ }
44
+ }
45
+
46
+ if (!args.targetClientId && !args.targetActorPrincipalId) {
47
+ return;
48
+ }
49
+
50
+ enforceSameActorOrThrow({
51
+ sourceActorPrincipalId: submittingActorPrincipalId,
52
+ targetActorPrincipalId: args.targetActorPrincipalId,
53
+ targetClientId: args.targetClientId ?? submittingClientId ?? "",
54
+ op: args.op,
55
+ hubForMissingTarget: assistantEventHub,
56
+ });
57
+ }
@@ -22,6 +22,7 @@ import {
22
22
  ForbiddenError,
23
23
  NotFoundError,
24
24
  } from "./errors.js";
25
+ import { assertHostProxyResultBinding } from "./host-proxy-result-binding.js";
25
26
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
26
27
 
27
28
  /**
@@ -86,6 +87,18 @@ async function handleTransferContentGet({
86
87
  op: "host_transfer",
87
88
  hubForMissingTarget: assistantEventHub,
88
89
  });
90
+ } else {
91
+ const targetActorPrincipalId =
92
+ match.proxy.getTargetActorPrincipalIdForTransfer(transferId);
93
+ if (targetActorPrincipalId) {
94
+ await assertHostProxyResultBinding({
95
+ headers: headers as Record<string, string | undefined>,
96
+ targetActorPrincipalId,
97
+ op: "host_transfer",
98
+ missingClientIdMessage:
99
+ "x-vellum-client-id header required for targeted transfer",
100
+ });
101
+ }
89
102
  }
90
103
 
91
104
  const content = match.proxy.getTransferContent(transferId);
@@ -172,6 +185,18 @@ async function handleTransferContentPut({
172
185
  op: "host_transfer",
173
186
  hubForMissingTarget: assistantEventHub,
174
187
  });
188
+ } else {
189
+ const targetActorPrincipalId =
190
+ match.proxy.getTargetActorPrincipalIdForTransfer(transferId);
191
+ if (targetActorPrincipalId) {
192
+ await assertHostProxyResultBinding({
193
+ headers: headers as Record<string, string | undefined>,
194
+ targetActorPrincipalId,
195
+ op: "host_transfer",
196
+ missingClientIdMessage:
197
+ "x-vellum-client-id header required for targeted transfer",
198
+ });
199
+ }
175
200
  }
176
201
 
177
202
  const data = rawBody ? Buffer.from(rawBody) : Buffer.alloc(0);
@@ -221,31 +246,14 @@ async function handleTransferResult({ body, headers }: RouteHandlerArgs) {
221
246
  );
222
247
  }
223
248
 
224
- if (peeked.targetClientId != null) {
225
- const headerMap = (headers as Record<string, string | undefined>) ?? {};
226
- const rawClientId = headerMap["x-vellum-client-id"];
227
- const submittingClientId = rawClientId?.trim() || undefined;
228
- if (!submittingClientId) {
229
- throw new BadRequestError(
230
- "x-vellum-client-id header is missing for a targeted host transfer request.",
231
- );
232
- }
233
- if (submittingClientId !== peeked.targetClientId) {
234
- throw new ForbiddenError(
235
- `Client "${submittingClientId}" is not the target for this request (expected "${peeked.targetClientId}").`,
236
- );
237
- }
238
-
239
- enforceSameActorOrThrow({
240
- sourceActorPrincipalId: await resolveActorPrincipalIdForLocalGuardian(
241
- headerMap["x-vellum-actor-principal-id"]?.trim() || undefined,
242
- ),
243
- targetActorPrincipalId: peeked.targetActorPrincipalId,
244
- targetClientId: peeked.targetClientId,
245
- op: "host_transfer",
246
- hubForMissingTarget: assistantEventHub,
247
- });
248
- }
249
+ await assertHostProxyResultBinding({
250
+ headers: headers as Record<string, string | undefined> | undefined,
251
+ targetClientId: peeked.targetClientId,
252
+ targetActorPrincipalId: peeked.targetActorPrincipalId,
253
+ op: "host_transfer",
254
+ missingClientIdMessage:
255
+ "x-vellum-client-id header is missing for a targeted host transfer request.",
256
+ });
249
257
 
250
258
  HostTransferProxy.instance.resolveTransferResult(requestId, {
251
259
  isError: isError ?? false,
@@ -140,7 +140,6 @@ import { ROUTES as SCHEDULE_WORKER_ROUTES } from "./schedule-worker-routes.js";
140
140
  import { ROUTES as SECRET_ROUTES } from "./secret-routes.js";
141
141
  import { ROUTES as SEQUENCE_ROUTES } from "./sequence-routes.js";
142
142
  import { ROUTES as SETTINGS_ROUTES } from "./settings-routes.js";
143
- import { ROUTES as SIGHT_FRAME_ROUTES } from "./sight-frame-routes.js";
144
143
  import { ROUTES as SKILL_ROUTES } from "./skills-routes.js";
145
144
  import { ROUTES as SLACK_CHANNEL_RESOLVE_ROUTES } from "./slack-channel-routes.js";
146
145
  import { ROUTES as SOUNDS_CONFIG_ROUTES } from "./sounds-config-routes.js";
@@ -290,7 +289,6 @@ export const ROUTES: RouteDefinition[] = [
290
289
  ...SANITY_ROUTES,
291
290
  ...SECRET_ROUTES,
292
291
  ...SETTINGS_ROUTES,
293
- ...SIGHT_FRAME_ROUTES,
294
292
  ...SOUNDS_CONFIG_ROUTES,
295
293
  ...SKILL_ROUTES,
296
294
  ...A2A_ROUTES,
@@ -230,7 +230,10 @@ export const ROUTES: RouteDefinition[] = [
230
230
  operationId: "integrations_a2a_config_get",
231
231
  endpoint: "integrations/a2a/config",
232
232
  method: "GET",
233
- policy: null,
233
+ policy: {
234
+ requiredScopes: ["settings.read"],
235
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
236
+ },
234
237
  summary: "Get A2A config",
235
238
  description: "Check current A2A channel configuration status.",
236
239
  tags: ["integrations"],
@@ -241,7 +244,10 @@ export const ROUTES: RouteDefinition[] = [
241
244
  operationId: "integrations_a2a_config_post",
242
245
  endpoint: "integrations/a2a/config",
243
246
  method: "POST",
244
- policy: null,
247
+ policy: {
248
+ requiredScopes: ["settings.write"],
249
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
250
+ },
245
251
  summary: "Enable A2A channel",
246
252
  description: "Enable the A2A channel for inter-assistant communication.",
247
253
  tags: ["integrations"],
@@ -252,7 +258,10 @@ export const ROUTES: RouteDefinition[] = [
252
258
  operationId: "integrations_a2a_config_delete",
253
259
  endpoint: "integrations/a2a/config",
254
260
  method: "DELETE",
255
- policy: null,
261
+ policy: {
262
+ requiredScopes: ["settings.write"],
263
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
264
+ },
256
265
  summary: "Disable A2A channel",
257
266
  description: "Disable the A2A channel.",
258
267
  tags: ["integrations"],