@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -1,9 +1,4 @@
1
- import { applyCanonicalGuardianDecision } from "../../approvals/guardian-decision-primitive.js";
2
- import {
3
- getCanonicalGuardianRequest,
4
- isRequestInConversationScope,
5
- } from "../../memory/canonical-guardian-store.js";
6
- import type { ApprovalAction } from "../../runtime/channel-approval-types.js";
1
+ import { processGuardianDecision } from "../../runtime/guardian-action-service.js";
7
2
  import { resolveLocalIpcTrustContext } from "../../runtime/local-actor-identity.js";
8
3
  import { listGuardianDecisionPrompts } from "../../runtime/routes/guardian-action-routes.js";
9
4
  import type {
@@ -12,14 +7,6 @@ import type {
12
7
  } from "../ipc-protocol.js";
13
8
  import { defineHandlers, log } from "./shared.js";
14
9
 
15
- const VALID_ACTIONS = new Set<string>([
16
- "approve_once",
17
- "approve_10m",
18
- "approve_thread",
19
- "approve_always",
20
- "reject",
21
- ]);
22
-
23
10
  export const guardianActionsHandlers = defineHandlers({
24
11
  guardian_actions_pending_request: (
25
12
  msg: GuardianActionsPendingRequest,
@@ -43,95 +30,41 @@ export const guardianActionsHandlers = defineHandlers({
43
30
  ctx,
44
31
  ) => {
45
32
  try {
46
- // Validate the action is one of the known actions
47
- if (!VALID_ACTIONS.has(msg.action)) {
48
- log.warn(
49
- { requestId: msg.requestId, action: msg.action },
50
- "Invalid guardian action",
51
- );
52
- ctx.send(socket, {
53
- type: "guardian_action_decision_response",
54
- applied: false,
55
- reason: "invalid_action",
56
- requestId: msg.requestId,
57
- });
58
- return;
59
- }
60
-
61
- // Verify conversationId scoping before applying the canonical decision.
62
- // The decision is allowed when the conversationId matches the request's
63
- // source conversation OR a recorded delivery destination conversation.
64
- if (msg.conversationId) {
65
- const canonicalRequest = getCanonicalGuardianRequest(msg.requestId);
66
- if (
67
- canonicalRequest &&
68
- canonicalRequest.conversationId &&
69
- !isRequestInConversationScope(
70
- msg.requestId,
71
- msg.conversationId,
72
- "vellum",
73
- )
74
- ) {
75
- log.warn(
76
- {
77
- requestId: msg.requestId,
78
- expected: canonicalRequest.conversationId,
79
- got: msg.conversationId,
80
- },
81
- "conversationId not in scope",
82
- );
83
- ctx.send(socket, {
84
- type: "guardian_action_decision_response",
85
- applied: false,
86
- reason: "not_found",
87
- requestId: msg.requestId,
88
- });
89
- return;
90
- }
91
- }
92
-
93
- // Resolve the local IPC actor's principal via the vellum guardian binding.
94
33
  const localTrustCtx = resolveLocalIpcTrustContext("vellum");
95
- const canonicalResult = await applyCanonicalGuardianDecision({
34
+
35
+ const result = await processGuardianDecision({
96
36
  requestId: msg.requestId,
97
- action: msg.action as ApprovalAction,
37
+ action: msg.action,
38
+ conversationId: msg.conversationId,
39
+ channel: "vellum",
98
40
  actorContext: {
99
41
  externalUserId: localTrustCtx.guardianExternalUserId,
100
- channel: "vellum",
101
42
  guardianPrincipalId: localTrustCtx.guardianPrincipalId ?? undefined,
102
43
  },
103
- userText: undefined,
104
44
  });
105
45
 
106
- if (canonicalResult.applied) {
107
- // When the CAS committed but the resolver failed, the side effect
108
- // (e.g. minting a verification session) did not happen. From the
109
- // caller's perspective the decision was not truly applied.
110
- if (canonicalResult.resolverFailed) {
111
- ctx.send(socket, {
112
- type: "guardian_action_decision_response",
113
- applied: false,
114
- reason: "resolver_failed",
115
- resolverFailureReason: canonicalResult.resolverFailureReason,
116
- requestId: canonicalResult.requestId,
117
- });
118
- return;
119
- }
120
-
46
+ if (!result.ok) {
121
47
  ctx.send(socket, {
122
48
  type: "guardian_action_decision_response",
123
- applied: true,
124
- requestId: canonicalResult.requestId,
49
+ applied: false,
50
+ reason: result.error,
51
+ requestId: msg.requestId,
125
52
  });
126
53
  return;
127
54
  }
128
55
 
129
- // Return the reason for failure (stale, expired, not_found, etc.)
130
56
  ctx.send(socket, {
131
57
  type: "guardian_action_decision_response",
132
- applied: false,
133
- reason: canonicalResult.reason,
134
- requestId: msg.requestId,
58
+ applied: result.applied,
59
+ ...(result.applied
60
+ ? { requestId: result.requestId }
61
+ : {
62
+ reason: result.reason,
63
+ ...(result.resolverFailureReason
64
+ ? { resolverFailureReason: result.resolverFailureReason }
65
+ : {}),
66
+ requestId: result.requestId ?? msg.requestId,
67
+ }),
135
68
  });
136
69
  } catch (err) {
137
70
  log.error(
@@ -1648,7 +1648,6 @@ export function handleHistoryRequest(
1648
1648
  ? {
1649
1649
  ...(s.data.preview ? { preview: s.data.preview } : {}),
1650
1650
  ...(s.data.appId ? { appId: s.data.appId } : {}),
1651
- ...(s.data.appType ? { appType: s.data.appType } : {}),
1652
1651
  }
1653
1652
  : {}),
1654
1653
  } as Record<string, unknown>,
@@ -178,7 +178,6 @@ export interface AppsListResponse {
178
178
  createdAt: number;
179
179
  version?: string;
180
180
  contentId?: string;
181
- appType?: string;
182
181
  }>;
183
182
  }
184
183
 
@@ -1,9 +1,9 @@
1
- // Ingress access control: invite management, member management, and escalation decisions.
1
+ // Contacts access control: invite management, member management, and escalation decisions.
2
2
 
3
3
  // === Client → Server ===
4
4
 
5
- export interface IngressInviteRequest {
6
- type: "ingress_invite";
5
+ export interface ContactsInviteRequest {
6
+ type: "contacts_invite";
7
7
  action: "create" | "list" | "revoke" | "redeem";
8
8
  /** Source channel for the invite (required for create and redeem). */
9
9
  sourceChannel?: string;
@@ -29,31 +29,6 @@ export interface IngressInviteRequest {
29
29
  guardianName?: string;
30
30
  }
31
31
 
32
- export interface IngressContactRequest {
33
- type: "ingress_member"; // Legacy discriminator — kept for client compatibility
34
- action: "list" | "upsert" | "revoke" | "block";
35
- /** Assistant ID for scoping member operations (defaults to 'self'). */
36
- assistantId?: string;
37
- /** Source channel (required for upsert, optional filter for list). */
38
- sourceChannel?: string;
39
- /** External user ID (upsert only). */
40
- externalUserId?: string;
41
- /** External chat ID (upsert only). */
42
- externalChatId?: string;
43
- /** Display name (upsert only). */
44
- displayName?: string;
45
- /** Username (upsert only). */
46
- username?: string;
47
- /** Access policy (upsert only). */
48
- policy?: "allow" | "deny" | "escalate";
49
- /** Member status (upsert only for setting, list only for filtering). */
50
- status?: "pending" | "active";
51
- /** Member ID (revoke and block only). */
52
- memberId?: string;
53
- /** Reason for revoke or block (revoke and block only). */
54
- reason?: string;
55
- }
56
-
57
32
  export interface AssistantInboxEscalationRequest {
58
33
  type: "assistant_inbox_escalation";
59
34
  action: "list" | "decide";
@@ -71,8 +46,8 @@ export interface AssistantInboxEscalationRequest {
71
46
 
72
47
  // === Server → Client ===
73
48
 
74
- export interface IngressInviteResponse {
75
- type: "ingress_invite_response";
49
+ export interface ContactsInviteResponse {
50
+ type: "contacts_invite_response";
76
51
  success: boolean;
77
52
  error?: string;
78
53
  /** Single invite (returned on create/revoke). Token field is only present on create. */
@@ -102,38 +77,6 @@ export interface IngressInviteResponse {
102
77
  }>;
103
78
  }
104
79
 
105
- export interface IngressContactResponse {
106
- type: "ingress_member_response"; // Legacy discriminator — kept for client compatibility
107
- success: boolean;
108
- error?: string;
109
- /** Single member (returned on upsert/revoke/block). */
110
- member?: {
111
- id: string;
112
- sourceChannel: string;
113
- externalUserId?: string;
114
- externalChatId?: string;
115
- displayName?: string;
116
- username?: string;
117
- status: string;
118
- policy: string;
119
- lastSeenAt?: number;
120
- createdAt: number;
121
- };
122
- /** List of members (returned on list). */
123
- members?: Array<{
124
- id: string;
125
- sourceChannel: string;
126
- externalUserId?: string;
127
- externalChatId?: string;
128
- displayName?: string;
129
- username?: string;
130
- status: string;
131
- policy: string;
132
- lastSeenAt?: number;
133
- createdAt: number;
134
- }>;
135
- }
136
-
137
80
  export interface AssistantInboxEscalationResponse {
138
81
  type: "assistant_inbox_escalation_response";
139
82
  success: boolean;
@@ -161,11 +104,9 @@ export interface AssistantInboxEscalationResponse {
161
104
  // --- Domain-level union aliases (consumed by the barrel file) ---
162
105
 
163
106
  export type _InboxClientMessages =
164
- | IngressInviteRequest
165
- | IngressContactRequest
107
+ | ContactsInviteRequest
166
108
  | AssistantInboxEscalationRequest;
167
109
 
168
110
  export type _InboxServerMessages =
169
- | IngressInviteResponse
170
- | IngressContactResponse
111
+ | ContactsInviteResponse
171
112
  | AssistantInboxEscalationResponse;
@@ -382,6 +382,7 @@ export type SessionErrorCode =
382
382
  | "PROVIDER_NETWORK"
383
383
  | "PROVIDER_RATE_LIMIT"
384
384
  | "PROVIDER_API"
385
+ | "PROVIDER_BILLING"
385
386
  | "CONTEXT_TOO_LARGE"
386
387
  | "QUEUE_FULL"
387
388
  | "SESSION_ABORTED"
@@ -98,7 +98,6 @@ export interface DynamicPageSurfaceData {
98
98
  width?: number;
99
99
  height?: number;
100
100
  appId?: string;
101
- appType?: string;
102
101
  reloadGeneration?: number;
103
102
  status?: string;
104
103
  preview?: DynamicPagePreview;
@@ -69,6 +69,7 @@
69
69
  "cancel",
70
70
  "confirmation_response",
71
71
  "contacts",
72
+ "contacts_invite",
72
73
  "conversation_search",
73
74
  "conversation_seen_signal",
74
75
  "cu_observation",
@@ -99,8 +100,6 @@
99
100
  "identity_get",
100
101
  "image_gen_model_set",
101
102
  "ingress_config",
102
- "ingress_invite",
103
- "ingress_member",
104
103
  "integration_connect",
105
104
  "integration_disconnect",
106
105
  "integration_list",
@@ -215,6 +214,7 @@
215
214
  "confirmation_request",
216
215
  "confirmation_state_changed",
217
216
  "contacts_changed",
217
+ "contacts_invite_response",
218
218
  "contacts_response",
219
219
  "context_compacted",
220
220
  "conversation_search_response",
@@ -252,8 +252,6 @@
252
252
  "identity_changed",
253
253
  "identity_get_response",
254
254
  "ingress_config_response",
255
- "ingress_invite_response",
256
- "ingress_member_response",
257
255
  "integration_connect_result",
258
256
  "integration_list_response",
259
257
  "ipc_blob_probe_result",
@@ -312,8 +312,20 @@ export async function runDaemon(): Promise<void> {
312
312
  registerBroadcastFn((msg) => server.broadcast(msg));
313
313
 
314
314
  const scheduler = startScheduler(
315
- async (conversationId, message) => {
316
- await server.processMessage(conversationId, message);
315
+ async (conversationId, message, options) => {
316
+ await server.processMessage(
317
+ conversationId,
318
+ message,
319
+ undefined,
320
+ options?.trustClass
321
+ ? {
322
+ trustContext: {
323
+ sourceChannel: "vellum",
324
+ trustClass: options.trustClass,
325
+ },
326
+ }
327
+ : undefined,
328
+ );
317
329
  },
318
330
  (reminder) => {
319
331
  void emitNotificationSignal({
@@ -56,6 +56,8 @@ export interface EventHandlerState {
56
56
  readonly persistedToolUseIds: Set<string>;
57
57
  readonly accumulatedDirectives: DirectiveRequest[];
58
58
  readonly accumulatedToolContentBlocks: ContentBlock[];
59
+ /** Maps index in accumulatedToolContentBlocks → tool name that produced it. */
60
+ readonly toolContentBlockToolNames: Map<number, string>;
59
61
  readonly directiveWarnings: string[];
60
62
  readonly toolUseIdToName: Map<string, string>;
61
63
  currentTurnToolNames: string[];
@@ -99,6 +101,7 @@ export function createEventHandlerState(): EventHandlerState {
99
101
  persistedToolUseIds: new Set(),
100
102
  accumulatedDirectives: [],
101
103
  accumulatedToolContentBlocks: [],
104
+ toolContentBlockToolNames: new Map(),
102
105
  directiveWarnings: [],
103
106
  toolUseIdToName: new Map(),
104
107
  currentTurnToolNames: [],
@@ -400,6 +403,12 @@ export function handleToolResult(
400
403
  for (const cb of event.contentBlocks) {
401
404
  if (cb.type === "image" || cb.type === "file") {
402
405
  state.accumulatedToolContentBlocks.push(cb);
406
+ if (toolName) {
407
+ state.toolContentBlockToolNames.set(
408
+ state.accumulatedToolContentBlocks.length - 1,
409
+ toolName,
410
+ );
411
+ }
403
412
  }
404
413
  }
405
414
  }
@@ -1238,6 +1238,7 @@ export async function runAgentLoopImpl(
1238
1238
  ctx.hasNoClient,
1239
1239
  ),
1240
1240
  state.lastAssistantMessageId,
1241
+ state.toolContentBlockToolNames,
1241
1242
  );
1242
1243
  const { assistantAttachments, emittedAttachments } = attachmentResult;
1243
1244
 
@@ -124,6 +124,7 @@ export async function resolveAssistantAttachments(
124
124
  workingDir: string,
125
125
  approveHostRead: ApproveHostRead,
126
126
  lastAssistantMessageId: string | undefined,
127
+ toolContentBlockToolNames?: ReadonlyMap<number, string>,
127
128
  ): Promise<AttachmentResolutionResult> {
128
129
  let assistantAttachments: AssistantAttachmentDraft[] = [];
129
130
  const emittedAttachments: UserMessageAttachment[] = [];
@@ -170,7 +171,10 @@ export async function resolveAssistantAttachments(
170
171
  "Directive resolution complete",
171
172
  );
172
173
 
173
- const toolDrafts = contentBlocksToDrafts(accumulatedToolContentBlocks);
174
+ const toolDrafts = contentBlocksToDrafts(
175
+ accumulatedToolContentBlocks,
176
+ toolContentBlockToolNames,
177
+ );
174
178
  // Most recent tool outputs (e.g., final browser screenshot) should win
175
179
  // the MAX_ASSISTANT_ATTACHMENTS cap over older intermediate screenshots.
176
180
  toolDrafts.reverse();
@@ -188,6 +188,24 @@ function classifyCore(
188
188
  retryable: false,
189
189
  };
190
190
  }
191
+ if (/credit balance is too low|insufficient.*credits?/i.test(message)) {
192
+ return {
193
+ code: "PROVIDER_BILLING",
194
+ userMessage: "Your API key has insufficient credits.",
195
+ retryable: false,
196
+ };
197
+ }
198
+ if (
199
+ /invalid.*api.?key|invalid.*x-api-key|authentication.?error|invalid.authentication/i.test(
200
+ message,
201
+ )
202
+ ) {
203
+ return {
204
+ code: "PROVIDER_BILLING",
205
+ userMessage: "Your API key is invalid.",
206
+ retryable: false,
207
+ };
208
+ }
191
209
  return {
192
210
  code: "PROVIDER_API",
193
211
  userMessage: "The AI provider rejected the request.",
@@ -13,7 +13,10 @@ import * as conversationStore from "../memory/conversation-store.js";
13
13
  import type { PermissionPrompter } from "../permissions/prompter.js";
14
14
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
15
15
  import type { ContentBlock, Message } from "../providers/types.js";
16
- import type { TrustClass } from "../runtime/actor-trust-resolver.js";
16
+ import {
17
+ isUntrustedTrustClass,
18
+ type TrustClass,
19
+ } from "../runtime/actor-trust-resolver.js";
17
20
  import { unregisterSessionSender } from "../tools/browser/browser-screencast.js";
18
21
  import { getLogger } from "../util/logger.js";
19
22
  import { repairHistory } from "./history-repair.js";
@@ -47,10 +50,6 @@ function parseProvenanceTrustClass(
47
50
  return undefined;
48
51
  }
49
52
 
50
- function isUntrustedTrustClass(trustClass: TrustClass | undefined): boolean {
51
- return trustClass === "trusted_contact" || trustClass === "unknown";
52
- }
53
-
54
53
  function filterMessagesForUntrustedActor(
55
54
  messages: conversationStore.MessageRow[],
56
55
  ): conversationStore.MessageRow[] {
@@ -19,15 +19,29 @@ export function stripMediaPayloadsForRetry(messages: Message[]): {
19
19
  latestUserIndex: number | null;
20
20
  } {
21
21
  let latestUserIndex: number | null = null;
22
+ let lastSummaryUserIndex: number | null = null;
22
23
  for (let i = messages.length - 1; i >= 0; i--) {
23
24
  const msg = messages[i];
24
25
  if (msg.role !== "user") continue;
25
- if (getSummaryFromContextMessage(msg) != null) continue;
26
26
  if (isToolResultOnlyMessage(msg)) continue;
27
+ if (getSummaryFromContextMessage(msg) != null) {
28
+ // Track the last summary message as a fallback — after aggressive
29
+ // compaction (minKeepRecentUserTurns: 0), the summary may be the only
30
+ // user message left and it can contain preserved image blocks that
31
+ // should not be stripped.
32
+ if (lastSummaryUserIndex == null) lastSummaryUserIndex = i;
33
+ continue;
34
+ }
27
35
  latestUserIndex = i;
28
36
  break;
29
37
  }
30
38
 
39
+ // Fall back to the summary message when compaction consumed all user turns,
40
+ // so images preserved by compaction are not unconditionally stripped.
41
+ if (latestUserIndex == null && lastSummaryUserIndex != null) {
42
+ latestUserIndex = lastSummaryUserIndex;
43
+ }
44
+
31
45
  let modified = false;
32
46
  let replacedBlocks = 0;
33
47
  let keptLatestMediaBlocks = 0;
@@ -1119,7 +1119,6 @@ export async function surfaceProxyResolver(
1119
1119
  const surfaceData: DynamicPageSurfaceData = {
1120
1120
  html: app.htmlDefinition,
1121
1121
  appId: app.id,
1122
- appType: app.appType,
1123
1122
  preview: {
1124
1123
  ...defaultPreview,
1125
1124
  ...preview,
@@ -7,10 +7,7 @@
7
7
  */
8
8
 
9
9
  import { isHttpAuthDisabled } from "../config/env.js";
10
- import type {
11
- ProxyApprovalCallback,
12
- ProxyApprovalRequest,
13
- } from "../outbound-proxy/index.js";
10
+ import { getBindingByConversation } from "../memory/external-conversation-store.js";
14
11
  import {
15
12
  generateAllowlistOptions,
16
13
  generateScopeOptions,
@@ -32,6 +29,8 @@ import { requestComputerControlTool } from "../tools/computer-use/request-comput
32
29
  import type { ToolExecutor } from "../tools/executor.js";
33
30
  import { getAllToolDefinitions } from "../tools/registry.js";
34
31
  import type {
32
+ ProxyApprovalCallback,
33
+ ProxyApprovalRequest,
35
34
  ToolExecutionResult,
36
35
  ToolLifecycleEventHandler,
37
36
  } from "../tools/types.js";
@@ -163,6 +162,10 @@ export function createToolExecutor(
163
162
  ctx.surfaceActionRequestIds?.has(ctx.currentRequestId ?? "") ?? false,
164
163
  requesterExternalUserId: ctx.trustContext?.requesterExternalUserId,
165
164
  requesterChatId: ctx.trustContext?.requesterChatId,
165
+ channelPermissionChannelId:
166
+ ctx.trustContext?.sourceChannel === "slack"
167
+ ? getBindingByConversation(ctx.conversationId)?.externalChatId
168
+ : undefined,
166
169
  onOutput,
167
170
  signal: ctx.abortController?.signal,
168
171
  sandboxOverride: ctx.sandboxOverride,
@@ -26,7 +26,8 @@ export interface ToolDomainEvents {
26
26
  | "allow_thread"
27
27
  | "always_allow"
28
28
  | "deny"
29
- | "always_deny";
29
+ | "always_deny"
30
+ | "temporary_override";
30
31
  riskLevel: string;
31
32
  decidedAtMs: number;
32
33
  };
@@ -115,7 +115,6 @@ export function ensurePrebuiltHomeBaseSeeded(): {
115
115
  description: buildDescription(metadata),
116
116
  schemaJson: "{}",
117
117
  htmlDefinition: html,
118
- appType: "app",
119
118
  });
120
119
 
121
120
  log.info({ appId: created.id }, "Seeded prebuilt Home Base app");
@@ -50,11 +50,8 @@ import type {
50
50
  ExtensionResponse,
51
51
  } from "../browser-extension-relay/protocol.js";
52
52
  import { extensionRelayServer } from "../browser-extension-relay/server.js";
53
- import { getGatewayInternalBaseUrl } from "../config/env.js";
54
- import {
55
- isSigningKeyInitialized,
56
- mintEdgeRelayToken,
57
- } from "../runtime/auth/token-service.js";
53
+ import { isSigningKeyInitialized } from "../runtime/auth/token-service.js";
54
+ import { gatewayPost } from "../runtime/gateway-internal-client.js";
58
55
 
59
56
  // ---------------------------------------------------------------------------
60
57
  // Types
@@ -145,26 +142,12 @@ async function sendRelayCommand(
145
142
  "Auth signing key not initialized — browser-relay commands require the daemon to be running",
146
143
  );
147
144
  }
148
- const token = mintEdgeRelayToken();
149
-
150
- const resp = await fetch(
151
- `${getGatewayInternalBaseUrl()}/v1/browser-relay/command`,
152
- {
153
- method: "POST",
154
- headers: {
155
- "Content-Type": "application/json",
156
- Authorization: `Bearer ${token}`,
157
- },
158
- body: JSON.stringify(command),
159
- },
160
- );
161
-
162
- if (!resp.ok) {
163
- const body = await resp.text();
164
- throw new Error(`Relay HTTP command failed (${resp.status}): ${body}`);
165
- }
166
145
 
167
- return (await resp.json()) as ExtensionResponse;
146
+ const { data } = await gatewayPost<ExtensionResponse>(
147
+ "/v1/browser-relay/command",
148
+ command,
149
+ );
150
+ return data;
168
151
  }
169
152
 
170
153
  // ---------------------------------------------------------------------------
@@ -16,6 +16,8 @@ export interface ImageGenerationRequest {
16
16
  export interface GeneratedImage {
17
17
  mimeType: string;
18
18
  dataBase64: string;
19
+ /** Short title derived from the model's text response, if available. */
20
+ title?: string;
19
21
  }
20
22
 
21
23
  export interface ImageGenerationResult {
@@ -74,10 +76,12 @@ export async function generateImage(
74
76
 
75
77
  const client = new GoogleGenAI({ apiKey });
76
78
 
77
- // Build contents array
79
+ // Build contents array — append a title request so the model's text
80
+ // response contains a short filename-safe title for the generated image.
81
+ const promptWithTitle = `${request.prompt}\n\nAlso respond with a short title (max 6 words) for the image on its own line, prefixed with "Title: ".`;
78
82
  const parts: Array<
79
83
  { text: string } | { inlineData: { mimeType: string; data: string } }
80
- > = [{ text: request.prompt }];
84
+ > = [{ text: promptWithTitle }];
81
85
 
82
86
  if (request.mode === "edit" && request.sourceImages) {
83
87
  for (const img of request.sourceImages) {
@@ -114,7 +118,15 @@ export async function generateImage(
114
118
  }
115
119
  }
116
120
 
117
- return { images, text };
121
+ // Extract title from the text response and apply to images
122
+ const title = extractTitle(text);
123
+ if (title) {
124
+ for (const img of images) {
125
+ img.title = title;
126
+ }
127
+ }
128
+
129
+ return { images, text: stripTitleLine(text), title };
118
130
  };
119
131
 
120
132
  if (variants === 1) {
@@ -141,3 +153,36 @@ export async function generateImage(
141
153
 
142
154
  return { images: allImages, text: combinedText, resolvedModel: model };
143
155
  }
156
+
157
+ // --- Title extraction helpers ---
158
+
159
+ const TITLE_RE = /^Title:\s*(.+)/im;
160
+
161
+ /**
162
+ * Extract a title from the model's text response.
163
+ * Looks for a line starting with "Title: " and sanitizes it for use as a filename.
164
+ */
165
+ function extractTitle(text?: string): string | undefined {
166
+ if (!text) return undefined;
167
+ const match = TITLE_RE.exec(text);
168
+ if (!match?.[1]) return undefined;
169
+ return match[1]
170
+ .trim()
171
+ .replace(/[^\w\s-]/g, "")
172
+ .replace(/\s+/g, "-")
173
+ .toLowerCase()
174
+ .slice(0, 60);
175
+ }
176
+
177
+ /**
178
+ * Remove the "Title: ..." line from text so it doesn't appear in
179
+ * the tool result content shown to the user.
180
+ */
181
+ function stripTitleLine(text?: string): string | undefined {
182
+ if (!text) return undefined;
183
+ const stripped = text
184
+ .replace(TITLE_RE, "")
185
+ .replace(/\n{3,}/g, "\n\n")
186
+ .trim();
187
+ return stripped.length > 0 ? stripped : undefined;
188
+ }