@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c

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 (281) hide show
  1. package/docs/architecture/memory.md +9 -1
  2. package/docs/architecture/scheduling.md +13 -5
  3. package/docs/desktop-browser-cli.md +3 -2
  4. package/docs/guardian-request-flow.md +1 -1
  5. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  6. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  11. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  13. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  14. package/openapi.yaml +152 -3
  15. package/package.json +1 -1
  16. package/src/__tests__/agent-loop.test.ts +76 -0
  17. package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
  18. package/src/__tests__/background-shell-bash.test.ts +34 -0
  19. package/src/__tests__/background-shell-host-bash.test.ts +98 -1
  20. package/src/__tests__/background-tool-registry.test.ts +39 -0
  21. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  22. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  23. package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
  24. package/src/__tests__/conversation-agent-loop.test.ts +529 -1
  25. package/src/__tests__/conversation-error.test.ts +18 -0
  26. package/src/__tests__/conversation-pairing.test.ts +69 -0
  27. package/src/__tests__/conversation-queue.test.ts +257 -4
  28. package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
  29. package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
  30. package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
  31. package/src/__tests__/gemini-provider.test.ts +9 -0
  32. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  33. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  34. package/src/__tests__/introduction-card-resolver.test.ts +53 -8
  35. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  36. package/src/__tests__/non-member-access-request.test.ts +48 -0
  37. package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
  38. package/src/__tests__/notification-platform-adapter.test.ts +97 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
  40. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  41. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  42. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  43. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
  44. package/src/__tests__/scheduler-result-notification.test.ts +521 -7
  45. package/src/__tests__/scheduler-wake.test.ts +1 -0
  46. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  47. package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
  48. package/src/__tests__/subagent-manager-notify.test.ts +48 -0
  49. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
  50. package/src/__tests__/usage-attribution.test.ts +31 -0
  51. package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
  52. package/src/agent/loop.ts +23 -0
  53. package/src/api/ask-question-tool.ts +50 -0
  54. package/src/api/events/generation-handoff.ts +4 -0
  55. package/src/api/events/message-complete.ts +4 -0
  56. package/src/api/events/tool-result.ts +11 -0
  57. package/src/api/index.ts +11 -0
  58. package/src/api/requests/guardian-actions.test.ts +45 -0
  59. package/src/api/requests/guardian-actions.ts +20 -0
  60. package/src/api/responses/conversation-message.ts +4 -0
  61. package/src/api/responses/home.ts +27 -0
  62. package/src/approvals/guardian-decision-primitive.ts +6 -4
  63. package/src/approvals/guardian-request-resolvers.ts +35 -30
  64. package/src/avatar/notification-avatar.test.ts +42 -34
  65. package/src/avatar/notification-avatar.ts +1 -0
  66. package/src/calls/__tests__/call-controller.test.ts +18 -0
  67. package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
  68. package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
  69. package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
  70. package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
  71. package/src/calls/call-controller.ts +3 -1
  72. package/src/calls/voice-control-protocol.ts +12 -4
  73. package/src/calls/voice-escalation-judge.ts +2 -2
  74. package/src/calls/voice-leg-coordinator.ts +23 -5
  75. package/src/calls/voice-screen-annotation.ts +44 -0
  76. package/src/calls/voice-session-bridge.ts +63 -20
  77. package/src/calls/voice-triage-escalate.ts +82 -27
  78. package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
  79. package/src/cli/commands/conversations.help.ts +7 -2
  80. package/src/cli/commands/conversations.ts +36 -30
  81. package/src/cli/commands/oauth/index.help.ts +8 -2
  82. package/src/cli/commands/oauth/request.test.ts +32 -0
  83. package/src/cli/commands/platform/index.help.ts +2 -1
  84. package/src/cli/commands/platform/index.ts +8 -2
  85. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
  86. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  87. package/src/cli/lib/bundled-marketplace.json +35 -0
  88. package/src/cli/lib/bundled-plugin-packages.json +21 -0
  89. package/src/cli/lib/plugin-catalog-cache.ts +17 -2
  90. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  91. package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
  92. package/src/config/__tests__/platform-identity.test.ts +59 -0
  93. package/src/config/__tests__/profile-order.test.ts +19 -0
  94. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  95. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  96. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  97. package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
  98. package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
  99. package/src/config/call-site-defaults.ts +6 -0
  100. package/src/config/default-profile-catalog.ts +30 -1
  101. package/src/config/default-profile-names.ts +36 -2
  102. package/src/config/feature-flag-registry.json +9 -1
  103. package/src/config/platform-identity.ts +22 -5
  104. package/src/config/profile-order.ts +7 -1
  105. package/src/config/schemas/call-site-catalog.ts +7 -0
  106. package/src/config/schemas/llm.ts +21 -5
  107. package/src/config/sync-gated-profiles.ts +96 -44
  108. package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
  109. package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
  110. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
  111. package/src/daemon/activity-trail.test.ts +206 -0
  112. package/src/daemon/activity-trail.ts +190 -0
  113. package/src/daemon/auto-profile-router.ts +431 -0
  114. package/src/daemon/computer-use-observation.test.ts +81 -0
  115. package/src/daemon/computer-use-observation.ts +23 -0
  116. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  117. package/src/daemon/conversation-agent-loop.ts +157 -32
  118. package/src/daemon/conversation-error.ts +19 -0
  119. package/src/daemon/conversation-lifecycle.ts +70 -7
  120. package/src/daemon/conversation-media-retry.ts +1 -2
  121. package/src/daemon/conversation-messaging.ts +31 -2
  122. package/src/daemon/conversation-process.ts +107 -16
  123. package/src/daemon/conversation-queue-manager.ts +7 -0
  124. package/src/daemon/conversation-runtime-assembly.ts +1 -0
  125. package/src/daemon/conversation-turn-finalize.ts +14 -0
  126. package/src/daemon/conversation.ts +25 -6
  127. package/src/daemon/event-loop-watchdog.test.ts +201 -2
  128. package/src/daemon/event-loop-watchdog.ts +182 -6
  129. package/src/daemon/host-cu-proxy.ts +34 -3
  130. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  131. package/src/daemon/pkb-reminder-builder.ts +1 -1
  132. package/src/daemon/wake-conversation-ops.ts +4 -2
  133. package/src/desktop/desktop-computer-use.test.ts +14 -4
  134. package/src/home/feed-types.ts +2 -0
  135. package/src/inbound/platform-callback-registration.ts +19 -6
  136. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  137. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  138. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
  139. package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
  140. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  141. package/src/live-voice/continuation-judge.ts +1 -1
  142. package/src/live-voice/live-voice-session.ts +25 -19
  143. package/src/live-voice/protocol.ts +16 -5
  144. package/src/messaging/providers/discord/withdraw.ts +4 -5
  145. package/src/messaging/providers/gmail/client.ts +4 -0
  146. package/src/messaging/providers/slack/withdraw.ts +5 -5
  147. package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
  148. package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
  149. package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
  150. package/src/monitoring/__tests__/process-usage.test.ts +243 -0
  151. package/src/monitoring/proc-wait-state.ts +366 -0
  152. package/src/monitoring/process-memory.ts +4 -3
  153. package/src/monitoring/process-usage.ts +215 -0
  154. package/src/monitoring/resource-sample-types.ts +8 -0
  155. package/src/monitoring/resource-sampler.ts +17 -2
  156. package/src/monitoring/stall-capture.ts +12 -0
  157. package/src/notifications/README.md +88 -9
  158. package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
  159. package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
  160. package/src/notifications/__tests__/broadcaster.test.ts +292 -1
  161. package/src/notifications/__tests__/completion-policy.test.ts +243 -0
  162. package/src/notifications/__tests__/decision-engine.test.ts +51 -4
  163. package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
  164. package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
  165. package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
  166. package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
  167. package/src/notifications/adapters/macos.ts +24 -12
  168. package/src/notifications/adapters/platform.ts +16 -5
  169. package/src/notifications/assistant-reply-producer.ts +96 -71
  170. package/src/notifications/background-result-producer.ts +369 -0
  171. package/src/notifications/broadcaster.ts +28 -8
  172. package/src/notifications/completion-policy.ts +115 -0
  173. package/src/notifications/completion-work.ts +46 -0
  174. package/src/notifications/conversation-pairing.ts +32 -13
  175. package/src/notifications/decision-engine.ts +18 -24
  176. package/src/notifications/emit-signal.ts +16 -1
  177. package/src/notifications/events-store.ts +4 -0
  178. package/src/notifications/has-pending-background-work.ts +91 -0
  179. package/src/notifications/home-feed-mirror.ts +44 -0
  180. package/src/notifications/home-feed-side-effect.ts +54 -28
  181. package/src/notifications/resolve-visible-in-source.ts +56 -2
  182. package/src/notifications/result-output.ts +154 -0
  183. package/src/notifications/schedule-result-producer.ts +6 -187
  184. package/src/notifications/types.ts +2 -0
  185. package/src/oauth/AGENTS.md +9 -1
  186. package/src/oauth/byo-connection.test.ts +28 -0
  187. package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
  188. package/src/persistence/conversation-crud.ts +26 -0
  189. package/src/persistence/conversation-types.ts +15 -37
  190. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
  191. package/src/persistence/jobs-store.ts +229 -0
  192. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  193. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  194. package/src/persistence/steps.ts +12 -0
  195. package/src/platform/sync-avatar.test.ts +29 -0
  196. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  197. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  198. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  199. package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
  200. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  201. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  202. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
  203. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  204. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  205. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  206. package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
  207. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
  208. package/src/plugins/defaults/memory/v3/injector.ts +4 -4
  209. package/src/plugins/types.ts +2 -0
  210. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  211. package/src/providers/anthropic/client.ts +4 -0
  212. package/src/providers/gemini/client.ts +7 -1
  213. package/src/providers/inference/auth.ts +25 -0
  214. package/src/providers/inference/profile-probe.ts +1 -0
  215. package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
  216. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
  217. package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
  218. package/src/providers/openai/api-error-normalization.ts +4 -0
  219. package/src/providers/retry.ts +2 -0
  220. package/src/providers/search-provider-catalog.ts +10 -0
  221. package/src/providers/types.ts +7 -0
  222. package/src/providers/usage-tracking.ts +5 -3
  223. package/src/runtime/__tests__/agent-wake.test.ts +308 -0
  224. package/src/runtime/access-request-helper.ts +16 -9
  225. package/src/runtime/agent-wake-queue.ts +52 -0
  226. package/src/runtime/agent-wake.ts +111 -18
  227. package/src/runtime/channel-approval-types.ts +6 -38
  228. package/src/runtime/channel-approvals.ts +4 -5
  229. package/src/runtime/http-server.ts +4 -1
  230. package/src/runtime/introduction-policy.test.ts +17 -1
  231. package/src/runtime/introduction-policy.ts +25 -7
  232. package/src/runtime/migrations/vbundle-builder.ts +9 -0
  233. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
  234. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  235. package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
  236. package/src/runtime/routes/auto-profile-routes.ts +83 -0
  237. package/src/runtime/routes/conversation-management-routes.ts +1 -3
  238. package/src/runtime/routes/conversation-routes.ts +9 -0
  239. package/src/runtime/routes/guardian-action-routes.ts +8 -23
  240. package/src/runtime/routes/host-cu-routes.ts +3 -1
  241. package/src/runtime/routes/index.ts +2 -0
  242. package/src/runtime/routes/inference-profiles-routes.ts +5 -2
  243. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  244. package/src/runtime/routes/migration-routes.ts +8 -2
  245. package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
  246. package/src/runtime/routes/platform-routes.ts +6 -1
  247. package/src/runtime/routes/user-route-dispatcher.ts +1 -2
  248. package/src/runtime/web-presence.ts +3 -4
  249. package/src/schedule/schedule-store.ts +10 -0
  250. package/src/schedule/scheduler.ts +92 -24
  251. package/src/subagent/manager.ts +32 -12
  252. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  253. package/src/telemetry/telemetry-wire-source.json +1 -1
  254. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  255. package/src/tools/background-tool-registry.ts +30 -3
  256. package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
  257. package/src/tools/host-terminal/host-shell.ts +7 -0
  258. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  259. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  260. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  261. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  262. package/src/tools/network/web-fetch.ts +30 -9
  263. package/src/tools/network/web-search.ts +235 -4
  264. package/src/tools/skills/load.ts +13 -17
  265. package/src/tools/skills/scaffold-managed.ts +86 -71
  266. package/src/tools/terminal/shell-launch.test.ts +27 -1
  267. package/src/tools/terminal/shell.ts +4 -0
  268. package/src/tools/workspace-tools/loader.ts +1 -2
  269. package/src/usage/attribution.ts +18 -1
  270. package/src/util/abort-reasons.ts +2 -0
  271. package/src/util/errors.ts +1 -0
  272. package/src/util/oauth-request-body.test.ts +5 -1
  273. package/src/util/provider-error-patterns.ts +11 -0
  274. package/src/util/single-flight.test.ts +6 -0
  275. package/src/util/single-flight.ts +2 -0
  276. package/src/util/worker-pipe.test.ts +56 -10
  277. package/src/util/worker-pipe.ts +9 -4
  278. package/src/workspace/git-service.ts +6 -1
  279. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  280. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  281. package/src/workspace/migrations/registry.ts +2 -0
@@ -214,7 +214,15 @@ static substrate context remain available. When the answer depends on a saved pe
214
214
  fact that is absent from that context, the front-door rule escalates instead of
215
215
  guessing.
216
216
 
217
- The escalated leg runs the ordinary memory pipeline before the quality model.
217
+ The escalated leg normally runs the memory pipeline before the quality model.
218
+ During screen sharing, the front door can emit `[ESCALATE_SCREEN]` for an
219
+ annotation or immediate on-screen action answerable from the screen and
220
+ current conversation. The bridge skips fresh retrieval only when that verdict
221
+ and an active screen share are both present. This per-turn policy gates the
222
+ legacy hook and both v3 injectors, including re-injection, and clears at turn
223
+ release. Static context and historical memory blocks remain available.
224
+ Requests needing saved facts or preferences absent from context, uncertain
225
+ classifications, and ordinary `[ESCALATE]` verdicts retain retrieval.
218
226
  V3 retrieval routes on the latest visible caller message, ignoring the hidden
219
227
  continuation message used to start that leg. The selector uses low effort to
220
228
  keep retrieval latency bounded. This keeps current-turn memory work off the
@@ -2,6 +2,12 @@
2
2
 
3
3
  Recurring schedules, watchers, and queued task execution architecture.
4
4
 
5
+ ## Execute-mode completion and time limits
6
+
7
+ An execute-mode run owns its automatic result notification, including results from background commands and subagents. The scheduler waits for the firing's own active turns, queued continuations, commands, child tasks, and command wakes to settle before marking the run successful and emitting `schedule.result`. Unrelated work in a reused conversation does not extend the wait. Ownership is recorded before asynchronous turn setup, covers dequeued messages while their processing claim and persistence are pending, and follows wakes through queueing, hydration, and execution. Dequeued dispatches retain a cancellation signal for teardown until the active turn takes over. User interrupts preserve accepted user prompts during that handoff; started turns remain cancellable. Background work retains its original timing through queueing, dispatch, active processing, and command wakes. The reply-notification gate and persisted-history check apply that timing to suppress stale kickoff alerts without hiding replies to newer user requests. Quiet runs skip this fallback, and explicit successful delivery suppresses duplicates.
8
+
9
+ The wait shares the run's `timeouts.scheduleTurnTimeoutSec` budget. If delegated work exceeds that budget, the scheduler records an error, cancels only commands, child tasks, queued continuations, and the active turn owned by that run. User work and other runs in the same conversation remain intact. The attempt follows the existing retry and exhaustion-alert policy; partial output never emits a success notification. Scheduled command wakes carry `cronRunId`, check the persisted run status before starting, and observe cancellation while running, so a late callback cannot restart a failed attempt.
10
+
5
11
  ## Recurrence Schedules — Cron and RRULE Dual-Syntax Engine
6
12
 
7
13
  The scheduler supports two recurrence syntaxes for recurring tasks:
@@ -88,11 +94,13 @@ sequenceDiagram
88
94
 
89
95
  The `enforceRoutingIntent()` step runs after the LLM produces a channel selection but before deterministic checks. It acts as a post-decision guard:
90
96
 
91
- | Intent | Enforcement Rule |
92
- | ---------------- | ------------------------------------------------------------------------------------------------- |
93
- | `single_channel` | No override. The LLM's channel selection stands. |
94
- | `multi_channel` | If the LLM selected < 2 channels and 2+ are connected, expand to at least two connected channels. |
95
- | `all_channels` | Replace the LLM's selection with all connected channels. |
97
+ | Intent | Enforcement Rule |
98
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
99
+ | `single_channel` | Cap delivery to one channel: the signal's source channel when it is connected, otherwise the first channel the LLM selected. |
100
+ | `multi_channel` | If the LLM selected < 2 channels and 2+ are connected, expand to at least two connected channels. |
101
+ | `all_channels` | Replace the LLM's selection with all connected channels. |
102
+
103
+ A decision the LLM suppressed (`shouldNotify: false`) is left alone under every intent. A signal with no routing intent keeps the LLM's selection.
96
104
 
97
105
  When enforcement changes the decision, the updated `selectedChannels` and annotated `reasoningSummary` are re-persisted to `notification_decisions` so the audit trail reflects what was actually dispatched.
98
106
 
@@ -53,8 +53,9 @@ Non-platform assistants keep their connected-computer behavior.
53
53
  Explicit `target: "connected-computer"` or `target_client_id` selects a connected
54
54
  computer on every client. The selected target never falls back to another
55
55
  computer when unavailable. The virtual desktop uses a fixed 1440x810 display.
56
- Observations return full-screen screenshots and mouse actions use the same
57
- screen coordinates.
56
+ Observations return full-screen screenshots and mouse actions use screen
57
+ points. Screenshot metadata uses the transport-optimized image dimensions and
58
+ provides per-axis conversion from image pixels to those screen points.
58
59
  Viewer resizing scales the canvas locally without changing the desktop layout.
59
60
 
60
61
  Observe first and pass the returned `observation_id` with each native action.
@@ -24,7 +24,7 @@ anti-pattern was retired in #35642 and again in the ask_question redesign).
24
24
  (generic actions[] + plainTextFallback) resolveQuestionContext)
25
25
  │
26
26
  ▼ per-channel rendering ONLY
27
- channel adapters (notifications/adapters/{telegram,slack,macos,platform},
27
+ channel adapters (notifications/adapters/{telegram,slack,discord,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
  │
@@ -67,8 +67,31 @@ describe("notificationAvatarDiscHex", () => {
67
67
  });
68
68
 
69
69
  describe("notificationAvatarSvg", () => {
70
+ test("fills the entire circular crop with a custom image", () => {
71
+ const svg = notificationAvatarSvg({
72
+ kind: "image",
73
+ innerPngBase64: PNG_BASE64,
74
+ accentHex: "#E9642F",
75
+ });
76
+
77
+ expect(attributes("image", svg)).toMatchObject({
78
+ x: "0",
79
+ y: "0",
80
+ width: "256",
81
+ height: "256",
82
+ preserveAspectRatio: "xMidYMid slice",
83
+ "clip-path": "url(#notification-avatar-disc)",
84
+ });
85
+ expect(allAttributes("circle", svg)[1]).toEqual({
86
+ cx: "128",
87
+ cy: "128",
88
+ r: "128",
89
+ });
90
+ });
91
+
70
92
  test("draws one disc and one inset avatar at the default size", () => {
71
93
  const svg = notificationAvatarSvg({
94
+ kind: "character",
72
95
  innerPngBase64: PNG_BASE64,
73
96
  accentHex: "#E9642F",
74
97
  });
@@ -100,6 +123,7 @@ describe("notificationAvatarSvg", () => {
100
123
  // upload fill the square instead of letterboxing is the pair of attributes
101
124
  // asserted here.
102
125
  const svg = notificationAvatarSvg({
126
+ kind: "character",
103
127
  innerPngBase64: PNG_BASE64,
104
128
  accentHex: "#E9642F",
105
129
  });
@@ -121,6 +145,7 @@ describe("notificationAvatarSvg", () => {
121
145
 
122
146
  test("scales the geometry to a custom size", () => {
123
147
  const svg = notificationAvatarSvg({
148
+ kind: "character",
124
149
  innerPngBase64: PNG_BASE64,
125
150
  accentHex: "#E9642F",
126
151
  size: 100,
@@ -142,6 +167,7 @@ describe("notificationAvatarSvg", () => {
142
167
 
143
168
  test("carries a non-PNG inner raster with its own media type", () => {
144
169
  const svg = notificationAvatarSvg({
170
+ kind: "character",
145
171
  innerPngBase64: PNG_BASE64,
146
172
  innerMediaType: "image/jpeg",
147
173
  accentHex: "#E9642F",
@@ -154,6 +180,7 @@ describe("notificationAvatarSvg", () => {
154
180
 
155
181
  test("paints the fallback disc when there is no accent", () => {
156
182
  const svg = notificationAvatarSvg({
183
+ kind: "character",
157
184
  innerPngBase64: PNG_BASE64,
158
185
  accentHex: null,
159
186
  });
@@ -164,8 +191,21 @@ describe("notificationAvatarSvg", () => {
164
191
  });
165
192
 
166
193
  describe("notificationAvatarGeometry", () => {
194
+ test("custom images reach the disc edge at any output size", () => {
195
+ expect(notificationAvatarGeometry("image")).toEqual({
196
+ radius: 128,
197
+ offset: 0,
198
+ inner: 256,
199
+ });
200
+ expect(notificationAvatarGeometry("image", 100)).toEqual({
201
+ radius: 50,
202
+ offset: 0,
203
+ inner: 100,
204
+ });
205
+ });
206
+
167
207
  test("derives the disc, the border and the avatar edge from the size", () => {
168
- expect(notificationAvatarGeometry(100)).toEqual({
208
+ expect(notificationAvatarGeometry("character", 100)).toEqual({
169
209
  radius: 50,
170
210
  offset: 11,
171
211
  inner: 78,
@@ -173,15 +213,19 @@ describe("notificationAvatarGeometry", () => {
173
213
  });
174
214
 
175
215
  test("measures the default size when given none", () => {
176
- expect(notificationAvatarGeometry()).toEqual(
177
- notificationAvatarGeometry(NOTIFICATION_AVATAR_SIZE),
216
+ expect(notificationAvatarGeometry("character")).toEqual(
217
+ notificationAvatarGeometry("character", NOTIFICATION_AVATAR_SIZE),
178
218
  );
179
219
  });
180
220
 
181
221
  test("is the geometry the SVG is drawn with", () => {
182
222
  const size = 100;
183
- const { radius, offset, inner } = notificationAvatarGeometry(size);
223
+ const { radius, offset, inner } = notificationAvatarGeometry(
224
+ "character",
225
+ size,
226
+ );
184
227
  const svg = notificationAvatarSvg({
228
+ kind: "character",
185
229
  innerPngBase64: PNG_BASE64,
186
230
  accentHex: null,
187
231
  size,
@@ -15,11 +15,12 @@
15
15
  */
16
16
 
17
17
  import { isAvatarAccentHex } from "./accent.js";
18
+ import type { AvatarKind } from "./manifest.js";
18
19
 
19
20
  /** Output edge in pixels: large enough for an iOS notification thumbnail. */
20
21
  export const NOTIFICATION_AVATAR_SIZE = 256;
21
22
 
22
- /** Free disc on each side, as a fraction of the edge; the avatar gets the rest. */
23
+ /** Free disc on each side of a character avatar, as a fraction of the edge. */
23
24
  export const NOTIFICATION_AVATAR_INSET = 0.11;
24
25
 
25
26
  /** The disc when the assistant has no accent. */
@@ -48,7 +49,7 @@ export const NOTIFICATION_AVATAR_MAX_LOCAL_BYTES = 512 * 1024;
48
49
  * Bumped whenever the drawing above changes, so a sync keyed on it re-uploads
49
50
  * a disc rendered by an older spec even when the avatar itself is unchanged.
50
51
  */
51
- export const NOTIFICATION_AVATAR_SPEC_VERSION = 1;
52
+ export const NOTIFICATION_AVATAR_SPEC_VERSION = 2;
52
53
 
53
54
  /** The id the inner raster's clip path is referenced by inside the document. */
54
55
  const CLIP_ID = "notification-avatar-disc";
@@ -74,24 +75,26 @@ export function notificationAvatarDiscHex(accentHex: string | null): string {
74
75
 
75
76
  /** Raster formats an `<image>` href carries here; what resvg and a canvas both decode. */
76
77
  export type NotificationAvatarMediaType =
77
- | "image/png"
78
- | "image/jpeg"
79
- | "image/gif";
78
+ "image/png" | "image/jpeg" | "image/gif";
80
79
 
81
80
  /**
82
81
  * The three measurements the drawing derives from the edge: the disc's radius,
83
82
  * which is also its centre; the free border around the avatar; and the
84
83
  * avatar's own edge. Every rasterizer reads them from here, so the daemon's
85
84
  * SVG and the desktop canvas cannot drift.
85
+ * Custom images fill the disc; character avatars retain space around their
86
+ * silhouette.
86
87
  */
87
88
  export function notificationAvatarGeometry(
89
+ kind: Exclude<AvatarKind, "none">,
88
90
  size: number = NOTIFICATION_AVATAR_SIZE,
89
91
  ): { radius: number; offset: number; inner: number } {
90
- const offset = size * NOTIFICATION_AVATAR_INSET;
92
+ const offset = kind === "character" ? size * NOTIFICATION_AVATAR_INSET : 0;
91
93
  return { radius: size / 2, offset, inner: size - 2 * offset };
92
94
  }
93
95
 
94
96
  export interface NotificationAvatarSvgOptions {
97
+ kind: Exclude<AvatarKind, "none">;
95
98
  /** The avatar raster to draw inside the disc, base64 with no data prefix. */
96
99
  innerPngBase64: string;
97
100
  /** What `innerPngBase64` holds; PNG unless an upload arrived as a JPEG or a GIF. */
@@ -107,7 +110,7 @@ function px(value: number): string {
107
110
 
108
111
  /**
109
112
  * The notification avatar as an SVG document: a filled disc with the avatar
110
- * drawn inset into it.
113
+ * filling it for custom images or inset for character avatars.
111
114
  *
112
115
  * The inner raster is cover-cropped (`xMidYMid slice`), matching what the web
113
116
  * canvas does, so a portrait or landscape upload fills the square instead of
@@ -119,12 +122,13 @@ function px(value: number): string {
119
122
  * rasterizer is known to render it.
120
123
  */
121
124
  export function notificationAvatarSvg({
125
+ kind,
122
126
  innerPngBase64,
123
127
  innerMediaType = "image/png",
124
128
  accentHex,
125
129
  size = NOTIFICATION_AVATAR_SIZE,
126
130
  }: NotificationAvatarSvgOptions): string {
127
- const { radius, offset, inner } = notificationAvatarGeometry(size);
131
+ const { radius, offset, inner } = notificationAvatarGeometry(kind, size);
128
132
  const href = `data:${innerMediaType};base64,${innerPngBase64}`;
129
133
  const disc = `cx="${px(radius)}" cy="${px(radius)}" r="${px(radius)}"`;
130
134
  return (
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ GUARDIAN_DECISION_ACTION_IDS,
5
+ isDenyingGuardianAction,
6
+ isParkGuardianAction,
7
+ } from "../guardian-requests.js";
8
+
9
+ describe("guardian decision actions", () => {
10
+ test("reject, leave_unverified and block deny; every other action approves", () => {
11
+ const denying = GUARDIAN_DECISION_ACTION_IDS.filter(
12
+ isDenyingGuardianAction,
13
+ );
14
+ expect(denying).toEqual(["reject", "leave_unverified", "block"]);
15
+ });
16
+
17
+ test("only leave_unverified parks the sender", () => {
18
+ const parking = GUARDIAN_DECISION_ACTION_IDS.filter(isParkGuardianAction);
19
+ expect(parking).toEqual(["leave_unverified"]);
20
+ });
21
+
22
+ test("an absent or unknown action neither denies nor parks", () => {
23
+ expect(isDenyingGuardianAction(undefined)).toBe(false);
24
+ expect(isParkGuardianAction("approve_always")).toBe(false);
25
+ });
26
+ });
@@ -1,7 +1,8 @@
1
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.
2
+ * Canonical vocabulary for guardian requests, shared by the gateway that owns
3
+ * the request rows, the daemon that decides and projects them, and the web
4
+ * that renders the projection: a request's lifecycle status, the actions a
5
+ * guardian can decide it with, and the weight a card gives each action.
5
6
  */
6
7
  import { z } from "zod";
7
8
 
@@ -16,3 +17,76 @@ export const GuardianRequestStatusSchema = z.enum(
16
17
  GUARDIAN_REQUEST_STATUS_VALUES,
17
18
  );
18
19
  export type GuardianRequestStatus = z.infer<typeof GuardianRequestStatusSchema>;
20
+
21
+ /**
22
+ * The actions a guardian can decide a request with.
23
+ *
24
+ * `approve_once` / `reject` are the generic decision pair used by every
25
+ * request kind. `trust` / `verify_code` / `leave_unverified` / `block` are the
26
+ * introduction-card actions, valid only for `access_request` requests.
27
+ */
28
+ export const GUARDIAN_DECISION_ACTION_IDS = [
29
+ "approve_once",
30
+ "reject",
31
+ "trust",
32
+ "verify_code",
33
+ "leave_unverified",
34
+ "block",
35
+ ] as const;
36
+ export const GuardianDecisionActionIdSchema = z.enum(
37
+ GUARDIAN_DECISION_ACTION_IDS,
38
+ );
39
+ export type GuardianDecisionActionId = z.infer<
40
+ typeof GuardianDecisionActionIdSchema
41
+ >;
42
+
43
+ /** Actions that resolve a request to `denied`; every other one approves. */
44
+ export const GUARDIAN_DENYING_ACTION_VALUES = [
45
+ "reject",
46
+ "leave_unverified",
47
+ "block",
48
+ ] as const satisfies readonly GuardianDecisionActionId[];
49
+
50
+ /**
51
+ * The denying actions that park the sender at `unverified`: a neutral hold,
52
+ * not a rejection. A parked contact is still admitted under the permissive
53
+ * admission floors (`any_contact`, `strangers`) and can be trusted or verified
54
+ * later; contrast `block` (revoked, a hard keep-out) and `reject` (an explicit
55
+ * decline). All three resolve the request to `denied`, so a resolved card
56
+ * consults this to read a park neutrally rather than as a denial.
57
+ */
58
+ export const GUARDIAN_PARK_ACTION_VALUES = [
59
+ "leave_unverified",
60
+ ] as const satisfies readonly GuardianDecisionActionId[];
61
+
62
+ const DENYING_ACTIONS: ReadonlySet<string> = new Set(
63
+ GUARDIAN_DENYING_ACTION_VALUES,
64
+ );
65
+ const PARK_ACTIONS: ReadonlySet<string> = new Set(GUARDIAN_PARK_ACTION_VALUES);
66
+
67
+ /** True when `action` resolves a request to `denied`. */
68
+ export function isDenyingGuardianAction(action: string | undefined): boolean {
69
+ return action !== undefined && DENYING_ACTIONS.has(action);
70
+ }
71
+
72
+ /** True when `action` parks the sender at `unverified`. */
73
+ export function isParkGuardianAction(action: string | undefined): boolean {
74
+ return action !== undefined && PARK_ACTIONS.has(action);
75
+ }
76
+
77
+ /**
78
+ * Surface-agnostic weight of a card action. Each renderer translates it to
79
+ * its own token (Slack primary/danger, a web button variant); absent means the
80
+ * renderer's default.
81
+ */
82
+ export const GUARDIAN_ACTION_EMPHASIS_VALUES = [
83
+ "primary",
84
+ "secondary",
85
+ "destructive",
86
+ ] as const;
87
+ export const GuardianActionEmphasisSchema = z.enum(
88
+ GUARDIAN_ACTION_EMPHASIS_VALUES,
89
+ );
90
+ export type GuardianActionEmphasis = z.infer<
91
+ typeof GuardianActionEmphasisSchema
92
+ >;
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ GUARDIAN_DECISION_ACTION_IDS,
5
+ isDenyingGuardianAction,
6
+ isParkGuardianAction,
7
+ } from "../guardian-requests.js";
8
+
9
+ describe("guardian decision actions", () => {
10
+ test("reject, leave_unverified and block deny; every other action approves", () => {
11
+ const denying = GUARDIAN_DECISION_ACTION_IDS.filter(
12
+ isDenyingGuardianAction,
13
+ );
14
+ expect(denying).toEqual(["reject", "leave_unverified", "block"]);
15
+ });
16
+
17
+ test("only leave_unverified parks the sender", () => {
18
+ const parking = GUARDIAN_DECISION_ACTION_IDS.filter(isParkGuardianAction);
19
+ expect(parking).toEqual(["leave_unverified"]);
20
+ });
21
+
22
+ test("an absent or unknown action neither denies nor parks", () => {
23
+ expect(isDenyingGuardianAction(undefined)).toBe(false);
24
+ expect(isParkGuardianAction("approve_always")).toBe(false);
25
+ });
26
+ });
@@ -1,7 +1,8 @@
1
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.
2
+ * Canonical vocabulary for guardian requests, shared by the gateway that owns
3
+ * the request rows, the daemon that decides and projects them, and the web
4
+ * that renders the projection: a request's lifecycle status, the actions a
5
+ * guardian can decide it with, and the weight a card gives each action.
5
6
  */
6
7
  import { z } from "zod";
7
8
 
@@ -16,3 +17,76 @@ export const GuardianRequestStatusSchema = z.enum(
16
17
  GUARDIAN_REQUEST_STATUS_VALUES,
17
18
  );
18
19
  export type GuardianRequestStatus = z.infer<typeof GuardianRequestStatusSchema>;
20
+
21
+ /**
22
+ * The actions a guardian can decide a request with.
23
+ *
24
+ * `approve_once` / `reject` are the generic decision pair used by every
25
+ * request kind. `trust` / `verify_code` / `leave_unverified` / `block` are the
26
+ * introduction-card actions, valid only for `access_request` requests.
27
+ */
28
+ export const GUARDIAN_DECISION_ACTION_IDS = [
29
+ "approve_once",
30
+ "reject",
31
+ "trust",
32
+ "verify_code",
33
+ "leave_unverified",
34
+ "block",
35
+ ] as const;
36
+ export const GuardianDecisionActionIdSchema = z.enum(
37
+ GUARDIAN_DECISION_ACTION_IDS,
38
+ );
39
+ export type GuardianDecisionActionId = z.infer<
40
+ typeof GuardianDecisionActionIdSchema
41
+ >;
42
+
43
+ /** Actions that resolve a request to `denied`; every other one approves. */
44
+ export const GUARDIAN_DENYING_ACTION_VALUES = [
45
+ "reject",
46
+ "leave_unverified",
47
+ "block",
48
+ ] as const satisfies readonly GuardianDecisionActionId[];
49
+
50
+ /**
51
+ * The denying actions that park the sender at `unverified`: a neutral hold,
52
+ * not a rejection. A parked contact is still admitted under the permissive
53
+ * admission floors (`any_contact`, `strangers`) and can be trusted or verified
54
+ * later; contrast `block` (revoked, a hard keep-out) and `reject` (an explicit
55
+ * decline). All three resolve the request to `denied`, so a resolved card
56
+ * consults this to read a park neutrally rather than as a denial.
57
+ */
58
+ export const GUARDIAN_PARK_ACTION_VALUES = [
59
+ "leave_unverified",
60
+ ] as const satisfies readonly GuardianDecisionActionId[];
61
+
62
+ const DENYING_ACTIONS: ReadonlySet<string> = new Set(
63
+ GUARDIAN_DENYING_ACTION_VALUES,
64
+ );
65
+ const PARK_ACTIONS: ReadonlySet<string> = new Set(GUARDIAN_PARK_ACTION_VALUES);
66
+
67
+ /** True when `action` resolves a request to `denied`. */
68
+ export function isDenyingGuardianAction(action: string | undefined): boolean {
69
+ return action !== undefined && DENYING_ACTIONS.has(action);
70
+ }
71
+
72
+ /** True when `action` parks the sender at `unverified`. */
73
+ export function isParkGuardianAction(action: string | undefined): boolean {
74
+ return action !== undefined && PARK_ACTIONS.has(action);
75
+ }
76
+
77
+ /**
78
+ * Surface-agnostic weight of a card action. Each renderer translates it to
79
+ * its own token (Slack primary/danger, a web button variant); absent means the
80
+ * renderer's default.
81
+ */
82
+ export const GUARDIAN_ACTION_EMPHASIS_VALUES = [
83
+ "primary",
84
+ "secondary",
85
+ "destructive",
86
+ ] as const;
87
+ export const GuardianActionEmphasisSchema = z.enum(
88
+ GUARDIAN_ACTION_EMPHASIS_VALUES,
89
+ );
90
+ export type GuardianActionEmphasis = z.infer<
91
+ typeof GuardianActionEmphasisSchema
92
+ >;
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { KnownBlock } from "@slack/types";
18
+ import { GuardianActionEmphasisSchema } from "@vellumai/service-contracts/guardian-requests";
18
19
  import { z } from "zod";
19
20
 
20
21
  // ---------------------------------------------------------------------------
@@ -41,10 +42,7 @@ export type AttachmentMetadata = z.infer<typeof AttachmentMetadataSchema>;
41
42
  export const ApprovalActionOptionSchema = z.object({
42
43
  id: z.string(),
43
44
  label: z.string(),
44
- // Surface-agnostic button weight. Renderers translate it to their platform
45
- // token (Slack primary/danger, Surface primary/destructive); absent means
46
- // the renderer applies its own default styling.
47
- emphasis: z.enum(["primary", "secondary", "destructive"]).optional(),
45
+ emphasis: GuardianActionEmphasisSchema.optional(),
48
46
  });
49
47
 
50
48
  export type ApprovalActionOption = z.infer<typeof ApprovalActionOptionSchema>;
@@ -0,0 +1,26 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ GUARDIAN_DECISION_ACTION_IDS,
5
+ isDenyingGuardianAction,
6
+ isParkGuardianAction,
7
+ } from "../guardian-requests.js";
8
+
9
+ describe("guardian decision actions", () => {
10
+ test("reject, leave_unverified and block deny; every other action approves", () => {
11
+ const denying = GUARDIAN_DECISION_ACTION_IDS.filter(
12
+ isDenyingGuardianAction,
13
+ );
14
+ expect(denying).toEqual(["reject", "leave_unverified", "block"]);
15
+ });
16
+
17
+ test("only leave_unverified parks the sender", () => {
18
+ const parking = GUARDIAN_DECISION_ACTION_IDS.filter(isParkGuardianAction);
19
+ expect(parking).toEqual(["leave_unverified"]);
20
+ });
21
+
22
+ test("an absent or unknown action neither denies nor parks", () => {
23
+ expect(isDenyingGuardianAction(undefined)).toBe(false);
24
+ expect(isParkGuardianAction("approve_always")).toBe(false);
25
+ });
26
+ });
@@ -1,7 +1,8 @@
1
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.
2
+ * Canonical vocabulary for guardian requests, shared by the gateway that owns
3
+ * the request rows, the daemon that decides and projects them, and the web
4
+ * that renders the projection: a request's lifecycle status, the actions a
5
+ * guardian can decide it with, and the weight a card gives each action.
5
6
  */
6
7
  import { z } from "zod";
7
8
 
@@ -16,3 +17,76 @@ export const GuardianRequestStatusSchema = z.enum(
16
17
  GUARDIAN_REQUEST_STATUS_VALUES,
17
18
  );
18
19
  export type GuardianRequestStatus = z.infer<typeof GuardianRequestStatusSchema>;
20
+
21
+ /**
22
+ * The actions a guardian can decide a request with.
23
+ *
24
+ * `approve_once` / `reject` are the generic decision pair used by every
25
+ * request kind. `trust` / `verify_code` / `leave_unverified` / `block` are the
26
+ * introduction-card actions, valid only for `access_request` requests.
27
+ */
28
+ export const GUARDIAN_DECISION_ACTION_IDS = [
29
+ "approve_once",
30
+ "reject",
31
+ "trust",
32
+ "verify_code",
33
+ "leave_unverified",
34
+ "block",
35
+ ] as const;
36
+ export const GuardianDecisionActionIdSchema = z.enum(
37
+ GUARDIAN_DECISION_ACTION_IDS,
38
+ );
39
+ export type GuardianDecisionActionId = z.infer<
40
+ typeof GuardianDecisionActionIdSchema
41
+ >;
42
+
43
+ /** Actions that resolve a request to `denied`; every other one approves. */
44
+ export const GUARDIAN_DENYING_ACTION_VALUES = [
45
+ "reject",
46
+ "leave_unverified",
47
+ "block",
48
+ ] as const satisfies readonly GuardianDecisionActionId[];
49
+
50
+ /**
51
+ * The denying actions that park the sender at `unverified`: a neutral hold,
52
+ * not a rejection. A parked contact is still admitted under the permissive
53
+ * admission floors (`any_contact`, `strangers`) and can be trusted or verified
54
+ * later; contrast `block` (revoked, a hard keep-out) and `reject` (an explicit
55
+ * decline). All three resolve the request to `denied`, so a resolved card
56
+ * consults this to read a park neutrally rather than as a denial.
57
+ */
58
+ export const GUARDIAN_PARK_ACTION_VALUES = [
59
+ "leave_unverified",
60
+ ] as const satisfies readonly GuardianDecisionActionId[];
61
+
62
+ const DENYING_ACTIONS: ReadonlySet<string> = new Set(
63
+ GUARDIAN_DENYING_ACTION_VALUES,
64
+ );
65
+ const PARK_ACTIONS: ReadonlySet<string> = new Set(GUARDIAN_PARK_ACTION_VALUES);
66
+
67
+ /** True when `action` resolves a request to `denied`. */
68
+ export function isDenyingGuardianAction(action: string | undefined): boolean {
69
+ return action !== undefined && DENYING_ACTIONS.has(action);
70
+ }
71
+
72
+ /** True when `action` parks the sender at `unverified`. */
73
+ export function isParkGuardianAction(action: string | undefined): boolean {
74
+ return action !== undefined && PARK_ACTIONS.has(action);
75
+ }
76
+
77
+ /**
78
+ * Surface-agnostic weight of a card action. Each renderer translates it to
79
+ * its own token (Slack primary/danger, a web button variant); absent means the
80
+ * renderer's default.
81
+ */
82
+ export const GUARDIAN_ACTION_EMPHASIS_VALUES = [
83
+ "primary",
84
+ "secondary",
85
+ "destructive",
86
+ ] as const;
87
+ export const GuardianActionEmphasisSchema = z.enum(
88
+ GUARDIAN_ACTION_EMPHASIS_VALUES,
89
+ );
90
+ export type GuardianActionEmphasis = z.infer<
91
+ typeof GuardianActionEmphasisSchema
92
+ >;