reuse_surface/hub/store.py: compose_state table tracking composed_at/stale, updated only on a *forced* recompose (refresh=true, webhook, future scheduled fallback) -- a plain GET still serves current best-effort data but never silently reports itself as freshly composed. reuse_surface/hub/webhooks.py: constant-time HMAC-SHA256 signature verification (fails closed on an empty/unconfigured secret) and path-only push-payload inspection (never parses file content, per design principle 2 -- webhook only decides whether to trigger a pull-based recompose). New endpoint POST /v1/webhooks/forgejo, accepting both X-Forgejo-Signature and X-Gitea-Signature headers since sibling repos migrate independently. GET /v1/federated and POST /v1/federated/compose now share an asyncio.Lock with the webhook so concurrent recompose triggers coalesce instead of overlapping. No separate /v1/recompose route was added -- POST /v1/federated/compose already did that job from earlier hub work; specs/FederationHubAPI.md now documents this explicitly instead of duplicating it. 28 new pytest cases (128 total pass). Live-verified: ran the actual hub service locally and sent a real HMAC-signed webhook over HTTP, confirming the full webhook-to-recompose path, signature rejection, and the irrelevant-path no-op. Does NOT deploy to the live reuse.coulomb.social hub -- that needs a container rebuild/push/k8s rollout, a separate production-deployment action out of scope here without explicit sign-off. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 KiB
Federation Hub API
Repository: reuse-surface
Artifact: specs/FederationHubAPI.md
Status: Draft 0.1 (REUSE-WP-0011-T01)
Schema: schemas/hub-registration.schema.yaml
1. Purpose
The federation hub is a hosted coordination service that records which repositories publish capability indexes and serves a composed federated index for agent discovery. It does not store capability entry Markdown bodies.
Companion deployment workplans: railiance-apps RAILIANCE-WP-0007 (Helm
release), RAILIANCE-WP-0008 (browser landing page).
Browser vs API routing
Production ingress (owned by railiance-apps) splits paths:
| Path | Handler |
|---|---|
GET / (HTTPS) |
Static landing page (reuse-surface-landing) — humans only |
GET /health, GET /v1/* |
Hub API (reuse-surface service) |
The landing page does not implement registration or federation; clients and
agents should use /health and /v1/* only. See
railiance-apps/docs/reuse-surface-on-railiance01.md.
2. Base URL and formats
| Item | Value |
|---|---|
| Default production URL | https://reuse.coulomb.social (A → 92.205.62.239) |
| API prefix | /v1 |
| Read formats | JSON (default), YAML via Accept: application/yaml or ?format=yaml |
| Write content type | application/json |
Environment variables for clients:
| Variable | Purpose |
|---|---|
REUSE_SURFACE_URL |
Service base URL (no trailing slash) |
REUSE_SURFACE_TOKEN |
Bearer token for write operations |
3. Authentication
| Endpoint class | Auth |
|---|---|
GET /health, GET /v1/repos, GET /v1/repos/{repo}, GET /v1/federated |
Public (read) |
POST /v1/repos, PATCH /v1/repos/{repo}, DELETE /v1/repos/{repo}, POST /v1/federated/compose |
Bearer token required |
Write requests must include:
Authorization: Bearer <REUSE_SURFACE_TOKEN>
Missing or invalid token → 401 Unauthorized.
4. Registration model
A registration mirrors federation url sources from
schemas/federation.schema.yaml, plus hub metadata:
| Field | Required | Notes |
|---|---|---|
repo |
yes | Slug [a-z][a-z0-9-]*; primary key |
url |
yes | HTTP(S) URL to capabilities.yaml |
enabled |
yes | Include in federated compose when true |
domain |
yes | e.g. helix_forge |
required |
no | Fail compose if fetch fails and no cache |
description |
no | Human-readable note |
cache_ttl_seconds |
no | Default 86400 |
auth_env |
no | Hub container env var for fetch auth (never returned in GET) |
auth_header |
no | Default Authorization |
registered_at |
hub | ISO-8601 UTC |
updated_at |
hub | ISO-8601 UTC |
registered_by |
no | Optional client-supplied actor label |
Local filesystem index paths are not accepted — registrations must use
published raw URLs.
5. Endpoints
5.1 GET /health
Liveness/readiness probe.
Response 200:
{
"status": "ok",
"service": "reuse-surface",
"version": "0.1.0"
}
5.2 GET /v1/repos
List all registrations (including disabled).
Response 200:
{
"count": 2,
"repos": [
{
"repo": "reuse-surface",
"url": "https://gitea.coulomb.social/coulomb/reuse-surface/raw/main/registry/indexes/capabilities.yaml",
"enabled": true,
"required": true,
"domain": "helix_forge",
"description": "Primary registry",
"cache_ttl_seconds": 86400,
"registered_at": "2026-06-15T12:00:00Z",
"updated_at": "2026-06-15T12:00:00Z"
}
]
}
auth_env is omitted from responses.
5.3 POST /v1/repos
Register a new repository. Auth required.
Request body: registration_request from schema.
Response 201: Full registration object.
Errors:
| Code | Condition |
|---|---|
400 |
Schema validation failure |
401 |
Missing/invalid token |
409 |
repo already registered |
5.4 GET /v1/repos/{repo}
Fetch one registration.
Response 200: Registration object.
Response 404: Unknown repo.
5.5 PATCH /v1/repos/{repo}
Update fields on an existing registration. Auth required.
Request body: registration_update from schema (at least one field).
Response 200: Updated registration.
Errors: 400, 401, 404.
5.6 DELETE /v1/repos/{repo}
Remove a registration. Auth required.
Response 204: No content.
Response 404: Unknown repo.
5.7 GET /v1/federated
Return the composed federated index from all enabled registrations.
Reuses WP-0010 remote fetch/cache logic server-side. Output shape matches
registry/indexes/federated.yaml:
version: 1
updated: "2026-06-15"
domain: helix_forge
collision_policy: warn
sources:
- repo: reuse-surface
url: https://...
count: 12
capabilities:
- id: capability.registry.register
source_repo: reuse-surface
source_url: https://...
# ... index fields ...
composed_at: "2026-07-07T16:22:09+00:00" # REUSE-WP-0019-T02
stale: false # REUSE-WP-0019-T02
composed_at/stale (added REUSE-WP-0019-T02) track forced recomposes
only (refresh=true, the webhook, or the scheduled fallback) — a plain
GET still serves current best-effort data (per-source cache_ttl_seconds
still applies) but never silently clears a staleness signal nothing
actually refreshed. composed_at is null until the first forced
recompose since the hub process's SQLite DB was created. stale: true
means a registry/indexes/ change was pushed (via webhook) since the last
forced recompose completed.
Query parameters:
| Param | Default | Meaning |
|---|---|---|
format |
json |
json or yaml |
refresh |
false |
Bypass remote cache when true; also updates composed_at and clears stale |
Warnings from compose (duplicate IDs, fetch fallbacks) are returned in response
header X-Federation-Warnings (semicolon-separated) for MVP; JSON envelope
extension is a future option. composed_at is also echoed as response header
X-Composed-At when set.
Response 200: Federated index document.
Response 502: Required source unavailable with no cache.
5.8 POST /v1/federated/compose
Trigger federated index refresh (same as GET /v1/federated?refresh=true).
Auth required. Useful for operators after bulk registration changes.
This is the hub's recompose endpoint referenced elsewhere as "trigger a
recompose" — there is no separate /v1/recompose route; this one already
does that job.
Response 200: Federated index document (with composed_at updated,
stale cleared).
5.9 POST /v1/webhooks/forgejo
Forgejo (or Gitea, during the transition) push-event webhook receiver. Added REUSE-WP-0019-T02. Design: webhook triggers, compose stays pull-based — the webhook never parses pushed file content into registry state; it only decides whether to trigger a pull-based recompose from the already-registered raw URLs.
Signature verification (required, not optional): HMAC-SHA256 over the
raw request body, hex-encoded, in X-Forgejo-Signature (or
X-Gitea-Signature — both accepted, since Forgejo is Gitea-compatible and
repos migrate independently). Secret from REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET
(never in code or committed config — route via the standard credential
mechanism for this deployment). A missing/wrong signature is 401; an
unconfigured secret is 503 (fails closed, not open).
Behavior:
- Verify signature (raw body, constant-time compare).
- Parse the push-event payload; check every commit's
added/modified/removedlists for any path underregistry/indexes/. - If none match:
200 {"accepted": false, "reason": "no registry/indexes/ change"}— no-op. - If a match: mark the compose state stale, run a real recompose
(
refresh=trueequivalent) under the same lock used byPOST /v1/federated/compose(concurrent triggers coalesce rather than overlap), recordcomposed_at, clearstale.
Response 200: {"accepted": true|false, ...}.
Response 401: Missing or invalid signature.
Response 503: Webhook secret not configured.
Response 502: Recompose failed (stale is deliberately left set so the
next check reports the failure honestly, not silently).
Org-level webhook rollout (single config covering all repos, T03) and the Forgejo Actions scheduled fallback for when webhooks are unavailable are separate, deployment-side follow-ups — this section covers the receiver contract only.
6. Error envelope
Non-2xx responses use:
{
"error": "validation_error",
"message": "Human-readable summary",
"details": ["optional field-level messages"]
}
error code |
HTTP |
|---|---|
validation_error |
400 |
unauthorized |
401 |
not_found |
404 |
conflict |
409 |
misconfigured |
503 |
compose_error |
502 |
7. Hub service configuration
| Env var | Required | Purpose |
|---|---|---|
REUSE_SURFACE_TOKEN |
yes | Write API bearer token |
REUSE_SURFACE_DB |
no | SQLite path (default /data/reuse.db) |
REUSE_SURFACE_CACHE_DIR |
no | Remote index cache (default /data/cache) |
REUSE_SURFACE_DOMAIN |
no | Default federated domain (default helix_forge) |
REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET |
for webhook | HMAC secret for POST /v1/webhooks/forgejo (REUSE-WP-0019-T02) |
REUSE_SURFACE_FORGE_BASE_URL |
no | Default target host for reuse-surface federation migrate-host (REUSE-WP-0019-T01), e.g. https://forgejo.coulomb.social |
8. CLI mapping
| CLI command | API call |
|---|---|
reuse-surface hub status |
GET /health |
reuse-surface hub list |
GET /v1/repos |
reuse-surface hub show --repo X |
GET /v1/repos/X |
reuse-surface hub register ... |
POST /v1/repos |
reuse-surface hub update ... |
PATCH /v1/repos/{repo} |
Run locally: reuse-surface serve. Global client flags: --base-url, env
REUSE_SURFACE_URL, REUSE_SURFACE_TOKEN.
9. Deployment reference
- Image:
gitea.coulomb.social/coulomb/reuse-surface:<tag> - Public URL:
https://reuse.coulomb.social - Secret:
reuse-surface-envwithREUSE_SURFACE_TOKEN - Probe path:
/health - Persistence: PVC at
/data(SQLite + fetch cache) - Helm release:
railiance-appsRAILIANCE-WP-0007 - Landing page at
/:railiance-appsRAILIANCE-WP-0008 (disable vialanding.enabled: falsein Helm values)