@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
@@ -1,10 +1,9 @@
1
- import { v4 as uuid } from 'uuid';
1
+ import type { ServerMessage } from '../../daemon/ipc-contract.js';
2
+ import { browserManager, SCREENCAST_HEIGHT, SCREENCAST_WIDTH } from './browser-manager.js';
2
3
 
3
- import type { BrowserFrame, BrowserViewSurfaceData, ServerMessage } from '../../daemon/ipc-contract.js';
4
- import { browserManager, SCREENCAST_HEIGHT,SCREENCAST_WIDTH } from './browser-manager.js';
5
-
6
- // Track active screencast sessions
7
- const activeScreencasts = new Map<string, { surfaceId: string }>();
4
+ // Track which sessions have an active browser page (no PiP surface — the user
5
+ // watches the actual browser window directly).
6
+ const activeBrowserSessions = new Set<string>();
8
7
 
9
8
  // Registry of sendToClient callbacks per session
10
9
  const sessionSenders = new Map<string, (msg: ServerMessage) => void>();
@@ -32,118 +31,55 @@ function getSender(sessionId: string): ((msg: ServerMessage) => void) | undefine
32
31
 
33
32
  export async function ensureScreencast(
34
33
  sessionId: string,
35
- sendToClient: (msg: ServerMessage) => void,
34
+ _sendToClient: (msg: ServerMessage) => void,
36
35
  ): Promise<void> {
37
- if (activeScreencasts.has(sessionId)) return;
36
+ if (activeBrowserSessions.has(sessionId)) return;
38
37
 
39
- const surfaceId = uuid();
40
- activeScreencasts.set(sessionId, { surfaceId });
38
+ activeBrowserSessions.add(sessionId);
41
39
 
42
40
  try {
43
- // Get current page info
44
- const page = await browserManager.getOrCreateSessionPage(sessionId);
45
- const currentUrl = page.url();
46
- const title = await page.title();
47
-
48
- // Send surface show
49
- sendToClient({
50
- type: 'ui_surface_show',
51
- sessionId,
52
- surfaceId,
53
- surfaceType: 'browser_view',
54
- title: 'Browser',
55
- data: {
56
- sessionId,
57
- currentUrl: currentUrl || 'about:blank',
58
- status: 'idle',
59
- pages: [{ id: sessionId, title: title || 'New Tab', url: currentUrl || 'about:blank', active: true }],
60
- } satisfies BrowserViewSurfaceData,
61
- display: 'panel',
62
- });
63
-
64
- // Start CDP screencast
65
- await browserManager.startScreencast(sessionId, (frame) => {
66
- sendToClient({
67
- type: 'browser_frame',
68
- sessionId,
69
- surfaceId,
70
- frame: frame.data,
71
- metadata: frame.metadata,
72
- } satisfies BrowserFrame);
73
- });
41
+ // Ensure the page exists (may trigger browser launch/connect)
42
+ await browserManager.getOrCreateSessionPage(sessionId);
43
+
44
+ // No PiP surface or CDP screencast — the user watches the actual
45
+ // browser window directly (positioned in top-right via positionWindowSidebar).
74
46
  } catch (err) {
75
- // Dismiss the surface we already showed so the client doesn't have an orphaned panel
76
- sendToClient({
77
- type: 'ui_surface_dismiss',
78
- sessionId,
79
- surfaceId,
80
- });
81
47
  // Roll back so future calls can retry
82
- activeScreencasts.delete(sessionId);
48
+ activeBrowserSessions.delete(sessionId);
83
49
  throw err;
84
50
  }
85
51
  }
86
52
 
87
53
  export function updateBrowserStatus(
88
54
  sessionId: string,
89
- sendToClient: (msg: ServerMessage) => void,
90
- status: 'navigating' | 'idle' | 'interacting',
91
- actionText?: string,
92
- currentUrl?: string,
55
+ _sendToClient: (msg: ServerMessage) => void,
56
+ _status: 'navigating' | 'idle' | 'interacting',
57
+ _actionText?: string,
58
+ _currentUrl?: string,
93
59
  ): void {
94
- const state = activeScreencasts.get(sessionId);
95
- if (!state) return;
96
-
97
- const update: Record<string, unknown> = { status };
98
- if (actionText !== undefined) update.actionText = actionText;
99
- if (currentUrl !== undefined) update.currentUrl = currentUrl;
100
-
101
- sendToClient({
102
- type: 'ui_surface_update',
103
- sessionId,
104
- surfaceId: state.surfaceId,
105
- data: update,
106
- });
60
+ // No-op: PiP surface was removed so there is no ui_surface to update.
61
+ // The function signature is preserved to avoid churn at callsites.
62
+ if (!activeBrowserSessions.has(sessionId)) return;
107
63
  }
108
64
 
109
65
  export async function updatePagesList(
110
66
  sessionId: string,
111
- sendToClient: (msg: ServerMessage) => void,
67
+ _sendToClient: (msg: ServerMessage) => void,
112
68
  ): Promise<void> {
113
- const state = activeScreencasts.get(sessionId);
114
- if (!state) return;
115
-
116
- const page = await browserManager.getOrCreateSessionPage(sessionId);
117
- const currentUrl = page.url();
118
- const title = await page.title();
119
-
120
- sendToClient({
121
- type: 'ui_surface_update',
122
- sessionId,
123
- surfaceId: state.surfaceId,
124
- data: {
125
- currentUrl,
126
- pages: [{ id: sessionId, title: title || 'Untitled', url: currentUrl, active: true }],
127
- },
128
- });
69
+ // No-op: PiP surface was removed so there is no ui_surface to update.
70
+ if (!activeBrowserSessions.has(sessionId)) return;
129
71
  }
130
72
 
131
73
  export async function stopBrowserScreencast(
132
74
  sessionId: string,
133
- sendToClient: (msg: ServerMessage) => void,
75
+ _sendToClient: (msg: ServerMessage) => void,
134
76
  ): Promise<void> {
135
- const state = activeScreencasts.get(sessionId);
136
- if (!state) return;
77
+ if (!activeBrowserSessions.has(sessionId)) return;
137
78
 
79
+ // Safe no-op if CDP screencast was never started
138
80
  await browserManager.stopScreencast(sessionId);
139
81
 
140
- sendToClient({
141
- type: 'ui_surface_dismiss',
142
- sessionId,
143
- surfaceId: state.surfaceId,
144
- });
145
-
146
- activeScreencasts.delete(sessionId);
82
+ activeBrowserSessions.delete(sessionId);
147
83
  }
148
84
 
149
85
  export async function getElementBounds(
@@ -175,44 +111,24 @@ export async function getElementBounds(
175
111
 
176
112
  export function updateHighlights(
177
113
  sessionId: string,
178
- sendToClient: (msg: ServerMessage) => void,
179
- highlights: Array<{ x: number; y: number; w: number; h: number; label: string }>,
114
+ _sendToClient: (msg: ServerMessage) => void,
115
+ _highlights: Array<{ x: number; y: number; w: number; h: number; label: string }>,
180
116
  ): void {
181
- const state = activeScreencasts.get(sessionId);
182
- if (!state) return;
183
- sendToClient({
184
- type: 'ui_surface_update',
185
- sessionId,
186
- surfaceId: state.surfaceId,
187
- data: { highlights },
188
- });
117
+ // No-op: PiP surface was removed so there is no ui_surface to update.
118
+ if (!activeBrowserSessions.has(sessionId)) return;
189
119
  }
190
120
 
191
121
  export async function stopAllScreencasts(): Promise<void> {
192
- const entries = Array.from(activeScreencasts.entries());
193
- for (const [sessionId, state] of entries) {
122
+ for (const sessionId of activeBrowserSessions) {
194
123
  try {
195
124
  await browserManager.stopScreencast(sessionId);
196
125
  } catch { /* best-effort */ }
197
- const sender = sessionSenders.get(sessionId);
198
- if (sender) {
199
- sender({
200
- type: 'ui_surface_dismiss',
201
- sessionId,
202
- surfaceId: state.surfaceId,
203
- });
204
- }
205
126
  }
206
- activeScreencasts.clear();
127
+ activeBrowserSessions.clear();
207
128
  }
208
129
 
209
130
  export function isScreencastActive(sessionId: string): boolean {
210
- return activeScreencasts.has(sessionId);
131
+ return activeBrowserSessions.has(sessionId);
211
132
  }
212
133
 
213
134
  export { getSender };
214
-
215
- export function getScreencastSurfaceId(sessionId: string): string | null {
216
- const state = activeScreencasts.get(sessionId);
217
- return state?.surfaceId ?? null;
218
- }
@@ -1,5 +1,6 @@
1
1
  import { startCall } from '../../calls/call-domain.js';
2
2
  import { getConfig } from '../../config/loader.js';
3
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
3
4
  import { findActiveSession } from '../../runtime/channel-guardian-service.js';
4
5
  import { normalizePhoneNumber } from '../../util/phone.js';
5
6
  import type { ToolContext, ToolExecutionResult } from '../types.js';
@@ -16,7 +17,7 @@ export async function executeCallStart(
16
17
  ? normalizePhoneNumber(input.phone_number)
17
18
  : null;
18
19
  if (requestedPhone) {
19
- const assistantId = context.assistantId ?? 'self';
20
+ const assistantId = context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
20
21
  const activeVoiceVerification = findActiveSession(assistantId, 'voice');
21
22
  const verificationDestination = activeVoiceVerification?.destinationAddress ?? activeVoiceVerification?.expectedPhoneE164;
22
23
  if (verificationDestination === requestedPhone) {
@@ -207,9 +207,14 @@ export class PermissionChecker {
207
207
  }
208
208
 
209
209
  if (response.decision === 'always_deny') {
210
- const ruleSaved = !!(persistentDecisionsAllowed && response.selectedPattern && response.selectedScope);
210
+ // For non-scoped tools (empty scopeOptions), default to 'everywhere' since
211
+ // the client has no scope picker and will send undefined.
212
+ const effectiveDenyScope = scopeOptions.length === 0
213
+ ? (response.selectedScope ?? 'everywhere')
214
+ : response.selectedScope;
215
+ const ruleSaved = !!(persistentDecisionsAllowed && response.selectedPattern && effectiveDenyScope);
211
216
  if (ruleSaved) {
212
- addRule(name, response.selectedPattern!, response.selectedScope!, 'deny');
217
+ addRule(name, response.selectedPattern!, effectiveDenyScope!, 'deny');
213
218
  }
214
219
  const denialReason = ruleSaved ? 'Permission denied by user (rule saved)' : 'Permission denied by user';
215
220
  const denialMessage = ruleSaved
@@ -237,7 +242,6 @@ export class PermissionChecker {
237
242
  persistentDecisionsAllowed
238
243
  && (response.decision === 'always_allow' || response.decision === 'always_allow_high_risk')
239
244
  && response.selectedPattern
240
- && response.selectedScope
241
245
  ) {
242
246
  const ruleOptions: {
243
247
  allowHighRisk?: boolean;
@@ -253,7 +257,14 @@ export class PermissionChecker {
253
257
  }
254
258
 
255
259
  const hasOptions = Object.keys(ruleOptions).length > 0;
256
- addRule(name, response.selectedPattern, response.selectedScope, 'allow', 100, hasOptions ? ruleOptions : undefined);
260
+ // Only default to 'everywhere' for non-scoped tools (empty scopeOptions).
261
+ // For scoped tools, require an explicit scope to prevent silent permission widening.
262
+ const effectiveScope = scopeOptions.length === 0
263
+ ? (response.selectedScope ?? 'everywhere')
264
+ : response.selectedScope;
265
+ if (effectiveScope) {
266
+ addRule(name, response.selectedPattern, effectiveScope, 'allow', 100, hasOptions ? ruleOptions : undefined);
267
+ }
257
268
  }
258
269
 
259
270
  return { allowed: true, decision, riskLevel };
@@ -132,6 +132,18 @@ function findWasmPath(pkg: string, file: string): string {
132
132
  return execDirPath;
133
133
  }
134
134
 
135
+ // Use module resolution to find the package. This handles hoisted
136
+ // dependencies (e.g. global bun installs where web-tree-sitter is at the
137
+ // top-level node_modules rather than nested under @vellumai/assistant).
138
+ try {
139
+ const resolved = require.resolve(`${pkg}/package.json`);
140
+ const pkgDir = dirname(resolved);
141
+ const resolvedPath = join(pkgDir, file);
142
+ if (existsSync(resolvedPath)) return resolvedPath;
143
+ } catch (err) {
144
+ log.warn({ err, pkg, file }, 'require.resolve failed for WASM package, falling back to manual resolution');
145
+ }
146
+
135
147
  const sourcePath = join(dir, '..', '..', '..', 'node_modules', pkg, file);
136
148
 
137
149
  if (existsSync(sourcePath)) return sourcePath;
@@ -1,4 +1,6 @@
1
1
  import { consumeGrantForInvocation } from '../approvals/approval-primitive.js';
2
+ import { getCanonicalGuardianRequest, updateCanonicalGuardianRequest } from '../memory/canonical-guardian-store.js';
3
+ import { DAEMON_INTERNAL_ASSISTANT_ID } from '../runtime/assistant-scope.js';
2
4
  import { createOrReuseToolGrantRequest } from '../runtime/tool-grant-request-helper.js';
3
5
  import { computeToolApprovalDigest } from '../security/tool-approval-digest.js';
4
6
  import { getTaskRunRules } from '../tasks/ephemeral-permissions.js';
@@ -10,6 +12,112 @@ import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifec
10
12
 
11
13
  const log = getLogger('tool-approval-handler');
12
14
 
15
+ /** Default polling interval for inline grant wait (ms). */
16
+ export const TC_GRANT_WAIT_INTERVAL_MS = 500;
17
+ /** Default maximum wait time for inline grant wait (ms). */
18
+ export const TC_GRANT_WAIT_MAX_MS = 60_000;
19
+
20
+ /**
21
+ * Inline wait result for trusted-contact grant polling.
22
+ * - `granted`: a grant was minted and consumed within the wait window.
23
+ * - `denied`: the guardian explicitly rejected the request.
24
+ * - `timeout`: the wait budget expired without a decision.
25
+ * - `aborted`: the session was cancelled during the wait.
26
+ * - `escalation_failed`: the grant request could not be created.
27
+ */
28
+ export type InlineGrantWaitOutcome =
29
+ | { outcome: 'granted'; grant: { id: string } }
30
+ | { outcome: 'denied'; requestId: string }
31
+ | { outcome: 'timeout'; requestId: string }
32
+ | { outcome: 'aborted' }
33
+ | { outcome: 'escalation_failed'; reason: string };
34
+
35
+ /**
36
+ * Wait bounded for a guardian to approve a tool grant request and for the
37
+ * resulting grant to become consumable. Polls both the canonical request
38
+ * status (to detect early rejection) and the grant store (to detect approval
39
+ * and atomically consume the grant).
40
+ *
41
+ * Only called for trusted_contact actors with valid guardian bindings.
42
+ */
43
+ export async function waitForInlineGrant(
44
+ escalationRequestId: string,
45
+ consumeParams: Parameters<typeof consumeGrantForInvocation>[0],
46
+ options?: { maxWaitMs?: number; intervalMs?: number; signal?: AbortSignal },
47
+ ): Promise<InlineGrantWaitOutcome> {
48
+ const maxWait = options?.maxWaitMs ?? TC_GRANT_WAIT_MAX_MS;
49
+ const interval = options?.intervalMs ?? TC_GRANT_WAIT_INTERVAL_MS;
50
+ const signal = options?.signal;
51
+ const deadline = Date.now() + maxWait;
52
+
53
+ log.info(
54
+ {
55
+ event: 'tc_inline_grant_wait_start',
56
+ escalationRequestId,
57
+ toolName: consumeParams.toolName,
58
+ maxWaitMs: maxWait,
59
+ intervalMs: interval,
60
+ },
61
+ 'Starting inline wait for guardian grant decision',
62
+ );
63
+
64
+ while (Date.now() < deadline) {
65
+ if (signal?.aborted) {
66
+ return { outcome: 'aborted' };
67
+ }
68
+
69
+ await new Promise((resolve) => setTimeout(resolve, interval));
70
+
71
+ if (signal?.aborted) {
72
+ return { outcome: 'aborted' };
73
+ }
74
+
75
+ // Check if the canonical request was rejected — exit early without
76
+ // waiting for the full timeout.
77
+ const request = getCanonicalGuardianRequest(escalationRequestId);
78
+ if (request && request.status === 'denied') {
79
+ log.info(
80
+ {
81
+ event: 'tc_inline_grant_wait_denied',
82
+ escalationRequestId,
83
+ toolName: consumeParams.toolName,
84
+ elapsedMs: maxWait - (deadline - Date.now()),
85
+ },
86
+ 'Guardian denied tool grant request during inline wait',
87
+ );
88
+ return { outcome: 'denied', requestId: escalationRequestId };
89
+ }
90
+
91
+ // Try to consume the grant — if the guardian approved, the canonical
92
+ // decision primitive will have minted a scoped grant by now.
93
+ const grantResult = await consumeGrantForInvocation(consumeParams, { maxWaitMs: 0 });
94
+ if (grantResult.ok) {
95
+ log.info(
96
+ {
97
+ event: 'tc_inline_grant_wait_granted',
98
+ escalationRequestId,
99
+ toolName: consumeParams.toolName,
100
+ grantId: grantResult.grant.id,
101
+ elapsedMs: maxWait - (deadline - Date.now()),
102
+ },
103
+ 'Grant found during inline wait — tool execution proceeding',
104
+ );
105
+ return { outcome: 'granted', grant: { id: grantResult.grant.id } };
106
+ }
107
+ }
108
+
109
+ log.info(
110
+ {
111
+ event: 'tc_inline_grant_wait_timeout',
112
+ escalationRequestId,
113
+ toolName: consumeParams.toolName,
114
+ maxWaitMs: maxWait,
115
+ },
116
+ 'Inline grant wait timed out — no guardian decision within budget',
117
+ );
118
+ return { outcome: 'timeout', requestId: escalationRequestId };
119
+ }
120
+
13
121
  function isUntrustedGuardianTrustClass(role: ToolContext['guardianTrustClass']): boolean {
14
122
  return role === 'trusted_contact' || role === 'unknown';
15
123
  }
@@ -39,12 +147,26 @@ export type PreExecutionGateResult =
39
147
  | { allowed: true; tool: Tool; grantConsumed?: boolean }
40
148
  | { allowed: false; result: ToolExecutionResult };
41
149
 
150
+ /** Configuration for the inline grant wait behavior. */
151
+ export interface InlineGrantWaitConfig {
152
+ /** Maximum time to wait for guardian approval (ms). Defaults to TC_GRANT_WAIT_MAX_MS. */
153
+ maxWaitMs?: number;
154
+ /** Polling interval during the wait (ms). Defaults to TC_GRANT_WAIT_INTERVAL_MS. */
155
+ intervalMs?: number;
156
+ }
157
+
42
158
  /**
43
159
  * Handles pre-execution approval gates: abort checks, guardian policy,
44
160
  * allowed-tool-set gating, and task-run preflight checks.
45
161
  * These run before the interactive permission prompt flow.
46
162
  */
47
163
  export class ToolApprovalHandler {
164
+ private inlineGrantWaitConfig: InlineGrantWaitConfig;
165
+
166
+ constructor(config?: { inlineGrantWait?: InlineGrantWaitConfig }) {
167
+ this.inlineGrantWaitConfig = config?.inlineGrantWait ?? {};
168
+ }
169
+
48
170
  /**
49
171
  * Evaluate all pre-execution approval gates for a tool invocation.
50
172
  * Returns the resolved Tool if all gates pass, or an early-return
@@ -128,7 +250,7 @@ export class ToolApprovalHandler {
128
250
  toolName: name,
129
251
  inputDigest,
130
252
  consumingRequestId: context.requestId ?? `preexec-${context.sessionId}-${Date.now()}`,
131
- assistantId: context.assistantId ?? 'self',
253
+ assistantId: context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
132
254
  executionChannel: context.executionChannel,
133
255
  conversationId: context.conversationId,
134
256
  callSessionId: context.callSessionId,
@@ -266,12 +388,15 @@ export class ToolApprovalHandler {
266
388
  return { allowed: false, result: { content: 'Cancelled', isError: true } };
267
389
  }
268
390
 
269
- // No matching grant or race condition — deny.
391
+ // No matching grant or race condition — deny or wait inline.
270
392
  //
271
- // For verified non-guardian actors with sufficient context, escalate to
272
- // the guardian by creating a canonical tool_grant_request. Unverified
273
- // actors remain fail-closed with no escalation.
274
- let escalationMessage: string | undefined;
393
+ // For verified non-guardian actors (trusted_contact) with sufficient
394
+ // context, escalate to the guardian by creating a canonical
395
+ // tool_grant_request. Then wait bounded for the grant to become
396
+ // available this lets the tool call succeed inline after guardian
397
+ // approval without the requester having to retry manually.
398
+ //
399
+ // Unverified actors remain fail-closed with no escalation or wait.
275
400
  if (
276
401
  context.guardianTrustClass === 'trusted_contact'
277
402
  && context.assistantId
@@ -291,24 +416,124 @@ export class ToolApprovalHandler {
291
416
  questionText: `Trusted contact is requesting permission to use "${name}"`,
292
417
  });
293
418
 
294
- if ('created' in escalation) {
295
- const codeSuffix = escalation.requestCode
296
- ? ` (request code: ${escalation.requestCode})`
297
- : '';
298
- escalationMessage = `Permission denied for "${name}": this action requires guardian approval. `
299
- + `A request has been sent to the guardian${codeSuffix}. `
300
- + `Please retry after the guardian approves.`;
301
- } else if ('deduped' in escalation) {
419
+ // Only wait inline if the escalation succeeded (created or deduped).
420
+ // If escalation failed (no binding, missing identity), fall through
421
+ // to the generic denial path.
422
+ if ('created' in escalation || 'deduped' in escalation) {
423
+ // Stamp the canonical request so the approval resolver knows an
424
+ // inline consumer is waiting. Without this, the resolver would
425
+ // send a stale "please retry" notification even though the
426
+ // original invocation is about to resume inline.
427
+ updateCanonicalGuardianRequest(escalation.requestId, {
428
+ followupState: 'inline_wait_active:' + Date.now(),
429
+ });
430
+
431
+ const waitResult = await waitForInlineGrant(
432
+ escalation.requestId,
433
+ deferredConsumeParams!,
434
+ {
435
+ maxWaitMs: this.inlineGrantWaitConfig.maxWaitMs,
436
+ intervalMs: this.inlineGrantWaitConfig.intervalMs,
437
+ signal: context.signal,
438
+ },
439
+ );
440
+
441
+ if (waitResult.outcome === 'granted') {
442
+ // Clear the inline-wait stamp now that the grant has been consumed.
443
+ updateCanonicalGuardianRequest(escalation.requestId, {
444
+ followupState: null,
445
+ });
446
+ log.info({
447
+ toolName: name,
448
+ sessionId: context.sessionId,
449
+ conversationId: context.conversationId,
450
+ trustClass: context.guardianTrustClass,
451
+ executionTarget,
452
+ grantId: waitResult.grant.id,
453
+ escalationRequestId: escalation.requestId,
454
+ }, 'Inline grant wait succeeded — allowing trusted contact tool invocation');
455
+ return { allowed: true, tool, grantConsumed: true };
456
+ }
457
+
458
+ if (waitResult.outcome === 'aborted') {
459
+ // Clear the inline-wait stamp so a later guardian approval
460
+ // (if the request is still pending) will send the retry notification.
461
+ updateCanonicalGuardianRequest(escalation.requestId, {
462
+ followupState: null,
463
+ });
464
+ const durationMs = Date.now() - startTime;
465
+ emitLifecycleEvent({
466
+ type: 'error',
467
+ toolName: name,
468
+ executionTarget,
469
+ input,
470
+ workingDir: context.workingDir,
471
+ sessionId: context.sessionId,
472
+ conversationId: context.conversationId,
473
+ requestId: context.requestId,
474
+ riskLevel,
475
+ decision: 'error',
476
+ durationMs,
477
+ errorMessage: 'Cancelled',
478
+ isExpected: true,
479
+ errorCategory: 'tool_failure',
480
+ });
481
+ return { allowed: false, result: { content: 'Cancelled', isError: true } };
482
+ }
483
+
484
+ // Clear the inline-wait stamp so a later guardian approval
485
+ // (if the request is still pending after timeout) will send
486
+ // the retry notification as expected.
487
+ updateCanonicalGuardianRequest(escalation.requestId, {
488
+ followupState: null,
489
+ });
490
+
302
491
  const codeSuffix = escalation.requestCode
303
492
  ? ` (request code: ${escalation.requestCode})`
304
493
  : '';
305
- escalationMessage = `Permission denied for "${name}": guardian approval is already pending${codeSuffix}. `
306
- + `Please retry after the guardian approves.`;
494
+
495
+ let escalationMessage: string;
496
+ if (waitResult.outcome === 'denied') {
497
+ escalationMessage = `Permission denied for "${name}": the guardian rejected the request${codeSuffix}.`;
498
+ } else {
499
+ // timeout
500
+ escalationMessage = `Permission denied for "${name}": guardian approval was not received in time${codeSuffix}. `
501
+ + `Please retry after the guardian approves.`;
502
+ }
503
+
504
+ log.warn({
505
+ toolName: name,
506
+ sessionId: context.sessionId,
507
+ conversationId: context.conversationId,
508
+ trustClass: context.guardianTrustClass,
509
+ executionTarget,
510
+ reason: 'guardian_approval_required',
511
+ grantMissReason: grantResult.reason,
512
+ waitOutcome: waitResult.outcome,
513
+ escalationRequestId: escalation.requestId,
514
+ }, 'Inline grant wait ended without approval — denying trusted contact tool invocation');
515
+ const durationMs = Date.now() - startTime;
516
+ emitLifecycleEvent({
517
+ type: 'permission_denied',
518
+ toolName: name,
519
+ executionTarget,
520
+ input,
521
+ workingDir: context.workingDir,
522
+ sessionId: context.sessionId,
523
+ conversationId: context.conversationId,
524
+ requestId: context.requestId,
525
+ riskLevel,
526
+ decision: 'deny',
527
+ reason: escalationMessage,
528
+ durationMs,
529
+ });
530
+ return { allowed: false, result: { content: escalationMessage, isError: true } };
307
531
  }
308
- // If escalation.failed, fall through to generic denial message.
532
+ // escalation.failed fall through to generic denial.
309
533
  }
310
534
 
311
- const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.guardianTrustClass, name);
535
+ // Unknown/unverified actors or escalation failures — generic denial.
536
+ const reason = guardianApprovalDeniedMessage(context.guardianTrustClass, name);
312
537
  log.warn({
313
538
  toolName: name,
314
539
  sessionId: context.sessionId,
@@ -317,7 +542,7 @@ export class ToolApprovalHandler {
317
542
  executionTarget,
318
543
  reason: 'guardian_approval_required',
319
544
  grantMissReason: grantResult.reason,
320
- escalated: !!escalationMessage,
545
+ escalated: false,
321
546
  }, 'Guardian approval gate blocked untrusted actor tool invocation (no matching grant)');
322
547
  const durationMs = Date.now() - startTime;
323
548
  emitLifecycleEvent({
@@ -16,6 +16,11 @@ const log = getLogger('workspace-git');
16
16
  * Strips all GIT_* env vars (e.g. GIT_DIR, GIT_WORK_TREE) that CI runners
17
17
  * or parent processes may set, then adds GIT_CEILING_DIRECTORIES to prevent
18
18
  * walking up to a parent repo.
19
+ *
20
+ * On macOS, augments PATH with common binary directories so the real git
21
+ * binary is found even when the daemon is launched from a .app bundle with
22
+ * a minimal PATH. Without this, the macOS /usr/bin/git shim triggers an
23
+ * "Install Command Line Developer Tools" popup on every git invocation.
19
24
  */
20
25
  function cleanGitEnv(workspaceDir: string): Record<string, string> {
21
26
  const env: Record<string, string> = {};
@@ -25,6 +30,20 @@ function cleanGitEnv(workspaceDir: string): Record<string, string> {
25
30
  }
26
31
  }
27
32
  env.GIT_CEILING_DIRECTORIES = workspaceDir;
33
+
34
+ const home = process.env.HOME ?? '';
35
+ const extraDirs = [
36
+ '/opt/homebrew/bin',
37
+ '/usr/local/bin',
38
+ `${home}/.local/bin`,
39
+ ];
40
+ const currentPath = env.PATH ?? '';
41
+ const pathDirs = currentPath.split(':');
42
+ const missing = extraDirs.filter(d => !pathDirs.includes(d));
43
+ if (missing.length > 0) {
44
+ env.PATH = [...missing, currentPath].filter(Boolean).join(':');
45
+ }
46
+
28
47
  return env;
29
48
  }
30
49