import test from 'node:test'; import assert from 'node:assert/strict'; import { EventEmitter } from 'node:events'; import { mkdtemp, mkdir, writeFile, readFile, symlink, rm } from 'node:fs/promises'; import os, { tmpdir } from 'node:os'; import path from 'node:path'; import { createRequire } from 'node:module'; import { ComputerUseSession } from '../computer-use/session.js'; import { ComputerActuator } from '../computer-use/actuator.js'; import { PortalInputBackend } from '../computer-use/portal-input.js'; import { createComputerObserveTools } from '../skills/computer-observe.js'; import { createComputerActTools } from '../skills/computer-act.js'; import { createPhase2Tools, filesystemRoots } from '../skills/phase2-tools.js'; import { VoiceStateMachine } from '../daemon/voice-state.js'; import { assertLocalEndpoint } from '../daemon/network-policy.js'; const require = createRequire(import.meta.url); const custom = require('../vendor/agent-harness/agent/custom-tools.js'); const sandbox = require('../vendor/agent-harness/agent/sandbox.js'); test('custom tool permissions survive registration and default to confirmation', () => { const id = 'review-permissions'; try { custom.register(id, ['read', 'write', 'dangerous', 'computer-use', undefined].map((permission, i) => ({ name: `review_${i}`, permission }))); assert.equal(custom.needsPermission(id, 'review_0', 'ask'), false); for (let i = 1; i < 5; i++) { assert.equal(custom.needsPermission(id, `review_${i}`, 'ask'), true); assert.equal(custom.needsPermission(id, `review_${i}`, 'always-approve'), false); } custom.unregister(id, 'review_1'); assert.equal(custom.needsPermission(id, 'review_1', 'ask'), false); } finally { custom.clear(id); } }); test('filesystem access lets path tools reach home while workspace stays jailed', async () => { const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-fs-access-')); const workspace = path.join(dir, 'workspace'); await mkdir(workspace); const homeProbe = await mkdtemp(path.join(os.homedir(), '.jarvis-fs-test-')); try { await writeFile(path.join(homeProbe, 'note.txt'), 'from-home'); const jailed = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('workspace', workspace) }).map(t => [t.name, t])); await assert.rejects(jailed.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), /outside/); const opened = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('filesystem', workspace) }).map(t => [t.name, t])); assert.equal(await opened.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), 'from-home'); const home = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('home', workspace) }).map(t => [t.name, t])); assert.equal(await home.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), 'from-home'); sandbox.setGrants({ roots: [path.resolve(workspace)] }); assert.equal(sandbox.isAllowed('jarvis-qvac', path.join(homeProbe, 'note.txt')), false); sandbox.setGrants({ roots: [path.resolve(workspace), '/'] }); assert.equal(sandbox.isAllowed('jarvis-qvac', path.join(homeProbe, 'note.txt')), true); } finally { await rm(homeProbe, { recursive: true, force: true }); await rm(dir, { recursive: true, force: true }); } }); test('workspace tools reject outside roots and symlink escapes and count UTF-8 bytes', async () => { const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-path-test-')); const root = path.join(dir, 'workspace'); await mkdir(root); const outside = path.join(dir, 'outside'); await mkdir(outside); await writeFile(path.join(outside, 'secret'), 'private'); await symlink(outside, path.join(root, 'link')); await symlink(path.join(outside, 'missing'), path.join(root, 'dangling')); const tools = Object.fromEntries(createPhase2Tools({ cwd: root }).map(t => [t.name, t])); await assert.rejects(tools.fs_read.execute({ file: 'link/secret' }), /outside/); await assert.rejects(tools.fs_write.execute({ file: 'link/new', contents: 'x', confirmed: true }), /outside/); await assert.rejects(tools.fs_write.execute({ file: 'dangling', contents: 'x', confirmed: true }), /symlink/); await assert.rejects(tools.fs_search.execute({ root: outside, query: 'secret' }), /outside/); const result = await tools.fs_write.execute({ file: 'new', contents: 'é', confirmed: true }); assert.equal(result.bytes, 2); assert.equal(await readFile(path.join(root, 'new'), 'utf8'), 'é'); }); test('coordinate click uses portal input even when accessibility actions exist', async () => { const session = new ComputerUseSession(); session.grant(); const sent = []; const actuator = new ComputerActuator({ session, input: { send: a => sent.push(a) }, atspiAction: () => assert.fail('unexpected semantic click'), sleep: async () => {} }); await actuator.click({ x: 20, y: 30 }); assert.equal(sent[0].x, 20); }); test('stale semantic refs fail before input and revocation during preview prevents action', async () => { const session = new ComputerUseSession(); session.grant(); const actuator = new ComputerActuator({ session, find: async () => [], input: { send: () => assert.fail('unexpected input') }, highlight: async () => session.revoke() }); await assert.rejects(actuator.type({ ref: 'missing', text: 'secret' }), /stale/); await assert.rejects(actuator.click({ x: 1, y: 1 }), /inactive/); }); test('desktop observation and actuation do not wait for a second Allow after Grant desktop', () => { const id = 'cu-observe-permission'; try { custom.register(id, [ ...createComputerObserveTools({ computer: { status: () => ({ active: true }) }, observer: {} }), ...createComputerActTools({ actuator: {} }), ]); assert.equal(custom.needsPermission(id, 'cu_observe', 'ask'), false); assert.equal(custom.needsPermission(id, 'cu_click', 'ask'), false); assert.equal(custom.needsPermission(id, 'cu_type', 'ask'), false); } finally { custom.clear(id); } }); test('expired grants prevent desktop observation', async () => { let now = 0; const computer = new ComputerUseSession({ clock: () => now }); computer.grant(); now = 180001; const [observe] = createComputerObserveTools({ computer, observer: { observe: () => assert.fail('expired observation') } }); await assert.rejects(observe.execute(), /grant/); }); test('observe tools return compact tree nodes without accessibility state dumps', async () => { const computer = { status: () => ({ active: true }) }; const observer = { observe: async () => ({ focused: { name: 'Discord' }, windows: [{ id: 1 }], tree: [{ ref: 'r1', role: 'frame', name: 'Discord', rect: [0, 0, 10, 10], state: ['1', '8', '24'] }], frame_source: 'pipewire', screenshot_path: '/tmp/x.webp', unavailable: [], }), find: async () => [{ ref: 'r1', role: 'entry', name: 'Message', rect: [1, 2, 3, 4], score: 90, state: ['focused'] }], }; const tools = Object.fromEntries(createComputerObserveTools({ computer, observer }).map((tool) => [tool.name, tool])); const seen = await tools.cu_observe.execute({}); assert.equal(seen.tree[0].ref, 'r1'); assert.equal(seen.tree[0].state, undefined); assert.match(seen.hint, /Do not paste/); const hits = await tools.cu_find.execute({ query: 'message' }); assert.equal(hits[0].score, 90); assert.equal(hits[0].state, undefined); }); function helper() { const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.stdin = new EventEmitter(); child.stdin.writable = true; child.stdin.write = () => {}; child.kill = () => { child.killed = true; }; return child; } test('portal helper reports a notify fallback as ready', async () => { const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child }); const grant = input.grant(); child.stdout.emit('data', '{"type":"ready","restore_token_present":true,"backend":"portal-notify"}\n'); assert.equal((await grant).backend, 'portal-notify'); }); test('Grant desktop starts a fresh helper and asks GNOME instead of restoring a share', async () => { let env; const child = helper(); const input = new PortalInputBackend({ spawnImpl: (_cmd, _args, opts) => { env = opts.env; return child; } }); const grant = input.grant({ persist: false, mode: 'act' }); assert.equal(env.JARVIS_CU_PERSIST, '0'); assert.equal(env.JARVIS_CU_MODE, 'act'); const source = await import('node:fs/promises').then((fs) => fs.readFile(new URL('../computer-use/py/portal_remote_desktop.py', import.meta.url), 'utf8')); assert.match(source, /PERSIST_MODE = 2 if PERSIST else 0/); assert.match(source, /request\(screencast, 'SelectSources', '\(oa\{sv\}\)', \(session,/); assert.match(source, /results = request\(remote, 'Start'/); input.revoke(); await assert.rejects(grant, /revoked/); }); test('revoke then Grant desktop starts a new portal helper', async () => { const children = []; const input = new PortalInputBackend({ spawnImpl: () => { const child = helper(); children.push(child); return child; } }); const first = input.grant({ persist: false }); children[0].stdout.emit('data', '{"type":"ready","backend":"portal-ei","screen":true}\n'); await first; input.revoke(); assert.equal(children[0].killed, true); const second = input.grant({ persist: false }); assert.equal(children.length, 2); children[1].stdout.emit('data', '{"type":"ready","backend":"portal-ei","screen":true}\n'); assert.equal((await second).screen, true); }); test('portal helper returns a PipeWire frame without ending the grant', async () => { const child = helper(); let written = ''; child.stdin.write = (chunk) => { written += String(chunk); }; const input = new PortalInputBackend({ spawnImpl: () => child }); const grant = input.grant(); child.stdout.emit('data', '{"type":"ready","backend":"portal-ei","screen":true}\n'); assert.equal((await grant).screen, true); const frame = input.captureFrame('/tmp/pw.png'); assert.match(written, /"type":"frame"/); child.stdout.emit('data', '{"type":"frame","path":"/tmp/pw.png","source":"pipewire"}\n'); assert.equal(await frame, '/tmp/pw.png'); assert.equal(input.available, true); }); test('a PipeWire frame error does not revoke desktop access', async () => { const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child }); const grant = input.grant(); child.stdout.emit('data', '{"type":"ready","backend":"portal-notify","screen":false}\n'); await grant; const frame = input.captureFrame('/tmp/pw.png'); child.stdout.emit('data', '{"type":"error","reason":"no PipeWire frame yet"}\n'); await assert.rejects(frame, /no PipeWire frame/); assert.equal(input.available, true); }); test('portal grants wait for a complete readiness message and clear on exit', async () => { const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child }); const grant = input.grant(); child.stdout.emit('data', '{"type":"rea'); assert.equal(input.available, false); child.stdout.emit('data', 'dy","restore_token_present":false}\n'); assert.equal((await grant).backend, 'portal-ei'); child.emit('close', 0); assert.equal(input.available, false); assert.throws(() => input.send({}), /unavailable/); }); test('revoked portal grants cannot become ready later', async () => { const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child }); const grant = input.grant(); input.revoke(); child.stdout.emit('data', '{"type":"ready"}\n'); await assert.rejects(grant, /revoked/); assert.equal(input.available, false); assert.equal(child.killed, true); }); test('portal helper errors reject and allow a new grant', async () => { const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child }); const grant = input.grant(); child.stdout.emit('data', '{"type":"error","reason":"denied"}\n'); await assert.rejects(grant, /denied/); assert.equal(input.process, null); }); test('idle timer never attempts to sleep a thinking or speaking voice', () => { let now = 0; const voice = new VoiceStateMachine({ now: () => now, idleMs: 10 }); voice.typedUtterance(); now = 100; assert.equal(voice.expireIdle(), 'THINKING'); voice.speak(); now = 200; assert.equal(voice.expireIdle(), 'SPEAKING'); }); test('local endpoint policy accepts IPv6 loopback', () => { assert.equal(assertLocalEndpoint('http://[::1]:8080').hostname, '[::1]'); });