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.
- package/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +113 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +137 -18
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +62 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +27 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- 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,
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
97
|
+
debounceTimers: new DebouncerMap({ defaultDelayMs: 200 }),
|
|
97
98
|
suppressConfigReload: false,
|
|
98
99
|
setSuppressConfigReload: () => {},
|
|
99
100
|
updateConfigFingerprint: () => {},
|