mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(gateway): /toolsets and /platforms read on the owner for a local session
Neither has a messaging handler; main's classic CLI and its TUI slash worker printed them from the process holding the agent, which on the shared gateway is the owner. Ink and Desktop got unsupported_command. The report bodies move verbatim into hermes_cli/cli_info_lines.py (byte-identical CLI output, checked old vs new), and slash.exec runs them for a local session (gateway/session_info_reads.py): /toolsets marks THIS session's toolsets (policy.current_toolsets), /platforms reads the owning profile's gateway config. One text on every surface. Proof: test_session_read_commands runs both through slash.exec on a real daemon (session created with toolsets=['todo']).
This commit is contained in:
1 parent
0d93ab57fa
commit
5707483e83
6 files changed
+117
-67
No files matched your search
@@ -34,6 +34,9 @@ _SETTING_COMMANDS = frozenset({'reasoning', 'voice', 'approvals', 'codex-runtime
|
||||
# Typed `/approve` / `/deny`: the messaging handler resolves THIS session's waiting approval (its
|
||||
# route is the session key), the same waiter `approval.respond` answers by prompt id.
|
||||
_APPROVAL_COMMANDS = frozenset({'approve', 'deny'})
|
||||
# Reports with no messaging handler: the classic CLI's own text, built by the owner for this
|
||||
# session (gateway/session_info_reads.py).
|
||||
_INFO_READS = frozenset({'toolsets', 'platforms'})
|
||||
|
||||
# Kanban verbs that only read the board. ``create`` would also subscribe the LOCAL chat to
|
||||
# messaging notifications, and every other verb mutates the board.
|
||||
@@ -67,7 +70,7 @@ def command_verdict(name, arg=''):
|
||||
if (name in _CONTROL_COMMANDS or name in _HANDLER_CONTROLS or name in _PROMPT_COMMANDS
|
||||
or name in _SETTING_COMMANDS or name in _APPROVAL_COMMANDS):
|
||||
return 'control'
|
||||
if name in _READ_COMMANDS and _READ_ARGUMENTS.get(name, lambda _arg: True)(arg):
|
||||
if name in _INFO_READS or (name in _READ_COMMANDS and _READ_ARGUMENTS.get(name, lambda _arg: True)(arg)):
|
||||
return 'read'
|
||||
return 'refused'
|
||||
|
||||
@@ -195,6 +198,10 @@ async def execute_command(connection, ref, params, *, dispatch=False):
|
||||
return _skill_directive(name, arg, live.route)
|
||||
return await asyncio.to_thread(build)
|
||||
|
||||
if canonical in _INFO_READS:
|
||||
from gateway.session_info_reads import run_info_read
|
||||
return await run_info_read(authority, live, canonical)
|
||||
|
||||
from gateway.platforms.base import MessageEvent
|
||||
from gateway.session_command_sink import capture
|
||||
typed = '/' + canonical + (' ' + arg if arg else '')
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Reports with no messaging handler that the shared gateway serves a local session itself.
|
||||
|
||||
Main's classic CLI and its TUI slash worker printed `/toolsets` and `/platforms` from the process
|
||||
that held the agent. On the shared gateway that process is the owner, so ``slash.exec`` runs the
|
||||
classic CLI's own line builders (hermes_cli/cli_info_lines.py) here, inside the owning profile, for
|
||||
this session's toolsets: the same text on the classic CLI, Ink and every other local client.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
def _toolsets(authority, policy):
|
||||
from hermes_cli.cli_info_lines import toolsets_lines
|
||||
return toolsets_lines(list(policy.current_toolsets(policy.config(authority, secrets=False))) if policy else [])
|
||||
|
||||
|
||||
def _platforms(authority, policy):
|
||||
from hermes_cli.cli_info_lines import platforms_lines
|
||||
return platforms_lines()
|
||||
|
||||
|
||||
READS = {'toolsets': _toolsets, 'platforms': _platforms}
|
||||
|
||||
|
||||
async def run_info_read(authority, live, name) -> dict:
|
||||
"""Render *name* off the owner loop, in the owning profile; None policy = no frozen route."""
|
||||
from gateway.session_authorities import owner_scope
|
||||
from gateway.session_policy import policy_for_source
|
||||
policy = policy_for_source(authority.runner, live.source)
|
||||
|
||||
def build():
|
||||
with owner_scope(authority):
|
||||
return '\n'.join(READS[name](authority, policy)).strip('\n')
|
||||
return {'type': 'exec', 'output': await asyncio.to_thread(build)}
|
||||
@@ -0,0 +1,61 @@
|
||||
"""The classic CLI's `/toolsets` and `/platforms` reports as plain lines.
|
||||
|
||||
Shared by the classic CLI (`CLIInfoMixin.show_toolsets` / `_show_gateway_status`) and the shared
|
||||
gateway, which runs the same report for a local session's `slash.exec` (gateway/session_info_reads.py):
|
||||
one text on every surface, read where the state lives (the session's toolsets, the gateway's
|
||||
platform config).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from agent.i18n import t
|
||||
|
||||
|
||||
def _box(title: str, width: int) -> list[str]:
|
||||
pad = width - len(title)
|
||||
return ["+" + "-" * width + "+", "|" + " " * (pad // 2) + title + " " * (pad - pad // 2) + "|",
|
||||
"+" + "-" * width + "+"]
|
||||
|
||||
|
||||
def toolsets_lines(enabled) -> list[str]:
|
||||
"""``/toolsets``: every toolset, ``(*)`` on the ones *enabled* names."""
|
||||
from toolsets import get_all_toolsets, get_toolset_info
|
||||
out = ["", *_box(t("cli.toolsets.header"), 58), ""]
|
||||
for name in sorted(get_all_toolsets()):
|
||||
info = get_toolset_info(name)
|
||||
if info:
|
||||
marker = "(*)" if enabled and name in enabled else " "
|
||||
out.append(f" {marker} {name:<18} [{info['tool_count']:>2} tools] - {info['description']}")
|
||||
return [*out, "", f" {t('cli.toolsets.currently_enabled_legend')}", "",
|
||||
f" {t('cli.toolsets.tip_enable_all')}", f" {t('cli.toolsets.example')}", ""]
|
||||
|
||||
|
||||
def platforms_lines() -> list[str]:
|
||||
"""``/platforms``: the messaging platforms the profile's gateway config enables."""
|
||||
from hermes_constants import display_hermes_home
|
||||
from gateway.config import load_gateway_config, Platform
|
||||
out = ["", *_box(t("cli.gateway_status.header"), 60), ""]
|
||||
config_file = f"{display_hermes_home()}/config.yaml"
|
||||
try:
|
||||
config = load_gateway_config()
|
||||
out += [f" {t('cli.gateway_status.platform_config_header')}", " " + "-" * 55]
|
||||
platform_status = {
|
||||
Platform.TELEGRAM: ("Telegram", "TELEGRAM_BOT_TOKEN"),
|
||||
Platform.DISCORD: ("Discord", "DISCORD_BOT_TOKEN"),
|
||||
Platform.SLACK: ("Slack", "SLACK_BOT_TOKEN"),
|
||||
Platform.WHATSAPP: ("WhatsApp", "WHATSAPP_ENABLED")}
|
||||
for platform, (name, env_var) in platform_status.items():
|
||||
pconfig = config.platforms.get(platform)
|
||||
if pconfig and pconfig.enabled:
|
||||
home = config.get_home_channel(platform)
|
||||
home_str = f" → {home.name}" if home else ""
|
||||
out.append(f" ✓ {name:<12} {t('cli.gateway_status.enabled')}{home_str}")
|
||||
else:
|
||||
out.append(f" ○ {name:<12} {t('cli.gateway_status.not_configured', env_var=env_var)}")
|
||||
return [*out, "", f" {t('cli.gateway_status.conversations_persist')}", "",
|
||||
f" {t('cli.gateway_status.to_start')}", " python cli.py --gateway", "",
|
||||
f" {t('cli.gateway_status.config_file', path=config_file)}", ""]
|
||||
except Exception as e:
|
||||
return [*out, f" {t('cli.gateway_status.load_error', error=str(e))}", "",
|
||||
f" {t('cli.gateway_status.to_configure')}", f" {t('cli.gateway_status.step_env_vars')}",
|
||||
" TELEGRAM_BOT_TOKEN=your_token", " DISCORD_BOT_TOKEN=your_token",
|
||||
f" {t('cli.gateway_status.step_config', path=config_file)}", ""]
|
||||
@@ -443,25 +443,8 @@ class CLIInfoMixin:
|
||||
|
||||
def show_toolsets(self):
|
||||
"""Display available toolsets with kawaii ASCII art."""
|
||||
from toolsets import get_all_toolsets, get_toolset_info
|
||||
all_toolsets = get_all_toolsets()
|
||||
|
||||
print()
|
||||
_ascii_box(t("cli.toolsets.header"), 58)
|
||||
print()
|
||||
|
||||
for name in sorted(all_toolsets.keys()):
|
||||
info = get_toolset_info(name)
|
||||
if info:
|
||||
marker = "(*)" if self.enabled_toolsets and name in self.enabled_toolsets else " "
|
||||
print(f" {marker} {name:<18} [{info['tool_count']:>2} tools] - {info['description']}")
|
||||
|
||||
print()
|
||||
print(f" {t('cli.toolsets.currently_enabled_legend')}")
|
||||
print()
|
||||
print(f" {t('cli.toolsets.tip_enable_all')}")
|
||||
print(f" {t('cli.toolsets.example')}")
|
||||
print()
|
||||
from hermes_cli.cli_info_lines import toolsets_lines
|
||||
print("\n".join(toolsets_lines(self.enabled_toolsets)))
|
||||
|
||||
def _handle_whoami_command(self):
|
||||
"""Display slash-command access for the local CLI surface."""
|
||||
@@ -554,48 +537,8 @@ class CLIInfoMixin:
|
||||
|
||||
def _show_gateway_status(self):
|
||||
"""Show status of the gateway and connected messaging platforms."""
|
||||
from hermes_constants import display_hermes_home
|
||||
from gateway.config import load_gateway_config, Platform
|
||||
|
||||
print()
|
||||
_ascii_box(t("cli.gateway_status.header"), 60)
|
||||
print()
|
||||
|
||||
try:
|
||||
config = load_gateway_config()
|
||||
print(f" {t('cli.gateway_status.platform_config_header')}")
|
||||
print(" " + "-" * 55)
|
||||
platform_status = {
|
||||
Platform.TELEGRAM: ("Telegram", "TELEGRAM_BOT_TOKEN"),
|
||||
Platform.DISCORD: ("Discord", "DISCORD_BOT_TOKEN"),
|
||||
Platform.SLACK: ("Slack", "SLACK_BOT_TOKEN"),
|
||||
Platform.WHATSAPP: ("WhatsApp", "WHATSAPP_ENABLED")}
|
||||
for platform, (name, env_var) in platform_status.items():
|
||||
pconfig = config.platforms.get(platform)
|
||||
if pconfig and pconfig.enabled:
|
||||
home = config.get_home_channel(platform)
|
||||
home_str = f" → {home.name}" if home else ""
|
||||
print(f" ✓ {name:<12} {t('cli.gateway_status.enabled')}{home_str}")
|
||||
else:
|
||||
print(f" ○ {name:<12} {t('cli.gateway_status.not_configured', env_var=env_var)}")
|
||||
|
||||
print()
|
||||
print(f" {t('cli.gateway_status.conversations_persist')}")
|
||||
print()
|
||||
print(f" {t('cli.gateway_status.to_start')}")
|
||||
print(" python cli.py --gateway")
|
||||
print()
|
||||
print(f" {t('cli.gateway_status.config_file', path=f'{display_hermes_home()}/config.yaml')}")
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f" {t('cli.gateway_status.load_error', error=str(e))}")
|
||||
print()
|
||||
print(f" {t('cli.gateway_status.to_configure')}")
|
||||
print(f" {t('cli.gateway_status.step_env_vars')}")
|
||||
print(" TELEGRAM_BOT_TOKEN=your_token")
|
||||
print(" DISCORD_BOT_TOKEN=your_token")
|
||||
print(f" {t('cli.gateway_status.step_config', path=f'{display_hermes_home()}/config.yaml')}")
|
||||
print()
|
||||
from hermes_cli.cli_info_lines import platforms_lines
|
||||
print("\n".join(platforms_lines()))
|
||||
|
||||
def _print_random_tip(self) -> None:
|
||||
"""Best-effort discovery tip (startup + /clear); never raises."""
|
||||
|
||||
@@ -41,7 +41,7 @@ def test_local_session_reads_report_this_session_and_refuse_their_write_forms(tm
|
||||
|
||||
async def probe(desc):
|
||||
async with websocket(home, desc) as ws:
|
||||
created = await rpc(ws, 'session.create', request_id='reads', source='cli', toolsets=[], cwd=str(work))
|
||||
created = await rpc(ws, 'session.create', request_id='reads', source='cli', toolsets=['todo'], cwd=str(work))
|
||||
sid = created['result']['session_id']
|
||||
await rpc(ws, 'prompt.submit', session_id=sid, input_id='warm', text='WARM_READS')
|
||||
deadline = time.monotonic() + 45
|
||||
@@ -54,7 +54,8 @@ def test_local_session_reads_report_this_session_and_refuse_their_write_forms(tm
|
||||
await asyncio.sleep(.1)
|
||||
for command in ('usage', 'insights', 'profile', 'topup', 'diff', 'memory', 'kanban list',
|
||||
'suggestions', 'bundles', 'usage reset', 'memory approval on', 'kanban create x',
|
||||
'suggestions catalog', 'agents', 'sessions', 'platform'):
|
||||
'suggestions catalog', 'agents', 'sessions', 'platform',
|
||||
'toolsets', 'platforms'):
|
||||
try:
|
||||
reply = await rpc(ws, 'slash.exec', session_id=sid, command=command)
|
||||
except TimeoutError:
|
||||
@@ -84,6 +85,9 @@ def test_local_session_reads_report_this_session_and_refuse_their_write_forms(tm
|
||||
assert out['kanban list'] == '(no matching tasks)', out['kanban list']
|
||||
assert 'No suggested automations' in out['suggestions'], out['suggestions']
|
||||
assert str(home / 'skill-bundles') in out['bundles'], out['bundles']
|
||||
# Reports with no messaging handler: the classic CLI's text for this session's toolsets.
|
||||
assert '(*) todo ' in out['toolsets'] and ' web ' in out['toolsets'], out['toolsets']
|
||||
assert 'Telegram' in out['platforms'] and 'TELEGRAM_BOT_TOKEN' in out['platforms'], out['platforms']
|
||||
# Write forms, messaging-model state and gateway-wide views stay refused (the per-session settings
|
||||
# run: tests/gateway/test_session_setting_commands.py).
|
||||
for command in ('usage reset', 'memory approval on', 'kanban create x', 'suggestions catalog',
|
||||
|
||||
@@ -26,7 +26,7 @@ narrower routes (`hermes_cli/gateway_chat_commands.py`).
|
||||
setting to this local session (its next turn and `config.get`), saving config.yaml only where the
|
||||
handler does; `client-local (cli_only)` commands are terminal/UI features of a client.
|
||||
|
||||
103 registry commands: 29 control, 15 read, 59 refused.
|
||||
103 registry commands: 29 control, 17 read, 57 refused.
|
||||
|
||||
| Command | Gateway (`slash.exec`) | Classic CLI | Ink | Desktop | ACP | Notes |
|
||||
|---|---|---|---|---|---|---|
|
||||
@@ -86,7 +86,7 @@ handler does; `client-local (cli_only)` commands are terminal/UI features of a c
|
||||
| `/pet` | refused | HermesCLI._handle_pet_command | slash.exec | action: pet | refused | client-local (cli_only) |
|
||||
| `/plan` | control | HermesCLI._handle_plan_command | slash.exec | slash.exec | refused | |
|
||||
| `/platform` | refused | messaging-only: unknown command | slash.exec: refused | slash.exec: refused | refused | messaging adapters; `pause`/`resume` mutate them |
|
||||
| `/platforms` (/gateway) | refused | HermesCLI._show_gateway_status | slash.exec: refused | unavailable: terminal | refused | client-local (cli_only) |
|
||||
| `/platforms` (/gateway) | read | HermesCLI._show_gateway_status | slash.exec | unavailable: terminal | refused | client-local (cli_only) |
|
||||
| `/plugins` | refused | HermesCLI._cmd_plugins | slash.exec | unavailable: terminal | refused | client-local (cli_only) |
|
||||
| `/profile` | read | HermesCLI._handle_profile_command | slash.exec | action: profile | refused | |
|
||||
| `/prompt` (/compose) | refused | HermesCLI._handle_prompt_compose_command | local | slash.exec: refused | refused | client-local (cli_only) |
|
||||
@@ -120,7 +120,7 @@ handler does; `client-local (cli_only)` commands are terminal/UI features of a c
|
||||
| `/timestamps` (/ts) | refused | HermesCLI._handle_timestamps_command | slash.exec: refused | slash.exec: refused | refused | client-local (cli_only) |
|
||||
| `/title` | control | HermesCLI._cmd_title | canonical route | action: title | refused | |
|
||||
| `/tools` | refused | HermesCLI._handle_tools_command | canonical route | slash.exec: refused | refused | client-local (cli_only) |
|
||||
| `/toolsets` | refused | HermesCLI.show_toolsets | slash.exec: refused | unavailable: terminal | refused | client-local (cli_only) |
|
||||
| `/toolsets` | read | HermesCLI.show_toolsets | slash.exec | unavailable: terminal | refused | client-local (cli_only) |
|
||||
| `/topic` | refused | messaging-only: unknown command | slash.exec: refused | slash.exec: refused | refused | messaging-only |
|
||||
| `/topup` | read | HermesCLI._show_billing | sidecar: billing.state | slash.exec | refused | |
|
||||
| `/undo` | refused | HermesCLI._cmd_undo | canonical route | slash.exec: refused | refused | |
|
||||
|
||||
Reference in new issue
Block a user