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>
79 lines
3.1 KiB
Go
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}
|
|
}
|