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 { 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('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/); }); 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 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]'); });