--- name: chatto-platform-plugin description: "Use when working with the Hermes Chatto platform plugin: its tools, cron delivery patterns, or extending the plugin." version: 1.2.0 author: Paul Klumpp license: MIT metadata: hermes: tags: [chatto, hermes, plugin, gateway, adapter, chattolib, tools] --- # Hermes Chatto Platform Plugin ## When to Use Using or developing/debugging the Chatto gateway plugin: its plugin tools (`chatto_send_dm`, `chatto_send_room`, `chatto_list_rooms`) or adding new tools to the plugin. Repo-internal rules (editing, linting, vendoring) live in the plugin root's `AGENTS.md` — see Development workflow. ## The plugin tools and when to use them ### chatto_list_rooms(name?) — resolve room names to IDs Lists every room the bot is a member of: `room_id`, `name`, `kind` (channel/DM), `threading_mode`, `archived`. Optional `name` filters case-insensitively by substring (unnamed DM rooms are skipped when a filter is set; without a filter they appear with an empty name and label `(DM room )`). **Primary use case: cron `deliver` targets.** The Hermes cron `deliver` field takes only `platform:chat_id` strings and does NO name resolution. When a scheduled job's report should go to a channel, call this tool first, take the `room_id`, and use it as the `chat_id` part. Never trust chat names in session/origin metadata as room IDs — resolve live. ### chatto_send_room(room_id|room_name, message, create_thread?, thread_root_event_id?) — post into a channel Resolves the room case-insensitively by name OR verifies the ID, then posts. Works for any room the bot is a member of. - `create_thread=true` posts the message as the ROOT of a NEW thread (first chunk = root, further chunks inside), then best-effort `follow_thread`. - `thread_root_event_id` posts INSIDE an existing thread. Mutually exclusive with `create_thread`. - Response carries `thread_root_event_id` + `created_thread`. **Mentions instead of DMs:** to notify a user inside a channel, mention them in the message TEXT (`@login, hier ist …`) — Chatto renders room-text mentions. A bot cannot deliver a personal notification any other way (see the DM limitation below). ### chatto_send_dm(user_login|user_id, message) — DM a user Looks up the user by login or ID and posts into the DM room. **Precondition: the recipient must have DMed the bot once.** Bots can NEVER start a DM — by design (Chatto dev-docs, guides/operations/permissions: "Only people can start a DM... A bot cannot start a DM, even with message.post."). No permission or role change lifts this. On `permission_denied`: the recipient must message the bot first, then the existing-room path works. ## Cron delivery patterns - **Split content destination from report destination.** Content goes out via `chatto_send_room`/`chatto_send_dm` called BY the job prompt; the cron `deliver` field routes only the final confirmation/report (e.g. `deliver: "chatto-platform:"` — resolve the ID via `chatto_list_rooms`). A `deliver` target without a `:thread_id` segment lands in the channel's main chat. - Pattern: prompt says "post X to room Y via chatto_send_room, mention @user, then answer with a short confirmation" — `deliver` stays `origin` or points at a separate admin channel. - Test any redesigned flow with `cronjob_manage action='run'` before the next scheduled fire. ## Plugin tool contract (ctx.register_tool) — for extending the plugin - **Async handlers need `is_async=True`** — the default is sync; an `async def` handler registered without it returns an unawaited coroutine and the dispatcher fails with `tool_result_contract: unsupported result type: coroutine` (`tools/registry.py: dispatch()`). - **Handlers must return JSON strings** (`json.dumps({...})`), never plain dicts — `_normalize_handler_result` accepts only str or the multimodal envelope. - **Config access: never read `os.environ` in handlers.** `.env` credentials like `CHATTO_TOKEN` are profile-scoped and NOT visible in cron fires' os.environ, and `hermes_validate_config` never runs in a fresh cron session. Resolve config through `_chatto_config_for_tools()` in adapter.py: (1) module-level `_resolved_chatto_config` set by `hermes_validate_config` in a running gateway; (2) empty `PlatformConfig(enabled=True, extra={})` wrapped in `ChattoConfiguration`, re-filled from `build_profile_secret_scope(get_hermes_home())` (`agent.secret_scope`) — re-reads the profile's own `.env`, works in gateway, cron and CLI alike; (3) clean error. Do NOT use `load_gateway_config().platforms[name]` as fallback (returns None in fresh processes) and do NOT leak a `token` parameter into the tool schema. - **Reuse `_chatto_tool_client(args, cfg)`** for client construction in new handlers instead of re-inlining token/base_url resolution. - **Verify in BOTH contexts:** a gateway-session `tool_call` is not enough — also fire a `cronjob_manage action='run'` test when the tool will run from cron, because cron sessions exercise the config-fallback tier gateway sessions never hit. Debug order: `tool_result_contract` error → contract bug (async/return type); `AttributeError` on the client → wrong stub/service name; `permission_denied` → server-side limit. ## Development workflow 1. **Read the repo's `AGENTS.md` first** — it is the binding rulebook for all work on this repo: editing, linting/type-checking gates, shell-script checks, typed access, overriding, capabilities, README discipline. This skill does not repeat those rules. 2. **Gateway restart is REQUIRED after plugin code changes** — the running Hermes session keeps the old module in memory. Ask the user to restart, then verify via `tool_call`. 3. **Before requesting a restart, verify config-dependent fixes in an isolated subprocess** simulating the cron scope: pop the relevant env vars (`CHATTO_TOKEN`, `CHATTO_BASE_URL`), `sys.path.insert` the Hermes kernel and plugin dir, set `HERMES_HOME`, call the handler directly. A green isolated test proves the cron path before burning a restart cycle. 4. **Bundled skills:** register additional skills in `register(ctx)` via `ctx.register_skill(name, path_to_SKILL.md)` — see the `skills/` directory of this plugin.