@vellumai/assistant 0.3.28 → 0.4.0

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 (199) hide show
  1. package/ARCHITECTURE.md +33 -3
  2. package/bun.lock +4 -1
  3. package/docs/trusted-contact-access.md +9 -2
  4. package/package.json +6 -3
  5. package/scripts/ipc/generate-swift.ts +3 -3
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  7. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +13 -5
  9. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  10. package/src/__tests__/asset-search-tool.test.ts +2 -0
  11. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  12. package/src/__tests__/attachments-store.test.ts +2 -0
  13. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  14. package/src/__tests__/call-controller.test.ts +30 -29
  15. package/src/__tests__/call-routes-http.test.ts +34 -32
  16. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  17. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  18. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  19. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  20. package/src/__tests__/clarification-resolver.test.ts +2 -0
  21. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  22. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  24. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  25. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  26. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  27. package/src/__tests__/config-schema.test.ts +5 -5
  28. package/src/__tests__/config-watcher.test.ts +3 -1
  29. package/src/__tests__/connection-policy.test.ts +14 -5
  30. package/src/__tests__/contacts-tools.test.ts +3 -1
  31. package/src/__tests__/contradiction-checker.test.ts +2 -0
  32. package/src/__tests__/conversation-pairing.test.ts +10 -0
  33. package/src/__tests__/conversation-routes.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  35. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  36. package/src/__tests__/credential-vault.test.ts +5 -4
  37. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  38. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  39. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  40. package/src/__tests__/encrypted-store.test.ts +10 -5
  41. package/src/__tests__/followup-tools.test.ts +3 -1
  42. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  43. package/src/__tests__/gmail-integration.test.ts +0 -1
  44. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  45. package/src/__tests__/guardian-dispatch.test.ts +2 -0
  46. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  47. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  48. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  49. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  50. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  51. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  52. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  53. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  55. package/src/__tests__/heartbeat-service.test.ts +20 -0
  56. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  57. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  58. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  59. package/src/__tests__/intent-routing.test.ts +2 -0
  60. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  61. package/src/__tests__/media-generate-image.test.ts +21 -0
  62. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  63. package/src/__tests__/memory-regressions.test.ts +20 -20
  64. package/src/__tests__/non-member-access-request.test.ts +183 -9
  65. package/src/__tests__/notification-decision-fallback.test.ts +2 -0
  66. package/src/__tests__/notification-decision-strategy.test.ts +61 -0
  67. package/src/__tests__/notification-guardian-path.test.ts +2 -0
  68. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  69. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  70. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  71. package/src/__tests__/pairing-routes.test.ts +171 -0
  72. package/src/__tests__/playbook-execution.test.ts +3 -1
  73. package/src/__tests__/playbook-tools.test.ts +3 -1
  74. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  75. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  76. package/src/__tests__/recording-handler.test.ts +11 -0
  77. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  78. package/src/__tests__/recording-state-machine.test.ts +13 -2
  79. package/src/__tests__/registry.test.ts +7 -3
  80. package/src/__tests__/relay-server.test.ts +148 -28
  81. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  82. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  83. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  84. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  85. package/src/__tests__/schedule-tools.test.ts +3 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  87. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  88. package/src/__tests__/session-agent-loop.test.ts +16 -0
  89. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  90. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  91. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  92. package/src/__tests__/session-profile-injection.test.ts +21 -0
  93. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  94. package/src/__tests__/session-queue.test.ts +23 -0
  95. package/src/__tests__/session-runtime-assembly.test.ts +50 -12
  96. package/src/__tests__/session-skill-tools.test.ts +27 -5
  97. package/src/__tests__/session-slash-known.test.ts +23 -0
  98. package/src/__tests__/session-slash-queue.test.ts +23 -0
  99. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  100. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  101. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  102. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  103. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  104. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  105. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  107. package/src/__tests__/skills.test.ts +8 -4
  108. package/src/__tests__/slack-channel-config.test.ts +3 -1
  109. package/src/__tests__/subagent-tools.test.ts +19 -0
  110. package/src/__tests__/swarm-recursion.test.ts +2 -0
  111. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  112. package/src/__tests__/swarm-tool.test.ts +2 -0
  113. package/src/__tests__/system-prompt.test.ts +3 -1
  114. package/src/__tests__/task-compiler.test.ts +3 -1
  115. package/src/__tests__/task-management-tools.test.ts +3 -1
  116. package/src/__tests__/task-tools.test.ts +3 -1
  117. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  118. package/src/__tests__/terminal-tools.test.ts +2 -0
  119. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  120. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  121. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  122. package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
  123. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  124. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  125. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  126. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  127. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  128. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  129. package/src/__tests__/view-image-tool.test.ts +3 -1
  130. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  131. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  132. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  133. package/src/__tests__/work-item-output.test.ts +3 -1
  134. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  135. package/src/calls/call-controller.ts +26 -23
  136. package/src/calls/guardian-action-sweep.ts +10 -2
  137. package/src/calls/relay-server.ts +216 -27
  138. package/src/calls/types.ts +1 -1
  139. package/src/calls/voice-session-bridge.ts +3 -3
  140. package/src/cli.ts +12 -0
  141. package/src/config/agent-schema.ts +14 -3
  142. package/src/config/calls-schema.ts +6 -6
  143. package/src/config/core-schema.ts +3 -3
  144. package/src/config/feature-flag-registry.json +8 -0
  145. package/src/config/mcp-schema.ts +1 -1
  146. package/src/config/memory-schema.ts +27 -19
  147. package/src/config/schema.ts +21 -21
  148. package/src/config/skills-schema.ts +7 -7
  149. package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
  150. package/src/daemon/handlers/config-inbox.ts +4 -4
  151. package/src/daemon/handlers/sessions.ts +148 -4
  152. package/src/daemon/ipc-contract/messages.ts +16 -0
  153. package/src/daemon/ipc-contract-inventory.json +1 -0
  154. package/src/daemon/lifecycle.ts +19 -0
  155. package/src/daemon/pairing-store.ts +86 -3
  156. package/src/daemon/session-agent-loop.ts +5 -5
  157. package/src/daemon/session-lifecycle.ts +25 -17
  158. package/src/daemon/session-memory.ts +2 -2
  159. package/src/daemon/session-process.ts +1 -20
  160. package/src/daemon/session-runtime-assembly.ts +28 -22
  161. package/src/daemon/session-tool-setup.ts +2 -2
  162. package/src/daemon/session.ts +3 -3
  163. package/src/memory/canonical-guardian-store.ts +63 -1
  164. package/src/memory/channel-guardian-store.ts +1 -0
  165. package/src/memory/conversation-crud.ts +7 -7
  166. package/src/memory/db-init.ts +4 -0
  167. package/src/memory/embedding-local.ts +257 -39
  168. package/src/memory/embedding-runtime-manager.ts +471 -0
  169. package/src/memory/guardian-bindings.ts +25 -1
  170. package/src/memory/indexer.ts +3 -3
  171. package/src/memory/ingress-invite-store.ts +45 -0
  172. package/src/memory/job-handlers/backfill.ts +16 -9
  173. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  174. package/src/memory/migrations/index.ts +1 -0
  175. package/src/memory/qdrant-client.ts +31 -22
  176. package/src/memory/schema.ts +4 -0
  177. package/src/notifications/copy-composer.ts +15 -0
  178. package/src/runtime/access-request-helper.ts +43 -7
  179. package/src/runtime/actor-trust-resolver.ts +46 -50
  180. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  181. package/src/runtime/channel-retry-sweep.ts +18 -6
  182. package/src/runtime/guardian-context-resolver.ts +38 -96
  183. package/src/runtime/guardian-reply-router.ts +31 -1
  184. package/src/runtime/ingress-service.ts +80 -3
  185. package/src/runtime/invite-redemption-service.ts +141 -2
  186. package/src/runtime/routes/channel-route-shared.ts +1 -1
  187. package/src/runtime/routes/channel-routes.ts +1 -1
  188. package/src/runtime/routes/conversation-routes.ts +2 -2
  189. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  190. package/src/runtime/routes/inbound-message-handler.ts +41 -10
  191. package/src/runtime/routes/ingress-routes.ts +52 -4
  192. package/src/runtime/routes/pairing-routes.ts +3 -0
  193. package/src/tools/guardian-control-plane-policy.ts +2 -2
  194. package/src/tools/tool-approval-handler.ts +11 -11
  195. package/src/tools/types.ts +2 -2
  196. package/src/util/logger.ts +20 -8
  197. package/src/util/platform.ts +10 -0
  198. package/src/util/voice-code.ts +29 -0
  199. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -8,10 +8,10 @@
8
8
  * POST /v1/ingress/members/:id/block — block a member
9
9
  *
10
10
  * Invites:
11
- * GET /v1/ingress/invites — list invites
12
- * POST /v1/ingress/invites — create an invite
13
- * DELETE /v1/ingress/invites/:id — revoke an invite
14
- * POST /v1/ingress/invites/redeem — redeem an invite
11
+ * GET /v1/ingress/invites — list invites
12
+ * POST /v1/ingress/invites — create an invite (supports voice)
13
+ * DELETE /v1/ingress/invites/:id — revoke an invite
14
+ * POST /v1/ingress/invites/redeem — redeem an invite (token or voice code)
15
15
  */
16
16
 
17
17
  import {
@@ -20,6 +20,7 @@ import {
20
20
  listIngressInvites,
21
21
  listIngressMembers,
22
22
  redeemIngressInvite,
23
+ redeemVoiceInviteCode,
23
24
  revokeIngressInvite,
24
25
  revokeIngressMember,
25
26
  upsertIngressMember,
@@ -130,6 +131,11 @@ export function handleListInvites(url: URL): Response {
130
131
 
131
132
  /**
132
133
  * POST /v1/ingress/invites
134
+ *
135
+ * For voice invites, pass `sourceChannel: "voice"` with required
136
+ * `expectedExternalUserId` (E.164 phone). Voice codes are always 6 digits.
137
+ * The response will include a one-time `voiceCode` field that must be
138
+ * communicated to the invited user out-of-band.
133
139
  */
134
140
  export async function handleCreateInvite(req: Request): Promise<Response> {
135
141
  const body = (await req.json()) as Record<string, unknown>;
@@ -139,6 +145,8 @@ export async function handleCreateInvite(req: Request): Promise<Response> {
139
145
  note: body.note as string | undefined,
140
146
  maxUses: body.maxUses as number | undefined,
141
147
  expiresInMs: body.expiresInMs as number | undefined,
148
+ expectedExternalUserId: body.expectedExternalUserId as string | undefined,
149
+ voiceCodeDigits: body.voiceCodeDigits as number | undefined,
142
150
  });
143
151
 
144
152
  if (!result.ok) {
@@ -161,10 +169,50 @@ export function handleRevokeInvite(inviteId: string): Response {
161
169
 
162
170
  /**
163
171
  * POST /v1/ingress/invites/redeem
172
+ *
173
+ * Unified invite redemption endpoint. Supports two modes:
174
+ *
175
+ * 1. **Token-based** (existing): pass `token`, `sourceChannel`, `externalUserId`, etc.
176
+ * 2. **Voice code** (new): pass `code` and `callerExternalUserId` (E.164 phone).
177
+ * Optionally pass `assistantId`.
178
+ *
179
+ * The presence of `code` in the body selects voice-code redemption.
164
180
  */
165
181
  export async function handleRedeemInvite(req: Request): Promise<Response> {
166
182
  const body = (await req.json()) as Record<string, unknown>;
167
183
 
184
+ // Voice-code redemption path: triggered when `code` is present
185
+ if (body.code != null) {
186
+ const callerExternalUserId = body.callerExternalUserId as string | undefined;
187
+ const code = body.code as string | undefined;
188
+
189
+ if (!callerExternalUserId || !code) {
190
+ return Response.json(
191
+ { ok: false, error: 'callerExternalUserId and code are required' },
192
+ { status: 400 },
193
+ );
194
+ }
195
+
196
+ const result = redeemVoiceInviteCode({
197
+ assistantId: body.assistantId as string | undefined,
198
+ callerExternalUserId,
199
+ sourceChannel: 'voice',
200
+ code,
201
+ });
202
+
203
+ if (!result.ok) {
204
+ return Response.json({ ok: false, error: result.reason }, { status: 400 });
205
+ }
206
+
207
+ return Response.json({
208
+ ok: true,
209
+ type: result.type,
210
+ memberId: result.memberId,
211
+ ...(result.type === 'redeemed' ? { inviteId: result.inviteId } : {}),
212
+ });
213
+ }
214
+
215
+ // Token-based redemption path (default)
168
216
  const result = redeemIngressInvite({
169
217
  token: body.token as string | undefined,
170
218
  externalUserId: body.externalUserId as string | undefined,
@@ -75,6 +75,9 @@ export async function handlePairingRequest(req: Request, ctx: PairingHandlerCont
75
75
 
76
76
  const result = ctx.pairingStore.beginRequest({ pairingRequestId, pairingSecret, deviceId, deviceName });
77
77
  if (!result.ok) {
78
+ if (result.reason === 'already_paired') {
79
+ return httpError('CONFLICT', 'This pairing request is already bound to another device', 409);
80
+ }
78
81
  const statusCode = result.reason === 'invalid_secret' ? 403 : result.reason === 'not_found' ? 403 : 410;
79
82
  return httpError('FORBIDDEN', 'Forbidden', statusCode);
80
83
  }
@@ -124,13 +124,13 @@ export function isGuardianControlPlaneInvocation(
124
124
  export function enforceGuardianOnlyPolicy(
125
125
  toolName: string,
126
126
  input: Record<string, unknown>,
127
- actorRole: string | undefined,
127
+ trustClass: string | undefined,
128
128
  ): { denied: boolean; reason?: string } {
129
129
  if (!isGuardianControlPlaneInvocation(toolName, input)) {
130
130
  return { denied: false };
131
131
  }
132
132
 
133
- if (actorRole === 'guardian' || actorRole === undefined) {
133
+ if (trustClass === 'guardian' || trustClass === undefined) {
134
134
  return { denied: false };
135
135
  }
136
136
 
@@ -10,8 +10,8 @@ import type { ExecutionTarget, Tool, ToolContext, ToolExecutionResult, ToolLifec
10
10
 
11
11
  const log = getLogger('tool-approval-handler');
12
12
 
13
- function isUntrustedGuardianActorRole(role: ToolContext['guardianActorRole']): boolean {
14
- return role === 'non-guardian' || role === 'unverified_channel';
13
+ function isUntrustedGuardianTrustClass(role: ToolContext['guardianTrustClass']): boolean {
14
+ return role === 'trusted_contact' || role === 'unknown';
15
15
  }
16
16
 
17
17
  function requiresGuardianApprovalForActor(
@@ -26,10 +26,10 @@ function requiresGuardianApprovalForActor(
26
26
  }
27
27
 
28
28
  function guardianApprovalDeniedMessage(
29
- actorRole: ToolContext['guardianActorRole'],
29
+ trustClass: ToolContext['guardianTrustClass'],
30
30
  toolName: string,
31
31
  ): string {
32
- if (actorRole === 'unverified_channel') {
32
+ if (trustClass === 'unknown') {
33
33
  return `Permission denied for "${toolName}": this action requires guardian approval from a verified channel identity.`;
34
34
  }
35
35
  return `Permission denied for "${toolName}": this action requires guardian approval and the current actor is not the guardian.`;
@@ -82,13 +82,13 @@ export class ToolApprovalHandler {
82
82
  }
83
83
 
84
84
  // Reject tool invocations targeting guardian control-plane endpoints from non-guardian actors.
85
- const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.guardianActorRole);
85
+ const guardianCheck = enforceGuardianOnlyPolicy(name, input, context.guardianTrustClass);
86
86
  if (guardianCheck.denied) {
87
87
  log.warn({
88
88
  toolName: name,
89
89
  sessionId: context.sessionId,
90
90
  conversationId: context.conversationId,
91
- actorRole: context.guardianActorRole,
91
+ trustClass: context.guardianTrustClass,
92
92
  reason: 'guardian_only_policy',
93
93
  }, 'Guardian-only policy blocked tool invocation');
94
94
  const durationMs = Date.now() - startTime;
@@ -118,7 +118,7 @@ export class ToolApprovalHandler {
118
118
  let deferredConsumeParams: Parameters<typeof consumeGrantForInvocation>[0] | null = null;
119
119
 
120
120
  if (
121
- isUntrustedGuardianActorRole(context.guardianActorRole)
121
+ isUntrustedGuardianTrustClass(context.guardianTrustClass)
122
122
  && requiresGuardianApprovalForActor(name, input, executionTarget)
123
123
  ) {
124
124
  const inputDigest = computeToolApprovalDigest(name, input);
@@ -233,7 +233,7 @@ export class ToolApprovalHandler {
233
233
  toolName: name,
234
234
  sessionId: context.sessionId,
235
235
  conversationId: context.conversationId,
236
- actorRole: context.guardianActorRole,
236
+ trustClass: context.guardianTrustClass,
237
237
  executionTarget,
238
238
  grantId: grantResult.grant.id,
239
239
  }, 'Scoped grant consumed — allowing untrusted actor tool invocation');
@@ -273,7 +273,7 @@ export class ToolApprovalHandler {
273
273
  // actors remain fail-closed with no escalation.
274
274
  let escalationMessage: string | undefined;
275
275
  if (
276
- context.guardianActorRole === 'non-guardian'
276
+ context.guardianTrustClass === 'trusted_contact'
277
277
  && context.assistantId
278
278
  && context.executionChannel
279
279
  && context.requesterExternalUserId
@@ -308,12 +308,12 @@ export class ToolApprovalHandler {
308
308
  // If escalation.failed, fall through to generic denial message.
309
309
  }
310
310
 
311
- const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.guardianActorRole, name);
311
+ const reason = escalationMessage ?? guardianApprovalDeniedMessage(context.guardianTrustClass, name);
312
312
  log.warn({
313
313
  toolName: name,
314
314
  sessionId: context.sessionId,
315
315
  conversationId: context.conversationId,
316
- actorRole: context.guardianActorRole,
316
+ trustClass: context.guardianTrustClass,
317
317
  executionTarget,
318
318
  reason: 'guardian_approval_required',
319
319
  grantMissReason: grantResult.reason,
@@ -137,8 +137,8 @@ export interface ToolContext {
137
137
  proxyApprovalCallback?: import('./network/script-proxy/types.js').ProxyApprovalCallback;
138
138
  /** Optional principal identifier propagated to sub-tool confirmation flows. */
139
139
  principal?: string;
140
- /** Guardian actor role for the session — used by the guardian control-plane policy gate. */
141
- guardianActorRole?: 'guardian' | 'non-guardian' | 'unverified_channel';
140
+ /** Inbound trust classification for the session — used by trust/policy gates. */
141
+ guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
142
142
  /** Channel through which the tool invocation originates (e.g. 'telegram', 'voice'). Used for scoped grant consumption. */
143
143
  executionChannel?: string;
144
144
  /** Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption. */
@@ -3,12 +3,22 @@ import { join } from 'node:path';
3
3
  import { Writable } from 'node:stream';
4
4
 
5
5
  import pino from 'pino';
6
+ import type { PrettyOptions } from 'pino-pretty';
6
7
  import pinoPretty from 'pino-pretty';
7
8
 
8
9
  import { getDebugMode, getDebugStdoutLogs,getLogStderr } from '../config/env-registry.js';
9
10
  import { logSerializers } from './log-redact.js';
10
11
  import { getLogPath } from './platform.js';
11
12
 
13
+ /** Common pino-pretty options that inline [module] into the message prefix. */
14
+ function prettyOpts(extra?: PrettyOptions): PrettyOptions {
15
+ return {
16
+ messageFormat: '[{module}] {msg}',
17
+ ignore: 'module',
18
+ ...extra,
19
+ };
20
+ }
21
+
12
22
  export type LogFileConfig = {
13
23
  dir: string | undefined;
14
24
  retentionDays: number;
@@ -59,7 +69,7 @@ let activeLogFileConfig: LogFileConfig | null = null;
59
69
 
60
70
  function buildRotatingLogger(config: LogFileConfig): pino.Logger {
61
71
  if (!config.dir) {
62
- return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty({ destination: 1 }));
72
+ return pino({ name: 'assistant', serializers: logSerializers }, pinoPretty(prettyOpts({ destination: 1 })));
63
73
  }
64
74
 
65
75
  if (!existsSync(config.dir)) {
@@ -68,9 +78,10 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
68
78
 
69
79
  const today = formatDate(new Date());
70
80
  const filePath = logFilePathForDate(config.dir, new Date());
71
- const fileStream = pino.destination({ dest: filePath, sync: false, mkdir: true, mode: 0o600 });
81
+ const fileDest = pino.destination({ dest: filePath, sync: false, mkdir: true, mode: 0o600 });
72
82
  // Tighten permissions on pre-existing log files that may have been created with looser modes
73
83
  try { chmodSync(filePath, 0o600); } catch { /* best-effort */ }
84
+ const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
74
85
 
75
86
  activeLogDate = today;
76
87
  activeLogFileConfig = config;
@@ -78,7 +89,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
78
89
  const level = getDebugMode() ? 'debug' : 'info';
79
90
 
80
91
  if (getDebugMode()) {
81
- const prettyStream = pinoPretty({ destination: 2 });
92
+ const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
82
93
  return pino(
83
94
  { name: 'assistant', level, serializers: logSerializers },
84
95
  pino.multistream([
@@ -92,7 +103,7 @@ function buildRotatingLogger(config: LogFileConfig): pino.Logger {
92
103
  { name: 'assistant', level, serializers: logSerializers },
93
104
  pino.multistream([
94
105
  { stream: fileStream, level: 'info' as const },
95
- { stream: pinoPretty({ destination: 1 }), level: 'info' as const },
106
+ { stream: pinoPretty(prettyOpts({ destination: 1 })), level: 'info' as const },
96
107
  ]),
97
108
  );
98
109
  }
@@ -135,12 +146,13 @@ function getRootLogger(): pino.Logger {
135
146
 
136
147
  try {
137
148
  const logPath = getLogPath();
138
- const fileStream = pino.destination({ dest: logPath, sync: false, mkdir: true, mode: 0o600 });
149
+ const fileDest = pino.destination({ dest: logPath, sync: false, mkdir: true, mode: 0o600 });
139
150
  // Tighten permissions on pre-existing log files that may have been created with looser modes
140
151
  try { chmodSync(logPath, 0o600); } catch { /* best-effort */ }
152
+ const fileStream = pinoPretty(prettyOpts({ destination: fileDest, colorize: false }));
141
153
 
142
154
  if (getDebugMode()) {
143
- const prettyStream = pinoPretty({ destination: 2 });
155
+ const prettyStream = pinoPretty(prettyOpts({ destination: 2 }));
144
156
  const multi = pino.multistream([
145
157
  { stream: fileStream, level: 'info' as const },
146
158
  { stream: prettyStream, level: 'debug' as const },
@@ -151,14 +163,14 @@ function getRootLogger(): pino.Logger {
151
163
  { level: 'info', serializers: logSerializers },
152
164
  pino.multistream([
153
165
  { stream: fileStream, level: 'info' as const },
154
- { stream: pinoPretty({ destination: 1 }), level: 'info' as const },
166
+ { stream: pinoPretty(prettyOpts({ destination: 1 })), level: 'info' as const },
155
167
  ]),
156
168
  );
157
169
  } else {
158
170
  rootLogger = pino({ level: 'info', serializers: logSerializers }, fileStream);
159
171
  }
160
172
  } catch {
161
- rootLogger = pino({ level: getDebugMode() ? 'debug' : 'info', serializers: logSerializers }, pinoPretty({ destination: 2 }));
173
+ rootLogger = pino({ level: getDebugMode() ? 'debug' : 'info', serializers: logSerializers }, pinoPretty(prettyOpts({ destination: 2 })));
162
174
  }
163
175
  }
164
176
  return rootLogger;
@@ -121,6 +121,15 @@ export function getDataDir(): string {
121
121
  return join(getWorkspaceDir(), 'data');
122
122
  }
123
123
 
124
+ /**
125
+ * Returns the embedding models directory (~/.vellum/workspace/embedding-models).
126
+ * Downloaded embedding runtime (onnxruntime-node, transformers bundle, model weights)
127
+ * is stored here, downloaded post-hatch rather than shipped with the app.
128
+ */
129
+ export function getEmbeddingModelsDir(): string {
130
+ return join(getWorkspaceDir(), 'embedding-models');
131
+ }
132
+
124
133
  /**
125
134
  * Returns the IPC blob directory (~/.vellum/workspace/data/ipc-blobs).
126
135
  * Temporary blob files for zero-copy IPC payloads live here.
@@ -357,6 +366,7 @@ export function ensureDataDir(): void {
357
366
  workspace,
358
367
  join(workspace, 'hooks'),
359
368
  join(workspace, 'skills'),
369
+ join(workspace, 'embedding-models'),
360
370
  // Data sub-dirs under workspace
361
371
  wsData,
362
372
  join(wsData, 'db'),
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Cryptographic voice invite code generation and hashing.
3
+ *
4
+ * Generates short numeric codes (default 6 digits) for voice-channel invite
5
+ * redemption. The plaintext code is returned once at creation time and never
6
+ * stored — only its SHA-256 hash is persisted.
7
+ */
8
+
9
+ import { createHash, randomInt } from 'node:crypto';
10
+
11
+ /**
12
+ * Generate a cryptographically random numeric code of the given length.
13
+ * Uses node:crypto randomInt for uniform distribution.
14
+ */
15
+ export function generateVoiceCode(digits: number = 6): string {
16
+ if (digits < 4 || digits > 10) {
17
+ throw new Error(`Voice code digit count must be between 4 and 10, got ${digits}`);
18
+ }
19
+ const min = Math.pow(10, digits - 1); // e.g. 100000 for 6 digits
20
+ const max = Math.pow(10, digits); // e.g. 1000000 for 6 digits
21
+ return String(randomInt(min, max));
22
+ }
23
+
24
+ /**
25
+ * SHA-256 hash a voice code for storage comparison.
26
+ */
27
+ export function hashVoiceCode(code: string): string {
28
+ return createHash('sha256').update(code).digest('hex');
29
+ }
@@ -1,124 +0,0 @@
1
- // Guardian invite intent resolution for deterministic first-turn routing.
2
- // Exports `resolveGuardianInviteIntent` as the single public entry point.
3
- // When a guardian invite management request is detected, the session pipeline
4
- // rewrites the message to force immediate entry into the trusted-contacts
5
- // skill flow, bypassing the normal agent loop's tendency to produce conceptual
6
- // preambles before loading the skill.
7
-
8
- export type GuardianInviteIntentResult =
9
- | { kind: 'none' }
10
- | { kind: 'invite_management'; rewrittenContent: string; action?: 'create' | 'list' | 'revoke' };
11
-
12
- // ── Direct invite patterns ────────────────────────────────────────────────
13
- // These capture imperative requests to manage Telegram invite links.
14
-
15
- const CREATE_INVITE_PATTERNS: RegExp[] = [
16
- /\bcreate\s+(?:an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
17
- /\binvite\s+(?:someone|somebody|a\s+friend|a\s+person)\s+(?:on|to|via|through)\s+telegram\b/i,
18
- /\b(?:make|generate|get)\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
19
- /\btelegram\s+invite\s*(?:link)?\b/i,
20
- /\bsend\s+(?:a\s+|an\s+)?invite\s+(?:link\s+)?(?:on|for|via|through)\s+telegram\b/i,
21
- /\bshare\s+(?:a\s+|an\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
22
- /\binvite\s+(?:link\s+)?for\s+telegram\b/i,
23
- ];
24
-
25
- const LIST_INVITE_PATTERNS: RegExp[] = [
26
- /\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:active\s+)?invite(?:s|\s*links?)\b/i,
27
- /\b(?:show|list|view|see|display)\s+(?:my\s+)?(?:telegram\s+)?invite(?:s|\s*links?)\b/i,
28
- /\bwhat\s+invite(?:s|\s*links?)\s+(?:do\s+I\s+have|are\s+active|exist)\b/i,
29
- /\bhow\s+many\s+invite(?:s|\s*links?)\b/i,
30
- ];
31
-
32
- const REVOKE_INVITE_PATTERNS: RegExp[] = [
33
- /\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?invite\s*(?:link)?\b/i,
34
- /\b(?:revoke|cancel|disable|invalidate|delete|remove)\s+(?:the\s+|my\s+|an?\s+)?(?:telegram\s+)?invite\s*(?:link)?\b/i,
35
- /\binvite\s*(?:link)?\s+(?:revoke|cancel|disable|invalidate|delete|remove)\b/i,
36
- ];
37
-
38
- // ── Conceptual / question patterns ──────────────────────────────────────
39
- // These indicate the user is asking *about* invites rather than requesting
40
- // to manage them. Return passthrough for these.
41
-
42
- const CONCEPTUAL_PATTERNS: RegExp[] = [
43
- /^\s*(?:how|what|why|when|where|who|which)\b.*\binvite/i,
44
- /\bwhat\s+(?:is|are)\s+(?:an?\s+)?invite\s*(?:link)?\b/i,
45
- /\bhow\s+(?:do|does|can)\s+(?:invite|invitation)s?\s+work\b/i,
46
- /\bexplain\s+(?:the\s+)?invite\b/i,
47
- /\btell\s+me\s+about\s+invite\b/i,
48
- ];
49
-
50
- /** Common polite/filler words stripped before checking intent-only status. */
51
- const FILLER_PATTERN =
52
- /\b(please|pls|plz|can\s+you|could\s+you|would\s+you|now|right\s+now|thanks|thank\s+you|thx|ty|for\s+me|ok(ay)?|hey|hi|hello|just|i\s+want\s+to|i'd\s+like\s+to|i\s+need\s+to|let's|let\s+me)\b/gi;
53
-
54
- // ── Internal helpers ─────────────────────────────────────────────────────
55
-
56
- function isConceptualQuestion(text: string): boolean {
57
- const cleaned = text.replace(/^\s*(hey|hi|hello|please|pls|plz)[,\s]+/i, '');
58
- // Allow actionable requests through even though they start with
59
- // question-like words — these are imperative invite management requests.
60
- if (LIST_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
61
- if (CREATE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
62
- if (REVOKE_INVITE_PATTERNS.some((p) => p.test(cleaned))) return false;
63
- return CONCEPTUAL_PATTERNS.some((p) => p.test(cleaned));
64
- }
65
-
66
- function detectAction(text: string): 'create' | 'list' | 'revoke' | undefined {
67
- // Check revoke and list before create — create patterns include the broad
68
- // `telegram invite link` matcher that would otherwise swallow revoke/list inputs.
69
- if (REVOKE_INVITE_PATTERNS.some((p) => p.test(text))) return 'revoke';
70
- if (LIST_INVITE_PATTERNS.some((p) => p.test(text))) return 'list';
71
- if (CREATE_INVITE_PATTERNS.some((p) => p.test(text))) return 'create';
72
- return undefined;
73
- }
74
-
75
- // ── Structured intent resolver ───────────────────────────────────────────
76
-
77
- /**
78
- * Resolves guardian invite management intent from user text.
79
- *
80
- * Pipeline:
81
- * 1. Skip slash commands entirely
82
- * 2. Conceptual question gate -- questions return `none`
83
- * 3. Detect create/list/revoke invite patterns
84
- * 4. On match, build a deterministic model instruction to load trusted-contacts
85
- */
86
- export function resolveGuardianInviteIntent(text: string): GuardianInviteIntentResult {
87
- const trimmed = text.trim();
88
-
89
- // Never intercept slash commands
90
- if (trimmed.startsWith('/')) {
91
- return { kind: 'none' };
92
- }
93
-
94
- // Conceptual questions pass through to normal agent processing
95
- if (isConceptualQuestion(trimmed)) {
96
- return { kind: 'none' };
97
- }
98
-
99
- // Strip fillers for pattern matching but keep original for context
100
- const withoutFillers = trimmed.replace(FILLER_PATTERN, '').replace(/\s{2,}/g, ' ').trim();
101
-
102
- const action = detectAction(withoutFillers);
103
- if (!action) {
104
- return { kind: 'none' };
105
- }
106
-
107
- // Build the rewritten content that deterministically loads the skill
108
- const actionDescriptions: Record<string, string> = {
109
- create: 'The user wants to create a Telegram invite link. Create the invite, look up the bot username, and present the shareable deep link with copy-paste instructions.',
110
- list: 'The user wants to see their invite links. List all invites (especially active ones for Telegram) and present them in a readable format.',
111
- revoke: 'The user wants to revoke an invite link. List invites to identify the target, confirm with the user, then revoke it.',
112
- };
113
-
114
- const rewrittenContent = [
115
- actionDescriptions[action],
116
- 'Please invoke the "Trusted Contacts" skill (ID: trusted-contacts) immediately using skill_load.',
117
- ].join('\n');
118
-
119
- return {
120
- kind: 'invite_management',
121
- rewrittenContent,
122
- action,
123
- };
124
- }