- remote_devices / remote_exec / remote_file / remote_task tools on the master (exec: bash -lc with timeout + output clipping; files: read/write/list; task: delegate a prompt to the device's own AI, wait for turn end, return its answer; optional permission preset, status/cancel) - only the master may control devices (allowPeerControl to relay between slaves), agentTools.enabled=false opts a device out - FORGE_CREDENTIALS opt-in pseudo-secret: ships the master's tea logins to a slave (tea config + git credential store + identity) - /dsh-remote/debug/inspect + deploy/session-log.mjs for transcripts - tests for exec/files/task/forge install
86 lines
4.1 KiB
JavaScript
86 lines
4.1 KiB
JavaScript
// Device-side agent ops: exec, file read/write/list, task delegation (with a fake
|
|
// session controller), forge login install into a temp HOME.
|
|
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import * as ops from '../lib/agent-ops.mjs'
|
|
|
|
test('exec: exit code, stdout/stderr, cwd, timeout', async () => {
|
|
const r = await ops.execCommand({ command: 'echo out; echo err >&2; pwd; exit 3', cwd: '/tmp' })
|
|
assert.equal(r.exitCode, 3)
|
|
assert.match(r.stdout, /^out\n\/tmp\n$/)
|
|
assert.equal(r.stderr, 'err\n')
|
|
const t = await ops.execCommand({ command: 'sleep 5', timeoutMs: 1000 })
|
|
assert.equal(t.timedOut, true)
|
|
assert.notEqual(t.exitCode, 0)
|
|
await assert.rejects(ops.execCommand({ command: 'true', cwd: '/nonexistent/dir' }), /cwd does not exist/)
|
|
})
|
|
|
|
test('files: write, append, read (text + binary), list', () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dshr-ops-'))
|
|
ops.writeFileOp({ path: 'a/b.txt', cwd: dir, content: 'hello' })
|
|
ops.writeFileOp({ path: 'a/b.txt', cwd: dir, content: ' world', append: true })
|
|
assert.equal(ops.readFileOp({ path: path.join(dir, 'a/b.txt') }).content, 'hello world')
|
|
ops.writeFileOp({ path: 'bin', cwd: dir, content: Buffer.from([0, 1, 2]).toString('base64'), encoding: 'base64' })
|
|
const b = ops.readFileOp({ path: 'bin', cwd: dir })
|
|
assert.equal(b.encoding, 'base64')
|
|
assert.equal(b.content, 'AAEC')
|
|
const l = ops.listDirOp({ path: dir })
|
|
assert.deepEqual(l.entries.map((e) => `${e.type}:${e.name}`).sort(), ['dir:a', 'file:bin'])
|
|
fs.rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('task: creates a session, prompts, waits for turn/end, returns last answer', async () => {
|
|
const events = [{ seq: 0, type: 'session/start' }]
|
|
let prompted = null
|
|
const sc = {
|
|
async create(req) { assert.ok(req.cwd || req.workspaceId); return { sessionId: 'session-x' } },
|
|
async inspect() { return { events: [...events] } },
|
|
async prompt(req) {
|
|
prompted = req
|
|
setTimeout(() => {
|
|
events.push({ seq: 1, type: 'turn/start', data: { turn: 1 } })
|
|
events.push({ seq: 2, type: 'tool/call', data: { name: 'bash' } })
|
|
events.push({ seq: 3, type: 'assistant/message', data: { message: { content: [{ type: 'text', text: 'all 12 tests passed' }] } } })
|
|
events.push({ seq: 4, type: 'turn/end', data: { reason: { kind: 'completed' } } })
|
|
}, 300)
|
|
return { accepted: true }
|
|
},
|
|
}
|
|
const r = await ops.runTask(sc, null, { prompt: 'run the tests', cwd: os.tmpdir(), timeoutMs: 15000 })
|
|
assert.equal(prompted.content[0].text, 'run the tests')
|
|
assert.equal(r.sessionId, 'session-x')
|
|
assert.equal(r.status, 'completed')
|
|
assert.equal(r.answer, 'all 12 tests passed')
|
|
assert.deepEqual(r.tools, { bash: 1 })
|
|
const s = await ops.taskStatus(sc, { sessionId: 'session-x' })
|
|
assert.equal(s.status, 'completed')
|
|
assert.equal(s.answer, 'all 12 tests passed')
|
|
})
|
|
|
|
test('forge logins: tea config + git credential store in a temp HOME', async () => {
|
|
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dshr-home-'))
|
|
const prev = { HOME: process.env.HOME, XDG: process.env.XDG_CONFIG_HOME }
|
|
process.env.HOME = home
|
|
delete process.env.XDG_CONFIG_HOME
|
|
const origHomedir = os.homedir
|
|
os.homedir = () => home
|
|
try {
|
|
const r = await ops.installForgeLogins([{ name: 'Agent', url: 'https://git.example.com', user: 'Agent', token: 'tok123' }])
|
|
assert.deepEqual(r.installed, ['Agent@git.example.com'])
|
|
const tea = fs.readFileSync(path.join(home, '.config/tea/config.yml'), 'utf8')
|
|
assert.match(tea, /token: tok123/)
|
|
assert.equal(fs.statSync(path.join(home, '.git-credentials')).mode & 0o777, 0o600)
|
|
assert.match(fs.readFileSync(path.join(home, '.git-credentials'), 'utf8'), /^https:\/\/Agent:tok123@git\.example\.com$/m)
|
|
const helper = await ops.execCommand({ command: 'HOME=' + home + ' git config --global credential.helper' })
|
|
assert.equal(helper.stdout.trim(), 'store')
|
|
} finally {
|
|
os.homedir = origHomedir
|
|
process.env.HOME = prev.HOME
|
|
if (prev.XDG) process.env.XDG_CONFIG_HOME = prev.XDG
|
|
fs.rmSync(home, { recursive: true, force: true })
|
|
}
|
|
})
|