@vellumai/assistant 0.3.27 → 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 (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -1,6 +1,7 @@
1
1
  import type { SecretPromptResult } from '../permissions/secret-prompter.js';
2
2
  import type { AllowlistOption, RiskLevel, ScopeOption } from '../permissions/types.js';
3
3
  import type { ContentBlock,ToolDefinition } from '../providers/types.js';
4
+ import type { SensitiveOutputBinding } from './sensitive-output-placeholders.js';
4
5
 
5
6
  export type ExecutionTarget = 'sandbox' | 'host';
6
7
 
@@ -136,14 +137,16 @@ export interface ToolContext {
136
137
  proxyApprovalCallback?: import('./network/script-proxy/types.js').ProxyApprovalCallback;
137
138
  /** Optional principal identifier propagated to sub-tool confirmation flows. */
138
139
  principal?: string;
139
- /** Guardian actor role for the session — used by the guardian control-plane policy gate. */
140
- guardianActorRole?: 'guardian' | 'non-guardian' | 'unverified_channel';
140
+ /** Inbound trust classification for the session — used by trust/policy gates. */
141
+ guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
141
142
  /** Channel through which the tool invocation originates (e.g. 'telegram', 'voice'). Used for scoped grant consumption. */
142
143
  executionChannel?: string;
143
144
  /** Voice/call session ID, if the invocation originates from a call. Used for scoped grant consumption. */
144
145
  callSessionId?: string;
145
146
  /** External user ID of the requester (non-guardian actor). Used for scoped grant consumption. */
146
147
  requesterExternalUserId?: string;
148
+ /** Chat ID of the requester (non-guardian actor). Used for tool grant request escalation notifications. */
149
+ requesterChatId?: string;
147
150
  }
148
151
 
149
152
  export interface DiffInfo {
@@ -161,6 +164,14 @@ export interface ToolExecutionResult {
161
164
  status?: string;
162
165
  /** Optional rich content blocks (e.g. images) to include alongside text in the tool result. */
163
166
  contentBlocks?: ContentBlock[];
167
+ /**
168
+ * Runtime-internal sensitive output bindings (placeholder -> real value).
169
+ * Populated by the executor when tool output contains
170
+ * `<vellum-sensitive-output>` directives. The agent loop merges these
171
+ * into a per-run substitution map for deterministic post-generation
172
+ * replacement. MUST NOT be emitted in client-facing events or logs.
173
+ */
174
+ sensitiveBindings?: SensitiveOutputBinding[];
164
175
  }
165
176
 
166
177
  export interface Tool {
@@ -0,0 +1,31 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+
4
+ /**
5
+ * Resolve the path to a bundled asset directory, handling compiled Bun binaries
6
+ * where `import.meta.dirname` points to the `/$bunfs/` virtual filesystem and
7
+ * non-JS files (.md, .html, .json, etc.) are not embedded.
8
+ *
9
+ * Falls back to:
10
+ * 1. `Contents/Resources/<bundleName>` (macOS .app bundle)
11
+ * 2. `<execDir>/<bundleName>` (next to the binary, non-app-bundle deployments)
12
+ * 3. Original resolved path (source mode, or last resort)
13
+ *
14
+ * This matches the pattern established by bundled-skills and WASM resolution.
15
+ *
16
+ * @param callerDir `import.meta.dirname ?? __dirname` from the call site
17
+ * @param relativePath Relative path from the source file (used in source/dev mode)
18
+ * @param bundleName Name of the asset directory in the app bundle
19
+ */
20
+ export function resolveBundledDir(callerDir: string, relativePath: string, bundleName: string): string {
21
+ if (callerDir.startsWith('/$bunfs/')) {
22
+ const execDir = dirname(process.execPath);
23
+ // macOS .app bundle: binary in Contents/MacOS/, resources in Contents/Resources/
24
+ const resourcesPath = join(execDir, '..', 'Resources', bundleName);
25
+ if (existsSync(resourcesPath)) return resourcesPath;
26
+ // Next to the binary itself (non-app-bundle deployments)
27
+ const execDirPath = join(execDir, bundleName);
28
+ if (existsSync(execDirPath)) return execDirPath;
29
+ }
30
+ return join(callerDir, relativePath);
31
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Channel-agnostic inbound identity canonicalization.
3
+ *
4
+ * Normalizes raw sender identifiers into a stable canonical form so that
5
+ * trust lookups, member matching, and guardian binding comparisons are
6
+ * immune to formatting variance across channels.
7
+ *
8
+ * Phone-like channels (sms, voice, whatsapp) normalize to E.164 using the
9
+ * existing phone utilities. Non-phone channels (telegram, slack, etc.)
10
+ * pass through the platform-stable ID as-is after whitespace trimming.
11
+ */
12
+
13
+ import type { ChannelId } from '../channels/types.js';
14
+ import { normalizePhoneNumber } from './phone.js';
15
+
16
+ /** Channels whose raw sender IDs are phone numbers. */
17
+ const PHONE_CHANNELS: ReadonlySet<ChannelId> = new Set(['sms', 'voice', 'whatsapp']);
18
+
19
+ /**
20
+ * Canonicalize a raw inbound sender identity for the given channel.
21
+ *
22
+ * - For phone-like channels: attempts E.164 normalization. Returns the
23
+ * normalized E.164 string on success, or the trimmed raw ID if
24
+ * normalization fails (defensive: don't discard an identity just because
25
+ * it doesn't parse as a phone number).
26
+ * - For non-phone channels: returns the trimmed raw ID unchanged (these
27
+ * platforms provide stable, unique identifiers that don't need normalization).
28
+ *
29
+ * Returns `null` only when `rawId` is empty/whitespace-only.
30
+ */
31
+ export function canonicalizeInboundIdentity(channel: ChannelId, rawId: string): string | null {
32
+ const trimmed = rawId.trim();
33
+ if (trimmed.length === 0) return null;
34
+
35
+ if (PHONE_CHANNELS.has(channel)) {
36
+ const e164 = normalizePhoneNumber(trimmed);
37
+ // Defensive: if normalization fails, preserve the raw ID so downstream
38
+ // lookups don't silently lose the identity.
39
+ return e164 ?? trimmed;
40
+ }
41
+
42
+ return trimmed;
43
+ }
44
+
45
+ /**
46
+ * Check whether a channel uses phone-number-based identity.
47
+ * Useful for call sites that need to know whether E.164 normalization
48
+ * applies without re-importing the channel set.
49
+ */
50
+ export function isPhoneChannel(channel: ChannelId): boolean {
51
+ return PHONE_CHANNELS.has(channel);
52
+ }
@@ -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
- }