dsh-remote 0.2.0: agent tools to drive slaves from the master
- 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
This commit is contained in:
1 parent
8872e73e76
commit
d1e134196d
7 files changed
+625
-8
No files matched your search
@@ -0,0 +1,85 @@
|
||||
// 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 })
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user