@vellumai/assistant 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +45 -18
  3. package/package.json +1 -1
  4. package/scripts/ipc/generate-swift.ts +13 -0
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +100 -0
  6. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  7. package/src/__tests__/approval-message-composer.test.ts +253 -0
  8. package/src/__tests__/call-domain.test.ts +12 -2
  9. package/src/__tests__/call-orchestrator.test.ts +391 -1
  10. package/src/__tests__/call-routes-http.test.ts +27 -2
  11. package/src/__tests__/channel-approval-routes.test.ts +397 -135
  12. package/src/__tests__/channel-approvals.test.ts +99 -3
  13. package/src/__tests__/channel-delivery-store.test.ts +30 -4
  14. package/src/__tests__/channel-guardian.test.ts +261 -22
  15. package/src/__tests__/channel-readiness-service.test.ts +257 -0
  16. package/src/__tests__/config-schema.test.ts +2 -1
  17. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  18. package/src/__tests__/daemon-lifecycle.test.ts +636 -0
  19. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  20. package/src/__tests__/entity-search.test.ts +615 -0
  21. package/src/__tests__/gateway-only-enforcement.test.ts +19 -13
  22. package/src/__tests__/handlers-twilio-config.test.ts +480 -0
  23. package/src/__tests__/ipc-snapshot.test.ts +63 -0
  24. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  25. package/src/__tests__/run-orchestrator-assistant-events.test.ts +4 -0
  26. package/src/__tests__/run-orchestrator.test.ts +22 -0
  27. package/src/__tests__/secret-scanner.test.ts +223 -0
  28. package/src/__tests__/session-runtime-assembly.test.ts +85 -1
  29. package/src/__tests__/shell-parser-property.test.ts +357 -2
  30. package/src/__tests__/sms-messaging-provider.test.ts +125 -0
  31. package/src/__tests__/system-prompt.test.ts +25 -1
  32. package/src/__tests__/tool-executor-lifecycle-events.test.ts +34 -1
  33. package/src/__tests__/twilio-routes.test.ts +39 -3
  34. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  35. package/src/__tests__/user-reference.test.ts +68 -0
  36. package/src/__tests__/web-search.test.ts +1 -1
  37. package/src/__tests__/work-item-output.test.ts +110 -0
  38. package/src/calls/call-domain.ts +8 -5
  39. package/src/calls/call-orchestrator.ts +85 -22
  40. package/src/calls/twilio-config.ts +17 -11
  41. package/src/calls/twilio-rest.ts +276 -0
  42. package/src/calls/twilio-routes.ts +39 -1
  43. package/src/cli/map.ts +6 -0
  44. package/src/commands/__tests__/cc-command-registry.test.ts +67 -0
  45. package/src/commands/cc-command-registry.ts +14 -1
  46. package/src/config/bundled-skills/claude-code/TOOLS.json +10 -3
  47. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  48. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  49. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  50. package/src/config/bundled-skills/media-processing/SKILL.md +199 -0
  51. package/src/config/bundled-skills/media-processing/TOOLS.json +320 -0
  52. package/src/config/bundled-skills/media-processing/services/capability-registry.ts +137 -0
  53. package/src/config/bundled-skills/media-processing/services/event-detection-service.ts +280 -0
  54. package/src/config/bundled-skills/media-processing/services/feedback-aggregation.ts +144 -0
  55. package/src/config/bundled-skills/media-processing/services/feedback-store.ts +136 -0
  56. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +261 -0
  57. package/src/config/bundled-skills/media-processing/services/retrieval-service.ts +95 -0
  58. package/src/config/bundled-skills/media-processing/services/timeline-service.ts +267 -0
  59. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +301 -0
  60. package/src/config/bundled-skills/media-processing/tools/detect-events.ts +110 -0
  61. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +190 -0
  62. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  63. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  64. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +166 -0
  65. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  66. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +300 -0
  67. package/src/config/bundled-skills/media-processing/tools/recalibrate.ts +235 -0
  68. package/src/config/bundled-skills/media-processing/tools/select-tracking-profile.ts +142 -0
  69. package/src/config/bundled-skills/media-processing/tools/submit-feedback.ts +150 -0
  70. package/src/config/bundled-skills/messaging/SKILL.md +24 -5
  71. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  72. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  73. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  74. package/src/config/defaults.ts +2 -1
  75. package/src/config/schema.ts +9 -3
  76. package/src/config/skills.ts +5 -32
  77. package/src/config/system-prompt.ts +40 -0
  78. package/src/config/templates/IDENTITY.md +2 -2
  79. package/src/config/user-reference.ts +29 -0
  80. package/src/config/vellum-skills/catalog.json +58 -0
  81. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +3 -3
  82. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +3 -3
  83. package/src/config/vellum-skills/sms-setup/SKILL.md +118 -0
  84. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -1
  85. package/src/config/vellum-skills/twilio-setup/SKILL.md +76 -6
  86. package/src/daemon/auth-manager.ts +103 -0
  87. package/src/daemon/computer-use-session.ts +8 -1
  88. package/src/daemon/config-watcher.ts +253 -0
  89. package/src/daemon/handlers/config.ts +819 -22
  90. package/src/daemon/handlers/dictation.ts +182 -0
  91. package/src/daemon/handlers/identity.ts +14 -23
  92. package/src/daemon/handlers/index.ts +2 -0
  93. package/src/daemon/handlers/sessions.ts +2 -0
  94. package/src/daemon/handlers/shared.ts +3 -0
  95. package/src/daemon/handlers/skills.ts +6 -7
  96. package/src/daemon/handlers/work-items.ts +15 -7
  97. package/src/daemon/ipc-contract-inventory.json +10 -0
  98. package/src/daemon/ipc-contract.ts +114 -4
  99. package/src/daemon/ipc-handler.ts +87 -0
  100. package/src/daemon/lifecycle.ts +18 -4
  101. package/src/daemon/ride-shotgun-handler.ts +11 -1
  102. package/src/daemon/server.ts +111 -504
  103. package/src/daemon/session-agent-loop.ts +10 -15
  104. package/src/daemon/session-runtime-assembly.ts +115 -44
  105. package/src/daemon/session-tool-setup.ts +2 -0
  106. package/src/daemon/session.ts +19 -2
  107. package/src/inbound/public-ingress-urls.ts +3 -3
  108. package/src/memory/channel-guardian-store.ts +2 -1
  109. package/src/memory/db-connection.ts +28 -0
  110. package/src/memory/db-init.ts +1163 -0
  111. package/src/memory/db.ts +2 -2007
  112. package/src/memory/embedding-backend.ts +79 -11
  113. package/src/memory/indexer.ts +2 -0
  114. package/src/memory/job-handlers/media-processing.ts +100 -0
  115. package/src/memory/job-utils.ts +64 -4
  116. package/src/memory/jobs-store.ts +2 -1
  117. package/src/memory/jobs-worker.ts +11 -1
  118. package/src/memory/media-store.ts +759 -0
  119. package/src/memory/recall-cache.ts +107 -0
  120. package/src/memory/retriever.ts +36 -2
  121. package/src/memory/schema-migration.ts +984 -0
  122. package/src/memory/schema.ts +99 -0
  123. package/src/memory/search/entity.ts +208 -25
  124. package/src/memory/search/ranking.ts +6 -1
  125. package/src/memory/search/types.ts +26 -0
  126. package/src/messaging/provider-types.ts +2 -0
  127. package/src/messaging/providers/sms/adapter.ts +204 -0
  128. package/src/messaging/providers/sms/client.ts +93 -0
  129. package/src/messaging/providers/sms/types.ts +7 -0
  130. package/src/permissions/checker.ts +16 -2
  131. package/src/permissions/prompter.ts +14 -3
  132. package/src/permissions/trust-store.ts +7 -0
  133. package/src/runtime/approval-message-composer.ts +143 -0
  134. package/src/runtime/channel-approvals.ts +29 -7
  135. package/src/runtime/channel-guardian-service.ts +44 -18
  136. package/src/runtime/channel-readiness-service.ts +292 -0
  137. package/src/runtime/channel-readiness-types.ts +29 -0
  138. package/src/runtime/gateway-client.ts +2 -1
  139. package/src/runtime/http-server.ts +65 -28
  140. package/src/runtime/http-types.ts +3 -0
  141. package/src/runtime/routes/call-routes.ts +2 -1
  142. package/src/runtime/routes/channel-routes.ts +237 -103
  143. package/src/runtime/routes/run-routes.ts +7 -1
  144. package/src/runtime/run-orchestrator.ts +43 -3
  145. package/src/security/secret-scanner.ts +218 -0
  146. package/src/skills/frontmatter.ts +63 -0
  147. package/src/skills/slash-commands.ts +23 -0
  148. package/src/skills/vellum-catalog-remote.ts +107 -0
  149. package/src/tools/assets/materialize.ts +2 -2
  150. package/src/tools/browser/auto-navigate.ts +132 -24
  151. package/src/tools/browser/browser-manager.ts +67 -61
  152. package/src/tools/calls/call-start.ts +1 -0
  153. package/src/tools/claude-code/claude-code.ts +55 -3
  154. package/src/tools/credentials/vault.ts +1 -1
  155. package/src/tools/execution-target.ts +11 -1
  156. package/src/tools/executor.ts +10 -2
  157. package/src/tools/network/web-search.ts +1 -1
  158. package/src/tools/skills/vellum-catalog.ts +61 -156
  159. package/src/tools/terminal/parser.ts +21 -5
  160. package/src/tools/types.ts +2 -0
  161. package/src/twitter/router.ts +1 -1
  162. package/src/util/platform.ts +43 -1
  163. package/src/util/retry.ts +4 -4
@@ -12,7 +12,7 @@ You are helping your user create a Slack App and OAuth credentials so the Messag
12
12
 
13
13
  ## Prerequisites
14
14
 
15
- Before starting, check that `ingress.publicBaseUrl` is configured (Settings > Public Ingress, or `INGRESS_PUBLIC_BASE_URL` env var). If it is not set, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the public URL. The OAuth redirect URI depends on this value.
15
+ Before starting, check that `ingress.publicBaseUrl` is configured (`INGRESS_PUBLIC_BASE_URL` env var or workspace config). If it is not set, load and execute the **public-ingress** skill first (`skill_load` with `skill: "public-ingress"`) to set up an ngrok tunnel and persist the public URL. The OAuth redirect URI depends on this value.
16
16
 
17
17
  ## Before You Start
18
18
 
@@ -89,7 +89,7 @@ Tell the user: "Permissions configured! Now let's set up the redirect URL and ge
89
89
 
90
90
  Navigate to the "OAuth & Permissions" page if not already there.
91
91
 
92
- The redirect URL must point to the gateway's OAuth callback endpoint. Determine the URL by reading the `ingress.publicBaseUrl` value from the assistant's workspace config (Settings > Public Ingress) or the `INGRESS_PUBLIC_BASE_URL` environment variable. The callback path is `/webhooks/oauth/callback`.
92
+ The redirect URL must point to the gateway's OAuth callback endpoint. Determine the URL by reading the `ingress.publicBaseUrl` value from the assistant's workspace config or the `INGRESS_PUBLIC_BASE_URL` environment variable. The callback path is `/webhooks/oauth/callback`.
93
93
 
94
94
  In the "Redirect URLs" section:
95
95
  1. Click "Add New Redirect URL"
@@ -98,7 +98,7 @@ In the "Redirect URLs" section:
98
98
 
99
99
  Take a `browser_snapshot` to confirm.
100
100
 
101
- Tell the user: "Redirect URL configured. Make sure your tunnel is running and `ingress.publicBaseUrl` is set in Settings so the callback can reach the gateway."
101
+ Tell the user: "Redirect URL configured. Make sure your tunnel is running and `ingress.publicBaseUrl` is set so the callback can reach the gateway. You can always check or update this from the Settings page."
102
102
 
103
103
  ## Step 5: Extract Client ID and Client Secret
104
104
 
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: "SMS Setup"
3
+ description: "Set up and troubleshoot SMS messaging with guided Twilio configuration, compliance, and verification"
4
+ user-invocable: true
5
+ metadata: {"vellum": {"emoji": "\ud83d\udce8"}}
6
+ ---
7
+
8
+ You are helping your user set up SMS messaging. This skill orchestrates Twilio setup, SMS-specific compliance, and end-to-end testing through a conversational flow.
9
+
10
+ ## Step 1: Check Channel Readiness
11
+
12
+ First, check the current SMS channel readiness state by sending the `channel_readiness` IPC message:
13
+
14
+ ```json
15
+ {
16
+ "type": "channel_readiness",
17
+ "action": "get",
18
+ "channel": "sms"
19
+ }
20
+ ```
21
+
22
+ Inspect the `channel_readiness_response`. The response contains `snapshots` with each channel's readiness state.
23
+
24
+ - If the SMS channel shows `ready: true` and all `localChecks` pass, skip to Step 3.
25
+ - If any local checks fail, proceed to Step 2 to fix the baseline.
26
+
27
+ ## Step 2: Establish Baseline (Twilio Setup)
28
+
29
+ If SMS baseline is not ready (missing credentials, phone number, or ingress), load the `twilio-setup` skill to walk the user through the basics:
30
+
31
+ ```
32
+ skill_load skill=twilio-setup
33
+ ```
34
+
35
+ Tell the user: *"SMS needs Twilio configured first. I've loaded the Twilio setup guide — let's walk through it."*
36
+
37
+ After twilio-setup completes, re-check readiness:
38
+
39
+ ```json
40
+ {
41
+ "type": "channel_readiness",
42
+ "action": "refresh",
43
+ "channel": "sms"
44
+ }
45
+ ```
46
+
47
+ If baseline is still not ready, report the specific failures and ask the user to address them before continuing.
48
+
49
+ ## Step 3: Remote Compliance Check
50
+
51
+ Once baseline is ready, run a full readiness check including remote (Twilio API) checks:
52
+
53
+ ```json
54
+ {
55
+ "type": "channel_readiness",
56
+ "action": "refresh",
57
+ "channel": "sms",
58
+ "includeRemote": true
59
+ }
60
+ ```
61
+
62
+ Examine the remote check results:
63
+ - If all remote checks pass, proceed to Step 4.
64
+ - If compliance issues are found (e.g., toll-free verification needed), guide the user through the compliance flow:
65
+ 1. Check compliance status using the `twilio_config` IPC with `action: "sms_compliance_status"` (if available).
66
+ 2. If toll-free verification is needed, collect user information and submit via `twilio_config` with `action: "sms_submit_tollfree_verification"`.
67
+ 3. Report verification status and next steps.
68
+
69
+ **Note:** Compliance actions (sms_compliance_status, sms_submit_tollfree_verification, etc.) may not be available yet. If the IPC action is not recognized, tell the user: *"Compliance automation isn't available yet. You may need to check Twilio Console manually for toll-free verification status."*
70
+
71
+ ### Data Collection for Verification (Individual-First)
72
+
73
+ When collecting information for toll-free verification:
74
+ - Assume the user is an **individual / sole proprietor** by default
75
+ - Do NOT ask for EIN, business registration number, or business registration authority
76
+ - Explain that Twilio labels some fields as "business" fields even for individual submitters
77
+ - Only collect what's required: business name (can be personal name), website (can be personal site), notification email, use case, message samples, opt-in info
78
+ - If Twilio rejects the submission requiring business registration, explain the situation and guide through the fallback path
79
+
80
+ ## Step 4: Test Send
81
+
82
+ Run a test SMS to verify end-to-end delivery:
83
+
84
+ Tell the user: *"Let's send a test SMS to verify everything works. What phone number should I send the test to?"*
85
+
86
+ After the user provides a number, send a test message using the messaging tools:
87
+ - Use `messaging_send` with `platform: "sms"`, `conversation_id: "<phone number>"`, and a test message like "Test SMS from your Vellum assistant."
88
+ - Report the result honestly:
89
+ - If the send succeeds: *"The message was accepted by Twilio. Note: 'accepted' means Twilio received it for delivery, not that it reached the handset yet. Delivery can take a few seconds to a few minutes."*
90
+ - If the send fails: report the error and suggest troubleshooting steps
91
+
92
+ ## Step 5: Final Status Report
93
+
94
+ After completing (or skipping) the test, present a clear summary:
95
+
96
+ **If everything passed:**
97
+ *"SMS is ready! Here's your setup status:"*
98
+ - Twilio credentials: configured
99
+ - Phone number: {number}
100
+ - Ingress: configured
101
+ - Compliance: {status}
102
+ - Test send: {result}
103
+
104
+ **If there are blockers:**
105
+ *"SMS setup is partially complete. Here's what still needs attention:"*
106
+ - List each blocker with the specific next action
107
+
108
+ ## Troubleshooting
109
+
110
+ If the user returns to this skill after initial setup:
111
+ 1. Always start with Step 1 (readiness check) to assess current state
112
+ 2. Skip steps that are already complete
113
+ 3. Focus on the specific issue the user is experiencing
114
+
115
+ Common issues:
116
+ - **"Messages not delivering"** — Check compliance status, verify the number isn't flagged
117
+ - **"Twilio error on send"** — Check credentials, phone number assignment, and ingress
118
+ - **"Trial account limitations"** — Explain that trial accounts can only send to verified numbers
@@ -103,12 +103,17 @@ Before reporting success, confirm the guardian binding was actually created. Sen
103
103
 
104
104
  ### Step 8: Report Success
105
105
 
106
+ First, retrieve the bot identity by sending a `telegram_config` IPC message with `action: "get"` and reading the `botUsername` field from the response.
107
+
106
108
  Summarize what was done:
109
+ - Bot identity: @{botUsername}
107
110
  - Bot verified and credentials stored securely via daemon
108
111
  - Webhook registration: handled automatically by the gateway
109
112
  - Bot commands registered: /new, /guardian_verify
110
- - Guardian identity verified (if completed and binding confirmed)
113
+ - Guardian identity: {verified | not configured}
114
+ - Guardian verification status: {verified via challenge | skipped}
111
115
  - Routing configuration validated
116
+ - To re-check guardian status later, send `guardian_verification` with `action: "status"` and `channel: "telegram"`
112
117
 
113
118
  The gateway automatically detects credentials from the vault, reconciles the Telegram webhook registration, and begins accepting Telegram webhooks shortly. In single-assistant mode, routing is automatically configured — no manual environment variable configuration or webhook registration is needed. If the webhook secret changes later, the gateway's credential watcher will automatically re-register the webhook. If the ingress URL changes (e.g., tunnel restart), the assistant daemon triggers an immediate internal reconcile so the webhook re-registers automatically without a gateway restart.
114
119
 
@@ -18,6 +18,27 @@ This skill manages the full Twilio lifecycle:
18
18
 
19
19
  All operations go through the `twilio_config` IPC handler on the daemon, which validates inputs, stores credentials securely, and manages phone number state.
20
20
 
21
+ ### Multi-Assistant Setups
22
+
23
+ In a multi-assistant environment (multiple assistants sharing the same daemon), some `twilio_config` actions are **assistant-scoped** while others are **global** (shared across all assistants):
24
+
25
+ **Global actions** (ignore `assistantId` — credentials are shared across all assistants):
26
+ - `set_credentials` — Stores Account SID and Auth Token in global secure storage (`credential:twilio:*` keys). All assistants share the same Twilio account credentials.
27
+ - `clear_credentials` — Removes the globally stored Account SID and Auth Token. This affects all assistants.
28
+
29
+ **Assistant-scoped actions** (use `assistantId` to scope phone number configuration per assistant):
30
+ - `get` — Returns the phone number assigned to the specified assistant (falls back to the legacy global number if no per-assistant mapping exists).
31
+ - `assign_number` — Assigns a phone number to a specific assistant via the per-assistant mapping.
32
+ - `provision_number` — Provisions a new number and assigns it to the specified assistant.
33
+ - `list_numbers` — Lists all phone numbers on the shared Twilio account (uses global credentials).
34
+
35
+ Include `assistantId` in assistant-scoped actions whenever:
36
+ - Multiple assistants share the same Twilio account but use different phone numbers
37
+ - You want to ensure configuration changes only affect a specific assistant
38
+ - The user has explicitly selected or referenced a particular assistant
39
+
40
+ All IPC examples below include the optional `assistantId` field in assistant-scoped actions. Omit it in single-assistant setups. For global actions (`set_credentials`, `clear_credentials`), the `assistantId` field is accepted but ignored.
41
+
21
42
  ## Step 1: Check Current Configuration
22
43
 
23
44
  First, check whether Twilio is already configured by sending the `twilio_config` IPC message with `action: "get"`:
@@ -25,7 +46,8 @@ First, check whether Twilio is already configured by sending the `twilio_config`
25
46
  ```json
26
47
  {
27
48
  "type": "twilio_config",
28
- "action": "get"
49
+ "action": "get",
50
+ "assistantId": "<optional — omit for single-assistant setups>"
29
51
  }
30
52
  ```
31
53
 
@@ -62,6 +84,8 @@ After both credentials are collected, retrieve them from secure storage and pass
62
84
 
63
85
  Both `accountSid` and `authToken` are required — the daemon validates the credentials against the Twilio API before storing them. If credentials are invalid, the daemon returns an error. Tell the user and ask them to re-enter via the secure prompt.
64
86
 
87
+ **Note:** `set_credentials` is a global operation — credentials are stored once and shared across all assistants. The `assistantId` field is accepted but ignored.
88
+
65
89
  ## Step 3: Get a Phone Number
66
90
 
67
91
  The assistant needs a phone number to make calls and send SMS. There are two paths:
@@ -75,7 +99,8 @@ If the user wants to buy a new number through Twilio, send:
75
99
  "type": "twilio_config",
76
100
  "action": "provision_number",
77
101
  "areaCode": "415",
78
- "country": "US"
102
+ "country": "US",
103
+ "assistantId": "<optional — omit for single-assistant setups>"
79
104
  }
80
105
  ```
81
106
 
@@ -100,7 +125,8 @@ If the user already has a Twilio phone number, first list available numbers:
100
125
  ```json
101
126
  {
102
127
  "type": "twilio_config",
103
- "action": "list_numbers"
128
+ "action": "list_numbers",
129
+ "assistantId": "<optional — omit for single-assistant setups>"
104
130
  }
105
131
  ```
106
132
 
@@ -112,7 +138,8 @@ Then assign the chosen number:
112
138
  {
113
139
  "type": "twilio_config",
114
140
  "action": "assign_number",
115
- "phoneNumber": "+14155551234"
141
+ "phoneNumber": "+14155551234",
142
+ "assistantId": "<optional — omit for single-assistant setups>"
116
143
  }
117
144
  ```
118
145
 
@@ -132,7 +159,8 @@ Then assign it through the IPC:
132
159
  {
133
160
  "type": "twilio_config",
134
161
  "action": "assign_number",
135
- "phoneNumber": "+14155551234"
162
+ "phoneNumber": "+14155551234",
163
+ "assistantId": "<optional — omit for single-assistant setups>"
136
164
  }
137
165
  ```
138
166
 
@@ -171,6 +199,46 @@ Confirm:
171
199
 
172
200
  Tell the user: **"Twilio is configured. Your assistant's phone number is {phoneNumber}. This number is used for both voice calls and SMS messaging."**
173
201
 
202
+ ## Step 5.5: Guardian Verification (SMS)
203
+
204
+ Now link the user's phone number as the trusted SMS guardian for this assistant. Tell the user: "Now let's verify your guardian identity for SMS. This links your phone number as the trusted guardian for SMS messaging."
205
+
206
+ 1. Send the `guardian_verification` IPC message with `action: "create_challenge"` and `channel: "sms"`:
207
+
208
+ ```json
209
+ {
210
+ "type": "guardian_verification",
211
+ "action": "create_challenge",
212
+ "channel": "sms",
213
+ "assistantId": "<optional — omit for single-assistant setups>"
214
+ }
215
+ ```
216
+
217
+ 2. The daemon returns a `guardian_verification_response` with `success: true`, `secret`, and `instruction`. Display the instruction to the user. It will look like: "Send `/guardian_verify <secret>` to your bot via SMS within 10 minutes."
218
+
219
+ 3. Wait for the user to confirm they have sent the verification code via SMS to the assistant's phone number.
220
+
221
+ 4. Check verification status by sending `guardian_verification` with `action: "status"` and `channel: "sms"`:
222
+
223
+ ```json
224
+ {
225
+ "type": "guardian_verification",
226
+ "action": "status",
227
+ "channel": "sms",
228
+ "assistantId": "<optional — omit for single-assistant setups>"
229
+ }
230
+ ```
231
+
232
+ 5. If `bound` is `true`: "Guardian verified! Your phone number is now the trusted SMS guardian."
233
+
234
+ 6. If `bound` is `false` and the user claims they sent the code: "The verification doesn't appear to have succeeded. Let's generate a new challenge." Repeat from substep 1.
235
+
236
+ **Note:** Guardian verification is optional but recommended. If the user declines or wants to skip, proceed to Step 6 without blocking.
237
+
238
+ To re-check guardian status later, send `guardian_verification` with `action: "status"` and `channel: "sms"`.
239
+
240
+ Report the guardian verification result: **"Guardian identity: {verified | not configured}."**
241
+
174
242
  ## Step 6: Enable Features
175
243
 
176
244
  Now that Twilio is configured, the user can enable the features that depend on it:
@@ -194,7 +262,9 @@ If the user wants to disconnect Twilio, send:
194
262
  }
195
263
  ```
196
264
 
197
- This removes the stored Account SID and Auth Token. Your phone number assignment will be preserved. Voice calls and SMS will stop working until credentials are reconfigured.
265
+ This removes the stored Account SID and Auth Token. Phone number assignments are preserved. Voice calls and SMS will stop working until credentials are reconfigured.
266
+
267
+ **Note:** `clear_credentials` is a global operation — it removes credentials for all assistants, not just the current one. The `assistantId` field is accepted but ignored. In multi-assistant setups, warn the user that clearing credentials will affect all assistants sharing this Twilio account.
198
268
 
199
269
  ## Troubleshooting
200
270
 
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Manages daemon-level authentication: session token lifecycle,
3
+ * per-socket auth state, and auth timeouts.
4
+ */
5
+ import * as net from 'node:net';
6
+ import { randomBytes } from 'node:crypto';
7
+ import { readFileSync, writeFileSync, chmodSync } from 'node:fs';
8
+ import { getSessionTokenPath } from '../util/platform.js';
9
+ import { hasNoAuthOverride } from './connection-policy.js';
10
+ import { getLogger } from '../util/logger.js';
11
+
12
+ const log = getLogger('auth-manager');
13
+
14
+ export const AUTH_TIMEOUT_MS = 5_000;
15
+
16
+ export class AuthManager {
17
+ private sessionToken = '';
18
+ private authenticatedSockets = new Set<net.Socket>();
19
+ private authTimeouts = new Map<net.Socket, ReturnType<typeof setTimeout>>();
20
+
21
+ /** Initialize the session token — reuse from disk or generate a new one. */
22
+ initToken(): void {
23
+ const tokenPath = getSessionTokenPath();
24
+ let existingToken: string | null = null;
25
+ try {
26
+ const raw = readFileSync(tokenPath, 'utf-8').trim();
27
+ if (raw.length >= 32) existingToken = raw;
28
+ } catch { /* file doesn't exist yet */ }
29
+
30
+ if (existingToken) {
31
+ this.sessionToken = existingToken;
32
+ log.info({ tokenPath }, 'Reusing existing session token');
33
+ } else {
34
+ this.sessionToken = randomBytes(32).toString('hex');
35
+ writeFileSync(tokenPath, this.sessionToken, { mode: 0o600 });
36
+ chmodSync(tokenPath, 0o600);
37
+ log.info({ tokenPath }, 'New session token generated');
38
+ }
39
+ }
40
+
41
+ isAuthenticated(socket: net.Socket): boolean {
42
+ return this.authenticatedSockets.has(socket);
43
+ }
44
+
45
+ /** Returns true if VELLUM_DAEMON_NOAUTH bypass is active. */
46
+ shouldAutoAuth(): boolean {
47
+ return hasNoAuthOverride();
48
+ }
49
+
50
+ markAuthenticated(socket: net.Socket): void {
51
+ this.authenticatedSockets.add(socket);
52
+ }
53
+
54
+ /** Validate a token and authenticate the socket. Returns true on success. */
55
+ authenticate(socket: net.Socket, token: string): boolean {
56
+ if (token === this.sessionToken) {
57
+ this.authenticatedSockets.add(socket);
58
+ return true;
59
+ }
60
+ log.warn('Client provided invalid auth token');
61
+ return false;
62
+ }
63
+
64
+ /** Start the auth timeout for a newly connected socket. */
65
+ startTimeout(socket: net.Socket, onTimeout: () => void): void {
66
+ const timer = setTimeout(() => {
67
+ if (!this.authenticatedSockets.has(socket)) {
68
+ log.warn('Client failed to authenticate within timeout, disconnecting');
69
+ onTimeout();
70
+ }
71
+ }, AUTH_TIMEOUT_MS);
72
+ this.authTimeouts.set(socket, timer);
73
+ }
74
+
75
+ /** Clear the auth timeout (called when the first message arrives). */
76
+ clearTimeout(socket: net.Socket): void {
77
+ const timer = this.authTimeouts.get(socket);
78
+ if (timer) {
79
+ clearTimeout(timer);
80
+ this.authTimeouts.delete(socket);
81
+ }
82
+ }
83
+
84
+ /** Remove all auth state for a disconnected socket. */
85
+ cleanupSocket(socket: net.Socket): void {
86
+ this.clearTimeout(socket);
87
+ this.authenticatedSockets.delete(socket);
88
+ }
89
+
90
+ /** Tear down all auth state on server stop. */
91
+ cleanupAll(): void {
92
+ for (const timer of this.authTimeouts.values()) {
93
+ clearTimeout(timer);
94
+ }
95
+ this.authTimeouts.clear();
96
+ this.authenticatedSockets.clear();
97
+ }
98
+
99
+ /** Iterate over authenticated sockets (for broadcasting). */
100
+ getAuthenticatedSockets(): Set<net.Socket> {
101
+ return this.authenticatedSockets;
102
+ }
103
+ }
@@ -897,7 +897,14 @@ export class ComputerUseSession {
897
897
  decision: UserDecision,
898
898
  selectedPattern?: string,
899
899
  selectedScope?: string,
900
+ decisionContext?: string,
900
901
  ): void {
901
- this.prompter?.resolveConfirmation(requestId, decision, selectedPattern, selectedScope);
902
+ this.prompter?.resolveConfirmation(
903
+ requestId,
904
+ decision,
905
+ selectedPattern,
906
+ selectedScope,
907
+ decisionContext,
908
+ );
902
909
  }
903
910
  }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * File watchers and config reload logic extracted from DaemonServer.
3
+ * Watches workspace files (config, prompts), protected directory
4
+ * (trust rules, secret allowlist), and skills directories for changes.
5
+ */
6
+ import { existsSync, readdirSync, watch, type FSWatcher } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { getRootDir, getWorkspaceDir, getWorkspaceSkillsDir } from '../util/platform.js';
9
+ import { getConfig, invalidateConfigCache } from '../config/loader.js';
10
+ import { initializeProviders } from '../providers/registry.js';
11
+ import { clearCache as clearTrustCache } from '../permissions/trust-store.js';
12
+ import { resetAllowlist, validateAllowlistFile } from '../security/secret-allowlist.js';
13
+ import { clearEmbeddingBackendCache } from '../memory/embedding-backend.js';
14
+ import { DebouncerMap } from '../util/debounce.js';
15
+ import { getLogger } from '../util/logger.js';
16
+
17
+ const log = getLogger('config-watcher');
18
+
19
+ export class ConfigWatcher {
20
+ private watchers: FSWatcher[] = [];
21
+ private debounceTimers = new DebouncerMap({
22
+ defaultDelayMs: 200,
23
+ maxEntries: 1000,
24
+ protectedKeyPrefix: '__',
25
+ });
26
+ private suppressReload = false;
27
+ private lastFingerprint = '';
28
+ private lastRefreshTime = 0;
29
+
30
+ static readonly REFRESH_INTERVAL_MS = 30_000;
31
+
32
+ /** Expose the debounce timers so handlers can schedule debounced work. */
33
+ get timers(): DebouncerMap {
34
+ return this.debounceTimers;
35
+ }
36
+
37
+ get suppressConfigReload(): boolean {
38
+ return this.suppressReload;
39
+ }
40
+
41
+ set suppressConfigReload(value: boolean) {
42
+ this.suppressReload = value;
43
+ }
44
+
45
+ get lastConfigRefreshTime(): number {
46
+ return this.lastRefreshTime;
47
+ }
48
+
49
+ set lastConfigRefreshTime(value: number) {
50
+ this.lastRefreshTime = value;
51
+ }
52
+
53
+ /** Compute a fingerprint of the current config for change detection. */
54
+ configFingerprint(config: ReturnType<typeof getConfig>): string {
55
+ return JSON.stringify(config);
56
+ }
57
+
58
+ /** Initialize the config fingerprint (call after first config load). */
59
+ initFingerprint(config: ReturnType<typeof getConfig>): void {
60
+ this.lastFingerprint = this.configFingerprint(config);
61
+ }
62
+
63
+ /** Update the fingerprint to match the current config. */
64
+ updateFingerprint(): void {
65
+ this.lastFingerprint = this.configFingerprint(getConfig());
66
+ this.lastRefreshTime = Date.now();
67
+ }
68
+
69
+ /**
70
+ * Reload config from disk + secure storage, and refresh providers only
71
+ * when effective config values (including API keys) have changed.
72
+ * Returns true if config actually changed.
73
+ */
74
+ refreshConfigFromSources(): boolean {
75
+ invalidateConfigCache();
76
+ const config = getConfig();
77
+ const fingerprint = this.configFingerprint(config);
78
+ if (fingerprint === this.lastFingerprint) {
79
+ return false;
80
+ }
81
+ clearTrustCache();
82
+ clearEmbeddingBackendCache();
83
+ const isFirstInit = this.lastFingerprint === '';
84
+ initializeProviders(config);
85
+ this.lastFingerprint = fingerprint;
86
+ return !isFirstInit;
87
+ }
88
+
89
+ /**
90
+ * Start all file watchers. `onSessionEvict` is called when watched
91
+ * files change and sessions need to be evicted for reload.
92
+ */
93
+ start(onSessionEvict: () => void): void {
94
+ const workspaceDir = getWorkspaceDir();
95
+ const protectedDir = join(getRootDir(), 'protected');
96
+
97
+ const workspaceHandlers: Record<string, () => void> = {
98
+ 'config.json': () => {
99
+ if (this.suppressReload) return;
100
+ try {
101
+ const changed = this.refreshConfigFromSources();
102
+ if (changed) onSessionEvict();
103
+ } catch (err) {
104
+ log.error({ err, configPath: join(workspaceDir, 'config.json') }, 'Failed to reload config after file change. Previous config remains active.');
105
+ }
106
+ },
107
+ 'SOUL.md': () => onSessionEvict(),
108
+ 'IDENTITY.md': () => onSessionEvict(),
109
+ 'USER.md': () => onSessionEvict(),
110
+ 'LOOKS.md': () => onSessionEvict(),
111
+ };
112
+
113
+ const protectedHandlers: Record<string, () => void> = {
114
+ 'trust.json': () => {
115
+ clearTrustCache();
116
+ },
117
+ 'secret-allowlist.json': () => {
118
+ resetAllowlist();
119
+ try {
120
+ const errors = validateAllowlistFile();
121
+ if (errors && errors.length > 0) {
122
+ for (const e of errors) {
123
+ log.warn({ index: e.index, pattern: e.pattern }, `Invalid regex in secret-allowlist.json: ${e.message}`);
124
+ }
125
+ }
126
+ } catch (err) {
127
+ log.warn({ err }, 'Failed to validate secret-allowlist.json');
128
+ }
129
+ },
130
+ };
131
+
132
+ const watchDir = (dir: string, handlers: Record<string, () => void>, label: string): void => {
133
+ try {
134
+ const watcher = watch(dir, (_eventType, filename) => {
135
+ if (!filename) return;
136
+ const file = String(filename);
137
+ if (!handlers[file]) return;
138
+ this.debounceTimers.schedule(`file:${file}`, () => {
139
+ log.info({ file }, 'File changed, reloading');
140
+ handlers[file]();
141
+ });
142
+ });
143
+ this.watchers.push(watcher);
144
+ log.info({ dir }, `Watching ${label}`);
145
+ } catch (err) {
146
+ log.warn({ err, dir }, `Failed to watch ${label}. Hot-reload will be unavailable.`);
147
+ }
148
+ };
149
+
150
+ watchDir(workspaceDir, workspaceHandlers, 'workspace directory for config/prompt changes');
151
+ if (existsSync(protectedDir)) {
152
+ watchDir(protectedDir, protectedHandlers, 'protected directory for trust/allowlist changes');
153
+ }
154
+
155
+ this.startSkillsWatchers(onSessionEvict);
156
+ }
157
+
158
+ stop(): void {
159
+ this.debounceTimers.cancelAll();
160
+ for (const watcher of this.watchers) {
161
+ watcher.close();
162
+ }
163
+ this.watchers = [];
164
+ }
165
+
166
+ private startSkillsWatchers(onSessionEvict: () => void): void {
167
+ const skillsDir = getWorkspaceSkillsDir();
168
+ if (!existsSync(skillsDir)) return;
169
+
170
+ const scheduleSkillsReload = (file: string): void => {
171
+ this.debounceTimers.schedule(`skills:${file}`, () => {
172
+ log.info({ file }, 'Skill file changed, reloading');
173
+ onSessionEvict();
174
+ });
175
+ };
176
+
177
+ try {
178
+ const recursiveWatcher = watch(skillsDir, { recursive: true }, (_eventType, filename) => {
179
+ scheduleSkillsReload(filename ? String(filename) : '(unknown)');
180
+ });
181
+ this.watchers.push(recursiveWatcher);
182
+ log.info({ dir: skillsDir }, 'Watching skills directory recursively');
183
+ return;
184
+ } catch (err) {
185
+ log.info({ err, dir: skillsDir }, 'Recursive skills watch unavailable; using per-directory watchers');
186
+ }
187
+
188
+ const childWatchers = new Map<string, FSWatcher>();
189
+
190
+ const watchDir = (dirPath: string, onChange: (filename: string) => void): FSWatcher | null => {
191
+ try {
192
+ const watcher = watch(dirPath, (_eventType, filename) => {
193
+ onChange(filename ? String(filename) : '(unknown)');
194
+ });
195
+ this.watchers.push(watcher);
196
+ return watcher;
197
+ } catch (err) {
198
+ log.warn({ err, dirPath }, 'Failed to watch skills directory');
199
+ return null;
200
+ }
201
+ };
202
+
203
+ const removeWatcher = (watcher: FSWatcher): void => {
204
+ const idx = this.watchers.indexOf(watcher);
205
+ if (idx !== -1) {
206
+ this.watchers.splice(idx, 1);
207
+ }
208
+ };
209
+
210
+ const refreshChildWatchers = (): void => {
211
+ const nextChildDirs = new Set<string>();
212
+
213
+ try {
214
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
215
+ for (const entry of entries) {
216
+ if (!entry.isDirectory()) continue;
217
+ const childDir = join(skillsDir, entry.name);
218
+ nextChildDirs.add(childDir);
219
+
220
+ if (childWatchers.has(childDir)) continue;
221
+
222
+ const watcher = watchDir(childDir, (filename) => {
223
+ const label = filename === '(unknown)' ? entry.name : `${entry.name}/${filename}`;
224
+ scheduleSkillsReload(label);
225
+ });
226
+ if (watcher) {
227
+ childWatchers.set(childDir, watcher);
228
+ }
229
+ }
230
+ } catch (err) {
231
+ log.warn({ err, skillsDir }, 'Failed to enumerate skill directories');
232
+ return;
233
+ }
234
+
235
+ for (const [childDir, watcher] of childWatchers.entries()) {
236
+ if (nextChildDirs.has(childDir)) continue;
237
+ watcher.close();
238
+ childWatchers.delete(childDir);
239
+ removeWatcher(watcher);
240
+ }
241
+ };
242
+
243
+ const rootWatcher = watchDir(skillsDir, (filename) => {
244
+ scheduleSkillsReload(filename);
245
+ refreshChildWatchers();
246
+ });
247
+
248
+ if (!rootWatcher) return;
249
+
250
+ refreshChildWatchers();
251
+ log.info({ dir: skillsDir }, 'Watching skills directory with non-recursive fallback');
252
+ }
253
+ }