HTTP Node Contract (V1)
This is a minimal, HTTP-only contract for decentralized workers ("nodes").
Endpoints
-
GET /health- No auth required.
- Response:
{"ok": true, "node_id": "node-1", "version": "dev", "ts": "2026-01-24T12:00:00Z"}
-
GET /capabilities- Auth required (see below).
- Response:
{ "node_id": "node-1", "version": "dev", "jobs": [ {"kind": "clockify.sync_weekly_rollups", "schema": {"type": "object"}} ] }
-
POST /run- Auth required.
- Request:
{ "job_id": "<uuid>", "kind": "clockify.sync_weekly_rollups", "payload": {"user_id": "<uuid>", "week_start": "YYYY-MM-DD"}, "attempt": 1, "lease_ms": 60000 } - Success response:
{"ok": true, "result": {"note": "handler result"}} - Error response:
{"ok": false, "error": "string message", "retryable": true}
Auth
- Header:
Authorization: Bearer <token> - Token is shared between Lucille Core and the node.
/runand/capabilitiesrequire auth./healthdoes not.
Encrypted payloads (optional)
The job payload may carry user data or secrets. When a node advertises a public
key (set as the public_key field of its LUCILLE_NODES_JSON entry on Core),
Core seals the payload to it before dispatch so it is confidential end-to-end,
not just in TLS transit. job_id and kind stay in clear so the node can route
before decrypting.
When encrypted, the payload field is an envelope instead of the raw object:
{
"job_id": "<uuid>",
"kind": "clockify.sync_weekly_rollups",
"payload": {
"encrypted": true,
"algorithm": "RSA-OAEP-AES256GCM",
"ciphertext": "<base64: 12-byte nonce + AES-256-GCM ciphertext of the payload JSON>",
"wrapped_key": "<base64: RSA-OAEP(SHA-256) of the 32-byte AES key>"
},
"attempt": 1,
"lease_ms": 60000
}
The node decrypts with its RSA private key (NODE_PRIVATE_KEY): unwrap
wrapped_key (RSA-OAEP/SHA-256) to recover the AES key, then AES-256-GCM-decrypt
ciphertext (first 12 bytes are the nonce). A node that receives an encrypted
payload without a configured private key must return
{"ok": false, "retryable": false}. This mirrors the worker result-encryption
envelope; the reference implementation is backend/app/node_crypto.py (Core
side) and nodes/template-python-http/app/main.py (node side).
Retry Semantics
- Nodes must return
retryable: truefor transient errors (timeouts, upstream 5xx). - Nodes must return
retryable: falsefor terminal errors (bad payload, unsupported kind). - Lucille Core uses
job_idfor idempotency. Handlers should be safe to re-run.
Timeouts and Payload Guidance
- Default client timeout: 15 seconds.
- Max payload size: keep JSON under ~64 KB for V1.
- Nodes should respond quickly and offload long-running work to their own queues if needed.