@vellumai/assistant 0.3.19 → 0.3.21

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 +151 -15
  2. package/Dockerfile +1 -0
  3. package/README.md +40 -4
  4. package/bun.lock +139 -2
  5. package/docs/architecture/integrations.md +7 -11
  6. package/package.json +2 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +54 -0
  8. package/src/__tests__/approval-primitive.test.ts +540 -0
  9. package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
  10. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
  11. package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
  12. package/src/__tests__/call-controller.test.ts +439 -108
  13. package/src/__tests__/channel-invite-transport.test.ts +264 -0
  14. package/src/__tests__/cli.test.ts +42 -1
  15. package/src/__tests__/config-schema.test.ts +11 -127
  16. package/src/__tests__/config-watcher.test.ts +0 -8
  17. package/src/__tests__/daemon-lifecycle.test.ts +1 -0
  18. package/src/__tests__/daemon-server-session-init.test.ts +8 -2
  19. package/src/__tests__/diff.test.ts +22 -0
  20. package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
  21. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +300 -32
  22. package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
  23. package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
  24. package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
  25. package/src/__tests__/guardian-dispatch.test.ts +124 -0
  26. package/src/__tests__/guardian-grant-minting.test.ts +6 -17
  27. package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +306 -0
  29. package/src/__tests__/ipc-snapshot.test.ts +57 -0
  30. package/src/__tests__/notification-decision-fallback.test.ts +88 -0
  31. package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
  32. package/src/__tests__/sandbox-host-parity.test.ts +6 -13
  33. package/src/__tests__/scoped-approval-grants.test.ts +6 -6
  34. package/src/__tests__/scoped-grant-security-matrix.test.ts +5 -4
  35. package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
  36. package/src/__tests__/session-load-history-repair.test.ts +169 -2
  37. package/src/__tests__/session-runtime-assembly.test.ts +33 -5
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
  39. package/src/__tests__/skill-feature-flags.test.ts +188 -0
  40. package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
  41. package/src/__tests__/skill-mirror-parity.test.ts +1 -0
  42. package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
  43. package/src/__tests__/system-prompt.test.ts +1 -1
  44. package/src/__tests__/terminal-sandbox.test.ts +142 -9
  45. package/src/__tests__/terminal-tools.test.ts +2 -93
  46. package/src/__tests__/thread-seed-composer.test.ts +18 -0
  47. package/src/__tests__/tool-approval-handler.test.ts +350 -0
  48. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
  49. package/src/__tests__/voice-scoped-grant-consumer.test.ts +46 -84
  50. package/src/agent/loop.ts +36 -1
  51. package/src/approvals/approval-primitive.ts +381 -0
  52. package/src/approvals/guardian-decision-primitive.ts +191 -0
  53. package/src/calls/call-controller.ts +252 -209
  54. package/src/calls/call-domain.ts +44 -6
  55. package/src/calls/guardian-dispatch.ts +48 -0
  56. package/src/calls/types.ts +1 -1
  57. package/src/calls/voice-session-bridge.ts +46 -30
  58. package/src/cli/core-commands.ts +0 -4
  59. package/src/cli/mcp.ts +58 -0
  60. package/src/cli.ts +76 -34
  61. package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
  62. package/src/config/assistant-feature-flags.ts +162 -0
  63. package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
  64. package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
  65. package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
  66. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  67. package/src/config/bundled-skills/reminder/SKILL.md +49 -2
  68. package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
  69. package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
  70. package/src/config/core-schema.ts +1 -1
  71. package/src/config/env-registry.ts +10 -0
  72. package/src/config/feature-flag-registry.json +61 -0
  73. package/src/config/loader.ts +22 -1
  74. package/src/config/mcp-schema.ts +46 -0
  75. package/src/config/sandbox-schema.ts +0 -39
  76. package/src/config/schema.ts +18 -2
  77. package/src/config/skill-state.ts +34 -0
  78. package/src/config/skills-schema.ts +0 -1
  79. package/src/config/skills.ts +9 -0
  80. package/src/config/system-prompt.ts +110 -46
  81. package/src/config/templates/SOUL.md +1 -1
  82. package/src/config/types.ts +19 -1
  83. package/src/config/vellum-skills/catalog.json +1 -1
  84. package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
  85. package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
  86. package/src/config/vellum-skills/telegram-setup/SKILL.md +6 -5
  87. package/src/config/vellum-skills/trusted-contacts/SKILL.md +105 -3
  88. package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
  89. package/src/daemon/config-watcher.ts +0 -1
  90. package/src/daemon/daemon-control.ts +1 -1
  91. package/src/daemon/guardian-invite-intent.ts +124 -0
  92. package/src/daemon/handlers/avatar.ts +68 -0
  93. package/src/daemon/handlers/browser.ts +2 -2
  94. package/src/daemon/handlers/guardian-actions.ts +120 -0
  95. package/src/daemon/handlers/index.ts +4 -0
  96. package/src/daemon/handlers/sessions.ts +19 -0
  97. package/src/daemon/handlers/shared.ts +3 -1
  98. package/src/daemon/install-cli-launchers.ts +58 -13
  99. package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
  100. package/src/daemon/ipc-contract/sessions.ts +8 -2
  101. package/src/daemon/ipc-contract/settings.ts +25 -2
  102. package/src/daemon/ipc-contract-inventory.json +10 -0
  103. package/src/daemon/ipc-contract.ts +4 -0
  104. package/src/daemon/lifecycle.ts +14 -2
  105. package/src/daemon/main.ts +1 -0
  106. package/src/daemon/providers-setup.ts +26 -1
  107. package/src/daemon/server.ts +1 -0
  108. package/src/daemon/session-lifecycle.ts +52 -7
  109. package/src/daemon/session-memory.ts +45 -0
  110. package/src/daemon/session-process.ts +258 -432
  111. package/src/daemon/session-runtime-assembly.ts +12 -0
  112. package/src/daemon/session-skill-tools.ts +14 -1
  113. package/src/daemon/session-tool-setup.ts +5 -0
  114. package/src/daemon/session.ts +11 -0
  115. package/src/daemon/shutdown-handlers.ts +11 -0
  116. package/src/daemon/tool-side-effects.ts +35 -9
  117. package/src/index.ts +2 -2
  118. package/src/mcp/client.ts +152 -0
  119. package/src/mcp/manager.ts +139 -0
  120. package/src/memory/conversation-display-order-migration.ts +44 -0
  121. package/src/memory/conversation-queries.ts +2 -0
  122. package/src/memory/conversation-store.ts +91 -0
  123. package/src/memory/db-init.ts +5 -1
  124. package/src/memory/embedding-local.ts +13 -8
  125. package/src/memory/guardian-action-store.ts +125 -2
  126. package/src/memory/ingress-invite-store.ts +95 -1
  127. package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
  128. package/src/memory/migrations/index.ts +2 -1
  129. package/src/memory/schema.ts +5 -1
  130. package/src/memory/scoped-approval-grants.ts +14 -5
  131. package/src/messaging/providers/slack/client.ts +12 -0
  132. package/src/messaging/providers/slack/types.ts +5 -0
  133. package/src/notifications/decision-engine.ts +49 -12
  134. package/src/notifications/emit-signal.ts +7 -0
  135. package/src/notifications/signal.ts +7 -0
  136. package/src/notifications/thread-seed-composer.ts +2 -1
  137. package/src/runtime/channel-approval-types.ts +16 -6
  138. package/src/runtime/channel-approvals.ts +19 -15
  139. package/src/runtime/channel-invite-transport.ts +85 -0
  140. package/src/runtime/channel-invite-transports/telegram.ts +105 -0
  141. package/src/runtime/guardian-action-grant-minter.ts +92 -35
  142. package/src/runtime/guardian-action-message-composer.ts +30 -0
  143. package/src/runtime/guardian-decision-types.ts +91 -0
  144. package/src/runtime/http-server.ts +23 -1
  145. package/src/runtime/ingress-service.ts +22 -0
  146. package/src/runtime/invite-redemption-service.ts +181 -0
  147. package/src/runtime/invite-redemption-templates.ts +39 -0
  148. package/src/runtime/routes/call-routes.ts +2 -1
  149. package/src/runtime/routes/guardian-action-routes.ts +206 -0
  150. package/src/runtime/routes/guardian-approval-interception.ts +66 -190
  151. package/src/runtime/routes/identity-routes.ts +73 -0
  152. package/src/runtime/routes/inbound-message-handler.ts +486 -394
  153. package/src/runtime/routes/pairing-routes.ts +4 -0
  154. package/src/security/encrypted-store.ts +31 -17
  155. package/src/security/keychain.ts +176 -2
  156. package/src/security/secure-keys.ts +97 -0
  157. package/src/security/tool-approval-digest.ts +1 -1
  158. package/src/tools/browser/browser-execution.ts +2 -2
  159. package/src/tools/browser/browser-manager.ts +46 -32
  160. package/src/tools/browser/browser-screencast.ts +2 -2
  161. package/src/tools/calls/call-start.ts +1 -1
  162. package/src/tools/executor.ts +22 -17
  163. package/src/tools/mcp/mcp-tool-factory.ts +100 -0
  164. package/src/tools/network/script-proxy/session-manager.ts +1 -5
  165. package/src/tools/registry.ts +64 -1
  166. package/src/tools/skills/load.ts +22 -8
  167. package/src/tools/system/avatar-generator.ts +119 -0
  168. package/src/tools/system/navigate-settings.ts +65 -0
  169. package/src/tools/system/open-system-settings.ts +75 -0
  170. package/src/tools/system/voice-config.ts +121 -32
  171. package/src/tools/terminal/backends/native.ts +40 -19
  172. package/src/tools/terminal/backends/types.ts +3 -3
  173. package/src/tools/terminal/parser.ts +1 -1
  174. package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
  175. package/src/tools/terminal/sandbox.ts +1 -12
  176. package/src/tools/terminal/shell.ts +3 -31
  177. package/src/tools/tool-approval-handler.ts +141 -3
  178. package/src/tools/tool-manifest.ts +6 -0
  179. package/src/tools/types.ts +10 -2
  180. package/src/util/diff.ts +36 -13
  181. package/Dockerfile.sandbox +0 -5
  182. package/src/__tests__/doordash-client.test.ts +0 -187
  183. package/src/__tests__/doordash-session.test.ts +0 -154
  184. package/src/__tests__/signup-e2e.test.ts +0 -354
  185. package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
  186. package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
  187. package/src/cli/doordash.ts +0 -1057
  188. package/src/config/bundled-skills/doordash/SKILL.md +0 -163
  189. package/src/config/templates/LOOKS.md +0 -25
  190. package/src/doordash/cart-queries.ts +0 -787
  191. package/src/doordash/client.ts +0 -1016
  192. package/src/doordash/order-queries.ts +0 -85
  193. package/src/doordash/queries.ts +0 -13
  194. package/src/doordash/query-extractor.ts +0 -94
  195. package/src/doordash/search-queries.ts +0 -203
  196. package/src/doordash/session.ts +0 -84
  197. package/src/doordash/store-queries.ts +0 -246
  198. package/src/doordash/types.ts +0 -367
  199. package/src/tools/terminal/backends/docker.ts +0 -379
@@ -0,0 +1,264 @@
1
+ import { beforeEach, describe, expect, mock, test } from 'bun:test';
2
+
3
+ // Mock the credential metadata store so the Telegram adapter can resolve
4
+ // the bot username without touching the filesystem.
5
+ let mockBotUsername: string | undefined = 'test_invite_bot';
6
+ mock.module('../tools/credentials/metadata-store.js', () => ({
7
+ getCredentialMetadata: (service: string, field: string) => {
8
+ if (service === 'telegram' && field === 'bot_token' && mockBotUsername) {
9
+ return { accountInfo: mockBotUsername };
10
+ }
11
+ return undefined;
12
+ },
13
+ upsertCredentialMetadata: () => {},
14
+ deleteCredentialMetadata: () => {},
15
+ listCredentialMetadata: () => [],
16
+ }));
17
+
18
+ import {
19
+ _resetRegistry,
20
+ type ChannelInviteTransport,
21
+ getTransport,
22
+ registerTransport,
23
+ } from '../runtime/channel-invite-transport.js';
24
+ // Importing the Telegram module auto-registers the transport
25
+ import { telegramInviteTransport } from '../runtime/channel-invite-transports/telegram.js';
26
+
27
+ describe('channel-invite-transport', () => {
28
+ beforeEach(() => {
29
+ _resetRegistry();
30
+ mockBotUsername = 'test_invite_bot';
31
+ // Re-register after reset so Telegram tests work
32
+ registerTransport(telegramInviteTransport);
33
+ });
34
+
35
+ // =========================================================================
36
+ // Registry
37
+ // =========================================================================
38
+
39
+ describe('registry', () => {
40
+ test('returns the Telegram transport for telegram channel', () => {
41
+ const transport = getTransport('telegram');
42
+ expect(transport).toBeDefined();
43
+ expect(transport!.channel).toBe('telegram');
44
+ });
45
+
46
+ test('returns undefined for an unregistered channel', () => {
47
+ const transport = getTransport('sms');
48
+ expect(transport).toBeUndefined();
49
+ });
50
+
51
+ test('overwrites a previously registered transport for the same channel', () => {
52
+ const custom: ChannelInviteTransport = {
53
+ channel: 'telegram',
54
+ buildShareableInvite: () => ({ url: 'custom', displayText: 'custom' }),
55
+ extractInboundToken: () => undefined,
56
+ };
57
+ registerTransport(custom);
58
+ const transport = getTransport('telegram');
59
+ expect(transport!.buildShareableInvite({ rawToken: 'x', sourceChannel: 'telegram' }).url).toBe('custom');
60
+ });
61
+
62
+ test('_resetRegistry clears all transports', () => {
63
+ _resetRegistry();
64
+ expect(getTransport('telegram')).toBeUndefined();
65
+ });
66
+ });
67
+
68
+ // =========================================================================
69
+ // Telegram adapter — buildShareableInvite
70
+ // =========================================================================
71
+
72
+ describe('telegram buildShareableInvite', () => {
73
+ test('produces a valid Telegram deep link', () => {
74
+ const result = telegramInviteTransport.buildShareableInvite({
75
+ rawToken: 'abc123_test-token',
76
+ sourceChannel: 'telegram',
77
+ });
78
+
79
+ expect(result.url).toBe('https://t.me/test_invite_bot?start=iv_abc123_test-token');
80
+ expect(result.displayText).toContain('https://t.me/test_invite_bot?start=iv_abc123_test-token');
81
+ });
82
+
83
+ test('deep link is deterministic for the same token', () => {
84
+ const a = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
85
+ const b = telegramInviteTransport.buildShareableInvite({ rawToken: 'tok1', sourceChannel: 'telegram' });
86
+ expect(a.url).toBe(b.url);
87
+ expect(a.displayText).toBe(b.displayText);
88
+ });
89
+
90
+ test('uses the configured bot username', () => {
91
+ mockBotUsername = 'my_custom_bot';
92
+ const result = telegramInviteTransport.buildShareableInvite({
93
+ rawToken: 'token',
94
+ sourceChannel: 'telegram',
95
+ });
96
+ expect(result.url).toBe('https://t.me/my_custom_bot?start=iv_token');
97
+ });
98
+
99
+ test('throws when bot username is not configured', () => {
100
+ mockBotUsername = undefined;
101
+ // Also clear the env var to ensure no fallback
102
+ const prev = process.env.TELEGRAM_BOT_USERNAME;
103
+ delete process.env.TELEGRAM_BOT_USERNAME;
104
+ try {
105
+ expect(() =>
106
+ telegramInviteTransport.buildShareableInvite({
107
+ rawToken: 'token',
108
+ sourceChannel: 'telegram',
109
+ }),
110
+ ).toThrow('bot username is not configured');
111
+ } finally {
112
+ if (prev !== undefined) process.env.TELEGRAM_BOT_USERNAME = prev;
113
+ }
114
+ });
115
+
116
+ test('falls back to TELEGRAM_BOT_USERNAME env var', () => {
117
+ mockBotUsername = undefined;
118
+ const prev = process.env.TELEGRAM_BOT_USERNAME;
119
+ process.env.TELEGRAM_BOT_USERNAME = 'env_bot';
120
+ try {
121
+ const result = telegramInviteTransport.buildShareableInvite({
122
+ rawToken: 'token',
123
+ sourceChannel: 'telegram',
124
+ });
125
+ expect(result.url).toBe('https://t.me/env_bot?start=iv_token');
126
+ } finally {
127
+ if (prev !== undefined) {
128
+ process.env.TELEGRAM_BOT_USERNAME = prev;
129
+ } else {
130
+ delete process.env.TELEGRAM_BOT_USERNAME;
131
+ }
132
+ }
133
+ });
134
+ });
135
+
136
+ // =========================================================================
137
+ // Telegram adapter — extractInboundToken
138
+ // =========================================================================
139
+
140
+ describe('telegram extractInboundToken', () => {
141
+ test('extracts token from structured commandIntent', () => {
142
+ const token = telegramInviteTransport.extractInboundToken({
143
+ commandIntent: { type: 'start', payload: 'iv_abc123' },
144
+ content: '/start iv_abc123',
145
+ });
146
+ expect(token).toBe('abc123');
147
+ });
148
+
149
+ test('extracts base64url token from commandIntent', () => {
150
+ const token = telegramInviteTransport.extractInboundToken({
151
+ commandIntent: { type: 'start', payload: 'iv_YWJjMTIz-_test' },
152
+ content: '/start iv_YWJjMTIz-_test',
153
+ });
154
+ expect(token).toBe('YWJjMTIz-_test');
155
+ });
156
+
157
+ test('returns undefined when commandIntent has no payload', () => {
158
+ const token = telegramInviteTransport.extractInboundToken({
159
+ commandIntent: { type: 'start' },
160
+ content: '/start',
161
+ });
162
+ expect(token).toBeUndefined();
163
+ });
164
+
165
+ test('returns undefined when commandIntent payload has wrong prefix (gv_)', () => {
166
+ const token = telegramInviteTransport.extractInboundToken({
167
+ commandIntent: { type: 'start', payload: 'gv_abc123' },
168
+ content: '/start gv_abc123',
169
+ });
170
+ expect(token).toBeUndefined();
171
+ });
172
+
173
+ test('returns undefined when commandIntent payload has no prefix', () => {
174
+ const token = telegramInviteTransport.extractInboundToken({
175
+ commandIntent: { type: 'start', payload: 'abc123' },
176
+ content: '/start abc123',
177
+ });
178
+ expect(token).toBeUndefined();
179
+ });
180
+
181
+ test('returns undefined when commandIntent type is not start', () => {
182
+ const token = telegramInviteTransport.extractInboundToken({
183
+ commandIntent: { type: 'help', payload: 'iv_abc123' },
184
+ content: '/help iv_abc123',
185
+ });
186
+ expect(token).toBeUndefined();
187
+ });
188
+
189
+ test('returns undefined when commandIntent payload is iv_ with empty token', () => {
190
+ const token = telegramInviteTransport.extractInboundToken({
191
+ commandIntent: { type: 'start', payload: 'iv_' },
192
+ content: '/start iv_',
193
+ });
194
+ expect(token).toBeUndefined();
195
+ });
196
+
197
+ test('returns undefined when commandIntent payload is iv_ with whitespace-only token', () => {
198
+ const token = telegramInviteTransport.extractInboundToken({
199
+ commandIntent: { type: 'start', payload: 'iv_ ' },
200
+ content: '/start iv_ ',
201
+ });
202
+ expect(token).toBeUndefined();
203
+ });
204
+
205
+ test('extracts token from raw content fallback', () => {
206
+ const token = telegramInviteTransport.extractInboundToken({
207
+ content: '/start iv_abc123',
208
+ });
209
+ expect(token).toBe('abc123');
210
+ });
211
+
212
+ test('extracts token from raw content with extra whitespace', () => {
213
+ const token = telegramInviteTransport.extractInboundToken({
214
+ content: '/start iv_token123',
215
+ });
216
+ expect(token).toBe('token123');
217
+ });
218
+
219
+ test('returns undefined for empty content', () => {
220
+ const token = telegramInviteTransport.extractInboundToken({
221
+ content: '',
222
+ });
223
+ expect(token).toBeUndefined();
224
+ });
225
+
226
+ test('returns undefined for content without /start', () => {
227
+ const token = telegramInviteTransport.extractInboundToken({
228
+ content: 'hello world',
229
+ });
230
+ expect(token).toBeUndefined();
231
+ });
232
+
233
+ test('returns undefined for /start without iv_ prefix in content', () => {
234
+ const token = telegramInviteTransport.extractInboundToken({
235
+ content: '/start gv_abc123',
236
+ });
237
+ expect(token).toBeUndefined();
238
+ });
239
+
240
+ test('returns undefined for malformed /start with only iv_ in content', () => {
241
+ const token = telegramInviteTransport.extractInboundToken({
242
+ content: '/start iv_',
243
+ });
244
+ expect(token).toBeUndefined();
245
+ });
246
+
247
+ test('prefers commandIntent over raw content', () => {
248
+ const token = telegramInviteTransport.extractInboundToken({
249
+ commandIntent: { type: 'start', payload: 'iv_from_intent' },
250
+ content: '/start iv_from_content',
251
+ });
252
+ expect(token).toBe('from_intent');
253
+ });
254
+
255
+ test('returns undefined when commandIntent rejects, even if content has token', () => {
256
+ // commandIntent present but payload has wrong prefix
257
+ const token = telegramInviteTransport.extractInboundToken({
258
+ commandIntent: { type: 'start', payload: 'gv_abc123' },
259
+ content: '/start iv_valid_token',
260
+ });
261
+ expect(token).toBeUndefined();
262
+ });
263
+ });
264
+ });
@@ -1,6 +1,10 @@
1
1
  import { describe, expect,test } from 'bun:test';
2
2
 
3
- import { sanitizeUrlForDisplay } from '../cli.js';
3
+ import {
4
+ formatConfirmationCommandPreview,
5
+ formatConfirmationInputLines,
6
+ sanitizeUrlForDisplay,
7
+ } from '../cli.js';
4
8
 
5
9
  describe('sanitizeUrlForDisplay', () => {
6
10
  test('removes userinfo from absolute URLs', () => {
@@ -25,3 +29,40 @@ describe('sanitizeUrlForDisplay', () => {
25
29
  expect(sanitizeUrlForDisplay(rawValue)).toBe('not-a-url //[REDACTED]@example.com');
26
30
  });
27
31
  });
32
+
33
+ describe('formatConfirmationInputLines', () => {
34
+ test('preserves full old_string and new_string values without truncation', () => {
35
+ const oldString = 'old '.repeat(120);
36
+ const newString = 'new '.repeat(120);
37
+ const lines = formatConfirmationInputLines({
38
+ old_string: oldString,
39
+ new_string: newString,
40
+ });
41
+
42
+ expect(lines).toContain(`old_string: ${oldString}`);
43
+ expect(lines).toContain(`new_string: ${newString}`);
44
+ expect(lines.some((line) => line.includes('...'))).toBe(false);
45
+ });
46
+
47
+ test('preserves multiline values', () => {
48
+ const lines = formatConfirmationInputLines({
49
+ old_string: 'line1\nline2\nline3',
50
+ });
51
+
52
+ expect(lines).toEqual([
53
+ 'old_string: line1',
54
+ ' line2',
55
+ ' line3',
56
+ ]);
57
+ });
58
+ });
59
+
60
+ describe('formatConfirmationCommandPreview', () => {
61
+ test('shows concise file_edit preview', () => {
62
+ const preview = formatConfirmationCommandPreview({
63
+ toolName: 'file_edit',
64
+ input: { path: '/tmp/sample.txt' },
65
+ });
66
+ expect(preview).toBe('edit /tmp/sample.txt');
67
+ });
68
+ });
@@ -52,7 +52,6 @@ mock.module('../util/platform.js', () => ({
52
52
  }));
53
53
 
54
54
  import { buildElevenLabsVoiceSpec, resolveVoiceQualityProfile } from '../calls/voice-quality.js';
55
- import { DEFAULT_CONFIG } from '../config/defaults.js';
56
55
  import { invalidateConfigCache,loadConfig } from '../config/loader.js';
57
56
  import { AssistantConfigSchema } from '../config/schema.js';
58
57
  import { _setStorePath } from '../security/encrypted-store.js';
@@ -96,15 +95,6 @@ describe('AssistantConfigSchema', () => {
96
95
  });
97
96
  expect(result.sandbox).toEqual({
98
97
  enabled: true,
99
- backend: 'docker',
100
- docker: {
101
- image: 'vellum-sandbox:latest',
102
- shell: 'bash',
103
- cpus: 1,
104
- memoryMb: 512,
105
- pidsLimit: 256,
106
- network: 'none',
107
- },
108
98
  });
109
99
  expect(result.rateLimit).toEqual({ maxRequestsPerMinute: 0, maxTokensPerSession: 0 });
110
100
  expect(result.secretDetection).toEqual({ enabled: true, action: 'redact', entropyThreshold: 4.0, allowOneTimeSend: false, blockIngress: true });
@@ -393,113 +383,22 @@ describe('AssistantConfigSchema', () => {
393
383
  }
394
384
  });
395
385
 
396
- // SANDBOX M11 cutover: Docker is now the default backend for stronger
397
- // container-level isolation. Native is available as opt-in fallback.
398
- test('default sandbox backend is docker', () => {
399
- const result = AssistantConfigSchema.parse({});
400
- expect(result.sandbox.backend).toBe('docker');
401
- });
402
-
403
- test('DEFAULT_CONFIG sandbox backend is docker', () => {
404
- expect(DEFAULT_CONFIG.sandbox.backend).toBe('docker');
405
- });
406
-
407
- test('backward compatibility: sandbox with only enabled still parses', () => {
386
+ test('sandbox with only enabled still parses', () => {
408
387
  const result = AssistantConfigSchema.parse({ sandbox: { enabled: false } });
409
388
  expect(result.sandbox.enabled).toBe(false);
410
- expect(result.sandbox.backend).toBe('docker');
411
- expect(result.sandbox.docker.memoryMb).toBe(512);
412
389
  });
413
390
 
414
- test('accepts docker backend with custom limits', () => {
415
- const result = AssistantConfigSchema.parse({
416
- sandbox: {
417
- enabled: true,
418
- backend: 'docker',
419
- docker: {
420
- image: 'ubuntu:22.04',
421
- cpus: 2,
422
- memoryMb: 1024,
423
- pidsLimit: 512,
424
- network: 'bridge',
425
- },
426
- },
427
- });
428
- expect(result.sandbox.backend).toBe('docker');
429
- expect(result.sandbox.docker.image).toBe('ubuntu:22.04');
430
- expect(result.sandbox.docker.cpus).toBe(2);
431
- expect(result.sandbox.docker.memoryMb).toBe(1024);
432
- expect(result.sandbox.docker.pidsLimit).toBe(512);
433
- expect(result.sandbox.docker.network).toBe('bridge');
434
- });
435
-
436
- test('applies docker defaults when backend is docker but docker config omitted', () => {
437
- const result = AssistantConfigSchema.parse({
438
- sandbox: { backend: 'docker' },
439
- });
440
- expect(result.sandbox.backend).toBe('docker');
441
- expect(result.sandbox.docker.cpus).toBe(1);
442
- expect(result.sandbox.docker.memoryMb).toBe(512);
443
- expect(result.sandbox.docker.pidsLimit).toBe(256);
444
- expect(result.sandbox.docker.network).toBe('none');
445
- });
446
-
447
- test('accepts partial docker config with defaults for missing fields', () => {
448
- const result = AssistantConfigSchema.parse({
449
- sandbox: {
450
- backend: 'docker',
451
- docker: { memoryMb: 2048 },
452
- },
453
- });
454
- expect(result.sandbox.docker.memoryMb).toBe(2048);
455
- expect(result.sandbox.docker.cpus).toBe(1);
456
- expect(result.sandbox.docker.pidsLimit).toBe(256);
457
- expect(result.sandbox.docker.network).toBe('none');
458
- });
459
-
460
- test('rejects invalid sandbox.backend', () => {
391
+ test('rejects unknown sandbox fields', () => {
461
392
  const result = AssistantConfigSchema.safeParse({
462
- sandbox: { backend: 'podman' },
463
- });
464
- expect(result.success).toBe(false);
465
- if (!result.success) {
466
- const msgs = result.error.issues.map(i => i.message);
467
- expect(msgs.some(m => m.includes('sandbox.backend'))).toBe(true);
468
- }
469
- });
470
-
471
- test('rejects invalid docker.network', () => {
472
- const result = AssistantConfigSchema.safeParse({
473
- sandbox: { docker: { network: 'host' } },
393
+ sandbox: { backend: 'docker' },
474
394
  });
475
- expect(result.success).toBe(false);
476
- if (!result.success) {
477
- const msgs = result.error.issues.map(i => i.message);
478
- expect(msgs.some(m => m.includes('sandbox.docker.network'))).toBe(true);
395
+ // Unknown keys are stripped by Zod passthrough/strip, so parse should still succeed
396
+ // but the unknown field should not appear in the output
397
+ if (result.success) {
398
+ expect((result.data.sandbox as Record<string, unknown>)['backend']).toBeUndefined();
479
399
  }
480
400
  });
481
401
 
482
- test('rejects non-positive docker.memoryMb', () => {
483
- const result = AssistantConfigSchema.safeParse({
484
- sandbox: { docker: { memoryMb: 0 } },
485
- });
486
- expect(result.success).toBe(false);
487
- });
488
-
489
- test('rejects non-integer docker.pidsLimit', () => {
490
- const result = AssistantConfigSchema.safeParse({
491
- sandbox: { docker: { pidsLimit: 3.5 } },
492
- });
493
- expect(result.success).toBe(false);
494
- });
495
-
496
- test('rejects negative docker.cpus', () => {
497
- const result = AssistantConfigSchema.safeParse({
498
- sandbox: { docker: { cpus: -1 } },
499
- });
500
- expect(result.success).toBe(false);
501
- });
502
-
503
402
  test('defaults permissions.mode to workspace', () => {
504
403
  const result = AssistantConfigSchema.parse({});
505
404
  expect(result.permissions).toEqual({ mode: 'workspace' });
@@ -1277,28 +1176,13 @@ describe('loadConfig with schema validation', () => {
1277
1176
  writeConfig({ sandbox: { enabled: false } });
1278
1177
  const config = loadConfig();
1279
1178
  expect(config.sandbox.enabled).toBe(false);
1280
- expect(config.sandbox.backend).toBe('docker');
1281
- expect(config.sandbox.docker.memoryMb).toBe(512);
1282
1179
  });
1283
1180
 
1284
- test('loads sandbox docker backend config', () => {
1285
- writeConfig({
1286
- sandbox: {
1287
- backend: 'docker',
1288
- docker: { memoryMb: 2048, network: 'bridge' },
1289
- },
1290
- });
1181
+ test('strips unknown sandbox fields', () => {
1182
+ writeConfig({ sandbox: { enabled: true, backend: 'docker' } });
1291
1183
  const config = loadConfig();
1292
- expect(config.sandbox.backend).toBe('docker');
1293
- expect(config.sandbox.docker.memoryMb).toBe(2048);
1294
- expect(config.sandbox.docker.network).toBe('bridge');
1295
- expect(config.sandbox.docker.cpus).toBe(1);
1296
- });
1297
-
1298
- test('falls back for invalid sandbox.backend', () => {
1299
- writeConfig({ sandbox: { backend: 'podman' } });
1300
- const config = loadConfig();
1301
- expect(config.sandbox.backend).toBe('docker');
1184
+ expect(config.sandbox.enabled).toBe(true);
1185
+ expect('backend' in config.sandbox).toBe(false);
1302
1186
  });
1303
1187
 
1304
1188
  test('falls back for invalid contextWindow relationship', () => {
@@ -193,14 +193,6 @@ describe('ConfigWatcher workspace file handlers', () => {
193
193
  expect(evictCallCount).toBe(1);
194
194
  });
195
195
 
196
- test('LOOKS.md change triggers onSessionEvict', async () => {
197
- watcher.start(onSessionEvict);
198
- simulateFileChange(WORKSPACE_DIR, 'LOOKS.md');
199
-
200
- await new Promise((r) => setTimeout(r, 300));
201
- expect(evictCallCount).toBe(1);
202
- });
203
-
204
196
  test('UPDATES.md change triggers onSessionEvict', async () => {
205
197
  watcher.start(onSessionEvict);
206
198
  simulateFileChange(WORKSPACE_DIR, 'UPDATES.md');
@@ -116,6 +116,7 @@ class MockSession {
116
116
  }
117
117
 
118
118
  async loadFromDb(): Promise<void> {}
119
+ async ensureActorScopedHistory(): Promise<void> {}
119
120
  updateClient(): void {}
120
121
  setSandboxOverride(): void {}
121
122
  isProcessing(): boolean { return this.processing; }
@@ -41,6 +41,7 @@ class MockSession {
41
41
  public readonly conversationId: string;
42
42
  public memoryPolicy: MockMemoryPolicy;
43
43
  public updateClientCalls = 0;
44
+ public ensureActorScopedHistoryCalls = 0;
44
45
  public lastUpdateClientHasNoClient: boolean | undefined;
45
46
  public lastUpdateClientSender: ((msg: Record<string, unknown>) => void) | undefined;
46
47
  public lastRunAgentLoopOptions: { skipPreMessageRollback?: boolean; isInteractive?: boolean } | undefined;
@@ -69,6 +70,10 @@ class MockSession {
69
70
 
70
71
  async loadFromDb(): Promise<void> {}
71
72
 
73
+ async ensureActorScopedHistory(): Promise<void> {
74
+ this.ensureActorScopedHistoryCalls += 1;
75
+ }
76
+
72
77
  updateClient(sender?: (msg: Record<string, unknown>) => void, hasNoClient = false): void {
73
78
  this.updateClientCalls += 1;
74
79
  this.lastUpdateClientSender = sender;
@@ -165,8 +170,8 @@ class MockSession {
165
170
  }
166
171
  }
167
172
 
168
- // Mock child_process to prevent getScreenDimensions() from running Swift on Linux CI
169
- // where CoreGraphics is not available and the execSync call hangs for 10s.
173
+ // Mock child_process to prevent getScreenDimensions() from running osascript on Linux CI
174
+ // where AppKit/NSScreen is not available and the execSync call would fail.
170
175
  mock.module('node:child_process', () => ({
171
176
  execSync: () => '1920x1080',
172
177
  execFileSync: () => '',
@@ -634,6 +639,7 @@ describe('DaemonServer initial session hydration', () => {
634
639
  const session = internal.sessions.get(conversation.id);
635
640
  expect(session).toBeDefined();
636
641
  expect(session!.lastRunAgentLoopOptions?.isInteractive).toBe(true);
642
+ expect(session!.ensureActorScopedHistoryCalls).toBeGreaterThanOrEqual(1);
637
643
 
638
644
  // Verify the session was marked interactive during the loop, then restored.
639
645
  // updateClientHistory: [0] = initial no-socket creation (hasNoClient: true),
@@ -91,6 +91,18 @@ describe('formatDiff', () => {
91
91
  expect(result).toContain('-old content');
92
92
  expect(result).toContain('-line 2');
93
93
  });
94
+
95
+ test('uses a full fallback diff for oversized files without truncation markers', () => {
96
+ const old = Array.from({ length: 6 }, (_, i) => `old-${i + 1}`).join('\n');
97
+ const updated = Array.from({ length: 6 }, (_, i) => (i === 3 ? 'new-4' : `old-${i + 1}`)).join('\n');
98
+ const result = stripAnsi(formatDiff(old, updated, 'oversized.ts', { maxExactLines: 2 }));
99
+
100
+ expect(result).toContain('--- a/oversized.ts');
101
+ expect(result).toContain('+++ b/oversized.ts');
102
+ expect(result).toContain('-old-4');
103
+ expect(result).toContain('+new-4');
104
+ expect(result).not.toContain('Diff too large to display');
105
+ });
94
106
  });
95
107
 
96
108
  describe('formatNewFileDiff', () => {
@@ -119,4 +131,14 @@ describe('formatNewFileDiff', () => {
119
131
  const result = stripAnsi(formatNewFileDiff(content, 'small.ts'));
120
132
  expect(result).not.toContain('more lines');
121
133
  });
134
+
135
+ test('allows unbounded output when maxLines is null', () => {
136
+ const lines = Array.from({ length: 50 }, (_, i) => `line${i + 1}`);
137
+ const content = lines.join('\n');
138
+ const result = stripAnsi(formatNewFileDiff(content, 'all-lines.ts', null));
139
+
140
+ expect(result).toContain('+line1');
141
+ expect(result).toContain('+line50');
142
+ expect(result).not.toContain('more lines');
143
+ });
122
144
  });
@@ -23,11 +23,16 @@ const ALL_SCENARIOS: GuardianActionMessageScenario[] = [
23
23
  'guardian_followup_failed',
24
24
  'guardian_followup_declined_ack',
25
25
  'guardian_followup_clarification',
26
+ 'guardian_pending_disambiguation',
26
27
  'guardian_expired_disambiguation',
27
28
  'guardian_followup_disambiguation',
28
29
  'guardian_stale_answered',
29
30
  'guardian_stale_expired',
30
31
  'guardian_stale_followup',
32
+ 'guardian_stale_superseded',
33
+ 'guardian_superseded_remap',
34
+ 'guardian_unknown_code',
35
+ 'guardian_auto_matched',
31
36
  'outbound_message_copy',
32
37
  'followup_message_sent',
33
38
  'followup_call_started',