@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.
Files changed (295) hide show
  1. package/ARCHITECTURE.md +142 -0
  2. package/Dockerfile +2 -2
  3. package/README.md +5 -5
  4. package/docs/architecture/http-token-refresh.md +252 -0
  5. package/docs/architecture/memory.md +5 -4
  6. package/docs/architecture/scheduling.md +4 -88
  7. package/docs/runbook-trusted-contacts.md +283 -0
  8. package/docs/trusted-contact-access.md +247 -0
  9. package/package.json +1 -1
  10. package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
  11. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
  12. package/src/__tests__/access-request-decision.test.ts +331 -0
  13. package/src/__tests__/asset-materialize-tool.test.ts +7 -7
  14. package/src/__tests__/asset-search-tool.test.ts +15 -15
  15. package/src/__tests__/attachments-store.test.ts +13 -13
  16. package/src/__tests__/call-controller.test.ts +150 -4
  17. package/src/__tests__/call-conversation-messages.test.ts +2 -2
  18. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  19. package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +108 -12
  21. package/src/__tests__/channel-guardian.test.ts +16 -14
  22. package/src/__tests__/checker.test.ts +24 -0
  23. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
  24. package/src/__tests__/config-watcher.test.ts +358 -0
  25. package/src/__tests__/conversation-pairing.test.ts +24 -24
  26. package/src/__tests__/conversation-store.test.ts +36 -36
  27. package/src/__tests__/date-context.test.ts +179 -1
  28. package/src/__tests__/db-migration-rollback.test.ts +4 -7
  29. package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
  30. package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
  31. package/src/__tests__/gateway-only-guard.test.ts +188 -0
  32. package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
  33. package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
  34. package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
  35. package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
  36. package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
  37. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
  38. package/src/__tests__/guardian-action-sweep.test.ts +9 -9
  39. package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
  40. package/src/__tests__/guardian-outbound-http.test.ts +202 -10
  41. package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
  42. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
  43. package/src/__tests__/handlers-telegram-config.test.ts +6 -6
  44. package/src/__tests__/hooks-runner.test.ts +13 -4
  45. package/src/__tests__/ingress-routes-http.test.ts +443 -0
  46. package/src/__tests__/intent-routing.test.ts +14 -0
  47. package/src/__tests__/ipc-snapshot.test.ts +2 -5
  48. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  49. package/src/__tests__/memory-regressions.test.ts +16 -12
  50. package/src/__tests__/non-member-access-request.test.ts +282 -0
  51. package/src/__tests__/notification-decision-strategy.test.ts +136 -0
  52. package/src/__tests__/notification-routing-intent.test.ts +11 -2
  53. package/src/__tests__/notification-thread-candidates.test.ts +166 -0
  54. package/src/__tests__/recording-intent-fallback.test.ts +0 -1
  55. package/src/__tests__/recording-intent-handler.test.ts +6 -3
  56. package/src/__tests__/recording-intent.test.ts +3 -2
  57. package/src/__tests__/recording-state-machine.test.ts +337 -26
  58. package/src/__tests__/registry.test.ts +17 -8
  59. package/src/__tests__/relay-server.test.ts +105 -0
  60. package/src/__tests__/reminder.test.ts +13 -0
  61. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +50 -0
  63. package/src/__tests__/server-history-render.test.ts +8 -8
  64. package/src/__tests__/session-agent-loop.test.ts +1 -0
  65. package/src/__tests__/session-runtime-assembly.test.ts +49 -0
  66. package/src/__tests__/session-skill-tools.test.ts +1 -0
  67. package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
  68. package/src/__tests__/slack-channel-config.test.ts +230 -0
  69. package/src/__tests__/subagent-manager-notify.test.ts +4 -4
  70. package/src/__tests__/swarm-session-integration.test.ts +2 -2
  71. package/src/__tests__/system-prompt.test.ts +43 -0
  72. package/src/__tests__/task-management-tools.test.ts +3 -3
  73. package/src/__tests__/task-tools.test.ts +3 -3
  74. package/src/__tests__/trust-store.test.ts +17 -1
  75. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
  76. package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
  77. package/src/__tests__/trusted-contact-verification.test.ts +360 -0
  78. package/src/__tests__/update-bulletin-format.test.ts +119 -0
  79. package/src/__tests__/update-bulletin-state.test.ts +129 -0
  80. package/src/__tests__/update-bulletin.test.ts +260 -0
  81. package/src/__tests__/update-template-contract.test.ts +29 -0
  82. package/src/agent/loop.ts +2 -2
  83. package/src/amazon/client.ts +2 -3
  84. package/src/calls/call-controller.ts +115 -34
  85. package/src/calls/call-conversation-messages.ts +2 -2
  86. package/src/calls/call-domain.ts +10 -3
  87. package/src/calls/call-pointer-messages.ts +17 -5
  88. package/src/calls/guardian-action-sweep.ts +77 -36
  89. package/src/calls/relay-server.ts +51 -12
  90. package/src/calls/twilio-routes.ts +3 -1
  91. package/src/calls/types.ts +1 -1
  92. package/src/calls/voice-session-bridge.ts +4 -4
  93. package/src/cli/core-commands.ts +3 -3
  94. package/src/cli/map.ts +8 -5
  95. package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
  96. package/src/config/bundled-skills/tasks/SKILL.md +1 -1
  97. package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
  98. package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
  99. package/src/config/computer-use-prompt.ts +1 -0
  100. package/src/config/core-schema.ts +16 -0
  101. package/src/config/env-registry.ts +1 -0
  102. package/src/config/env.ts +16 -1
  103. package/src/config/memory-schema.ts +5 -0
  104. package/src/config/schema.ts +4 -0
  105. package/src/config/system-prompt.ts +69 -2
  106. package/src/config/templates/BOOTSTRAP.md +1 -1
  107. package/src/config/templates/IDENTITY.md +8 -4
  108. package/src/config/templates/SOUL.md +14 -0
  109. package/src/config/templates/UPDATES.md +16 -0
  110. package/src/config/templates/USER.md +5 -1
  111. package/src/config/types.ts +1 -0
  112. package/src/config/update-bulletin-format.ts +52 -0
  113. package/src/config/update-bulletin-state.ts +49 -0
  114. package/src/config/update-bulletin.ts +82 -0
  115. package/src/config/vellum-skills/catalog.json +6 -0
  116. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
  117. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
  119. package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
  120. package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
  121. package/src/context/window-manager.ts +43 -3
  122. package/src/daemon/config-watcher.ts +1 -0
  123. package/src/daemon/connection-policy.ts +21 -1
  124. package/src/daemon/daemon-control.ts +164 -7
  125. package/src/daemon/date-context.ts +174 -1
  126. package/src/daemon/guardian-action-generators.ts +175 -0
  127. package/src/daemon/guardian-verification-intent.ts +120 -0
  128. package/src/daemon/handlers/apps.ts +1 -3
  129. package/src/daemon/handlers/config-channels.ts +8 -8
  130. package/src/daemon/handlers/config-heartbeat.ts +1 -1
  131. package/src/daemon/handlers/config-inbox.ts +55 -159
  132. package/src/daemon/handlers/config-ingress.ts +1 -1
  133. package/src/daemon/handlers/config-integrations.ts +1 -1
  134. package/src/daemon/handlers/config-platform.ts +1 -1
  135. package/src/daemon/handlers/config-scheduling.ts +2 -2
  136. package/src/daemon/handlers/config-slack-channel.ts +190 -0
  137. package/src/daemon/handlers/config-telegram.ts +1 -1
  138. package/src/daemon/handlers/config-twilio.ts +1 -1
  139. package/src/daemon/handlers/config-voice.ts +100 -0
  140. package/src/daemon/handlers/config.ts +3 -0
  141. package/src/daemon/handlers/index.ts +1 -1
  142. package/src/daemon/handlers/misc.ts +84 -6
  143. package/src/daemon/handlers/navigate-settings.ts +27 -0
  144. package/src/daemon/handlers/recording.ts +270 -144
  145. package/src/daemon/handlers/sessions.ts +107 -24
  146. package/src/daemon/handlers/subagents.ts +3 -3
  147. package/src/daemon/handlers/work-items.ts +10 -7
  148. package/src/daemon/ipc-contract/integrations.ts +9 -1
  149. package/src/daemon/ipc-contract/messages.ts +4 -0
  150. package/src/daemon/ipc-contract/sessions.ts +1 -1
  151. package/src/daemon/ipc-contract/settings.ts +26 -0
  152. package/src/daemon/ipc-contract/shared.ts +2 -0
  153. package/src/daemon/ipc-contract/work-items.ts +1 -7
  154. package/src/daemon/ipc-contract-inventory.json +5 -1
  155. package/src/daemon/ipc-contract.ts +5 -1
  156. package/src/daemon/lifecycle.ts +306 -266
  157. package/src/daemon/recording-executor.ts +1 -1
  158. package/src/daemon/recording-intent.ts +0 -41
  159. package/src/daemon/response-tier.ts +2 -2
  160. package/src/daemon/server.ts +6 -6
  161. package/src/daemon/session-agent-loop-handlers.ts +34 -9
  162. package/src/daemon/session-agent-loop.ts +15 -8
  163. package/src/daemon/session-history.ts +3 -2
  164. package/src/daemon/session-media-retry.ts +3 -0
  165. package/src/daemon/session-messaging.ts +38 -4
  166. package/src/daemon/session-notifiers.ts +2 -2
  167. package/src/daemon/session-process.ts +256 -23
  168. package/src/daemon/session-queue-manager.ts +2 -0
  169. package/src/daemon/session-runtime-assembly.ts +39 -0
  170. package/src/daemon/session-skill-tools.ts +13 -4
  171. package/src/daemon/session-tool-setup.ts +6 -7
  172. package/src/daemon/session.ts +19 -8
  173. package/src/daemon/tls-certs.ts +55 -13
  174. package/src/daemon/tool-side-effects.ts +13 -5
  175. package/src/gallery/default-gallery.ts +32 -9
  176. package/src/influencer/client.ts +2 -1
  177. package/src/memory/channel-delivery-store.ts +37 -567
  178. package/src/memory/channel-guardian-store.ts +66 -1317
  179. package/src/memory/conflict-store.ts +4 -4
  180. package/src/memory/conversation-attention-store.ts +4 -7
  181. package/src/memory/conversation-crud.ts +668 -0
  182. package/src/memory/conversation-queries.ts +361 -0
  183. package/src/memory/conversation-store.ts +45 -983
  184. package/src/memory/db-connection.ts +3 -0
  185. package/src/memory/db-init.ts +25 -0
  186. package/src/memory/delivery-channels.ts +175 -0
  187. package/src/memory/delivery-crud.ts +211 -0
  188. package/src/memory/delivery-status.ts +199 -0
  189. package/src/memory/embedding-backend.ts +70 -4
  190. package/src/memory/embedding-local.ts +12 -2
  191. package/src/memory/entity-extractor.ts +3 -8
  192. package/src/memory/fts-reconciler.ts +121 -0
  193. package/src/memory/guardian-action-store.ts +366 -3
  194. package/src/memory/guardian-approvals.ts +569 -0
  195. package/src/memory/guardian-bindings.ts +130 -0
  196. package/src/memory/guardian-rate-limits.ts +196 -0
  197. package/src/memory/guardian-verification.ts +520 -0
  198. package/src/memory/job-handlers/index-maintenance.ts +2 -1
  199. package/src/memory/job-utils.ts +8 -5
  200. package/src/memory/jobs-store.ts +66 -6
  201. package/src/memory/jobs-worker.ts +23 -1
  202. package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
  203. package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
  204. package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
  205. package/src/memory/migrations/100-core-tables.ts +1 -1
  206. package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
  207. package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
  208. package/src/memory/migrations/112-assistant-inbox.ts +1 -1
  209. package/src/memory/migrations/113-late-migrations.ts +1 -1
  210. package/src/memory/migrations/116-messages-fts.ts +13 -0
  211. package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
  212. package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
  213. package/src/memory/migrations/index.ts +8 -3
  214. package/src/memory/migrations/validate-migration-state.ts +114 -15
  215. package/src/memory/qdrant-circuit-breaker.ts +105 -0
  216. package/src/memory/retriever.ts +46 -13
  217. package/src/memory/schema-migration.ts +3 -0
  218. package/src/memory/schema.ts +25 -7
  219. package/src/memory/search/semantic.ts +8 -90
  220. package/src/notifications/README.md +1 -1
  221. package/src/notifications/broadcaster.ts +20 -2
  222. package/src/notifications/conversation-pairing.ts +3 -3
  223. package/src/notifications/decision-engine.ts +173 -8
  224. package/src/notifications/deliveries-store.ts +27 -8
  225. package/src/notifications/preferences-store.ts +7 -7
  226. package/src/notifications/thread-candidates.ts +234 -0
  227. package/src/notifications/types.ts +18 -0
  228. package/src/permissions/defaults.ts +11 -1
  229. package/src/permissions/prompter.ts +17 -0
  230. package/src/permissions/trust-store.ts +2 -0
  231. package/src/providers/failover.ts +19 -0
  232. package/src/providers/registry.ts +46 -1
  233. package/src/runtime/approval-message-composer.ts +1 -1
  234. package/src/runtime/channel-guardian-service.ts +15 -3
  235. package/src/runtime/channel-retry-sweep.ts +7 -2
  236. package/src/runtime/guardian-action-conversation-turn.ts +85 -0
  237. package/src/runtime/guardian-action-followup-executor.ts +301 -0
  238. package/src/runtime/guardian-action-message-composer.ts +245 -0
  239. package/src/runtime/guardian-outbound-actions.ts +35 -15
  240. package/src/runtime/guardian-verification-templates.ts +15 -9
  241. package/src/runtime/http-errors.ts +93 -0
  242. package/src/runtime/http-server.ts +140 -51
  243. package/src/runtime/http-types.ts +53 -0
  244. package/src/runtime/ingress-service.ts +237 -0
  245. package/src/runtime/middleware/error-handler.ts +4 -3
  246. package/src/runtime/middleware/rate-limiter.ts +160 -0
  247. package/src/runtime/middleware/request-logger.ts +71 -0
  248. package/src/runtime/middleware/twilio-validation.ts +7 -6
  249. package/src/runtime/pending-interactions.ts +12 -0
  250. package/src/runtime/routes/access-request-decision.ts +215 -0
  251. package/src/runtime/routes/app-routes.ts +25 -18
  252. package/src/runtime/routes/approval-routes.ts +18 -47
  253. package/src/runtime/routes/attachment-routes.ts +15 -41
  254. package/src/runtime/routes/call-routes.ts +20 -20
  255. package/src/runtime/routes/channel-delivery-routes.ts +6 -5
  256. package/src/runtime/routes/contact-routes.ts +4 -9
  257. package/src/runtime/routes/conversation-attention-routes.ts +5 -4
  258. package/src/runtime/routes/conversation-routes.ts +26 -57
  259. package/src/runtime/routes/debug-routes.ts +71 -0
  260. package/src/runtime/routes/events-routes.ts +3 -2
  261. package/src/runtime/routes/guardian-approval-interception.ts +221 -0
  262. package/src/runtime/routes/identity-routes.ts +14 -10
  263. package/src/runtime/routes/inbound-conversation.ts +3 -2
  264. package/src/runtime/routes/inbound-message-handler.ts +527 -62
  265. package/src/runtime/routes/ingress-routes.ts +174 -0
  266. package/src/runtime/routes/integration-routes.ts +82 -20
  267. package/src/runtime/routes/pairing-routes.ts +11 -10
  268. package/src/runtime/routes/secret-routes.ts +10 -18
  269. package/src/runtime/verification-rate-limiter.ts +83 -0
  270. package/src/schedule/schedule-store.ts +13 -1
  271. package/src/schedule/scheduler.ts +2 -2
  272. package/src/security/secret-ingress.ts +5 -2
  273. package/src/security/secret-scanner.ts +72 -6
  274. package/src/subagent/manager.ts +6 -4
  275. package/src/swarm/plan-validator.ts +4 -1
  276. package/src/tasks/task-runner.ts +3 -1
  277. package/src/tools/browser/api-map.ts +9 -6
  278. package/src/tools/calls/call-start.ts +20 -0
  279. package/src/tools/executor.ts +50 -568
  280. package/src/tools/permission-checker.ts +272 -0
  281. package/src/tools/registry.ts +14 -6
  282. package/src/tools/reminder/reminder-store.ts +7 -7
  283. package/src/tools/reminder/reminder.ts +6 -3
  284. package/src/tools/secret-detection-handler.ts +301 -0
  285. package/src/tools/subagent/message.ts +1 -1
  286. package/src/tools/system/voice-config.ts +62 -0
  287. package/src/tools/tasks/index.ts +3 -3
  288. package/src/tools/tasks/work-item-list.ts +3 -3
  289. package/src/tools/tasks/work-item-update.ts +4 -5
  290. package/src/tools/tool-approval-handler.ts +192 -0
  291. package/src/tools/tool-manifest.ts +2 -0
  292. package/src/watcher/watcher-store.ts +9 -9
  293. package/src/work-items/work-item-runner.ts +9 -6
  294. /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts β†’ 026a-embeddings-nullable-vector-json.ts} +0 -0
  295. /package/src/memory/migrations/{027-guardian-bootstrap-token.ts β†’ 027a-guardian-bootstrap-token.ts} +0 -0
@@ -12,21 +12,17 @@ import {
12
12
  } from '../../memory/channel-guardian-store.js';
13
13
  import { addMessage, getMessages } from '../../memory/conversation-store.js';
14
14
  import { getBindingByConversation } from '../../memory/external-conversation-store.js';
15
- import {
16
- createInvite,
17
- type InviteStatus,
18
- listInvites,
19
- redeemInvite,
20
- revokeInvite,
21
- } from '../../memory/ingress-invite-store.js';
22
- import {
23
- blockMember,
24
- type IngressMember,
25
- listMembers,
26
- revokeMember,
27
- upsertMember,
28
- } from '../../memory/ingress-member-store.js';
29
15
  import { deliverChannelReply } from '../../runtime/gateway-client.js';
16
+ import {
17
+ blockIngressMember,
18
+ createIngressInvite,
19
+ listIngressInvites,
20
+ listIngressMembers,
21
+ redeemIngressInvite,
22
+ revokeIngressInvite,
23
+ revokeIngressMember,
24
+ upsertIngressMember,
25
+ } from '../../runtime/ingress-service.js';
30
26
  import type { AssistantInboxEscalationRequest, IngressInviteRequest, IngressMemberRequest } from '../ipc-protocol.js';
31
27
  import { defineHandlers, type HandlerContext, log } from './shared.js';
32
28
  import { renderHistoryContent } from './shared.js';
@@ -39,121 +35,60 @@ export function handleIngressInvite(
39
35
  try {
40
36
  switch (msg.action) {
41
37
  case 'create': {
42
- if (!msg.sourceChannel) {
43
- ctx.send(socket, { type: 'ingress_invite_response', success: false, error: 'sourceChannel is required for create' });
44
- return;
45
- }
46
- const { invite, rawToken } = createInvite({
38
+ const result = createIngressInvite({
47
39
  sourceChannel: msg.sourceChannel,
48
40
  note: msg.note,
49
41
  maxUses: msg.maxUses,
50
42
  expiresInMs: msg.expiresInMs,
51
43
  });
52
- ctx.send(socket, {
53
- type: 'ingress_invite_response',
54
- success: true,
55
- invite: {
56
- id: invite.id,
57
- sourceChannel: invite.sourceChannel,
58
- token: rawToken,
59
- tokenHash: invite.tokenHash,
60
- maxUses: invite.maxUses,
61
- useCount: invite.useCount,
62
- expiresAt: invite.expiresAt,
63
- status: invite.status,
64
- note: invite.note ?? undefined,
65
- createdAt: invite.createdAt,
66
- },
67
- });
44
+ if (!result.ok) {
45
+ ctx.send(socket, { type: 'ingress_invite_response', success: false, error: result.error });
46
+ return;
47
+ }
48
+ ctx.send(socket, { type: 'ingress_invite_response', success: true, invite: result.data });
68
49
  return;
69
50
  }
70
51
 
71
52
  case 'list': {
72
- const invites = listInvites({
53
+ const result = listIngressInvites({
73
54
  sourceChannel: msg.sourceChannel,
74
- status: msg.status as InviteStatus | undefined,
75
- });
76
- ctx.send(socket, {
77
- type: 'ingress_invite_response',
78
- success: true,
79
- invites: invites.map((inv) => ({
80
- id: inv.id,
81
- sourceChannel: inv.sourceChannel,
82
- tokenHash: inv.tokenHash,
83
- maxUses: inv.maxUses,
84
- useCount: inv.useCount,
85
- expiresAt: inv.expiresAt,
86
- status: inv.status,
87
- note: inv.note ?? undefined,
88
- createdAt: inv.createdAt,
89
- })),
55
+ status: msg.status,
90
56
  });
57
+ if (!result.ok) {
58
+ ctx.send(socket, { type: 'ingress_invite_response', success: false, error: result.error });
59
+ return;
60
+ }
61
+ ctx.send(socket, { type: 'ingress_invite_response', success: true, invites: result.data });
91
62
  return;
92
63
  }
93
64
 
94
65
  case 'revoke': {
95
- if (!msg.inviteId) {
96
- ctx.send(socket, { type: 'ingress_invite_response', success: false, error: 'inviteId is required for revoke' });
97
- return;
98
- }
99
- const revoked = revokeInvite(msg.inviteId);
100
- if (!revoked) {
101
- ctx.send(socket, { type: 'ingress_invite_response', success: false, error: 'Invite not found or already revoked' });
66
+ const result = revokeIngressInvite(msg.inviteId);
67
+ if (!result.ok) {
68
+ ctx.send(socket, { type: 'ingress_invite_response', success: false, error: result.error });
102
69
  return;
103
70
  }
104
- ctx.send(socket, {
105
- type: 'ingress_invite_response',
106
- success: true,
107
- invite: {
108
- id: revoked.id,
109
- sourceChannel: revoked.sourceChannel,
110
- tokenHash: revoked.tokenHash,
111
- maxUses: revoked.maxUses,
112
- useCount: revoked.useCount,
113
- expiresAt: revoked.expiresAt,
114
- status: revoked.status,
115
- note: revoked.note ?? undefined,
116
- createdAt: revoked.createdAt,
117
- },
118
- });
71
+ ctx.send(socket, { type: 'ingress_invite_response', success: true, invite: result.data });
119
72
  return;
120
73
  }
121
74
 
122
75
  case 'redeem': {
123
- if (!msg.token) {
124
- ctx.send(socket, { type: 'ingress_invite_response', success: false, error: 'token is required for redeem' });
125
- return;
126
- }
127
- const result = redeemInvite({
128
- rawToken: msg.token,
76
+ const result = redeemIngressInvite({
77
+ token: msg.token,
129
78
  externalUserId: msg.externalUserId,
130
79
  externalChatId: msg.externalChatId,
131
80
  sourceChannel: msg.sourceChannel,
132
81
  });
133
- if ('error' in result) {
82
+ if (!result.ok) {
134
83
  ctx.send(socket, { type: 'ingress_invite_response', success: false, error: result.error });
135
84
  return;
136
85
  }
137
- ctx.send(socket, {
138
- type: 'ingress_invite_response',
139
- success: true,
140
- invite: {
141
- id: result.invite.id,
142
- sourceChannel: result.invite.sourceChannel,
143
- tokenHash: result.invite.tokenHash,
144
- maxUses: result.invite.maxUses,
145
- useCount: result.invite.useCount,
146
- expiresAt: result.invite.expiresAt,
147
- status: result.invite.status,
148
- note: result.invite.note ?? undefined,
149
- createdAt: result.invite.createdAt,
150
- },
151
- });
86
+ ctx.send(socket, { type: 'ingress_invite_response', success: true, invite: result.data });
152
87
  return;
153
88
  }
154
89
 
155
90
  default: {
156
- ctx.send(socket, { type: 'ingress_invite_response', success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
91
+ ctx.send(socket, { type: 'ingress_invite_response', success: false, error: `Unknown action: ${String(msg.action)}` });
157
92
  }
158
93
  }
159
94
  } catch (err) {
@@ -163,21 +98,6 @@ export function handleIngressInvite(
163
98
  }
164
99
  }
165
100
 
166
- function memberToResponse(m: IngressMember) {
167
- return {
168
- id: m.id,
169
- sourceChannel: m.sourceChannel,
170
- externalUserId: m.externalUserId ?? undefined,
171
- externalChatId: m.externalChatId ?? undefined,
172
- displayName: m.displayName ?? undefined,
173
- username: m.username ?? undefined,
174
- status: m.status,
175
- policy: m.policy,
176
- lastSeenAt: m.lastSeenAt ?? undefined,
177
- createdAt: m.createdAt,
178
- };
179
- }
180
-
181
101
  export function handleIngressMember(
182
102
  msg: IngressMemberRequest,
183
103
  socket: net.Socket,
@@ -186,30 +106,22 @@ export function handleIngressMember(
186
106
  try {
187
107
  switch (msg.action) {
188
108
  case 'list': {
189
- const members = listMembers({
109
+ const result = listIngressMembers({
190
110
  assistantId: msg.assistantId,
191
111
  sourceChannel: msg.sourceChannel,
192
112
  status: msg.status,
193
113
  policy: msg.policy,
194
114
  });
195
- ctx.send(socket, {
196
- type: 'ingress_member_response',
197
- success: true,
198
- members: members.map(memberToResponse),
199
- });
115
+ if (!result.ok) {
116
+ ctx.send(socket, { type: 'ingress_member_response', success: false, error: result.error });
117
+ return;
118
+ }
119
+ ctx.send(socket, { type: 'ingress_member_response', success: true, members: result.data });
200
120
  return;
201
121
  }
202
122
 
203
123
  case 'upsert': {
204
- if (!msg.sourceChannel) {
205
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: 'sourceChannel is required for upsert' });
206
- return;
207
- }
208
- if (!msg.externalUserId && !msg.externalChatId) {
209
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: 'At least one of externalUserId or externalChatId is required for upsert' });
210
- return;
211
- }
212
- const member = upsertMember({
124
+ const result = upsertIngressMember({
213
125
  assistantId: msg.assistantId,
214
126
  sourceChannel: msg.sourceChannel,
215
127
  externalUserId: msg.externalUserId,
@@ -219,52 +131,36 @@ export function handleIngressMember(
219
131
  policy: msg.policy,
220
132
  status: msg.status,
221
133
  });
222
- ctx.send(socket, {
223
- type: 'ingress_member_response',
224
- success: true,
225
- member: memberToResponse(member),
226
- });
134
+ if (!result.ok) {
135
+ ctx.send(socket, { type: 'ingress_member_response', success: false, error: result.error });
136
+ return;
137
+ }
138
+ ctx.send(socket, { type: 'ingress_member_response', success: true, member: result.data });
227
139
  return;
228
140
  }
229
141
 
230
142
  case 'revoke': {
231
- if (!msg.memberId) {
232
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: 'memberId is required for revoke' });
143
+ const result = revokeIngressMember(msg.memberId, msg.reason);
144
+ if (!result.ok) {
145
+ ctx.send(socket, { type: 'ingress_member_response', success: false, error: result.error });
233
146
  return;
234
147
  }
235
- const revoked = revokeMember(msg.memberId, msg.reason);
236
- if (!revoked) {
237
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: 'Member not found or cannot be revoked' });
238
- return;
239
- }
240
- ctx.send(socket, {
241
- type: 'ingress_member_response',
242
- success: true,
243
- member: memberToResponse(revoked),
244
- });
148
+ ctx.send(socket, { type: 'ingress_member_response', success: true, member: result.data });
245
149
  return;
246
150
  }
247
151
 
248
152
  case 'block': {
249
- if (!msg.memberId) {
250
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: 'memberId is required for block' });
251
- return;
252
- }
253
- const blocked = blockMember(msg.memberId, msg.reason);
254
- if (!blocked) {
255
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: 'Member not found or already blocked' });
153
+ const result = blockIngressMember(msg.memberId, msg.reason);
154
+ if (!result.ok) {
155
+ ctx.send(socket, { type: 'ingress_member_response', success: false, error: result.error });
256
156
  return;
257
157
  }
258
- ctx.send(socket, {
259
- type: 'ingress_member_response',
260
- success: true,
261
- member: memberToResponse(blocked),
262
- });
158
+ ctx.send(socket, { type: 'ingress_member_response', success: true, member: result.data });
263
159
  return;
264
160
  }
265
161
 
266
162
  default: {
267
- ctx.send(socket, { type: 'ingress_member_response', success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
163
+ ctx.send(socket, { type: 'ingress_member_response', success: false, error: `Unknown action: ${String(msg.action)}` });
268
164
  }
269
165
  }
270
166
  } catch (err) {
@@ -342,7 +238,7 @@ export function handleInboxEscalation(
342
238
  }
343
239
 
344
240
  default: {
345
- ctx.send(socket, { type: 'assistant_inbox_escalation_response', success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
241
+ ctx.send(socket, { type: 'assistant_inbox_escalation_response', success: false, error: `Unknown action: ${String(msg.action)}` });
346
242
  }
347
243
  }
348
244
  } catch (err) {
@@ -482,7 +378,7 @@ async function executeDeny(
482
378
 
483
379
  // Store a system note about the denial in the conversation
484
380
  const denialInterface = isInterfaceId(sourceChannel) ? sourceChannel : undefined;
485
- addMessage(conversationId, 'assistant', denialText, {
381
+ await addMessage(conversationId, 'assistant', denialText, {
486
382
  provenanceActorRole: 'guardian' as const,
487
383
  userMessageChannel: sourceChannel,
488
384
  assistantMessageChannel: sourceChannel,
@@ -219,7 +219,7 @@ export async function handleIngressConfig(
219
219
  }
220
220
  }
221
221
  } else {
222
- ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}` });
222
+ ctx.send(socket, { type: 'ingress_config_response', enabled: false, publicBaseUrl: '', localGatewayTarget, success: false, error: `Unknown action: ${String(msg.action)}` });
223
223
  }
224
224
  } catch (err) {
225
225
  const message = err instanceof Error ? err.message : String(err);
@@ -258,7 +258,7 @@ export function handleTwitterIntegrationConfig(
258
258
  managedAvailable: false,
259
259
  localClientConfigured: false,
260
260
  connected: false,
261
- error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
261
+ error: `Unknown action: ${String(msg.action)}`,
262
262
  });
263
263
  }
264
264
  } catch (err) {
@@ -38,7 +38,7 @@ export async function handlePlatformConfig(
38
38
  type: 'platform_config_response',
39
39
  baseUrl: '',
40
40
  success: false,
41
- error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
41
+ error: `Unknown action: ${String(msg.action)}`,
42
42
  });
43
43
  }
44
44
  } catch (err) {
@@ -84,10 +84,10 @@ export async function handleScheduleRunNow(
84
84
  log.info({ jobId: schedule.id, name: schedule.name, taskId }, 'Executing scheduled task manually (run now)');
85
85
  const { runTask } = await import('../../tasks/task-runner.js');
86
86
  const result = await runTask(
87
- { taskId, workingDir: process.cwd() },
87
+ { taskId, workingDir: process.cwd(), source: 'schedule' },
88
88
  async (conversationId, message, taskRunId) => {
89
89
  const session = await ctx.getOrCreateSession(conversationId, socket, true);
90
- (session as unknown as { taskRunId?: string }).taskRunId = taskRunId;
90
+ session.taskRunId = taskRunId;
91
91
  await session.processMessage(message, [], (event) => {
92
92
  ctx.send(socket, event);
93
93
  }, undefined, undefined, undefined, { isInteractive: false });
@@ -0,0 +1,190 @@
1
+ import { deleteSecureKey, getSecureKey, setSecureKey } from '../../security/secure-keys.js';
2
+ import { deleteCredentialMetadata, getCredentialMetadata, upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
3
+ import { log } from './shared.js';
4
+
5
+ // -- Result type --
6
+
7
+ export interface SlackChannelConfigResult {
8
+ success: boolean;
9
+ hasBotToken: boolean;
10
+ hasAppToken: boolean;
11
+ connected: boolean;
12
+ teamId?: string;
13
+ teamName?: string;
14
+ botUserId?: string;
15
+ botUsername?: string;
16
+ error?: string;
17
+ warning?: string;
18
+ }
19
+
20
+ // -- Metadata stored as JSON in accountInfo --
21
+
22
+ interface SlackChannelMetadata {
23
+ teamId?: string;
24
+ teamName?: string;
25
+ botUserId?: string;
26
+ botUsername?: string;
27
+ }
28
+
29
+ function parseMetadata(raw: string | undefined): SlackChannelMetadata {
30
+ if (!raw) return {};
31
+ try {
32
+ return JSON.parse(raw) as SlackChannelMetadata;
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ // -- Business logic --
39
+
40
+ export function getSlackChannelConfig(): SlackChannelConfigResult {
41
+ const hasBotToken = !!getSecureKey('credential:slack_channel:bot_token');
42
+ const hasAppToken = !!getSecureKey('credential:slack_channel:app_token');
43
+ const meta = getCredentialMetadata('slack_channel', 'bot_token');
44
+ const metadata = parseMetadata(meta?.accountInfo);
45
+ return {
46
+ success: true,
47
+ hasBotToken,
48
+ hasAppToken,
49
+ connected: hasBotToken && hasAppToken,
50
+ ...metadata,
51
+ };
52
+ }
53
+
54
+ export async function setSlackChannelConfig(
55
+ botToken?: string,
56
+ appToken?: string,
57
+ ): Promise<SlackChannelConfigResult> {
58
+ let metadata: SlackChannelMetadata = {};
59
+ let warning: string | undefined;
60
+
61
+ // Validate and store bot token
62
+ if (botToken) {
63
+ // Validate bot token by calling Slack auth.test
64
+ try {
65
+ const res = await fetch('https://slack.com/api/auth.test', {
66
+ method: 'POST',
67
+ headers: { Authorization: `Bearer ${botToken}` },
68
+ });
69
+ const data = (await res.json()) as {
70
+ ok: boolean;
71
+ error?: string;
72
+ team_id?: string;
73
+ team?: string;
74
+ user_id?: string;
75
+ user?: string;
76
+ };
77
+ if (!data.ok) {
78
+ const storedBotToken = !!getSecureKey('credential:slack_channel:bot_token');
79
+ const storedAppToken = !!getSecureKey('credential:slack_channel:app_token');
80
+ return {
81
+ success: false,
82
+ hasBotToken: storedBotToken,
83
+ hasAppToken: storedAppToken,
84
+ connected: storedBotToken && storedAppToken,
85
+ error: `Slack API validation failed: ${data.error ?? 'unknown error'}`,
86
+ };
87
+ }
88
+ metadata = {
89
+ teamId: data.team_id,
90
+ teamName: data.team,
91
+ botUserId: data.user_id,
92
+ botUsername: data.user,
93
+ };
94
+ } catch (err) {
95
+ const message = err instanceof Error ? err.message : String(err);
96
+ const storedBotToken = !!getSecureKey('credential:slack_channel:bot_token');
97
+ const storedAppToken = !!getSecureKey('credential:slack_channel:app_token');
98
+ return {
99
+ success: false,
100
+ hasBotToken: storedBotToken,
101
+ hasAppToken: storedAppToken,
102
+ connected: storedBotToken && storedAppToken,
103
+ error: `Failed to validate bot token: ${message}`,
104
+ };
105
+ }
106
+
107
+ const stored = setSecureKey('credential:slack_channel:bot_token', botToken);
108
+ if (!stored) {
109
+ const storedBotToken = !!getSecureKey('credential:slack_channel:bot_token');
110
+ const storedAppToken = !!getSecureKey('credential:slack_channel:app_token');
111
+ return {
112
+ success: false,
113
+ hasBotToken: storedBotToken,
114
+ hasAppToken: storedAppToken,
115
+ connected: storedBotToken && storedAppToken,
116
+ error: 'Failed to store bot token in secure storage',
117
+ };
118
+ }
119
+
120
+ upsertCredentialMetadata('slack_channel', 'bot_token', {
121
+ accountInfo: JSON.stringify(metadata),
122
+ });
123
+ } else {
124
+ // Use existing metadata if no new bot token provided
125
+ const existingMeta = getCredentialMetadata('slack_channel', 'bot_token');
126
+ metadata = parseMetadata(existingMeta?.accountInfo);
127
+ }
128
+
129
+ // Validate and store app token
130
+ if (appToken) {
131
+ if (!appToken.startsWith('xapp-')) {
132
+ const storedBotToken = !!getSecureKey('credential:slack_channel:bot_token');
133
+ const storedAppToken = !!getSecureKey('credential:slack_channel:app_token');
134
+ return {
135
+ success: false,
136
+ hasBotToken: storedBotToken,
137
+ hasAppToken: storedAppToken,
138
+ connected: storedBotToken && storedAppToken,
139
+ error: 'Invalid app token: must start with "xapp-"',
140
+ };
141
+ }
142
+
143
+ const stored = setSecureKey('credential:slack_channel:app_token', appToken);
144
+ if (!stored) {
145
+ const storedBotToken = !!getSecureKey('credential:slack_channel:bot_token');
146
+ const storedAppToken = !!getSecureKey('credential:slack_channel:app_token');
147
+ return {
148
+ success: false,
149
+ hasBotToken: storedBotToken,
150
+ hasAppToken: storedAppToken,
151
+ connected: storedBotToken && storedAppToken,
152
+ error: 'Failed to store app token in secure storage',
153
+ };
154
+ }
155
+
156
+ upsertCredentialMetadata('slack_channel', 'app_token', {});
157
+ }
158
+
159
+ const hasBotToken = !!getSecureKey('credential:slack_channel:bot_token');
160
+ const hasAppToken = !!getSecureKey('credential:slack_channel:app_token');
161
+
162
+ if (hasBotToken && !hasAppToken) {
163
+ warning = 'Bot token stored but app token is missing β€” connection incomplete.';
164
+ } else if (!hasBotToken && hasAppToken) {
165
+ warning = 'App token stored but bot token is missing β€” connection incomplete.';
166
+ }
167
+
168
+ return {
169
+ success: true,
170
+ hasBotToken,
171
+ hasAppToken,
172
+ connected: hasBotToken && hasAppToken,
173
+ ...metadata,
174
+ ...(warning ? { warning } : {}),
175
+ };
176
+ }
177
+
178
+ export function clearSlackChannelConfig(): SlackChannelConfigResult {
179
+ deleteSecureKey('credential:slack_channel:bot_token');
180
+ deleteCredentialMetadata('slack_channel', 'bot_token');
181
+ deleteSecureKey('credential:slack_channel:app_token');
182
+ deleteCredentialMetadata('slack_channel', 'app_token');
183
+
184
+ return {
185
+ success: true,
186
+ hasBotToken: false,
187
+ hasAppToken: false,
188
+ connected: false,
189
+ };
190
+ }
@@ -319,7 +319,7 @@ export async function handleTelegramConfig(
319
319
  hasBotToken: false,
320
320
  connected: false,
321
321
  hasWebhookSecret: false,
322
- error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
322
+ error: `Unknown action: ${String(msg.action)}`,
323
323
  };
324
324
  }
325
325
 
@@ -1062,7 +1062,7 @@ export async function handleTwilioConfig(
1062
1062
  type: 'twilio_config_response',
1063
1063
  success: false,
1064
1064
  hasCredentials: hasTwilioCredentials(),
1065
- error: `Unknown action: ${String((msg as unknown as Record<string, unknown>).action)}`,
1065
+ error: `Unknown action: ${String(msg.action)}`,
1066
1066
  });
1067
1067
  }
1068
1068
  } catch (err) {
@@ -0,0 +1,100 @@
1
+ import * as net from 'node:net';
2
+
3
+ import type { VoiceConfigUpdateRequest } from '../ipc-contract/settings.js';
4
+ import { defineHandlers, type HandlerContext, log } from './shared.js';
5
+
6
+ /**
7
+ * Send a client_settings_update message to all connected clients.
8
+ * Used to push configuration changes (e.g. activation key) from the daemon
9
+ * to macOS/iOS clients so they can apply settings immediately.
10
+ */
11
+ export function broadcastClientSettingsUpdate(
12
+ key: string,
13
+ value: string,
14
+ ctx: HandlerContext,
15
+ ): void {
16
+ ctx.broadcast({
17
+ type: 'client_settings_update',
18
+ key,
19
+ value,
20
+ });
21
+ log.info({ key, value }, 'Broadcast client_settings_update');
22
+ }
23
+
24
+ // ── Activation key validation ────────────────────────────────────────
25
+
26
+ const VALID_ACTIVATION_KEYS = ['fn', 'ctrl', 'fn_shift', 'none'] as const;
27
+ export type ActivationKey = (typeof VALID_ACTIVATION_KEYS)[number];
28
+
29
+ /**
30
+ * Map natural-language activation key names to canonical enum values.
31
+ * Case-insensitive matching is applied by the caller.
32
+ */
33
+ const NATURAL_LANGUAGE_MAP: Record<string, ActivationKey> = {
34
+ fn: 'fn',
35
+ globe: 'fn',
36
+ 'fn key': 'fn',
37
+ 'globe key': 'fn',
38
+ ctrl: 'ctrl',
39
+ control: 'ctrl',
40
+ 'ctrl key': 'ctrl',
41
+ 'control key': 'ctrl',
42
+ fn_shift: 'fn_shift',
43
+ 'fn+shift': 'fn_shift',
44
+ 'fn shift': 'fn_shift',
45
+ 'shift+fn': 'fn_shift',
46
+ none: 'none',
47
+ off: 'none',
48
+ disabled: 'none',
49
+ disable: 'none',
50
+ };
51
+
52
+ /**
53
+ * Validate and normalise a user-provided activation key string.
54
+ * Accepts both canonical enum values and natural-language variants.
55
+ * Returns the canonical value on success, or an error message on failure.
56
+ */
57
+ export function normalizeActivationKey(
58
+ input: string,
59
+ ): { ok: true; value: ActivationKey } | { ok: false; reason: string } {
60
+ const trimmed = input.trim().toLowerCase();
61
+
62
+ // Direct enum match
63
+ if ((VALID_ACTIVATION_KEYS as readonly string[]).includes(trimmed)) {
64
+ return { ok: true, value: trimmed as ActivationKey };
65
+ }
66
+
67
+ // Natural-language match
68
+ const mapped = NATURAL_LANGUAGE_MAP[trimmed];
69
+ if (mapped) {
70
+ return { ok: true, value: mapped };
71
+ }
72
+
73
+ return {
74
+ ok: false,
75
+ reason: `Invalid activation key "${input}". Valid values: fn (Fn/Globe key), ctrl (Control key), fn_shift (Fn+Shift), none (disable PTT).`,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Process a voice configuration update request from a session or IPC client.
81
+ * Validates the activation key and broadcasts the change to all connected clients.
82
+ */
83
+ export function handleVoiceConfigUpdate(
84
+ msg: VoiceConfigUpdateRequest,
85
+ _socket: net.Socket,
86
+ ctx: HandlerContext,
87
+ ): void {
88
+ const result = normalizeActivationKey(msg.activationKey);
89
+ if (!result.ok) {
90
+ log.warn({ input: msg.activationKey }, result.reason);
91
+ return;
92
+ }
93
+
94
+ broadcastClientSettingsUpdate('activationKey', result.value, ctx);
95
+ log.info({ activationKey: result.value }, 'Voice config updated: activation key');
96
+ }
97
+
98
+ export const voiceHandlers = defineHandlers({
99
+ voice_config_update: handleVoiceConfigUpdate,
100
+ });