vellum 0.2.13 → 0.2.14

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 (207) hide show
  1. package/README.md +32 -0
  2. package/bun.lock +2 -2
  3. package/docs/skills.md +4 -4
  4. package/package.json +2 -2
  5. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
  6. package/src/__tests__/app-git-history.test.ts +176 -0
  7. package/src/__tests__/app-git-service.test.ts +169 -0
  8. package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
  9. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
  10. package/src/__tests__/browser-skill-endstate.test.ts +6 -6
  11. package/src/__tests__/call-bridge.test.ts +105 -13
  12. package/src/__tests__/call-domain.test.ts +163 -0
  13. package/src/__tests__/call-orchestrator.test.ts +113 -0
  14. package/src/__tests__/call-routes-http.test.ts +246 -6
  15. package/src/__tests__/channel-approval-routes.test.ts +438 -0
  16. package/src/__tests__/channel-approval.test.ts +266 -0
  17. package/src/__tests__/channel-approvals.test.ts +393 -0
  18. package/src/__tests__/channel-delivery-store.test.ts +447 -0
  19. package/src/__tests__/checker.test.ts +607 -1048
  20. package/src/__tests__/cli.test.ts +1 -56
  21. package/src/__tests__/config-schema.test.ts +137 -18
  22. package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
  23. package/src/__tests__/conflict-policy.test.ts +121 -0
  24. package/src/__tests__/conflict-store.test.ts +2 -0
  25. package/src/__tests__/contacts-tools.test.ts +3 -3
  26. package/src/__tests__/contradiction-checker.test.ts +99 -1
  27. package/src/__tests__/credential-security-invariants.test.ts +22 -6
  28. package/src/__tests__/credential-vault-unit.test.ts +780 -0
  29. package/src/__tests__/elevenlabs-client.test.ts +62 -0
  30. package/src/__tests__/ephemeral-permissions.test.ts +73 -23
  31. package/src/__tests__/filesystem-tools.test.ts +579 -0
  32. package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
  33. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
  34. package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
  35. package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
  36. package/src/__tests__/handlers-slack-config.test.ts +2 -1
  37. package/src/__tests__/handlers-telegram-config.test.ts +855 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +141 -1
  39. package/src/__tests__/hooks-runner.test.ts +6 -2
  40. package/src/__tests__/host-file-edit-tool.test.ts +124 -0
  41. package/src/__tests__/host-file-read-tool.test.ts +62 -0
  42. package/src/__tests__/host-file-write-tool.test.ts +59 -0
  43. package/src/__tests__/host-shell-tool.test.ts +251 -0
  44. package/src/__tests__/ingress-reconcile.test.ts +581 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +100 -41
  46. package/src/__tests__/ipc-validate.test.ts +50 -0
  47. package/src/__tests__/key-migration.test.ts +23 -0
  48. package/src/__tests__/memory-regressions.test.ts +99 -0
  49. package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
  50. package/src/__tests__/oauth-callback-registry.test.ts +11 -4
  51. package/src/__tests__/playbook-execution.test.ts +502 -0
  52. package/src/__tests__/playbook-tools.test.ts +4 -6
  53. package/src/__tests__/public-ingress-urls.test.ts +34 -0
  54. package/src/__tests__/qdrant-manager.test.ts +267 -0
  55. package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
  56. package/src/__tests__/recurrence-engine.test.ts +9 -0
  57. package/src/__tests__/recurrence-types.test.ts +8 -0
  58. package/src/__tests__/registry.test.ts +1 -1
  59. package/src/__tests__/runtime-runs.test.ts +1 -25
  60. package/src/__tests__/schedule-store.test.ts +16 -14
  61. package/src/__tests__/schedule-tools.test.ts +83 -0
  62. package/src/__tests__/scheduler-recurrence.test.ts +111 -10
  63. package/src/__tests__/secret-allowlist.test.ts +18 -17
  64. package/src/__tests__/secret-ingress-handler.test.ts +11 -0
  65. package/src/__tests__/secret-scanner.test.ts +43 -0
  66. package/src/__tests__/session-conflict-gate.test.ts +442 -6
  67. package/src/__tests__/session-init.benchmark.test.ts +3 -0
  68. package/src/__tests__/session-process-bridge.test.ts +242 -0
  69. package/src/__tests__/session-skill-tools.test.ts +1 -1
  70. package/src/__tests__/shell-identity.test.ts +256 -0
  71. package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
  72. package/src/__tests__/subagent-tools.test.ts +637 -54
  73. package/src/__tests__/task-management-tools.test.ts +936 -0
  74. package/src/__tests__/task-runner.test.ts +2 -2
  75. package/src/__tests__/terminal-tools.test.ts +840 -0
  76. package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
  77. package/src/__tests__/tool-executor.test.ts +85 -151
  78. package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
  79. package/src/__tests__/trust-store.test.ts +27 -453
  80. package/src/__tests__/twilio-provider.test.ts +153 -3
  81. package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
  82. package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
  83. package/src/__tests__/twilio-routes.test.ts +17 -262
  84. package/src/__tests__/twitter-auth-handler.test.ts +2 -1
  85. package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
  86. package/src/__tests__/twitter-cli-routing.test.ts +252 -0
  87. package/src/__tests__/twitter-oauth-client.test.ts +209 -0
  88. package/src/__tests__/workspace-policy.test.ts +213 -0
  89. package/src/calls/call-bridge.ts +92 -19
  90. package/src/calls/call-domain.ts +157 -5
  91. package/src/calls/call-orchestrator.ts +93 -7
  92. package/src/calls/call-store.ts +6 -0
  93. package/src/calls/elevenlabs-client.ts +8 -0
  94. package/src/calls/elevenlabs-config.ts +7 -5
  95. package/src/calls/twilio-provider.ts +91 -0
  96. package/src/calls/twilio-routes.ts +32 -37
  97. package/src/calls/types.ts +3 -1
  98. package/src/calls/voice-quality.ts +29 -7
  99. package/src/cli/twitter.ts +200 -21
  100. package/src/cli.ts +1 -20
  101. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
  102. package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
  103. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
  104. package/src/config/bundled-skills/messaging/SKILL.md +17 -2
  105. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
  106. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  107. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
  108. package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
  109. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
  110. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
  111. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
  112. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
  113. package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
  114. package/src/config/bundled-skills/twitter/SKILL.md +103 -17
  115. package/src/config/defaults.ts +10 -4
  116. package/src/config/schema.ts +80 -21
  117. package/src/config/types.ts +1 -0
  118. package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
  119. package/src/daemon/assistant-attachments.ts +4 -2
  120. package/src/daemon/handlers/apps.ts +69 -0
  121. package/src/daemon/handlers/config.ts +543 -24
  122. package/src/daemon/handlers/index.ts +1 -0
  123. package/src/daemon/handlers/sessions.ts +22 -6
  124. package/src/daemon/handlers/shared.ts +2 -1
  125. package/src/daemon/handlers/skills.ts +5 -20
  126. package/src/daemon/ipc-contract-inventory.json +28 -0
  127. package/src/daemon/ipc-contract.ts +168 -10
  128. package/src/daemon/ipc-validate.ts +17 -0
  129. package/src/daemon/lifecycle.ts +2 -0
  130. package/src/daemon/server.ts +78 -72
  131. package/src/daemon/session-attachments.ts +1 -1
  132. package/src/daemon/session-conflict-gate.ts +62 -6
  133. package/src/daemon/session-notifiers.ts +1 -1
  134. package/src/daemon/session-process.ts +62 -3
  135. package/src/daemon/session-tool-setup.ts +1 -2
  136. package/src/daemon/tls-certs.ts +189 -0
  137. package/src/daemon/video-thumbnail.ts +5 -3
  138. package/src/hooks/manager.ts +5 -9
  139. package/src/memory/app-git-service.ts +295 -0
  140. package/src/memory/app-store.ts +21 -0
  141. package/src/memory/conflict-intent.ts +47 -4
  142. package/src/memory/conflict-policy.ts +73 -0
  143. package/src/memory/conflict-store.ts +9 -1
  144. package/src/memory/contradiction-checker.ts +28 -0
  145. package/src/memory/conversation-key-store.ts +15 -0
  146. package/src/memory/db.ts +81 -0
  147. package/src/memory/embedding-local.ts +3 -13
  148. package/src/memory/external-conversation-store.ts +234 -0
  149. package/src/memory/job-handlers/conflict.ts +22 -2
  150. package/src/memory/jobs-worker.ts +67 -28
  151. package/src/memory/runs-store.ts +54 -7
  152. package/src/memory/schema.ts +20 -0
  153. package/src/messaging/provider.ts +9 -0
  154. package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
  155. package/src/messaging/providers/telegram-bot/client.ts +104 -0
  156. package/src/messaging/providers/telegram-bot/types.ts +15 -0
  157. package/src/messaging/registry.ts +1 -0
  158. package/src/permissions/checker.ts +48 -44
  159. package/src/permissions/prompter.ts +0 -4
  160. package/src/permissions/shell-identity.ts +227 -0
  161. package/src/permissions/trust-store.ts +76 -53
  162. package/src/permissions/types.ts +0 -19
  163. package/src/permissions/workspace-policy.ts +114 -0
  164. package/src/providers/retry.ts +12 -37
  165. package/src/runtime/assistant-event-hub.ts +41 -4
  166. package/src/runtime/channel-approval-parser.ts +60 -0
  167. package/src/runtime/channel-approval-types.ts +71 -0
  168. package/src/runtime/channel-approvals.ts +145 -0
  169. package/src/runtime/gateway-client.ts +16 -0
  170. package/src/runtime/http-server.ts +29 -9
  171. package/src/runtime/routes/call-routes.ts +52 -2
  172. package/src/runtime/routes/channel-routes.ts +296 -16
  173. package/src/runtime/routes/events-routes.ts +97 -28
  174. package/src/runtime/routes/run-routes.ts +2 -7
  175. package/src/runtime/run-orchestrator.ts +0 -3
  176. package/src/schedule/recurrence-engine.ts +26 -2
  177. package/src/schedule/recurrence-types.ts +1 -1
  178. package/src/schedule/schedule-store.ts +12 -3
  179. package/src/security/secret-scanner.ts +7 -0
  180. package/src/tasks/ephemeral-permissions.ts +0 -2
  181. package/src/tasks/task-scheduler.ts +2 -1
  182. package/src/tools/calls/call-start.ts +8 -0
  183. package/src/tools/execution-target.ts +21 -0
  184. package/src/tools/execution-timeout.ts +49 -0
  185. package/src/tools/executor.ts +6 -135
  186. package/src/tools/network/web-search.ts +9 -32
  187. package/src/tools/policy-context.ts +29 -0
  188. package/src/tools/schedule/update.ts +8 -1
  189. package/src/tools/terminal/parser.ts +16 -18
  190. package/src/tools/types.ts +4 -11
  191. package/src/twitter/oauth-client.ts +102 -0
  192. package/src/twitter/router.ts +101 -0
  193. package/src/util/debounce.ts +88 -0
  194. package/src/util/network-info.ts +47 -0
  195. package/src/util/platform.ts +29 -4
  196. package/src/util/promise-guard.ts +37 -0
  197. package/src/util/retry.ts +98 -0
  198. package/src/util/truncate.ts +1 -1
  199. package/src/workspace/git-service.ts +129 -112
  200. package/src/tools/contacts/contact-merge.ts +0 -55
  201. package/src/tools/contacts/contact-search.ts +0 -58
  202. package/src/tools/contacts/contact-upsert.ts +0 -64
  203. package/src/tools/playbooks/index.ts +0 -4
  204. package/src/tools/playbooks/playbook-create.ts +0 -96
  205. package/src/tools/playbooks/playbook-delete.ts +0 -52
  206. package/src/tools/playbooks/playbook-list.ts +0 -74
  207. package/src/tools/playbooks/playbook-update.ts +0 -111
@@ -2,13 +2,14 @@
2
2
  * Tests for gateway-only ingress enforcement in the runtime HTTP server.
3
3
  *
4
4
  * Verifies:
5
+ * - Runtime does not expose any Telegram webhook ingress routes
5
6
  * - Direct Twilio webhook routes return 410
6
7
  * - Internal forwarding routes (gateway→runtime) still work
7
8
  * - Relay WebSocket upgrade blocked for non-private-network origins (isPrivateNetworkOrigin)
8
9
  * - Relay WebSocket upgrade allowed from private network peers/origins
9
10
  * - Startup warning when RUNTIME_HTTP_HOST is not loopback
10
11
  */
11
- import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
12
+ import { describe, test, expect, beforeAll, afterAll, mock } from 'bun:test';
12
13
  import { mkdtempSync, realpathSync } from 'node:fs';
13
14
  import { tmpdir } from 'node:os';
14
15
  import { join } from 'node:path';
@@ -127,6 +128,15 @@ mock.module('../security/oauth-callback-registry.js', () => ({
127
128
  consumeCallbackError: () => true,
128
129
  }));
129
130
 
131
+ // Mock call-store so WebSocket close handlers don't hit the real DB
132
+ mock.module('../calls/call-store.js', () => ({
133
+ getCallSession: () => null,
134
+ getCallSessionByCallSid: () => null,
135
+ updateCallSession: () => {},
136
+ recordCallEvent: () => {},
137
+ expirePendingQuestions: () => {},
138
+ }));
139
+
130
140
  import { RuntimeHttpServer, isPrivateAddress } from '../runtime/http-server.js';
131
141
 
132
142
  // ---------------------------------------------------------------------------
@@ -148,8 +158,10 @@ describe('gateway-only ingress enforcement', () => {
148
158
  let server: RuntimeHttpServer;
149
159
  let port: number;
150
160
 
151
- beforeEach(async () => {
152
- logMessages.length = 0;
161
+ // Share a single server across all tests to avoid EADDRINUSE flakes from
162
+ // rapid port allocation/deallocation when creating a server per test.
163
+ // All tests are read-only (HTTP requests checking status codes) so sharing is safe.
164
+ beforeAll(async () => {
153
165
  server = new RuntimeHttpServer({
154
166
  port: 0,
155
167
  hostname: '127.0.0.1',
@@ -159,10 +171,63 @@ describe('gateway-only ingress enforcement', () => {
159
171
  port = server.actualPort;
160
172
  });
161
173
 
162
- afterEach(async () => {
174
+ afterAll(async () => {
163
175
  await server.stop();
164
176
  });
165
177
 
178
+ // ── Runtime does not expose Telegram webhook ingress ─────────────
179
+
180
+ describe('runtime has no Telegram webhook routes', () => {
181
+
182
+ test('POST /webhooks/telegram is rejected (not handled by runtime)', async () => {
183
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/telegram`, {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ update_id: 1, message: { text: 'hello' } }),
187
+ });
188
+ // The runtime has no route for /webhooks/telegram. Without auth, the
189
+ // request is rejected with 401 (auth middleware fires before 404).
190
+ // With auth, it would 404. Either way, no Telegram handler runs.
191
+ expect(res.status).toBe(401);
192
+ });
193
+
194
+ test('GET /webhooks/telegram is rejected', async () => {
195
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/telegram`);
196
+ expect(res.status).toBe(401);
197
+ });
198
+
199
+ test('POST /webhooks/telegram/test is rejected', async () => {
200
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/telegram/test`, {
201
+ method: 'POST',
202
+ headers: { 'Content-Type': 'application/json' },
203
+ body: JSON.stringify({}),
204
+ });
205
+ expect(res.status).toBe(401);
206
+ });
207
+
208
+ test('POST /webhooks/telegram returns 404 when authenticated (no handler exists)', async () => {
209
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/telegram`, {
210
+ method: 'POST',
211
+ headers: { ...AUTH_HEADERS, 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({ update_id: 1, message: { text: 'hello' } }),
213
+ });
214
+ // With valid auth, the request passes the auth middleware and reaches
215
+ // route matching — confirming no Telegram webhook handler exists.
216
+ expect(res.status).toBe(404);
217
+ });
218
+
219
+ test('POST /webhooks/telegram/test returns 404 when authenticated (no handler exists)', async () => {
220
+ const res = await fetch(`http://127.0.0.1:${port}/webhooks/telegram/test`, {
221
+ method: 'POST',
222
+ headers: { ...AUTH_HEADERS, 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({}),
224
+ });
225
+ // With valid auth, the request passes the auth middleware and reaches
226
+ // route matching — confirming no Telegram subpath handler exists.
227
+ expect(res.status).toBe(404);
228
+ });
229
+ });
230
+
166
231
  // ── Direct Twilio webhook routes blocked in gateway_only mode ──────
167
232
 
168
233
  describe('direct webhook routes are blocked', () => {
@@ -408,6 +473,51 @@ describe('gateway-only ingress enforcement', () => {
408
473
  });
409
474
  });
410
475
 
476
+ // ── Channel sync endpoints require auth ─────────────────────────────
477
+
478
+ describe('channel sync endpoints require authentication', () => {
479
+
480
+ test('POST /v1/channels/inbound without auth returns 401', async () => {
481
+ const res = await fetch(`http://127.0.0.1:${port}/v1/channels/inbound`, {
482
+ method: 'POST',
483
+ headers: { 'Content-Type': 'application/json' },
484
+ body: JSON.stringify({
485
+ sourceChannel: 'telegram',
486
+ externalChatId: '12345',
487
+ externalMessageId: 'msg-1',
488
+ content: 'hello',
489
+ }),
490
+ });
491
+ expect(res.status).toBe(401);
492
+ });
493
+
494
+ test('DELETE /v1/channels/conversation without auth returns 401', async () => {
495
+ const res = await fetch(`http://127.0.0.1:${port}/v1/channels/conversation`, {
496
+ method: 'DELETE',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({
499
+ sourceChannel: 'telegram',
500
+ externalChatId: '12345',
501
+ }),
502
+ });
503
+ expect(res.status).toBe(401);
504
+ });
505
+
506
+ test('POST /v1/channels/delivery-ack without auth returns 401', async () => {
507
+ const res = await fetch(`http://127.0.0.1:${port}/v1/channels/delivery-ack`, {
508
+ method: 'POST',
509
+ headers: { 'Content-Type': 'application/json' },
510
+ body: JSON.stringify({
511
+ sourceChannel: 'telegram',
512
+ externalChatId: '12345',
513
+ externalMessageId: 'msg-1',
514
+ }),
515
+ });
516
+ expect(res.status).toBe(401);
517
+ });
518
+
519
+ });
520
+
411
521
  // ── Startup warning for non-loopback host ──────────────────────────
412
522
 
413
523
  describe('startup guard — non-loopback host', () => {
@@ -0,0 +1,202 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, test, expect, beforeEach } from 'bun:test';
3
+ import { mkdtempSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import * as net from 'node:net';
7
+ import { mock } from 'bun:test';
8
+
9
+ const testDir = mkdtempSync(join(tmpdir(), 'trust-rule-metadata-test-'));
10
+
11
+ mock.module('../util/platform.js', () => ({
12
+ getRootDir: () => testDir,
13
+ getDataDir: () => join(testDir, 'data'),
14
+ getWorkspaceSkillsDir: () => join(testDir, 'skills'),
15
+ isMacOS: () => process.platform === 'darwin',
16
+ isLinux: () => process.platform === 'linux',
17
+ isWindows: () => process.platform === 'win32',
18
+ getSocketPath: () => join(testDir, 'test.sock'),
19
+ getPidPath: () => join(testDir, 'test.pid'),
20
+ getDbPath: () => join(testDir, 'test.db'),
21
+ getLogPath: () => join(testDir, 'test.log'),
22
+ ensureDataDir: () => {},
23
+ getIpcBlobDir: () => join(testDir, 'ipc-blobs'),
24
+ }));
25
+
26
+ mock.module('../util/logger.js', () => ({
27
+ getLogger: () => ({
28
+ info: () => {},
29
+ warn: () => {},
30
+ error: () => {},
31
+ debug: () => {},
32
+ trace: () => {},
33
+ fatal: () => {},
34
+ child: () => ({
35
+ info: () => {},
36
+ warn: () => {},
37
+ error: () => {},
38
+ debug: () => {},
39
+ }),
40
+ }),
41
+ }));
42
+
43
+ const testConfig: Record<string, any> = {
44
+ permissions: { mode: 'legacy' as 'legacy' | 'strict' | 'workspace' },
45
+ skills: { load: { extraDirs: [] as string[] } },
46
+ sandbox: { enabled: true },
47
+ };
48
+
49
+ mock.module('../config/loader.js', () => ({
50
+ getConfig: () => testConfig,
51
+ loadConfig: () => testConfig,
52
+ invalidateConfigCache: () => {},
53
+ saveConfig: () => {},
54
+ loadRawConfig: () => ({}),
55
+ saveRawConfig: () => {},
56
+ getNestedValue: () => undefined,
57
+ setNestedValue: () => {},
58
+ }));
59
+
60
+ import { handleAddTrustRule } from '../daemon/handlers/config.js';
61
+ import { getAllRules, clearAllRules, clearCache } from '../permissions/trust-store.js';
62
+ import type { AddTrustRule } from '../daemon/ipc-contract.js';
63
+ import type { HandlerContext } from '../daemon/handlers.js';
64
+ import type { ServerMessage } from '../daemon/ipc-contract.js';
65
+ import { DebouncerMap } from '../util/debounce.js';
66
+
67
+ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
68
+ const sent: ServerMessage[] = [];
69
+ const ctx: HandlerContext = {
70
+ sessions: new Map(),
71
+ socketToSession: new Map(),
72
+ cuSessions: new Map(),
73
+ socketToCuSession: new Map(),
74
+ cuObservationParseSequence: new Map(),
75
+ socketSandboxOverride: new Map(),
76
+ sharedRequestTimestamps: [],
77
+ debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
78
+ suppressConfigReload: false,
79
+ setSuppressConfigReload: () => {},
80
+ updateConfigFingerprint: () => {},
81
+ send: (_socket, msg) => { sent.push(msg); },
82
+ broadcast: () => {},
83
+ clearAllSessions: () => 0,
84
+ getOrCreateSession: () => { throw new Error('not implemented'); },
85
+ touchSession: () => {},
86
+ };
87
+ return { ctx, sent };
88
+ }
89
+
90
+ describe('handleAddTrustRule metadata plumbing', () => {
91
+ beforeEach(() => {
92
+ clearAllRules();
93
+ clearCache();
94
+ });
95
+
96
+ test('persists allowHighRisk and executionTarget fields when provided', () => {
97
+ const { ctx } = createTestContext();
98
+ const msg: AddTrustRule = {
99
+ type: 'add_trust_rule',
100
+ toolName: 'bash',
101
+ pattern: 'git *',
102
+ scope: '/projects/my-app',
103
+ decision: 'allow',
104
+ allowHighRisk: true,
105
+ executionTarget: 'host',
106
+ };
107
+
108
+ handleAddTrustRule(msg, {} as net.Socket, ctx);
109
+
110
+ const rules = getAllRules();
111
+ const userRule = rules.find((r) => r.tool === 'bash' && r.pattern === 'git *');
112
+ expect(userRule).toBeDefined();
113
+ expect(userRule!.allowHighRisk).toBe(true);
114
+ expect(userRule!.executionTarget).toBe('host');
115
+ });
116
+
117
+ test('backward compatibility: rules work without any metadata fields', () => {
118
+ const { ctx } = createTestContext();
119
+ const msg: AddTrustRule = {
120
+ type: 'add_trust_rule',
121
+ toolName: 'file_write',
122
+ pattern: '**',
123
+ scope: 'everywhere',
124
+ decision: 'allow',
125
+ };
126
+
127
+ handleAddTrustRule(msg, {} as net.Socket, ctx);
128
+
129
+ const rules = getAllRules();
130
+ const userRule = rules.find((r) => r.tool === 'file_write' && r.pattern === '**');
131
+ expect(userRule).toBeDefined();
132
+ expect(userRule!.decision).toBe('allow');
133
+ // Metadata fields should be absent
134
+ expect(userRule!.allowHighRisk).toBeUndefined();
135
+ expect(userRule!.executionTarget).toBeUndefined();
136
+ });
137
+
138
+ test('rule can be retrieved after being added with metadata', () => {
139
+ const { ctx } = createTestContext();
140
+ const msg: AddTrustRule = {
141
+ type: 'add_trust_rule',
142
+ toolName: 'bash',
143
+ pattern: 'npm install *',
144
+ scope: '/projects/web',
145
+ decision: 'allow',
146
+ allowHighRisk: false,
147
+ executionTarget: 'sandbox',
148
+ };
149
+
150
+ handleAddTrustRule(msg, {} as net.Socket, ctx);
151
+
152
+ // Force re-read from disk to verify persistence
153
+ clearCache();
154
+ const rules = getAllRules();
155
+ const userRule = rules.find((r) => r.tool === 'bash' && r.pattern === 'npm install *');
156
+ expect(userRule).toBeDefined();
157
+ expect(userRule!.scope).toBe('/projects/web');
158
+ expect(userRule!.decision).toBe('allow');
159
+ expect(userRule!.allowHighRisk).toBe(false);
160
+ expect(userRule!.executionTarget).toBe('sandbox');
161
+ });
162
+
163
+ test('partial metadata: only allowHighRisk is forwarded when others are absent', () => {
164
+ const { ctx } = createTestContext();
165
+ const msg: AddTrustRule = {
166
+ type: 'add_trust_rule',
167
+ toolName: 'bash',
168
+ pattern: 'docker *',
169
+ scope: 'everywhere',
170
+ decision: 'allow',
171
+ allowHighRisk: true,
172
+ };
173
+
174
+ handleAddTrustRule(msg, {} as net.Socket, ctx);
175
+
176
+ const rules = getAllRules();
177
+ const userRule = rules.find((r) => r.tool === 'bash' && r.pattern === 'docker *');
178
+ expect(userRule).toBeDefined();
179
+ expect(userRule!.allowHighRisk).toBe(true);
180
+ expect(userRule!.executionTarget).toBeUndefined();
181
+ });
182
+
183
+ test('partial metadata: only executionTarget is forwarded when others are absent', () => {
184
+ const { ctx } = createTestContext();
185
+ const msg: AddTrustRule = {
186
+ type: 'add_trust_rule',
187
+ toolName: 'bash',
188
+ pattern: 'curl *',
189
+ scope: 'everywhere',
190
+ decision: 'allow',
191
+ executionTarget: 'sandbox',
192
+ };
193
+
194
+ handleAddTrustRule(msg, {} as net.Socket, ctx);
195
+
196
+ const rules = getAllRules();
197
+ const userRule = rules.find((r) => r.tool === 'bash' && r.pattern === 'curl *');
198
+ expect(userRule).toBeDefined();
199
+ expect(userRule!.executionTarget).toBe('sandbox');
200
+ expect(userRule!.allowHighRisk).toBeUndefined();
201
+ });
202
+ });
@@ -45,6 +45,7 @@ mock.module('../util/logger.js', () => ({
45
45
  import { handleMessage, type HandlerContext } from '../daemon/handlers.js';
46
46
  import type { CuObservation, IpcBlobRef, ServerMessage } from '../daemon/ipc-contract.js';
47
47
  import { ComputerUseSession } from '../daemon/computer-use-session.js';
48
+ import { DebouncerMap } from '../util/debounce.js';
48
49
 
49
50
  /** Write a blob file to the test blob directory and return the IpcBlobRef. */
50
51
  function writeBlobFile(content: Buffer, kind: IpcBlobRef['kind'], encoding: IpcBlobRef['encoding']): IpcBlobRef {
@@ -86,7 +87,7 @@ function createTestContext(sessionId: string): {
86
87
  cuObservationParseSequence: new Map(),
87
88
  socketSandboxOverride: new Map(),
88
89
  sharedRequestTimestamps: [],
89
- debounceTimers: new Map(),
90
+ debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
90
91
  suppressConfigReload: false,
91
92
  setSuppressConfigReload: () => {},
92
93
  updateConfigFingerprint: () => {},
@@ -44,6 +44,7 @@ mock.module('../util/logger.js', () => ({
44
44
 
45
45
  import { handleMessage, type HandlerContext } from '../daemon/handlers.js';
46
46
  import type { IpcBlobProbe, ServerMessage } from '../daemon/ipc-contract.js';
47
+ import { DebouncerMap } from '../util/debounce.js';
47
48
 
48
49
  /** Write a probe file to the test blob directory. */
49
50
  function writeProbeFile(probeId: string, content: Buffer): string {
@@ -63,7 +64,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
63
64
  cuObservationParseSequence: new Map(),
64
65
  socketSandboxOverride: new Map(),
65
66
  sharedRequestTimestamps: [],
66
- debounceTimers: new Map(),
67
+ debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
67
68
  suppressConfigReload: false,
68
69
  setSuppressConfigReload: () => {},
69
70
  updateConfigFingerprint: () => {},
@@ -82,6 +82,7 @@ import type {
82
82
  ShareToSlackRequest,
83
83
  ServerMessage,
84
84
  } from '../daemon/ipc-contract.js';
85
+ import { DebouncerMap } from '../util/debounce.js';
85
86
 
86
87
  function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
87
88
  const sent: ServerMessage[] = [];
@@ -93,7 +94,7 @@ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
93
94
  cuObservationParseSequence: new Map(),
94
95
  socketSandboxOverride: new Map(),
95
96
  sharedRequestTimestamps: [],
96
- debounceTimers: new Map(),
97
+ debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
97
98
  suppressConfigReload: false,
98
99
  setSuppressConfigReload: () => {},
99
100
  updateConfigFingerprint: () => {},