@vellumai/assistant 0.4.33 → 0.4.34

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 (137) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/access-request-decision.test.ts +2 -3
  3. package/src/__tests__/actor-token-service.test.ts +4 -11
  4. package/src/__tests__/approval-primitive.test.ts +0 -45
  5. package/src/__tests__/assistant-id-boundary-guard.test.ts +150 -0
  6. package/src/__tests__/callback-handoff-copy.test.ts +0 -1
  7. package/src/__tests__/channel-approval-routes.test.ts +5 -45
  8. package/src/__tests__/channel-guardian.test.ts +122 -345
  9. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +4 -3
  10. package/src/__tests__/contacts-tools.test.ts +4 -5
  11. package/src/__tests__/conversation-attention-store.test.ts +2 -65
  12. package/src/__tests__/conversation-attention-telegram.test.ts +0 -2
  13. package/src/__tests__/conversation-pairing.test.ts +0 -1
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -2
  15. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -7
  16. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -74
  17. package/src/__tests__/guardian-action-late-reply.test.ts +1 -8
  18. package/src/__tests__/guardian-grant-minting.test.ts +0 -1
  19. package/src/__tests__/guardian-routing-state.test.ts +0 -3
  20. package/src/__tests__/inbound-invite-redemption.test.ts +0 -3
  21. package/src/__tests__/non-member-access-request.test.ts +0 -7
  22. package/src/__tests__/notification-broadcaster.test.ts +1 -2
  23. package/src/__tests__/notification-decision-fallback.test.ts +0 -2
  24. package/src/__tests__/notification-decision-strategy.test.ts +0 -1
  25. package/src/__tests__/relay-server.test.ts +11 -83
  26. package/src/__tests__/scoped-approval-grants.test.ts +9 -40
  27. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -36
  28. package/src/__tests__/send-endpoint-busy.test.ts +0 -1
  29. package/src/__tests__/send-notification-tool.test.ts +0 -1
  30. package/src/__tests__/slack-inbound-verification.test.ts +2 -4
  31. package/src/__tests__/thread-seed-composer.test.ts +0 -1
  32. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  33. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -4
  34. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -5
  35. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +1 -17
  36. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -13
  37. package/src/__tests__/trusted-contact-verification.test.ts +3 -15
  38. package/src/__tests__/twilio-routes.test.ts +2 -2
  39. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  40. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -37
  41. package/src/approvals/approval-primitive.ts +0 -15
  42. package/src/approvals/guardian-decision-primitive.ts +0 -3
  43. package/src/approvals/guardian-request-resolvers.ts +0 -5
  44. package/src/calls/call-domain.ts +0 -3
  45. package/src/calls/call-store.ts +0 -3
  46. package/src/calls/guardian-action-sweep.ts +2 -1
  47. package/src/calls/guardian-dispatch.ts +1 -2
  48. package/src/calls/relay-access-wait.ts +0 -4
  49. package/src/calls/relay-server.ts +3 -11
  50. package/src/calls/relay-setup-router.ts +1 -2
  51. package/src/calls/relay-verification.ts +0 -1
  52. package/src/calls/twilio-routes.ts +0 -3
  53. package/src/calls/types.ts +0 -1
  54. package/src/calls/voice-session-bridge.ts +0 -1
  55. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -1
  56. package/src/contacts/contact-store.ts +13 -88
  57. package/src/contacts/contacts-write.ts +3 -11
  58. package/src/contacts/types.ts +0 -1
  59. package/src/daemon/handlers/config-channels.ts +16 -42
  60. package/src/daemon/handlers/config-inbox.ts +6 -6
  61. package/src/daemon/handlers/contacts.ts +3 -11
  62. package/src/daemon/handlers/index.ts +0 -2
  63. package/src/daemon/session-process.ts +0 -4
  64. package/src/memory/conversation-attention-store.ts +4 -19
  65. package/src/memory/conversation-crud.ts +0 -2
  66. package/src/memory/db-init.ts +4 -0
  67. package/src/memory/guardian-action-store.ts +0 -12
  68. package/src/memory/guardian-approvals.ts +35 -80
  69. package/src/memory/guardian-rate-limits.ts +1 -14
  70. package/src/memory/guardian-verification.ts +6 -34
  71. package/src/memory/invite-store.ts +5 -14
  72. package/src/memory/migrations/134-contacts-notes-column.ts +64 -45
  73. package/src/memory/migrations/136-drop-assistant-id-columns.ts +263 -0
  74. package/src/memory/migrations/index.ts +1 -0
  75. package/src/memory/migrations/registry.ts +14 -1
  76. package/src/memory/schema/calls.ts +0 -7
  77. package/src/memory/schema/contacts.ts +0 -8
  78. package/src/memory/schema/guardian.ts +0 -5
  79. package/src/memory/schema/infrastructure.ts +0 -2
  80. package/src/memory/schema/notifications.ts +3 -17
  81. package/src/memory/scoped-approval-grants.ts +2 -24
  82. package/src/notifications/adapters/sms.ts +2 -1
  83. package/src/notifications/broadcaster.ts +1 -6
  84. package/src/notifications/decision-engine.ts +3 -4
  85. package/src/notifications/deliveries-store.ts +0 -4
  86. package/src/notifications/destination-resolver.ts +4 -6
  87. package/src/notifications/deterministic-checks.ts +1 -6
  88. package/src/notifications/emit-signal.ts +4 -11
  89. package/src/notifications/events-store.ts +7 -17
  90. package/src/notifications/preference-summary.ts +2 -2
  91. package/src/notifications/preferences-store.ts +2 -9
  92. package/src/notifications/signal.ts +0 -1
  93. package/src/notifications/thread-candidates.ts +1 -11
  94. package/src/notifications/types.ts +0 -3
  95. package/src/runtime/access-request-helper.ts +3 -10
  96. package/src/runtime/actor-refresh-token-store.ts +0 -6
  97. package/src/runtime/actor-token-store.ts +3 -16
  98. package/src/runtime/actor-trust-resolver.ts +1 -4
  99. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -9
  100. package/src/runtime/auth/credential-service.ts +1 -15
  101. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  102. package/src/runtime/channel-guardian-service.ts +15 -46
  103. package/src/runtime/channel-invite-transport.ts +8 -0
  104. package/src/runtime/channel-invite-transports/email.ts +4 -0
  105. package/src/runtime/channel-invite-transports/slack.ts +6 -0
  106. package/src/runtime/channel-invite-transports/sms.ts +4 -0
  107. package/src/runtime/channel-invite-transports/telegram.ts +6 -0
  108. package/src/runtime/confirmation-request-guardian-bridge.ts +0 -1
  109. package/src/runtime/guardian-action-followup-executor.ts +3 -2
  110. package/src/runtime/guardian-action-grant-minter.ts +0 -1
  111. package/src/runtime/guardian-outbound-actions.ts +2 -12
  112. package/src/runtime/guardian-vellum-migration.ts +2 -3
  113. package/src/runtime/http-server.ts +0 -1
  114. package/src/runtime/invite-redemption-service.ts +1 -14
  115. package/src/runtime/local-actor-identity.ts +2 -5
  116. package/src/runtime/routes/access-request-decision.ts +0 -1
  117. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +0 -9
  118. package/src/runtime/routes/channel-readiness-routes.ts +29 -18
  119. package/src/runtime/routes/contact-routes.ts +15 -40
  120. package/src/runtime/routes/conversation-attention-routes.ts +0 -2
  121. package/src/runtime/routes/global-search-routes.ts +0 -2
  122. package/src/runtime/routes/guardian-bootstrap-routes.ts +5 -6
  123. package/src/runtime/routes/guardian-expiry-sweep.ts +3 -2
  124. package/src/runtime/routes/inbound-message-handler.ts +0 -3
  125. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +7 -43
  126. package/src/runtime/routes/inbound-stages/background-dispatch.ts +1 -4
  127. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -6
  128. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +0 -1
  129. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +0 -1
  130. package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -7
  131. package/src/runtime/routes/pairing-routes.ts +4 -4
  132. package/src/runtime/tool-grant-request-helper.ts +0 -1
  133. package/src/tools/browser/browser-manager.ts +22 -21
  134. package/src/tools/browser/runtime-check.ts +111 -6
  135. package/src/tools/calls/call-start.ts +1 -3
  136. package/src/tools/followups/followup_create.ts +1 -2
  137. package/src/tools/tool-approval-handler.ts +0 -2
@@ -191,14 +191,8 @@ export async function enforceIngressAcl(
191
191
  // omitted: rebind sessions create a consumable challenge while a
192
192
  // binding already exists, and the identity check inside
193
193
  // validateAndConsumeChallenge prevents unauthorized takeovers.
194
- const hasPendingChallenge = !!getPendingChallenge(
195
- canonicalAssistantId,
196
- sourceChannel,
197
- );
198
- const hasActiveOutboundSession = !!findActiveSession(
199
- canonicalAssistantId,
200
- sourceChannel,
201
- );
194
+ const hasPendingChallenge = !!getPendingChallenge(sourceChannel);
195
+ const hasActiveOutboundSession = !!findActiveSession(sourceChannel);
202
196
  if (hasPendingChallenge || hasActiveOutboundSession) {
203
197
  denyNonMember = false;
204
198
  } else {
@@ -219,7 +213,6 @@ export async function enforceIngressAcl(
219
213
  ).payload as string;
220
214
  const bootstrapTokenForAcl = bootstrapPayload.slice(3); // strip 'gv_' prefix
221
215
  const bootstrapSessionForAcl = resolveBootstrapToken(
222
- canonicalAssistantId,
223
216
  sourceChannel,
224
217
  bootstrapTokenForAcl,
225
218
  );
@@ -299,13 +292,8 @@ export async function enforceIngressAcl(
299
292
  // user can reply with the code in the DM to self-verify.
300
293
  if (sourceChannel === "slack" && (canonicalSenderId ?? rawSenderId)) {
301
294
  const slackVerifyResult = initiateSlackVerificationChallenge({
302
- canonicalAssistantId,
303
295
  sourceChannel,
304
- conversationExternalId,
305
296
  senderUserId: (canonicalSenderId ?? rawSenderId)!,
306
- replyCallbackUrl,
307
- mintBearerToken,
308
- assistantId,
309
297
  });
310
298
 
311
299
  if (slackVerifyResult.initiated) {
@@ -420,14 +408,8 @@ export async function enforceIngressAcl(
420
408
  // revoked/blocked — otherwise the user can never re-verify.
421
409
  let denyInactiveMember = true;
422
410
  if (isGuardianVerifyCode) {
423
- const hasPendingChallenge = !!getPendingChallenge(
424
- canonicalAssistantId,
425
- sourceChannel,
426
- );
427
- const hasActiveOutboundSession = !!findActiveSession(
428
- canonicalAssistantId,
429
- sourceChannel,
430
- );
411
+ const hasPendingChallenge = !!getPendingChallenge(sourceChannel);
412
+ const hasActiveOutboundSession = !!findActiveSession(sourceChannel);
431
413
  if (hasPendingChallenge || hasActiveOutboundSession) {
432
414
  denyInactiveMember = false;
433
415
  } else {
@@ -448,7 +430,6 @@ export async function enforceIngressAcl(
448
430
  ).payload as string;
449
431
  const bootstrapTokenForAcl = bootstrapPayload.slice(3);
450
432
  const bootstrapSessionForAcl = resolveBootstrapToken(
451
- canonicalAssistantId,
452
433
  sourceChannel,
453
434
  bootstrapTokenForAcl,
454
435
  );
@@ -538,13 +519,8 @@ export async function enforceIngressAcl(
538
519
  (canonicalSenderId ?? rawSenderId)
539
520
  ) {
540
521
  const slackVerifyResult = initiateSlackVerificationChallenge({
541
- canonicalAssistantId,
542
522
  sourceChannel,
543
- conversationExternalId,
544
523
  senderUserId: (canonicalSenderId ?? rawSenderId)!,
545
- replyCallbackUrl,
546
- mintBearerToken,
547
- assistantId,
548
524
  });
549
525
 
550
526
  if (slackVerifyResult.initiated) {
@@ -1098,28 +1074,17 @@ interface SlackVerificationResult {
1098
1074
  * creates a trusted contact record (not a guardian binding).
1099
1075
  */
1100
1076
  function initiateSlackVerificationChallenge(params: {
1101
- canonicalAssistantId: string;
1102
1077
  sourceChannel: ChannelId;
1103
- conversationExternalId: string;
1104
1078
  senderUserId: string;
1105
- replyCallbackUrl: string | undefined;
1106
- mintBearerToken: () => string;
1107
- assistantId: string;
1108
1079
  }): SlackVerificationResult {
1109
- const { canonicalAssistantId, sourceChannel, senderUserId } = params;
1080
+ const { sourceChannel, senderUserId } = params;
1110
1081
 
1111
1082
  // Skip if there is already a pending challenge or active session for
1112
1083
  // this sender to avoid flooding them with duplicate codes. We scope by
1113
1084
  // sender identity (expectedExternalUserId) so that a pending session for
1114
1085
  // user A does not suppress challenges for user B.
1115
- const existingChallenge = getPendingChallenge(
1116
- canonicalAssistantId,
1117
- sourceChannel,
1118
- );
1119
- const existingSession = findActiveSession(
1120
- canonicalAssistantId,
1121
- sourceChannel,
1122
- );
1086
+ const existingChallenge = getPendingChallenge(sourceChannel);
1087
+ const existingSession = findActiveSession(sourceChannel);
1123
1088
  const senderHasPending =
1124
1089
  (existingChallenge &&
1125
1090
  existingChallenge.expectedExternalUserId === senderUserId) ||
@@ -1140,7 +1105,6 @@ function initiateSlackVerificationChallenge(params: {
1140
1105
 
1141
1106
  try {
1142
1107
  const session = createOutboundSession({
1143
- assistantId: canonicalAssistantId,
1144
1108
  channel: sourceChannel,
1145
1109
  expectedExternalUserId: senderUserId,
1146
1110
  expectedChatId: senderUserId,
@@ -507,10 +507,7 @@ export function startTrustedContactApprovalNotifier(params: {
507
507
 
508
508
  if (info && !globalNotifiedApprovalRequestIds.has(info.requestId)) {
509
509
  globalNotifiedApprovalRequestIds.set(info.requestId, conversationId);
510
- const guardian = findGuardianForChannel(
511
- sourceChannel,
512
- assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
513
- );
510
+ const guardian = findGuardianForChannel(sourceChannel);
514
511
  const guardianName = resolveGuardianName(
515
512
  guardian?.contact.displayName,
516
513
  );
@@ -74,11 +74,7 @@ export async function handleBootstrapIntercept(
74
74
  }
75
75
 
76
76
  const bootstrapToken = (commandIntent.payload as string).slice(3);
77
- const bootstrapSession = resolveBootstrapToken(
78
- canonicalAssistantId,
79
- sourceChannel,
80
- bootstrapToken,
81
- );
77
+ const bootstrapSession = resolveBootstrapToken(sourceChannel, bootstrapToken);
82
78
 
83
79
  if (!bootstrapSession || bootstrapSession.status !== "pending_bootstrap") {
84
80
  // Not found or expired — fall through to normal /start handling
@@ -94,7 +90,6 @@ export async function handleBootstrapIntercept(
94
90
  // Create a new identity-bound outbound session with a fresh secret.
95
91
  // The old bootstrap session is auto-revoked by createOutboundSession.
96
92
  const newSession = createOutboundSession({
97
- assistantId: canonicalAssistantId,
98
93
  channel: sourceChannel,
99
94
  expectedExternalUserId: rawSenderId,
100
95
  expectedChatId: conversationExternalId,
@@ -134,7 +134,6 @@ export function handleEscalationIntercept(
134
134
  sourceEventName: "ingress.escalation",
135
135
  sourceChannel,
136
136
  sourceSessionId: conversationId,
137
- assistantId: canonicalAssistantId,
138
137
  attentionHints: {
139
138
  requiresAction: true,
140
139
  urgency: "high",
@@ -115,7 +115,6 @@ export function runSecretIngressCheck(params: SecretIngressCheckParams): void {
115
115
  : "User sent media attachment";
116
116
  recordConversationSeenSignal({
117
117
  conversationId,
118
- assistantId: canonicalAssistantId,
119
118
  signalType: "telegram_inbound_message",
120
119
  confidence: "inferred",
121
120
  sourceChannel: "telegram",
@@ -95,8 +95,8 @@ export async function handleVerificationIntercept(
95
95
  // Only intercept when there is a pending challenge or active outbound session
96
96
  const shouldIntercept =
97
97
  guardianVerifyCode !== undefined &&
98
- (!!getPendingChallenge(canonicalAssistantId, sourceChannel) ||
99
- !!findActiveSession(canonicalAssistantId, sourceChannel));
98
+ (!!getPendingChallenge(sourceChannel) ||
99
+ !!findActiveSession(sourceChannel));
100
100
 
101
101
  if (
102
102
  isDuplicate ||
@@ -108,7 +108,6 @@ export async function handleVerificationIntercept(
108
108
  }
109
109
 
110
110
  const verifyResult = validateAndConsumeChallenge(
111
- canonicalAssistantId,
112
111
  sourceChannel,
113
112
  guardianVerifyCode,
114
113
  canonicalSenderId ?? rawSenderId,
@@ -144,7 +143,6 @@ export async function handleVerificationIntercept(
144
143
  : actorDisplayName;
145
144
 
146
145
  upsertMember({
147
- assistantId: canonicalAssistantId,
148
146
  sourceChannel,
149
147
  externalUserId: canonicalSenderId ?? rawSenderId,
150
148
  externalChatId: conversationExternalId,
@@ -181,7 +179,7 @@ export async function handleVerificationIntercept(
181
179
  );
182
180
  } else {
183
181
  // Revoke any existing active binding before creating a new one (same-user re-verification)
184
- revokeGuardianBinding(canonicalAssistantId, sourceChannel);
182
+ revokeGuardianBinding(sourceChannel);
185
183
 
186
184
  const metadata: Record<string, string> = {};
187
185
  if (actorUsername && actorUsername.trim().length > 0) {
@@ -202,7 +200,6 @@ export async function handleVerificationIntercept(
202
200
  rawSenderId;
203
201
 
204
202
  createGuardianBinding({
205
- assistantId: canonicalAssistantId,
206
203
  channel: sourceChannel,
207
204
  guardianExternalUserId: canonicalSenderId ?? rawSenderId,
208
205
  guardianDeliveryChatId: conversationExternalId,
@@ -235,7 +232,6 @@ export async function handleVerificationIntercept(
235
232
  sourceEventName: "ingress.trusted_contact.activated",
236
233
  sourceChannel,
237
234
  sourceSessionId: conversationId,
238
- assistantId: canonicalAssistantId,
239
235
  attentionHints: {
240
236
  requiresAction: false,
241
237
  urgency: "low",
@@ -37,22 +37,22 @@ function mintPairingCredentials(
37
37
  platform: string,
38
38
  ): PairingCredentials | null {
39
39
  try {
40
- const assistantId = DAEMON_INTERNAL_ASSISTANT_ID;
41
40
  // Pairing can run before a local client has touched the actor-token
42
41
  // bootstrap path. Ensure the vellum guardian principal exists so iOS
43
42
  // pairings always have a mint target.
44
- const guardianPrincipalId = ensureVellumGuardianBinding(assistantId);
43
+ const guardianPrincipalId = ensureVellumGuardianBinding(
44
+ DAEMON_INTERNAL_ASSISTANT_ID,
45
+ );
45
46
  const hashedDeviceId = hashDeviceId(deviceId);
46
47
 
47
48
  const credentials = mintCredentialPair({
48
- assistantId,
49
49
  platform,
50
50
  deviceId,
51
51
  guardianPrincipalId,
52
52
  hashedDeviceId,
53
53
  });
54
54
 
55
- log.info({ assistantId, platform }, "Minted credentials during pairing");
55
+ log.info({ platform }, "Minted credentials during pairing");
56
56
  return {
57
57
  accessToken: credentials.accessToken,
58
58
  accessTokenExpiresAt: credentials.accessTokenExpiresAt,
@@ -146,7 +146,6 @@ export function createOrReuseToolGrantRequest(
146
146
  sourceEventName: "guardian.question",
147
147
  sourceChannel,
148
148
  sourceSessionId: conversationId,
149
- assistantId,
150
149
  attentionHints: {
151
150
  requiresAction: true,
152
151
  urgency: "high",
@@ -1,11 +1,11 @@
1
- import { mkdirSync } from "node:fs";
1
+ import { existsSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { getLogger } from "../../util/logger.js";
5
5
  import { getDataDir } from "../../util/platform.js";
6
6
  import { authSessionCache } from "./auth-cache.js";
7
7
  import type { ExtractedCredential } from "./network-recording-types.js";
8
- import { checkBrowserRuntime } from "./runtime-check.js";
8
+ import { importPlaywright } from "./runtime-check.js";
9
9
 
10
10
  const log = getLogger("browser-manager");
11
11
 
@@ -129,20 +129,6 @@ export function setLaunchFn(fn: LaunchFn | null): void {
129
129
  launchPersistentContext = fn;
130
130
  }
131
131
 
132
- async function getDefaultLaunchFn(): Promise<LaunchFn> {
133
- const pw = await import("playwright");
134
- // In compiled Bun binaries, CJS→ESM interop may place named exports
135
- // under .default instead of at the top level of the module namespace.
136
- const chromium =
137
- pw.chromium ?? (pw.default as typeof pw | undefined)?.chromium;
138
- if (!chromium) {
139
- throw new Error(
140
- "Failed to resolve Playwright chromium — the module loaded but 'chromium' is missing",
141
- );
142
- }
143
- return chromium.launchPersistentContext.bind(chromium);
144
- }
145
-
146
132
  function getProfileDir(): string {
147
133
  return join(getDataDir(), "browser-profile");
148
134
  }
@@ -177,10 +163,24 @@ class BrowserManager {
177
163
  this.contextCreating = (async () => {
178
164
  await authSessionCache.load();
179
165
 
180
- // Auto-install Chromium if needed (skip when test launcher is injected)
181
- if (!launchPersistentContext) {
182
- const status = await checkBrowserRuntime();
183
- if (status.playwrightAvailable && !status.chromiumInstalled) {
166
+ // Resolve launch function: use injected test launcher or resolve
167
+ // playwright (may install at runtime in compiled binaries).
168
+ let launch: LaunchFn;
169
+ if (launchPersistentContext) {
170
+ launch = launchPersistentContext;
171
+ } else {
172
+ const pw = await importPlaywright();
173
+
174
+ // Auto-install Chromium if the browser binary is missing
175
+ let chromiumInstalled = false;
176
+ try {
177
+ const execPath = pw.chromium.executablePath();
178
+ chromiumInstalled = existsSync(execPath);
179
+ } catch {
180
+ // executablePath() may throw if registry is missing
181
+ }
182
+
183
+ if (!chromiumInstalled) {
184
184
  log.info("Chromium not installed, installing via playwright...");
185
185
  const proc = Bun.spawn(
186
186
  ["bunx", "playwright", "install", "chromium"],
@@ -213,11 +213,12 @@ class BrowserManager {
213
213
  throw new Error(`Failed to install Chromium: ${msg}`);
214
214
  }
215
215
  }
216
+
217
+ launch = pw.chromium.launchPersistentContext.bind(pw.chromium);
216
218
  }
217
219
 
218
220
  const profileDir = getProfileDir();
219
221
  mkdirSync(profileDir, { recursive: true });
220
- const launch = launchPersistentContext ?? (await getDefaultLaunchFn());
221
222
  const headless = !canDisplayGui();
222
223
  const ctx = await launch(profileDir, { headless });
223
224
  log.info(
@@ -1,4 +1,7 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getWorkspaceDir } from "../../util/platform.js";
2
5
 
3
6
  export interface BrowserRuntimeStatus {
4
7
  playwrightAvailable: boolean;
@@ -7,14 +10,116 @@ export interface BrowserRuntimeStatus {
7
10
  error: string | null;
8
11
  }
9
12
 
13
+ /**
14
+ * Resolve playwright's chromium export from a module namespace object,
15
+ * handling CJS→ESM interop where named exports may land under .default.
16
+ */
17
+ function resolveChromium(
18
+ pw: Record<string, unknown>,
19
+ ): Record<string, unknown> | undefined {
20
+ if (pw.chromium) return pw;
21
+ const def = pw.default as Record<string, unknown> | undefined;
22
+ if (def?.chromium) return def;
23
+ return undefined;
24
+ }
25
+
26
+ /**
27
+ * Try importing playwright from the bundled binary. Returns the module
28
+ * if chromium is resolvable, otherwise undefined. This never installs
29
+ * anything — safe for diagnostic/read-only use.
30
+ */
31
+ async function tryBundledPlaywright(): Promise<
32
+ typeof import("playwright") | undefined
33
+ > {
34
+ try {
35
+ const pw = await import("playwright");
36
+ const mod = resolveChromium(pw as unknown as Record<string, unknown>);
37
+ if (mod?.chromium) return mod as unknown as typeof import("playwright");
38
+ } catch {
39
+ // Bundled import failed entirely
40
+ }
41
+ return undefined;
42
+ }
43
+
44
+ /**
45
+ * Resolve the package entry point from its package.json exports/main fields.
46
+ */
47
+ function resolvePackageEntry(pkgDir: string): string {
48
+ try {
49
+ const pkg = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf-8"));
50
+ // Prefer ESM entry from exports map
51
+ const exportsRoot = pkg.exports?.["."];
52
+ if (typeof exportsRoot === "object" && exportsRoot?.import) {
53
+ return join(pkgDir, exportsRoot.import);
54
+ }
55
+ if (pkg.module) return join(pkgDir, pkg.module);
56
+ if (pkg.main) return join(pkgDir, pkg.main);
57
+ } catch {
58
+ // Fall through to default
59
+ }
60
+ return join(pkgDir, "index.mjs");
61
+ }
62
+
63
+ /**
64
+ * Import playwright, falling back to a runtime-installed copy if the
65
+ * bundled import fails (compiled Bun binaries can't initialize
66
+ * playwright's in-process client/server bridge correctly).
67
+ */
68
+ export async function importPlaywright(): Promise<typeof import("playwright")> {
69
+ // Try bundled import (works in dev/source mode)
70
+ const bundled = await tryBundledPlaywright();
71
+ if (bundled) return bundled;
72
+
73
+ // Compiled binary fallback: install playwright to disk and import
74
+ // from an absolute path so the JS runtime resolves it from the
75
+ // filesystem instead of the compiled module cache.
76
+ const externalDir = join(getWorkspaceDir(), "external");
77
+ const pwPkg = join(externalDir, "node_modules", "playwright");
78
+
79
+ if (!existsSync(join(pwPkg, "package.json"))) {
80
+ mkdirSync(externalDir, { recursive: true });
81
+ if (!existsSync(join(externalDir, "package.json"))) {
82
+ writeFileSync(join(externalDir, "package.json"), '{"private":true}\n');
83
+ }
84
+ const proc = Bun.spawn(["bun", "add", "playwright"], {
85
+ cwd: externalDir,
86
+ stdout: "pipe",
87
+ stderr: "pipe",
88
+ });
89
+ const exitCode = await proc.exited;
90
+ if (exitCode !== 0) {
91
+ const stderr = await new Response(proc.stderr).text();
92
+ throw new Error(`Failed to install playwright: ${stderr}`);
93
+ }
94
+ }
95
+
96
+ // Dynamic import with a runtime-computed path — bun can't statically
97
+ // analyze this, so it resolves from the filesystem at runtime.
98
+ const entryPath = resolvePackageEntry(pwPkg);
99
+ const pw: Record<string, unknown> = await import(entryPath);
100
+ const mod = resolveChromium(pw);
101
+ if (!mod?.chromium) {
102
+ throw new Error(
103
+ "Failed to resolve Playwright chromium from runtime-installed copy",
104
+ );
105
+ }
106
+ return mod as unknown as typeof import("playwright");
107
+ }
108
+
10
109
  export async function checkBrowserRuntime(): Promise<BrowserRuntimeStatus> {
11
- // Check if playwright can be imported
110
+ // Diagnostic only no side effects (no playwright installation)
12
111
  let chromium: { executablePath: () => string };
13
112
  try {
14
- const pw = await import("playwright");
15
- // In compiled Bun binaries, CJS→ESM interop may place named exports
16
- // under .default instead of at the top level of the module namespace.
17
- chromium = pw.chromium ?? (pw.default as typeof pw | undefined)?.chromium;
113
+ const pw = await tryBundledPlaywright();
114
+ if (!pw) {
115
+ return {
116
+ playwrightAvailable: false,
117
+ chromiumInstalled: false,
118
+ chromiumPath: null,
119
+ error: "playwright package not available",
120
+ };
121
+ }
122
+ chromium = pw.chromium;
18
123
  } catch {
19
124
  return {
20
125
  playwrightAvailable: false,
@@ -1,6 +1,5 @@
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";
4
3
  import { findActiveSession } from "../../runtime/channel-guardian-service.js";
5
4
  import { normalizePhoneNumber } from "../../util/phone.js";
6
5
  import type { ToolContext, ToolExecutionResult } from "../types.js";
@@ -22,8 +21,7 @@ export async function executeCallStart(
22
21
  ? normalizePhoneNumber(input.phone_number)
23
22
  : null;
24
23
  if (requestedPhone) {
25
- const assistantId = context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID;
26
- const activeVoiceVerification = findActiveSession(assistantId, "voice");
24
+ const activeVoiceVerification = findActiveSession("voice");
27
25
  const verificationDestination =
28
26
  activeVoiceVerification?.destinationAddress ??
29
27
  activeVoiceVerification?.expectedPhoneE164;
@@ -1,7 +1,6 @@
1
1
  import { getContact } from "../../contacts/contact-store.js";
2
2
  import { createFollowUp } from "../../followups/followup-store.js";
3
3
  import type { FollowUp } from "../../followups/types.js";
4
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../../runtime/assistant-scope.js";
5
4
  import type { ToolContext, ToolExecutionResult } from "../types.js";
6
5
 
7
6
  function formatFollowUp(f: FollowUp): string {
@@ -63,7 +62,7 @@ export async function executeFollowupCreate(
63
62
 
64
63
  // Validate contact exists if provided
65
64
  if (contactId) {
66
- const contact = getContact(contactId, DAEMON_INTERNAL_ASSISTANT_ID);
65
+ const contact = getContact(contactId);
67
66
  if (!contact) {
68
67
  return {
69
68
  content: `Error: Contact "${contactId}" not found`,
@@ -5,7 +5,6 @@ import {
5
5
  updateCanonicalGuardianRequest,
6
6
  } from "../memory/canonical-guardian-store.js";
7
7
  import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
8
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
9
8
  import { createOrReuseToolGrantRequest } from "../runtime/tool-grant-request-helper.js";
10
9
  import { computeToolApprovalDigest } from "../security/tool-approval-digest.js";
11
10
  import { getTaskRunRules } from "../tasks/ephemeral-permissions.js";
@@ -275,7 +274,6 @@ export class ToolApprovalHandler {
275
274
  inputDigest,
276
275
  consumingRequestId:
277
276
  context.requestId ?? `preexec-${context.sessionId}-${Date.now()}`,
278
- assistantId: context.assistantId ?? DAEMON_INTERNAL_ASSISTANT_ID,
279
277
  executionChannel: context.executionChannel,
280
278
  conversationId: context.conversationId,
281
279
  callSessionId: context.callSessionId,