deepdebug-local-agent 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { classify, bumpFor, nextVersion } from '../scripts/next-version.mjs'
4
+
5
+ test('fix and other non-feature types are a patch', () => {
6
+ for (const s of ['fix: handle empty body', 'fix(ci): x', 'chore: tidy', 'docs: readme', 'ci: workflow', 'perf: faster', 'refactor: split', 'Update thing']) {
7
+ assert.equal(classify(s), 'patch', s)
8
+ }
9
+ })
10
+
11
+ test('feat is a minor', () => {
12
+ assert.equal(classify('feat: add DLP masking'), 'minor')
13
+ assert.equal(classify('feat(ws): metadata only channel'), 'minor')
14
+ })
15
+
16
+ test('breaking changes are a major, by "!" or by footer', () => {
17
+ assert.equal(classify('feat!: drop node 18'), 'major')
18
+ assert.equal(classify('fix(api)!: rename endpoint'), 'major')
19
+ assert.equal(classify('fix: x\n\nBREAKING CHANGE: signature enforce is mandatory'), 'major')
20
+ assert.equal(classify('chore: y\n\nBREAKING-CHANGE: z'), 'major')
21
+ })
22
+
23
+ test('auto-bump and merge commits are ignored', () => {
24
+ assert.equal(classify('ci: auto bump to 1.1.0 [skip ci]'), null)
25
+ assert.equal(classify('Merge pull request #55 from x/y'), null)
26
+ assert.equal(classify(''), null)
27
+ })
28
+
29
+ test('the highest level wins', () => {
30
+ assert.equal(bumpFor(['fix: a', 'fix: b']), 'patch')
31
+ assert.equal(bumpFor(['fix: a', 'feat: b', 'chore: c']), 'minor')
32
+ assert.equal(bumpFor(['feat: a', 'fix!: b', 'fix: c']), 'major')
33
+ assert.equal(bumpFor([]), 'patch')
34
+ assert.equal(bumpFor(['ci: auto bump to 1.2.0 [skip ci]']), 'patch')
35
+ })
36
+
37
+ test('a footer that is not at the start of a line is not breaking', () => {
38
+ assert.equal(classify('fix: mention BREAKING CHANGE: in prose'), 'patch')
39
+ })
40
+
41
+ test('nextVersion resets the lower levels', () => {
42
+ assert.equal(nextVersion('1.1.0', 'patch'), '1.1.1')
43
+ assert.equal(nextVersion('1.1.7', 'minor'), '1.2.0')
44
+ assert.equal(nextVersion('1.9.4', 'major'), '2.0.0')
45
+ assert.equal(nextVersion('0.0.1', 'minor'), '0.1.0')
46
+ assert.throws(() => nextVersion('1.1', 'patch'))
47
+ assert.throws(() => nextVersion('1.x.0', 'patch'))
48
+ })
@@ -0,0 +1,67 @@
1
+ // Contract tests for the metadata-only outbound allowlist (T4-10).
2
+ import { test } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { validateOutbound, projectResponse, OUTBOUND_SCHEMAS } from '../../src/ws-channel/allowlist.js';
5
+
6
+ const H = 'a'.repeat(64);
7
+
8
+ test('contract: every allowed message type validates with a golden example', () => {
9
+ const golden = [
10
+ { type: 'register', tenantId: 'tenant-1', port: 5055, workspaceHash: H },
11
+ { type: 'hello', agentVersion: '1.1.0', protocol: 2, signatureMode: 'enforce', metadataOnly: 'enforce' },
12
+ { type: 'status', state: 'idle', openWorkspaces: 2, queueDepth: 0, uptimeSeconds: 120 },
13
+ { type: 'cost', requestId: 'req-1', model: 'claude-sonnet-5-5', tokensIn: 10, tokensOut: 5, costMicros: 1200 },
14
+ { requestId: 'r1', command: 'health', ok: true, status: 'ok', openWorkspaces: 1 },
15
+ { requestId: 'r2', command: 'workspace.open', ok: false, errorCode: 'command_error' },
16
+ ];
17
+ for (const m of golden) assert.deepEqual(validateOutbound(m).ok, true, JSON.stringify(m));
18
+ });
19
+
20
+ test('contract: schema set is closed (changing it is a deliberate, reviewed edit)', () => {
21
+ assert.deepEqual(Object.keys(OUTBOUND_SCHEMAS).sort(), ['cost', 'hello', 'register', 'response', 'status']);
22
+ });
23
+
24
+ test('rejects unknown types, unknown fields and non-objects', () => {
25
+ assert.equal(validateOutbound({ type: 'file-content', content: 'x' }).reason, 'unknown-type');
26
+ assert.equal(validateOutbound({ type: 'status', state: 'idle', content: 'x' }).reason, 'unknown-field');
27
+ assert.equal(validateOutbound('ping').reason, 'not-an-object');
28
+ assert.equal(validateOutbound([]).reason, 'not-an-object');
29
+ assert.equal(validateOutbound({ type: '__proto__' }).reason, 'unknown-type');
30
+ });
31
+
32
+ test('rejects free text: code, diffs, paths, log lines and chat do not fit any string field', () => {
33
+ const leaks = ['const x = require("fs");', '--- a/x\n+++ b/x', '/Users/me/project/src/A.java', 'ERROR something failed', 'hello how are you', ''];
34
+ for (const v of leaks) {
35
+ assert.equal(validateOutbound({ type: 'register', tenantId: v, port: 1 }).ok, false, JSON.stringify(v));
36
+ assert.equal(validateOutbound({ type: 'cost', requestId: 'r', model: v }).ok, false);
37
+ assert.equal(validateOutbound({ requestId: 'r', command: v }).ok, false);
38
+ }
39
+ assert.equal(validateOutbound({ type: 'register', tenantId: 't', port: 1, workspaceHash: '/etc/passwd' }).field, 'workspaceHash');
40
+ });
41
+
42
+ test('rejects nested values, wrong numeric ranges and missing required fields', () => {
43
+ assert.equal(validateOutbound({ type: 'status', state: { a: 1 } }).ok, false);
44
+ assert.equal(validateOutbound({ type: 'status', openWorkspaces: -1 }).ok, false);
45
+ assert.equal(validateOutbound({ type: 'status', openWorkspaces: 1.5 }).ok, false);
46
+ assert.equal(validateOutbound({ type: 'register', port: 70000, tenantId: 't' }).ok, false);
47
+ assert.equal(validateOutbound({ type: 'register', port: 5055 }).reason, 'missing-field');
48
+ assert.equal(validateOutbound({ type: 'status', state: 'unknown-state' }).ok, false);
49
+ });
50
+
51
+ test('validation reports field names and codes, never the offending value', () => {
52
+ const secret = 'AKIA_SECRET_VALUE_123 /home/x';
53
+ const r = validateOutbound({ type: 'register', tenantId: secret, port: 1 });
54
+ assert.equal(JSON.stringify(r).includes('SECRET'), false);
55
+ });
56
+
57
+ test('projectResponse strips paths, turns free text into errorCode, rejects content fields', () => {
58
+ assert.deepEqual(projectResponse('r', 'health', { status: 'ok', workspace: '/home/u/p', openWorkspaces: 1 }).message,
59
+ { requestId: 'r', command: 'health', status: 'ok', openWorkspaces: 1 });
60
+ assert.deepEqual(projectResponse('r', 'workspace.open', { ok: true, root: '/home/u/p' }).message,
61
+ { requestId: 'r', command: 'workspace.open', ok: true });
62
+ assert.deepEqual(projectResponse('r', 'x', { error: 'ENOENT /home/u/secret.txt' }).message,
63
+ { requestId: 'r', command: 'x', ok: false, errorCode: 'command_error' });
64
+ const c = projectResponse('r', 'workspace.file-content', { content: 'code', found: true });
65
+ assert.equal(c.ok, false);
66
+ assert.equal(c.field, 'content');
67
+ });
@@ -0,0 +1,104 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createWsMetadataGuard } from '../../src/ws-channel/metadata-guard.js';
4
+ import { resolveWsMetadataConfig } from '../../src/ws-channel/config.js';
5
+ import { createTransitGuard } from '../../src/ws-channel/transit-guard.js';
6
+
7
+ const capture = () => { const lines = []; return { lines, warn: (m) => lines.push(String(m)), log: (m) => lines.push(String(m)) }; };
8
+ const enforce = { mode: 'enforce' };
9
+
10
+ test('flag config: default off, enforce, and unrecognised values fail closed', () => {
11
+ const warn = () => {};
12
+ assert.equal(resolveWsMetadataConfig({}, warn).mode, 'off');
13
+ assert.equal(resolveWsMetadataConfig({ LOCAL_AGENT_WS_METADATA_ONLY: '' }, warn).mode, 'off');
14
+ assert.equal(resolveWsMetadataConfig({ LOCAL_AGENT_WS_METADATA_ONLY: 'off' }, warn).mode, 'off');
15
+ assert.equal(resolveWsMetadataConfig({ LOCAL_AGENT_WS_METADATA_ONLY: ' ENFORCE ' }, warn).mode, 'enforce');
16
+ assert.equal(resolveWsMetadataConfig({ LOCAL_AGENT_WS_METADATA_ONLY: 'enfroce' }, warn).mode, 'enforce');
17
+ });
18
+
19
+ test('flag off: no guard is created and no socket is opened', () => {
20
+ let socketsOpened = 0;
21
+ const openSocket = () => { socketsOpened++; };
22
+ const guard = createWsMetadataGuard({ config: resolveWsMetadataConfig({}) });
23
+ assert.equal(guard, null);
24
+ if (guard) openSocket(); // the conditional start point: only reached when the guard exists
25
+ assert.equal(socketsOpened, 0);
26
+ });
27
+
28
+ test('legacy content commands are refused before execution, with a metadata-only error', () => {
29
+ const log = capture();
30
+ const g = createWsMetadataGuard({ config: enforce, logger: log });
31
+ for (const cmd of ['workspace.file-content', 'workspace.diff', 'workspace.safe-patch', 'mcp.read-file', 'mcp.execute-command', 'mcp.apply-patch']) {
32
+ const a = g.admitCommand('req-1', cmd);
33
+ assert.equal(a.ok, false, cmd);
34
+ assert.deepEqual(JSON.parse(a.errorFrame), { requestId: 'req-1', command: cmd, ok: false, errorCode: 'blocked_metadata_only' });
35
+ }
36
+ });
37
+
38
+ test('health reply carries only metadata (workspace path dropped)', () => {
39
+ const g = createWsMetadataGuard({ config: enforce, logger: capture() });
40
+ assert.equal(g.admitCommand('r1', 'health').ok, true);
41
+ const out = JSON.parse(g.frameResponse('r1', 'health', { status: 'ok', workspace: '/Users/x/client-repo', openWorkspaces: 2 }));
42
+ assert.deepEqual(out, { requestId: 'r1', command: 'health', status: 'ok', openWorkspaces: 2 });
43
+ });
44
+
45
+ test('content in a reply is blocked, replaced by an error, and never logged', () => {
46
+ const log = capture();
47
+ const g = createWsMetadataGuard({ config: enforce, logger: log });
48
+ g.admitCommand('r2', 'health');
49
+ const secret = 'SUPER_SECRET_SOURCE_CODE_LINE';
50
+ const out = JSON.parse(g.frameResponse('r2', 'health', { status: 'ok', diff: secret }));
51
+ assert.deepEqual(out, { requestId: 'r2', command: 'health', ok: false, errorCode: 'blocked_metadata_only' });
52
+ assert.ok(log.lines.some((l) => l.includes('BLOCKED') && l.includes('unknown-field') && l.includes('diff')));
53
+ assert.equal(log.lines.join('\n').includes(secret), false, 'payload must never be logged');
54
+ assert.equal(JSON.stringify(out).includes(secret), false);
55
+ });
56
+
57
+ test('free-text error messages become an errorCode and are not sent', () => {
58
+ const g = createWsMetadataGuard({ config: enforce, logger: capture() });
59
+ g.admitCommand('r3', 'workspace.open');
60
+ const out = JSON.parse(g.frameResponse('r3', 'workspace.open', { error: 'ENOENT: /home/u/secret/file.js' }));
61
+ assert.deepEqual(out, { requestId: 'r3', command: 'workspace.open', ok: false, errorCode: 'command_error' });
62
+ });
63
+
64
+ test('spontaneous messages are validated; violations return null (dropped, not queued)', () => {
65
+ const log = capture();
66
+ const g = createWsMetadataGuard({ config: enforce, logger: log });
67
+ assert.ok(g.frameMessage({ type: 'status', state: 'idle', openWorkspaces: 1 }));
68
+ assert.equal(g.frameMessage({ type: 'chat', text: 'hi' }), null);
69
+ assert.equal(g.frameMessage({ type: 'register', tenantId: 't', port: 1, workspacePath: '/x' }), null);
70
+ assert.equal(g.stats.blocked, 2);
71
+ });
72
+
73
+ test('D17 TTL: reply to an unknown, expired or already-answered request is dropped', () => {
74
+ let t = 1_000;
75
+ const transit = createTransitGuard({ now: () => t, limits: { requestTtlMs: 1000 } });
76
+ const g = createWsMetadataGuard({ config: enforce, transit, logger: capture() });
77
+ assert.equal(g.frameResponse('never', 'health', { status: 'ok' }), null);
78
+ g.admitCommand('a', 'health');
79
+ assert.ok(g.frameResponse('a', 'health', { status: 'ok' }));
80
+ assert.equal(g.frameResponse('a', 'health', { status: 'ok' }), null, 'single use');
81
+ g.admitCommand('b', 'health');
82
+ t += 1500;
83
+ assert.equal(g.frameResponse('b', 'health', { status: 'ok' }), null, 'expired');
84
+ });
85
+
86
+ test('D17 limits: message size, rate and in-flight bound', () => {
87
+ const transit = createTransitGuard({ limits: { rateMax: 2, maxInFlight: 2, maxMessageBytes: 60 } });
88
+ const g = createWsMetadataGuard({ config: enforce, transit, logger: capture() });
89
+ assert.equal(g.frameMessage({ type: 'cost', requestId: 'r', model: 'm'.repeat(100) }), null, 'oversize');
90
+ assert.ok(g.frameMessage({ type: 'status', state: 'idle' }));
91
+ assert.ok(g.frameMessage({ type: 'status', state: 'busy' }));
92
+ assert.equal(g.frameMessage({ type: 'status', state: 'idle' }), null, 'rate limited');
93
+ assert.equal(g.admitCommand('x1', 'health').ok, true);
94
+ assert.equal(g.admitCommand('x2', 'health').ok, true);
95
+ assert.equal(g.admitCommand('x3', 'health').ok, false, 'in-flight cap');
96
+ });
97
+
98
+ test('D17: the guard persists nothing (no fs/network usage in the module)', async () => {
99
+ const { readFile } = await import('node:fs/promises');
100
+ for (const f of ['metadata-guard', 'transit-guard', 'allowlist']) {
101
+ const src = await readFile(new URL(`../../src/ws-channel/${f}.js`, import.meta.url), 'utf8');
102
+ assert.equal(/node:fs|from "fs"|appendFile|writeFile|node:net|node:http/.test(src), false, f);
103
+ }
104
+ });
@@ -0,0 +1,99 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createConnectionState } from '../../src/ws-channel/connection-state.js';
4
+ import { createOfflinePolicyGuard, requiresPolicyGate, decideOffline } from '../../src/ws-channel/policy-offline.js';
5
+ import { decideExec } from '../../src/utils/exec-policy.js';
6
+
7
+ test('connection state: online only while open and fresh', () => {
8
+ let t = 0;
9
+ const c = createConnectionState({ now: () => t, staleAfterMs: 90_000 });
10
+ assert.equal(c.isOnline(), false);
11
+ c.markOpen();
12
+ assert.equal(c.isOnline(), true);
13
+ t = 60_000; c.markSeen();
14
+ t = 120_000;
15
+ assert.equal(c.isOnline(), true);
16
+ t = 200_000;
17
+ assert.equal(c.isOnline(), false, 'stale = offline');
18
+ c.markSeen(); // ignored while stale? still open, refreshes
19
+ assert.equal(c.isOnline(), true);
20
+ c.markClosed();
21
+ assert.equal(c.isOnline(), false);
22
+ c.markSeen();
23
+ assert.equal(c.isOnline(), false, 'a message on a closed socket does not revive it');
24
+ });
25
+
26
+ test('D19 classification: only PolicyGate-backed decisions need the gate', () => {
27
+ assert.equal(requiresPolicyGate('platform-tool:git_push'), true);
28
+ assert.equal(requiresPolicyGate('human-approval:ana'), true);
29
+ assert.equal(requiresPolicyGate('allowlist:git'), false);
30
+ assert.equal(requiresPolicyGate('policy-off'), false);
31
+ assert.deepEqual(decideOffline({ gated: true, online: false }).decision, 'DENY');
32
+ assert.deepEqual(decideOffline({ gated: true, online: true }).decision, 'ALLOW');
33
+ assert.deepEqual(decideOffline({ gated: false, online: false }).decision, 'ALLOW');
34
+ });
35
+
36
+ test('D19 guard: offline DENY for gated allows, allowlisted local commands unaffected', () => {
37
+ const state = { online: false };
38
+ const denied = [];
39
+ const guard = createOfflinePolicyGuard({ connection: { isOnline: () => state.online }, onDeny: (d) => denied.push(d) });
40
+ const gated = { allow: true, via: 'platform-tool:git_push' };
41
+ const local = { allow: true, via: 'allowlist:git' };
42
+
43
+ const d = guard(gated);
44
+ assert.equal(d.allow, false);
45
+ assert.equal(d.status, 403);
46
+ assert.equal(d.via, 'policy-gate-offline');
47
+ assert.deepEqual(denied, [{ via: 'platform-tool' }]);
48
+ assert.equal(guard(local), local);
49
+ assert.deepEqual(guard({ allow: false, via: 'rejected' }), { allow: false, via: 'rejected' });
50
+
51
+ state.online = true;
52
+ assert.equal(guard(gated), gated, 'online: PolicyGate path unchanged');
53
+ });
54
+
55
+ test('D19 via decideExec: a signed platform tool is denied offline, allowed online; flag off = unchanged', () => {
56
+ const input = {
57
+ command: 'git push origin feat/x', root: process.cwd(), headers: {},
58
+ body: { commandOrigin: 'platform', tool: 'git_push' }, vhSignature: { verified: true },
59
+ };
60
+ const base = { mode: 'enforce' };
61
+ assert.equal(decideExec(input, base).allow, true, 'flag off (no guard): unchanged');
62
+ let online = false;
63
+ const cfg = { mode: 'enforce', offlineGuard: createOfflinePolicyGuard({ connection: { isOnline: () => online } }) };
64
+ const off = decideExec(input, cfg);
65
+ assert.equal(off.allow, false);
66
+ assert.equal(off.via, 'policy-gate-offline');
67
+ online = true;
68
+ assert.equal(decideExec(input, cfg).allow, true);
69
+ });
70
+
71
+ test('D19 on the MCP bridge: /mcp/execute-command denies a platform tool offline, allows it online', async () => {
72
+ const net = await import('node:net');
73
+ const { startMCPHttpServer } = await import('../../src/mcp-http-server.js');
74
+ const { internalSignatureHeaders } = await import('../../src/utils/request-signing.js');
75
+ const port = await new Promise((resolve) => {
76
+ const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => resolve(p)); });
77
+ });
78
+ let online = false;
79
+ const execPolicy = { mode: 'enforce', offlineGuard: createOfflinePolicyGuard({ connection: { isOnline: () => online } }) };
80
+ const app = startMCPHttpServer({ resolveRoot: () => process.cwd(), count: 0, list: () => [] }, port, execPolicy);
81
+ await new Promise((r) => (app.locals.server.listening ? r() : app.locals.server.once('listening', r)));
82
+ try {
83
+ const call = () => fetch(`http://127.0.0.1:${port}/mcp/execute-command`, {
84
+ method: 'POST',
85
+ headers: { 'content-type': 'application/json', ...internalSignatureHeaders() },
86
+ body: JSON.stringify({ workspaceId: 'w', command: 'git --version', commandOrigin: 'platform', tool: 'current_branch' }),
87
+ });
88
+ const off = await call();
89
+ const offBody = await off.json();
90
+ assert.equal(off.status, 403);
91
+ assert.equal(offBody.ok, false);
92
+ assert.match(String(offBody.reason), /control plane|offline|policy/i);
93
+ online = true;
94
+ const on = await call();
95
+ assert.notEqual(on.status, 403, 'online: PolicyGate path unchanged');
96
+ } finally {
97
+ app.locals.server.close();
98
+ }
99
+ });
@@ -0,0 +1,88 @@
1
+ // End-to-end: boots the real agent against a fake control-plane WS server and
2
+ // inspects what actually leaves the socket (T4-10).
3
+ import { test } from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import net from 'node:net';
6
+ import os from 'node:os';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { spawn } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { WebSocketServer } from 'ws';
12
+
13
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
14
+ const SECRET = 'TOP_SECRET_SOURCE_MARKER_8841';
15
+
16
+ const freePort = () => new Promise((res) => { const s = net.createServer().listen(0, '127.0.0.1', () => { const p = s.address().port; s.close(() => res(p)); }); });
17
+
18
+ async function run({ flag, disableTunnel = false }) {
19
+ const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'wsmeta-ws-'));
20
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'wsmeta-home-'));
21
+ fs.writeFileSync(path.join(ws, 'secret.txt'), SECRET);
22
+ const wss = new WebSocketServer({ port: 0, host: '127.0.0.1' });
23
+ await new Promise((r) => wss.on('listening', r));
24
+ const frames = [];
25
+ let connections = 0;
26
+ wss.on('connection', (sock) => {
27
+ connections++;
28
+ sock.on('message', (d) => {
29
+ const t = d.toString();
30
+ if (t === 'ping') return;
31
+ frames.push(JSON.parse(t));
32
+ if (frames[frames.length - 1].type === 'register') {
33
+ sock.send(JSON.stringify({ requestId: 'req-health', command: 'health', params: {} }));
34
+ sock.send(JSON.stringify({ requestId: 'req-file', command: 'workspace.file-content', params: { path: 'secret.txt', workspacePath: ws } }));
35
+ }
36
+ });
37
+ });
38
+ const env = { ...process.env, PORT: String(await freePort()), GATEWAY_URL: `http://127.0.0.1:${wss.address().port}`,
39
+ TENANT_ID: 'tenant-test', AUTH_TOKEN: 'test-token', WORKSPACE_ROOT: ws, HOME: home,
40
+ LOCAL_AGENT_SIGNATURE_MODE: 'off', ...(flag ? { LOCAL_AGENT_WS_METADATA_ONLY: flag } : {}),
41
+ ...(disableTunnel ? { DISABLE_WS_TUNNEL: 'true' } : {}) };
42
+ delete env.LOCAL_AGENT_TOKEN;
43
+ const child = spawn(process.execPath, ['src/server.js'], { cwd: ROOT, env, stdio: 'ignore' });
44
+ try {
45
+ const deadline = Date.now() + (disableTunnel ? 4000 : 20000);
46
+ while (Date.now() < deadline) {
47
+ if (frames.some((f) => f.requestId === 'req-file') && frames.some((f) => f.requestId === 'req-health')) break;
48
+ await new Promise((r) => setTimeout(r, 100));
49
+ }
50
+ } finally {
51
+ child.kill('SIGKILL');
52
+ for (const c of wss.clients) c.terminate();
53
+ await new Promise((r) => wss.close(r));
54
+ fs.rmSync(ws, { recursive: true, force: true });
55
+ fs.rmSync(home, { recursive: true, force: true });
56
+ }
57
+ return { frames, connections };
58
+ }
59
+
60
+ test('flag off (default): legacy channel is unchanged', async () => {
61
+ const { frames } = await run({});
62
+ const reg = frames.find((f) => f.type === 'register');
63
+ assert.ok(reg && 'workspacePath' in reg, 'legacy register still carries workspacePath');
64
+ const file = frames.find((f) => f.requestId === 'req-file');
65
+ assert.equal(file.content, SECRET, 'legacy content command still works with the flag off');
66
+ });
67
+
68
+ test('enforce: nothing but allowlisted metadata leaves the socket, content never does', async () => {
69
+ const { frames } = await run({ flag: 'enforce' });
70
+ assert.ok(frames.length >= 3, 'register + 2 replies expected');
71
+ assert.equal(JSON.stringify(frames).includes(SECRET), false);
72
+ assert.equal(JSON.stringify(frames).includes('secret.txt'), false);
73
+ const reg = frames.find((f) => f.type === 'register');
74
+ assert.equal(reg.workspacePath, undefined);
75
+ assert.match(reg.workspaceHash, /^[a-f0-9]{64}$/);
76
+ assert.deepEqual(frames.find((f) => f.requestId === 'req-file'),
77
+ { requestId: 'req-file', command: 'workspace.file-content', ok: false, errorCode: 'blocked_metadata_only' });
78
+ const health = frames.find((f) => f.requestId === 'req-health');
79
+ assert.equal(health.status, 'ok');
80
+ assert.equal('workspace' in health, false);
81
+ });
82
+
83
+ test('DISABLE_WS_TUNNEL=true: no socket is opened, whatever the flag', async () => {
84
+ for (const flag of [undefined, 'enforce']) {
85
+ const { connections } = await run({ flag, disableTunnel: true });
86
+ assert.equal(connections, 0, `flag=${flag}`);
87
+ }
88
+ });