@vellumai/assistant 0.12.4 → 0.12.5-dev.202609250721.20229d0
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.
- package/docs/architecture/memory.md +9 -1
- package/docs/architecture/scheduling.md +13 -5
- package/docs/desktop-browser-cli.md +3 -2
- package/docs/guardian-request-flow.md +1 -1
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/openapi.yaml +152 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +98 -1
- package/src/__tests__/background-tool-registry.test.ts +39 -0
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
- package/src/__tests__/conversation-agent-loop.test.ts +529 -1
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-pairing.test.ts +69 -0
- package/src/__tests__/conversation-queue.test.ts +257 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
- package/src/__tests__/gemini-provider.test.ts +9 -0
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/introduction-card-resolver.test.ts +53 -8
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/non-member-access-request.test.ts +48 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
- package/src/__tests__/notification-platform-adapter.test.ts +97 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
- package/src/__tests__/scheduler-result-notification.test.ts +521 -7
- package/src/__tests__/scheduler-wake.test.ts +1 -0
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
- package/src/__tests__/subagent-manager-notify.test.ts +48 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
- package/src/agent/loop.ts +23 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/generation-handoff.ts +4 -0
- package/src/api/events/message-complete.ts +4 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +11 -0
- package/src/api/requests/guardian-actions.test.ts +45 -0
- package/src/api/requests/guardian-actions.ts +20 -0
- package/src/api/responses/conversation-message.ts +4 -0
- package/src/api/responses/home.ts +27 -0
- package/src/approvals/guardian-decision-primitive.ts +6 -4
- package/src/approvals/guardian-request-resolvers.ts +35 -30
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +12 -4
- package/src/calls/voice-escalation-judge.ts +2 -2
- package/src/calls/voice-leg-coordinator.ts +23 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +63 -20
- package/src/calls/voice-triage-escalate.ts +82 -27
- package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
- package/src/cli/commands/conversations.help.ts +7 -2
- package/src/cli/commands/conversations.ts +36 -30
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/bundled-marketplace.json +35 -0
- package/src/cli/lib/bundled-plugin-packages.json +21 -0
- package/src/cli/lib/plugin-catalog-cache.ts +17 -2
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/profile-order.test.ts +19 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
- package/src/config/call-site-defaults.ts +6 -0
- package/src/config/default-profile-catalog.ts +30 -1
- package/src/config/default-profile-names.ts +36 -2
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-order.ts +7 -1
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +21 -5
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
- package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
- package/src/daemon/activity-trail.test.ts +206 -0
- package/src/daemon/activity-trail.ts +190 -0
- package/src/daemon/auto-profile-router.ts +431 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +157 -32
- package/src/daemon/conversation-error.ts +19 -0
- package/src/daemon/conversation-lifecycle.ts +70 -7
- package/src/daemon/conversation-media-retry.ts +1 -2
- package/src/daemon/conversation-messaging.ts +31 -2
- package/src/daemon/conversation-process.ts +107 -16
- package/src/daemon/conversation-queue-manager.ts +7 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/conversation-turn-finalize.ts +14 -0
- package/src/daemon/conversation.ts +25 -6
- package/src/daemon/event-loop-watchdog.test.ts +201 -2
- package/src/daemon/event-loop-watchdog.ts +182 -6
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/wake-conversation-ops.ts +4 -2
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +25 -19
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/discord/withdraw.ts +4 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/messaging/providers/slack/withdraw.ts +5 -5
- package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
- package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
- package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
- package/src/monitoring/__tests__/process-usage.test.ts +243 -0
- package/src/monitoring/proc-wait-state.ts +366 -0
- package/src/monitoring/process-memory.ts +4 -3
- package/src/monitoring/process-usage.ts +215 -0
- package/src/monitoring/resource-sample-types.ts +8 -0
- package/src/monitoring/resource-sampler.ts +17 -2
- package/src/monitoring/stall-capture.ts +12 -0
- package/src/notifications/README.md +88 -9
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
- package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
- package/src/notifications/__tests__/broadcaster.test.ts +292 -1
- package/src/notifications/__tests__/completion-policy.test.ts +243 -0
- package/src/notifications/__tests__/decision-engine.test.ts +51 -4
- package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
- package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
- package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
- package/src/notifications/adapters/macos.ts +24 -12
- package/src/notifications/adapters/platform.ts +16 -5
- package/src/notifications/assistant-reply-producer.ts +96 -71
- package/src/notifications/background-result-producer.ts +369 -0
- package/src/notifications/broadcaster.ts +28 -8
- package/src/notifications/completion-policy.ts +115 -0
- package/src/notifications/completion-work.ts +46 -0
- package/src/notifications/conversation-pairing.ts +32 -13
- package/src/notifications/decision-engine.ts +18 -24
- package/src/notifications/emit-signal.ts +16 -1
- package/src/notifications/events-store.ts +4 -0
- package/src/notifications/has-pending-background-work.ts +91 -0
- package/src/notifications/home-feed-mirror.ts +44 -0
- package/src/notifications/home-feed-side-effect.ts +54 -28
- package/src/notifications/resolve-visible-in-source.ts +56 -2
- package/src/notifications/result-output.ts +154 -0
- package/src/notifications/schedule-result-producer.ts +6 -187
- package/src/notifications/types.ts +2 -0
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
- package/src/persistence/conversation-crud.ts +26 -0
- package/src/persistence/conversation-types.ts +15 -37
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/injector.ts +4 -4
- package/src/plugins/types.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +4 -0
- package/src/providers/gemini/client.ts +7 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/inference/profile-probe.ts +1 -0
- package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
- package/src/providers/openai/api-error-normalization.ts +4 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +7 -0
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/__tests__/agent-wake.test.ts +308 -0
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake-queue.ts +52 -0
- package/src/runtime/agent-wake.ts +111 -18
- package/src/runtime/channel-approval-types.ts +6 -38
- package/src/runtime/channel-approvals.ts +4 -5
- package/src/runtime/http-server.ts +4 -1
- package/src/runtime/introduction-policy.test.ts +17 -1
- package/src/runtime/introduction-policy.ts +25 -7
- package/src/runtime/migrations/vbundle-builder.ts +9 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/auto-profile-routes.ts +83 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -3
- package/src/runtime/routes/conversation-routes.ts +9 -0
- package/src/runtime/routes/guardian-action-routes.ts +8 -23
- package/src/runtime/routes/host-cu-routes.ts +3 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +5 -2
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/migration-routes.ts +8 -2
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/user-route-dispatcher.ts +1 -2
- package/src/runtime/web-presence.ts +3 -4
- package/src/schedule/schedule-store.ts +10 -0
- package/src/schedule/scheduler.ts +92 -24
- package/src/subagent/manager.ts +32 -12
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/tools/background-tool-registry.ts +30 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +7 -0
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/load.ts +13 -17
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/terminal/shell-launch.test.ts +27 -1
- package/src/tools/terminal/shell.ts +4 -0
- package/src/tools/workspace-tools/loader.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/abort-reasons.ts +2 -0
- package/src/util/errors.ts +1 -0
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/provider-error-patterns.ts +11 -0
- package/src/util/single-flight.test.ts +6 -0
- package/src/util/single-flight.ts +2 -0
- package/src/util/worker-pipe.test.ts +56 -10
- package/src/util/worker-pipe.ts +9 -4
- package/src/workspace/git-service.ts +6 -1
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- 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
|
|
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` |
|
|
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
|
|
57
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
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
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
|
3
|
-
* the
|
|
4
|
-
*
|
|
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
|
+
>;
|