Phase 8: Hub API (/capabilities, read-only /ask, /notify)

The project side of the frozen Hub contract
(central_ai_llm_master/docs/ask-contract.md), on this service's own port:

  GET  /capabilities  the routing topics the Hub reads, project "compass_nhs"
  POST /ask           one short, read-only answer about a child's school work
  POST /notify        accept the contract's notification shape, deliver via Apprise

All three require the shared bearer token SCHOOL_HUB_TOKEN; with no token they
answer 503. GET /health now also carries "ok": true and "hub_api".

/ask answers deterministically (no model on the path) from the stored Compass
items, reusing dashboard.Classify so the answer and the dashboard never
disagree. It is one short sentence, for example "Harry has 8 school events
today from 8:55am to 6:00pm and 6 overdue tasks", with the buckets in `data`.
It reads only, enforced by a Reader interface with no write method and by
TestHubAskReadsOnly.

Fail loudly (FR-12): a missing or failed scan is never an empty day. /ask says
due work is unknown, and an answer older than 36 hours carries a stale warning.
Permission follows the contract: a parent may ask about any child; anyone else
only about themselves.

/notify follows the house interpretation already used by internet_use_control:
the contract shape is accepted and delivered to the named person, recorded in
notifications with scope "hub". An unknown person is refused, not misdelivered.

Live proof on .13 (deploy/verify-phase8.sh):
  /health        200, ok true
  /capabilities  200, project compass_nhs; unsigned 401
  /ask           200, the sentence above, duration 1ms
  /notify        400 on a bad level (auth proven, nothing sent), then 200 with
                 a real delivery to Sam on email and WhatsApp

The live Hub registration (HUB_PROJECTS + HUB_PROJECT_COMPASS_NHS_TOKEN +
hub restart) is a change to a running family service and is pending approval.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 19:47:32 +11:00
parent 09639c5125
commit 9cbfa7db9b
13 changed files with 1720 additions and 19 deletions

View File

@@ -4,8 +4,36 @@ A school service for Harry and Finn: **Compass** for the daily schedule and
notices, **Teams** for homework and assignments. It stores what is due per child,
puts overdue work first, and tells the family before work is late.
> Build spec: [`AGENTS.md`](AGENTS.md). This repository is at **Phase 7** (notify
> and send via Apprise). See §11 of the spec for the phase order.
> Build spec: [`AGENTS.md`](AGENTS.md). This repository is at **Phase 8** (the
> Hub API). See §11 of the spec for the phase order.
## Status — Phase 8
Phase 8 exposes the project side of the frozen Hub contract
(`central_ai_llm_master/docs/ask-contract.md`) on this service's own port
(FR-11):
| Method | Path | Notes |
|---|---|---|
| `GET` | `/capabilities` | `{"project":"compass_nhs","topics":[…],"people":[…]}`, matching the Hub's `routing.md` row. |
| `POST` | `/ask` | **Read-only.** Answers from the stored Compass items in one short sentence; returns the contract's `answer: null` + `reason` when it cannot answer. A failed or stale scan is stated in the answer, never as "nothing due" (FR-12). |
| `POST` | `/notify` | Accepts the contract's `{"from","level","message","person"}` and delivers it through Apprise. |
All three require `Authorization: Bearer $SCHOOL_HUB_TOKEN`. With no token set
they answer `503`. See [`docs/phase-8-hub-notes.md`](docs/phase-8-hub-notes.md).
```bash
make verify-phase8 # /health ok, /capabilities, /ask, /notify auth
deploy/verify-phase8.sh --send # also delivers one real /notify message to Sam
# The Hub's own client shape, against the live service:
curl -sS -H "Authorization: Bearer $SCHOOL_HUB_TOKEN" -H 'Content-Type: application/json' \
-d '{"question":"what does Harry have on today at school?","asker":"sam","person":"harry"}' \
http://127.0.0.1:8792/ask
```
The `/ask` answers are deterministic (no model on the path), so the Hub's 3
second cap is never the reason an answer is missing.
## Status — Phase 7
@@ -98,8 +126,8 @@ Built and deployed on `.13`:
- Its own container and port **8792**, behind Caddy at
`school.lab.audasmedia.com.au`.
Not built yet (later phases): the Hub API. The service itself does **not** scan
the school; n8n triggers the scan. Teams (FR-15) is a separate source.
Not built yet (later phases): Teams (FR-15) and the Phase 9 documentation set.
The service itself does **not** scan the school; n8n triggers the scan.
### Fail loudly (FR-12)
@@ -120,6 +148,7 @@ internal/parse/ HTML -> structured items (tasks, messages, events)
internal/llm/ OmniRoute client (importance classification only)
internal/apprise/ Apprise API client (tag-based, redacts errors)
internal/brief/ build + deliver the morning briefs (FR-9, FR-14)
internal/hub/ the Hub contract: capabilities + read-only answers (FR-11)
internal/calendar/ Home Assistant calendar client + the idempotent sync
internal/auth/ session login (signed cookies, CSRF)
internal/web/ HTTP handlers, templates, static assets
@@ -139,8 +168,8 @@ docker compose up -d --build
Generate secrets with the built image:
```bash
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.5.0-phase7 gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.5.0-phase7 hash-password 'the-password'
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.0-phase8 gen-secret
docker run --rm --entrypoint /app/school compass-teams-nhs/school:0.6.0-phase8 hash-password 'the-password'
```
Note: Docker Compose interpolates `$VAR` inside `env_file`. A bcrypt hash
@@ -149,7 +178,8 @@ contains `$`, so escape each one as `$$` in the env file.
## Verify
```bash
make verify-phase1
make verify-phase1 # container, /health, login, the Caddy route
make verify-phase8 # the Hub API: /capabilities, /ask, /notify
```
It checks the container is healthy, `GET /health` returns 200, the login page