Status banner
Sign in for status

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.
  • /run and /capabilities require auth. /health does 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: true for transient errors (timeouts, upstream 5xx).
  • Nodes must return retryable: false for terminal errors (bad payload, unsupported kind).
  • Lucille Core uses job_id for 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.