@vellumai/assistant 0.4.2 → 0.4.4

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 (221) hide show
  1. package/.env.example +3 -0
  2. package/ARCHITECTURE.md +124 -10
  3. package/README.md +43 -35
  4. package/docs/trusted-contact-access.md +20 -0
  5. package/package.json +1 -1
  6. package/scripts/ipc/generate-swift.ts +1 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
  8. package/src/__tests__/access-request-decision.test.ts +0 -1
  9. package/src/__tests__/actor-token-service.test.ts +1099 -0
  10. package/src/__tests__/agent-loop.test.ts +51 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -0
  12. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +415 -0
  14. package/src/__tests__/call-controller.test.ts +49 -0
  15. package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
  16. package/src/__tests__/call-pointer-messages.test.ts +93 -3
  17. package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
  18. package/src/__tests__/call-routes-http.test.ts +0 -25
  19. package/src/__tests__/callback-handoff-copy.test.ts +186 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +133 -12
  21. package/src/__tests__/channel-guardian.test.ts +0 -86
  22. package/src/__tests__/channel-readiness-service.test.ts +10 -16
  23. package/src/__tests__/checker.test.ts +33 -12
  24. package/src/__tests__/config-schema.test.ts +6 -0
  25. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
  26. package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
  27. package/src/__tests__/conversation-routes.test.ts +12 -3
  28. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  29. package/src/__tests__/daemon-server-session-init.test.ts +4 -0
  30. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  31. package/src/__tests__/guardian-actions-endpoint.test.ts +39 -13
  32. package/src/__tests__/guardian-dispatch.test.ts +8 -0
  33. package/src/__tests__/guardian-outbound-http.test.ts +4 -5
  34. package/src/__tests__/guardian-question-mode.test.ts +200 -0
  35. package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
  36. package/src/__tests__/guardian-routing-state.test.ts +525 -0
  37. package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
  38. package/src/__tests__/handlers-telegram-config.test.ts +0 -83
  39. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
  40. package/src/__tests__/headless-browser-navigate.test.ts +2 -0
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -1
  42. package/src/__tests__/ingress-routes-http.test.ts +55 -0
  43. package/src/__tests__/ipc-snapshot.test.ts +18 -51
  44. package/src/__tests__/non-member-access-request.test.ts +159 -9
  45. package/src/__tests__/notification-decision-fallback.test.ts +129 -4
  46. package/src/__tests__/notification-decision-strategy.test.ts +106 -2
  47. package/src/__tests__/notification-guardian-path.test.ts +3 -0
  48. package/src/__tests__/recording-intent-handler.test.ts +1 -0
  49. package/src/__tests__/relay-server.test.ts +1475 -33
  50. package/src/__tests__/send-endpoint-busy.test.ts +5 -0
  51. package/src/__tests__/session-agent-loop.test.ts +1 -0
  52. package/src/__tests__/session-confirmation-signals.test.ts +523 -0
  53. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  54. package/src/__tests__/session-runtime-assembly.test.ts +4 -1
  55. package/src/__tests__/session-surfaces-task-progress.test.ts +44 -1
  56. package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
  57. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
  58. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
  59. package/src/__tests__/tool-executor.test.ts +21 -2
  60. package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
  61. package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
  62. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
  63. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +11 -1
  64. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  65. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  66. package/src/__tests__/twilio-config.test.ts +2 -13
  67. package/src/__tests__/twilio-routes.test.ts +4 -3
  68. package/src/__tests__/update-bulletin.test.ts +0 -1
  69. package/src/agent/loop.ts +1 -1
  70. package/src/approvals/guardian-decision-primitive.ts +12 -3
  71. package/src/approvals/guardian-request-resolvers.ts +169 -11
  72. package/src/calls/call-constants.ts +29 -0
  73. package/src/calls/call-controller.ts +11 -3
  74. package/src/calls/call-domain.ts +33 -11
  75. package/src/calls/call-pointer-message-composer.ts +154 -0
  76. package/src/calls/call-pointer-messages.ts +106 -27
  77. package/src/calls/guardian-dispatch.ts +4 -2
  78. package/src/calls/relay-server.ts +921 -112
  79. package/src/calls/twilio-config.ts +4 -11
  80. package/src/calls/twilio-routes.ts +4 -6
  81. package/src/calls/types.ts +3 -1
  82. package/src/calls/voice-session-bridge.ts +4 -3
  83. package/src/cli/core-commands.ts +7 -4
  84. package/src/cli.ts +5 -4
  85. package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
  86. package/src/config/bundled-skills/app-builder/SKILL.md +309 -10
  87. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
  89. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
  90. package/src/config/bundled-skills/messaging/SKILL.md +61 -12
  91. package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
  92. package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
  93. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
  94. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
  96. package/src/config/bundled-skills/twitter/SKILL.md +3 -3
  97. package/src/config/bundled-skills/vercel-token-setup/SKILL.md +215 -0
  98. package/src/config/calls-schema.ts +36 -0
  99. package/src/config/env.ts +22 -0
  100. package/src/config/feature-flag-registry.json +8 -8
  101. package/src/config/schema.ts +2 -2
  102. package/src/config/skills.ts +11 -0
  103. package/src/config/system-prompt.ts +11 -1
  104. package/src/config/templates/SOUL.md +2 -0
  105. package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
  106. package/src/config/vellum-skills/trusted-contacts/SKILL.md +8 -1
  107. package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
  108. package/src/daemon/call-pointer-generators.ts +59 -0
  109. package/src/daemon/computer-use-session.ts +2 -5
  110. package/src/daemon/handlers/apps.ts +76 -20
  111. package/src/daemon/handlers/config-channels.ts +9 -61
  112. package/src/daemon/handlers/config-inbox.ts +11 -3
  113. package/src/daemon/handlers/config-ingress.ts +28 -3
  114. package/src/daemon/handlers/config-telegram.ts +12 -0
  115. package/src/daemon/handlers/config.ts +2 -6
  116. package/src/daemon/handlers/index.ts +2 -1
  117. package/src/daemon/handlers/pairing.ts +2 -0
  118. package/src/daemon/handlers/publish.ts +11 -46
  119. package/src/daemon/handlers/sessions.ts +59 -5
  120. package/src/daemon/handlers/shared.ts +17 -2
  121. package/src/daemon/ipc-contract/apps.ts +1 -0
  122. package/src/daemon/ipc-contract/inbox.ts +4 -0
  123. package/src/daemon/ipc-contract/integrations.ts +1 -97
  124. package/src/daemon/ipc-contract/messages.ts +47 -1
  125. package/src/daemon/ipc-contract/notifications.ts +11 -0
  126. package/src/daemon/ipc-contract-inventory.json +2 -4
  127. package/src/daemon/lifecycle.ts +17 -0
  128. package/src/daemon/server.ts +16 -2
  129. package/src/daemon/session-agent-loop-handlers.ts +20 -0
  130. package/src/daemon/session-agent-loop.ts +24 -12
  131. package/src/daemon/session-lifecycle.ts +1 -1
  132. package/src/daemon/session-process.ts +11 -1
  133. package/src/daemon/session-runtime-assembly.ts +6 -1
  134. package/src/daemon/session-surfaces.ts +32 -3
  135. package/src/daemon/session.ts +88 -1
  136. package/src/daemon/tool-side-effects.ts +22 -0
  137. package/src/home-base/prebuilt/brain-graph.html +1483 -0
  138. package/src/home-base/prebuilt/index.html +40 -0
  139. package/src/inbound/platform-callback-registration.ts +157 -0
  140. package/src/memory/canonical-guardian-store.ts +1 -1
  141. package/src/memory/conversation-crud.ts +2 -1
  142. package/src/memory/conversation-title-service.ts +16 -2
  143. package/src/memory/db-init.ts +8 -0
  144. package/src/memory/delivery-crud.ts +2 -1
  145. package/src/memory/guardian-action-store.ts +2 -1
  146. package/src/memory/guardian-approvals.ts +3 -2
  147. package/src/memory/ingress-invite-store.ts +12 -2
  148. package/src/memory/ingress-member-store.ts +4 -3
  149. package/src/memory/migrations/038-actor-token-records.ts +39 -0
  150. package/src/memory/migrations/124-voice-invite-display-metadata.ts +14 -0
  151. package/src/memory/migrations/index.ts +2 -0
  152. package/src/memory/schema.ts +26 -5
  153. package/src/messaging/provider-types.ts +24 -0
  154. package/src/messaging/provider.ts +7 -0
  155. package/src/messaging/providers/gmail/adapter.ts +127 -0
  156. package/src/messaging/providers/sms/adapter.ts +40 -37
  157. package/src/notifications/adapters/macos.ts +45 -2
  158. package/src/notifications/broadcaster.ts +16 -0
  159. package/src/notifications/copy-composer.ts +50 -2
  160. package/src/notifications/decision-engine.ts +22 -9
  161. package/src/notifications/destination-resolver.ts +16 -2
  162. package/src/notifications/emit-signal.ts +18 -9
  163. package/src/notifications/guardian-question-mode.ts +419 -0
  164. package/src/notifications/signal.ts +14 -3
  165. package/src/permissions/checker.ts +13 -1
  166. package/src/permissions/prompter.ts +14 -0
  167. package/src/providers/anthropic/client.ts +20 -0
  168. package/src/providers/provider-send-message.ts +15 -3
  169. package/src/runtime/access-request-helper.ts +82 -4
  170. package/src/runtime/actor-token-service.ts +234 -0
  171. package/src/runtime/actor-token-store.ts +236 -0
  172. package/src/runtime/actor-trust-resolver.ts +2 -2
  173. package/src/runtime/assistant-scope.ts +10 -0
  174. package/src/runtime/channel-approvals.ts +5 -3
  175. package/src/runtime/channel-readiness-service.ts +23 -64
  176. package/src/runtime/channel-readiness-types.ts +3 -4
  177. package/src/runtime/channel-retry-sweep.ts +4 -1
  178. package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
  179. package/src/runtime/guardian-action-followup-executor.ts +1 -1
  180. package/src/runtime/guardian-context-resolver.ts +82 -0
  181. package/src/runtime/guardian-outbound-actions.ts +5 -7
  182. package/src/runtime/guardian-reply-router.ts +67 -30
  183. package/src/runtime/guardian-vellum-migration.ts +57 -0
  184. package/src/runtime/http-server.ts +75 -31
  185. package/src/runtime/http-types.ts +13 -0
  186. package/src/runtime/ingress-service.ts +14 -0
  187. package/src/runtime/invite-redemption-service.ts +10 -1
  188. package/src/runtime/local-actor-identity.ts +76 -0
  189. package/src/runtime/middleware/actor-token.ts +271 -0
  190. package/src/runtime/middleware/twilio-validation.ts +2 -4
  191. package/src/runtime/routes/approval-routes.ts +82 -7
  192. package/src/runtime/routes/brain-graph-routes.ts +222 -0
  193. package/src/runtime/routes/call-routes.ts +2 -1
  194. package/src/runtime/routes/channel-readiness-routes.ts +71 -0
  195. package/src/runtime/routes/channel-route-shared.ts +3 -3
  196. package/src/runtime/routes/conversation-attention-routes.ts +2 -1
  197. package/src/runtime/routes/conversation-routes.ts +142 -53
  198. package/src/runtime/routes/events-routes.ts +22 -8
  199. package/src/runtime/routes/guardian-action-routes.ts +45 -3
  200. package/src/runtime/routes/guardian-approval-interception.ts +29 -0
  201. package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
  202. package/src/runtime/routes/inbound-conversation.ts +4 -3
  203. package/src/runtime/routes/inbound-message-handler.ts +147 -5
  204. package/src/runtime/routes/ingress-routes.ts +2 -0
  205. package/src/runtime/routes/integration-routes.ts +7 -15
  206. package/src/runtime/routes/pairing-routes.ts +163 -0
  207. package/src/runtime/routes/twilio-routes.ts +934 -0
  208. package/src/runtime/tool-grant-request-helper.ts +3 -1
  209. package/src/security/oauth2.ts +27 -2
  210. package/src/security/token-manager.ts +46 -10
  211. package/src/tools/browser/browser-execution.ts +4 -3
  212. package/src/tools/browser/browser-handoff.ts +10 -18
  213. package/src/tools/browser/browser-manager.ts +80 -25
  214. package/src/tools/browser/browser-screencast.ts +35 -119
  215. package/src/tools/calls/call-start.ts +2 -1
  216. package/src/tools/permission-checker.ts +15 -4
  217. package/src/tools/terminal/parser.ts +12 -0
  218. package/src/tools/tool-approval-handler.ts +244 -19
  219. package/src/workspace/git-service.ts +19 -0
  220. package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
  221. package/src/daemon/handlers/config-twilio.ts +0 -1082
@@ -7,6 +7,8 @@ import { v4 as uuid } from 'uuid';
7
7
 
8
8
  import { packageApp } from '../../bundler/app-bundler.js';
9
9
  import { defaultGallery } from '../../gallery/default-gallery.js';
10
+ import { resolveHomeBaseAppId } from '../../home-base/bootstrap.js';
11
+ import { isPrebuiltHomeBaseApp } from '../../home-base/prebuilt-home-base-updater.js';
10
12
  import { getAppDiff, getAppFileAtVersion, getAppHistory, restoreAppVersion } from '../../memory/app-git-service.js';
11
13
  import { createApp, createAppRecord, deleteAppRecord, getApp, getAppPreview, listApps, queryAppRecords, updateApp,updateAppRecord } from '../../memory/app-store.js';
12
14
  import { createSharedAppLink } from '../../memory/shared-app-links-store.js';
@@ -77,28 +79,54 @@ export function handleAppDataRequest(
77
79
  }
78
80
 
79
81
  export function handleAppOpenRequest(msg: { appId: string }, socket: net.Socket, ctx: HandlerContext): void {
80
- const appId = msg.appId;
81
- if (!appId) {
82
- ctx.send(socket, { type: 'error', message: 'app_open_request requires appId' });
83
- return;
84
- }
82
+ try {
83
+ const appId = msg.appId;
84
+ if (!appId) {
85
+ ctx.send(socket, { type: 'error', message: 'app_open_request requires appId' });
86
+ return;
87
+ }
88
+
89
+ const app = getApp(appId);
90
+ if (app) {
91
+ const surfaceId = `app-open-${uuid()}`;
92
+ ctx.send(socket, {
93
+ type: 'ui_surface_show',
94
+ sessionId: 'app-panel',
95
+ surfaceId,
96
+ surfaceType: 'dynamic_page',
97
+ title: app.name,
98
+ data: { html: app.htmlDefinition, appId: app.id, appType: app.appType },
99
+ display: 'panel',
100
+ } as UiSurfaceShow);
101
+ return;
102
+ }
103
+
104
+ // Fallback: the ID might be a surfaceId from an ephemeral ui_show surface
105
+ // (not a persistent app). Search active sessions for cached surface data.
106
+ for (const session of ctx.sessions.values()) {
107
+ const cached = session.surfaceState.get(appId);
108
+ if (cached && cached.surfaceType === 'dynamic_page') {
109
+ const newSurfaceId = `app-open-${uuid()}`;
110
+ ctx.send(socket, {
111
+ type: 'ui_surface_show',
112
+ sessionId: 'app-panel',
113
+ surfaceId: newSurfaceId,
114
+ surfaceType: 'dynamic_page',
115
+ title: cached.title ?? (cached.data as { preview?: { title?: string } }).preview?.title,
116
+ data: cached.data,
117
+ display: 'panel',
118
+ } as UiSurfaceShow);
119
+ return;
120
+ }
121
+ }
85
122
 
86
- const app = getApp(appId);
87
- if (!app) {
123
+ log.warn({ appId }, 'App not found in store or session surfaces');
88
124
  ctx.send(socket, { type: 'error', message: `App not found: ${appId}` });
89
- return;
125
+ } catch (err) {
126
+ const message = err instanceof Error ? err.message : String(err);
127
+ log.error({ err, appId: msg.appId }, 'Failed to handle app open request');
128
+ ctx.send(socket, { type: 'error', message: `Failed to open app: ${message}` });
90
129
  }
91
-
92
- const surfaceId = `app-open-${uuid()}`;
93
- ctx.send(socket, {
94
- type: 'ui_surface_show',
95
- sessionId: 'app-panel',
96
- surfaceId,
97
- surfaceType: 'dynamic_page',
98
- title: app.name,
99
- data: { html: app.htmlDefinition, appId: app.id, appType: app.appType },
100
- display: 'panel',
101
- } as UiSurfaceShow);
102
130
  }
103
131
 
104
132
  export function handleAppUpdatePreview(msg: AppUpdatePreviewRequest, socket: net.Socket, ctx: HandlerContext): void {
@@ -114,7 +142,35 @@ export function handleAppUpdatePreview(msg: AppUpdatePreviewRequest, socket: net
114
142
 
115
143
  export function handleAppsList(socket: net.Socket, ctx: HandlerContext): void {
116
144
  try {
117
- const apps = listApps();
145
+ const allApps = listApps();
146
+ const homeBaseId = resolveHomeBaseAppId();
147
+
148
+ // When no home base was found by ID, do a single targeted search for an app
149
+ // matching the HTML marker. listApps() returns metadata-only (no htmlDefinition),
150
+ // so the HTML marker check requires loading the full app from disk. We limit
151
+ // this expensive operation to the case where homeBaseId is null.
152
+ const excludeIds = new Set<string>();
153
+ if (homeBaseId) {
154
+ excludeIds.add(homeBaseId);
155
+ } else {
156
+ for (const a of allApps) {
157
+ if (isPrebuiltHomeBaseApp(a)) {
158
+ excludeIds.add(a.id);
159
+ continue;
160
+ }
161
+ const fullApp = getApp(a.id);
162
+ if (fullApp && isPrebuiltHomeBaseApp(fullApp)) {
163
+ excludeIds.add(a.id);
164
+ continue;
165
+ }
166
+ }
167
+ }
168
+
169
+ const apps = allApps.filter((a) => {
170
+ if (excludeIds.has(a.id)) return false;
171
+ if (isPrebuiltHomeBaseApp(a)) return false;
172
+ return true;
173
+ });
118
174
  ctx.send(socket, {
119
175
  type: 'apps_list_response',
120
176
  apps: apps.map((a) => {
@@ -3,6 +3,7 @@ import * as net from 'node:net';
3
3
  import type { ChannelId } from '../../channels/types.js';
4
4
  import * as externalConversationStore from '../../memory/external-conversation-store.js';
5
5
  import { findMember, revokeMember } from '../../memory/ingress-member-store.js';
6
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
6
7
  import {
7
8
  createVerificationChallenge,
8
9
  findActiveSession,
@@ -17,9 +18,7 @@ import {
17
18
  resendOutbound,
18
19
  startOutbound,
19
20
  } from '../../runtime/guardian-outbound-actions.js';
20
- import { normalizeAssistantId } from '../../util/platform.js';
21
21
  import type {
22
- ChannelReadinessRequest,
23
22
  GuardianVerificationRequest,
24
23
  GuardianVerificationResponse,
25
24
  } from '../ipc-protocol.js';
@@ -60,11 +59,10 @@ export function getReadinessService(): ChannelReadinessService {
60
59
 
61
60
  export function createGuardianChallenge(
62
61
  channel?: ChannelId,
63
- assistantId?: string,
64
62
  rebind?: boolean,
65
63
  sessionId?: string,
66
64
  ): GuardianVerificationResult {
67
- const resolvedAssistantId = normalizeAssistantId(assistantId ?? 'self');
65
+ const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
68
66
  const resolvedChannel = channel ?? 'telegram';
69
67
 
70
68
  const existingBinding = getGuardianBinding(resolvedAssistantId, resolvedChannel);
@@ -89,9 +87,8 @@ export function createGuardianChallenge(
89
87
 
90
88
  export function getGuardianStatus(
91
89
  channel?: ChannelId,
92
- assistantId?: string,
93
90
  ): GuardianVerificationResult {
94
- const resolvedAssistantId = normalizeAssistantId(assistantId ?? 'self');
91
+ const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
95
92
  const resolvedChannel = channel ?? 'telegram';
96
93
 
97
94
  const binding = getGuardianBinding(resolvedAssistantId, resolvedChannel);
@@ -161,17 +158,15 @@ export function handleGuardianVerification(
161
158
  socket: net.Socket,
162
159
  ctx: HandlerContext,
163
160
  ): void {
164
- // Normalize the assistant ID so challenges are always stored under the
165
- // same key the inbound-call path will use for lookups (typically "self").
166
- const assistantId = normalizeAssistantId(msg.assistantId ?? 'self');
161
+ const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
167
162
  const channel = msg.channel ?? 'telegram';
168
163
 
169
164
  try {
170
165
  if (msg.action === 'create_challenge') {
171
- const result = createGuardianChallenge(channel, assistantId, msg.rebind, msg.sessionId);
166
+ const result = createGuardianChallenge(channel, msg.rebind, msg.sessionId);
172
167
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
173
168
  } else if (msg.action === 'status') {
174
- const result = getGuardianStatus(channel, assistantId);
169
+ const result = getGuardianStatus(channel);
175
170
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
176
171
  } else if (msg.action === 'revoke') {
177
172
  // Capture binding before revoking so we can revoke the guardian's
@@ -200,13 +195,13 @@ export function handleGuardianVerification(
200
195
  channel,
201
196
  });
202
197
  } else if (msg.action === 'start_outbound') {
203
- const result = startOutbound({ channel, assistantId, destination: msg.destination, rebind: msg.rebind, originConversationId: msg.originConversationId });
198
+ const result = startOutbound({ channel, destination: msg.destination, rebind: msg.rebind, originConversationId: msg.originConversationId });
204
199
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
205
200
  } else if (msg.action === 'resend_outbound') {
206
- const result = resendOutbound({ channel, assistantId, originConversationId: msg.originConversationId });
201
+ const result = resendOutbound({ channel, originConversationId: msg.originConversationId });
207
202
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
208
203
  } else if (msg.action === 'cancel_outbound') {
209
- const result = cancelOutbound({ channel, assistantId });
204
+ const result = cancelOutbound({ channel });
210
205
  ctx.send(socket, { type: 'guardian_verification_response', ...result });
211
206
  } else {
212
207
  ctx.send(socket, {
@@ -229,53 +224,6 @@ export function handleGuardianVerification(
229
224
  }
230
225
 
231
226
 
232
- // ---------------------------------------------------------------------------
233
- // Channel readiness handler
234
- // ---------------------------------------------------------------------------
235
-
236
- export async function handleChannelReadiness(
237
- msg: ChannelReadinessRequest,
238
- socket: net.Socket,
239
- ctx: HandlerContext,
240
- ): Promise<void> {
241
- try {
242
- const service = getReadinessService();
243
-
244
- if (msg.action === 'refresh') {
245
- if (msg.channel) {
246
- service.invalidateChannel(msg.channel, msg.assistantId);
247
- } else {
248
- service.invalidateAll();
249
- }
250
- }
251
-
252
- const snapshots = await service.getReadiness(msg.channel, msg.includeRemote, msg.assistantId);
253
-
254
- ctx.send(socket, {
255
- type: 'channel_readiness_response',
256
- success: true,
257
- snapshots: snapshots.map((s) => ({
258
- channel: s.channel,
259
- ready: s.ready,
260
- checkedAt: s.checkedAt,
261
- stale: s.stale,
262
- reasons: s.reasons,
263
- localChecks: s.localChecks,
264
- remoteChecks: s.remoteChecks,
265
- })),
266
- });
267
- } catch (err) {
268
- const message = err instanceof Error ? err.message : String(err);
269
- log.error({ err }, 'Failed to handle channel readiness');
270
- ctx.send(socket, {
271
- type: 'channel_readiness_response',
272
- success: false,
273
- error: message,
274
- });
275
- }
276
- }
277
-
278
227
  export const channelHandlers = defineHandlers({
279
- channel_readiness: handleChannelReadiness,
280
228
  guardian_verification: handleGuardianVerification,
281
229
  });
@@ -40,6 +40,8 @@ export function handleIngressInvite(
40
40
  note: msg.note,
41
41
  maxUses: msg.maxUses,
42
42
  expiresInMs: msg.expiresInMs,
43
+ friendName: msg.friendName,
44
+ guardianName: msg.guardianName,
43
45
  });
44
46
  if (!result.ok) {
45
47
  ctx.send(socket, { type: 'ingress_invite_response', success: false, error: result.error });
@@ -303,9 +305,15 @@ async function executeApprove(
303
305
  }
304
306
  }
305
307
  session.setAssistantId(assistantId);
306
- // The guardian approved this escalation, so tag as guardian trust to avoid
307
- // unknown-provenance memory gating.
308
- session.setGuardianContext({ trustClass: 'guardian', sourceChannel: sourceChannel ?? 'vellum' });
308
+ // The guardian already approved this escalation via the inbox, so we
309
+ // directly set guardian trust. Going through resolveLocalIpcGuardianContext
310
+ // would look up the vellum binding's guardian ID and compare it against
311
+ // a different channel's binding (e.g. telegram/sms), misclassifying the
312
+ // actor as 'unknown'.
313
+ session.setGuardianContext({
314
+ sourceChannel: sourceChannel ?? 'vellum',
315
+ trustClass: 'guardian',
316
+ });
309
317
  session.setCommandIntent(null);
310
318
 
311
319
  // Process the message through the agent loop (no IPC event callback
@@ -10,6 +10,7 @@ import {
10
10
  setIngressPublicBaseUrl,
11
11
  } from '../../config/env.js';
12
12
  import { loadRawConfig, saveRawConfig } from '../../config/loader.js';
13
+ import { registerCallbackRoute, resolveCallbackUrl, shouldUsePlatformCallbacks } from '../../inbound/platform-callback-registration.js';
13
14
  import {
14
15
  getTwilioSmsWebhookUrl,
15
16
  getTwilioStatusCallbackUrl,
@@ -92,9 +93,21 @@ export async function syncTwilioWebhooks(
92
93
  ingressConfig: IngressConfig,
93
94
  ): Promise<{ success: boolean; warning?: string }> {
94
95
  try {
95
- const voiceUrl = getTwilioVoiceWebhookUrl(ingressConfig);
96
- const statusCallbackUrl = getTwilioStatusCallbackUrl(ingressConfig);
97
- const smsUrl = getTwilioSmsWebhookUrl(ingressConfig);
96
+ const voiceUrl = await resolveCallbackUrl(
97
+ () => getTwilioVoiceWebhookUrl(ingressConfig),
98
+ 'webhooks/twilio/voice',
99
+ 'twilio_voice',
100
+ );
101
+ const statusCallbackUrl = await resolveCallbackUrl(
102
+ () => getTwilioStatusCallbackUrl(ingressConfig),
103
+ 'webhooks/twilio/status',
104
+ 'twilio_status',
105
+ );
106
+ const smsUrl = await resolveCallbackUrl(
107
+ () => getTwilioSmsWebhookUrl(ingressConfig),
108
+ 'webhooks/twilio/sms',
109
+ 'twilio_sms',
110
+ );
98
111
  await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
99
112
  voiceUrl,
100
113
  statusCallbackUrl,
@@ -183,6 +196,18 @@ export async function handleIngressConfig(
183
196
  // Use the effective URL from process.env (which accounts for the
184
197
  // fallback branch above) rather than the raw `value` from the UI.
185
198
  const effectiveUrl = isEnabled ? getIngressPublicBaseUrl() : undefined;
199
+
200
+ // When containerized with a platform, register the Telegram callback
201
+ // route so the platform knows how to forward Telegram webhooks.
202
+ // This must happen independently of effectiveUrl — in containerized
203
+ // deployments without ingress.publicBaseUrl, platform callbacks are the
204
+ // only way to receive Telegram webhooks.
205
+ if (shouldUsePlatformCallbacks()) {
206
+ registerCallbackRoute('webhooks/telegram', 'telegram').catch((err) => {
207
+ log.warn({ err }, 'Failed to register Telegram platform callback route');
208
+ });
209
+ }
210
+
186
211
  triggerGatewayReconcile(effectiveUrl);
187
212
 
188
213
  // Best-effort Twilio webhook reconciliation: when ingress is being
@@ -1,6 +1,7 @@
1
1
  import * as net from 'node:net';
2
2
 
3
3
  import { getIngressPublicBaseUrl } from '../../config/env.js';
4
+ import { registerCallbackRoute, shouldUsePlatformCallbacks } from '../../inbound/platform-callback-registration.js';
4
5
  import { deleteSecureKey,getSecureKey, setSecureKey } from '../../security/secure-keys.js';
5
6
  import { deleteCredentialMetadata, getCredentialMetadata,upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
6
7
  import type { TelegramConfigRequest, TelegramConfigResponse } from '../ipc-protocol.js';
@@ -161,6 +162,17 @@ export async function setTelegramConfig(botToken?: string): Promise<TelegramConf
161
162
  hasWebhookSecret,
162
163
  };
163
164
 
165
+ // When containerized with a platform, register the Telegram callback
166
+ // route so the platform knows how to forward Telegram webhooks.
167
+ // This must happen independently of effectiveUrl — in containerized
168
+ // deployments without ingress.publicBaseUrl, platform callbacks are the
169
+ // only way to receive Telegram webhooks.
170
+ if (shouldUsePlatformCallbacks()) {
171
+ registerCallbackRoute('webhooks/telegram', 'telegram').catch((err) => {
172
+ log.warn({ err }, 'Failed to register Telegram platform callback route');
173
+ });
174
+ }
175
+
164
176
  // Trigger gateway reconcile so the webhook registration updates immediately
165
177
  const effectiveUrl = getIngressPublicBaseUrl();
166
178
  if (effectiveUrl) {
@@ -11,13 +11,12 @@
11
11
  * config-platform.ts — Platform base URL configuration
12
12
  * config-integrations.ts — Vercel API & Twitter integration
13
13
  * config-telegram.ts — Telegram bot configuration
14
- * config-twilio.ts Twilio SMS/voice configuration
15
- * config-channels.ts — Channel guardian & readiness
14
+ * config-channels.ts Channel guardian verification
16
15
  * config-tools.ts — Env vars, tool permission simulation, tool names
17
16
  */
18
17
 
19
18
  // Re-export individual handlers for direct import by tests and other modules
20
- export { getReadinessService,handleChannelReadiness, handleGuardianVerification } from './config-channels.js';
19
+ export { getReadinessService, handleGuardianVerification } from './config-channels.js';
21
20
  export { handleHeartbeatChecklistRead, handleHeartbeatChecklistWrite,handleHeartbeatConfig, handleHeartbeatRunNow, handleHeartbeatRunsList } from './config-heartbeat.js';
22
21
  export { computeGatewayTarget, handleIngressConfig, syncTwilioWebhooks,triggerGatewayReconcile } from './config-ingress.js';
23
22
  export { handleTwitterIntegrationConfig,handleVercelApiConfig } from './config-integrations.js';
@@ -28,7 +27,6 @@ export { handleShareToSlack, handleSlackWebhookConfig } from './config-slack.js'
28
27
  export { handleTelegramConfig, summarizeTelegramError } from './config-telegram.js';
29
28
  export { handleEnvVarsRequest, handleToolNamesList,handleToolPermissionSimulate } from './config-tools.js';
30
29
  export { handleAcceptStarterBundle,handleAddTrustRule, handleRemoveTrustRule, handleTrustRulesList, handleUpdateTrustRule } from './config-trust.js';
31
- export { handleTwilioConfig } from './config-twilio.js';
32
30
  export { broadcastClientSettingsUpdate, handleVoiceConfigUpdate, normalizeActivationKey } from './config-voice.js';
33
31
 
34
32
  // Assemble the combined dispatch map from domain-specific handler groups
@@ -43,7 +41,6 @@ import { slackHandlers } from './config-slack.js';
43
41
  import { telegramHandlers } from './config-telegram.js';
44
42
  import { toolHandlers } from './config-tools.js';
45
43
  import { trustHandlers } from './config-trust.js';
46
- import { twilioHandlers } from './config-twilio.js';
47
44
  import { voiceHandlers } from './config-voice.js';
48
45
 
49
46
  export const configHandlers = {
@@ -55,7 +52,6 @@ export const configHandlers = {
55
52
  ...platformHandlers,
56
53
  ...integrationHandlers,
57
54
  ...telegramHandlers,
58
- ...twilioHandlers,
59
55
  ...channelHandlers,
60
56
  ...toolHandlers,
61
57
  ...heartbeatHandlers,
@@ -2,6 +2,7 @@ import * as net from 'node:net';
2
2
 
3
3
  import { type Confidence, recordConversationSeenSignal, type SignalType } from '../../memory/conversation-attention-store.js';
4
4
  import { updateDeliveryClientOutcome } from '../../notifications/deliveries-store.js';
5
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
5
6
  import type { ClientMessage } from '../ipc-protocol.js';
6
7
  import { handleRideShotgunStart, handleRideShotgunStop } from '../ride-shotgun-handler.js';
7
8
  import { handleWatchObservation } from '../watch-handler.js';
@@ -104,7 +105,7 @@ const inlineHandlers = defineHandlers({
104
105
  try {
105
106
  recordConversationSeenSignal({
106
107
  conversationId: msg.conversationId,
107
- assistantId: 'self',
108
+ assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
108
109
  sourceChannel: msg.sourceChannel,
109
110
  signalType: msg.signalType as SignalType,
110
111
  confidence: msg.confidence as Confidence,
@@ -1,5 +1,6 @@
1
1
  import * as net from 'node:net';
2
2
 
3
+ import { cleanupPairingState } from '../../runtime/routes/pairing-routes.js';
3
4
  import {
4
5
  approveDevice,
5
6
  clearAllDevices,
@@ -46,6 +47,7 @@ function handlePairingApprovalResponse(
46
47
 
47
48
  if (msg.decision === 'deny') {
48
49
  pairingStoreRef.deny(msg.pairingRequestId);
50
+ cleanupPairingState(msg.pairingRequestId);
49
51
  log.info({ pairingRequestId: msg.pairingRequestId }, 'Pairing request denied');
50
52
  return;
51
53
  }
@@ -4,15 +4,13 @@ import * as net from 'node:net';
4
4
  import { v4 as uuid } from 'uuid';
5
5
 
6
6
  import { createPublishedPage, getPublishedPageByDeploymentId, getPublishedPageByHash, markDeleted, updatePublishedPage } from '../../memory/published-pages-store.js';
7
- import { setSecureKey } from '../../security/secure-keys.js';
8
7
  import { deleteVercelDeployment,deployHtmlToVercel } from '../../services/vercel-deploy.js';
9
8
  import { credentialBroker } from '../../tools/credentials/broker.js';
10
- import { getCredentialMetadata, upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
11
9
  import type {
12
10
  PublishPageRequest,
13
11
  UnpublishPageRequest,
14
12
  } from '../ipc-protocol.js';
15
- import { defineHandlers, type HandlerContext,log, requestSecretStandalone } from './shared.js';
13
+ import { defineHandlers, type HandlerContext,log } from './shared.js';
16
14
 
17
15
  export async function handlePublishPage(
18
16
  msg: PublishPageRequest,
@@ -60,57 +58,24 @@ export async function handlePublishPage(
60
58
  return { url: result.url, deploymentId: result.deploymentId };
61
59
  };
62
60
 
63
- let useResult = await credentialBroker.serverUse({
61
+ const useResult = await credentialBroker.serverUse({
64
62
  service: 'vercel',
65
63
  field: 'api_token',
66
64
  toolName: 'publish_page',
67
65
  execute: publishExecute,
68
66
  });
69
67
 
70
- // If no credential found, prompt the user and retry
68
+ // If no credential found, return a structured error so the client can
69
+ // trigger the assistant-driven token setup flow instead of blocking on
70
+ // a vault dialog.
71
71
  if (!useResult.success && useResult.reason?.includes('No credential found')) {
72
- const allowedTools = ['publish_page', 'unpublish_page'];
73
- const secretResult = await requestSecretStandalone(socket, ctx, {
74
- service: 'vercel',
75
- field: 'api_token',
76
- label: 'Vercel API Token',
77
- description: 'Required to publish site apps to the web. Create a token at vercel.com/account/tokens.',
78
- placeholder: 'Enter your Vercel API token',
79
- purpose: 'Publish site apps to the web',
80
- allowedTools,
81
- allowedDomains: ['api.vercel.com'],
82
- });
83
-
84
- if (!secretResult.value) {
85
- ctx.send(socket, {
86
- type: 'publish_page_response',
87
- success: false,
88
- error: 'Cancelled',
89
- });
90
- return;
91
- }
92
-
93
- if (secretResult.delivery === 'transient_send') {
94
- // One-time send: inject for single use without persisting to keychain.
95
- // Metadata must exist for broker policy checks.
96
- if (!getCredentialMetadata('vercel', 'api_token')) {
97
- upsertCredentialMetadata('vercel', 'api_token', { allowedTools });
98
- }
99
- credentialBroker.injectTransient('vercel', 'api_token', secretResult.value);
100
- } else {
101
- // Default: persist to keychain
102
- const storageKey = `credential:vercel:api_token`;
103
- setSecureKey(storageKey, secretResult.value);
104
- upsertCredentialMetadata('vercel', 'api_token', { allowedTools });
105
- }
106
-
107
- // Retry with the newly stored credential
108
- useResult = await credentialBroker.serverUse({
109
- service: 'vercel',
110
- field: 'api_token',
111
- toolName: 'publish_page',
112
- execute: publishExecute,
72
+ ctx.send(socket, {
73
+ type: 'publish_page_response',
74
+ success: false,
75
+ error: 'Vercel API token not configured',
76
+ errorCode: 'credentials_missing',
113
77
  });
78
+ return;
114
79
  }
115
80
 
116
81
  if (useResult.success && useResult.result) {
@@ -17,7 +17,9 @@ import { getAttentionStateByConversationIds } from '../../memory/conversation-at
17
17
  import * as conversationStore from '../../memory/conversation-store.js';
18
18
  import { GENERATING_TITLE, queueGenerateConversationTitle, UNTITLED_FALLBACK } from '../../memory/conversation-title-service.js';
19
19
  import * as externalConversationStore from '../../memory/external-conversation-store.js';
20
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
20
21
  import { routeGuardianReply } from '../../runtime/guardian-reply-router.js';
22
+ import { resolveLocalIpcGuardianContext } from '../../runtime/local-actor-identity.js';
21
23
  import * as pendingInteractions from '../../runtime/pending-interactions.js';
22
24
  import { checkIngressForSecrets } from '../../security/secret-ingress.js';
23
25
  import { compileCustomPatterns, redactSecrets } from '../../security/secret-scanner.js';
@@ -175,6 +177,10 @@ export async function handleUserMessage(
175
177
  conversationId: msg.sessionId,
176
178
  sourceChannel: ipcChannel,
177
179
  });
180
+ // Route prompter-originated events (confirmation_request/secret_request)
181
+ // through the IPC wrapper so pending-interactions + canonical tracking
182
+ // are updated before the message is sent to the client.
183
+ session.updateClient(sendEvent, false);
178
184
  const ipcInterface = parseInterfaceId(msg.interface);
179
185
  if (!ipcInterface) {
180
186
  ctx.send(socket, {
@@ -263,6 +269,7 @@ export async function handleUserMessage(
263
269
  }
264
270
 
265
271
  rlog.info({ source }, 'Processing user message');
272
+ session.emitActivityState('thinking', 'message_dequeued', 'assistant_turn', dispatchRequestId);
266
273
  session.setTurnChannelContext({
267
274
  userMessageChannel: ipcChannel,
268
275
  assistantMessageChannel: ipcChannel,
@@ -271,10 +278,12 @@ export async function handleUserMessage(
271
278
  userMessageInterface: ipcInterface,
272
279
  assistantMessageInterface: ipcInterface,
273
280
  });
274
- session.setAssistantId('self');
275
- // IPC/desktop user IS the guardian default to guardian trust so
276
- // messages are not tagged as unknown provenance.
277
- session.setGuardianContext({ trustClass: 'guardian', sourceChannel: ipcChannel });
281
+ session.setAssistantId(DAEMON_INTERNAL_ASSISTANT_ID);
282
+ // Resolve local IPC actor identity through the same trust pipeline
283
+ // used by HTTP channel ingress. The vellum guardian binding provides
284
+ // the guardianPrincipalId, and resolveGuardianContext classifies the
285
+ // local user as 'guardian' via binding match.
286
+ session.setGuardianContext(resolveLocalIpcGuardianContext(ipcChannel));
278
287
  session.setCommandIntent(null);
279
288
  // Fire-and-forget: don't block the IPC handler so the connection can
280
289
  // continue receiving messages (e.g. cancel, confirmations, or
@@ -599,9 +608,29 @@ export async function handleUserMessage(
599
608
  conversationId: msg.sessionId,
600
609
  pendingRequestIds: pendingRequestIdsForConversation,
601
610
  approvalConversationGenerator: desktopApprovalConversationGenerator,
611
+ emissionContext: {
612
+ source: 'inline_nl',
613
+ causedByRequestId: requestId,
614
+ decisionText: messageText.trim(),
615
+ },
602
616
  });
603
617
 
604
618
  if (routerResult.consumed && routerResult.type !== 'nl_keep_pending') {
619
+ // Success-path emissions (approved/denied) are handled centrally
620
+ // by handleConfirmationResponse (called via the resolver chain).
621
+ // However, stale/failed paths never reach handleConfirmationResponse,
622
+ // so we emit resolved_stale here for those cases.
623
+ if (routerResult.requestId && !routerResult.decisionApplied) {
624
+ session.emitConfirmationStateChanged({
625
+ sessionId: msg.sessionId,
626
+ requestId: routerResult.requestId,
627
+ state: 'resolved_stale',
628
+ source: 'inline_nl',
629
+ causedByRequestId: requestId,
630
+ decisionText: messageText.trim(),
631
+ });
632
+ }
633
+
605
634
  const consumedChannelMeta = {
606
635
  userMessageChannel: ipcChannel,
607
636
  assistantMessageChannel: ipcChannel,
@@ -685,6 +714,19 @@ export async function handleUserMessage(
685
714
  // will see the denial and can re-request the tool if still needed.
686
715
  if (session.hasAnyPendingConfirmation()) {
687
716
  rlog.info('Auto-denying pending confirmation(s) due to new user message');
717
+ // Emit authoritative confirmation state for each auto-denied request
718
+ // before the prompter clears them.
719
+ for (const interaction of pendingInteractions.getByConversation(msg.sessionId)) {
720
+ if (interaction.session === session && interaction.kind === 'confirmation') {
721
+ session.emitConfirmationStateChanged({
722
+ sessionId: msg.sessionId,
723
+ requestId: interaction.requestId,
724
+ state: 'denied',
725
+ source: 'auto_deny',
726
+ causedByRequestId: requestId,
727
+ });
728
+ }
729
+ }
688
730
  session.denyAllPendingConfirmations();
689
731
  // Keep the pending-interaction tracker aligned with the prompter so
690
732
  // stale request IDs are not reused as routing candidates.
@@ -730,6 +772,8 @@ export function handleConfirmationResponse(
730
772
  msg.decision,
731
773
  msg.selectedPattern,
732
774
  msg.selectedScope,
775
+ undefined,
776
+ { source: 'button' },
733
777
  );
734
778
  syncCanonicalStatusFromIpcConfirmationDecision(msg.requestId, msg.decision);
735
779
  pendingInteractions.resolve(msg.requestId);
@@ -909,6 +953,7 @@ export async function handleSessionCreate(
909
953
  conversationId: conversation.id,
910
954
  sourceChannel: transportChannel,
911
955
  });
956
+ session.updateClient(sendEvent, false);
912
957
  session.setTurnChannelContext({
913
958
  userMessageChannel: transportChannel,
914
959
  assistantMessageChannel: transportChannel,
@@ -1154,7 +1199,15 @@ export function handleHistoryRequest(
1154
1199
  surfaceId: s.surfaceId,
1155
1200
  surfaceType: s.surfaceType,
1156
1201
  title: s.title,
1157
- data: {} as Record<string, unknown>,
1202
+ data: {
1203
+ ...(s.surfaceType === 'dynamic_page'
1204
+ ? {
1205
+ ...(s.data.preview ? { preview: s.data.preview } : {}),
1206
+ ...(s.data.appId ? { appId: s.data.appId } : {}),
1207
+ ...(s.data.appType ? { appType: s.data.appType } : {}),
1208
+ }
1209
+ : {}),
1210
+ } as Record<string, unknown>,
1158
1211
  ...(s.actions ? { actions: s.actions } : {}),
1159
1212
  ...(s.display ? { display: s.display } : {}),
1160
1213
  })))
@@ -1251,6 +1304,7 @@ export async function handleRegenerate(
1251
1304
  conversationId: msg.sessionId,
1252
1305
  sourceChannel: regenerateChannel,
1253
1306
  });
1307
+ session.updateClient(sendEvent, false);
1254
1308
  const requestId = uuid();
1255
1309
  session.traceEmitter.emit('request_received', 'Regenerate requested', {
1256
1310
  requestId,