2026-09-13 16:29:03 +00:00
|
|
|
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 { scanCommand, scanToolUse, buildGuardConfigContent, DENY_GLOBS, isSafeRecursiveRm } from '../src/guard.mjs'
|
|
|
|
|
|
|
|
|
|
test('scanCommand blocks destructive git and shell commands', () => {
|
|
|
|
|
for (const cmd of ['git commit -m "x"', 'git push origin main', 'rm -rf build', 'git reset --hard HEAD~1', 'npm publish', 'git filter-branch --tree-filter x']) {
|
|
|
|
|
assert.ok(scanCommand(cmd), `expected blocked: ${cmd}`)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('scanCommand allows harmless commands', () => {
|
|
|
|
|
for (const cmd of ['git status', 'git diff HEAD', 'pnpm test', 'npm run build', 'ls -la', 'rm file.txt']) {
|
|
|
|
|
assert.equal(scanCommand(cmd), null, `expected allowed: ${cmd}`)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('scanCommand allows recursive deletes that only touch temp dirs', () => {
|
|
|
|
|
const cmds = [
|
|
|
|
|
'rm -rf /tmp/kilo/tf-test',
|
|
|
|
|
'rm -rf /tmp/build /var/tmp/cache',
|
|
|
|
|
"rm -rf /tmp/kilo/tf-test && mkdir -p /tmp/kilo/tf-test && cd /tmp/kilo/tf-test",
|
|
|
|
|
'rm -fr /tmp/scratch',
|
|
|
|
|
'rm -r -f /tmp/one /tmp/two',
|
|
|
|
|
]
|
|
|
|
|
for (const cmd of cmds) assert.equal(scanCommand(cmd), null, `expected allowed: ${cmd}`)
|
|
|
|
|
assert.equal(isSafeRecursiveRm('rm -rf /tmp/kilo/tf-test'), true)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-13 19:38:01 +00:00
|
|
|
test('scanCommand allows relative recursive deletes after cd into a temp dir', () => {
|
|
|
|
|
const cmds = [
|
|
|
|
|
'cd /tmp && rm -rf tpl-onlineshop',
|
|
|
|
|
'cd /tmp/kilo && rm -rf ./scratch',
|
|
|
|
|
'cd /tmp && rm -rf a b && git clone ssh://x/a b',
|
|
|
|
|
'mkdir -p /tmp/x && cd /tmp/x && rm -rf build',
|
|
|
|
|
'cd /var/tmp && rm -rf cache',
|
|
|
|
|
]
|
|
|
|
|
for (const cmd of cmds) assert.equal(scanCommand(cmd), null, `expected allowed: ${cmd}`)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('scanCommand blocks relative recursive deletes after cd into a non-temp dir', () => {
|
|
|
|
|
const cmds = [
|
|
|
|
|
'cd /repo && rm -rf src',
|
|
|
|
|
'cd /home/joe/work && rm -rf node_modules',
|
|
|
|
|
'cd /tmp && rm -rf /home/joe/work', // an unsafe absolute target wins
|
|
|
|
|
'cd /tmp && rm -rf ..', // escapes the temp root
|
|
|
|
|
]
|
|
|
|
|
for (const cmd of cmds) {
|
|
|
|
|
const hit = scanCommand(cmd)
|
|
|
|
|
assert.ok(hit, `expected blocked: ${cmd}`)
|
|
|
|
|
assert.equal(hit.id, 'rm-recursive-force')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-13 16:29:03 +00:00
|
|
|
test('scanCommand still blocks recursive deletes outside temp dirs', () => {
|
|
|
|
|
const cmds = [
|
|
|
|
|
'rm -rf build',
|
|
|
|
|
'rm -rf .',
|
|
|
|
|
'rm -rf ..',
|
|
|
|
|
'rm -rf /',
|
|
|
|
|
'rm -rf /tmp',
|
|
|
|
|
'rm -rf /home/joe/sources/wiz4apps',
|
|
|
|
|
'rm -rf /tmp/ok /home/joe/work', // one unsafe target makes the whole command unsafe
|
|
|
|
|
'cd /repo && rm -rf src',
|
|
|
|
|
]
|
|
|
|
|
for (const cmd of cmds) {
|
|
|
|
|
const hit = scanCommand(cmd)
|
|
|
|
|
assert.ok(hit, `expected blocked: ${cmd}`)
|
|
|
|
|
assert.equal(hit.id, 'rm-recursive-force')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('DENY_GLOBS no longer blanket-denies every rm -rf', () => {
|
|
|
|
|
assert.ok(!DENY_GLOBS.includes('*rm -rf*'))
|
|
|
|
|
assert.ok(!DENY_GLOBS.includes('*rm -fr*'))
|
|
|
|
|
assert.ok(DENY_GLOBS.some((g) => g.includes('/home/')))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('scanToolUse only inspects bash tool calls', () => {
|
|
|
|
|
assert.equal(scanToolUse({ tool: 'edit', state: { input: { filePath: 'x' } } }), null)
|
|
|
|
|
const hit = scanToolUse({ tool: 'bash', state: { input: { command: 'git commit -am x' } } })
|
|
|
|
|
assert.equal(hit.id, 'git-commit')
|
|
|
|
|
assert.equal(hit.command, 'git commit -am x')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('buildGuardConfigContent emits deny globs when bash is unspecified', () => {
|
|
|
|
|
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-guard-'))
|
|
|
|
|
fs.writeFileSync(path.join(project, 'kilo.json'), JSON.stringify({ permission: { glob: 'allow' } }))
|
|
|
|
|
const content = buildGuardConfigContent(project)
|
|
|
|
|
const parsed = JSON.parse(content)
|
|
|
|
|
for (const glob of DENY_GLOBS) assert.equal(parsed.permission.bash[glob], 'deny')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('buildGuardConfigContent bails out on a scalar bash permission', () => {
|
|
|
|
|
const project = fs.mkdtempSync(path.join(os.tmpdir(), 'kilo-guard-'))
|
|
|
|
|
fs.writeFileSync(path.join(project, 'kilo.json'), JSON.stringify({ permission: { bash: 'allow' } }))
|
|
|
|
|
assert.equal(buildGuardConfigContent(project), null)
|
|
|
|
|
})
|