Draft routing.md v1: topic map, overlap rule, never-route list

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-10 17:08:00 +11:00
parent 449fdd02c5
commit cb57495f08

195
routing.md Normal file
View File

@@ -0,0 +1,195 @@
# 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. |