# Routing Rules (`routing.md`) **Purpose:** tell the running Hub how to choose a destination. **Owner:** Sam (human-edited). **Status:** v1 draft. First pass. Edit the tables; keep the three sections. **Related:** `hub_brief.md` (design), `docs/hub-api.md` (inbound API, section 5), `docs/ask-contract.md` (project interface). This file is **not** `AGENTS.md`. `AGENTS.md` tells coding agents how to work. This file tells the Hub how to choose. Keep them separate. --- ## How the Hub uses this file For each request, the Hub runs the checks in this order: 1. **Never-route check.** If the request is a control action (section 3), return `status: "refused"`. Stop. Do not ask an LLM. 2. **Read capabilities.** Read the topic list from each project's `/capabilities` (`docs/ask-contract.md`). 3. **Read this file.** Read the topic map (section 1) and the overlap rule (section 2). 4. **Ask the LLM.** Give the LLM the question, the capability lists, and this file. The LLM returns one of four answers: - `route` — send to one project. - `fanout` — ask several projects, then merge. - `clarify` — ask the user one short question. - `no_match` — say it does not know. 5. **Route.** Send the request. The action call holds no LLM (brief rule 4). A project name in this file must match the `project` value in that project's `/capabilities` reply. A new project adds one row here and one `/capabilities` reply. Build the ability once. **Fanout:** use it only when the question clearly needs two topics at once, for example "what do I have on today?" (school + email + calendar). Default to `route`, then `clarify` when unsure. --- ## 1. Topic to project map The Hub uses these topics as the plain-language keys for routing. Keep the list short. Add a topic only when a project really answers it. | Topic (what the user says) | Project | Kind | Status | |---|---|---|---| | email, inbox, mail, message, unread | `email_agent` | query `/ask` | placeholder — not built | | school, Compass, homework, timetable, class, NHS appointment | `compass_nhs` | query `/ask` | placeholder — not built | | screen time, internet use, time limits, what did the kids do online | `internet_control` | query `/ask` | placeholder — not built | | lights, lamp, switch, plug, timer, reminder, calendar event, device, thermostat | `home_assistant` | command (direct) | active service | | speak, say it out loud, house speakers, announce | `speakers` | command (direct) | active service | | search, look up, what is, who is, weather, news | `web_search` | command (direct) | placeholder — not built | | notify, notification, alert me, remind my phone | `apprise` | command (direct) | active service | | file a job, plan this, research this, slow task, work bench | `paperclip` | decision (approval) | active service | | phone screen, tap, open an app, type on the phone, scroll | `artemis` | command (direct, action only) | placeholder — not built | | visitor, front door, who is at the door | `front_door_cam` | peer intercom | placeholder — not built | | talk to the house voice, Hey Jervis | `jervis` | way in (voice only) | active service | | chat, per-person chat, dsh | `dsh` | peer (keeps its own chat) | active service | **Not destinations.** These are ways in, not projects to route to. A request that names them still routes to the real destination. | Name | Why it is not a destination | |---|---| | `phone_app` | Way in. Sends voice to the Hub. | | `console_portal` | Way in and dashboard. | | `admin_app` | Way in for restricted actions. Calls the internet control service directly. | ### Placeholder entries Each project below is planned but **not built**. Keep the row and the placeholder so the file is ready. Replace the placeholder when the project declares its `/capabilities`. | Project | Expected topics (placeholder) | First build step | |---|---|---| | `email_agent` | email, inbox, mail | brief build order step 5 | | `compass_nhs` | school, homework, timetable | brief build order step 5 | | `internet_control` | screen time, internet use, limits | brief build order step 6 | | `artemis` | phone screen control | brief build order step 7 | | `web_search` | search, weather, facts | pending | | `front_door_cam` | visitor | pending | --- ## 2. Preferred project when two overlap Some topics fit two projects. The Hub uses this preference order. The left project wins. If the question still fits both, the Hub uses `clarify`. | Overlap | Preferred | Other | Reason | |---|---|---|---| | "today" / "schedule" | `home_assistant` (calendar) | `compass_nhs` (timetable) | The calendar answers the whole day. School answers school only. | | "what do I have on" | `fanout` (`compass_nhs` + `email_agent` + `home_assistant`) | one project | Merge the three, then answer once. | | "message" | `email_agent` (email) | `dsh` (chat) | An email question is about the inbox, not the chat. A chat question stays in dsh. | | "reminder" | `home_assistant` | `apprise` | Home Assistant owns timers and reminders. Apprise only delivers. | | "appointment" | `compass_nhs` | `home_assistant` (calendar) | A health or school appointment is NHS or school data first. | | "what did the kids do" | `internet_control` | `email_agent` | Online use is the internet control service's data. | | "is the doorbell" / "visitor" | `front_door_cam` | `artemis` | The camera owns the visitor. Artemis only acts on the screen. | Rules: 1. A **domain question** goes to the project that owns the data. Do not fanout for one topic. 2. A **day summary** fans out. Merge to one short sentence. 3. Two projects with equal claim, and the preference table does not decide, use `clarify`. Ask one short question. Example: "School or email?" 4. Never fanout to more than three projects. 5. A project that is down returns `status: "unavailable"` for that part. Do not fail the whole fanout. --- ## 3. Never-route list (control actions) The Hub **never** routes a control action. It returns `status: "refused"` and points at the admin app. This list is the authority for that decision. Add every new control action here before you add the ability. A control action is a **high-consequence change**. It changes parental controls, limits, or a child's access. Only the admin app does it, and it calls the internet control service **directly**. The Hub is not in that path (brief rules 8 and 11). The Hub must refuse when the request means any of these: | Control action | Refuse? | |---|---| | Cut the kids' internet. | **Refuse** | | Restore the kids' internet. | **Refuse** | | Change an internet time limit or daily allowance. | **Refuse** | | Change an internet schedule (bedtime, school hours). | **Refuse** | | Add, remove, or change a blocklist or content filter. | **Refuse** | | Change any parental control on any account or device. | **Refuse** | | Add, change, or remove a child profile or a device rule. | **Refuse** | | Pause, lock, or unlock a family device for a policy reason. | **Refuse** | | Any other state change on the internet control service. | **Refuse** | The alternative reply is always the same: > "Use the admin app to change internet limits." `status: "refused"`, `route.kind: "refused"`, `route.reason: "high_consequence_control"`. See `docs/hub-api.md`, section 5. ### Not in the never-route list These do **not** route as control. Keep them out of this list. | Action | Path | Why | |---|---|---| | Turn on a lamp. | Hub → Home Assistant | Fast command. Low consequence. | | Set a timer or reminder. | Hub → Home Assistant | Fast command. Low consequence. | | Add a calendar event. | Hub → calendar project | Fast write. Approval not required. | | Speak on the house speakers. | Hub → speakers | Output only. | | File a Paperclip job. | Hub → approval → Paperclip | Slow decision. Needs a tap, but it does route through the Hub. | | Ask about screen time. | Hub → `internet_control` `/ask` | Read only. A query is allowed. | **A spoken keycode is not the lock.** Voice may ask. Only the admin app confirms. --- ## 4. What the admin app may send to the Hub The admin app is the only trusted door. It uses its own token (`HUB_TOKEN_ADMIN`). It may send the Hub: 1. **Queries and fast commands** — the same as any way in, through `POST /ask`. 2. **A confirmation decision** — one yes or no for a Hub-owned approval, through `POST /approve`. In v1 this files a Paperclip job. The admin app **must not** send a parental control change to the Hub. There is no Hub route for it. The admin app calls the **internet control service directly**, then shows the result. The Hub does not record, route, or run that change. In one line: **the admin app may send the Hub a query, a fast command, or a Hub-owned approval decision — never a parental control change.** --- ## 5. Reply shape (short note) The Hub returns the reply to the source and keeps it short (brief rule 6). The shape comes from `source` and `want` in `docs/hub-api.md`, section 9. This file does not change the reply shape. --- ## Change log | Version | Date | Change | |---|---|---| | v1 | 2026-10-10 | First draft. Topic map, overlap rule, never-route list, admin app line. |