feat: lift shell read restrictions and page bash/read tool outputs to 200 lines

This commit is contained in:
Agent committed 2026-10-12 22:33:11 +00:00
1 parent ee78e6d3a8
commit de37af4b8c
7 files changed
+213 -32

No files matched your search

+1 -1
View File
@@ -38,7 +38,7 @@ function load() {
globalThis.window = { __ModuleLoader__: { load: ({ factory }) => { mod = factory((id) => (id === 'react' ? React : P)) } }, setTimeout }
new Function(SRC)()
let view
mod.apply({ slots: { inject: (_n, f) => f(), register: (_m, C) => { view = C } } })
mod.apply({ slots: { inject: (_n, f) => { const r = f(); if (r && typeof r.next === 'function') [...r] }, register: (m, C) => { if (m.key === 'edit') view = C } } })
return { React, view }
}
+34 -1
View File
@@ -1,7 +1,7 @@
// Guard rules for shell commands, focused on inline interpreter scripts.
import test from 'node:test'
import assert from 'node:assert/strict'
import { shellViolation, inlineScriptViolation } from '../index.js'
import { shellViolation, inlineScriptViolation, pageText, pageShellResult } from '../index.js'
const ALLOW = [
// harmless computation one-liners
@@ -25,6 +25,14 @@ const ALLOW = [
// unrelated commands
'git status',
'npm test 2>&1 | tail -5',
// reading / searching through the shell is allowed (output is paged by the bash wrapper)
'cat README.md',
'head -50 index.js',
'sed -n 10,20p index.js',
'grep -rn foo .',
'find . -name "*.js"',
'ls -R lib',
`awk '{print $1}' f.txt`,
]
const BLOCK = [
@@ -61,6 +69,12 @@ const BLOCK = [
`sudo node -e "require('fs')"`,
`/usr/bin/node -e "require('fs')"`,
`ls && node -e "require('fs')"`,
// shell writes stay blocked
`awk '{print > "out.txt"}' f.txt`,
`awk 'BEGIN{system("rm x")}'`,
'find . -name "*.tmp" -delete',
'sed -i s/a/b/ f.txt',
'echo x > f.txt',
]
test('harmless inline one-liners and normal commands are allowed', () => {
@@ -76,3 +90,22 @@ test('denial message points at the right tools', () => {
assert.match(m, /read\/grep\/edit/)
assert.match(m, /computation-only one-liners are fine/)
})
test('pageText caps output at pageLines and names the next read', () => {
const text = Array.from({ length: 450 }, (_, i) => `l${i + 1}`).join('\n') + '\n'
let saved
const p = pageText(text, 200, (t) => { saved = t; return '/tmp/x.txt' })
assert.equal(saved, text)
assert.equal(p.text.split('\n').length, 201)
assert.match(p.text, /\[output lines 1-200 of 450\. Read lines 201-400: read file_path="\/tmp\/x\.txt" offset=201/)
assert.equal(pageText('a\nb\n', 200, () => { throw new Error('no save') }), null)
})
test('pageShellResult pages stdout and leaves short streams alone', () => {
const long = Array.from({ length: 250 }, (_, i) => String(i)).join('\n')
const v = pageShellResult({ kind: 'foreground', stdout: { text: long, truncated: false }, stderr: { text: 'e', truncated: false } }, 200, () => '/tmp/full.txt')
assert.equal(v.stdout.spillPath, '/tmp/full.txt')
assert.match(v.stdout.text, /stdout lines 1-200 of 250/)
assert.equal(v.stderr.text, 'e')
assert.deepEqual(pageShellResult({ kind: 'background', jobId: 'j' }, 200), { kind: 'background', jobId: 'j' })
})
+4 -2
View File
@@ -162,8 +162,10 @@ test('guard denies shell edits/reads/searches and non-hashline write tools via t
return next()
}
assert.equal((await gate('bash', { command: 'sed -i s/a/b/ f.txt' })).kind, 'deny')
assert.equal((await gate('bash', { command: 'grep -rn foo .' })).kind, 'deny')
assert.equal((await gate('bash', { command: 'cat README.md' })).kind, 'deny')
assert.equal((await gate('bash', { command: 'grep -rn foo .' })).kind, 'allow')
assert.equal((await gate('bash', { command: 'cat README.md' })).kind, 'allow')
assert.equal((await gate('bash', { command: 'sed -n 1,5p README.md' })).kind, 'allow')
assert.equal((await gate('bash', { command: 'cat a > b' })).kind, 'deny')
h.write('exists.txt', 'old\n')
const d = await gate('write', { file_path: 'exists.txt', content: 'y' })
assert.equal(d.kind, 'deny')