mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-16 09:31:37 +00:00
- chat-messages: match tool rows by overlapping query/context/preview values so preview-first `tool.progress` rows reliably adopt later stable-id `tool.start` payloads instead of spawning ghost rows or mis-merging parallel same-name calls; preserve prior args/result across phases. - tui_gateway: emit full args + parsed result on `tool.start` / `tool.complete`, drop redundant `tool.started` re-emit from `tool.progress`. - electron/main: prefer SOURCE_REPO_ROOT before PATH `hermes` in dev so local backend edits actually run; split hardening helpers into `electron/hardening.cjs` with tests. - thread/tool UI: one-shot enter animation keyed by stable ids, braille spinner for running rows, Cursor-like disclosure rows, drill-down + duration/count formatting via new tool-fallback-model. - composer: extract `text-utils`, drop liquid-glass overrides. - right-rail: split preview-pane into preview-console / preview-file. - runtime: incremental external-store runtime + runtime-readiness gate; onboarding store + tests; route-resume hook test. - regression tests for live tool reconciliation (parallel tools, id-less progress, preview-first rows, structured args/results).
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)
|
|
})
|