@vellumai/assistant 0.11.8-staging.2 → 0.11.9-dev.202609040043.7972a34

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 (434) hide show
  1. package/ARCHITECTURE.md +4 -4
  2. package/Dockerfile +17 -48
  3. package/docker-bun-no-autoserve.js +38 -0
  4. package/docker-entrypoint.sh +5 -1
  5. package/docker-kata-apt-env.sh +3 -0
  6. package/docker-kata-apt-shims.sh +127 -0
  7. package/docker-kata-apt-wrapper.sh +45 -0
  8. package/docker-kata-chroot-exec.sh +35 -0
  9. package/docker-kata-pip.sh +8 -2
  10. package/docker-node-launcher.sh +37 -0
  11. package/docs/architecture/memory.md +8 -4
  12. package/docs/guardian-request-flow.md +21 -14
  13. package/docs/runbook-trusted-contacts.md +13 -2
  14. package/docs/trusted-contact-access.md +1 -7
  15. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  16. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +8 -0
  17. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  18. package/node_modules/@vellumai/avatar-manifest/package.json +2 -1
  19. package/node_modules/@vellumai/avatar-manifest/src/__tests__/accent.test.ts +118 -0
  20. package/node_modules/@vellumai/avatar-manifest/src/__tests__/manifest.test.ts +83 -4
  21. package/node_modules/@vellumai/avatar-manifest/src/accent.ts +150 -0
  22. package/node_modules/@vellumai/avatar-manifest/src/index.ts +7 -0
  23. package/node_modules/@vellumai/avatar-manifest/src/manifest.ts +55 -3
  24. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +5 -1
  25. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +40 -1
  26. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +21 -0
  27. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/desktop-presence.ts +11 -0
  28. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  29. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  30. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  31. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  32. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +5 -1
  33. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +40 -1
  34. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +21 -0
  35. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/desktop-presence.ts +11 -0
  36. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  37. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  39. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  40. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  41. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  42. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  43. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  44. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  45. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  46. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  47. package/node_modules/@vellumai/service-contracts/package.json +5 -1
  48. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +40 -1
  49. package/node_modules/@vellumai/service-contracts/src/channels.ts +21 -0
  50. package/node_modules/@vellumai/service-contracts/src/desktop-presence.ts +11 -0
  51. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  52. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  53. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  54. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  55. package/openapi.yaml +817 -21
  56. package/package.json +2 -2
  57. package/src/__tests__/access-request-card-view.test.ts +6 -5
  58. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  59. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  60. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  61. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  62. package/src/__tests__/attachments-store.test.ts +27 -0
  63. package/src/__tests__/attachments.test.ts +43 -0
  64. package/src/__tests__/bun-no-autoserve.test.ts +178 -0
  65. package/src/__tests__/canned-reply-release.test.ts +48 -5
  66. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  67. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  68. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  69. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  70. package/src/__tests__/compactor-assistant-tail-preview.test.ts +111 -0
  71. package/src/__tests__/compactor-tail-resolution.test.ts +116 -0
  72. package/src/__tests__/config-loader-validation-reset-notice.test.ts +47 -15
  73. package/src/__tests__/context-search-conversations-source.test.ts +27 -5
  74. package/src/__tests__/conversation-error.test.ts +15 -0
  75. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  76. package/src/__tests__/conversation-pairing.test.ts +151 -10
  77. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  78. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  79. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  80. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  81. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -4
  82. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  83. package/src/__tests__/credential-health-service.test.ts +98 -10
  84. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  85. package/src/__tests__/delete-propagation.test.ts +469 -0
  86. package/src/__tests__/delivered-post-record.test.ts +107 -0
  87. package/src/__tests__/dm-persistence.test.ts +16 -0
  88. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  89. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  90. package/src/__tests__/gemini-provider.test.ts +46 -0
  91. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  92. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  93. package/src/__tests__/guardian-question-mode.test.ts +106 -48
  94. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  95. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  96. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  97. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  98. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  99. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  100. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  101. package/src/__tests__/llm-schema.test.ts +38 -2
  102. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  103. package/src/__tests__/messaging-send-tool.test.ts +98 -121
  104. package/src/__tests__/notification-decision-fallback.test.ts +249 -105
  105. package/src/__tests__/notification-decision-strategy.test.ts +188 -231
  106. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  107. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  108. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  109. package/src/__tests__/openai-provider.test.ts +5 -5
  110. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  111. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  112. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  113. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  114. package/src/__tests__/pricing.test.ts +11 -0
  115. package/src/__tests__/process-message-display-content.test.ts +20 -9
  116. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  117. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  118. package/src/__tests__/reaction-persistence.test.ts +22 -198
  119. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  120. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  121. package/src/__tests__/skill-load-tool.test.ts +27 -0
  122. package/src/__tests__/skills.test.ts +34 -1
  123. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  124. package/src/__tests__/terminal-tools.test.ts +9 -0
  125. package/src/__tests__/thread-backfill.test.ts +5 -3
  126. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +4 -13
  127. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  128. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  129. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  130. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  131. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  132. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  133. package/src/agent/attachments.ts +13 -2
  134. package/src/agent/loop.ts +3 -19
  135. package/src/api/README.md +9 -5
  136. package/src/api/events/contact-record-request.ts +16 -4
  137. package/src/api/events/contact-request.ts +10 -1
  138. package/src/api/index.ts +9 -8
  139. package/src/api/package.json +1 -0
  140. package/src/api/responses/conversation-message.ts +8 -0
  141. package/src/api/responses/home.ts +7 -18
  142. package/src/api/surfaces.ts +115 -7
  143. package/src/approvals/AGENTS.md +1 -1
  144. package/src/avatar/__tests__/avatar-manifest.test.ts +10 -0
  145. package/src/avatar/__tests__/avatar-store.test.ts +130 -8
  146. package/src/avatar/__tests__/ensure-raster.test.ts +15 -0
  147. package/src/avatar/avatar-accent.ts +57 -0
  148. package/src/avatar/avatar-manifest.ts +20 -2
  149. package/src/avatar/avatar-store.ts +111 -4
  150. package/src/calls/__tests__/voice-turn-sight-hold.test.ts +301 -0
  151. package/src/calls/voice-session-bridge.ts +35 -1
  152. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  153. package/src/channels/__tests__/types.test.ts +22 -1
  154. package/src/channels/gateway-guardian-requests.ts +0 -22
  155. package/src/channels/types.ts +8 -6
  156. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  157. package/src/cli/__tests__/contacts-address-prompt-cli.test.ts +701 -0
  158. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +276 -15
  159. package/src/cli/__tests__/roadmap-cli.test.ts +313 -0
  160. package/src/cli/__tests__/run-assistant-command.ts +59 -20
  161. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  162. package/src/cli/commands/channels/index.help.ts +19 -6
  163. package/src/cli/commands/channels/index.ts +6 -2
  164. package/src/cli/commands/contacts.help.ts +179 -12
  165. package/src/cli/commands/contacts.ts +442 -47
  166. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  167. package/src/cli/commands/db/index.help.ts +1 -1
  168. package/src/cli/commands/db/status.ts +172 -1
  169. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  170. package/src/cli/commands/platform/__tests__/credits-format.test.ts +63 -0
  171. package/src/cli/commands/platform/__tests__/credits.test.ts +10 -0
  172. package/src/cli/commands/platform/connect.ts +14 -5
  173. package/src/cli/commands/platform/credits-format.ts +51 -0
  174. package/src/cli/commands/platform/index.help.ts +17 -11
  175. package/src/cli/commands/platform/index.ts +7 -18
  176. package/src/cli/commands/plugins.help.ts +6 -5
  177. package/src/cli/commands/roadmap.help.ts +171 -0
  178. package/src/cli/commands/roadmap.ts +358 -0
  179. package/src/cli/index.help.ts +2 -0
  180. package/src/cli/lib/__tests__/cli-output.test.ts +5 -0
  181. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  182. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  183. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  184. package/src/cli/lib/bundled-marketplace.json +14 -0
  185. package/src/cli/lib/cli-output.ts +6 -6
  186. package/src/cli/lib/install-from-github.ts +9 -5
  187. package/src/cli/lib/install-from-platform.ts +12 -1
  188. package/src/cli/program.ts +2 -0
  189. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  190. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  191. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  192. package/src/config/bundled-skills/contacts/SKILL.md +61 -11
  193. package/src/config/bundled-skills/media-processing/SKILL.md +3 -3
  194. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -1
  195. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +76 -36
  196. package/src/config/bundled-skills/schedule/SKILL.md +27 -13
  197. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  198. package/src/config/call-site-defaults.ts +4 -1
  199. package/src/config/env-registry.ts +2 -0
  200. package/src/config/feature-flag-registry.json +27 -10
  201. package/src/config/loader.ts +1 -0
  202. package/src/config/schemas/llm.ts +34 -7
  203. package/src/config/schemas/memory-v3.ts +4 -3
  204. package/src/config/schemas/services.ts +8 -0
  205. package/src/context/compactor.ts +12 -13
  206. package/src/context/strip-injections.ts +17 -56
  207. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  208. package/src/conversations/message-consolidation.ts +4 -3
  209. package/src/credential-health/credential-health-service.ts +130 -0
  210. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  211. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  212. package/src/daemon/conversation-error.ts +2 -0
  213. package/src/daemon/conversation-messaging.ts +195 -47
  214. package/src/daemon/conversation-process.ts +20 -5
  215. package/src/daemon/conversation-runtime-assembly.ts +54 -45
  216. package/src/daemon/conversation-store.ts +159 -11
  217. package/src/daemon/conversation-surfaces.ts +12 -6
  218. package/src/daemon/conversation-tool-setup.ts +9 -2
  219. package/src/daemon/conversation.ts +290 -19
  220. package/src/daemon/dictation-text-processing.ts +2 -7
  221. package/src/daemon/handlers/config-channels.ts +33 -3
  222. package/src/daemon/handlers/config-model.test.ts +1 -0
  223. package/src/daemon/handlers/shared.ts +9 -0
  224. package/src/daemon/port-oversized-content.test.ts +117 -0
  225. package/src/daemon/port-oversized-content.ts +110 -0
  226. package/src/daemon/process-message.ts +87 -16
  227. package/src/daemon/reaction-record.test.ts +23 -11
  228. package/src/daemon/reaction-record.ts +6 -9
  229. package/src/documents/document-store.ts +1 -5
  230. package/src/home/conversation-starter-validation.ts +1 -5
  231. package/src/live-voice/__tests__/live-voice-photo.test.ts +1012 -50
  232. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  233. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  234. package/src/live-voice/live-voice-photo.ts +585 -61
  235. package/src/live-voice/live-voice-session.ts +6 -2
  236. package/src/live-voice/protocol.ts +20 -1
  237. package/src/messaging/provider-message-metadata.ts +40 -1
  238. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  239. package/src/messaging/providers/channel-transport.ts +28 -0
  240. package/src/messaging/providers/discord/send.ts +3 -2
  241. package/src/messaging/providers/slack/api.ts +11 -1
  242. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  243. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  244. package/src/messaging/providers/slack/send.test.ts +58 -0
  245. package/src/messaging/providers/slack/send.ts +4 -4
  246. package/src/messaging/providers/slack/transport.ts +28 -2
  247. package/src/messaging/providers/telegram-bot/adapter.ts +9 -2
  248. package/src/messaging/providers/telegram-bot/client.ts +8 -2
  249. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  250. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  251. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  252. package/src/messaging/providers/whatsapp/adapter.ts +9 -2
  253. package/src/messaging/providers/whatsapp/client.ts +8 -2
  254. package/src/messaging/reaction-envelopes.test.ts +73 -0
  255. package/src/messaging/reaction-envelopes.ts +33 -5
  256. package/src/messaging/read-provider-metadata.ts +4 -3
  257. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  258. package/src/monitoring/recovery/db.ts +35 -0
  259. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  260. package/src/monitoring/recovery/run-recovery.ts +6 -2
  261. package/src/monitoring/recovery/stale-processing.ts +3 -20
  262. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  263. package/src/notifications/AGENTS.md +7 -2
  264. package/src/notifications/README.md +21 -7
  265. package/src/notifications/__tests__/assistant-reply-producer.test.ts +14 -19
  266. package/src/notifications/__tests__/background-failure-signal.test.ts +96 -0
  267. package/src/notifications/__tests__/broadcaster.test.ts +206 -11
  268. package/src/notifications/__tests__/edit-notification.test.ts +59 -0
  269. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  270. package/src/notifications/__tests__/resolve-visible-in-source.test.ts +96 -0
  271. package/src/notifications/access-request-copy.ts +126 -114
  272. package/src/notifications/adapters/discord.ts +9 -5
  273. package/src/notifications/adapters/shared.ts +17 -4
  274. package/src/notifications/adapters/slack.ts +14 -4
  275. package/src/notifications/adapters/telegram.ts +8 -4
  276. package/src/notifications/approval-card-data.ts +34 -15
  277. package/src/notifications/assistant-reply-producer.ts +7 -11
  278. package/src/notifications/background-failure-signal.ts +13 -1
  279. package/src/notifications/broadcaster.ts +122 -27
  280. package/src/notifications/conversation-pairing.ts +330 -156
  281. package/src/notifications/copy-composer.ts +7 -32
  282. package/src/notifications/decision-engine.ts +56 -246
  283. package/src/notifications/delivered-post-record.ts +81 -0
  284. package/src/notifications/deliveries-store.ts +8 -1
  285. package/src/notifications/edit-notification.ts +16 -0
  286. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  287. package/src/notifications/guardian-feed-projection.ts +0 -15
  288. package/src/notifications/guardian-question-mode.ts +35 -102
  289. package/src/notifications/notification-utils.ts +125 -1
  290. package/src/notifications/resolve-visible-in-source.ts +56 -0
  291. package/src/oauth/__tests__/seed-providers-managed.test.ts +41 -0
  292. package/src/oauth/seed-providers.ts +138 -3
  293. package/src/permissions/confirmation-guardian-request.ts +2 -2
  294. package/src/permissions/prompter.ts +1 -1
  295. package/src/persistence/conversation-crud.ts +93 -14
  296. package/src/persistence/conversation-queries.ts +144 -17
  297. package/src/persistence/conversation-types.ts +39 -5
  298. package/src/persistence/delivery-crud.ts +224 -28
  299. package/src/persistence/delivery-status.ts +21 -2
  300. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  301. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  302. package/src/persistence/migrations/376-notification-deliveries-canonical-message-id.ts +37 -0
  303. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  304. package/src/persistence/schema/conversations.ts +71 -2
  305. package/src/persistence/schema/notifications.ts +8 -0
  306. package/src/persistence/schema-contract.test.ts +78 -0
  307. package/src/persistence/schema-contract.ts +96 -0
  308. package/src/persistence/steps.ts +6 -0
  309. package/src/platform/client.ts +55 -0
  310. package/src/platform/sync-avatar.test.ts +3 -0
  311. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  312. package/src/plugins/defaults/injector-order.ts +2 -1
  313. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  314. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +29 -17
  315. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  316. package/src/plugins/defaults/memory/injectors.ts +1 -1
  317. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  318. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  319. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  320. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  321. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  322. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  323. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  324. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  325. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  326. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  327. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  328. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  329. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  330. package/src/plugins/mcp-servers.ts +14 -2
  331. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  332. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  333. package/src/providers/anthropic/client.ts +21 -24
  334. package/src/providers/call-site-routing.ts +5 -4
  335. package/src/providers/connection-resolution.ts +29 -1
  336. package/src/providers/content-block-size.test.ts +82 -0
  337. package/src/providers/content-block-size.ts +99 -0
  338. package/src/providers/file-block-text.test.ts +64 -0
  339. package/src/providers/file-block-text.ts +28 -0
  340. package/src/providers/gemini/client.ts +15 -9
  341. package/src/providers/inference/auth.ts +6 -6
  342. package/src/providers/media-resolve.ts +14 -0
  343. package/src/providers/model-catalog.ts +47 -34
  344. package/src/providers/openai/chat-completions-provider.ts +9 -25
  345. package/src/providers/openai/responses-provider.ts +11 -18
  346. package/src/providers/registry.ts +10 -7
  347. package/src/providers/routing-identity.ts +2 -1
  348. package/src/providers/types.ts +1 -4
  349. package/src/providers/vellum-model-routing.ts +3 -2
  350. package/src/runtime/AGENTS.md +46 -16
  351. package/src/runtime/__tests__/background-job-runner.test.ts +115 -0
  352. package/src/runtime/__tests__/guardian-form-registry.test.ts +0 -32
  353. package/src/runtime/__tests__/host-observe.test.ts +44 -0
  354. package/src/runtime/approval-message-composer.ts +1 -6
  355. package/src/runtime/assistant-event-hub.ts +9 -47
  356. package/src/runtime/background-job-runner.ts +10 -0
  357. package/src/runtime/channel-approval-types.ts +1 -5
  358. package/src/runtime/channel-reply-delivery.ts +59 -153
  359. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  360. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  361. package/src/runtime/channel-retry-sweep.ts +102 -1
  362. package/src/runtime/finalize-event-delivery.ts +4 -4
  363. package/src/runtime/guardian-action-message-composer.ts +1 -4
  364. package/src/runtime/guardian-reply-router.ts +4 -75
  365. package/src/runtime/host-observe.ts +56 -6
  366. package/src/runtime/http-router.ts +1 -5
  367. package/src/runtime/http-server.ts +12 -0
  368. package/src/runtime/outbound-post-reconciliation.ts +183 -0
  369. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  370. package/src/runtime/routes/__tests__/avatar-state-routes.test.ts +147 -27
  371. package/src/runtime/routes/__tests__/contact-address-prompt-route.test.ts +231 -0
  372. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +59 -3
  373. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  374. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  375. package/src/runtime/routes/__tests__/guardian-form-routes.test.ts +85 -0
  376. package/src/runtime/routes/__tests__/platform-credits-routes.test.ts +145 -0
  377. package/src/runtime/routes/__tests__/roadmap-routes.test.ts +378 -0
  378. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  379. package/src/runtime/routes/__tests__/watch-routes.test.ts +70 -1
  380. package/src/runtime/routes/acp-routes.ts +1 -1
  381. package/src/runtime/routes/avatar-routes.ts +84 -28
  382. package/src/runtime/routes/canned-reply-release.ts +19 -9
  383. package/src/runtime/routes/channel-route-shared.ts +0 -39
  384. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  385. package/src/runtime/routes/contact-prompt-routes.ts +177 -28
  386. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  387. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  388. package/src/runtime/routes/conversation-routes.ts +138 -59
  389. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  390. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  391. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  392. package/src/runtime/routes/guardian-form-routes.ts +6 -4
  393. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  394. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  395. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  396. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  397. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  398. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  399. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  400. package/src/runtime/routes/index.ts +4 -0
  401. package/src/runtime/routes/platform-routes.ts +157 -13
  402. package/src/runtime/routes/roadmap-routes.ts +665 -0
  403. package/src/runtime/routes/settings-routes.ts +1 -1
  404. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  405. package/src/runtime/routes/watch-routes.ts +50 -0
  406. package/src/runtime/sync/resource-sync-events.ts +14 -37
  407. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  408. package/src/runtime/web-presence.ts +13 -9
  409. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  410. package/src/tools/client-os.ts +11 -1
  411. package/src/tools/host-filesystem/edit.ts +2 -2
  412. package/src/tools/host-filesystem/read.ts +2 -2
  413. package/src/tools/host-filesystem/transfer.ts +2 -2
  414. package/src/tools/host-filesystem/write.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.ts +2 -2
  416. package/src/tools/skills/load.ts +1 -1
  417. package/src/tools/terminal/__tests__/safe-env.test.ts +60 -1
  418. package/src/tools/terminal/safe-env.ts +76 -1
  419. package/src/tools/tool-input-schemas.ts +2 -0
  420. package/src/tools/tool-manifest.ts +2 -0
  421. package/src/tools/ui-surface/definitions.ts +4 -3
  422. package/src/tools/watch/watch-retro-report.ts +205 -0
  423. package/src/watch/__tests__/watch-retro.test.ts +269 -40
  424. package/src/watch/__tests__/watch-session-manager.test.ts +75 -1
  425. package/src/watch/watch-retro.ts +191 -77
  426. package/src/watch/watch-session-manager.ts +17 -0
  427. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  428. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  429. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  430. package/src/workspace/migrations/registry.ts +4 -0
  431. package/docker-kata-pip-chroot.sh +0 -22
  432. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  433. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  434. package/src/api/events/conversation-list-invalidated.ts +0 -38
package/ARCHITECTURE.md CHANGED
@@ -120,7 +120,7 @@ Scoped approval grants allow a guardian's approval decision on one channel (e.g.
120
120
 
121
121
  ### Guardian Decision Primitive
122
122
 
123
- All guardian approval decisions — regardless of how they arrive — route through a single primitive in `src/approvals/guardian-decision-primitive.ts`, which centralizes decision logic for callback button handlers, the conversational approval engine, and channel reactions/text.
123
+ All guardian approval decisions, regardless of how they arrive, route through a single primitive in `src/approvals/guardian-decision-primitive.ts`, which centralizes decision logic for callback button handlers, the conversational approval engine, and channel text replies.
124
124
 
125
125
  **Core API:**
126
126
 
@@ -145,8 +145,8 @@ All guardian approval decisions — regardless of how they arrive — route thro
145
145
 
146
146
  **Text fallback path (always available):**
147
147
 
148
- - Every prompt includes a `requestCode` (6-char alphanumeric). Guardians can reply with `<requestCode> approve` or `<requestCode> reject` on any channel.
149
- - `access_request` prompts additionally embed explicit text directives in `questionText`: the request-code approve/reject directive and the `"open invite flow"` phrase for starting the Trusted Contacts invite flow.
148
+ - Every prompt includes a `requestCode` (6-char alphanumeric). Guardians can reply with `<requestCode> approve` or `<requestCode> reject` on any channel. The instruction saying so travels only in the card's `plainTextFallback`, which a transport appends when it sends text without buttons; composed copy and `questionText` never carry it.
149
+ - `access_request` prompts carry the request-code verify/trust/reject/block directive in the card's `plainTextFallback`, appended by a transport only when it sends text without buttons; the `"open invite flow"` phrase for starting the Trusted Contacts invite flow stays in the text on every surface because nothing offers a button for it.
150
150
  - `pending_question` prompts (voice-originated) support `<requestCode> <your answer>` for free-text answers.
151
151
  - The `routeGuardianReply` router processes text replies through a priority-ordered pipeline: callback parsing -> request code parsing -> NL classification. All paths converge on `applyGuardianDecision`.
152
152
 
@@ -199,7 +199,7 @@ The guardian request system provides a channel-agnostic, unified domain for all
199
199
 
200
200
  4. **Deterministic API (prompt listing and decision endpoints):** Desktop clients and API consumers use `GET /v1/guardian-actions/pending` and `POST /v1/guardian-actions/decision` (HTTP). These endpoints surface guardian requests alongside legacy pending interactions and channel approval records, with deduplication to avoid double-rendering.
201
201
 
202
- 5. **Buttons first, text fallback:** All request kinds (`tool_approval`, `pending_question`, `access_request`) are rendered as structured button cards when displayed in macOS guardian conversations. Each prompt also embeds deterministic text fallback instructions (request-code-based approve/reject directives, and for `access_request` the "open invite flow" phrase) so text-based channels and manual fallback always work. Code-only messages (just a request code without decision text) return clarification instead of auto-approving. Disambiguation with multiple pending requests stays fail-closed — no auto-resolve when the target is ambiguous.
202
+ 5. **Buttons first, text fallback:** All request kinds (`tool_approval`, `pending_question`, `access_request`) are rendered as structured button cards when displayed in macOS guardian conversations. Each card also carries deterministic text fallback instructions (request-code-based directives, and for `access_request` the "open invite flow" phrase) in its `plainTextFallback`, appended by a transport only when it sends text without buttons, so text-based channels and manual fallback always work without the instructions duplicating the buttons. Code-only messages (just a request code without decision text) return clarification instead of auto-approving. Disambiguation with multiple pending requests stays fail-closed: no auto-resolve when the target is ambiguous.
203
203
 
204
204
  **Resolver registry:** Kind-specific resolvers (`src/approvals/guardian-request-resolvers.ts`) handle side effects after CAS resolution. Built-in resolvers: `tool_approval` (channel/desktop approval path), `pending_question` (voice call question path), and `access_request` (trusted-contact verification session creation). New request kinds register resolvers without touching the core primitive.
205
205
 
package/Dockerfile CHANGED
@@ -143,6 +143,14 @@ COPY plugins/marketplace.json /app/assistant/src/cli/lib/bundled-marketplace.jso
143
143
  RUN printf '#!/usr/bin/env sh\nexec bun run /app/assistant/src/index.ts "$@"\n' > /usr/local/bin/assistant && \
144
144
  chmod +x /usr/local/bin/assistant
145
145
 
146
+ # The image ships no Node, so `bunx <pkg>` follows a bin's `#!/usr/bin/env node`
147
+ # shebang into this launcher. It hands off to a real Node when one is installed
148
+ # later and otherwise runs the bin under Bun with the auto-serve shim preloaded,
149
+ # so a CLI whose bundle default-exports an object with `fetch` exits instead of
150
+ # being turned into an HTTP server by `bun:main`. Installing it here also stops
151
+ # Bun synthesizing its own `node` shim in /tmp/bun-node-<hash> at daemon launch.
152
+ RUN ln -s /app/assistant/docker-node-launcher.sh /usr/local/bin/node
153
+
146
154
  # Create non-root user that also has sudo access so it can like install stuff
147
155
  RUN groupadd --system --gid 1001 assistant && \
148
156
  useradd --system --uid 1001 --gid assistant --create-home --shell /bin/bash assistant && \
@@ -186,53 +194,11 @@ RUN printf '%s\n' \
186
194
  >> /home/assistant/.bashrc && \
187
195
  chown assistant:assistant /home/assistant/.bashrc
188
196
 
189
- RUN printf '%s\n' \
190
- '#!/usr/bin/env sh' \
191
- 'set -eu' \
192
- '. /app/assistant/docker-kata-runtime-family.sh' \
193
- 'if ! vellum_is_kata_family_runtime; then' \
194
- ' exec /usr/bin/apt-get "$@"' \
195
- 'fi' \
196
- 'export DEBIAN_FRONTEND=noninteractive' \
197
- 'DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"' \
198
- '/app/assistant/docker-init-apt-root.sh' \
199
- 'if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/apt-get" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then' \
200
- ' exec chroot "${DATA_ROOT}" /usr/bin/apt-get "$@"' \
201
- 'fi' \
202
- 'exec /usr/bin/apt-get "$@"' \
203
- > /usr/local/bin/apt-get && \
204
- chmod +x /usr/local/bin/apt-get && \
205
- printf '%s\n' \
206
- '#!/usr/bin/env sh' \
207
- 'set -eu' \
208
- '. /app/assistant/docker-kata-runtime-family.sh' \
209
- 'if ! vellum_is_kata_family_runtime; then' \
210
- ' exec /usr/bin/apt "$@"' \
211
- 'fi' \
212
- 'export DEBIAN_FRONTEND=noninteractive' \
213
- 'DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"' \
214
- '/app/assistant/docker-init-apt-root.sh' \
215
- 'if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/apt" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then' \
216
- ' exec chroot "${DATA_ROOT}" /usr/bin/apt "$@"' \
217
- 'fi' \
218
- 'exec /usr/bin/apt "$@"' \
219
- > /usr/local/bin/apt && \
220
- chmod +x /usr/local/bin/apt && \
221
- printf '%s\n' \
222
- '#!/usr/bin/env sh' \
223
- 'set -eu' \
224
- '. /app/assistant/docker-kata-runtime-family.sh' \
225
- 'if ! vellum_is_kata_family_runtime; then' \
226
- ' exec /usr/bin/dpkg "$@"' \
227
- 'fi' \
228
- 'DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"' \
229
- '/app/assistant/docker-init-apt-root.sh' \
230
- 'if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/dpkg" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then' \
231
- ' exec chroot "${DATA_ROOT}" /usr/bin/dpkg "$@"' \
232
- 'fi' \
233
- 'exec /usr/bin/dpkg "$@"' \
234
- > /usr/local/bin/dpkg && \
235
- chmod +x /usr/local/bin/dpkg
197
+ # apt/apt-get/dpkg must persist like pip: on Kata-family runtimes the wrapper
198
+ # routes package operations into the persistent apt chroot.
199
+ RUN ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/apt-get && \
200
+ ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/apt && \
201
+ ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/dpkg
236
202
 
237
203
  # pip must persist like apt: on Kata-family runtimes the wrapper routes root
238
204
  # installs into the persistent apt chroot.
@@ -274,9 +240,12 @@ RUN chmod +x \
274
240
  /app/assistant/docker-entrypoint.sh \
275
241
  /app/assistant/docker-init-apt-root.sh \
276
242
  /app/assistant/docker-kata-apt-env.sh \
243
+ /app/assistant/docker-kata-apt-shims.sh \
244
+ /app/assistant/docker-kata-apt-wrapper.sh \
245
+ /app/assistant/docker-kata-chroot-exec.sh \
277
246
  /app/assistant/docker-kata-pip.sh \
278
- /app/assistant/docker-kata-pip-chroot.sh \
279
247
  /app/assistant/docker-kata-runtime-family.sh \
248
+ /app/assistant/docker-node-launcher.sh \
280
249
  /usr/local/bin/vellum-block-volume-common.sh \
281
250
  /usr/local/bin/vellum-block-volume-init.sh \
282
251
  /usr/local/bin/vellum-block-volume-mount.sh \
@@ -0,0 +1,38 @@
1
+ // Neuters Bun's implicit auto-serve of an entrypoint module whose default
2
+ // export carries a `fetch` method: `bun:main` calls `Bun.serve()` on that
3
+ // export, and the process then stays alive forever waiting on the socket.
4
+ //
5
+ // The assistant image ships no Node, so `bunx <pkg>` follows a bin's
6
+ // `#!/usr/bin/env node` shebang into the `node` launcher the Dockerfile
7
+ // installs, which is Bun with this file preloaded. Without the shim, any
8
+ // third-party CLI whose bundle ends in something like `export { cli as
9
+ // default }` prints its answer and then hangs until the bash tool times out.
10
+ //
11
+ // Only the implicit call is suppressed. Bun's auto-serve frame is `bun:main`,
12
+ // so an explicit `Bun.serve()` from user code (whose immediate caller frame is
13
+ // that user code) passes straight through to the real implementation.
14
+ const realServe = Bun.serve;
15
+
16
+ Bun.serve = function (options) {
17
+ const caller = ((new Error().stack || "").split("\n")[2] || "").trim();
18
+ if (!caller.startsWith("at bun:main")) {
19
+ return realServe.call(Bun, options);
20
+ }
21
+ // Bun announces the auto-started server on the next console.debug call.
22
+ const realDebug = console.debug;
23
+ console.debug = (...args) => {
24
+ console.debug = realDebug;
25
+ if (!String(args[0]).startsWith("Started ")) {
26
+ realDebug(...args);
27
+ }
28
+ };
29
+ return {
30
+ stop() {},
31
+ reload() {},
32
+ port: 0,
33
+ hostname: "localhost",
34
+ protocol: "http",
35
+ development: false,
36
+ url: new URL("http://localhost:0"),
37
+ };
38
+ };
@@ -11,7 +11,11 @@ KATA_APT_INIT_PID=""
11
11
  if vellum_is_kata_family_runtime && [ -x /app/assistant/docker-init-apt-root.sh ]; then
12
12
  export VELLUM_APT_DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"
13
13
  # Warm the chroot used by Kata-family apt wrappers without blocking assistant readiness.
14
- /app/assistant/docker-init-apt-root.sh &
14
+ (
15
+ /app/assistant/docker-init-apt-root.sh
16
+ # Refresh persisted wrapper-script shims before commands use the overlay.
17
+ /app/assistant/docker-kata-apt-shims.sh || true
18
+ ) &
15
19
  KATA_APT_INIT_PID="$!"
16
20
  fi
17
21
 
@@ -36,6 +36,9 @@ _vellum_kata_prepend_library_path() {
36
36
  esac
37
37
  }
38
38
 
39
+ # Shims for chroot wrapper scripts with hardcoded absolute paths must shadow
40
+ # the broken originals in the chroot bin dirs below (docker-kata-apt-shims.sh).
41
+ _vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/.host-shims"
39
42
  _vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/bin"
40
43
  _vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/usr/local/sbin"
41
44
  _vellum_kata_append_path "${VELLUM_APT_DATA_ROOT}/usr/local/bin"
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env sh
2
+ # Regenerates host-side shims for chroot-installed commands whose bin entry
3
+ # is a pure trampoline script exec'ing a hardcoded absolute path (Debian
4
+ # 7zip's /usr/bin/7z execs /usr/lib/7zip/7z, for example). Those targets only
5
+ # exist inside the chroot, so running the wrapper through the host PATH
6
+ # overlay fails; each shim execs the chroot-prefixed target instead.
7
+ # docker-kata-apt-env.sh and buildSanitizedEnv put the shim dir on PATH ahead
8
+ # of the chroot bin dirs so shims shadow the broken wrappers.
9
+ set -eu
10
+
11
+ DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"
12
+ SHIM_DIR="${DATA_ROOT}/.host-shims"
13
+ MARKER="# vellum-apt-shim:"
14
+ # dpkg-managed dirs that can hold trampolines (Debian policy keeps packages
15
+ # out of /usr/local; /bin and /sbin are usrmerge symlinks into these).
16
+ SCAN_DIRS="usr/bin usr/sbin usr/games"
17
+ # Every overlay bin dir in PATH priority order (usrmerge collapses bin/sbin
18
+ # into their usr counterparts): precedence must also see the /usr/local dirs
19
+ # that chroot pip installs populate, so a dpkg trampoline never shims over a
20
+ # higher-priority local command.
21
+ PRIORITY_DIRS="usr/bin usr/local/sbin usr/local/bin usr/sbin usr/games"
22
+
23
+ [ -d "${DATA_ROOT}/usr/bin" ] || exit 0
24
+ mkdir -p "${SHIM_DIR}"
25
+
26
+ # Scanning the bin dirs forks a few processes per file; skip it when the
27
+ # overlay state is unchanged (read-only invocations like `apt list` or
28
+ # `dpkg -l`). The stamp covers command names, links, effective modes, and small
29
+ # executable contents so reinstalls and pip changes invalidate stale shims.
30
+ STAMP_FILE="${SHIM_DIR}/.overlay-stamp"
31
+ STAMP="$(
32
+ {
33
+ md5sum "${DATA_ROOT}/var/lib/dpkg/status" 2>/dev/null
34
+ for d in ${PRIORITY_DIRS}; do
35
+ printf '%s\0' "${d}"
36
+ find "${DATA_ROOT}/${d}" -mindepth 1 -maxdepth 1 -printf '%P %y %m %l\0' 2>/dev/null | sort -z
37
+ find -L "${DATA_ROOT}/${d}" -mindepth 1 -maxdepth 1 -printf '%P %y %m\0' 2>/dev/null | sort -z
38
+ done
39
+ for d in ${SCAN_DIRS}; do
40
+ find -L "${DATA_ROOT}/${d}" -mindepth 1 -maxdepth 1 -type f -size -513c -perm /111 -print0 2>/dev/null |
41
+ sort -z |
42
+ xargs -0 -r md5sum -z
43
+ done
44
+ } | md5sum | cut -d' ' -f1
45
+ )"
46
+ if [ "${STAMP}" = "$(cat "${STAMP_FILE}" 2>/dev/null || true)" ]; then
47
+ exit 0
48
+ fi
49
+
50
+ # Prints the target of a pure trampoline: a tiny script whose body is nothing
51
+ # but the shebang, comments, blank lines, and a single `exec /abs/path "$@"`
52
+ # line. Wrappers that do anything else (export env vars, cd, pass extra
53
+ # flags) print nothing: a direct-exec shim would bypass that setup.
54
+ wrapper_target() {
55
+ [ -f "$1" ] && [ -x "$1" ] || return 0
56
+ [ "$(wc -c <"$1")" -le 512 ] || return 0
57
+ case "$(head -c 2 "$1" 2>/dev/null)" in
58
+ '#!') ;;
59
+ *) return 0 ;;
60
+ esac
61
+ body="$(sed -e '1d' -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' "$1")"
62
+ [ -n "${body}" ] || return 0
63
+ [ "$(printf '%s' "${body}" | wc -l)" -eq 0 ] || return 0
64
+ printf '%s\n' "${body}" | sed -n 's/^[[:space:]]*exec[[:space:]][[:space:]]*\(\/[^"[:space:]][^"[:space:]]*\)[[:space:]][[:space:]]*"\$@"[[:space:]]*$/\1/p'
65
+ }
66
+
67
+ # Highest-priority overlay dir that holds an entry for this shim name.
68
+ shim_source() {
69
+ for d in ${PRIORITY_DIRS}; do
70
+ candidate="${DATA_ROOT}/${d}/$1"
71
+ if [ -f "${candidate}" ] && [ -x "${candidate}" ]; then
72
+ printf '%s\n' "${candidate}"
73
+ return 0
74
+ fi
75
+ done
76
+ }
77
+
78
+ generation_failed=0
79
+ for d in ${SCAN_DIRS}; do
80
+ for bin in "${DATA_ROOT}/${d}/"*; do
81
+ # Shims outrank every chroot bin dir on PATH, so only the
82
+ # highest-priority entry for a name may produce one: a trampoline in a
83
+ # lower dir must not shadow a real command above it.
84
+ [ "$(shim_source "${bin##*/}")" = "${bin}" ] || continue
85
+ target="$(wrapper_target "${bin}")"
86
+ [ -n "${target}" ] || continue
87
+ # Only shim when the hardcoded path is broken on the host but real in
88
+ # the chroot.
89
+ if [ -e "${target}" ] || [ ! -x "${DATA_ROOT}${target}" ]; then
90
+ continue
91
+ fi
92
+ shim="${SHIM_DIR}/${bin##*/}"
93
+ if ! tmp="$(mktemp "${SHIM_DIR}/.shim.XXXXXX")"; then
94
+ generation_failed=1
95
+ continue
96
+ fi
97
+ if printf '%s\n' \
98
+ '#!/bin/sh' \
99
+ "${MARKER} ${target}" \
100
+ "exec \"${DATA_ROOT}${target}\" \"\$@\"" \
101
+ >"${tmp}" &&
102
+ chmod 0755 "${tmp}" &&
103
+ mv -f "${tmp}" "${shim}"; then
104
+ :
105
+ else
106
+ rm -f "${tmp}"
107
+ generation_failed=1
108
+ fi
109
+ done
110
+ done
111
+
112
+ if [ "${generation_failed}" -ne 0 ]; then
113
+ exit 1
114
+ fi
115
+
116
+ # Drop shims that remain stale after desired replacements are installed.
117
+ for shim in "${SHIM_DIR}"/*; do
118
+ [ -f "${shim}" ] || continue
119
+ grep -qs "^${MARKER}" "${shim}" || continue
120
+ target="$(sed -n "s|^${MARKER} ||p" "${shim}" | sed -n '1p')"
121
+ src="$(shim_source "$(basename "${shim}")")"
122
+ if [ -z "${target}" ] || [ -z "${src}" ] || [ ! -x "${DATA_ROOT}${target}" ] || [ "$(wrapper_target "${src}")" != "${target}" ]; then
123
+ rm -f "${shim}"
124
+ fi
125
+ done
126
+
127
+ printf '%s\n' "${STAMP}" >"${STAMP_FILE}"
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env sh
2
+ # apt/apt-get/dpkg wrapper (symlinked from /usr/local/bin). On Kata-family
3
+ # runtimes, package operations are routed into the persistent apt chroot so
4
+ # they survive machine saves; the image rootfs is discarded on every save.
5
+ # The chroot runs through docker-kata-chroot-exec.sh in a private mount
6
+ # namespace with /dev, /dev/pts and /proc available, so apt can allocate a
7
+ # pty for term.log and maintainer scripts see a normal system.
8
+ set -eu
9
+
10
+ TOOL_NAME="$(basename "$0")"
11
+ case "${TOOL_NAME}" in
12
+ apt | apt-get | dpkg) ;;
13
+ *) TOOL_NAME="apt-get" ;;
14
+ esac
15
+
16
+ . /app/assistant/docker-kata-runtime-family.sh
17
+
18
+ if ! vellum_is_kata_family_runtime; then
19
+ exec "/usr/bin/${TOOL_NAME}" "$@"
20
+ fi
21
+
22
+ if [ "${TOOL_NAME}" != "dpkg" ]; then
23
+ export DEBIAN_FRONTEND=noninteractive
24
+ fi
25
+ DATA_ROOT="${VELLUM_APT_DATA_ROOT:-/data/system}"
26
+
27
+ /app/assistant/docker-init-apt-root.sh
28
+ if [ -x "${DATA_ROOT}/bin/sh" ] && [ -x "${DATA_ROOT}/usr/bin/${TOOL_NAME}" ] && [ -f "${DATA_ROOT}/.rootfs-initialized" ] && ! grep -qs " ${DATA_ROOT} .*noexec" /proc/mounts; then
29
+ rc=0
30
+ # Platform kata pods are privileged inside their per-assistant VM, so
31
+ # unshare -m works there; if this environment can't create a mount
32
+ # namespace, fall back to a bare chroot: installs still persist, but with
33
+ # no /dev/pts or /proc apt logs a pty warning and the caller's cwd is not
34
+ # restored.
35
+ if unshare -m true 2>/dev/null; then
36
+ unshare -m /app/assistant/docker-kata-chroot-exec.sh "${DATA_ROOT}" "${PWD}" "/usr/bin/${TOOL_NAME}" "$@" || rc=$?
37
+ else
38
+ chroot "${DATA_ROOT}" "/usr/bin/${TOOL_NAME}" "$@" || rc=$?
39
+ fi
40
+ # Packages can install wrapper scripts that hardcode absolute paths which
41
+ # only resolve inside the chroot; refresh host-side shims for those.
42
+ /app/assistant/docker-kata-apt-shims.sh || true
43
+ exit "${rc}"
44
+ fi
45
+ exec "/usr/bin/${TOOL_NAME}" "$@"
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env sh
2
+ # Runs a command inside the persistent apt chroot. Must be invoked under
3
+ # `unshare -m` (see docker-kata-pip.sh and docker-kata-apt-wrapper.sh): the
4
+ # bind mounts below live in a private mount namespace so they disappear with
5
+ # the process and are never visible to other processes; a lingering /data
6
+ # bind inside the chroot would create a path cycle (/data/system/data/system/...)
7
+ # for anything walking /data.
8
+ set -eu
9
+
10
+ DATA_ROOT="$1"
11
+ CALLER_CWD="$2"
12
+ shift 2
13
+
14
+ for dir in /workspace /data /tmp /var/tmp; do
15
+ [ -d "${dir}" ] || continue
16
+ mkdir -p "${DATA_ROOT}${dir}"
17
+ mount --bind "${dir}" "${DATA_ROOT}${dir}" 2>/dev/null || true
18
+ done
19
+
20
+ # /proc and /dev make the chroot behave like a normal system: apt needs a pty
21
+ # (posix_openpt via /dev/ptmx) to write /var/log/apt/term.log, and package
22
+ # maintainer scripts routinely read /proc. The recursive /dev bind carries the
23
+ # container's /dev/pts along; the devpts mount is a fallback for environments
24
+ # where it did not produce one.
25
+ mkdir -p "${DATA_ROOT}/proc" "${DATA_ROOT}/dev"
26
+ mount --bind /proc "${DATA_ROOT}/proc" 2>/dev/null || true
27
+ mount --rbind /dev "${DATA_ROOT}/dev" 2>/dev/null || true
28
+ if [ ! -e "${DATA_ROOT}/dev/pts/ptmx" ]; then
29
+ mkdir -p "${DATA_ROOT}/dev/pts" 2>/dev/null || true
30
+ mount -t devpts -o gid=5,mode=620,ptmxmode=666 devpts "${DATA_ROOT}/dev/pts" 2>/dev/null || true
31
+ fi
32
+
33
+ # chroot(1) always chdirs to /; restore the caller's cwd when it exists inside
34
+ # the chroot so relative paths keep working.
35
+ exec chroot "${DATA_ROOT}" /bin/sh -c 'cd "$1" 2>/dev/null || cd /; shift; exec "$@"' sh "${CALLER_CWD}" "$@"
@@ -38,10 +38,16 @@ if [ "$(id -u)" = "0" ]; then
38
38
  # unshare -m works there; if this environment can't create a mount
39
39
  # namespace, fall back to a bare chroot — index installs still persist,
40
40
  # only caller-path installs lose visibility.
41
+ rc=0
41
42
  if unshare -m true 2>/dev/null; then
42
- exec unshare -m /app/assistant/docker-kata-pip-chroot.sh "${DATA_ROOT}" "${PWD}" "/usr/bin/${PIP_NAME}" "$@"
43
+ unshare -m /app/assistant/docker-kata-chroot-exec.sh "${DATA_ROOT}" "${PWD}" "/usr/bin/${PIP_NAME}" "$@" || rc=$?
44
+ else
45
+ chroot "${DATA_ROOT}" /bin/sh -c 'cd "$1" 2>/dev/null || cd /; shift; exec "$@"' sh "${PWD}" "/usr/bin/${PIP_NAME}" "$@" || rc=$?
43
46
  fi
44
- exec chroot "${DATA_ROOT}" /bin/sh -c 'cd "$1" 2>/dev/null || cd /; shift; exec "$@"' sh "${PWD}" "/usr/bin/${PIP_NAME}" "$@"
47
+ # pip can add usr/local commands that outrank existing shims on the
48
+ # PATH overlay; refresh so a stale shim never shadows them.
49
+ /app/assistant/docker-kata-apt-shims.sh || true
50
+ exit "${rc}"
45
51
  fi
46
52
  fi
47
53
  echo "Warning: persistent pip root unavailable; falling back to the image pip (installs will not survive a save)" >&2
@@ -0,0 +1,37 @@
1
+ #!/bin/sh
2
+ # Installed as /usr/local/bin/node. The image ships no Node, so `bunx <pkg>`
3
+ # follows a bin's `#!/usr/bin/env node` shebang here.
4
+ #
5
+ # A real Node interpreter always wins. One can appear after the image is built:
6
+ # `apt-get install nodejs` writes /usr/bin/node, and on Kata-family runtimes the
7
+ # persistent apt chroot puts it under $VELLUM_APT_DATA_ROOT/usr/bin, which the
8
+ # sandbox PATH carries. Delegating keeps Node CLIs on Node semantics.
9
+ #
10
+ # With no real Node on PATH the bin runs under Bun with the auto-serve shim
11
+ # preloaded, so a CLI whose default export carries a `fetch` method exits
12
+ # instead of being turned into an HTTP server by `bun:main`.
13
+ #
14
+ # Bun's own synthesized shim dirs (<temp>/bun-node-<hash>/node, symlinks to
15
+ # bun) are skipped: that `node` is Bun without the preload, which is the hang
16
+ # this launcher exists to prevent.
17
+
18
+ launcher=/usr/local/bin/node
19
+ shim=/app/assistant/docker-bun-no-autoserve.js
20
+
21
+ IFS=:
22
+ for dir in $PATH; do
23
+ [ -n "$dir" ] || continue
24
+ candidate="$dir/node"
25
+ case "$candidate" in
26
+ "$launcher" | */bun-node-*/node) continue ;;
27
+ esac
28
+ [ -f "$candidate" ] && [ -x "$candidate" ] || continue
29
+ if [ -L "$candidate" ] && [ "$(readlink -f "$candidate")" = "$launcher" ]; then
30
+ continue
31
+ fi
32
+ unset IFS
33
+ exec "$candidate" "$@"
34
+ done
35
+ unset IFS
36
+
37
+ exec /usr/local/bin/bun --preload="$shim" "$@"
@@ -170,10 +170,14 @@ Ingested pages carry provenance frontmatter with distinct consumers:
170
170
 
171
171
  ### Read paths
172
172
 
173
- - **v3 (live)**: per-turn lane selection over concept pages — dense/sparse
174
- retrieval (`substrate/sim.ts` over the concept-page collection),
175
- learned edges, entity/hot/fresh/core sets — rendered as the `<memory>`
176
- card by `v3/injector.ts`. The static `<info>` block
173
+ - **v3 (live)**: per-turn lane selection over concept pages (dense/sparse
174
+ retrieval via `substrate/sim.ts` over the concept-page collection,
175
+ learned edges, entity/hot/fresh/core sets) rendered as the `<memory>`
176
+ card by `v3/injector.ts`. Frozen cards stay on historical user messages
177
+ so the provider prefix stays cacheable. Each turn's `<memory_spotlight>`
178
+ stays on the user message that was sent with it. A new spotlight is
179
+ added only on the new tail, so older messages are not rewritten. The
180
+ static `<info>` block
177
181
  (`substrate/static-context.ts`: essentials/threads/recent/buffer) also
178
182
  injects whenever the substrate is active.
179
183
  - **v2 (transitional)**: activation/router engine in `v2/`
@@ -21,17 +21,17 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
21
21
  notification pipeline (notifications/emit-signal.ts →
22
22
  decision engine → destination resolver → decision-engine.ts → destination-resolver.ts →
23
23
  broadcaster builds the card context ONCE broadcaster.ts: resolveApprovalContext /
24
- (generic actions[] + plainTextFallback) resolveQuestionOptionsContext)
24
+ (generic actions[] + plainTextFallback) resolveQuestionContext)
25
25
  │
26
26
  ▼ per-channel rendering ONLY
27
27
  channel adapters (notifications/adapters/{telegram,slack,macos,platform},
28
28
  telegram: inline keyboard; slack: blocks; vellum: conversation card via approval-card-builder)
29
29
  card deliveries recorded per channel (guardian-delivery-recorder.ts → guardian_request_deliveries)
30
30
  │
31
- ▼ user responds: button tap / emoji reaction / "CODE <reply>" / bare text
31
+ ▼ user responds: button tap / "CODE <reply>" / bare text
32
32
  guardian reply router (runtime/guardian-reply-router.ts, invoked from
33
- reactions → callbacks → request codes → routes/inbound-stages/guardian-reply-intercept.ts,
34
- bare answer → explicit approve/reject → NL BEFORE background dispatch — replies to parked
33
+ callbacks → request codes → bare answer → routes/inbound-stages/guardian-reply-intercept.ts,
34
+ explicit approve/reject → NL BEFORE background dispatch: replies to parked
35
35
  │ prompts resolve inline, never deferred)
36
36
  ▼ one decision primitive
37
37
  applyGuardianDecision (approvals/guardian-decision-primitive.ts:
@@ -45,14 +45,14 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
45
45
 
46
46
  ## Who owns what
47
47
 
48
- | Concern | Owner | Never |
49
- | --------------------------------------- | -------------------------------------------------------- | ----------------------------------- |
50
- | The "what": card text, actions, options | broadcaster context build (once per broadcast) | built per-adapter |
51
- | The "how": channel-native rendering | `notifications/adapters/<channel>` | domain logic, payload parsing |
52
- | Request state | gateway `guardian_requests` (+ deliveries) | daemon-side request tables |
53
- | Decisions | `applyGuardianDecision` (CAS, atomic ACL outcome) | inline decision logic at call sites |
54
- | Kind-specific follow-through | resolver registry (`kind` → resolver) | switch statements in the router |
55
- | Reply understanding | guardian reply router (codes, buttons, reactions, modes) | per-feature inbound intercepts |
48
+ | Concern | Owner | Never |
49
+ | --------------------------------------- | ------------------------------------------------- | ----------------------------------- |
50
+ | The "what": card text, actions, options | broadcaster context build (once per broadcast) | built per-adapter |
51
+ | The "how": channel-native rendering | `notifications/adapters/<channel>` | domain logic, payload parsing |
52
+ | Request state | gateway `guardian_requests` (+ deliveries) | daemon-side request tables |
53
+ | Decisions | `applyGuardianDecision` (CAS, atomic ACL outcome) | inline decision logic at call sites |
54
+ | Kind-specific follow-through | resolver registry (`kind` → resolver) | switch statements in the router |
55
+ | Reply understanding | guardian reply router (codes, buttons, modes) | per-feature inbound intercepts |
56
56
 
57
57
  ## The canonical home-feed projection
58
58
 
@@ -131,6 +131,13 @@ routing after a restart even though the card is absent from the model's history.
131
131
  Two instruction modes exist per request kind (`notifications/guardian-question-mode.ts`):
132
132
  **approval** ("CODE approve" / approve–reject buttons) and **answer**
133
133
  ("CODE <your answer>" / option buttons). `pending_question` is answer-mode.
134
+ The "CODE <reply>" instruction lives in exactly one place: the
135
+ `plainTextFallback` the broadcaster builds beside the card's actions. Composed
136
+ copy never carries it (the decision engine strips a model's echo of it on
137
+ every channel), and a transport appends it only when it sends text without
138
+ buttons: a rich delivery that failed, or a request with no actions to draw,
139
+ which is how an option-less voice question stays answerable. Every surface
140
+ with buttons, and the bell, banner, and push, show the ask alone.
134
141
 
135
142
  ## Worked example: `ask_question` on a channel
136
143
 
@@ -186,8 +193,8 @@ chat the turn is running in. On Slack that chat can be a shared room, and the
186
193
  card carries the tool, a command preview and live buttons.
187
194
  `resolveGuardianPromptDelivery` addresses it to the guardian's bound DM
188
195
  instead, by chat id rather than user id because that address is written to the
189
- delivery row and read back to match reactions, scope plain-text replies and
190
- edit the decided card. It returns the address and its route together, since
196
+ delivery row and read back to scope plain-text replies and edit the decided
197
+ card. It returns the address and its route together, since
191
198
  the turn's own callback carries a `threadTs` naming a thread that does not
192
199
  exist in the DM. When no private address resolves it returns nothing and the
193
200
  prompt is left to the in-app confirmation, because the room is the disclosure
@@ -146,9 +146,13 @@ sqlite3 "file:$GW_DB?mode=ro" \
146
146
 
147
147
  ## 3b. Contact writes
148
148
 
149
- All write operations on contacts are gateway-owned. Use the gateway CLI, not
149
+ Contact writes are gateway-owned, and they reach it two ways. An operator ACL
150
+ write (a risk ceiling) goes through the gateway CLI directly, not through
150
151
  `assistant contacts`. From the host, run the same command via
151
- `vellum exec --service gateway --`.
152
+ `vellum exec --service gateway --`. A write that changes the contact graph
153
+ itself (the record, or a channel binding) goes through `assistant contacts`,
154
+ which opens a form in the guardian's app and writes nothing until they submit
155
+ it; the gateway still performs the write.
152
156
 
153
157
  ```bash
154
158
  gateway contacts set-risk-threshold <contactId> --threshold high
@@ -156,6 +160,13 @@ gateway contacts set-risk-threshold <contactId> --threshold high
156
160
 
157
161
  `inherit` clears the risk ceiling so the contact follows room and trust-class settings. Stored vocabulary is `none`, `low`, `medium`, or `high`.
158
162
 
163
+ Binding an address to a contact goes through the guardian instead of the operator. The command below opens a form in the guardian's app naming that contact, and the gateway writes the channel only when they submit it. An address already held by a different contact is refused, naming that contact.
164
+
165
+ ```bash
166
+ assistant contacts channels add <contactId> --channel <type> --address <addr>
167
+ assistant contacts channels add <contactId> --channel email --address alice@example.com
168
+ ```
169
+
159
170
  ## 4. Force-Revoke a Trusted Contact
160
171
 
161
172
  ### Via CLI (preferred)
@@ -24,13 +24,7 @@ Design doc defining how unknown users gain access to a Vellum assistant via chan
24
24
  2. **The message is denied at the ingress ACL.** The gateway resolves the actor against its ACL DB and stamps a trust verdict onto the inbound metadata; the daemon's ACL stage (`runtime/routes/inbound-stages/acl-enforcement.ts`) consumes that verdict, finds no active member, replies _"Hmm looks like you don't have access to talk to me. I'll let &lt;guardian&gt; know you tried talking to me and get back to you."_ and returns `{ denied: true, reason: 'not_a_member' }`.
25
25
  3. **Notification pipeline alerts the guardian.** The denial triggers `notifyGuardianOfAccessRequest()` (`runtime/access-request-helper.ts`), which creates an access request in the gateway (`guardian_requests`, `kind: 'access_request'`, `toolName: 'ingress_access_request'`, via the `channels/gateway-guardian-requests.ts` client) and calls `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'`. The notification routes through the decision engine to the guardian's surfaces (vellum app, Telegram, Slack, etc.). The guardian sees who is requesting access, including a request code for approve/reject and an `open invite flow` option to start the Trusted Contacts invite flow.
26
26
 
27
- **Access-request copy contract:** Every guardian-facing access-request notification must contain:
28
- 1. **Requester context** — best-available identity (display name, username, external ID, source channel), sanitized to prevent control-character injection.
29
- 2. **Request-code decision directive** — e.g., `Reply "A1B2C3 approve" to grant access or "A1B2C3 reject" to deny.`
30
- 3. **Invite directive** — the exact phrase `Reply "open invite flow" to start Trusted Contacts invite flow.`
31
- 4. **Revoked-member warning** (when applicable) — `Note: this user was previously revoked.`
32
-
33
- Model-generated phrasing is permitted for the surrounding copy, but a post-generation enforcement step in the decision engine validates that all required directive elements are present. If any are missing, the full deterministic contract text is appended. This ensures the guardian can always parse and act on the notification regardless of LLM output quality.
27
+ **Access-request copy contract:** Every guardian-facing access-request notification carries the requester context (`buildAccessRequestContextText`): best-available identity (display name, username, external ID, source channel) sanitized to prevent control-character injection, the message preview, and a revoked-member warning when applicable (`Note: this user was previously revoked.`). The context also carries the invite directive (`Reply "open invite flow" to start Trusted Contacts invite flow.`), because no surface offers a button for the invite flow and typing the phrase is the only way to start it. The typed-reply mechanics are separate (`buildAccessRequestReplyMechanics`): the request-code directive (`Reply "A1B2C3 verify" ... "A1B2C3 trust" ... "A1B2C3 reject" ... "A1B2C3 block"`, without `verify` when no handshake is offered). It rides only in the card's `plainTextFallback`; a transport appends it when it sends text without buttons, and the decision engine strips any copy of it the model wrote into the composed text, so surfaces with buttons show the context alone.
34
28
 
35
29
  **Guardian identity resolution** is anchored on the assistant's vellum principal (`resolveAnchoredGuardian`), so access requests cannot bind to stale or cross-assistant contacts:
36
30
  1. A source-channel guardian binding (from the gateway contact list) that matches the vellum anchor principal.
@@ -1,7 +1,13 @@
1
1
  import { AVATAR_COLORS } from "./colors.js";
2
2
  import type { CharacterComponents } from "./types.js";
3
3
 
4
- const SCLERA = "#F2F2F2";
4
+ /**
5
+ * The white of an eye. Exported because a consumer that reduces an eye pair to
6
+ * a silhouette (an Android themed-icon mask, say) has to tell the sclera paths
7
+ * from the pupils, and matching on this is the only way to do that from the
8
+ * path list alone.
9
+ */
10
+ export const SCLERA = "#F2F2F2";
5
11
  const PUPIL = "#1A1A1A";
6
12
 
7
13
  /**
@@ -17,3 +17,11 @@ export const AVATAR_COLORS: ColorDefinition[] = [
17
17
  { id: "teal", hex: "#0E9B8B" },
18
18
  { id: "yellow", hex: "#E9C91A" },
19
19
  ];
20
+
21
+ /** The palette hex for a colour id, or null for an id the palette does not have. */
22
+ export function accentHexForColorId(colorId: string | null | undefined): string | null {
23
+ if (!colorId) {
24
+ return null;
25
+ }
26
+ return AVATAR_COLORS.find((c) => c.id === colorId)?.hex ?? null;
27
+ }
@@ -22,4 +22,4 @@
22
22
  *
23
23
  * Leaf package: no dependencies, no runtime imports.
24
24
  */
25
- export { getCharacterComponents } from "./catalog.js";
25
+ export { getCharacterComponents, SCLERA } from "./catalog.js";