@vellumai/assistant 0.3.14 → 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 +2 -2
- 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-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- 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 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- 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 +8 -8
- 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/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- 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-executor.ts +1 -1
- 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 +6 -7
- 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 +4 -7
- 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 +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- 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 +5 -4
- 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 +82 -20
- 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 +2 -2
- 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
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# Trusted Contacts — Operator Runbook
|
|
2
|
+
|
|
3
|
+
Operational procedures for inspecting, managing, and debugging the trusted contact access flow. All HTTP commands use the gateway API (default `http://localhost:7830`) with bearer authentication.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Read the bearer token
|
|
9
|
+
TOKEN=$(cat ~/.vellum/http-token)
|
|
10
|
+
|
|
11
|
+
# Base URL (adjust if using a non-default port)
|
|
12
|
+
BASE=http://localhost:7830
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 1. Inspect Trusted Contacts (Members)
|
|
16
|
+
|
|
17
|
+
### List all active trusted contacts
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
curl -s "$BASE/v1/ingress/members?status=active" \
|
|
21
|
+
-H "Authorization: Bearer $TOKEN" | jq
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Filter by channel
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Telegram contacts only
|
|
28
|
+
curl -s "$BASE/v1/ingress/members?sourceChannel=telegram&status=active" \
|
|
29
|
+
-H "Authorization: Bearer $TOKEN" | jq
|
|
30
|
+
|
|
31
|
+
# SMS contacts only
|
|
32
|
+
curl -s "$BASE/v1/ingress/members?sourceChannel=sms&status=active" \
|
|
33
|
+
-H "Authorization: Bearer $TOKEN" | jq
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### List all members (including revoked and blocked)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl -s "$BASE/v1/ingress/members" \
|
|
40
|
+
-H "Authorization: Bearer $TOKEN" | jq
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Response shape:
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"ok": true,
|
|
47
|
+
"members": [
|
|
48
|
+
{
|
|
49
|
+
"id": "uuid",
|
|
50
|
+
"sourceChannel": "telegram",
|
|
51
|
+
"externalUserId": "123456789",
|
|
52
|
+
"externalChatId": "123456789",
|
|
53
|
+
"displayName": "Alice",
|
|
54
|
+
"username": "alice_handle",
|
|
55
|
+
"status": "active",
|
|
56
|
+
"policy": "allow",
|
|
57
|
+
"lastSeenAt": 1700000000000,
|
|
58
|
+
"createdAt": 1699000000000
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 2. Inspect Pending Access Requests
|
|
65
|
+
|
|
66
|
+
Access requests are stored in the `channel_guardian_approval_requests` table. Use SQLite to inspect pending requests directly.
|
|
67
|
+
|
|
68
|
+
### Via SQLite CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
72
|
+
"SELECT id, channel, requester_external_user_id, requester_chat_id, \
|
|
73
|
+
guardian_external_user_id, status, tool_name, created_at, expires_at \
|
|
74
|
+
FROM channel_guardian_approval_requests \
|
|
75
|
+
WHERE tool_name = 'ingress_access_request' AND status = 'pending' \
|
|
76
|
+
ORDER BY created_at DESC;"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Check all access requests (including resolved)
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
83
|
+
"SELECT id, channel, requester_external_user_id, status, \
|
|
84
|
+
decided_by_external_user_id, created_at \
|
|
85
|
+
FROM channel_guardian_approval_requests \
|
|
86
|
+
WHERE tool_name = 'ingress_access_request' \
|
|
87
|
+
ORDER BY created_at DESC LIMIT 20;"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 3. Inspect Pending Verification Sessions
|
|
91
|
+
|
|
92
|
+
Verification challenges are stored in `channel_guardian_verification_challenges`. Active sessions have `status = 'awaiting_response'` and `expires_at > now`.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
96
|
+
"SELECT id, channel, status, identity_binding_status, \
|
|
97
|
+
expected_external_user_id, expected_chat_id, expected_phone_e164, \
|
|
98
|
+
expires_at, created_at \
|
|
99
|
+
FROM channel_guardian_verification_challenges \
|
|
100
|
+
WHERE status IN ('awaiting_response', 'pending_bootstrap') \
|
|
101
|
+
AND expires_at > $(date +%s)000 \
|
|
102
|
+
ORDER BY created_at DESC;"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 4. Force-Revoke a Trusted Contact
|
|
106
|
+
|
|
107
|
+
### Via HTTP API
|
|
108
|
+
|
|
109
|
+
First, find the member's `id` from the list endpoint, then revoke:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Find the member
|
|
113
|
+
MEMBER_ID=$(curl -s "$BASE/v1/ingress/members?sourceChannel=telegram&status=active" \
|
|
114
|
+
-H "Authorization: Bearer $TOKEN" | jq -r '.members[] | select(.externalUserId == "TARGET_USER_ID") | .id')
|
|
115
|
+
|
|
116
|
+
# Revoke with reason
|
|
117
|
+
curl -s -X DELETE "$BASE/v1/ingress/members/$MEMBER_ID" \
|
|
118
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
119
|
+
-H "Content-Type: application/json" \
|
|
120
|
+
-d '{"reason": "Revoked by operator"}' | jq
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Block a member (stronger than revoke)
|
|
124
|
+
|
|
125
|
+
Blocking prevents the member from re-entering the flow without explicit unblocking.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
curl -s -X POST "$BASE/v1/ingress/members/$MEMBER_ID/block" \
|
|
129
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
130
|
+
-H "Content-Type: application/json" \
|
|
131
|
+
-d '{"reason": "Blocked by operator"}' | jq
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Via SQLite (emergency)
|
|
135
|
+
|
|
136
|
+
If the HTTP API is unavailable:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
140
|
+
"UPDATE assistant_ingress_members \
|
|
141
|
+
SET status = 'revoked', revoked_reason = 'Emergency operator revocation', \
|
|
142
|
+
updated_at = $(date +%s)000 \
|
|
143
|
+
WHERE external_user_id = 'TARGET_USER_ID' AND source_channel = 'telegram';"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## 5. Debug Verification Failures
|
|
147
|
+
|
|
148
|
+
### Check rate limit state
|
|
149
|
+
|
|
150
|
+
If a user is getting "invalid or expired code" errors, they may be rate-limited:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
154
|
+
"SELECT * FROM channel_guardian_rate_limits \
|
|
155
|
+
WHERE external_user_id = 'TARGET_USER_ID' \
|
|
156
|
+
OR chat_id = 'TARGET_CHAT_ID' \
|
|
157
|
+
ORDER BY created_at DESC LIMIT 5;"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Reset rate limits for a user
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
164
|
+
"DELETE FROM channel_guardian_rate_limits \
|
|
165
|
+
WHERE external_user_id = 'TARGET_USER_ID' AND channel = 'telegram';"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Check verification challenge state
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
172
|
+
"SELECT id, channel, status, identity_binding_status, \
|
|
173
|
+
expected_external_user_id, expected_chat_id, expected_phone_e164, \
|
|
174
|
+
expires_at, consumed_by_external_user_id \
|
|
175
|
+
FROM channel_guardian_verification_challenges \
|
|
176
|
+
WHERE expected_external_user_id = 'TARGET_USER_ID' \
|
|
177
|
+
OR expected_chat_id = 'TARGET_CHAT_ID' \
|
|
178
|
+
ORDER BY created_at DESC LIMIT 5;"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Common verification failure causes
|
|
182
|
+
|
|
183
|
+
| Symptom | Likely cause | Resolution |
|
|
184
|
+
|---------|-------------|------------|
|
|
185
|
+
| "Invalid or expired code" (correct code) | Identity mismatch: the code was entered from a different user/chat than expected | Verify the requester is using the same account that originally requested access |
|
|
186
|
+
| "Invalid or expired code" (correct code, correct user) | Rate-limited (5+ failures in 15 min window) | Wait 30 minutes or reset rate limits via SQLite |
|
|
187
|
+
| "Invalid or expired code" (old code) | Code TTL expired (10 min) | Guardian must re-approve to generate a new code |
|
|
188
|
+
| Code never delivered to guardian | `deliverChannelReply` failed | Check daemon logs for "Failed to deliver verification code to guardian" |
|
|
189
|
+
| No notification to guardian | No guardian binding for channel | Verify guardian is bound: check `channel_guardian_bindings` table |
|
|
190
|
+
|
|
191
|
+
## 6. Check Notification Delivery Status
|
|
192
|
+
|
|
193
|
+
### Check if the access request notification was delivered
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
197
|
+
"SELECT ne.id, ne.source_event_name, ne.dedupe_key, ne.created_at, \
|
|
198
|
+
nd.channel, nd.status, nd.confidence \
|
|
199
|
+
FROM notification_events ne \
|
|
200
|
+
LEFT JOIN notification_decisions nd ON nd.event_id = ne.id \
|
|
201
|
+
WHERE ne.source_event_name LIKE 'ingress.%' \
|
|
202
|
+
ORDER BY ne.created_at DESC LIMIT 20;"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Check delivery records
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
209
|
+
"SELECT ndel.id, ndel.channel, ndel.status, ndel.error_message, \
|
|
210
|
+
ndel.created_at, ne.source_event_name \
|
|
211
|
+
FROM notification_deliveries ndel \
|
|
212
|
+
JOIN notification_events ne ON ne.id = ndel.event_id \
|
|
213
|
+
WHERE ne.source_event_name LIKE 'ingress.%' \
|
|
214
|
+
ORDER BY ndel.created_at DESC LIMIT 20;"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Check lifecycle signals
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
221
|
+
"SELECT source_event_name, source_channel, dedupe_key, created_at \
|
|
222
|
+
FROM notification_events \
|
|
223
|
+
WHERE source_event_name LIKE 'ingress.trusted_contact.%' \
|
|
224
|
+
ORDER BY created_at DESC LIMIT 20;"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 7. Manually Add a Trusted Contact (Bypass Verification)
|
|
228
|
+
|
|
229
|
+
If the verification flow cannot be completed, an operator can directly create an active member:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
curl -s -X POST "$BASE/v1/ingress/members" \
|
|
233
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
234
|
+
-H "Content-Type: application/json" \
|
|
235
|
+
-d '{
|
|
236
|
+
"sourceChannel": "telegram",
|
|
237
|
+
"externalUserId": "123456789",
|
|
238
|
+
"externalChatId": "123456789",
|
|
239
|
+
"displayName": "Alice",
|
|
240
|
+
"policy": "allow",
|
|
241
|
+
"status": "active"
|
|
242
|
+
}' | jq
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
For SMS contacts, use the E.164 phone number as the external user/chat ID:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
curl -s -X POST "$BASE/v1/ingress/members" \
|
|
249
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
250
|
+
-H "Content-Type: application/json" \
|
|
251
|
+
-d '{
|
|
252
|
+
"sourceChannel": "sms",
|
|
253
|
+
"externalUserId": "+15551234567",
|
|
254
|
+
"externalChatId": "+15551234567",
|
|
255
|
+
"displayName": "Bob",
|
|
256
|
+
"policy": "allow",
|
|
257
|
+
"status": "active"
|
|
258
|
+
}' | jq
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 8. Clean Up Expired Data
|
|
262
|
+
|
|
263
|
+
### Purge expired verification sessions
|
|
264
|
+
|
|
265
|
+
Expired sessions are already invisible to the verification flow (filtered by `expires_at`), but you can clean them up:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
269
|
+
"DELETE FROM channel_guardian_verification_challenges \
|
|
270
|
+
WHERE expires_at < $(date +%s)000 \
|
|
271
|
+
AND status IN ('awaiting_response', 'pending_bootstrap');"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Purge expired approval requests
|
|
275
|
+
|
|
276
|
+
The `sweepExpiredGuardianApprovals()` timer handles this automatically every 60 seconds, but manual cleanup:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
sqlite3 ~/.vellum/workspace/data/db/assistant.db \
|
|
280
|
+
"UPDATE channel_guardian_approval_requests \
|
|
281
|
+
SET status = 'expired' \
|
|
282
|
+
WHERE status = 'pending' AND expires_at < $(date +%s)000;"
|
|
283
|
+
```
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Trusted Contact Access Flow
|
|
2
|
+
|
|
3
|
+
Design doc defining how unknown users gain access to a Vellum assistant via channel-mediated trusted contact onboarding.
|
|
4
|
+
|
|
5
|
+
## Roles
|
|
6
|
+
|
|
7
|
+
| Role | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `guardian` | The verified owner/administrator of the assistant on a given channel. Has an active `channel_guardian_bindings` record. Approves or denies access requests. |
|
|
10
|
+
| `trusted_contact` | An external user who has completed the verification flow and holds an active `assistant_ingress_members` record with `status: 'active'` and `policy: 'allow'`. |
|
|
11
|
+
| `assistant` | The Vellum assistant daemon. Mediates the flow, enforces ACL, generates verification codes, and activates trusted contacts upon successful verification. |
|
|
12
|
+
|
|
13
|
+
## User Journey
|
|
14
|
+
|
|
15
|
+
1. **Unknown user messages the assistant** on Telegram (or SMS, or any channel).
|
|
16
|
+
2. **Assistant rejects the message** via the ingress ACL in `inbound-message-handler.ts`. The user has no `assistant_ingress_members` record, so the handler replies: *"Sorry, you haven't been approved to message this assistant. You can ask its Guardian for an invite."* and returns `{ denied: true, reason: 'not_a_member' }`.
|
|
17
|
+
3. **Notification pipeline alerts the guardian.** The rejection triggers `emitNotificationSignal()` with `sourceEventName: 'ingress.access_request'`, routing through the decision engine to all connected channels (vellum macOS app, Telegram, etc.). The guardian sees who is requesting access.
|
|
18
|
+
4. **Guardian approves the request.** The guardian responds to the notification (via Telegram inline button, macOS app, or IPC). On approval, the assistant creates a verification session via `createOutboundSession()` and generates a 6-digit verification code.
|
|
19
|
+
5. **Guardian receives the verification code.** The assistant delivers the code to the guardian's verified channel (Telegram chat, SMS, etc.).
|
|
20
|
+
6. **Guardian gives the code to the requester out-of-band** (in person, text message, phone call, etc.). This out-of-band transfer is the trust anchor: it proves the requester has a real-world relationship with the guardian.
|
|
21
|
+
7. **Requester enters the code** back to the assistant on the same channel. The inbound message handler intercepts bare 6-digit codes when a pending verification session exists for that channel.
|
|
22
|
+
8. **Assistant verifies the code and activates the user.** `validateAndConsumeChallenge()` hashes the code, matches it against the pending session, verifies identity binding (the code must come from the expected channel identity), consumes the challenge, and calls `upsertMember()` with `status: 'active'` and `policy: 'allow'`.
|
|
23
|
+
9. **All subsequent messages are accepted normally.** The ingress ACL finds an active member record and allows the message through.
|
|
24
|
+
|
|
25
|
+
## Lifecycle States
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
requested -> pending_guardian -> verification_pending -> active | denied | expired
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| State | Description | Store representation |
|
|
32
|
+
|-------|-------------|---------------------|
|
|
33
|
+
| `requested` | Unknown user messaged the assistant and was rejected. The system records the access attempt. | No member record exists. The rejection is logged in `channel_inbound_events`. A notification signal is emitted via `emitNotificationSignal()`. |
|
|
34
|
+
| `pending_guardian` | The guardian has been notified and a decision is pending. | A `channel_guardian_approval_requests` record exists with `status: 'pending'`, `toolName: 'ingress_access_request'`. |
|
|
35
|
+
| `verification_pending` | The guardian approved. A verification session is active with a 6-digit code waiting for the requester to enter. | `channel_guardian_verification_challenges` record with `status: 'awaiting_response'`, identity-bound to the requester's expected channel identity. The approval request is updated to `status: 'approved'`. |
|
|
36
|
+
| `active` | The requester entered the correct code. They are now a trusted contact. | `assistant_ingress_members` record with `status: 'active'`, `policy: 'allow'`. The verification session is `status: 'consumed'`. |
|
|
37
|
+
| `denied` | The guardian explicitly denied the request. | The approval request has `status: 'denied'`. No member record is created (or if one existed, it remains unchanged). |
|
|
38
|
+
| `expired` | The guardian never responded (approval TTL elapsed) or the requester never entered the code (session TTL elapsed). | Approval request: `status: 'expired'` (set by `sweepExpiredGuardianApprovals()`). Verification session: expires naturally when `expiresAt < Date.now()`. |
|
|
39
|
+
|
|
40
|
+
## Identity Binding Rules
|
|
41
|
+
|
|
42
|
+
Identity binding ensures the verification code can only be consumed by the intended recipient on the intended channel. The binding fields are set on the `channel_guardian_verification_challenges` record when the session is created.
|
|
43
|
+
|
|
44
|
+
| Channel | Identity fields | Binding behavior |
|
|
45
|
+
|---------|----------------|------------------|
|
|
46
|
+
| Telegram | `expectedExternalUserId` = Telegram user ID, `expectedChatId` = Telegram chat ID | Both are set when the guardian provides the requester's Telegram identity (from the original rejected message metadata). The `identityBindingStatus` is `'bound'`. Verification requires `actorExternalUserId` or `actorChatId` to match. |
|
|
47
|
+
| SMS | `expectedPhoneE164` = phone number in E.164 format | Set from the requester's phone number. Verification requires `actorExternalUserId` to match the expected phone. |
|
|
48
|
+
| Voice | `expectedPhoneE164` = phone number in E.164 format | Same as SMS: phone-based identity binding. |
|
|
49
|
+
| HTTP API | `expectedExternalUserId` = API caller identity | Bound to whatever external user ID the API client provides. |
|
|
50
|
+
|
|
51
|
+
**Anti-oracle invariant:** When identity verification fails, the error message is identical to the "invalid or expired code" message. This prevents attackers from distinguishing between a wrong code and a wrong identity, which would leak information about which identities have pending sessions.
|
|
52
|
+
|
|
53
|
+
## Mapping to Existing Stores
|
|
54
|
+
|
|
55
|
+
### Stage: `requested` (unknown user rejected)
|
|
56
|
+
|
|
57
|
+
- **No new records created.** The rejection is a stateless ACL check in `inbound-message-handler.ts` (line ~260: `findMember()` returns null, handler replies with rejection text).
|
|
58
|
+
- The inbound event is recorded in `channel_inbound_events` via `channelDeliveryStore.recordInbound()`.
|
|
59
|
+
- A notification signal is emitted via `emitNotificationSignal()`, persisted in `notification_events`.
|
|
60
|
+
|
|
61
|
+
### Stage: `pending_guardian` (guardian notified, awaiting decision)
|
|
62
|
+
|
|
63
|
+
| Store | Table | Record |
|
|
64
|
+
|-------|-------|--------|
|
|
65
|
+
| `channel-guardian-store.ts` | `channel_guardian_approval_requests` | `status: 'pending'`, `toolName: 'ingress_access_request'`, `requesterExternalUserId`, `requesterChatId`, `guardianExternalUserId`, `guardianChatId` (resolved from active `channel_guardian_bindings`), `expiresAt` (GUARDIAN_APPROVAL_TTL_MS from now). |
|
|
66
|
+
| `notification_events` | `notification_events` | Event with `sourceEventName: 'ingress.access_request'`, links to the conversation. |
|
|
67
|
+
| `notification_decisions` | `notification_decisions` | Decision engine output: which channels to notify, confidence, reasoning. |
|
|
68
|
+
| `notification_deliveries` | `notification_deliveries` | Per-channel delivery records (Telegram, vellum, etc.). |
|
|
69
|
+
|
|
70
|
+
### Stage: `verification_pending` (guardian approved, code issued)
|
|
71
|
+
|
|
72
|
+
| Store | Table | Record |
|
|
73
|
+
|-------|-------|--------|
|
|
74
|
+
| `channel-guardian-store.ts` | `channel_guardian_approval_requests` | Updated to `status: 'approved'`, `decidedByExternalUserId` set. |
|
|
75
|
+
| `channel-guardian-store.ts` | `channel_guardian_verification_challenges` | New record: `status: 'awaiting_response'`, `identityBindingStatus: 'bound'`, `expectedExternalUserId`/`expectedChatId`/`expectedPhoneE164` set to the requester's identity, `challengeHash` = SHA-256 of the 6-digit code, `expiresAt` = 10 minutes from creation, `codeDigits: 6`. |
|
|
76
|
+
|
|
77
|
+
### Stage: `active` (code verified, trusted contact created)
|
|
78
|
+
|
|
79
|
+
| Store | Table | Record |
|
|
80
|
+
|-------|-------|--------|
|
|
81
|
+
| `ingress-member-store.ts` | `assistant_ingress_members` | Upserted via `upsertMember()`: `status: 'active'`, `policy: 'allow'`, `sourceChannel`, `externalUserId`, `externalChatId`, `displayName`, `username`. |
|
|
82
|
+
| `channel-guardian-store.ts` | `channel_guardian_verification_challenges` | Updated to `status: 'consumed'`, `consumedByExternalUserId`, `consumedByChatId` set. |
|
|
83
|
+
| `channel-guardian-store.ts` | `channel_guardian_rate_limits` | Reset via `resetRateLimit()` on successful verification. |
|
|
84
|
+
|
|
85
|
+
### Stage: `denied` (guardian rejected)
|
|
86
|
+
|
|
87
|
+
| Store | Table | Record |
|
|
88
|
+
|-------|-------|--------|
|
|
89
|
+
| `channel-guardian-store.ts` | `channel_guardian_approval_requests` | Updated to `status: 'denied'`, `decidedByExternalUserId` set. |
|
|
90
|
+
|
|
91
|
+
No member record is created. No verification session is created.
|
|
92
|
+
|
|
93
|
+
### Stage: `expired`
|
|
94
|
+
|
|
95
|
+
| Store | Table | Record |
|
|
96
|
+
|-------|-------|--------|
|
|
97
|
+
| `channel-guardian-store.ts` | `channel_guardian_approval_requests` | Updated to `status: 'expired'` by `sweepExpiredGuardianApprovals()` (runs every 60s). |
|
|
98
|
+
| `channel-guardian-store.ts` | `channel_guardian_verification_challenges` | Expires naturally: `expiresAt < Date.now()` makes it invisible to `findPendingChallengeByHash()`. |
|
|
99
|
+
|
|
100
|
+
### Invites (alternative path)
|
|
101
|
+
|
|
102
|
+
The `assistant_ingress_invites` table supports a parallel invite-based onboarding path. An invite carries a SHA-256 hashed token and can be redeemed via `redeemInvite()`, which atomically creates an active member record. This path is distinct from the trusted contact flow but serves the same end state: an active member in `assistant_ingress_members`.
|
|
103
|
+
|
|
104
|
+
| Table | Purpose in trusted contact flow |
|
|
105
|
+
|-------|--------------------------------|
|
|
106
|
+
| `assistant_ingress_invites` | Not used in the guardian-mediated flow. Available as an alternative for direct invite links (e.g., guardian shares a URL instead of going through the approval + verification flow). |
|
|
107
|
+
|
|
108
|
+
## Sequence Diagram
|
|
109
|
+
|
|
110
|
+
```mermaid
|
|
111
|
+
sequenceDiagram
|
|
112
|
+
participant U as Unknown User
|
|
113
|
+
participant A as Assistant (Daemon)
|
|
114
|
+
participant G as Guardian
|
|
115
|
+
participant N as Notification Pipeline
|
|
116
|
+
|
|
117
|
+
U->>A: Send message on Telegram/SMS
|
|
118
|
+
A->>A: findMember() → null
|
|
119
|
+
A-->>U: "You haven't been approved. Ask the Guardian."
|
|
120
|
+
|
|
121
|
+
A->>N: emitNotificationSignal('ingress.access_request')
|
|
122
|
+
N->>N: evaluateSignal() → shouldNotify: true
|
|
123
|
+
N->>G: Deliver notification (Telegram/vellum/SMS)
|
|
124
|
+
|
|
125
|
+
Note over G: Guardian sees access request<br/>with requester identity
|
|
126
|
+
|
|
127
|
+
alt Guardian approves
|
|
128
|
+
G->>A: Approve (inline button / IPC / plain text)
|
|
129
|
+
A->>A: resolveApprovalRequest(id, 'approved')
|
|
130
|
+
A->>A: createOutboundSession(bound to requester identity)
|
|
131
|
+
A-->>G: "Approved. Verification code: 847293.<br/>Give this to the requester."
|
|
132
|
+
|
|
133
|
+
Note over G,U: Out-of-band code transfer<br/>(in person, text, call)
|
|
134
|
+
|
|
135
|
+
U->>A: Send "847293" on same channel
|
|
136
|
+
A->>A: parseGuardianVerifyCommand() → bare 6-digit code
|
|
137
|
+
A->>A: validateAndConsumeChallenge()
|
|
138
|
+
A->>A: Identity check: actorId matches expected
|
|
139
|
+
A->>A: Hash matches, not expired → consume
|
|
140
|
+
A->>A: upsertMember(status: 'active', policy: 'allow')
|
|
141
|
+
A-->>U: "Verification successful! You now have access."
|
|
142
|
+
|
|
143
|
+
U->>A: Subsequent messages
|
|
144
|
+
A->>A: findMember() → active, policy: allow
|
|
145
|
+
A->>A: Process message normally
|
|
146
|
+
|
|
147
|
+
else Guardian denies
|
|
148
|
+
G->>A: Deny (inline button / IPC / plain text)
|
|
149
|
+
A->>A: resolveApprovalRequest(id, 'denied')
|
|
150
|
+
A-->>U: (No notification — user only knows<br/>they were denied if they message again)
|
|
151
|
+
|
|
152
|
+
else Guardian never responds
|
|
153
|
+
Note over A: sweepExpiredGuardianApprovals()<br/>runs every 60 seconds
|
|
154
|
+
A->>A: Approval TTL elapsed → status: 'expired'
|
|
155
|
+
A->>A: handleChannelDecision(reject)
|
|
156
|
+
A-->>U: "Your access request has expired."
|
|
157
|
+
A-->>G: "The access request has expired."
|
|
158
|
+
|
|
159
|
+
else Code expires (requester never enters it)
|
|
160
|
+
Note over A: Verification session TTL: 10 min
|
|
161
|
+
A->>A: Session expiresAt < now
|
|
162
|
+
Note over A: Next attempt returns<br/>"code invalid or expired"
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Failure and Stale Paths
|
|
167
|
+
|
|
168
|
+
### Guardian never responds
|
|
169
|
+
|
|
170
|
+
- The `sweepExpiredGuardianApprovals()` timer runs every 60 seconds and finds approval requests where `expiresAt <= Date.now()` and `status === 'pending'`.
|
|
171
|
+
- It auto-denies the underlying request via `handleChannelDecision()` and notifies both the requester and guardian.
|
|
172
|
+
- The approval request is updated to `status: 'expired'`.
|
|
173
|
+
|
|
174
|
+
### Verification code expires
|
|
175
|
+
|
|
176
|
+
- Verification sessions have a 10-minute TTL (`CHALLENGE_TTL_MS`).
|
|
177
|
+
- After expiry, `findPendingChallengeByHash()` filters by `expiresAt > now`, so the code silently becomes invalid.
|
|
178
|
+
- The requester receives the generic "code is invalid or has expired" message.
|
|
179
|
+
- The guardian can re-initiate the flow by approving again, which creates a new session (auto-revoking any prior pending sessions).
|
|
180
|
+
|
|
181
|
+
### Wrong code entered
|
|
182
|
+
|
|
183
|
+
- `validateAndConsumeChallenge()` hashes the input and looks for a matching challenge. No match returns a generic failure.
|
|
184
|
+
- The invalid attempt is recorded via `recordInvalidAttempt()` with a sliding window (`RATE_LIMIT_WINDOW_MS = 15 min`).
|
|
185
|
+
- After `RATE_LIMIT_MAX_ATTEMPTS = 5` failures within the window, the actor is locked out for `RATE_LIMIT_LOCKOUT_MS = 30 min`.
|
|
186
|
+
- The lockout message is identical to the "invalid code" message (anti-oracle).
|
|
187
|
+
|
|
188
|
+
### Identity mismatch
|
|
189
|
+
|
|
190
|
+
- If the code is entered from a different channel identity than expected (e.g., a different Telegram user ID), the identity check in `validateAndConsumeChallenge()` fails.
|
|
191
|
+
- The error message is identical to "invalid or expired" to prevent identity oracle attacks.
|
|
192
|
+
- The attempt counts toward the rate limit.
|
|
193
|
+
|
|
194
|
+
### Duplicate access requests
|
|
195
|
+
|
|
196
|
+
- If the unknown user messages the assistant multiple times before the guardian responds, each message hits the ACL rejection path independently.
|
|
197
|
+
- The notification pipeline's deduplication (`dedupeKey` on `notification_events`) prevents flooding the guardian with duplicate notifications.
|
|
198
|
+
- Only one approval request should be active at a time per (channel, requester) pair.
|
|
199
|
+
|
|
200
|
+
### Requester already has a member record in non-active state
|
|
201
|
+
|
|
202
|
+
- `revoked`: The ACL check in `inbound-message-handler.ts` finds the member but `status !== 'active'`, returning `{ denied: true, reason: 'member_revoked' }`. The trusted contact flow can be re-initiated by the guardian.
|
|
203
|
+
- `blocked`: Same rejection path, returning `{ denied: true, reason: 'member_blocked' }`. Blocked members cannot re-enter the flow without the guardian explicitly unblocking them first.
|
|
204
|
+
- `pending`: Same rejection path. The member exists but has not completed verification.
|
|
205
|
+
|
|
206
|
+
### Guardian revokes a trusted contact
|
|
207
|
+
|
|
208
|
+
- `revokeMember()` sets `status: 'revoked'` and optional `revokedReason`.
|
|
209
|
+
- Subsequent messages from the revoked user are rejected at the ACL layer.
|
|
210
|
+
- The user can be re-onboarded by going through the full flow again.
|
|
211
|
+
|
|
212
|
+
## Replay Protection
|
|
213
|
+
|
|
214
|
+
### Code reuse prevention
|
|
215
|
+
|
|
216
|
+
- Each verification session creates a single `channel_guardian_verification_challenges` record.
|
|
217
|
+
- `consumeChallenge()` atomically sets `status: 'consumed'`, making the code permanently unusable.
|
|
218
|
+
- `findPendingChallengeByHash()` only matches challenges with `status IN ('pending', 'pending_bootstrap', 'awaiting_response')`, so consumed challenges are invisible.
|
|
219
|
+
|
|
220
|
+
### Session supersession
|
|
221
|
+
|
|
222
|
+
- `createVerificationSession()` auto-revokes all prior `pending`/`pending_bootstrap`/`awaiting_response` sessions for the same `(assistantId, channel)` before creating a new one.
|
|
223
|
+
- This ensures only one session is valid at any time, preventing replay of older codes.
|
|
224
|
+
|
|
225
|
+
### Rate limiting
|
|
226
|
+
|
|
227
|
+
- Per-actor, per-channel sliding window rate limiting via `channel_guardian_rate_limits`.
|
|
228
|
+
- Individual attempt timestamps are stored (not just a counter) for true sliding window behavior.
|
|
229
|
+
- After `maxAttempts` (5) within `windowMs` (15 min), the actor is locked out for `lockoutMs` (30 min).
|
|
230
|
+
- Successful verification resets the rate limit counter via `resetRateLimit()`.
|
|
231
|
+
|
|
232
|
+
### Brute-force resistance
|
|
233
|
+
|
|
234
|
+
- Identity-bound sessions use 6-digit numeric codes (10^6 = 1M possibilities), which is acceptable because the identity binding provides a second factor: the attacker must also control the correct channel identity.
|
|
235
|
+
- Unbound sessions (legacy inbound challenges) use 32-byte hex secrets (~2^128 entropy), making enumeration infeasible.
|
|
236
|
+
- The 10-minute TTL limits the attack window.
|
|
237
|
+
- Rate limiting (5 attempts / 15 min, 30 min lockout) further constrains brute-force attempts.
|
|
238
|
+
|
|
239
|
+
### Deduplication of approval requests
|
|
240
|
+
|
|
241
|
+
- The notification pipeline uses `dedupeKey` to prevent duplicate notification events.
|
|
242
|
+
- Approval requests should include a deduplication key derived from `(channel, requesterExternalUserId)` to prevent multiple concurrent approval requests for the same requester.
|
|
243
|
+
|
|
244
|
+
### Anti-oracle design
|
|
245
|
+
|
|
246
|
+
- All failure messages (wrong code, expired code, identity mismatch, rate-limited) return the same generic text: *"The verification code is invalid or has expired."*
|
|
247
|
+
- This prevents attackers from distinguishing between failure modes, which could leak information about valid codes, valid identities, or rate-limit state.
|
package/package.json
CHANGED
|
@@ -83,6 +83,8 @@ const INVENTORY_UNEXTRACTABLE = new Set<string>([
|
|
|
83
83
|
const SWIFT_AHEAD_ALLOWLIST = new Set<string>([
|
|
84
84
|
// Defined in Swift LayoutConfig.swift ahead of daemon implementation
|
|
85
85
|
'ui_layout_config',
|
|
86
|
+
// Defined in Swift HTTPDaemonClient ahead of daemon token rotation endpoint
|
|
87
|
+
'token_rotated',
|
|
86
88
|
]);
|
|
87
89
|
|
|
88
90
|
// --- Extract Swift decode cases ---
|
|
@@ -1347,6 +1347,7 @@ exports[`IPC message snapshots ServerMessage types session_list_response seriali
|
|
|
1347
1347
|
{
|
|
1348
1348
|
"sessions": [
|
|
1349
1349
|
{
|
|
1350
|
+
"createdAt": 1699999000,
|
|
1350
1351
|
"id": "sess-001",
|
|
1351
1352
|
"threadType": "standard",
|
|
1352
1353
|
"title": "First session",
|
|
@@ -1359,6 +1360,7 @@ exports[`IPC message snapshots ServerMessage types session_list_response seriali
|
|
|
1359
1360
|
"lastSeenSignalType": "macos_notification_view",
|
|
1360
1361
|
"latestAssistantMessageAt": 1700001000,
|
|
1361
1362
|
},
|
|
1363
|
+
"createdAt": 1700000000,
|
|
1362
1364
|
"id": "sess-002",
|
|
1363
1365
|
"threadType": "standard",
|
|
1364
1366
|
"title": "Second session",
|
|
@@ -2790,12 +2792,6 @@ exports[`IPC message snapshots ServerMessage types tasks_changed serializes to e
|
|
|
2790
2792
|
}
|
|
2791
2793
|
`;
|
|
2792
2794
|
|
|
2793
|
-
exports[`IPC message snapshots ServerMessage types open_tasks_window serializes to expected JSON 1`] = `
|
|
2794
|
-
{
|
|
2795
|
-
"type": "open_tasks_window",
|
|
2796
|
-
}
|
|
2797
|
-
`;
|
|
2798
|
-
|
|
2799
2795
|
exports[`IPC message snapshots ServerMessage types task_run_thread_created serializes to expected JSON 1`] = `
|
|
2800
2796
|
{
|
|
2801
2797
|
"conversationId": "conv-task-run-001",
|