feat(box): expand agent tool capabilities with files, web, and service ops
This commit is contained in:
+123
-1
@@ -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:]
|
||||
|
||||
Reference in New Issue
Block a user