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
@@ -0,0 +1,336 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { describe, test, expect, beforeEach, mock } 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
+
8
+ const testDir = mkdtempSync(join(tmpdir(), 'permsim-handler-test-'));
9
+
10
+ mock.module('../util/platform.js', () => ({
11
+ getRootDir: () => testDir,
12
+ getDataDir: () => join(testDir, 'data'),
13
+ getWorkspaceSkillsDir: () => join(testDir, 'skills'),
14
+ isMacOS: () => process.platform === 'darwin',
15
+ isLinux: () => process.platform === 'linux',
16
+ isWindows: () => process.platform === 'win32',
17
+ getSocketPath: () => join(testDir, 'test.sock'),
18
+ getPidPath: () => join(testDir, 'test.pid'),
19
+ getDbPath: () => join(testDir, 'test.db'),
20
+ getLogPath: () => join(testDir, 'test.log'),
21
+ ensureDataDir: () => {},
22
+ getIpcBlobDir: () => join(testDir, 'ipc-blobs'),
23
+ }));
24
+
25
+ mock.module('../util/logger.js', () => ({
26
+ getLogger: () => ({
27
+ info: () => {},
28
+ warn: () => {},
29
+ error: () => {},
30
+ debug: () => {},
31
+ trace: () => {},
32
+ fatal: () => {},
33
+ child: () => ({
34
+ info: () => {},
35
+ warn: () => {},
36
+ error: () => {},
37
+ debug: () => {},
38
+ }),
39
+ }),
40
+ }));
41
+
42
+ const testConfig: Record<string, any> = {
43
+ permissions: { mode: 'legacy' as 'legacy' | 'strict' | 'workspace' },
44
+ skills: { load: { extraDirs: [] as string[] } },
45
+ sandbox: { enabled: true },
46
+ };
47
+
48
+ mock.module('../config/loader.js', () => ({
49
+ getConfig: () => testConfig,
50
+ loadConfig: () => testConfig,
51
+ invalidateConfigCache: () => {},
52
+ saveConfig: () => {},
53
+ loadRawConfig: () => ({}),
54
+ saveRawConfig: () => {},
55
+ getNestedValue: () => undefined,
56
+ setNestedValue: () => {},
57
+ }));
58
+
59
+ import { handleToolPermissionSimulate } from '../daemon/handlers/config.js';
60
+ import { addRule, clearAllRules, clearCache } from '../permissions/trust-store.js';
61
+ import type { ToolPermissionSimulateRequest, ToolPermissionSimulateResponse, ServerMessage } from '../daemon/ipc-contract.js';
62
+ import type { HandlerContext } from '../daemon/handlers.js';
63
+ import { DebouncerMap } from '../util/debounce.js';
64
+
65
+ function createTestContext(): { ctx: HandlerContext; sent: ServerMessage[] } {
66
+ const sent: ServerMessage[] = [];
67
+ const ctx: HandlerContext = {
68
+ sessions: new Map(),
69
+ socketToSession: new Map(),
70
+ cuSessions: new Map(),
71
+ socketToCuSession: new Map(),
72
+ cuObservationParseSequence: new Map(),
73
+ socketSandboxOverride: new Map(),
74
+ sharedRequestTimestamps: [],
75
+ debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
76
+ suppressConfigReload: false,
77
+ setSuppressConfigReload: () => {},
78
+ updateConfigFingerprint: () => {},
79
+ send: (_socket, msg) => { sent.push(msg); },
80
+ broadcast: () => {},
81
+ clearAllSessions: () => 0,
82
+ getOrCreateSession: () => { throw new Error('not implemented'); },
83
+ touchSession: () => {},
84
+ };
85
+ return { ctx, sent };
86
+ }
87
+
88
+ function getResponse(sent: ServerMessage[]): ToolPermissionSimulateResponse {
89
+ const msg = sent.find((m) => (m as any).type === 'tool_permission_simulate_response');
90
+ if (!msg) throw new Error('No tool_permission_simulate_response found in sent messages');
91
+ return msg as unknown as ToolPermissionSimulateResponse;
92
+ }
93
+
94
+ describe('tool_permission_simulate handler', () => {
95
+ beforeEach(() => {
96
+ clearAllRules();
97
+ clearCache();
98
+ testConfig.permissions.mode = 'legacy';
99
+ });
100
+
101
+ test('validation: returns error when toolName is missing', async () => {
102
+ const { ctx, sent } = createTestContext();
103
+ const msg = { type: 'tool_permission_simulate' } as any as ToolPermissionSimulateRequest;
104
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
105
+
106
+ const res = getResponse(sent);
107
+ expect(res.success).toBe(false);
108
+ expect(res.error).toContain('toolName is required');
109
+ });
110
+
111
+ test('validation: returns error when input is missing', async () => {
112
+ const { ctx, sent } = createTestContext();
113
+ const msg = {
114
+ type: 'tool_permission_simulate',
115
+ toolName: 'bash',
116
+ } as any as ToolPermissionSimulateRequest;
117
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
118
+
119
+ const res = getResponse(sent);
120
+ expect(res.success).toBe(false);
121
+ expect(res.error).toContain('input is required');
122
+ });
123
+
124
+ test('low-risk auto-allow: file_read is auto-allowed', async () => {
125
+ const { ctx, sent } = createTestContext();
126
+ const msg: ToolPermissionSimulateRequest = {
127
+ type: 'tool_permission_simulate',
128
+ toolName: 'file_read',
129
+ input: { path: '/tmp/test.txt' },
130
+ };
131
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
132
+
133
+ const res = getResponse(sent);
134
+ expect(res.success).toBe(true);
135
+ expect(res.decision).toBe('allow');
136
+ expect(res.riskLevel).toBe('low');
137
+ });
138
+
139
+ test('deny rule produces deny decision', async () => {
140
+ // file_write deny rule — no default allow-all rule competes
141
+ addRule('file_write', 'file_write:/tmp/**', 'everywhere', 'deny');
142
+
143
+ const { ctx, sent } = createTestContext();
144
+ const msg: ToolPermissionSimulateRequest = {
145
+ type: 'tool_permission_simulate',
146
+ toolName: 'file_write',
147
+ input: { path: '/tmp/test.txt', content: 'hello' },
148
+ };
149
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
150
+
151
+ const res = getResponse(sent);
152
+ expect(res.success).toBe(true);
153
+ expect(res.decision).toBe('deny');
154
+ expect(res.matchedRuleId).toBeDefined();
155
+ });
156
+
157
+ test('prompt decision includes allowlist and scope options', async () => {
158
+ const { ctx, sent } = createTestContext();
159
+ // file_write is medium risk and will prompt without a trust rule
160
+ const msg: ToolPermissionSimulateRequest = {
161
+ type: 'tool_permission_simulate',
162
+ toolName: 'file_write',
163
+ input: { path: '/tmp/test.txt', content: 'hello' },
164
+ };
165
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
166
+
167
+ const res = getResponse(sent);
168
+ expect(res.success).toBe(true);
169
+ expect(res.decision).toBe('prompt');
170
+ expect(res.promptPayload).toBeDefined();
171
+ expect(res.promptPayload!.allowlistOptions.length).toBeGreaterThan(0);
172
+ expect(res.promptPayload!.scopeOptions.length).toBeGreaterThan(0);
173
+ expect(res.promptPayload!.persistentDecisionsAllowed).toBe(true);
174
+ });
175
+
176
+ test('proxied bash disables persistent decisions', async () => {
177
+ const { ctx, sent } = createTestContext();
178
+ const msg: ToolPermissionSimulateRequest = {
179
+ type: 'tool_permission_simulate',
180
+ toolName: 'bash',
181
+ input: { command: 'curl https://example.com', network_mode: 'proxied' },
182
+ };
183
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
184
+
185
+ const res = getResponse(sent);
186
+ expect(res.success).toBe(true);
187
+ expect(res.decision).toBe('prompt');
188
+ expect(res.promptPayload).toBeDefined();
189
+ expect(res.promptPayload!.persistentDecisionsAllowed).toBe(false);
190
+ });
191
+
192
+ test('forcePromptSideEffects promotes allow to prompt for side-effect tools', async () => {
193
+ // file_read is low-risk, auto-allowed, and NOT a side-effect tool
194
+ // so we use bash with an allow rule to test the promotion
195
+ addRule('bash', 'bash:ls*', 'everywhere', 'allow');
196
+
197
+ const { ctx, sent } = createTestContext();
198
+ const msg: ToolPermissionSimulateRequest = {
199
+ type: 'tool_permission_simulate',
200
+ toolName: 'bash',
201
+ input: { command: 'ls' },
202
+ forcePromptSideEffects: true,
203
+ };
204
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
205
+
206
+ const res = getResponse(sent);
207
+ expect(res.success).toBe(true);
208
+ // bash is a side-effect tool, so allow gets promoted to prompt
209
+ expect(res.decision).toBe('prompt');
210
+ expect(res.reason).toContain('Private thread');
211
+ });
212
+
213
+ test('forcePromptSideEffects does not affect non-side-effect tools', async () => {
214
+ const { ctx, sent } = createTestContext();
215
+ // file_read is low-risk and not a side-effect tool
216
+ const msg: ToolPermissionSimulateRequest = {
217
+ type: 'tool_permission_simulate',
218
+ toolName: 'file_read',
219
+ input: { path: '/tmp/test.txt' },
220
+ forcePromptSideEffects: true,
221
+ };
222
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
223
+
224
+ const res = getResponse(sent);
225
+ expect(res.success).toBe(true);
226
+ expect(res.decision).toBe('allow');
227
+ });
228
+
229
+ test('non-interactive converts prompt to deny', async () => {
230
+ const { ctx, sent } = createTestContext();
231
+ // file_write is medium risk → prompt without a rule
232
+ const msg: ToolPermissionSimulateRequest = {
233
+ type: 'tool_permission_simulate',
234
+ toolName: 'file_write',
235
+ input: { path: '/tmp/test.txt', content: 'hello' },
236
+ isInteractive: false,
237
+ };
238
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
239
+
240
+ const res = getResponse(sent);
241
+ expect(res.success).toBe(true);
242
+ expect(res.decision).toBe('deny');
243
+ expect(res.reason).toContain('Non-interactive');
244
+ // No prompt payload when decision is deny
245
+ expect(res.promptPayload).toBeUndefined();
246
+ });
247
+
248
+ test('non-interactive does not affect allow decisions', async () => {
249
+ const { ctx, sent } = createTestContext();
250
+ const msg: ToolPermissionSimulateRequest = {
251
+ type: 'tool_permission_simulate',
252
+ toolName: 'file_read',
253
+ input: { path: '/tmp/test.txt' },
254
+ isInteractive: false,
255
+ };
256
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
257
+
258
+ const res = getResponse(sent);
259
+ expect(res.success).toBe(true);
260
+ expect(res.decision).toBe('allow');
261
+ });
262
+
263
+ test('allow rule with matching pattern returns allow', async () => {
264
+ addRule('file_write', 'file_write:/tmp/**', 'everywhere', 'allow');
265
+
266
+ const { ctx, sent } = createTestContext();
267
+ const msg: ToolPermissionSimulateRequest = {
268
+ type: 'tool_permission_simulate',
269
+ toolName: 'file_write',
270
+ input: { path: '/tmp/test.txt', content: 'hello' },
271
+ };
272
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
273
+
274
+ const res = getResponse(sent);
275
+ expect(res.success).toBe(true);
276
+ expect(res.decision).toBe('allow');
277
+ expect(res.matchedRuleId).toBeDefined();
278
+ });
279
+
280
+ test('executionTarget: sandbox-scoped rule matches when tool resolves to sandbox', async () => {
281
+ // file_write resolves to 'sandbox' (no host_ prefix)
282
+ addRule('file_write', 'file_write:/tmp/**', 'everywhere', 'allow', 100, {
283
+ executionTarget: 'sandbox',
284
+ });
285
+
286
+ const { ctx, sent } = createTestContext();
287
+ const msg: ToolPermissionSimulateRequest = {
288
+ type: 'tool_permission_simulate',
289
+ toolName: 'file_write',
290
+ input: { path: '/tmp/test.txt', content: 'hello' },
291
+ };
292
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
293
+
294
+ const res = getResponse(sent);
295
+ expect(res.success).toBe(true);
296
+ expect(res.decision).toBe('allow');
297
+ expect(res.matchedRuleId).toBeDefined();
298
+ expect(res.executionTarget).toBe('sandbox');
299
+ });
300
+
301
+ test('executionTarget: sandbox-scoped rule does NOT match when tool resolves to host', async () => {
302
+ // host_file_write resolves to 'host' (host_ prefix)
303
+ addRule('host_file_write', 'host_file_write:/tmp/**', 'everywhere', 'allow', 100, {
304
+ executionTarget: 'sandbox',
305
+ });
306
+
307
+ const { ctx, sent } = createTestContext();
308
+ const msg: ToolPermissionSimulateRequest = {
309
+ type: 'tool_permission_simulate',
310
+ toolName: 'host_file_write',
311
+ input: { path: '/tmp/test.txt', content: 'hello' },
312
+ };
313
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
314
+
315
+ const res = getResponse(sent);
316
+ expect(res.success).toBe(true);
317
+ // The sandbox-scoped rule should not match a host tool
318
+ expect(res.decision).toBe('prompt');
319
+ expect(res.matchedRuleId).toBeUndefined();
320
+ expect(res.executionTarget).toBe('host');
321
+ });
322
+
323
+ test('executionTarget: response includes resolved execution target', async () => {
324
+ const { ctx, sent } = createTestContext();
325
+ const msg: ToolPermissionSimulateRequest = {
326
+ type: 'tool_permission_simulate',
327
+ toolName: 'host_bash',
328
+ input: { command: 'echo hi' },
329
+ };
330
+ await handleToolPermissionSimulate(msg, {} as net.Socket, ctx);
331
+
332
+ const res = getResponse(sent);
333
+ expect(res.success).toBe(true);
334
+ expect(res.executionTarget).toBe('host');
335
+ });
336
+ });