@vellumai/assistant 0.11.8 → 0.11.9-staging.2

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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. 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
 
@@ -146,7 +146,7 @@ All guardian approval decisions — regardless of how they arrive — route thro
146
146
  **Text fallback path (always available):**
147
147
 
148
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.
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
 
package/Dockerfile CHANGED
@@ -186,53 +186,11 @@ RUN printf '%s\n' \
186
186
  >> /home/assistant/.bashrc && \
187
187
  chown assistant:assistant /home/assistant/.bashrc
188
188
 
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
189
+ # apt/apt-get/dpkg must persist like pip: on Kata-family runtimes the wrapper
190
+ # routes package operations into the persistent apt chroot.
191
+ RUN ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/apt-get && \
192
+ ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/apt && \
193
+ ln -s /app/assistant/docker-kata-apt-wrapper.sh /usr/local/bin/dpkg
236
194
 
237
195
  # pip must persist like apt: on Kata-family runtimes the wrapper routes root
238
196
  # installs into the persistent apt chroot.
@@ -274,8 +232,10 @@ RUN chmod +x \
274
232
  /app/assistant/docker-entrypoint.sh \
275
233
  /app/assistant/docker-init-apt-root.sh \
276
234
  /app/assistant/docker-kata-apt-env.sh \
235
+ /app/assistant/docker-kata-apt-shims.sh \
236
+ /app/assistant/docker-kata-apt-wrapper.sh \
237
+ /app/assistant/docker-kata-chroot-exec.sh \
277
238
  /app/assistant/docker-kata-pip.sh \
278
- /app/assistant/docker-kata-pip-chroot.sh \
279
239
  /app/assistant/docker-kata-runtime-family.sh \
280
240
  /usr/local/bin/vellum-block-volume-common.sh \
281
241
  /usr/local/bin/vellum-block-volume-init.sh \
@@ -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
@@ -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,10 @@ 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
+ Whether a channel's copy carries the "CODE <reply>" instruction at all is one
135
+ rule, `guardianCopyCarriesReplyMechanics`: text chats do, while the vellum
136
+ bell and banner, the platform push, and Slack approval cards act through the
137
+ card and have it stripped.
134
138
 
135
139
  ## Worked example: `ask_question` on a channel
136
140
 
@@ -186,8 +190,8 @@ chat the turn is running in. On Slack that chat can be a shared room, and the
186
190
  card carries the tool, a command preview and live buttons.
187
191
  `resolveGuardianPromptDelivery` addresses it to the guardian's bound DM
188
192
  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
193
+ delivery row and read back to scope plain-text replies and edit the decided
194
+ card. It returns the address and its route together, since
191
195
  the turn's own callback carries a `threadTs` naming a thread that does not
192
196
  exist in the DM. When no private address resolves it returns nothing and the
193
197
  prompt is left to the in-app confirmation, because the room is the disclosure
@@ -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
  /**
@@ -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";
@@ -22,7 +22,10 @@
22
22
  "./redacted-credential": "./src/redacted-credential.ts",
23
23
  "./secret-detection": "./src/secret-detection.ts",
24
24
  "./stripe-currency": "./src/stripe-currency.ts",
25
- "./error": "./src/error.ts"
25
+ "./error": "./src/error.ts",
26
+ "./reactions": "./src/reactions.ts",
27
+ "./guardian-requests": "./src/guardian-requests.ts",
28
+ "./platform-credential": "./src/platform-credential.ts"
26
29
  },
27
30
  "scripts": {
28
31
  "typecheck": "bunx tsc --noEmit",
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Canonical vocabulary for a guardian request's lifecycle status, shared by
3
+ * the gateway that owns the request rows, the daemon that decides and
4
+ * projects them, and the web that renders the projection.
5
+ */
6
+ import { z } from "zod";
7
+
8
+ export const GUARDIAN_REQUEST_STATUS_VALUES = [
9
+ "pending",
10
+ "approved",
11
+ "denied",
12
+ "expired",
13
+ "cancelled",
14
+ ] as const;
15
+ export const GuardianRequestStatusSchema = z.enum(
16
+ GUARDIAN_REQUEST_STATUS_VALUES,
17
+ );
18
+ export type GuardianRequestStatus = z.infer<typeof GuardianRequestStatusSchema>;
@@ -28,6 +28,7 @@ export * from "./rpc.js";
28
28
  export * from "./trust-rules.js";
29
29
  export * from "./ingress.js";
30
30
  export * from "./no-response.js";
31
+ export * from "./platform-credential.js";
31
32
  export * from "./remote-web-pairing.js";
32
33
  export * from "./twilio-ingress.js";
33
34
  export * from "./url-normalization.js";
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Wire contract for asking an assistant whether its stored platform-managed
3
+ * credential still authenticates.
4
+ *
5
+ * The daemon route is the authoritative serving side:
6
+ * - `POST /v1/platform/verify-credential`
7
+ * (`assistant/src/runtime/routes/platform-routes.ts`)
8
+ *
9
+ * The daemon's platform client produces the verdict, the route reports it,
10
+ * and the `vellum` CLI reads it before deciding whether a stored key can be
11
+ * re-injected (`cli/src/lib/assistant-api-key-resolution.ts`), so all three
12
+ * share one definition and cannot silently drift. The web app reads the same
13
+ * shape through its generated daemon client.
14
+ */
15
+ import { z } from "zod";
16
+
17
+ /**
18
+ * What the platform said about the stored credential, right now.
19
+ *
20
+ * - `valid`: the platform accepted it.
21
+ * - `rejected`: the platform refused it (unauthorized or forbidden); the
22
+ * credential needs replacing.
23
+ * - `unknown`: the check itself could not run (no credential stored, the
24
+ * platform unreachable, a server error). Not evidence either way.
25
+ */
26
+ export const PlatformCredentialVerificationStatusSchema = z.enum([
27
+ "valid",
28
+ "rejected",
29
+ "unknown",
30
+ ]);
31
+ export type PlatformCredentialVerificationStatus = z.infer<
32
+ typeof PlatformCredentialVerificationStatusSchema
33
+ >;
34
+
35
+ /** `POST /v1/platform/verify-credential` response body. */
36
+ export const PlatformVerifyCredentialResponseSchema = z.object({
37
+ status: PlatformCredentialVerificationStatusSchema,
38
+ });
39
+ export type PlatformVerifyCredentialResponse = z.infer<
40
+ typeof PlatformVerifyCredentialResponseSchema
41
+ >;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Canonical vocabulary for a reaction's emoji, shared by every service that
3
+ * handles one: the gateway that normalizes it, the daemon that stores and
4
+ * projects it, and the web that renders it.
5
+ *
6
+ * The kind is said by the channel rather than inferred from how the emoji
7
+ * is spelled. Modelled on Zulip's `reaction_type`, the one surveyed system
8
+ * that separates the namespace from the name.
9
+ *
10
+ * `shortcode` is a name in a channel's own namespace whose kind the channel
11
+ * does not disclose: Slack sends `+1` for the standard emoji and `blob_wave`
12
+ * for a workspace upload with nothing to tell them apart, and only the
13
+ * workspace token can resolve the second. It is a distinct kind from
14
+ * `unicode`, not a stand-in for an unknown one.
15
+ */
16
+ import { z } from "zod";
17
+
18
+ export const REACTION_EMOJI_KINDS = ["unicode", "shortcode", "custom"] as const;
19
+ export type ReactionEmojiKind = (typeof REACTION_EMOJI_KINDS)[number];
20
+
21
+ /**
22
+ * The typed emoji fields every schema that carries a reaction spreads in,
23
+ * so the wire contract, the stored envelopes, and the response projection
24
+ * describe one shape. Optional throughout: a persisted row or a replayed
25
+ * payload may carry only the spelling.
26
+ */
27
+ export const ReactionEmojiFieldsSchema = z.object({
28
+ /** Which namespace the emoji was drawn from. */
29
+ emojiKind: z.enum(REACTION_EMOJI_KINDS).optional(),
30
+ /**
31
+ * The emoji's name in that namespace: the character itself for `unicode`,
32
+ * the bare name for `shortcode` and `custom`. Never a mention form.
33
+ */
34
+ emojiName: z.string().optional(),
35
+ /** The channel's id for a `custom` emoji, absent for every other kind. */
36
+ emojiId: z.string().optional(),
37
+ /** Whether a `custom` emoji animates. Absent for every other kind. */
38
+ emojiAnimated: z.boolean().optional(),
39
+ });
40
+ export type ReactionEmojiFields = z.infer<typeof ReactionEmojiFieldsSchema>;
41
+
42
+ /**
43
+ * The typed emoji fields a source actually carries, with undefined ones
44
+ * omitted: an absent key and a present-but-undefined one serialize alike, but the
45
+ * stored envelope and the response should carry only what was declared. Every writer of a reaction shape (the wire
46
+ * payload, both stored envelopes, the response projection) copies the
47
+ * fields through this rather than restating the four-way pick.
48
+ */
49
+ export function pickReactionEmojiFields(
50
+ source: ReactionEmojiFields,
51
+ ): ReactionEmojiFields {
52
+ return {
53
+ ...(source.emojiKind !== undefined ? { emojiKind: source.emojiKind } : {}),
54
+ ...(source.emojiName !== undefined ? { emojiName: source.emojiName } : {}),
55
+ ...(source.emojiId !== undefined ? { emojiId: source.emojiId } : {}),
56
+ ...(source.emojiAnimated !== undefined
57
+ ? { emojiAnimated: source.emojiAnimated }
58
+ : {}),
59
+ };
60
+ }
@@ -22,7 +22,10 @@
22
22
  "./redacted-credential": "./src/redacted-credential.ts",
23
23
  "./secret-detection": "./src/secret-detection.ts",
24
24
  "./stripe-currency": "./src/stripe-currency.ts",
25
- "./error": "./src/error.ts"
25
+ "./error": "./src/error.ts",
26
+ "./reactions": "./src/reactions.ts",
27
+ "./guardian-requests": "./src/guardian-requests.ts",
28
+ "./platform-credential": "./src/platform-credential.ts"
26
29
  },
27
30
  "scripts": {
28
31
  "typecheck": "bunx tsc --noEmit",