Files
Sam Rolfe 8c42daf4d9 Phase 9 wiring: name the Hub project internet_control
The Hub routes by the project name in its human-owned routing.md, which
already names this project `internet_control` (the placeholder row from the
Hub brief). The frozen contract requires the /capabilities name to match that
row, so /capabilities now reports `internet_control` as well.

Found while wiring the live Hub (approved confirmation 60b235f3): the Hub
routed the screen-time question to `internet_control` and returned
`no_client` against a project registered as `internet_use_control`. Aligning
the name, rather than editing the Hub repository or its routing file, makes
the end-to-end round-trip work.

Live proof after the rename and the Hub registration:

  POST http://192.168.20.13:8788/ask
  {"text":"how long have the kids been on screen today?","asker":"sam","source":"console"}
  200 {"status":"ok","answer":"The kids have had 1h 22m of screen time today.",
       "route":{"kind":"project","project":"internet_control"}}

Repository, module and service names are unchanged; only the Hub-facing
project string changed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-13 19:33:54 +11:00

79 lines
3.1 KiB
Go

// Package hub implements the project side of the Hub contract
// (central_ai_llm_master/docs/ask-contract.md) for the Internet Use Control
// Control Service.
//
// The contract is one-directional by design: the Hub asks a project about its
// own data and the project answers. A project never learns the Hub's data model
// and the Hub never learns the project's.
//
// GET /capabilities -> the topics this project can answer (Hub routing)
// POST /ask -> a short answer about this project's data
// POST /notify -> accept a notification and deliver it to Sam
//
// /ask and /capabilities are read-only. /ask answers from SQLite and never
// writes, so the "read only" promise (AGENTS.md §10 rule 9) is a testable
// property, not a comment (TestAskWritesNothing).
package hub
// ProjectName is the value the Hub matches in routing.md. The Hub's topic map
// (and src/understand/rules.go) already names this project `internet_control`,
// and the contract requires the /capabilities name to match that row, so this
// is the Hub-facing name. The repository and the service keep the fuller
// "Internet Use Control" name.
const ProjectName = "internet_control"
// Capabilities is the GET /capabilities reply. The Hub reads it to route a
// question to this project. Keep the topic list short and plain; it holds no
// private data.
type Capabilities struct {
Project string `json:"project"`
Topics []string `json:"topics"`
People []string `json:"people"`
}
// DefaultCapabilities returns the topics this service can answer. They match
// the screen-time domain in AGENTS.md §1 and §4.
func DefaultCapabilities() Capabilities {
return Capabilities{
Project: ProjectName,
Topics: []string{"internet", "screen time", "screens", "usage", "devices", "blocking", "kids"},
People: []string{"harry", "finn"},
}
}
// AskRequest is the POST /ask body (ask-contract.md).
type AskRequest struct {
Question string `json:"question"`
Asker string `json:"asker"`
Person string `json:"person,omitempty"`
Source string `json:"source,omitempty"`
}
// AskResponse is the POST /ask reply. A nil Answer means the project cannot
// answer and Reason explains why. The shape is frozen by the contract.
type AskResponse struct {
Answer *string `json:"answer"`
Data map[string]any `json:"data,omitempty"`
Expires string `json:"expires,omitempty"`
Reason string `json:"reason,omitempty"`
}
// NotifyRequest is the POST /notify body (ask-contract.md, "Notify"). The
// contract defines it as project-to-Hub; this service accepts the same shape so
// a caller (Hub, admin app, or a future phone tamper report) can have Sam
// notified without inventing a second shape.
type NotifyRequest struct {
From string `json:"from"`
Level string `json:"level"`
Message string `json:"message"`
Person string `json:"person,omitempty"`
}
// answerText is a small helper so pointer handling stays in one place.
func answerText(s string) *string { return &s }
// cannotAnswer builds the contract's "cannot answer" reply.
func cannotAnswer(reason string) AskResponse {
return AskResponse{Answer: nil, Reason: reason}
}