@vellumai/assistant 0.3.15 → 0.3.16
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/ARCHITECTURE.md +142 -0
- package/Dockerfile +1 -1
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-outbound-http.test.ts +194 -2
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -1
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent.test.ts +1 -0
- package/src/__tests__/recording-state-machine.test.ts +328 -17
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +2 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/misc.ts +83 -5
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +100 -17
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +5 -6
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +0 -3
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +26 -6
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +133 -44
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +78 -16
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +1 -1
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -65,6 +65,57 @@ The policy is implemented in `src/tools/guardian-control-plane-policy.ts`, which
|
|
|
65
65
|
|
|
66
66
|
The `guardian-verify-setup` skill is the exclusive handler for guardian verification intents in the system prompt. Other skills (e.g., `phone-calls`) hand off to `guardian-verify-setup` rather than orchestrating verification directly.
|
|
67
67
|
|
|
68
|
+
### Guardian Action Timeout-to-Follow-Up Lifecycle
|
|
69
|
+
|
|
70
|
+
When a voice call's ASK_GUARDIAN consultation times out before the guardian responds, the system enters a follow-up lifecycle that allows the guardian to act on their late answer after the call has moved on. The entire flow uses LLM-generated copy (never hardcoded user-facing strings) to maintain a natural, conversational tone across voice and text channels.
|
|
71
|
+
|
|
72
|
+
**Lifecycle stages:**
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
ASK_GUARDIAN fires on call
|
|
76
|
+
|
|
|
77
|
+
v
|
|
78
|
+
[pending] -- guardian answers in time --> [answered] (normal flow)
|
|
79
|
+
|
|
|
80
|
+
| (timeout expires)
|
|
81
|
+
v
|
|
82
|
+
[expired, followup_state=none]
|
|
83
|
+
|
|
|
84
|
+
| (guardian replies late)
|
|
85
|
+
v
|
|
86
|
+
[expired, followup_state=awaiting_guardian_choice]
|
|
87
|
+
|
|
|
88
|
+
| (conversation engine classifies intent)
|
|
89
|
+
v
|
|
90
|
+
call_back / message_back / decline
|
|
91
|
+
| |
|
|
92
|
+
v v
|
|
93
|
+
[dispatching] [declined] (terminal)
|
|
94
|
+
|
|
|
95
|
+
| (executor runs action)
|
|
96
|
+
v
|
|
97
|
+
[completed] or [failed] (terminal)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Generated messaging requirement:** All user-facing copy in the guardian timeout/follow-up path is generated through the `guardian-action-message-composer.ts` composition system, which uses a 2-tier priority chain: (1) daemon-injected LLM generator for natural, varied text; (2) deterministic fallback templates for reliability. No hardcoded user-facing strings exist in the flow files (call-controller, inbound-message-handler, session-process) outside of internal log messages and LLM-instruction prompts. A guard test (`guardian-action-no-hardcoded-copy.test.ts`) enforces this invariant.
|
|
101
|
+
|
|
102
|
+
**Callback/message-back branch:** When the conversation engine classifies the guardian's intent as `call_back`, the executor starts an outbound call to the counterparty with context about the guardian's answer. When classified as `message_back`, the executor sends an SMS to the counterparty via the gateway's `/deliver/sms` endpoint. The counterparty phone number is resolved from the original call session by call direction (inbound: `fromNumber`; outbound: `toNumber`).
|
|
103
|
+
|
|
104
|
+
**Key source files:**
|
|
105
|
+
|
|
106
|
+
| File | Purpose |
|
|
107
|
+
|------|---------|
|
|
108
|
+
| `src/memory/guardian-action-store.ts` | Follow-up state machine with atomic transitions (`startFollowupFromExpiredRequest`, `progressFollowupState`, `finalizeFollowup`) and query helpers for pending/expired/follow-up deliveries |
|
|
109
|
+
| `src/runtime/guardian-action-message-composer.ts` | 2-tier text generation: daemon-injected LLM generator with deterministic fallback templates. Covers all scenarios from timeout acknowledgment through follow-up completion |
|
|
110
|
+
| `src/runtime/guardian-action-conversation-turn.ts` | Follow-up decision engine: classifies guardian replies into `call_back`, `message_back`, `decline`, or `keep_pending` dispositions using LLM tool calling |
|
|
111
|
+
| `src/runtime/guardian-action-followup-executor.ts` | Action dispatch: resolves counterparty from call session, executes `message_back` (SMS via gateway) or `call_back` (outbound call via `startCall`), finalizes follow-up state |
|
|
112
|
+
| `src/daemon/guardian-action-generators.ts` | Daemon-injected generator factories: `createGuardianActionCopyGenerator` (latency-optimized text rewriting) and `createGuardianFollowUpConversationGenerator` (tool-calling intent classification) |
|
|
113
|
+
| `src/calls/call-controller.ts` | Voice timeout handling: marks requests as timed out, sends expiry notices, injects `[GUARDIAN_TIMEOUT]` instruction for generated voice response |
|
|
114
|
+
| `src/runtime/routes/inbound-message-handler.ts` | Late reply interception for Telegram/SMS channels: matches late answers to expired requests, routes follow-up conversation turns, dispatches actions |
|
|
115
|
+
| `src/daemon/session-process.ts` | Late reply interception for mac/IPC channel: same logic as inbound-message-handler but using conversation-ID-based delivery lookup |
|
|
116
|
+
| `src/calls/guardian-action-sweep.ts` | Periodic sweep for stale pending requests; sends expiry notices to guardian destinations |
|
|
117
|
+
| `src/memory/migrations/030-guardian-action-followup.ts` | Schema migration adding follow-up columns (`followup_state`, `late_answer_text`, `late_answered_at`, `followup_action`, `followup_completed_at`) |
|
|
118
|
+
|
|
68
119
|
### SMS Channel (Twilio)
|
|
69
120
|
|
|
70
121
|
The SMS channel provides text-only messaging via Twilio, sharing the same telephony provider as voice calls. It follows the same ingress/egress pattern as Telegram but uses Twilio's HMAC-SHA1 signature validation instead of a secret header.
|
|
@@ -134,6 +185,97 @@ These can be set via environment variables or stored in the credential vault (ke
|
|
|
134
185
|
|
|
135
186
|
**SMS Compliance & Admin**: The `twilio_config` IPC contract extends beyond credential and number management with compliance and admin actions: `sms_compliance_status` detects toll-free vs local number type and fetches verification status; `sms_submit_tollfree_verification`, `sms_update_tollfree_verification`, and `sms_delete_tollfree_verification` manage the Twilio toll-free verification lifecycle; `release_number` removes a phone number from the Twilio account and clears all local references. All compliance actions validate required fields and Twilio enum values before calling the API.
|
|
136
187
|
|
|
188
|
+
### Slack Channel (Socket Mode)
|
|
189
|
+
|
|
190
|
+
The Slack channel provides text-based messaging via Slack's Socket Mode API. Unlike other channels that use HTTP webhooks, Slack uses a persistent WebSocket connection managed by the gateway — no public ingress URL is required. The assistant-side manages credential storage and validation through HTTP config endpoints.
|
|
191
|
+
|
|
192
|
+
**Control-plane endpoints** (`/v1/integrations/slack/channel/config`):
|
|
193
|
+
|
|
194
|
+
| Endpoint | Method | Description |
|
|
195
|
+
|----------|--------|-------------|
|
|
196
|
+
| `/v1/integrations/slack/channel/config` | GET | Returns current config status: `hasBotToken`, `hasAppToken`, `connected`, plus workspace metadata (`teamId`, `teamName`, `botUserId`, `botUsername`) |
|
|
197
|
+
| `/v1/integrations/slack/channel/config` | POST | Validates and stores credentials. Body: `{ botToken?: string, appToken?: string }` |
|
|
198
|
+
| `/v1/integrations/slack/channel/config` | DELETE | Clears all Slack channel credentials from secure storage and credential metadata |
|
|
199
|
+
|
|
200
|
+
All endpoints are bearer-authenticated via the runtime HTTP token (`~/.vellum/http-token`).
|
|
201
|
+
|
|
202
|
+
**Credential storage pattern:**
|
|
203
|
+
|
|
204
|
+
Both tokens are stored in the secure key store (macOS Keychain with encrypted file fallback):
|
|
205
|
+
|
|
206
|
+
| Secure key | Content |
|
|
207
|
+
|-----------|---------|
|
|
208
|
+
| `credential:slack_channel:bot_token` | Slack bot token (used for `chat.postMessage` and `auth.test`) |
|
|
209
|
+
| `credential:slack_channel:app_token` | Slack app token (`xapp-...`, used for Socket Mode `apps.connections.open`) |
|
|
210
|
+
|
|
211
|
+
Workspace metadata (team ID, team name, bot user ID, bot username) is stored as JSON in the credential metadata store under `('slack_channel', 'bot_token')`.
|
|
212
|
+
|
|
213
|
+
**Token validation via `auth.test`:**
|
|
214
|
+
|
|
215
|
+
When a bot token is provided via `POST /v1/integrations/slack/channel/config`, the handler calls `POST https://slack.com/api/auth.test` with the token before storing it. A successful response yields workspace metadata (`team_id`, `team`, `user_id`, `user`) that is persisted alongside the token. If `auth.test` fails, the token is rejected and not stored.
|
|
216
|
+
|
|
217
|
+
The app token is validated by format only — it must start with `xapp-`.
|
|
218
|
+
|
|
219
|
+
**Connection status:**
|
|
220
|
+
|
|
221
|
+
The `GET` endpoint reports `connected: true` only when both `hasBotToken` and `hasAppToken` are true. If only one token is stored, a `warning` field describes which token is missing.
|
|
222
|
+
|
|
223
|
+
**Key source files:**
|
|
224
|
+
|
|
225
|
+
| File | Purpose |
|
|
226
|
+
|------|---------|
|
|
227
|
+
| `src/daemon/handlers/config-slack-channel.ts` | Business logic for get/set/clear Slack channel config |
|
|
228
|
+
| `src/runtime/routes/integration-routes.ts` | HTTP route handlers for `/v1/integrations/slack/channel/config` |
|
|
229
|
+
|
|
230
|
+
### Trusted Contact Access (Channel-Agnostic)
|
|
231
|
+
|
|
232
|
+
External users who are not the guardian can gain access to the assistant through a guardian-mediated verification flow. The flow is channel-agnostic — it works identically on Telegram, SMS, voice, and any future channel.
|
|
233
|
+
|
|
234
|
+
**Full design doc:** [`docs/trusted-contact-access.md`](docs/trusted-contact-access.md)
|
|
235
|
+
|
|
236
|
+
**Flow summary:**
|
|
237
|
+
1. Unknown user messages the assistant on any channel.
|
|
238
|
+
2. Ingress ACL (`inbound-message-handler.ts`) rejects the message and emits an `ingress.access_request` notification signal to the guardian.
|
|
239
|
+
3. Guardian approves or denies via callback button or conversational intent (routed through `guardian-approval-interception.ts`).
|
|
240
|
+
4. On approval, an identity-bound verification session with a 6-digit code is created (`access-request-decision.ts` → `channel-guardian-service.ts`).
|
|
241
|
+
5. Guardian gives the code to the requester out-of-band.
|
|
242
|
+
6. Requester enters the code; identity binding is verified, the challenge is consumed, and an active member record is created in `assistant_ingress_members`.
|
|
243
|
+
7. All subsequent messages are accepted through the ingress ACL.
|
|
244
|
+
|
|
245
|
+
**Channel-agnostic design:** The entire flow operates on abstract `ChannelId` and `externalUserId`/`externalChatId` fields. Identity binding adapts per channel: Telegram uses chat IDs, SMS/voice use E.164 phone numbers, HTTP API uses caller-provided identity. No channel-specific branching exists in the trusted contact code paths.
|
|
246
|
+
|
|
247
|
+
**Lifecycle states:** `requested → pending_guardian → verification_pending → active | denied | expired`
|
|
248
|
+
|
|
249
|
+
**Notification signals:** The flow emits signals at each lifecycle transition via `emitNotificationSignal()`:
|
|
250
|
+
- `ingress.access_request` — non-member denied, guardian notified
|
|
251
|
+
- `ingress.trusted_contact.guardian_decision` — guardian approved or denied
|
|
252
|
+
- `ingress.trusted_contact.verification_sent` — code created and delivered
|
|
253
|
+
- `ingress.trusted_contact.activated` — requester verified, member active
|
|
254
|
+
- `ingress.trusted_contact.denied` — guardian explicitly denied
|
|
255
|
+
|
|
256
|
+
**HTTP API (for management):**
|
|
257
|
+
|
|
258
|
+
| Endpoint | Method | Description |
|
|
259
|
+
|----------|--------|-------------|
|
|
260
|
+
| `/v1/ingress/members` | GET | List trusted contacts (filterable by channel, status, policy) |
|
|
261
|
+
| `/v1/ingress/members` | POST | Upsert a member (add/update trusted contact) |
|
|
262
|
+
| `/v1/ingress/members/:id` | DELETE | Revoke a trusted contact |
|
|
263
|
+
| `/v1/ingress/members/:id/block` | POST | Block a member |
|
|
264
|
+
|
|
265
|
+
**Key source files:**
|
|
266
|
+
|
|
267
|
+
| File | Purpose |
|
|
268
|
+
|------|---------|
|
|
269
|
+
| `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL, non-member rejection, verification code interception |
|
|
270
|
+
| `src/runtime/routes/access-request-decision.ts` | Guardian decision → verification session creation |
|
|
271
|
+
| `src/runtime/routes/guardian-approval-interception.ts` | Routes guardian decisions (button + conversational) to access request handler |
|
|
272
|
+
| `src/runtime/channel-guardian-service.ts` | Verification challenge lifecycle, identity binding, rate limiting |
|
|
273
|
+
| `src/runtime/routes/ingress-routes.ts` | HTTP API handlers for member/invite management |
|
|
274
|
+
| `src/runtime/ingress-service.ts` | Business logic for member CRUD |
|
|
275
|
+
| `src/memory/ingress-member-store.ts` | Member record persistence |
|
|
276
|
+
| `src/memory/channel-guardian-store.ts` | Approval request and verification challenge persistence |
|
|
277
|
+
| `src/config/vellum-skills/trusted-contacts/SKILL.md` | Skill teaching the assistant to manage contacts via HTTP API |
|
|
278
|
+
|
|
137
279
|
---
|
|
138
280
|
|
|
139
281
|
|
package/Dockerfile
CHANGED
|
@@ -89,7 +89,7 @@ RUN echo 'Dir::State "/data/dpkg";' > /etc/apt/apt.conf.d/99data-dir && \
|
|
|
89
89
|
ENV PATH="/data/usr/bin:/data/usr/sbin:${PATH}"
|
|
90
90
|
ENV LD_LIBRARY_PATH="/data/usr/lib:/data/usr/lib/x86_64-linux-gnu:${LD_LIBRARY_PATH}"
|
|
91
91
|
|
|
92
|
-
USER
|
|
92
|
+
USER root
|
|
93
93
|
|
|
94
94
|
EXPOSE 3001
|
|
95
95
|
|
package/README.md
CHANGED
|
@@ -262,7 +262,7 @@ The channel guardian service generates verification challenge instructions with
|
|
|
262
262
|
|
|
263
263
|
### Operator Notes
|
|
264
264
|
|
|
265
|
-
- **
|
|
265
|
+
- **Verification input format:** Channel verification accepts a bare code reply only (6-digit numeric for identity-bound sessions; 64-char hex for unbound inbound/bootstrap compatibility).
|
|
266
266
|
- **Rebind requirement:** Creating a new guardian challenge when a binding already exists requires `rebind: true` in the IPC request. Without it, the daemon returns `already_bound`. This prevents accidental guardian replacement.
|
|
267
267
|
- **Takeover prevention:** Verification is rejected when an active binding exists for a different external user. Same-user re-verification is allowed.
|
|
268
268
|
|
|
@@ -274,9 +274,9 @@ This section documents the end-to-end flow from guardian verification through in
|
|
|
274
274
|
|
|
275
275
|
Guardian verification establishes a cryptographic trust binding between a human identity and an `(assistantId, channel)` pair. The flow is:
|
|
276
276
|
|
|
277
|
-
1. **Challenge creation** — The owner initiates verification from the desktop UI, which sends a
|
|
278
|
-
2. **
|
|
279
|
-
3. **Verification** — When the message arrives at `/channels/inbound`, the handler intercepts
|
|
277
|
+
1. **Challenge creation** — The owner initiates verification from the desktop UI, which sends a guardian-verification IPC message (`create_challenge` action) to the daemon. The daemon generates a random secret (32-byte hex for unbound inbound/bootstrap sessions, 6-digit numeric for identity-bound sessions), hashes it with SHA-256, stores the hash with a 10-minute TTL, and returns the raw secret to the desktop.
|
|
278
|
+
2. **Code sharing** — The desktop displays the code and instructs the owner to reply with that code in the target channel conversation (e.g., Telegram or SMS).
|
|
279
|
+
3. **Verification** — When the message arrives at `/channels/inbound`, the handler intercepts valid verification-code replies before normal message processing. It hashes the provided code, looks up a matching pending challenge, validates expiry, and consumes the challenge (preventing replay).
|
|
280
280
|
4. **Binding** — On success, any existing active binding for the `(assistantId, channel)` pair is revoked, and a new guardian binding is created with the verifier's `externalUserId` and `chatId`. The verifier receives a confirmation message.
|
|
281
281
|
|
|
282
282
|
Rate limiting protects against brute-force attempts: 5 invalid attempts within 15 minutes trigger a 30-minute lockout per `(assistantId, channel, actor)` tuple. The same generic failure message is returned for both invalid codes and rate-limited attempts to avoid leaking state.
|
|
@@ -317,7 +317,7 @@ Guardian verification and ingress membership are complementary but independent s
|
|
|
317
317
|
|------|---------|
|
|
318
318
|
| `src/runtime/channel-guardian-service.ts` | Challenge lifecycle: `createVerificationChallenge`, `validateAndConsumeChallenge`, `getGuardianBinding`, `isGuardian` |
|
|
319
319
|
| `src/runtime/guardian-context-resolver.ts` | Actor role classification: guardian / non-guardian / unverified_channel |
|
|
320
|
-
| `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL enforcement,
|
|
320
|
+
| `src/runtime/routes/inbound-message-handler.ts` | Ingress ACL enforcement, verification-code intercept, escalation creation |
|
|
321
321
|
| `src/memory/ingress-member-store.ts` | Member CRUD: `findMember`, `upsertMember`, `revokeMember`, `blockMember` |
|
|
322
322
|
| `src/memory/ingress-invite-store.ts` | Invite lifecycle: `createInvite`, `redeemInvite` (atomically creates member record) |
|
|
323
323
|
| `src/memory/channel-guardian-store.ts` | Persistence for guardian bindings, verification challenges, and approval requests |
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# HTTP Token Refresh Protocol
|
|
2
|
+
|
|
3
|
+
Design for how the daemon notifies clients of bearer token rotation and how clients recover from stale tokens.
|
|
4
|
+
|
|
5
|
+
## Current State
|
|
6
|
+
|
|
7
|
+
The daemon's HTTP bearer token is generated at startup and persisted to `~/.vellum/http-token` (mode 0600). Clients read this file at connection time:
|
|
8
|
+
|
|
9
|
+
- **macOS (local)**: Reads `~/.vellum/http-token` from disk via `resolveHttpTokenPath()` / `readHttpToken()`. Has direct filesystem access to the token file.
|
|
10
|
+
- **iOS (remote)**: Receives the bearer token during the QR-code pairing flow. The token is stored in the iOS Keychain and used for all subsequent HTTP/SSE requests.
|
|
11
|
+
- **Chrome extension**: User manually pastes the token from `~/.vellum/http-token` into the extension popup.
|
|
12
|
+
|
|
13
|
+
Token regeneration today (macOS Settings > Connect > Regenerate Bearer Token):
|
|
14
|
+
1. macOS client writes a new random token to `~/.vellum/http-token`.
|
|
15
|
+
2. macOS client kills the daemon process.
|
|
16
|
+
3. The health monitor restarts the daemon, which reads the new token from disk.
|
|
17
|
+
4. macOS client re-reads the token from disk on next health check.
|
|
18
|
+
5. **iOS clients are broken** -- they still hold the old token and get 401s. The only recovery is to re-pair via QR code.
|
|
19
|
+
|
|
20
|
+
## Problem
|
|
21
|
+
|
|
22
|
+
When the bearer token is rotated (manually or programmatically), remote clients (iOS, Chrome extension) have no way to learn about the new token. They receive 401 responses and cannot recover without manual re-configuration.
|
|
23
|
+
|
|
24
|
+
## Design
|
|
25
|
+
|
|
26
|
+
### 1. SSE Token Rotation Event
|
|
27
|
+
|
|
28
|
+
When the daemon detects that its bearer token has changed, it emits a `token_rotated` SSE event to all connected clients **before** the old token is invalidated. This gives clients a window to capture the new token and seamlessly reconnect.
|
|
29
|
+
|
|
30
|
+
**Event format** (delivered as an `AssistantEvent` envelope wrapping a new `ServerMessage` type):
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// New ServerMessage variant
|
|
34
|
+
interface TokenRotatedMessage {
|
|
35
|
+
type: 'token_rotated';
|
|
36
|
+
newToken: string; // The replacement bearer token
|
|
37
|
+
expiresOldAt: number; // Unix timestamp (ms) -- old token stops working after this
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Grace period**: The daemon accepts both old and new tokens for a configurable grace window (default: 30 seconds) after emitting the event. This gives slow clients time to process the event and switch tokens. After the grace period, only the new token is valid.
|
|
42
|
+
|
|
43
|
+
**Sequence diagram (routine rotation)**:
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
sequenceDiagram
|
|
47
|
+
participant Trigger as Rotation Trigger
|
|
48
|
+
participant Daemon as Daemon
|
|
49
|
+
participant SSE as SSE Stream
|
|
50
|
+
participant Client as iOS / Chrome Client
|
|
51
|
+
|
|
52
|
+
Trigger->>Daemon: Rotate token (manual, API, periodic)
|
|
53
|
+
Daemon->>Daemon: Generate new token, write to ~/.vellum/http-token
|
|
54
|
+
Daemon->>Daemon: Enter grace period (accept old + new)
|
|
55
|
+
Daemon->>SSE: Emit token_rotated {newToken, expiresOldAt}
|
|
56
|
+
SSE->>Client: token_rotated event
|
|
57
|
+
Client->>Client: Store new token (Keychain / localStorage)
|
|
58
|
+
Client->>Client: Update Authorization header
|
|
59
|
+
Client->>Daemon: Reconnect SSE with new token
|
|
60
|
+
Note over Daemon: Grace period expires
|
|
61
|
+
Daemon->>Daemon: Reject old token (401)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Sequence diagram (revocation rotation)**:
|
|
65
|
+
|
|
66
|
+
```mermaid
|
|
67
|
+
sequenceDiagram
|
|
68
|
+
participant Trigger as Security Event
|
|
69
|
+
participant Daemon as Daemon
|
|
70
|
+
participant SSE as SSE Stream
|
|
71
|
+
participant Client as iOS / Chrome Client
|
|
72
|
+
|
|
73
|
+
Trigger->>Daemon: Rotate token (revoke: true)
|
|
74
|
+
Daemon->>Daemon: Generate new token, immediately invalidate old token
|
|
75
|
+
Daemon->>SSE: Terminate all old-token connections
|
|
76
|
+
Daemon->>Daemon: Write new token to ~/.vellum/http-token
|
|
77
|
+
Note over SSE: No token_rotated event emitted
|
|
78
|
+
Client->>Daemon: Next request with old token → 401
|
|
79
|
+
Client->>Client: Trigger fallback recovery (re-pair / re-read / re-paste)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Client 401 Recovery (Stale Token Detection)
|
|
83
|
+
|
|
84
|
+
If a client misses the SSE event (network partition, app backgrounded, SSE disconnected at rotation time), it needs a fallback recovery mechanism.
|
|
85
|
+
|
|
86
|
+
**401 response handling**:
|
|
87
|
+
|
|
88
|
+
When a client receives a `401 Unauthorized` response:
|
|
89
|
+
|
|
90
|
+
1. **macOS (local)**: Re-reads `~/.vellum/http-token` from disk. If the token differs from the in-memory token, updates and retries. This already works implicitly since macOS re-reads the token on most HTTP calls via `resolveLocalDaemonHTTPEndpoint()`.
|
|
91
|
+
|
|
92
|
+
2. **iOS (remote)**: Cannot read the token file. Must re-pair via QR code. The 401 response triggers the client to surface a "Session expired -- re-pair required" UI prompt. This is the expected behavior when the SSE notification is missed.
|
|
93
|
+
|
|
94
|
+
3. **Chrome extension**: Surfaces an error message directing the user to paste the new token.
|
|
95
|
+
|
|
96
|
+
**Retry logic**: Clients should retry at most once after a 401 before surfacing the error UI. This prevents retry storms during legitimate auth failures (wrong token, not just stale).
|
|
97
|
+
|
|
98
|
+
### 3. Token Rotation Triggers
|
|
99
|
+
|
|
100
|
+
The token can be rotated via:
|
|
101
|
+
|
|
102
|
+
| Trigger | Description | Current | Proposed | Mode |
|
|
103
|
+
|---------|-------------|---------|----------|------|
|
|
104
|
+
| Manual (macOS Settings) | User clicks "Regenerate Bearer Token" | Yes (kills daemon) | Graceful rotation via daemon API | Routine |
|
|
105
|
+
| API endpoint | `POST /v1/auth/rotate-token` | No | New endpoint | Routine (default) or Revocation (`revoke: true`) |
|
|
106
|
+
| Periodic rotation | Automatic rotation on a configurable schedule | No | Future consideration | Routine |
|
|
107
|
+
| Security event | Forced rotation after suspicious activity | No | Future consideration | Revocation |
|
|
108
|
+
|
|
109
|
+
**`POST /v1/auth/rotate-token`** (new endpoint):
|
|
110
|
+
|
|
111
|
+
Allows programmatic token rotation without restarting the daemon.
|
|
112
|
+
|
|
113
|
+
Request body (optional): `{ "revoke": boolean }` (default: `false`).
|
|
114
|
+
|
|
115
|
+
**Routine mode** (`revoke: false`, default):
|
|
116
|
+
1. Generates a new random token.
|
|
117
|
+
2. Writes it to `~/.vellum/http-token`.
|
|
118
|
+
3. Emits the `token_rotated` SSE event with grace period.
|
|
119
|
+
4. Starts accepting both tokens during the grace period.
|
|
120
|
+
5. After grace period, rejects the old token.
|
|
121
|
+
|
|
122
|
+
**Revocation mode** (`revoke: true`):
|
|
123
|
+
1. Generates a new random token.
|
|
124
|
+
2. Immediately invalidates the old token in memory (no grace period).
|
|
125
|
+
3. Terminates all SSE connections authenticated with the old token.
|
|
126
|
+
4. Writes the new token to `~/.vellum/http-token`.
|
|
127
|
+
5. Does **not** emit `token_rotated` -- the new token is never sent to old-token sessions.
|
|
128
|
+
6. Clients must recover via their platform-specific fallback (re-read from disk, re-pair, or re-paste).
|
|
129
|
+
|
|
130
|
+
This eliminates the current "kill and restart" approach for token rotation.
|
|
131
|
+
|
|
132
|
+
### 4. Daemon-Side Implementation
|
|
133
|
+
|
|
134
|
+
**Grace period token validation**: During routine rotation, `verifyBearerToken()` accepts either the old or new token. The `RuntimeHttpServer` holds both tokens:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Conceptual extension to RuntimeHttpServer
|
|
138
|
+
private currentToken: string;
|
|
139
|
+
private previousToken: string | null = null;
|
|
140
|
+
private graceDeadline: number | null = null;
|
|
141
|
+
|
|
142
|
+
// Modified auth check
|
|
143
|
+
private isValidToken(provided: string): boolean {
|
|
144
|
+
if (verifyBearerToken(provided, this.currentToken)) return true;
|
|
145
|
+
if (this.previousToken && this.graceDeadline && Date.now() < this.graceDeadline) {
|
|
146
|
+
return verifyBearerToken(provided, this.previousToken);
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Rotation has two ordering strategies depending on revoke mode:
|
|
152
|
+
// - Revocation: invalidate in-memory FIRST (security-critical), then persist.
|
|
153
|
+
// A disk write failure must never leave a compromised token valid.
|
|
154
|
+
// - Routine: persist to disk FIRST, then update in-memory state.
|
|
155
|
+
// A disk write failure aborts rotation — clients keep the old token
|
|
156
|
+
// rather than being locked out by an in-memory-only switch.
|
|
157
|
+
private rotateToken(revoke: boolean): string {
|
|
158
|
+
const newToken = generateToken();
|
|
159
|
+
|
|
160
|
+
if (revoke) {
|
|
161
|
+
// Revocation: invalidate the compromised token immediately.
|
|
162
|
+
// Even if the disk write below fails, the old token is gone from memory.
|
|
163
|
+
this.currentToken = newToken;
|
|
164
|
+
this.previousToken = null;
|
|
165
|
+
this.graceDeadline = null;
|
|
166
|
+
this.terminateOldTokenSSEConnections();
|
|
167
|
+
writeTokenToDisk(newToken);
|
|
168
|
+
} else {
|
|
169
|
+
// Routine: persist to disk first — if this throws, auth state is untouched
|
|
170
|
+
writeTokenToDisk(newToken);
|
|
171
|
+
this.previousToken = this.currentToken;
|
|
172
|
+
this.currentToken = newToken;
|
|
173
|
+
this.graceDeadline = Date.now() + GRACE_PERIOD_MS;
|
|
174
|
+
this.emitTokenRotatedEvent(newToken, this.graceDeadline);
|
|
175
|
+
}
|
|
176
|
+
return newToken;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**SSE event emission** (routine rotation only): The `token_rotated` event is published to `assistantEventHub` as a `ServerMessage`, reaching all connected SSE subscribers across all conversations. This event is never emitted during revocation rotations.
|
|
181
|
+
|
|
182
|
+
### 5. iOS Client Implementation
|
|
183
|
+
|
|
184
|
+
**SSE event handler** (in `HTTPTransport`):
|
|
185
|
+
|
|
186
|
+
```swift
|
|
187
|
+
// In parseSSEData, handle the new message type
|
|
188
|
+
case .tokenRotated(let msg):
|
|
189
|
+
// Persist the new token to Keychain
|
|
190
|
+
DaemonConfigStore.shared.updateBearerToken(msg.newToken)
|
|
191
|
+
// Update in-memory token
|
|
192
|
+
self.bearerToken = msg.newToken
|
|
193
|
+
// Reconnect SSE with the new token
|
|
194
|
+
self.stopSSE()
|
|
195
|
+
self.startSSE()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**401 response handler**:
|
|
199
|
+
|
|
200
|
+
```swift
|
|
201
|
+
// In any HTTP request that receives 401
|
|
202
|
+
if http.statusCode == 401 {
|
|
203
|
+
// Token is stale and we missed the rotation event
|
|
204
|
+
// Surface re-pairing UI
|
|
205
|
+
onMessage?(.sessionError(SessionErrorMessage(
|
|
206
|
+
sessionId: sessionId,
|
|
207
|
+
code: .authenticationRequired,
|
|
208
|
+
userMessage: "Session expired. Please re-pair your device.",
|
|
209
|
+
retryable: false
|
|
210
|
+
)))
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 6. Security Considerations
|
|
215
|
+
|
|
216
|
+
- **Rotation modes**: Token rotation has two distinct modes with different security requirements:
|
|
217
|
+
|
|
218
|
+
1. **Routine rotation** (manual refresh, periodic schedule): The old token is not compromised -- the goal is seamless credential rollover. The SSE `token_rotated` event delivers the new token to connected clients, and the grace period allows them to transition. This is safe because the SSE channel is authenticated, and any session holding the old token is a legitimate client.
|
|
219
|
+
|
|
220
|
+
2. **Revocation rotation** (security event, suspected compromise): The old token may be in the hands of an attacker. In this mode, the daemon **must not** push the replacement token to old-token SSE sessions -- doing so would hand the new credential to the very sessions being revoked. Instead:
|
|
221
|
+
- The daemon immediately invalidates the old token (no grace period).
|
|
222
|
+
- All SSE connections authenticated with the old token are terminated.
|
|
223
|
+
- The `POST /v1/auth/rotate-token` endpoint accepts an optional `revoke: true` flag to select this mode.
|
|
224
|
+
- Legitimate clients recover via their fallback path: macOS re-reads `~/.vellum/http-token` from disk; iOS prompts for re-pairing; Chrome extension prompts for a new token.
|
|
225
|
+
|
|
226
|
+
The `token_rotated` SSE event is only emitted during routine rotations. The rotation trigger determines the mode.
|
|
227
|
+
|
|
228
|
+
- **Grace period length**: 30 seconds is long enough for clients to process the event but short enough to limit the window where both tokens are valid. Only applies to routine rotations.
|
|
229
|
+
- **No token in logs**: The `token_rotated` event payload must be excluded from any server-side event logging. Use the existing log-redaction patterns.
|
|
230
|
+
- **Constant-time comparison**: The existing `verifyBearerToken()` using `timingSafeEqual` continues to be used for both old and new token checks during the grace period.
|
|
231
|
+
|
|
232
|
+
### 7. Migration Path
|
|
233
|
+
|
|
234
|
+
This design is additive and backward-compatible:
|
|
235
|
+
|
|
236
|
+
1. **Phase 1**: Add `POST /v1/auth/rotate-token` endpoint and `token_rotated` SSE event to the daemon. Update macOS Settings to call the API endpoint instead of kill-and-restart.
|
|
237
|
+
2. **Phase 2**: Add `token_rotated` handler to `HTTPTransport.swift` (shared between macOS and iOS). Add 401 retry-once logic.
|
|
238
|
+
3. **Phase 3** (future): Add periodic rotation and security-event-triggered rotation.
|
|
239
|
+
|
|
240
|
+
Clients that do not understand the `token_rotated` event will simply ignore it (SSE events with unknown types are safe to skip). They will eventually get 401s after the grace period and fall back to their existing recovery path (re-read from disk for macOS, re-pair for iOS).
|
|
241
|
+
|
|
242
|
+
## Key Files
|
|
243
|
+
|
|
244
|
+
| File | Role |
|
|
245
|
+
|------|------|
|
|
246
|
+
| `assistant/src/runtime/http-server.ts` | Auth check, grace period logic, rotation endpoint |
|
|
247
|
+
| `assistant/src/runtime/middleware/auth.ts` | `verifyBearerToken()` -- constant-time token comparison |
|
|
248
|
+
| `assistant/src/runtime/assistant-event.ts` | `AssistantEvent` envelope, SSE framing |
|
|
249
|
+
| `assistant/src/daemon/lifecycle.ts` | Token generation and persistence at startup |
|
|
250
|
+
| `clients/shared/IPC/HTTPDaemonClient.swift` | `HTTPTransport` -- SSE stream, 401 handling |
|
|
251
|
+
| `clients/shared/IPC/DaemonClient.swift` | `readHttpToken()`, `resolveHttpTokenPath()` |
|
|
252
|
+
| `clients/macos/.../SettingsConnectTab.swift` | Manual token regeneration UI |
|
|
@@ -366,14 +366,14 @@ The Anthropic provider places `cache_control: { type: 'ephemeral' }` on the **la
|
|
|
366
366
|
|
|
367
367
|
## Temporal Context Injection — Date Grounding
|
|
368
368
|
|
|
369
|
-
The session injects a `<temporal_context>` block into every user message at runtime, giving the model awareness of the current date, timezone, upcoming weekend/work week windows, and a 14-day horizon of labelled future dates. This enables reliable reasoning about future dates (e.g. "plan a trip for next weekend") without persisting volatile temporal data in conversation history.
|
|
369
|
+
The session injects a `<temporal_context>` block into every user message at runtime, giving the model awareness of the current date, current local time, current UTC time, timezone source metadata, upcoming weekend/work week windows, and a 14-day horizon of labelled future dates. This enables reliable reasoning about future dates (e.g. "plan a trip for next weekend") without persisting volatile temporal data in conversation history.
|
|
370
370
|
|
|
371
371
|
### Per-turn flow
|
|
372
372
|
|
|
373
373
|
```mermaid
|
|
374
374
|
graph TB
|
|
375
375
|
subgraph "Per-Turn Flow"
|
|
376
|
-
BUILD["buildTemporalContext(timeZone)<br/>→ compact XML block"]
|
|
376
|
+
BUILD["buildTemporalContext(timeZone, hostTimeZone, userTimeZone)<br/>→ compact XML block"]
|
|
377
377
|
INJECT["applyRuntimeInjections<br/>prepend temporal block<br/>to user message"]
|
|
378
378
|
AGENT["AgentLoop.run(runMessages)"]
|
|
379
379
|
STRIP["stripTemporalContext<br/>remove block from persisted history"]
|
|
@@ -387,7 +387,9 @@ graph TB
|
|
|
387
387
|
### Key design decisions
|
|
388
388
|
|
|
389
389
|
- **Fresh each turn**: `buildTemporalContext()` is called at the start of every agent loop invocation, ensuring the model always sees the current date even in long-running conversations.
|
|
390
|
-
- **
|
|
390
|
+
- **Clock source invariant**: Absolute time (`now`) always comes from the assistant host clock (`Date.now()`), never from channel/client clocks.
|
|
391
|
+
- **Timezone precedence**: If `ui.userTimezone` is configured, temporal context uses it for local-date interpretation. Otherwise it falls back to dynamic profile memory, then assistant host timezone.
|
|
392
|
+
- **Timezone-aware**: Uses `Intl.DateTimeFormat` APIs for DST-safe date arithmetic and timezone validation/canonicalization.
|
|
391
393
|
- **Bounded output**: Hard-capped at 1500 characters and 14 horizon entries to prevent prompt bloat.
|
|
392
394
|
- **Runtime-only**: The injected `<temporal_context>` block is stripped from `this.messages` after the agent loop completes via `stripTemporalContext`. It never persists in conversation history.
|
|
393
395
|
- **Specific strip prefix**: The strip function matches the exact injected prefix (`<temporal_context>\nToday:`) to avoid accidentally removing user-authored text that starts with `<temporal_context>`.
|
|
@@ -512,4 +514,3 @@ graph TB
|
|
|
512
514
|
| `assistant/src/config/schema.ts` | `WorkspaceGitConfigSchema`: timeout, backoff, and enrichment queue configuration |
|
|
513
515
|
|
|
514
516
|
---
|
|
515
|
-
|
|
@@ -91,7 +91,7 @@ The `enforceRoutingIntent()` step runs after the LLM produces a channel selectio
|
|
|
91
91
|
| Intent | Enforcement Rule |
|
|
92
92
|
|--------|-----------------|
|
|
93
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
|
|
94
|
+
| `multi_channel` | If the LLM selected < 2 channels and 2+ are connected, expand to at least two connected channels. |
|
|
95
95
|
| `all_channels` | Replace the LLM's selection with all connected channels. |
|
|
96
96
|
|
|
97
97
|
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.
|
|
@@ -189,9 +189,9 @@ graph TD
|
|
|
189
189
|
|
|
190
190
|
**Data tables:** `watchers` (config, watermark, status, error tracking) and `watcher_events` (detected events, dedup on `(watcher_id, external_id)`, disposition tracking).
|
|
191
191
|
|
|
192
|
-
## Task Queue —
|
|
192
|
+
## Task Queue — Conversation-Managed Task Execution
|
|
193
193
|
|
|
194
|
-
The Task Queue
|
|
194
|
+
The Task Queue provides an ordered execution pipeline with human-in-the-loop review. Task management happens entirely through conversation — the user creates, updates, runs, and reviews tasks by talking to the assistant. There is no standalone Tasks UI window.
|
|
195
195
|
|
|
196
196
|
### Terminology
|
|
197
197
|
|
|
@@ -258,19 +258,6 @@ flowchart TD
|
|
|
258
258
|
DB[(SQLite)]
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
-
subgraph "Daemon IPC Handlers"
|
|
262
|
-
HC[handleWorkItemCreate]
|
|
263
|
-
HU[handleWorkItemUpdate]
|
|
264
|
-
HCo[handleWorkItemComplete]
|
|
265
|
-
HR[handleWorkItemRunTask]
|
|
266
|
-
BC[tasks_changed broadcast]
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
subgraph "macOS Client"
|
|
270
|
-
TW[TasksWindowView]
|
|
271
|
-
DC[DaemonClient]
|
|
272
|
-
end
|
|
273
|
-
|
|
274
261
|
TLA -->|"if_exists check"| DUPE
|
|
275
262
|
DUPE -->|"no match"| WIS
|
|
276
263
|
DUPE -->|"match found → reuse/update"| TLU
|
|
@@ -278,81 +265,10 @@ flowchart TD
|
|
|
278
265
|
RWI --> WIS
|
|
279
266
|
TLS --> WIS
|
|
280
267
|
WIS --> DB
|
|
281
|
-
|
|
282
|
-
HC --> WIS
|
|
283
|
-
HU --> WIS
|
|
284
|
-
HCo --> WIS
|
|
285
|
-
HR --> WIS
|
|
286
|
-
HC --> BC
|
|
287
|
-
HU --> BC
|
|
288
|
-
HCo --> BC
|
|
289
|
-
HR --> BC
|
|
290
|
-
|
|
291
|
-
BC -->|"via socket"| DC
|
|
292
|
-
DC -->|"onTasksChanged"| TW
|
|
293
|
-
TW -->|"debounced refetch (300ms)"| DC
|
|
294
268
|
```
|
|
295
269
|
|
|
296
270
|
**Key behaviors:**
|
|
297
271
|
|
|
272
|
+
- **Conversation-first management** — All task operations (create, update, run, review, delete) are performed through natural language conversation with the assistant, which invokes the model tools (`task_list_add`, `task_list_update`, `task_list_show`) on the user's behalf.
|
|
298
273
|
- **`task_list_update`** uses `resolveWorkItem` to find the target work item by work item ID, task ID, or title (case-insensitive exact match). When multiple items match by task ID or title, the resolver applies a deterministic tie-break (lowest priority tier, then earliest `createdAt`).
|
|
299
274
|
- **`task_list_add`** has duplicate prevention via the `if_exists` parameter (default: `reuse_existing`). Before creating, it calls `findActiveWorkItemsByTitle` to check for active items with the same title. If a match is found, the tool either returns the existing item (`reuse_existing`), updates it in place (`update_existing`), or proceeds to create a duplicate (`create_duplicate`).
|
|
300
|
-
- **All daemon work-item handlers** (`handleWorkItemCreate`, `handleWorkItemUpdate`, `handleWorkItemComplete`, `handleWorkItemRunTask`) emit a `tasks_changed` broadcast after mutations via `ctx.broadcast({ type: 'tasks_changed' })`. They also emit the more specific `work_item_status_changed` with the affected item's current state.
|
|
301
|
-
- **The macOS Tasks window** (`TasksWindowView`) subscribes to both `tasks_changed` and `work_item_status_changed` callbacks on `DaemonClient`. Both trigger a debounced refetch (300ms) so rapid successive mutations coalesce into a single re-fetch.
|
|
302
|
-
|
|
303
|
-
### IPC Messages
|
|
304
|
-
|
|
305
|
-
**Client → Server:**
|
|
306
|
-
|
|
307
|
-
| Message | Purpose |
|
|
308
|
-
|---------|---------|
|
|
309
|
-
| `work_items_list` | List work items, filterable by status |
|
|
310
|
-
| `work_item_get` | Fetch a single work item with full details |
|
|
311
|
-
| `work_item_create` | Create a new work item pointing to a Task |
|
|
312
|
-
| `work_item_update` | Update title, notes, priority, or sort order |
|
|
313
|
-
| `work_item_complete` | Mark an item as `done` after review |
|
|
314
|
-
| `work_item_run_task` | Trigger execution of a queued work item |
|
|
315
|
-
| `work_item_delete` | Delete a work item from the queue |
|
|
316
|
-
|
|
317
|
-
**Server → Client (push):**
|
|
318
|
-
|
|
319
|
-
| Message | Purpose |
|
|
320
|
-
|---------|---------|
|
|
321
|
-
| `work_item_status_changed` | Notify the client when a work item transitions state (includes item snapshot) |
|
|
322
|
-
| `tasks_changed` | Lightweight broadcast after any work-item mutation; triggers client-side refetch |
|
|
323
|
-
|
|
324
|
-
### Run-Button State Machine
|
|
325
|
-
|
|
326
|
-
When the user clicks "Run" on a queued work item, the button follows a deterministic state machine:
|
|
327
|
-
|
|
328
|
-
```
|
|
329
|
-
idle (visible) → in-flight (hidden) → success/failure → re-enabled (via refetch)
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
**Sequence:**
|
|
333
|
-
|
|
334
|
-
1. **Idle** — The run button is visible only when `item.status == "queued"`. The `TasksWindowRow` renders it conditionally based on the `WorkItemStatus` enum.
|
|
335
|
-
2. **In-flight** — The client sends `work_item_run_task` with the work item ID. The daemon validates the request, sets the item's status to `running`, and returns `work_item_run_task_response` with `success: true`. It then broadcasts `work_item_status_changed` and `tasks_changed`. The client's debounced refetch picks up the `running` status, which hides the run button and shows a spinner in the status column.
|
|
336
|
-
3. **Completion** — The daemon executes the task asynchronously. On success, the item transitions to `awaiting_review`; on failure, to `failed`. Both trigger another `work_item_status_changed` + `tasks_changed` broadcast, which the client refetches and renders accordingly (showing a "Reviewed" button for `awaiting_review`, or the run button again for `failed` to allow retry).
|
|
337
|
-
|
|
338
|
-
**Error handling in `work_item_run_task_response`:**
|
|
339
|
-
|
|
340
|
-
The response includes a typed `errorCode` field (`WorkItemRunTaskErrorCode`) so the client can deterministically decide what to do without parsing error strings:
|
|
341
|
-
|
|
342
|
-
| `errorCode` | Meaning | Client behavior |
|
|
343
|
-
|-------------|---------|-----------------|
|
|
344
|
-
| `not_found` | Work item does not exist (deleted concurrently) | Refetch removes the stale row |
|
|
345
|
-
| `already_running` | Item is already executing | No-op; status column already shows spinner |
|
|
346
|
-
| `invalid_status` | Item is `done` or `archived` and cannot be run | Refetch updates the row to reflect terminal status |
|
|
347
|
-
| `no_task` | The associated Task template was deleted | Refetch; row may show an error state |
|
|
348
|
-
|
|
349
|
-
In all error cases, the subsequent `tasks_changed` broadcast triggers a refetch that brings the UI back to a consistent state, so the button is never stuck in a disabled/hidden state without a path to recovery.
|
|
350
|
-
|
|
351
|
-
### Delete Flow
|
|
352
|
-
|
|
353
|
-
Deletion uses optimistic UI with rollback:
|
|
354
|
-
|
|
355
|
-
1. **Optimistic removal** — `TasksWindowViewModel.removeTask()` snapshots the current `items` array, then immediately removes the target item with animation.
|
|
356
|
-
2. **IPC request** — Sends `work_item_delete` with the item ID. The daemon looks up the item; if found, deletes it and responds with `work_item_delete_response { success: true }`, then broadcasts `tasks_changed`.
|
|
357
|
-
3. **Failure rollback** — If the send throws (socket error), the view model restores the snapshot with animation. If the daemon responds with `success: false` (item not found), the `onWorkItemDeleteResponse` callback triggers a full refetch to reconcile.
|
|
358
|
-
|