feat(exec): add template aliases, tolerant read-only validation, and thread titling filter

This commit is contained in:
operator
2026-10-05 15:14:24 +00:00
parent 8e6dd1e892
commit 5ff32b5202
3 changed files with 188 additions and 17 deletions
+65 -3
View File
@@ -1053,6 +1053,60 @@ OPS = {
# identity -> set of ops. 'master' may invoke everything. Unknown identities
# get the read-only subset. Per-agent tokens inherit their agent name as the
# identity; tighten per agent here as needed.
# ---- read-only tolerance + hyphenated aliases used by job templates ----
_ARG_SYNONYMS = {'job': 'name', 'id': 'name', 'job_name': 'name', 'unit': 'name'}
def _tolerant_args(spec, args):
"""Read-only ops: drop unknown args (mapping common synonyms) instead of failing.
Returns (args, dropped_names)."""
if args is None:
return {}, []
if not isinstance(args, dict):
return args, []
args, dropped = dict(args), []
for _ in range(len(args) + 1):
try:
spec['validate'](args)
break
except OpError as e:
m = re.match(r'unknown arg: (\S+)', str(e))
if m and m.group(1) in args:
k = m.group(1)
v = args.pop(k)
syn = _ARG_SYNONYMS.get(k)
if syn and syn not in args:
args[syn] = v
else:
dropped.append(k)
continue
if 'takes no' in str(e):
dropped.extend(args)
args = {}
continue
break
return args, dropped
def _register_aliases():
alias = {'job-list': 'cron.runs', 'quality.validate': 'quality.check',
'vars-list': 'vars.list', 'fleet-status': 'health.check'}
for new, old in alias.items():
if new not in OPS and old in OPS:
OPS[new] = dict(OPS[old], desc=OPS[old]['desc'] + f' (alias of {old})')
def _ctl(*words):
return lambda a: [sys.executable, os.path.join(BIN_DIR, 'box-ctl.py'), *words]
def _noargs(raw):
return {}
for new, words, desc in (('loop-status', ('loop-status',), 'Fleet loop health (read-only)'),
('timer-list', ('timer', 'list'), 'List box timers (read-only)')):
if new not in OPS:
OPS[new] = {'validate': _noargs, 'build': _ctl(*words), 'timeout': 30,
'side_effecting': False, 'desc': desc}
_register_aliases()
PERMISSIONS = {
'master': set(OPS),
'operator-main': set(OPS),
@@ -1211,12 +1265,17 @@ class Handler(BaseHTTPRequestHandler):
'op': op})
self._send(403, {'error': 'forbidden for this identity'})
return
dropped = []
try:
spec = OPS[op]
if not spec.get('side_effecting'):
args, dropped = _tolerant_args(spec, args)
clean = spec['validate'](args)
argv = spec['build'](clean)
except OpError as e:
self._send(400, {'error': f'bad args: {e}'})
self._send(400, {'error': f'bad args: {e}', 'op': op,
'desc': OPS[op].get('desc', ''),
'hint': 'GET /ops for the allowlist; see desc for usage'})
return
except Exception as e:
sys.stderr.write(f'Validation/build exception for {op}: {e}\n')
@@ -1247,12 +1306,15 @@ class Handler(BaseHTTPRequestHandler):
audit({'event': 'exec_done', 'ident': ident, 'peer': peer,
'op': op, 'rc': rc, 'duration_s': dt, 'ok': ok})
if ok:
self._send(200, {'rc': rc, 'stdout': out, 'stderr': err,
'duration_s': dt})
resp = {'rc': rc, 'stdout': out, 'stderr': err, 'duration_s': dt}
if dropped:
resp['dropped_args'] = dropped
self._send(200, resp)
else:
self._send(500, {'error': err, 'rc': rc})
# ------------------------------------------------------- main
def main():