import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, symlinkSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; import { join } from 'node:path'; import { discoverDiscordAgents, inspectDiscord } from '../src/discord.mjs'; import { scan, scanAgent } from '../src/scan.mjs'; import { replyToRow, startServer } from '../src/serve.mjs'; import { identityOf } from '../../discord/src/journal.mjs'; import { envelope } from '../../discord/src/context.mjs'; import { makeRegistration, writeRegistration } from '../../seat/src/seat.mjs'; test('STOP access failure is unknown, not absence, under a non-root identity', () => { const result = spawnSync(process.execPath, ['--input-type=module', '-e', ` import assert from 'node:assert/strict'; import { mkdtempSync, mkdirSync, writeFileSync, chmodSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { inspectDiscord } from ${JSON.stringify(new URL('../src/discord.mjs', import.meta.url).href)}; assert.notEqual(process.getuid(), 0, 'permission regression must not run as root'); const root = mkdtempSync(join(tmpdir(), 'board-stop-permission-')); const dir = join(root, 'journal'), connector = { binding: 'fixture', journalDir: dir }; mkdirSync(dir); try { assert.equal(inspectDiscord(connector).braked, false, 'verified absence'); writeFileSync(join(dir, 'STOP'), 'private contents never read'); assert.equal(inspectDiscord(connector).braked, true, 'verified presence'); chmodSync(dir, 0o000); const inaccessible = inspectDiscord(connector); assert.equal(inaccessible.braked, null, 'inaccessible STOP is unknown'); assert.equal(inaccessible.alive, false); chmodSync(dir, 0o700); assert.equal(inspectDiscord(connector).braked, true, 'restored access sees STOP'); rmSync(join(dir, 'STOP')); assert.equal(inspectDiscord(connector).braked, false, 'verified removal'); } finally { chmodSync(dir, 0o700); rmSync(root, { recursive: true, force: true }); } `], { encoding: 'utf8', timeout: 10000, ...(process.getuid() === 0 ? { uid: 65534, gid: 65534 } : {}) }); assert.equal(result.status, 0, result.stdout + result.stderr); }); test('connector Task never inherits Discord routing envelopes; ordinary Task still uses user text', () => { const f = fixture(); try { const text = envelope({ guildName: 'Synthetic guild', channelName: 'synthetic-channel', authorId: '111111111111111111', messageId: '222222222222222222', text: 'Synthetic user request' }); const sessionsDir = join(f.root, 'sessions', 'discord-demo'); writeFileSync(join(sessionsDir, 's.jsonl'), JSON.stringify({ type: 'message', message: { role: 'user', content: [{ type: 'text', text }] } }) + '\n'); const connector = scanAgent(discoverDiscordAgents(f.root).specs[0]); assert.equal(connector.task, 'Discord connector'); assert.equal(connector.taskSource, 'connector'); for (const secret of ['111111111111111111', '222222222222222222', 'Synthetic user request']) assert.equal(connector.task.includes(secret), false); const ordinary = scanAgent({ agent: 'native', project: 'repo', sessionsDir, tmux: {} }, { isAlive: () => true }); assert.equal(ordinary.task, text.replace(/\s+/g, ' ').trim()); assert.equal(ordinary.taskSource, 'first-user-message'); } finally { f.cleanup(); } }); function fixture() { const root = mkdtempSync(join(tmpdir(), 'board-discord-')); mkdirSync(join(root, 'discord', 'demo', 'run.lock'), { recursive: true }); writeFileSync(join(root, 'discord', 'demo.json'), JSON.stringify({ name: 'demo', seat: 'researcher', tokenFile: '/never-read-secret', guildId: 'PRIVATE-ID', users: ['PRIVATE-USER'] }), { mode: 0o600 }); const sessions = join(root, 'sessions', 'discord-demo'); mkdirSync(sessions, { recursive: true }); writeFileSync(join(sessions, 's.jsonl'), JSON.stringify({ type: 'message', timestamp: '2026-09-14T00:00:00Z', message: { role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text: 'done' }] } }) + '\n'); const owner = join(root, 'discord', 'demo', 'run.lock', 'owner.json'); writeFileSync(owner, JSON.stringify({ pid: process.pid, ...identityOf(process.pid) })); return { root, owner, cleanup: () => rmSync(root, { recursive: true, force: true }) }; } test('connector discovery keeps only safe identity; rejects modes, mismatches, links and traversal', () => { const f = fixture(); try { const result = discoverDiscordAgents(f.root); assert.equal(result.specs.length, 1); assert.deepEqual(result.errors, []); assert.equal(result.specs[0].agent, 'researcher (discord: demo)'); assert.equal(JSON.stringify(result).includes('PRIVATE'), false); assert.equal(JSON.stringify(result).includes('tokenFile'), false); writeFileSync(join(f.root, 'discord', 'bad.json'), '{"name":"bad","seat":"../../escape","secret":"PRIVATE"}', { mode: 0o600 }); symlinkSync(join(f.root, 'discord', 'demo.json'), join(f.root, 'discord', 'linked.json')); writeFileSync(join(f.root, 'discord', 'other.json'), '{"name":"wrong","seat":"x"}', { mode: 0o600 }); chmodSync(join(f.root, 'discord', 'demo.json'), 0o644); const bad = discoverDiscordAgents(f.root); assert.equal(bad.specs.length, 0); assert.equal(bad.errors.length, 4); assert.equal(JSON.stringify(bad).includes('PRIVATE'), false); } finally { f.cleanup(); } }); test('canonical owner identity and STOP are independent; no tmux fallback or forged registration', () => { const f = fixture(); try { const spec = discoverDiscordAgents(f.root).specs[0]; const row = () => scanAgent(spec, { isAlive: () => { throw Error('must not ask tmux'); }, registration: { pid: process.pid, task: 'forged', taskSetBy: 'forged-setter', tmux: { session: 'forged' } } }); assert.equal(row().alive, true); assert.equal(row().state, 'idle'); assert.equal(row().registered, null); // #1511: the connector's fixed task borrows no attribution from a native registration. assert.equal(row().task, 'Discord connector'); assert.equal(row().taskSource, 'connector'); assert.equal(row().taskSetBy, null); assert.equal(JSON.stringify(row()).includes('forged-setter'), false); writeFileSync(join(f.root, 'discord', 'demo', 'STOP'), 'PRIVATE BRAKE CONTENT'); assert.equal(row().connector.braked, true); assert.equal(row().state, 'idle'); assert.equal(JSON.stringify(row()).includes('PRIVATE'), false); assert.equal(inspectDiscord(spec.connector, { identity: () => ({ start: '1', boot: identityOf(process.pid).boot }) }).alive, false); assert.equal(inspectDiscord(spec.connector, { identity: () => ({ start: null, boot: null }) }).ownerState, 'unknown'); assert.equal(inspectDiscord(spec.connector, { identity: () => ({ start: identityOf(process.pid).start, boot: '00000000-0000-0000-0000-000000000000' }) }).ownerState, 'mismatch'); for (const value of ['broken PRIVATE', JSON.stringify({ pid: process.pid }), JSON.stringify({ pid: 2147483647, ...identityOf(process.pid) })]) { writeFileSync(f.owner, value); assert.equal(row().alive, false); assert.equal(row().state, 'offline'); } rmSync(f.owner); assert.equal(row().connector.ownerState, 'absent'); symlinkSync('/never-read-secret', f.owner); assert.equal(row().alive, false); } finally { f.cleanup(); } }); test('connector reply refusal precedes forged live tmux registration; ordinary agent still sends', () => { let calls = 0; const exec = () => { calls++; return { status: 0, stdout: '', stderr: '' }; }; const r = { agent: 'researcher (discord: demo)', project: 'fleet', connector: { binding: 'demo' }, registered: { alive: true, tmux: { session: 'forged' } } }; assert.equal(replyToRow({ index: { sessions: [r] }, key: 'fleet/' + r.agent, text: 'hello', exec }).status, 409); delete r.connector; assert.equal(replyToRow({ index: { sessions: [r] }, key: 'fleet/' + r.agent, text: 'hello', exec }).status, 409); assert.equal(calls, 0); r.agent = 'ordinary'; assert.equal(replyToRow({ index: { sessions: [r] }, key: 'fleet/ordinary', text: 'hello', exec }).status, 200); assert.equal(calls, 1); }); test('server rescans connector discovery and refuses HTTP reply without transport', async () => { const f = fixture(); let server; try { const sessionsDir = join(f.root, 'sessions', 'discord-demo'), seatsDir = join(f.root, 'seats'); writeRegistration(seatsDir, makeRegistration({ resolved: { seat: 'forged', project: 'fleet', sessionsDir, seatDir: sessionsDir, launchScript: join(f.root, 'unused.sh'), layout: 'repo', defaultWorkspace: f.root }, task: 'forged', tmux: { session: 'forged', socket: null }, pid: process.pid })); server = await startServer({ port: 0, specs: [], seatsDir, boardDir: join(f.root, 'board'), discordDataRoot: f.root, exec: () => { throw Error('transport forbidden'); } }); const base = `http://127.0.0.1:${server.address().port}`; let response = await fetch(base + '/api/board'); const board = await response.json(); assert.equal(board.sessions[0].state, 'idle'); assert.equal(board.sessions[0].connector.alive, true); assert.equal(board.sessions[0].registered, null); assert.notEqual(board.sessions[0].task, 'forged'); response = await fetch(base + '/api/reply', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent: 'fleet/researcher (discord: demo)', text: 'hello' }) }); assert.equal(response.status, 409); rmSync(join(f.root, 'discord', 'demo.json')); assert.equal((await (await fetch(base + '/api/board')).json()).sessions.length, 0); } finally { if (server) await new Promise(r => server.close(r)); f.cleanup(); } }); test('connector session links and linked directories are not read', () => { const f = fixture(); try { const sessions = join(f.root, 'sessions', 'discord-demo'); rmSync(sessions, { recursive: true }); symlinkSync(join(f.root, 'discord'), sessions); const result = scan([], { boardDir: join(f.root, 'board'), discordDataRoot: f.root }); assert.equal(result.sessions[0].sessionFile, null); rmSync(join(f.root, 'discord'), { recursive: true }); symlinkSync('/never-read-secret', join(f.root, 'discord')); assert.equal(discoverDiscordAgents(f.root).specs.length, 0); } finally { f.cleanup(); } });