mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-13 14:02:16 +00:00
hermes --tui launches the native OpenTUI engine (Bun) when HERMES_TUI_ENGINE=opentui (env) or display.tui_engine=opentui (config); Ink stays the default and the shipping path is untouched. - _resolve_tui_engine() (env > config > ink); refuses opentui on Windows/Termux (no Bun) -> falls back to ink with a notice. - _make_opentui_argv() -> [bun, src/entry.real.tsx] (no build step). - _bun_bin() with HERMES_BUN override. - Branch at top of _make_tui_argv BEFORE _ensure_tui_node (Bun-only host must not bootstrap Node). - Gate _launch_tui NODE_OPTIONS/--max-old-space-size on engine==ink (Bun is JSC; the V8 flag errors/ignores). Verified end-to-end via tmux: real hermes --tui -> Bun -> OpenTUI -> real Python gateway streamed a real reply. No-flag default still ink.
116 lines
3.7 KiB
JavaScript
116 lines
3.7 KiB
JavaScript
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const os = require('node:os')
|
|
const path = require('node:path')
|
|
const test = require('node:test')
|
|
const { pathToFileURL } = require('node:url')
|
|
|
|
const {
|
|
DEFAULT_FETCH_TIMEOUT_MS,
|
|
encryptDesktopSecret,
|
|
resolveReadableFileForIpc,
|
|
resolveTimeoutMs,
|
|
sensitiveFileBlockReason
|
|
} = require('./hardening.cjs')
|
|
|
|
test('resolveTimeoutMs falls back to defaults and accepts overrides', () => {
|
|
assert.equal(resolveTimeoutMs(undefined), DEFAULT_FETCH_TIMEOUT_MS)
|
|
assert.equal(resolveTimeoutMs(0), DEFAULT_FETCH_TIMEOUT_MS)
|
|
assert.equal(resolveTimeoutMs(-25), DEFAULT_FETCH_TIMEOUT_MS)
|
|
assert.equal(resolveTimeoutMs('2750'), 2750)
|
|
})
|
|
|
|
test('encryptDesktopSecret requires available secure storage', () => {
|
|
assert.equal(
|
|
encryptDesktopSecret('', { isEncryptionAvailable: () => true, encryptString: () => Buffer.alloc(0) }),
|
|
null
|
|
)
|
|
|
|
assert.throws(
|
|
() => encryptDesktopSecret('token', { isEncryptionAvailable: () => false, encryptString: () => Buffer.alloc(0) }),
|
|
/Secure token storage is unavailable/
|
|
)
|
|
})
|
|
|
|
test('encryptDesktopSecret stores safeStorage base64 payload', () => {
|
|
const secret = encryptDesktopSecret('token-123', {
|
|
isEncryptionAvailable: () => true,
|
|
encryptString: value => Buffer.from(`enc:${value}`, 'utf8')
|
|
})
|
|
|
|
assert.deepEqual(secret, {
|
|
encoding: 'safeStorage',
|
|
value: Buffer.from('enc:token-123', 'utf8').toString('base64')
|
|
})
|
|
})
|
|
|
|
test('sensitiveFileBlockReason blocks obvious secret file patterns', () => {
|
|
assert.match(String(sensitiveFileBlockReason('/tmp/.env')), /\.env/)
|
|
assert.equal(sensitiveFileBlockReason('/tmp/.env.example'), null)
|
|
assert.match(String(sensitiveFileBlockReason('/Users/me/.ssh/id_ed25519')), /SSH/)
|
|
assert.match(String(sensitiveFileBlockReason('/tmp/server-cert.pem')), /\.pem/)
|
|
})
|
|
|
|
test('resolveReadableFileForIpc validates existence type size and sensitivity', async t => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-hardening-'))
|
|
t.after(() => fs.rmSync(tempDir, { recursive: true, force: true }))
|
|
|
|
const textPath = path.join(tempDir, 'notes.txt')
|
|
fs.writeFileSync(textPath, 'hello world', 'utf8')
|
|
|
|
const fromRelative = await resolveReadableFileForIpc('notes.txt', {
|
|
baseDir: tempDir,
|
|
maxBytes: 256,
|
|
purpose: 'File preview'
|
|
})
|
|
assert.equal(fromRelative.resolvedPath, textPath)
|
|
assert.equal(fromRelative.stat.size, 11)
|
|
|
|
const fromFileUrl = await resolveReadableFileForIpc(pathToFileURL(textPath).toString(), {
|
|
purpose: 'File preview'
|
|
})
|
|
assert.equal(fromFileUrl.resolvedPath, textPath)
|
|
|
|
await assert.rejects(
|
|
resolveReadableFileForIpc('missing.txt', {
|
|
baseDir: tempDir,
|
|
purpose: 'Text preview'
|
|
}),
|
|
/file does not exist/
|
|
)
|
|
|
|
const nestedDir = path.join(tempDir, 'directory')
|
|
fs.mkdirSync(nestedDir)
|
|
await assert.rejects(
|
|
resolveReadableFileForIpc(nestedDir, {
|
|
purpose: 'Text preview'
|
|
}),
|
|
/path points to a directory/
|
|
)
|
|
|
|
const largePath = path.join(tempDir, 'large.txt')
|
|
fs.writeFileSync(largePath, 'x'.repeat(40), 'utf8')
|
|
await assert.rejects(
|
|
resolveReadableFileForIpc(largePath, {
|
|
maxBytes: 8,
|
|
purpose: 'File preview'
|
|
}),
|
|
/file is too large/
|
|
)
|
|
|
|
const envPath = path.join(tempDir, '.env')
|
|
fs.writeFileSync(envPath, 'SECRET_TOKEN=123', 'utf8')
|
|
await assert.rejects(
|
|
resolveReadableFileForIpc(envPath, {
|
|
purpose: 'File preview'
|
|
}),
|
|
/blocked for sensitive file/
|
|
)
|
|
|
|
const envTemplatePath = path.join(tempDir, '.env.example')
|
|
fs.writeFileSync(envTemplatePath, 'EXAMPLE_TOKEN=value', 'utf8')
|
|
const envTemplate = await resolveReadableFileForIpc(envTemplatePath, {
|
|
purpose: 'File preview'
|
|
})
|
|
assert.equal(envTemplate.resolvedPath, envTemplatePath)
|
|
})
|