Site Icon THEREiSmodule-contract한국어

Title :

module-contract

Author
[Yahum]

Module contract (Hub ↔ Module)

Secretary is split into one hub + many modules. The hub and modules only have to keep the contract (HTTP API) below, and they don’t import each other’s code. Modules can be written in any language.

Client ──HMAC──▶ Hub ──internal token──▶ Module (mail, tapo, sms, kakao, summary ...)
▲ │
└───── event callback ────┘
Task Hub Module
Users, device keys, HMAC auth O X
Permission (scope) checks, role ceilings O (based on the manifest) Declares only
Approval queue O (based on the manifest) Declares only
Audit log O Its own server log only
Input validation, actual execution, external service keys X O

1. Registering a module

Two lines of hub settings and you’re done. The hub code isn’t touched.

SECRETARY_MODULES=mail=http://secretary-mail:8080,tapo=http://secretary-tapo:8080
SECRETARY_MODULE_TOKENS=mail=<random value>,tapo=<random value>
  • Module names: 2–24 lowercase letters, digits, and hyphens. system, approvals, modules, and events are used by the hub, so they can’t be used.
  • Create a separate token for each module (openssl rand -hex 32). Even if one module is compromised, it can’t call other modules.

2. Authentication

  • Hub → module: Authorization: Bearer <that module's token>
  • Module → hub (events): The same token. The hub figures out which module it is from the token.
  • If the token is wrong, the module returns 401. Module ports aren’t opened to the outside (internal Docker network only).

3. APIs a module provides

GET /manifest

The module’s self-introduction. The hub rereads it every minute.

{
"name": "mail",
"title": "Mail",
"actions": {
"send": {
"title": "Send email",
"approval": true,
"roles": null,
"params": { "type": "object", "required": ["to", "subject", "body"] }
}
}
}
Field Meaning
name Must match the name registered with the hub
actions.<name> What it can do. The required scope is automatically <module>:<action> (e.g. mail:send)
approval If true, it isn’t executed right away but goes on the approval queue
roles List of roles allowed to use this action. null means all roles
params Description of the input format (JSON Schema). For client screens. The actual validation is done by the module

POST /actions/{action}/validate

Checks the input before execution and returns a one-line summary to show on the approval screen.

Request:

{
"params": { "to": ["grandma@family.example"], "subject": "Leaving work", "body": "Heading out now" },
"caller": { "user_id": "dad", "name": "Dad", "role": "admin", "email": "dad@example.com" }
}

Response: 200 {"summary": "Mail → grandma@family.example : Leaving work"} or 422 {"message": "Some recipients aren't allowed."}

POST /actions/{action}

Actually executes it. The request body is the same as validate. Actions that need approval are called after being approved. The module must check the input once more right before executing (settings may have changed between the request and the approval).

Response: 200 {"result": "Sent"} or 422 {"message": "..."}. Any other status code is treated by the hub as “execution failed.”

GET /healthz

200 {"ok": true}. No authentication. For the Docker health check.

4. Event callback (module → hub)

Things a module needs to report first (a Kakao notification came in, a conversation summary finished) are sent to the hub as events.

POST http://secretary:8080/internal/events
Authorization: Bearer <that module's token>
{ "type": "message.received", "user": "dad", "title": "Mom: What's for dinner?" }
  • If user is null, the event is visible to the whole family.
  • title is up to 200 characters. Don’t include sensitive content like full message bodies.
  • Clients pick them up with GET /v1/events. The hub deletes events older than 7 days.
  • /internal/* returns 404 for requests that came through Cloudflare. It can only be used on the internal network.

5. Checklist for building a new module

  1. Build the 4 APIs above (/manifest, /actions/{a}/validate, /actions/{a}, /healthz).
  2. Put external service keys only in that module’s container.
  3. Add the service to docker-compose.yml and attach it to the modules network (and egress too if it needs the internet).
  4. Add MODULE_TOKEN_<name> to .env, and add one line each to the hub’s SECRETARY_MODULES and SECRETARY_MODULE_TOKENS.
  5. Grant permission to devices: device add dad phone --scope tapo:on or --scope tapo:*