@vellumai/assistant 0.3.28 → 0.4.0

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 (199) hide show
  1. package/ARCHITECTURE.md +33 -3
  2. package/bun.lock +4 -1
  3. package/docs/trusted-contact-access.md +9 -2
  4. package/package.json +6 -3
  5. package/scripts/ipc/generate-swift.ts +3 -3
  6. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  7. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  8. package/src/__tests__/approval-routes-http.test.ts +13 -5
  9. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  10. package/src/__tests__/asset-search-tool.test.ts +2 -0
  11. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  12. package/src/__tests__/attachments-store.test.ts +2 -0
  13. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  14. package/src/__tests__/call-controller.test.ts +30 -29
  15. package/src/__tests__/call-routes-http.test.ts +34 -32
  16. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  17. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  18. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  19. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  20. package/src/__tests__/clarification-resolver.test.ts +2 -0
  21. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  22. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  24. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  25. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  26. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  27. package/src/__tests__/config-schema.test.ts +5 -5
  28. package/src/__tests__/config-watcher.test.ts +3 -1
  29. package/src/__tests__/connection-policy.test.ts +14 -5
  30. package/src/__tests__/contacts-tools.test.ts +3 -1
  31. package/src/__tests__/contradiction-checker.test.ts +2 -0
  32. package/src/__tests__/conversation-pairing.test.ts +10 -0
  33. package/src/__tests__/conversation-routes.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  35. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  36. package/src/__tests__/credential-vault.test.ts +5 -4
  37. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  38. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  39. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  40. package/src/__tests__/encrypted-store.test.ts +10 -5
  41. package/src/__tests__/followup-tools.test.ts +3 -1
  42. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  43. package/src/__tests__/gmail-integration.test.ts +0 -1
  44. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  45. package/src/__tests__/guardian-dispatch.test.ts +2 -0
  46. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  47. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  48. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  49. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  50. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  51. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  52. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  53. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  55. package/src/__tests__/heartbeat-service.test.ts +20 -0
  56. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  57. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  58. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  59. package/src/__tests__/intent-routing.test.ts +2 -0
  60. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  61. package/src/__tests__/media-generate-image.test.ts +21 -0
  62. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  63. package/src/__tests__/memory-regressions.test.ts +20 -20
  64. package/src/__tests__/non-member-access-request.test.ts +183 -9
  65. package/src/__tests__/notification-decision-fallback.test.ts +2 -0
  66. package/src/__tests__/notification-decision-strategy.test.ts +61 -0
  67. package/src/__tests__/notification-guardian-path.test.ts +2 -0
  68. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  69. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  70. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  71. package/src/__tests__/pairing-routes.test.ts +171 -0
  72. package/src/__tests__/playbook-execution.test.ts +3 -1
  73. package/src/__tests__/playbook-tools.test.ts +3 -1
  74. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  75. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  76. package/src/__tests__/recording-handler.test.ts +11 -0
  77. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  78. package/src/__tests__/recording-state-machine.test.ts +13 -2
  79. package/src/__tests__/registry.test.ts +7 -3
  80. package/src/__tests__/relay-server.test.ts +148 -28
  81. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  82. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  83. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  84. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  85. package/src/__tests__/schedule-tools.test.ts +3 -1
  86. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  87. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  88. package/src/__tests__/session-agent-loop.test.ts +16 -0
  89. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  90. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  91. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  92. package/src/__tests__/session-profile-injection.test.ts +21 -0
  93. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  94. package/src/__tests__/session-queue.test.ts +23 -0
  95. package/src/__tests__/session-runtime-assembly.test.ts +50 -12
  96. package/src/__tests__/session-skill-tools.test.ts +27 -5
  97. package/src/__tests__/session-slash-known.test.ts +23 -0
  98. package/src/__tests__/session-slash-queue.test.ts +23 -0
  99. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  100. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  101. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  102. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  103. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  104. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  105. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  106. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  107. package/src/__tests__/skills.test.ts +8 -4
  108. package/src/__tests__/slack-channel-config.test.ts +3 -1
  109. package/src/__tests__/subagent-tools.test.ts +19 -0
  110. package/src/__tests__/swarm-recursion.test.ts +2 -0
  111. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  112. package/src/__tests__/swarm-tool.test.ts +2 -0
  113. package/src/__tests__/system-prompt.test.ts +3 -1
  114. package/src/__tests__/task-compiler.test.ts +3 -1
  115. package/src/__tests__/task-management-tools.test.ts +3 -1
  116. package/src/__tests__/task-tools.test.ts +3 -1
  117. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  118. package/src/__tests__/terminal-tools.test.ts +2 -0
  119. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  120. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  121. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  122. package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
  123. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  124. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  125. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  126. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  127. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  128. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  129. package/src/__tests__/view-image-tool.test.ts +3 -1
  130. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  131. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  132. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  133. package/src/__tests__/work-item-output.test.ts +3 -1
  134. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  135. package/src/calls/call-controller.ts +26 -23
  136. package/src/calls/guardian-action-sweep.ts +10 -2
  137. package/src/calls/relay-server.ts +216 -27
  138. package/src/calls/types.ts +1 -1
  139. package/src/calls/voice-session-bridge.ts +3 -3
  140. package/src/cli.ts +12 -0
  141. package/src/config/agent-schema.ts +14 -3
  142. package/src/config/calls-schema.ts +6 -6
  143. package/src/config/core-schema.ts +3 -3
  144. package/src/config/feature-flag-registry.json +8 -0
  145. package/src/config/mcp-schema.ts +1 -1
  146. package/src/config/memory-schema.ts +27 -19
  147. package/src/config/schema.ts +21 -21
  148. package/src/config/skills-schema.ts +7 -7
  149. package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
  150. package/src/daemon/handlers/config-inbox.ts +4 -4
  151. package/src/daemon/handlers/sessions.ts +148 -4
  152. package/src/daemon/ipc-contract/messages.ts +16 -0
  153. package/src/daemon/ipc-contract-inventory.json +1 -0
  154. package/src/daemon/lifecycle.ts +19 -0
  155. package/src/daemon/pairing-store.ts +86 -3
  156. package/src/daemon/session-agent-loop.ts +5 -5
  157. package/src/daemon/session-lifecycle.ts +25 -17
  158. package/src/daemon/session-memory.ts +2 -2
  159. package/src/daemon/session-process.ts +1 -20
  160. package/src/daemon/session-runtime-assembly.ts +28 -22
  161. package/src/daemon/session-tool-setup.ts +2 -2
  162. package/src/daemon/session.ts +3 -3
  163. package/src/memory/canonical-guardian-store.ts +63 -1
  164. package/src/memory/channel-guardian-store.ts +1 -0
  165. package/src/memory/conversation-crud.ts +7 -7
  166. package/src/memory/db-init.ts +4 -0
  167. package/src/memory/embedding-local.ts +257 -39
  168. package/src/memory/embedding-runtime-manager.ts +471 -0
  169. package/src/memory/guardian-bindings.ts +25 -1
  170. package/src/memory/indexer.ts +3 -3
  171. package/src/memory/ingress-invite-store.ts +45 -0
  172. package/src/memory/job-handlers/backfill.ts +16 -9
  173. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  174. package/src/memory/migrations/index.ts +1 -0
  175. package/src/memory/qdrant-client.ts +31 -22
  176. package/src/memory/schema.ts +4 -0
  177. package/src/notifications/copy-composer.ts +15 -0
  178. package/src/runtime/access-request-helper.ts +43 -7
  179. package/src/runtime/actor-trust-resolver.ts +46 -50
  180. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  181. package/src/runtime/channel-retry-sweep.ts +18 -6
  182. package/src/runtime/guardian-context-resolver.ts +38 -96
  183. package/src/runtime/guardian-reply-router.ts +31 -1
  184. package/src/runtime/ingress-service.ts +80 -3
  185. package/src/runtime/invite-redemption-service.ts +141 -2
  186. package/src/runtime/routes/channel-route-shared.ts +1 -1
  187. package/src/runtime/routes/channel-routes.ts +1 -1
  188. package/src/runtime/routes/conversation-routes.ts +2 -2
  189. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  190. package/src/runtime/routes/inbound-message-handler.ts +41 -10
  191. package/src/runtime/routes/ingress-routes.ts +52 -4
  192. package/src/runtime/routes/pairing-routes.ts +3 -0
  193. package/src/tools/guardian-control-plane-policy.ts +2 -2
  194. package/src/tools/tool-approval-handler.ts +11 -11
  195. package/src/tools/types.ts +2 -2
  196. package/src/util/logger.ts +20 -8
  197. package/src/util/platform.ts +10 -0
  198. package/src/util/voice-code.ts +29 -0
  199. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -30,6 +30,8 @@ mock.module('../providers/registry.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'mock-provider',
34
36
  maxTokens: 4096,
35
37
  thinking: false,
@@ -62,6 +64,13 @@ mock.module('../security/secret-allowlist.js', () => ({
62
64
  }));
63
65
 
64
66
  mock.module('../memory/conversation-store.js', () => ({
67
+ getConversationThreadType: () => 'default',
68
+ setConversationOriginChannelIfUnset: () => {},
69
+ updateConversationContextWindow: () => {},
70
+ deleteMessageById: () => {},
71
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
72
+ getConversationOriginInterface: () => null,
73
+ getConversationOriginChannel: () => null,
65
74
  getMessages: () => [],
66
75
  getConversation: () => ({
67
76
  id: 'conv-1',
@@ -165,6 +174,20 @@ mock.module('../agent/loop.js', () => ({
165
174
  }
166
175
  },
167
176
  }));
177
+ mock.module('../memory/canonical-guardian-store.js', () => ({
178
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
179
+ listCanonicalGuardianRequests: () => [],
180
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
181
+ getCanonicalGuardianRequest: () => null,
182
+ getCanonicalGuardianRequestByCode: () => null,
183
+ updateCanonicalGuardianRequest: () => {},
184
+ resolveCanonicalGuardianRequest: () => {},
185
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
186
+ listCanonicalGuardianDeliveries: () => [],
187
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
188
+ updateCanonicalGuardianDelivery: () => {},
189
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
190
+ }));
168
191
 
169
192
  // ---------------------------------------------------------------------------
170
193
  // Import Session AFTER mocks are registered.
@@ -30,6 +30,8 @@ mock.module('../providers/registry.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'mock-provider',
34
36
  maxTokens: 4096,
35
37
  thinking: false,
@@ -62,6 +64,13 @@ mock.module('../security/secret-allowlist.js', () => ({
62
64
  }));
63
65
 
64
66
  mock.module('../memory/conversation-store.js', () => ({
67
+ getConversationThreadType: () => 'default',
68
+ setConversationOriginChannelIfUnset: () => {},
69
+ updateConversationContextWindow: () => {},
70
+ deleteMessageById: () => {},
71
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
72
+ getConversationOriginInterface: () => null,
73
+ getConversationOriginChannel: () => null,
65
74
  getMessages: () => [],
66
75
  getConversation: () => ({
67
76
  id: 'conv-1',
@@ -165,6 +174,20 @@ mock.module('../agent/loop.js', () => ({
165
174
  }
166
175
  },
167
176
  }));
177
+ mock.module('../memory/canonical-guardian-store.js', () => ({
178
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
179
+ listCanonicalGuardianRequests: () => [],
180
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
181
+ getCanonicalGuardianRequest: () => null,
182
+ getCanonicalGuardianRequestByCode: () => null,
183
+ updateCanonicalGuardianRequest: () => {},
184
+ resolveCanonicalGuardianRequest: () => {},
185
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
186
+ listCanonicalGuardianDeliveries: () => [],
187
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
188
+ updateCanonicalGuardianDelivery: () => {},
189
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
190
+ }));
168
191
 
169
192
  // ---------------------------------------------------------------------------
170
193
  // Import Session AFTER mocks are registered.
@@ -30,6 +30,8 @@ mock.module('../providers/registry.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'mock-provider',
34
36
  maxTokens: 4096,
35
37
  thinking: false,
@@ -64,6 +66,13 @@ mock.module('../security/secret-allowlist.js', () => ({
64
66
  const addMessageCalls: Array<{ convId: string; role: string; content: string }> = [];
65
67
 
66
68
  mock.module('../memory/conversation-store.js', () => ({
69
+ getConversationThreadType: () => 'default',
70
+ setConversationOriginChannelIfUnset: () => {},
71
+ updateConversationContextWindow: () => {},
72
+ deleteMessageById: () => {},
73
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
74
+ getConversationOriginInterface: () => null,
75
+ getConversationOriginChannel: () => null,
67
76
  getMessages: () => [],
68
77
  getConversation: () => ({
69
78
  id: 'conv-1',
@@ -177,6 +186,20 @@ mock.module('../agent/loop.js', () => ({
177
186
  }
178
187
  },
179
188
  }));
189
+ mock.module('../memory/canonical-guardian-store.js', () => ({
190
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
191
+ listCanonicalGuardianRequests: () => [],
192
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
193
+ getCanonicalGuardianRequest: () => null,
194
+ getCanonicalGuardianRequestByCode: () => null,
195
+ updateCanonicalGuardianRequest: () => {},
196
+ resolveCanonicalGuardianRequest: () => {},
197
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
198
+ listCanonicalGuardianDeliveries: () => [],
199
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
200
+ updateCanonicalGuardianDelivery: () => {},
201
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
202
+ }));
180
203
 
181
204
  // ---------------------------------------------------------------------------
182
205
  // Import Session AFTER mocks are registered.
@@ -23,6 +23,8 @@ mock.module('../providers/registry.js', () => ({
23
23
 
24
24
  mock.module('../config/loader.js', () => ({
25
25
  getConfig: () => ({
26
+ ui: {},
27
+
26
28
  provider: 'mock-provider',
27
29
  maxTokens: 4096,
28
30
  thinking: false,
@@ -76,6 +78,11 @@ mock.module('../security/secret-allowlist.js', () => ({
76
78
  }));
77
79
 
78
80
  mock.module('../memory/conversation-store.js', () => ({
81
+ getConversationThreadType: () => 'default',
82
+ setConversationOriginChannelIfUnset: () => {},
83
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
84
+ getConversationOriginInterface: () => null,
85
+ getConversationOriginChannel: () => null,
79
86
  getMessages: () => [],
80
87
  getConversation: () => ({
81
88
  id: 'conv-1',
@@ -37,6 +37,8 @@ mock.module('../providers/registry.js', () => ({
37
37
 
38
38
  mock.module('../config/loader.js', () => ({
39
39
  getConfig: () => ({
40
+ ui: {},
41
+
40
42
  provider: 'mock-provider',
41
43
  maxTokens: 4096,
42
44
  thinking: false,
@@ -67,6 +69,11 @@ mock.module('../permissions/trust-store.js', () => ({ addRule: () => {}, findHig
67
69
  mock.module('../security/secret-allowlist.js', () => ({ resetAllowlist: () => {} }));
68
70
 
69
71
  mock.module('../memory/conversation-store.js', () => ({
72
+ getConversationThreadType: () => 'default',
73
+ setConversationOriginChannelIfUnset: () => {},
74
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
75
+ getConversationOriginInterface: () => null,
76
+ getConversationOriginChannel: () => null,
70
77
  getMessages: () => [],
71
78
  getConversation: () => ({
72
79
  id: 'conv-1', contextSummary: null, contextCompactedMessageCount: 0,
@@ -134,6 +141,20 @@ mock.module('../agent/loop.js', () => ({
134
141
  }
135
142
  },
136
143
  }));
144
+ mock.module('../memory/canonical-guardian-store.js', () => ({
145
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
146
+ listCanonicalGuardianRequests: () => [],
147
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
148
+ getCanonicalGuardianRequest: () => null,
149
+ getCanonicalGuardianRequestByCode: () => null,
150
+ updateCanonicalGuardianRequest: () => {},
151
+ resolveCanonicalGuardianRequest: () => {},
152
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
153
+ listCanonicalGuardianDeliveries: () => [],
154
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
155
+ updateCanonicalGuardianDelivery: () => {},
156
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
157
+ }));
137
158
 
138
159
  import { Session } from '../daemon/session.js';
139
160
 
@@ -36,6 +36,8 @@ mock.module('../providers/registry.js', () => ({
36
36
 
37
37
  mock.module('../config/loader.js', () => ({
38
38
  getConfig: () => ({
39
+ ui: {},
40
+
39
41
  provider: 'mock-provider',
40
42
  maxTokens: 4096,
41
43
  thinking: false,
@@ -66,6 +68,11 @@ mock.module('../permissions/trust-store.js', () => ({ addRule: () => {}, findHig
66
68
  mock.module('../security/secret-allowlist.js', () => ({ resetAllowlist: () => {} }));
67
69
 
68
70
  mock.module('../memory/conversation-store.js', () => ({
71
+ getConversationThreadType: () => 'default',
72
+ setConversationOriginChannelIfUnset: () => {},
73
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
74
+ getConversationOriginInterface: () => null,
75
+ getConversationOriginChannel: () => null,
69
76
  getMessages: () => [],
70
77
  getConversation: () => ({
71
78
  id: 'conv-1', contextSummary: null, contextCompactedMessageCount: 0,
@@ -124,6 +131,20 @@ mock.module('../agent/loop.js', () => ({
124
131
  }
125
132
  },
126
133
  }));
134
+ mock.module('../memory/canonical-guardian-store.js', () => ({
135
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
136
+ listCanonicalGuardianRequests: () => [],
137
+ createCanonicalGuardianRequest: () => ({ id: 'mock-cg-id', code: 'MOCK', status: 'pending' }),
138
+ getCanonicalGuardianRequest: () => null,
139
+ getCanonicalGuardianRequestByCode: () => null,
140
+ updateCanonicalGuardianRequest: () => {},
141
+ resolveCanonicalGuardianRequest: () => {},
142
+ createCanonicalGuardianDelivery: () => ({ id: 'mock-cgd-id' }),
143
+ listCanonicalGuardianDeliveries: () => [],
144
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
145
+ updateCanonicalGuardianDelivery: () => {},
146
+ generateCanonicalRequestCode: () => 'MOCK-CODE',
147
+ }));
127
148
 
128
149
  import { Session } from '../daemon/session.js';
129
150
 
@@ -20,6 +20,8 @@ mock.module('../tools/registry.js', () => ({
20
20
  // Mock config
21
21
  mock.module('../config/loader.js', () => ({
22
22
  getConfig: () => ({
23
+ ui: {},
24
+
23
25
  timeouts: { shellDefaultTimeoutSec: 120, shellMaxTimeoutSec: 600 },
24
26
  sandbox: { enabled: false, backend: 'none' },
25
27
  secretDetection: { allowOneTimeSend: false },
@@ -20,7 +20,7 @@ let currentConfig: Record<string, unknown> = {
20
20
  };
21
21
 
22
22
  const DECLARED_SKILL_ID = 'hatch-new-assistant';
23
- const DECLARED_LEGACY_KEY = 'skills.hatch-new-assistant.enabled';
23
+ const DECLARED_FLAG_KEY = 'feature_flags.hatch-new-assistant.enabled';
24
24
 
25
25
  mock.module('../util/platform.js', () => ({
26
26
  getRootDir: () => TEST_DIR,
@@ -121,7 +121,7 @@ describe('buildSystemPrompt feature flag filtering', () => {
121
121
 
122
122
  currentConfig = {
123
123
  sandbox: { enabled: false, backend: 'native' },
124
- featureFlags: { [DECLARED_LEGACY_KEY]: false },
124
+ assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
125
125
  };
126
126
 
127
127
  const result = buildSystemPrompt();
@@ -137,7 +137,7 @@ describe('buildSystemPrompt feature flag filtering', () => {
137
137
 
138
138
  currentConfig = {
139
139
  sandbox: { enabled: false, backend: 'native' },
140
- featureFlags: {},
140
+ assistantFeatureFlagValues: {},
141
141
  };
142
142
 
143
143
  const result = buildSystemPrompt();
@@ -152,9 +152,9 @@ describe('buildSystemPrompt feature flag filtering', () => {
152
152
 
153
153
  currentConfig = {
154
154
  sandbox: { enabled: false, backend: 'native' },
155
- featureFlags: {
156
- [DECLARED_LEGACY_KEY]: false,
157
- 'skills.twitter.enabled': false,
155
+ assistantFeatureFlagValues: {
156
+ [DECLARED_FLAG_KEY]: false,
157
+ 'feature_flags.twitter.enabled': false,
158
158
  },
159
159
  };
160
160
 
@@ -15,7 +15,7 @@ let currentConfig: Record<string, unknown> = {
15
15
  };
16
16
 
17
17
  const DECLARED_SKILL_ID = 'hatch-new-assistant';
18
- const DECLARED_LEGACY_KEY = 'skills.hatch-new-assistant.enabled';
18
+ const DECLARED_FLAG_KEY = 'feature_flags.hatch-new-assistant.enabled';
19
19
 
20
20
  const platformOverrides: Record<string, (...args: unknown[]) => unknown> = {
21
21
  getRootDir: () => TEST_DIR,
@@ -54,6 +54,7 @@ mock.module('../util/logger.js', () => ({
54
54
  getLogger: () => new Proxy({} as Record<string, unknown>, {
55
55
  get: () => () => {},
56
56
  }),
57
+ isDebug: () => false,
57
58
  }));
58
59
 
59
60
  mock.module('../config/loader.js', () => ({
@@ -101,7 +102,7 @@ describe('skill_load feature flag enforcement', () => {
101
102
  writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), `- ${DECLARED_SKILL_ID}\n`);
102
103
 
103
104
  currentConfig = {
104
- featureFlags: { [DECLARED_LEGACY_KEY]: false },
105
+ assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: false },
105
106
  };
106
107
 
107
108
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });
@@ -116,7 +117,7 @@ describe('skill_load feature flag enforcement', () => {
116
117
  writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), `- ${DECLARED_SKILL_ID}\n`);
117
118
 
118
119
  currentConfig = {
119
- featureFlags: { [DECLARED_LEGACY_KEY]: true },
120
+ assistantFeatureFlagValues: { [DECLARED_FLAG_KEY]: true },
120
121
  };
121
122
 
122
123
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });
@@ -130,7 +131,7 @@ describe('skill_load feature flag enforcement', () => {
130
131
  writeFileSync(join(TEST_DIR, 'skills', 'SKILLS.md'), `- ${DECLARED_SKILL_ID}\n`);
131
132
 
132
133
  currentConfig = {
133
- featureFlags: {},
134
+ assistantFeatureFlagValues: {},
134
135
  };
135
136
 
136
137
  const result = await executeSkillLoad({ skill: DECLARED_SKILL_ID });
@@ -31,10 +31,31 @@ const DECLARED_LEGACY_KEY = 'skills.hatch-new-assistant.enabled';
31
31
 
32
32
  mock.module('../config/skills.js', () => ({
33
33
  loadSkillCatalog: () => mockCatalog,
34
+ checkSkillRequirements: () => ({ satisfied: true, missing: [] }),
34
35
  }));
35
36
 
36
37
  mock.module('../config/loader.js', () => ({
37
38
  getConfig: () => currentConfig,
39
+ loadConfig: () => currentConfig,
40
+ invalidateConfigCache: () => {},
41
+ }));
42
+
43
+ mock.module('../config/assistant-feature-flags.js', () => ({
44
+ isAssistantFeatureFlagEnabled: (key: string, config: Record<string, unknown>) => {
45
+ const vals = (config as { assistantFeatureFlagValues?: Record<string, boolean> }).assistantFeatureFlagValues;
46
+ if (vals && typeof vals[key] === 'boolean') return vals[key];
47
+ // Check legacy featureFlags too
48
+ const legacy = (config as { featureFlags?: Record<string, boolean> }).featureFlags;
49
+ if (legacy && typeof legacy[key] === 'boolean') return legacy[key];
50
+ return true; // default enabled
51
+ },
52
+ loadDefaultsRegistry: () => ({}),
53
+ getAssistantFeatureFlagDefaults: () => ({}),
54
+ _resetDefaultsCache: () => {},
55
+ }));
56
+
57
+ mock.module('../config/skill-state.js', () => ({
58
+ skillFlagKey: (skillId: string) => `skills.${skillId}.enabled`,
38
59
  }));
39
60
 
40
61
  mock.module('../skills/active-skill-tools.js', () => {
@@ -184,6 +205,7 @@ mock.module('../util/logger.js', () => ({
184
205
  debug: () => {},
185
206
  error: () => {},
186
207
  }),
208
+ isDebug: () => false,
187
209
  }));
188
210
 
189
211
  // ---------------------------------------------------------------------------
@@ -518,14 +518,14 @@ describe('bundled browser skill', () => {
518
518
  expect(browserSkill!.disableModelInvocation).toBe(false);
519
519
  });
520
520
 
521
- test('browser skill has a valid tool manifest with 10 tools', () => {
521
+ test('browser skill has a valid tool manifest with 14 tools', () => {
522
522
  const catalog = loadSkillCatalog();
523
523
  const browserSkill = catalog.find((s) => s.id === 'browser');
524
524
  expect(browserSkill).toBeDefined();
525
525
  expect(browserSkill!.toolManifest).toBeDefined();
526
526
  expect(browserSkill!.toolManifest!.present).toBe(true);
527
527
  expect(browserSkill!.toolManifest!.valid).toBe(true);
528
- expect(browserSkill!.toolManifest!.toolCount).toBe(10);
528
+ expect(browserSkill!.toolManifest!.toolCount).toBe(14);
529
529
  expect(browserSkill!.toolManifest!.toolNames).toEqual([
530
530
  'browser_navigate',
531
531
  'browser_snapshot',
@@ -534,8 +534,12 @@ describe('bundled browser skill', () => {
534
534
  'browser_click',
535
535
  'browser_type',
536
536
  'browser_press_key',
537
+ 'browser_scroll',
538
+ 'browser_select_option',
539
+ 'browser_hover',
537
540
  'browser_wait_for',
538
541
  'browser_extract',
542
+ 'browser_wait_for_download',
539
543
  'browser_fill_credential',
540
544
  ]);
541
545
  });
@@ -618,10 +622,10 @@ describe('ingress-dependent setup skills declare public-ingress', () => {
618
622
  expect(includes).toContain('public-ingress');
619
623
  });
620
624
 
621
- test('slack-oauth-setup includes public-ingress', () => {
625
+ test('slack-oauth-setup includes browser', () => {
622
626
  const includes = readSkillIncludes(VELLUM_SKILLS_DIR, 'slack-oauth-setup');
623
627
  expect(includes).toBeDefined();
624
- expect(includes).toContain('public-ingress');
628
+ expect(includes).toContain('browser');
625
629
  });
626
630
  });
627
631
 
@@ -7,7 +7,9 @@ import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test';
7
7
  const testDir = mkdtempSync(join(tmpdir(), 'slack-channel-cfg-test-'));
8
8
 
9
9
  mock.module('../config/loader.js', () => ({
10
- getConfig: () => ({}),
10
+ getConfig: () => ({
11
+ ui: {},
12
+ }),
11
13
  loadConfig: () => ({}),
12
14
  loadRawConfig: () => ({}),
13
15
  saveRawConfig: () => {},
@@ -6,6 +6,25 @@ import { describe, expect, mock, test } from 'bun:test';
6
6
  // Mock conversation-store before importing tool executors that depend on it.
7
7
  let mockGetMessages: (conversationId: string) => Array<{ role: string; content: string }> | null = () => null;
8
8
  mock.module('../memory/conversation-store.js', () => ({
9
+ getConversationThreadType: () => 'default',
10
+ setConversationOriginChannelIfUnset: () => {},
11
+ updateConversationContextWindow: () => {},
12
+ deleteMessageById: () => {},
13
+ updateConversationTitle: () => {},
14
+ updateConversationUsage: () => {},
15
+ addMessage: () => ({ id: 'mock-msg-id' }),
16
+ getConversation: () => ({
17
+ id: 'conv-1',
18
+ contextSummary: null,
19
+ contextCompactedMessageCount: 0,
20
+ totalInputTokens: 0,
21
+ totalOutputTokens: 0,
22
+ totalEstimatedCost: 0,
23
+ title: null,
24
+ }),
25
+ provenanceFromGuardianContext: () => ({ source: 'user', guardianContext: undefined }),
26
+ getConversationOriginInterface: () => null,
27
+ getConversationOriginChannel: () => null,
9
28
  getMessages: (conversationId: string) => mockGetMessages(conversationId),
10
29
  createConversation: () => ({ id: 'mock-conv' }),
11
30
  }));
@@ -30,6 +30,8 @@ mock.module('../util/logger.js', () => ({
30
30
 
31
31
  mock.module('../config/loader.js', () => ({
32
32
  getConfig: () => ({
33
+ ui: {},
34
+
33
35
  provider: 'anthropic',
34
36
  providerOrder: ['anthropic'],
35
37
  apiKeys: { anthropic: 'test-key' },
@@ -21,6 +21,8 @@ let hasApiKey = true;
21
21
 
22
22
  mock.module('../config/loader.js', () => ({
23
23
  getConfig: () => ({
24
+ ui: {},
25
+
24
26
  provider: 'anthropic',
25
27
  providerOrder: ['anthropic'],
26
28
  apiKeys: { anthropic: hasApiKey ? 'test-key' : '' },
@@ -12,6 +12,8 @@ mock.module('../util/logger.js', () => ({
12
12
 
13
13
  mock.module('../config/loader.js', () => ({
14
14
  getConfig: () => ({
15
+ ui: {},
16
+
15
17
  provider: 'anthropic',
16
18
  providerOrder: ['anthropic'],
17
19
  apiKeys: { anthropic: 'test-key' },
@@ -49,6 +49,8 @@ mock.module('../util/logger.js', () => ({
49
49
 
50
50
  mock.module('../config/loader.js', () => ({
51
51
  getConfig: () => ({
52
+ ui: {},
53
+
52
54
  sandbox: { enabled: true },
53
55
  }),
54
56
  }));
@@ -203,7 +205,7 @@ describe('buildSystemPrompt', () => {
203
205
 
204
206
  test('config section uses workspace directory from platform util', () => {
205
207
  const result = buildSystemPrompt();
206
- expect(result).toContain(`Your workspace is mounted at \`/workspace/\` inside the Docker sandbox (host path: \`${TEST_DIR}/\`)`);
208
+ expect(result).toContain(`Your configuration directory is \`${TEST_DIR}/\`.`);
207
209
  });
208
210
 
209
211
  test('omits user skills from catalog when none are configured', () => {
@@ -28,7 +28,9 @@ mock.module('../util/logger.js', () => ({
28
28
  }));
29
29
 
30
30
  mock.module('../config/loader.js', () => ({
31
- getConfig: () => ({ memory: {} }),
31
+ getConfig: () => ({
32
+ ui: {},
33
+ memory: {} }),
32
34
  }));
33
35
 
34
36
  mock.module('./indexer.js', () => ({
@@ -27,7 +27,9 @@ mock.module('../util/logger.js', () => ({
27
27
  }));
28
28
 
29
29
  mock.module('../config/loader.js', () => ({
30
- getConfig: () => ({ memory: {} }),
30
+ getConfig: () => ({
31
+ ui: {},
32
+ memory: {} }),
31
33
  }));
32
34
 
33
35
  mock.module('../tools/registry.js', () => ({
@@ -27,7 +27,9 @@ mock.module('../util/logger.js', () => ({
27
27
  }));
28
28
 
29
29
  mock.module('../config/loader.js', () => ({
30
- getConfig: () => ({ memory: {} }),
30
+ getConfig: () => ({
31
+ ui: {},
32
+ memory: {} }),
31
33
  }));
32
34
 
33
35
  mock.module('./indexer.js', () => ({
@@ -177,20 +177,21 @@ describe('terminal sandbox — macOS sandbox-exec behavior', () => {
177
177
  expect(result.args.slice(2)).toEqual(['bash', '-c', '--', 'echo hello']);
178
178
  });
179
179
 
180
- test('throws ToolError for working dirs with SBPL metacharacters', () => {
181
- expect(() => wrapCommand('pwd', '/tmp/bad"dir', nativeConfig())).toThrow(ToolError);
182
- expect(() => wrapCommand('pwd', '/tmp/bad(dir', nativeConfig())).toThrow(ToolError);
183
- expect(() => wrapCommand('pwd', '/tmp/bad;dir', nativeConfig())).toThrow(ToolError);
180
+ test('escapes SBPL metacharacters in working dirs instead of throwing', () => {
181
+ // The sandbox now escapes metacharacters rather than rejecting them
182
+ const result1 = wrapCommand('pwd', '/tmp/bad"dir', nativeConfig());
183
+ expect(result1.sandboxed).toBe(true);
184
+ const result2 = wrapCommand('pwd', '/tmp/bad(dir', nativeConfig());
185
+ expect(result2.sandboxed).toBe(true);
186
+ const result3 = wrapCommand('pwd', '/tmp/bad;dir', nativeConfig());
187
+ expect(result3.sandboxed).toBe(true);
184
188
  });
185
189
 
186
- test('SBPL metacharacter error mentions unsafe characters', () => {
187
- try {
188
- wrapCommand('pwd', '/tmp/bad"dir', nativeConfig());
189
- throw new Error('should have thrown');
190
- } catch (err) {
191
- expect(err).toBeInstanceOf(ToolError);
192
- expect((err as Error).message).toContain('SBPL metacharacters');
193
- }
190
+ test('SBPL profile escapes metacharacters in working dir path', () => {
191
+ // Verify the sandbox profile is written with escaped chars
192
+ wrapCommand('pwd', '/tmp/bad"dir', nativeConfig());
193
+ const profileContent = writeFileSyncMock.mock.calls[0]?.[1] as string;
194
+ expect(profileContent).toContain('bad\\"dir');
194
195
  });
195
196
  });
196
197
 
@@ -34,6 +34,8 @@ mock.module('../util/platform.js', () => ({
34
34
 
35
35
  mock.module('../config/loader.js', () => ({
36
36
  getConfig: () => ({
37
+ ui: {},
38
+
37
39
  timeouts: { shellDefaultTimeoutSec: 120, shellMaxTimeoutSec: 600 },
38
40
  sandbox: {
39
41
  enabled: false,