feat(messaging): balanced TOOL parsing, DM shorthand, box.exec, tools.list

- response-harvester: extract [TOOL]/[EXEC] JSON args with balanced-brace
  scanning (']' and nesting inside args no longer truncate calls); add
  [DM {...}] shorthand mapping to dm.send; native aliases (dm, box,
  tools) plus arg-synonym normalization; formatters and expanded hints.
- exec-constrained: new read-only box.exec op (27 allowlisted box-ctl
  reads) and tools.list op backed by --list-ops for dynamic discovery.
- prompt_envelope: advertise dm.send/box.exec/tools.list in every timer
  DM; add dm_call builder.
- lookup_engine + regex_patterns.json: canonical tool_call pattern
  accepts the DM engine, ']' in args, one nesting level.
- tests/test_tool_calls.py: 38 tests; docs/INBAND-MESSAGING-SPEC.md:
  accepted decision record (Final).
This commit is contained in:
operator-main
2026-10-06 07:29:16 +00:00
parent 345eb09559
commit f2640397ed
7 changed files with 571 additions and 24 deletions
+84
View File
@@ -994,6 +994,67 @@ def _swarm_results_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'box-ctl.py'), 'swarm-results', a['swarm_id']]
# box.exec allowlist: read-only box-ctl actions only (mirrors the read-only
# subset of box-ctl.py IDEMPOTENT_ACTIONS). Actions with side-effecting
# subverbs (main-loop enable/disable), path args (git-diff/git-log), or
# complex argv shapes (job-next, policy-*, quality-validate, job-result)
# are deliberately excluded.
BOX_EXEC_NOARG_ACTIONS = frozenset({
'fleet-status', 'watchdog-alerts', 'relay-health', 'cdp-latency',
'chrome-errors', 'identity-audit', 'timer-list', 'job-list',
'vars-list', 'strat-list', 'loop-status', 'loop-health', 'loop-breaks',
'thread-list', 'dm-log', 'unread', 'swarm-list', 'quality-check',
'git-status',
})
BOX_EXEC_ONEARG_ACTIONS = frozenset({
'job-get', 'job-status', 'timer-status', 'vars-get', 'vars-history',
'strat-get', 'swarm-status', 'swarm-results',
})
_BOX_ARG_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9/_.-]{0,127}$')
def _box_exec_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
for k in raw:
if k not in {'action', 'arg', 'agent'}:
raise OpError(f'unknown arg: {k}')
action = raw.get('action')
if action in BOX_EXEC_NOARG_ACTIONS:
if raw.get('arg') is not None:
raise OpError(f'{action} takes no arg')
return {'action': action}
if action in BOX_EXEC_ONEARG_ACTIONS:
arg = raw.get('arg')
if arg is None:
return {'action': action}
if not isinstance(arg, str) or not _BOX_ARG_RE.fullmatch(arg):
raise OpError('arg must match safe token')
return {'action': action, 'arg': arg}
raise OpError(f'unknown or non-read-only box action: {action}')
def _box_exec_build(a):
argv = [sys.executable, os.path.join(BIN_DIR, 'box-ctl.py'), a['action']]
if a.get('arg'):
argv.append(a['arg'])
return argv
def _tools_list_validate(raw):
if not isinstance(raw, dict):
raise OpError('args must be an object')
for k in raw:
if k not in {'agent'}:
raise OpError(f'unknown arg: {k}')
return {}
def _tools_list_build(a):
return [sys.executable, os.path.join(BIN_DIR, 'exec-constrained.py'),
'--list-ops']
# op -> {validate, build, timeout, side_effecting, description}
OPS = {
'dm.send': {
@@ -1207,6 +1268,16 @@ OPS = {
'timeout': 10, 'side_effecting': False,
'desc': 'Canary no-op for watchdogs',
},
'box.exec': {
'validate': _box_exec_validate, 'build': _box_exec_build,
'timeout': 60, 'side_effecting': False,
'desc': 'Call a read-only box-ctl action (fleet-status, dm-log, job-get, ...)',
},
'tools.list': {
'validate': _tools_list_validate, 'build': _tools_list_build,
'timeout': 15, 'side_effecting': False,
'desc': 'List all exec ops with descriptions (dynamic discovery)',
},
}
# identity -> set of ops. 'master' may invoke everything. Unknown identities
@@ -1495,6 +1566,19 @@ def main():
ap.add_argument('--key-file',
default='/home/super/.exec-constrained-key.pem')
ap.add_argument('--work-dir', default='/home/super')
ap.add_argument('--list-ops', action='store_true',
help='Print the op registry as JSON and exit (backs tools.list)')
args = ap.parse_args()
if args.list_ops:
print(json.dumps({
'ok': True,
'ops': [{'op': k, 'desc': v.get('desc', ''),
'side_effecting': bool(v.get('side_effecting', False)),
'timeout': v.get('timeout', 30)}
for k, v in sorted(OPS.items())],
}))
return
args = ap.parse_args()
TOKEN_FILE, TOKEN_DIR = args.token_file, args.token_dir