feat(box): expand agent tool capabilities with files, web, and service ops

This commit is contained in:
operator
2026-10-05 03:51:43 +00:00
parent 581bbfe3b4
commit 2132d15cb4
4 changed files with 379 additions and 1 deletions
+123 -1
View File
@@ -609,6 +609,102 @@ def _vars_set_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'box-ctl.py'), 'vars-set', a['name'], a['value']]
def _files_read_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
allowed = {'path', 'lines'}
for k in raw:
if k not in allowed:
raise OpError(f'unknown arg: {k}')
p = raw.get('path')
if not isinstance(p, str) or not p.strip() or '..' in p:
raise OpError('path must be a safe relative or repo path without ".."')
return {
'path': p.strip(),
'lines': _opt_int(raw.get('lines', 100), 1, 1000, 'lines') or 100
}
def _files_read_build(a):
# Pass JSON args via stdin to box-sys-op.py
return [sys.executable, os.path.join(BIN_DIR, 'box-sys-op.py'), 'files.read']
def _files_write_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
allowed = {'path', 'content'}
for k in raw:
if k not in allowed:
raise OpError(f'unknown arg: {k}')
p = raw.get('path')
if not isinstance(p, str) or not p.strip() or '..' in p:
raise OpError('path must be a safe relative or repo path without ".."')
content = raw.get('content')
if not isinstance(content, str):
raise OpError('content must be a string')
if len(content.encode('utf-8')) > 64 * 1024:
raise OpError('content exceeds max size 64KB')
return {'path': p.strip(), 'content': content}
def _files_write_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'box-sys-op.py'), 'files.write']
def _web_fetch_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
allowed = {'url'}
for k in raw:
if k not in allowed:
raise OpError(f'unknown arg: {k}')
u = raw.get('url')
if not isinstance(u, str) or not u.strip():
raise OpError('url must be a string')
if not (u.startswith('http://') or u.startswith('https://')):
raise OpError('url must start with http:// or https://')
return {'url': u.strip()}
def _web_fetch_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'box-sys-op.py'), 'web.fetch']
def _service_status_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
allowed = {'unit', 'name'}
for k in raw:
if k not in allowed:
raise OpError(f'unknown arg: {k}')
unit = raw.get('unit') or raw.get('name')
if not isinstance(unit, str) or not NAME_RE.fullmatch(unit):
raise OpError('unit must match safe identifier')
return {'unit': unit}
def _service_status_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'box-sys-op.py'), 'service.status']
def _service_restart_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
allowed = {'unit', 'name'}
for k in raw:
if k not in allowed:
raise OpError(f'unknown arg: {k}')
unit = raw.get('unit') or raw.get('name')
if not isinstance(unit, str) or not NAME_RE.fullmatch(unit):
raise OpError('unit must match safe identifier')
return {'unit': unit}
def _service_restart_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'box-sys-op.py'), 'service.restart']
# op -> {validate, build, timeout, side_effecting, description}
OPS = {
'dm.send': {
@@ -701,6 +797,31 @@ OPS = {
'timeout': 30, 'side_effecting': True,
'desc': 'Update intrinsic loop variable',
},
'files.read': {
'validate': _files_read_validate, 'build': _files_read_build,
'timeout': 30, 'side_effecting': False,
'desc': 'Read sandboxed file within repository tree',
},
'files.write': {
'validate': _files_write_validate, 'build': _files_write_build,
'timeout': 30, 'side_effecting': True,
'desc': 'Write sandboxed file within repository tree',
},
'web.fetch': {
'validate': _web_fetch_validate, 'build': _web_fetch_build,
'timeout': 30, 'side_effecting': False,
'desc': 'Perform safe HTTP/HTTPS GET request with SSRF guard',
},
'service.status': {
'validate': _service_status_validate, 'build': _service_status_build,
'timeout': 30, 'side_effecting': False,
'desc': 'Inspect allowlisted fleet systemd service status',
},
'service.restart': {
'validate': _service_restart_validate, 'build': _service_restart_build,
'timeout': 30, 'side_effecting': True,
'desc': 'Restart allowlisted fleet systemd service',
},
'exec.ping': {
'validate': _health_validate,
'build': lambda a: ['/bin/echo', 'PONG'],
@@ -891,7 +1012,8 @@ class Handler(BaseHTTPRequestHandler):
).hexdigest()[:16]})
t0 = time.monotonic()
try:
p = subprocess.run(argv, capture_output=True, text=True,
stdin_input = json.dumps(clean) if op.startswith(('files.', 'web.', 'service.')) else None
p = subprocess.run(argv, input=stdin_input, capture_output=True, text=True,
timeout=spec['timeout'], cwd=WORK_DIR)
rc = p.returncode
out = p.stdout[-MAX_OUTPUT:]