@vellumai/assistant 0.3.4 → 0.3.5

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 (122) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +37 -2
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +70 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +21 -17
  12. package/src/__tests__/channel-approvals.test.ts +48 -1
  13. package/src/__tests__/channel-guardian.test.ts +74 -22
  14. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  15. package/src/__tests__/config-schema.test.ts +2 -1
  16. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  17. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  18. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  19. package/src/__tests__/entity-search.test.ts +615 -0
  20. package/src/__tests__/handlers-twilio-config.test.ts +407 -0
  21. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  22. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  23. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  24. package/src/__tests__/run-orchestrator.test.ts +22 -0
  25. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  26. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  27. package/src/__tests__/twilio-routes.test.ts +39 -3
  28. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  29. package/src/__tests__/web-search.test.ts +1 -1
  30. package/src/__tests__/work-item-output.test.ts +110 -0
  31. package/src/calls/call-domain.ts +8 -5
  32. package/src/calls/call-orchestrator.ts +22 -11
  33. package/src/calls/twilio-config.ts +17 -11
  34. package/src/calls/twilio-rest.ts +276 -0
  35. package/src/calls/twilio-routes.ts +39 -1
  36. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  37. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  38. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  39. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  40. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  41. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  42. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  43. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  44. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  45. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  46. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  47. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  48. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  49. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  50. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  51. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  52. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  53. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  54. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  55. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  56. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  57. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  58. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  59. package/src/config/bundled-skills/messaging/SKILL.md +21 -6
  60. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  61. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  62. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  63. package/src/config/defaults.ts +2 -1
  64. package/src/config/schema.ts +9 -3
  65. package/src/config/system-prompt.ts +24 -0
  66. package/src/config/templates/IDENTITY.md +2 -2
  67. package/src/config/vellum-skills/catalog.json +6 -0
  68. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  69. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  70. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  71. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  72. package/src/daemon/handlers/config.ts +783 -9
  73. package/src/daemon/handlers/dictation.ts +182 -0
  74. package/src/daemon/handlers/identity.ts +14 -23
  75. package/src/daemon/handlers/index.ts +2 -0
  76. package/src/daemon/handlers/sessions.ts +2 -0
  77. package/src/daemon/handlers/shared.ts +3 -0
  78. package/src/daemon/handlers/work-items.ts +15 -7
  79. package/src/daemon/ipc-contract-inventory.json +10 -0
  80. package/src/daemon/ipc-contract.ts +108 -4
  81. package/src/daemon/lifecycle.ts +2 -0
  82. package/src/daemon/ride-shotgun-handler.ts +1 -1
  83. package/src/daemon/server.ts +6 -2
  84. package/src/daemon/session-agent-loop.ts +5 -1
  85. package/src/daemon/session-runtime-assembly.ts +55 -0
  86. package/src/daemon/session-tool-setup.ts +2 -0
  87. package/src/daemon/session.ts +11 -1
  88. package/src/inbound/public-ingress-urls.ts +3 -3
  89. package/src/memory/channel-guardian-store.ts +2 -1
  90. package/src/memory/db-init.ts +144 -0
  91. package/src/memory/job-handlers/media-processing.ts +100 -0
  92. package/src/memory/jobs-store.ts +2 -1
  93. package/src/memory/jobs-worker.ts +4 -0
  94. package/src/memory/media-store.ts +759 -0
  95. package/src/memory/retriever.ts +6 -1
  96. package/src/memory/schema.ts +98 -0
  97. package/src/memory/search/entity.ts +208 -25
  98. package/src/memory/search/ranking.ts +6 -1
  99. package/src/memory/search/types.ts +24 -0
  100. package/src/messaging/provider-types.ts +2 -0
  101. package/src/messaging/providers/sms/adapter.ts +204 -0
  102. package/src/messaging/providers/sms/client.ts +93 -0
  103. package/src/messaging/providers/sms/types.ts +7 -0
  104. package/src/permissions/checker.ts +16 -2
  105. package/src/runtime/approval-message-composer.ts +143 -0
  106. package/src/runtime/channel-approvals.ts +12 -4
  107. package/src/runtime/channel-guardian-service.ts +44 -18
  108. package/src/runtime/channel-readiness-service.ts +292 -0
  109. package/src/runtime/channel-readiness-types.ts +29 -0
  110. package/src/runtime/http-server.ts +53 -27
  111. package/src/runtime/http-types.ts +3 -0
  112. package/src/runtime/routes/call-routes.ts +2 -1
  113. package/src/runtime/routes/channel-routes.ts +67 -21
  114. package/src/runtime/run-orchestrator.ts +35 -2
  115. package/src/tools/assets/materialize.ts +2 -2
  116. package/src/tools/calls/call-start.ts +1 -0
  117. package/src/tools/credentials/vault.ts +1 -1
  118. package/src/tools/execution-target.ts +11 -1
  119. package/src/tools/network/web-search.ts +1 -1
  120. package/src/tools/types.ts +2 -0
  121. package/src/twitter/router.ts +1 -1
  122. package/src/util/platform.ts +35 -0
@@ -44,6 +44,8 @@ import type {
44
44
  MessageProcessor,
45
45
  RuntimeAttachmentMetadata,
46
46
  } from '../http-types.js';
47
+ import type { GuardianRuntimeContext } from '../../daemon/session-runtime-assembly.js';
48
+ import { composeApprovalMessage } from '../approval-message-composer.js';
47
49
 
48
50
  const log = getLogger('runtime-http');
49
51
 
@@ -110,6 +112,19 @@ export interface GuardianContext {
110
112
  denialReason?: DenialReason;
111
113
  }
112
114
 
115
+ function toGuardianRuntimeContext(sourceChannel: string, ctx: GuardianContext): GuardianRuntimeContext {
116
+ return {
117
+ sourceChannel,
118
+ actorRole: ctx.actorRole,
119
+ guardianChatId: ctx.guardianChatId,
120
+ guardianExternalUserId: ctx.guardianExternalUserId,
121
+ requesterIdentifier: ctx.requesterIdentifier,
122
+ requesterExternalUserId: ctx.requesterExternalUserId,
123
+ requesterChatId: ctx.requesterChatId,
124
+ denialReason: ctx.denialReason,
125
+ };
126
+ }
127
+
113
128
  /** Guardian approval request expiry (30 minutes). */
114
129
  const GUARDIAN_APPROVAL_TTL_MS = 30 * 60 * 1000;
115
130
 
@@ -138,10 +153,10 @@ function buildGuardianDenyContext(
138
153
  sourceChannel: string,
139
154
  ): string {
140
155
  if (denialReason === 'no_identity') {
141
- return `Permission denied: the action "${toolName}" requires guardian approval, but your identity could not be verified on ${sourceChannel}. Do not retry yet. Explain this clearly, ask the user to message from a verifiable direct account/chat, and then retry after identity is available.`;
156
+ return `Permission denied: ${composeApprovalMessage({ scenario: 'guardian_deny_no_identity', toolName, channel: sourceChannel })} Do not retry yet. Ask the user to message from a verifiable direct account/chat, and then retry after identity is available.`;
142
157
  }
143
158
 
144
- return `Permission denied: the action "${toolName}" requires guardian approval, but no guardian is configured for this ${sourceChannel} channel. Do not retry yet. Explain that a guardian must be set up first. The guardian/admin should open the Channels section in Settings and click "Verify Guardian", or ask the assistant to set up guardian verification. The setup flow will provide a verification token to send as /guardian_verify <token> in the ${sourceChannel} chat.`;
159
+ return `Permission denied: ${composeApprovalMessage({ scenario: 'guardian_deny_no_binding', toolName, channel: sourceChannel })} Do not retry yet. Offer to set up guardian verification. The setup flow will provide a verification token to send as /guardian_verify <token>.`;
145
160
  }
146
161
 
147
162
  // ---------------------------------------------------------------------------
@@ -396,11 +411,13 @@ export async function handleChannelInbound(
396
411
  token,
397
412
  body.senderExternalUserId,
398
413
  externalChatId,
414
+ body.senderUsername,
415
+ body.senderName,
399
416
  );
400
417
 
401
418
  const replyText = verifyResult.success
402
- ? 'Guardian verified successfully. Your identity is now linked to this bot.'
403
- : 'Verification failed. Please try again later.';
419
+ ? composeApprovalMessage({ scenario: 'guardian_verify_success' })
420
+ : verifyResult.reason;
404
421
 
405
422
  try {
406
423
  await deliverChannelReply(replyCallbackUrl, {
@@ -427,15 +444,25 @@ export async function handleChannelInbound(
427
444
  // side-effect controls and their approvals route to the guardian's chat.
428
445
  //
429
446
  // Guardian actor-role resolution always runs.
430
- let guardianCtx: GuardianContext = { actorRole: 'guardian' };
447
+ let guardianCtx: GuardianContext;
431
448
  if (body.senderExternalUserId) {
449
+ const requesterLabel = body.senderUsername
450
+ ? `@${body.senderUsername}`
451
+ : body.senderExternalUserId;
432
452
  const senderIsGuardian = isGuardian(assistantId, sourceChannel, body.senderExternalUserId);
433
- if (!senderIsGuardian) {
453
+ if (senderIsGuardian) {
454
+ const binding = getGuardianBinding(assistantId, sourceChannel);
455
+ guardianCtx = {
456
+ actorRole: 'guardian',
457
+ guardianChatId: binding?.guardianDeliveryChatId ?? externalChatId,
458
+ guardianExternalUserId: binding?.guardianExternalUserId ?? body.senderExternalUserId,
459
+ requesterIdentifier: requesterLabel,
460
+ requesterExternalUserId: body.senderExternalUserId,
461
+ requesterChatId: externalChatId,
462
+ };
463
+ } else {
434
464
  const binding = getGuardianBinding(assistantId, sourceChannel);
435
465
  if (binding) {
436
- const requesterLabel = body.senderUsername
437
- ? `@${body.senderUsername}`
438
- : body.senderExternalUserId;
439
466
  guardianCtx = {
440
467
  actorRole: 'non-guardian',
441
468
  guardianChatId: binding.guardianDeliveryChatId,
@@ -450,6 +477,7 @@ export async function handleChannelInbound(
450
477
  guardianCtx = {
451
478
  actorRole: 'unverified_channel',
452
479
  denialReason: 'no_binding',
480
+ requesterIdentifier: requesterLabel,
453
481
  requesterExternalUserId: body.senderExternalUserId,
454
482
  requesterChatId: externalChatId,
455
483
  };
@@ -462,6 +490,7 @@ export async function handleChannelInbound(
462
490
  guardianCtx = {
463
491
  actorRole: 'unverified_channel',
464
492
  denialReason: 'no_identity',
493
+ requesterIdentifier: body.senderUsername ? `@${body.senderUsername}` : undefined,
465
494
  requesterExternalUserId: undefined,
466
495
  requesterChatId: externalChatId,
467
496
  };
@@ -524,6 +553,7 @@ export async function handleChannelInbound(
524
553
  senderName: body.senderName,
525
554
  senderExternalUserId: body.senderExternalUserId,
526
555
  senderUsername: body.senderUsername,
556
+ guardianCtx,
527
557
  replyCallbackUrl,
528
558
  assistantId,
529
559
  });
@@ -562,6 +592,8 @@ export async function handleChannelInbound(
562
592
  bearerToken,
563
593
  guardianCtx,
564
594
  assistantId,
595
+ metadataHints,
596
+ metadataUxBrief,
565
597
  });
566
598
  } else {
567
599
  // Fire-and-forget: process the message and deliver the reply in the background.
@@ -574,6 +606,7 @@ export async function handleChannelInbound(
574
606
  attachmentIds: hasAttachments ? attachmentIds : undefined,
575
607
  sourceChannel,
576
608
  externalChatId,
609
+ guardianCtx,
577
610
  metadataHints,
578
611
  metadataUxBrief,
579
612
  replyCallbackUrl,
@@ -598,6 +631,7 @@ interface BackgroundProcessingParams {
598
631
  attachmentIds?: string[];
599
632
  sourceChannel: string;
600
633
  externalChatId: string;
634
+ guardianCtx: GuardianContext;
601
635
  metadataHints: string[];
602
636
  metadataUxBrief?: string;
603
637
  replyCallbackUrl?: string;
@@ -614,6 +648,7 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
614
648
  attachmentIds,
615
649
  sourceChannel,
616
650
  externalChatId,
651
+ guardianCtx,
617
652
  metadataHints,
618
653
  metadataUxBrief,
619
654
  replyCallbackUrl,
@@ -633,6 +668,8 @@ function processChannelMessageInBackground(params: BackgroundProcessingParams):
633
668
  hints: metadataHints.length > 0 ? metadataHints : undefined,
634
669
  uxBrief: metadataUxBrief,
635
670
  },
671
+ assistantId,
672
+ guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
636
673
  },
637
674
  sourceChannel,
638
675
  );
@@ -695,6 +732,8 @@ interface ApprovalProcessingParams {
695
732
  bearerToken?: string;
696
733
  guardianCtx: GuardianContext;
697
734
  assistantId: string;
735
+ metadataHints: string[];
736
+ metadataUxBrief?: string;
698
737
  }
699
738
 
700
739
  /**
@@ -722,6 +761,8 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
722
761
  bearerToken,
723
762
  guardianCtx,
724
763
  assistantId,
764
+ metadataHints,
765
+ metadataUxBrief,
725
766
  } = params;
726
767
 
727
768
  const isNonGuardian = guardianCtx.actorRole === 'non-guardian';
@@ -736,6 +777,10 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
736
777
  {
737
778
  ...((isNonGuardian || isUnverifiedChannel) ? { forceStrictSideEffects: true } : {}),
738
779
  sourceChannel,
780
+ hints: metadataHints.length > 0 ? metadataHints : undefined,
781
+ uxBrief: metadataUxBrief,
782
+ assistantId,
783
+ guardianContext: toGuardianRuntimeContext(sourceChannel, guardianCtx),
739
784
  },
740
785
  );
741
786
 
@@ -826,7 +871,7 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
826
871
  try {
827
872
  await deliverChannelReply(replyCallbackUrl, {
828
873
  chatId: guardianCtx.requesterChatId ?? externalChatId,
829
- text: `Your request to run "${pending[0].toolName}" could not be sent to the guardian for approval. The action has been denied.`,
874
+ text: composeApprovalMessage({ scenario: 'guardian_delivery_failed', toolName: pending[0].toolName }),
830
875
  assistantId,
831
876
  }, bearerToken);
832
877
  } catch (notifyErr) {
@@ -839,7 +884,7 @@ function processChannelMessageWithApprovals(params: ApprovalProcessingParams): v
839
884
  try {
840
885
  await deliverChannelReply(replyCallbackUrl, {
841
886
  chatId: guardianCtx.requesterChatId ?? externalChatId,
842
- text: `Your request to run "${pending[0].toolName}" has been sent to the guardian for approval.`,
887
+ text: composeApprovalMessage({ scenario: 'guardian_request_forwarded', toolName: pending[0].toolName }),
843
888
  assistantId,
844
889
  }, bearerToken);
845
890
  } catch (err) {
@@ -1066,7 +1111,7 @@ async function handleApprovalInterception(
1066
1111
  try {
1067
1112
  await deliverChannelReply(replyCallbackUrl, {
1068
1113
  chatId: externalChatId,
1069
- text: `You have ${allPending.length} pending approval requests. Please use the approval buttons to respond to a specific request.`,
1114
+ text: composeApprovalMessage({ scenario: 'guardian_disambiguation', pendingCount: allPending.length }),
1070
1115
  assistantId,
1071
1116
  }, bearerToken);
1072
1117
  } catch (err) {
@@ -1099,7 +1144,7 @@ async function handleApprovalInterception(
1099
1144
  try {
1100
1145
  await deliverChannelReply(replyCallbackUrl, {
1101
1146
  chatId: externalChatId,
1102
- text: 'Only the verified guardian can approve or deny this request.',
1147
+ text: composeApprovalMessage({ scenario: 'guardian_identity_mismatch' }),
1103
1148
  assistantId,
1104
1149
  }, bearerToken);
1105
1150
  } catch (err) {
@@ -1133,10 +1178,11 @@ async function handleApprovalInterception(
1133
1178
 
1134
1179
  if (result.applied) {
1135
1180
  // Notify the requester's chat about the outcome with the tool name
1136
- const toolLabel = guardianApproval.toolName;
1137
- const outcomeText = decision.action === 'reject'
1138
- ? `Your request to run "${toolLabel}" was denied by the guardian.`
1139
- : `Your request to run "${toolLabel}" was approved by the guardian.`;
1181
+ const outcomeText = composeApprovalMessage({
1182
+ scenario: 'guardian_decision_outcome',
1183
+ decision: decision.action === 'reject' ? 'denied' : 'approved',
1184
+ toolName: guardianApproval.toolName,
1185
+ });
1140
1186
  try {
1141
1187
  await deliverChannelReply(replyCallbackUrl, {
1142
1188
  chatId: guardianApproval.requesterChatId,
@@ -1242,7 +1288,7 @@ async function handleApprovalInterception(
1242
1288
  try {
1243
1289
  await deliverChannelReply(replyCallbackUrl, {
1244
1290
  chatId: externalChatId,
1245
- text: 'Your request is pending guardian approval. Only the verified guardian can approve or deny this request.',
1291
+ text: composeApprovalMessage({ scenario: 'request_pending_guardian' }),
1246
1292
  assistantId,
1247
1293
  }, bearerToken);
1248
1294
  } catch (err) {
@@ -1269,7 +1315,7 @@ async function handleApprovalInterception(
1269
1315
  try {
1270
1316
  await deliverChannelReply(replyCallbackUrl, {
1271
1317
  chatId: externalChatId,
1272
- text: 'Your guardian approval request has expired and the action has been denied. Please try again.',
1318
+ text: composeApprovalMessage({ scenario: 'guardian_expired_requester', toolName: pending[0].toolName }),
1273
1319
  assistantId,
1274
1320
  }, bearerToken);
1275
1321
  } catch (err) {
@@ -1538,7 +1584,7 @@ export function sweepExpiredGuardianApprovals(
1538
1584
  // Notify the requester that the approval expired
1539
1585
  deliverChannelReply(deliverUrl, {
1540
1586
  chatId: approval.requesterChatId,
1541
- text: `Your guardian approval request for "${approval.toolName}" has expired and the action has been denied. Please try again.`,
1587
+ text: composeApprovalMessage({ scenario: 'guardian_expired_requester', toolName: approval.toolName }),
1542
1588
  assistantId: approval.assistantId,
1543
1589
  }, bearerToken).catch((err) => {
1544
1590
  log.error({ err, runId: approval.runId }, 'Failed to notify requester of guardian approval expiry');
@@ -1547,7 +1593,7 @@ export function sweepExpiredGuardianApprovals(
1547
1593
  // Notify the guardian that the approval expired
1548
1594
  deliverChannelReply(deliverUrl, {
1549
1595
  chatId: approval.guardianChatId,
1550
- text: `The approval request for "${approval.toolName}" from user ${approval.requesterExternalUserId} has expired and was automatically denied.`,
1596
+ text: composeApprovalMessage({ scenario: 'guardian_expired_guardian', toolName: approval.toolName, requesterIdentifier: approval.requesterExternalUserId }),
1551
1597
  assistantId: approval.assistantId,
1552
1598
  }, bearerToken).catch((err) => {
1553
1599
  log.error({ err, runId: approval.runId }, 'Failed to notify guardian of approval expiry');
@@ -18,6 +18,7 @@ import type { Run } from '../memory/runs-store.js';
18
18
  import type { Session } from '../daemon/session.js';
19
19
  import type { ServerMessage } from '../daemon/ipc-protocol.js';
20
20
  import { resolveChannelCapabilities } from '../daemon/session-runtime-assembly.js';
21
+ import type { GuardianRuntimeContext } from '../daemon/session-runtime-assembly.js';
21
22
  import type { UserDecision } from '../permissions/types.js';
22
23
  import { checkIngressForSecrets } from '../security/secret-ingress.js';
23
24
  import { IngressBlockedError } from '../util/errors.js';
@@ -37,7 +38,11 @@ interface PendingRunState {
37
38
  }
38
39
 
39
40
  export interface RunOrchestratorDeps {
40
- getOrCreateSession: (conversationId: string) => Promise<Session>;
41
+ getOrCreateSession: (conversationId: string, transport?: {
42
+ channelId: string;
43
+ hints?: string[];
44
+ uxBrief?: string;
45
+ }) => Promise<Session>;
41
46
  resolveAttachments: (attachmentIds: string[]) => Array<{
42
47
  id: string;
43
48
  filename: string;
@@ -67,6 +72,20 @@ export interface RunStartOptions {
67
72
  * default 'http-api'.
68
73
  */
69
74
  sourceChannel?: string;
75
+ /**
76
+ * Transport hints from sourceMetadata (e.g. reply-context cues).
77
+ * Forwarded to the session so the agent loop can incorporate them.
78
+ */
79
+ hints?: string[];
80
+ /**
81
+ * Brief UX context from sourceMetadata (e.g. UI surface description).
82
+ * Forwarded to the session so the agent loop can tailor its response.
83
+ */
84
+ uxBrief?: string;
85
+ /** Assistant scope for multi-assistant channels. */
86
+ assistantId?: string;
87
+ /** Guardian trust/identity context for the inbound actor. */
88
+ guardianContext?: GuardianRuntimeContext;
70
89
  }
71
90
 
72
91
  // ---------------------------------------------------------------------------
@@ -104,7 +123,17 @@ export class RunOrchestrator {
104
123
  throw new IngressBlockedError(ingressCheck.userNotice!, ingressCheck.detectedTypes);
105
124
  }
106
125
 
107
- const session = await this.deps.getOrCreateSession(conversationId);
126
+ // Build transport metadata when channel context is available so the
127
+ // session receives the same hints/uxBrief as the non-orchestrator path.
128
+ const transport = options?.sourceChannel
129
+ ? {
130
+ channelId: options.sourceChannel,
131
+ hints: options.hints,
132
+ uxBrief: options.uxBrief,
133
+ }
134
+ : undefined;
135
+
136
+ const session = await this.deps.getOrCreateSession(conversationId, transport);
108
137
 
109
138
  if (session.isProcessing()) {
110
139
  throw new Error('Session is already processing a message');
@@ -121,6 +150,8 @@ export class RunOrchestrator {
121
150
  ...session.memoryPolicy,
122
151
  strictSideEffects,
123
152
  };
153
+ session.setAssistantId(options?.assistantId ?? 'self');
154
+ session.setGuardianContext(options?.guardianContext ?? null);
124
155
 
125
156
  const attachments = attachmentIds
126
157
  ? this.deps.resolveAttachments(attachmentIds)
@@ -201,6 +232,8 @@ export class RunOrchestrator {
201
232
  // Reset channel capabilities so a subsequent IPC/desktop session on the
202
233
  // same conversation is not incorrectly treated as an HTTP-API client.
203
234
  session.setChannelCapabilities(null);
235
+ session.setGuardianContext(null);
236
+ session.setAssistantId('self');
204
237
  // Reset the session's client callback to a no-op so the stale
205
238
  // closure doesn't intercept events from future runs on the same session.
206
239
  // Set hasNoClient=true here since the run is done and no HTTP caller
@@ -41,8 +41,8 @@ function formatBytes(bytes: number): string {
41
41
  /**
42
42
  * Load an attachment row (including base64 data) by its primary key.
43
43
  *
44
- * Not scoped by assistantId because ToolContext doesn't carry it.
45
- * Cross-thread isolation is enforced by the visibility check in execute().
44
+ * Not scoped by assistantId because attachment access is enforced by
45
+ * conversation visibility checks in execute().
46
46
  */
47
47
  function loadAttachmentById(
48
48
  attachmentId: string,
@@ -54,6 +54,7 @@ class CallStartTool implements Tool {
54
54
  task: input.task as string,
55
55
  context: input.context as string | undefined,
56
56
  conversationId: context.conversationId,
57
+ assistantId: context.assistantId,
57
58
  callerIdentityMode: input.caller_identity_mode as 'assistant_number' | 'user_number' | undefined,
58
59
  });
59
60
 
@@ -630,7 +630,7 @@ class CredentialStoreTool implements Tool {
630
630
  const dmChannel = await conversationsOpen(botToken, installingUserId);
631
631
  const welcomeMsg =
632
632
  `You have installed ${identity.user}, an AI Assistant, on ${identity.team}. ` +
633
- `Manage the assistant experience for this workspace in the workspace settings page.`;
633
+ `You can manage the assistant experience for this workspace by chatting with the assistant or from the Settings page.`;
634
634
  await postMessage(botToken, dmChannel.channel.id, welcomeMsg);
635
635
  }
636
636
  } catch (err) {
@@ -1,7 +1,12 @@
1
1
  import type { ExecutionTarget } from './types.js';
2
2
  import { getTool } from './registry.js';
3
3
 
4
- export function resolveExecutionTarget(toolName: string): ExecutionTarget {
4
+ export interface ManifestOverride {
5
+ risk: 'low' | 'medium' | 'high';
6
+ execution_target: 'host' | 'sandbox';
7
+ }
8
+
9
+ export function resolveExecutionTarget(toolName: string, manifestOverride?: ManifestOverride): ExecutionTarget {
5
10
  const tool = getTool(toolName);
6
11
  // Manifest-declared execution target is authoritative — check it first so
7
12
  // skill tools with host_/computer_use_ prefixes aren't mis-classified.
@@ -13,6 +18,11 @@ export function resolveExecutionTarget(toolName: string): ExecutionTarget {
13
18
  if (tool?.executionMode === 'proxy') {
14
19
  return 'host';
15
20
  }
21
+ // Use manifest metadata for unregistered skill tools so the Permission
22
+ // Simulator shows accurate execution targets instead of defaulting to sandbox.
23
+ if (!tool && manifestOverride) {
24
+ return manifestOverride.execution_target;
25
+ }
16
26
  // Prefix heuristics for core tools that don't declare an explicit target.
17
27
  if (toolName.startsWith('host_') || toolName.startsWith('computer_use_')) {
18
28
  return 'host';
@@ -268,7 +268,7 @@ class WebSearchTool implements Tool {
268
268
  apiKey = fallbackKey;
269
269
  } else {
270
270
  return {
271
- content: 'Error: No web search API key configured. Set PERPLEXITY_API_KEY or BRAVE_API_KEY environment variable, or configure a key in settings.',
271
+ content: 'Error: No web search API key configured. Set a PERPLEXITY_API_KEY or BRAVE_API_KEY environment variable, or configure it from the Settings page under API Keys.',
272
272
  isError: true,
273
273
  };
274
274
  }
@@ -87,6 +87,8 @@ export interface ToolContext {
87
87
  workingDir: string;
88
88
  sessionId: string;
89
89
  conversationId: string;
90
+ /** Logical assistant scope for multi-assistant routing. */
91
+ assistantId?: string;
90
92
  /** When set, the tool execution is part of a task run. Used to retrieve ephemeral permission rules. */
91
93
  taskRunId?: string;
92
94
  /** Per-message request ID for log correlation across session/connection boundaries. */
@@ -41,7 +41,7 @@ export async function routedPostTweet(
41
41
  if (strategy === 'oauth') {
42
42
  // User explicitly wants OAuth
43
43
  if (!oauthIsAvailable()) {
44
- throw Object.assign(new Error('OAuth is not configured. Set up OAuth credentials in Settings, or switch to browser strategy: `vellum x strategy set browser`.'), {
44
+ throw Object.assign(new Error('OAuth is not configured. Provide your X developer credentials here in the chat to set up OAuth, or switch to browser strategy: `vellum x strategy set browser`.'), {
45
45
  pathUsed: 'oauth' as const,
46
46
  suggestAlternative: 'browser' as const,
47
47
  });
@@ -45,6 +45,41 @@ export function getClipboardCommand(): string | null {
45
45
  return null;
46
46
  }
47
47
 
48
+ /**
49
+ * Read and parse the lockfile, trying the primary path (~/.vellum.lock.json)
50
+ * first, then falling back to the legacy path (~/.vellum.lockfile.json).
51
+ * Respects BASE_DATA_DIR for non-standard home directories.
52
+ * Returns null if neither file exists or both are malformed.
53
+ */
54
+ export function readLockfile(): Record<string, unknown> | null {
55
+ const base = process.env.BASE_DATA_DIR?.trim() || homedir();
56
+ const candidates = [
57
+ join(base, '.vellum.lock.json'),
58
+ join(base, '.vellum.lockfile.json'),
59
+ ];
60
+ for (const lockPath of candidates) {
61
+ if (!existsSync(lockPath)) continue;
62
+ try {
63
+ const raw = JSON.parse(readFileSync(lockPath, 'utf-8'));
64
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
65
+ return raw as Record<string, unknown>;
66
+ }
67
+ } catch {
68
+ // malformed JSON — try next
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Write data to the primary lockfile (~/.vellum.lock.json).
76
+ * Respects BASE_DATA_DIR for non-standard home directories.
77
+ */
78
+ export function writeLockfile(data: Record<string, unknown>): void {
79
+ const base = process.env.BASE_DATA_DIR?.trim() || homedir();
80
+ writeFileSync(join(base, '.vellum.lock.json'), JSON.stringify(data, null, 2) + '\n');
81
+ }
82
+
48
83
  /**
49
84
  * Returns the root ~/.vellum directory. User-facing files (config, prompt
50
85
  * files, skills) and runtime files (socket, PID) live here.