@vellumai/assistant 0.3.27 → 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 (247) hide show
  1. package/ARCHITECTURE.md +81 -4
  2. package/Dockerfile +2 -2
  3. package/bun.lock +4 -1
  4. package/docs/trusted-contact-access.md +9 -2
  5. package/package.json +6 -3
  6. package/scripts/ipc/generate-swift.ts +9 -5
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
  8. package/src/__tests__/agent-loop-thinking.test.ts +1 -1
  9. package/src/__tests__/agent-loop.test.ts +119 -0
  10. package/src/__tests__/approval-routes-http.test.ts +13 -5
  11. package/src/__tests__/asset-materialize-tool.test.ts +2 -0
  12. package/src/__tests__/asset-search-tool.test.ts +2 -0
  13. package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
  14. package/src/__tests__/attachments-store.test.ts +2 -0
  15. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  16. package/src/__tests__/bundled-asset.test.ts +107 -0
  17. package/src/__tests__/call-controller.test.ts +30 -29
  18. package/src/__tests__/call-routes-http.test.ts +34 -32
  19. package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
  20. package/src/__tests__/canonical-guardian-store.test.ts +636 -0
  21. package/src/__tests__/channel-approval-routes.test.ts +174 -1
  22. package/src/__tests__/channel-invite-transport.test.ts +6 -6
  23. package/src/__tests__/channel-reply-delivery.test.ts +19 -0
  24. package/src/__tests__/channel-retry-sweep.test.ts +130 -0
  25. package/src/__tests__/clarification-resolver.test.ts +2 -0
  26. package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
  27. package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
  28. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
  29. package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
  30. package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
  31. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
  32. package/src/__tests__/config-schema.test.ts +5 -5
  33. package/src/__tests__/config-watcher.test.ts +3 -1
  34. package/src/__tests__/connection-policy.test.ts +14 -5
  35. package/src/__tests__/contacts-tools.test.ts +3 -1
  36. package/src/__tests__/contradiction-checker.test.ts +2 -0
  37. package/src/__tests__/conversation-pairing.test.ts +10 -0
  38. package/src/__tests__/conversation-routes.test.ts +1 -1
  39. package/src/__tests__/credential-security-invariants.test.ts +16 -6
  40. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  41. package/src/__tests__/credential-vault.test.ts +5 -4
  42. package/src/__tests__/daemon-lifecycle.test.ts +9 -0
  43. package/src/__tests__/daemon-server-session-init.test.ts +27 -0
  44. package/src/__tests__/elevenlabs-config.test.ts +2 -0
  45. package/src/__tests__/emit-signal-routing-intent.test.ts +43 -1
  46. package/src/__tests__/encrypted-store.test.ts +10 -5
  47. package/src/__tests__/followup-tools.test.ts +3 -1
  48. package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
  49. package/src/__tests__/gmail-integration.test.ts +0 -1
  50. package/src/__tests__/guardian-actions-endpoint.test.ts +205 -345
  51. package/src/__tests__/guardian-control-plane-policy.test.ts +19 -19
  52. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +599 -0
  53. package/src/__tests__/guardian-dispatch.test.ts +21 -19
  54. package/src/__tests__/guardian-grant-minting.test.ts +68 -1
  55. package/src/__tests__/guardian-outbound-http.test.ts +12 -9
  56. package/src/__tests__/guardian-routing-invariants.test.ts +1092 -0
  57. package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
  58. package/src/__tests__/handlers-slack-config.test.ts +3 -1
  59. package/src/__tests__/handlers-telegram-config.test.ts +3 -1
  60. package/src/__tests__/handlers-twilio-config.test.ts +3 -1
  61. package/src/__tests__/handlers-twitter-config.test.ts +3 -1
  62. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
  63. package/src/__tests__/heartbeat-service.test.ts +20 -0
  64. package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
  65. package/src/__tests__/ingress-reconcile.test.ts +3 -1
  66. package/src/__tests__/ingress-routes-http.test.ts +231 -4
  67. package/src/__tests__/intent-routing.test.ts +2 -0
  68. package/src/__tests__/ipc-snapshot.test.ts +13 -0
  69. package/src/__tests__/mcp-cli.test.ts +77 -0
  70. package/src/__tests__/media-generate-image.test.ts +21 -0
  71. package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
  72. package/src/__tests__/memory-regressions.test.ts +20 -20
  73. package/src/__tests__/non-member-access-request.test.ts +212 -36
  74. package/src/__tests__/notification-decision-fallback.test.ts +63 -3
  75. package/src/__tests__/notification-decision-strategy.test.ts +78 -0
  76. package/src/__tests__/notification-guardian-path.test.ts +15 -15
  77. package/src/__tests__/oauth-connect-handler.test.ts +3 -1
  78. package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
  79. package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
  80. package/src/__tests__/onboarding-template-contract.test.ts +116 -21
  81. package/src/__tests__/pairing-routes.test.ts +171 -0
  82. package/src/__tests__/playbook-execution.test.ts +3 -1
  83. package/src/__tests__/playbook-tools.test.ts +3 -1
  84. package/src/__tests__/provider-error-scenarios.test.ts +59 -8
  85. package/src/__tests__/proxy-approval-callback.test.ts +2 -0
  86. package/src/__tests__/recording-handler.test.ts +11 -0
  87. package/src/__tests__/recording-intent-handler.test.ts +15 -0
  88. package/src/__tests__/recording-state-machine.test.ts +13 -2
  89. package/src/__tests__/registry.test.ts +7 -3
  90. package/src/__tests__/relay-server.test.ts +148 -28
  91. package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
  92. package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
  93. package/src/__tests__/runtime-events-sse.test.ts +4 -2
  94. package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
  95. package/src/__tests__/schedule-tools.test.ts +3 -1
  96. package/src/__tests__/secret-scanner-executor.test.ts +59 -0
  97. package/src/__tests__/secret-scanner.test.ts +8 -0
  98. package/src/__tests__/send-endpoint-busy.test.ts +4 -0
  99. package/src/__tests__/sensitive-output-placeholders.test.ts +208 -0
  100. package/src/__tests__/session-abort-tool-results.test.ts +23 -0
  101. package/src/__tests__/session-agent-loop.test.ts +16 -0
  102. package/src/__tests__/session-conflict-gate.test.ts +21 -0
  103. package/src/__tests__/session-load-history-repair.test.ts +27 -17
  104. package/src/__tests__/session-pre-run-repair.test.ts +23 -0
  105. package/src/__tests__/session-profile-injection.test.ts +21 -0
  106. package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
  107. package/src/__tests__/session-queue.test.ts +23 -0
  108. package/src/__tests__/session-runtime-assembly.test.ts +126 -59
  109. package/src/__tests__/session-skill-tools.test.ts +27 -5
  110. package/src/__tests__/session-slash-known.test.ts +23 -0
  111. package/src/__tests__/session-slash-queue.test.ts +23 -0
  112. package/src/__tests__/session-slash-unknown.test.ts +23 -0
  113. package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
  114. package/src/__tests__/session-workspace-injection.test.ts +21 -0
  115. package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
  116. package/src/__tests__/shell-credential-ref.test.ts +2 -0
  117. package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
  118. package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
  119. package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
  120. package/src/__tests__/skills.test.ts +8 -4
  121. package/src/__tests__/slack-channel-config.test.ts +3 -1
  122. package/src/__tests__/subagent-tools.test.ts +19 -0
  123. package/src/__tests__/swarm-recursion.test.ts +2 -0
  124. package/src/__tests__/swarm-session-integration.test.ts +2 -0
  125. package/src/__tests__/swarm-tool.test.ts +2 -0
  126. package/src/__tests__/system-prompt.test.ts +3 -1
  127. package/src/__tests__/task-compiler.test.ts +3 -1
  128. package/src/__tests__/task-management-tools.test.ts +3 -1
  129. package/src/__tests__/task-tools.test.ts +3 -1
  130. package/src/__tests__/terminal-sandbox.test.ts +13 -12
  131. package/src/__tests__/terminal-tools.test.ts +2 -0
  132. package/src/__tests__/tool-approval-handler.test.ts +15 -15
  133. package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
  134. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
  135. package/src/__tests__/tool-grant-request-escalation.test.ts +497 -0
  136. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
  137. package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
  138. package/src/__tests__/trusted-contact-verification.test.ts +91 -0
  139. package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
  140. package/src/__tests__/twitter-auth-handler.test.ts +3 -1
  141. package/src/__tests__/twitter-cli-routing.test.ts +3 -1
  142. package/src/__tests__/view-image-tool.test.ts +3 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +329 -0
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
  145. package/src/__tests__/voice-session-bridge.test.ts +10 -10
  146. package/src/__tests__/work-item-output.test.ts +3 -1
  147. package/src/__tests__/workspace-lifecycle.test.ts +13 -2
  148. package/src/agent/loop.ts +46 -3
  149. package/src/approvals/guardian-decision-primitive.ts +285 -0
  150. package/src/approvals/guardian-request-resolvers.ts +539 -0
  151. package/src/calls/call-controller.ts +26 -23
  152. package/src/calls/guardian-action-sweep.ts +10 -2
  153. package/src/calls/guardian-dispatch.ts +46 -40
  154. package/src/calls/relay-server.ts +358 -24
  155. package/src/calls/types.ts +1 -1
  156. package/src/calls/voice-session-bridge.ts +3 -3
  157. package/src/cli.ts +12 -0
  158. package/src/config/agent-schema.ts +14 -3
  159. package/src/config/calls-schema.ts +6 -6
  160. package/src/config/core-schema.ts +3 -3
  161. package/src/config/feature-flag-registry.json +8 -0
  162. package/src/config/mcp-schema.ts +1 -1
  163. package/src/config/memory-schema.ts +27 -19
  164. package/src/config/schema.ts +21 -21
  165. package/src/config/skills-schema.ts +7 -7
  166. package/src/config/system-prompt.ts +2 -1
  167. package/src/config/templates/BOOTSTRAP.md +47 -31
  168. package/src/config/templates/USER.md +5 -0
  169. package/src/config/update-bulletin-template-path.ts +4 -1
  170. package/src/config/vellum-skills/trusted-contacts/SKILL.md +149 -21
  171. package/src/daemon/handlers/config-inbox.ts +4 -4
  172. package/src/daemon/handlers/guardian-actions.ts +45 -66
  173. package/src/daemon/handlers/sessions.ts +148 -4
  174. package/src/daemon/ipc-contract/guardian-actions.ts +7 -0
  175. package/src/daemon/ipc-contract/messages.ts +16 -0
  176. package/src/daemon/ipc-contract-inventory.json +1 -0
  177. package/src/daemon/lifecycle.ts +22 -16
  178. package/src/daemon/pairing-store.ts +86 -3
  179. package/src/daemon/server.ts +18 -0
  180. package/src/daemon/session-agent-loop-handlers.ts +5 -4
  181. package/src/daemon/session-agent-loop.ts +33 -6
  182. package/src/daemon/session-lifecycle.ts +25 -17
  183. package/src/daemon/session-memory.ts +2 -2
  184. package/src/daemon/session-process.ts +68 -326
  185. package/src/daemon/session-runtime-assembly.ts +119 -25
  186. package/src/daemon/session-tool-setup.ts +3 -2
  187. package/src/daemon/session.ts +4 -3
  188. package/src/home-base/prebuilt/seed.ts +2 -1
  189. package/src/hooks/templates.ts +2 -1
  190. package/src/memory/canonical-guardian-store.ts +586 -0
  191. package/src/memory/channel-guardian-store.ts +2 -0
  192. package/src/memory/conversation-crud.ts +7 -7
  193. package/src/memory/db-init.ts +20 -0
  194. package/src/memory/embedding-local.ts +257 -39
  195. package/src/memory/embedding-runtime-manager.ts +471 -0
  196. package/src/memory/guardian-action-store.ts +7 -60
  197. package/src/memory/guardian-approvals.ts +9 -4
  198. package/src/memory/guardian-bindings.ts +25 -1
  199. package/src/memory/indexer.ts +3 -3
  200. package/src/memory/ingress-invite-store.ts +45 -0
  201. package/src/memory/job-handlers/backfill.ts +16 -9
  202. package/src/memory/migrations/036-normalize-phone-identities.ts +289 -0
  203. package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
  204. package/src/memory/migrations/118-reminder-routing-intent.ts +3 -3
  205. package/src/memory/migrations/121-canonical-guardian-requests.ts +59 -0
  206. package/src/memory/migrations/122-canonical-guardian-requester-chat-id.ts +15 -0
  207. package/src/memory/migrations/123-canonical-guardian-deliveries-destination-index.ts +15 -0
  208. package/src/memory/migrations/index.ts +5 -0
  209. package/src/memory/migrations/registry.ts +5 -0
  210. package/src/memory/qdrant-client.ts +31 -22
  211. package/src/memory/schema-migration.ts +1 -0
  212. package/src/memory/schema.ts +56 -0
  213. package/src/notifications/copy-composer.ts +31 -4
  214. package/src/notifications/decision-engine.ts +57 -0
  215. package/src/permissions/defaults.ts +2 -0
  216. package/src/runtime/access-request-helper.ts +173 -0
  217. package/src/runtime/actor-trust-resolver.ts +221 -0
  218. package/src/runtime/channel-guardian-service.ts +12 -4
  219. package/src/runtime/channel-invite-transports/voice.ts +58 -0
  220. package/src/runtime/channel-retry-sweep.ts +18 -6
  221. package/src/runtime/guardian-context-resolver.ts +38 -71
  222. package/src/runtime/guardian-decision-types.ts +6 -0
  223. package/src/runtime/guardian-reply-router.ts +717 -0
  224. package/src/runtime/http-server.ts +8 -0
  225. package/src/runtime/ingress-service.ts +80 -3
  226. package/src/runtime/invite-redemption-service.ts +141 -2
  227. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +116 -0
  228. package/src/runtime/routes/channel-route-shared.ts +1 -1
  229. package/src/runtime/routes/channel-routes.ts +1 -1
  230. package/src/runtime/routes/conversation-routes.ts +20 -2
  231. package/src/runtime/routes/guardian-action-routes.ts +100 -109
  232. package/src/runtime/routes/guardian-approval-interception.ts +17 -6
  233. package/src/runtime/routes/inbound-message-handler.ts +205 -529
  234. package/src/runtime/routes/ingress-routes.ts +52 -4
  235. package/src/runtime/routes/pairing-routes.ts +3 -0
  236. package/src/runtime/tool-grant-request-helper.ts +195 -0
  237. package/src/tools/executor.ts +13 -1
  238. package/src/tools/guardian-control-plane-policy.ts +2 -2
  239. package/src/tools/sensitive-output-placeholders.ts +203 -0
  240. package/src/tools/tool-approval-handler.ts +53 -10
  241. package/src/tools/types.ts +13 -2
  242. package/src/util/bundled-asset.ts +31 -0
  243. package/src/util/canonicalize-identity.ts +52 -0
  244. package/src/util/logger.ts +20 -8
  245. package/src/util/platform.ts +10 -0
  246. package/src/util/voice-code.ts +29 -0
  247. package/src/daemon/guardian-invite-intent.ts +0 -124
@@ -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,
@@ -100,7 +100,7 @@ function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
100
100
  conversationId: 'conv-1',
101
101
  assistantId: 'self',
102
102
  requestId: 'req-1',
103
- guardianActorRole: 'non-guardian',
103
+ guardianTrustClass: 'trusted_contact',
104
104
  ...overrides,
105
105
  };
106
106
  }
@@ -134,7 +134,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
134
134
  );
135
135
  expect(mintResult.ok).toBe(true);
136
136
 
137
- const context = makeContext({ guardianActorRole: 'non-guardian' });
137
+ const context = makeContext({ guardianTrustClass: 'trusted_contact' });
138
138
  const result = await handler.checkPreExecutionGates(
139
139
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
140
140
  );
@@ -149,7 +149,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
149
149
  const toolName = 'bash';
150
150
  const input = { command: 'rm -rf /' };
151
151
 
152
- const context = makeContext({ guardianActorRole: 'non-guardian' });
152
+ const context = makeContext({ guardianTrustClass: 'trusted_contact' });
153
153
  const result = await handler.checkPreExecutionGates(
154
154
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
155
155
  );
@@ -177,7 +177,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
177
177
  }),
178
178
  );
179
179
 
180
- const context = makeContext({ guardianActorRole: 'unverified_channel' });
180
+ const context = makeContext({ guardianTrustClass: 'unknown' });
181
181
  const result = await handler.checkPreExecutionGates(
182
182
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
183
183
  );
@@ -189,7 +189,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
189
189
  const toolName = 'bash';
190
190
  const input = { command: 'deploy' };
191
191
 
192
- const context = makeContext({ guardianActorRole: 'unverified_channel' });
192
+ const context = makeContext({ guardianTrustClass: 'unknown' });
193
193
  const result = await handler.checkPreExecutionGates(
194
194
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
195
195
  );
@@ -212,7 +212,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
212
212
  }),
213
213
  );
214
214
 
215
- const context = makeContext({ guardianActorRole: 'non-guardian' });
215
+ const context = makeContext({ guardianTrustClass: 'trusted_contact' });
216
216
 
217
217
  // First invocation — should consume the grant and allow
218
218
  const first = await handler.checkPreExecutionGates(
@@ -241,7 +241,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
241
241
  }),
242
242
  );
243
243
 
244
- const context = makeContext({ guardianActorRole: 'non-guardian' });
244
+ const context = makeContext({ guardianTrustClass: 'trusted_contact' });
245
245
  const result = await handler.checkPreExecutionGates(
246
246
  toolName, invokeInput, context, 'host', 'high', Date.now(), emitLifecycleEvent,
247
247
  );
@@ -264,7 +264,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
264
264
  }),
265
265
  );
266
266
 
267
- const context = makeContext({ guardianActorRole: 'non-guardian' });
267
+ const context = makeContext({ guardianTrustClass: 'trusted_contact' });
268
268
  const result = await handler.checkPreExecutionGates(
269
269
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
270
270
  );
@@ -277,7 +277,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
277
277
  const input = { command: 'deploy' };
278
278
 
279
279
  // No grants minted at all
280
- const context = makeContext({ guardianActorRole: 'guardian' });
280
+ const context = makeContext({ guardianTrustClass: 'guardian' });
281
281
  const result = await handler.checkPreExecutionGates(
282
282
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
283
283
  );
@@ -290,7 +290,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
290
290
  const toolName = 'bash';
291
291
  const input = { command: 'deploy' };
292
292
 
293
- const context = makeContext({ guardianActorRole: undefined });
293
+ const context = makeContext({ guardianTrustClass: undefined });
294
294
  const result = await handler.checkPreExecutionGates(
295
295
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
296
296
  );
@@ -309,7 +309,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
309
309
  }),
310
310
  );
311
311
 
312
- const context = makeContext({ guardianActorRole: 'non-guardian', requestId: 'req-1' });
312
+ const context = makeContext({ guardianTrustClass: 'trusted_contact', requestId: 'req-1' });
313
313
  const result = await handler.checkPreExecutionGates(
314
314
  toolName, input, context, 'host', 'high', Date.now(), emitLifecycleEvent,
315
315
  );
@@ -333,7 +333,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
333
333
 
334
334
  // Context conversationId does not match the grant's conversationId
335
335
  const context = makeContext({
336
- guardianActorRole: 'non-guardian',
336
+ guardianTrustClass: 'trusted_contact',
337
337
  conversationId: 'conv-1',
338
338
  });
339
339
  const result = await handler.checkPreExecutionGates(
@@ -349,7 +349,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
349
349
 
350
350
  // executionChannel defaults to undefined (non-voice)
351
351
  const context = makeContext({
352
- guardianActorRole: 'non-guardian',
352
+ guardianTrustClass: 'trusted_contact',
353
353
  executionChannel: 'telegram',
354
354
  });
355
355
 
@@ -383,7 +383,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
383
383
  }, 300);
384
384
 
385
385
  const context = makeContext({
386
- guardianActorRole: 'non-guardian',
386
+ guardianTrustClass: 'trusted_contact',
387
387
  executionChannel: 'voice',
388
388
  });
389
389
 
@@ -408,7 +408,7 @@ describe('ToolApprovalHandler / pre-exec gate grant check', () => {
408
408
  setTimeout(() => controller.abort(), 200);
409
409
 
410
410
  const context = makeContext({
411
- guardianActorRole: 'non-guardian',
411
+ guardianTrustClass: 'trusted_contact',
412
412
  executionChannel: 'voice',
413
413
  signal: controller.signal,
414
414
  });
@@ -22,6 +22,8 @@ import { mock } from 'bun:test';
22
22
 
23
23
  mock.module('../config/loader.js', () => ({
24
24
  getConfig: () => ({
25
+ ui: {},
26
+
25
27
  provider: 'anthropic',
26
28
  model: 'test',
27
29
  apiKeys: {},
@@ -59,6 +59,8 @@ mock.module('../permissions/trust-store.js', () => ({
59
59
 
60
60
  mock.module('../config/loader.js', () => ({
61
61
  getConfig: () => ({
62
+ ui: {},
63
+
62
64
  provider: 'mock-provider',
63
65
  timeouts: { permissionTimeoutSec: 5, toolExecutionTimeoutSec: 120 },
64
66
  permissions: { mode: 'legacy' },