initial upload

This commit is contained in:
2026-06-16 11:01:43 -04:00
parent b759c9f3a8
commit ab5b61a161
2 changed files with 211 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
# Orchestrator
**Purpose:** Receives diagnostic record envelopes from Hubzilla addons, validates them, and persists them. Anchors Scenario submissions on-chain when Ğ1 payment is available.
---
## What the Orchestrator Does
The orchestrator is the only thing on this node that writes diagnostic records to durable storage. Hubzilla addons collect input, validate it locally, assemble a complete envelope, and POST it once. The orchestrator never asks an addon for more information mid-transaction. The envelope arrives complete or it does not arrive.
The orchestrator does three things:
1. Authenticates the request (single shared `node_token`)
2. Validates the envelope against the receiver contract for its `addon` field
3. Persists the record according to that record type's storage rule
It does not render HTML. It does not manage sessions. It does not know what a Privacy Group is. Those are Hubzilla's job.
---
## Trust Boundary — One Shared Token
All addons on this node (`vs01`, `dsc01`, `scn01`, `g1wallet`) share a single `node_token`. This is a deliberate scope decision, not an oversight.
**What this means:** any addon holding the shared token is trusted to identify itself correctly via the `addon` field in its envelope. The orchestrator does not independently verify that a request claiming `"addon": "scn01"` actually originated from the scn01 module. The token proves the request came from this node. It does not prove which addon on this node sent it.
**Why this is acceptable:** every addon on this node is part of the same codebase, reviewed under the same coding guidelines, deployed through the same tarball-and-pull workflow, and authored by the same operator. There is no scenario in the current architecture where one addon needs to be protected from another addon on the same node.
**What this rules out:** a future addon that requires its own authorization boundary — for example, a third-party-operated addon, a addon with a different trust level, or an addon whose compromise should not grant write access to other addons' record types — does not fit this model. That addon needs its own token, its own scope, and likely its own orchestrator instance or a scoped-token upgrade to this one. Do not bolt that requirement onto the shared-token design. Build it separately, or revisit this document and upgrade the authentication model deliberately, with a version bump.
If you are adding a sixth addon to this node and asking "does it need its own token," the answer is governed by one question: **does this addon's compromise need to be contained from the others?** If yes, it does not belong in the shared-token model as currently built.
---
## Record Types and Storage Rules
The orchestrator has exactly two storage behaviors. Every record type uses one of them.
### Overwrite-by-Placekey (mutable institutional memory)
Used by: `vs01`, `dsc01`
The participant's `participant_id` (their Placekey) is the storage key. Each submission replaces the previous one in full. There is no version history, no audit trail of prior states, and no concept of "undo." The current file always represents the participant's current understanding.
This is intentional. A Vital Signs or DSC Category record is not a complaint or a claim — it is an ongoing assessment. The record belongs to the address, not the person occupying it at any given moment. When a unit changes hands, the new occupant inherits the existing record exactly as the prior occupant left it, in the same way an incoming Board inherits the obligations and liabilities of the outgoing one. Staleness is expected and acceptable. Silence from a non-participating occupant is itself informative — the absence of an update is a fact about that address, not a gap to be hidden or auto-filled.
Storage path:
```
/srv/civic-orchestrator/data/{addon}/{association_slug}/{participant_id}.json
```
### Append-only (immutable attestation record)
Used by: `scn01`
Every submission is appended to an array. Nothing is ever overwritten, edited, or deleted by the orchestrator. Each entry carries its own `submitted_at` timestamp and a snapshot of the participant's VS and DSC records at the moment of submission, so the entry is self-contained and does not depend on the current state of the mutable records to be understood.
Storage path:
```
/srv/civic-orchestrator/data/scn01/{association_slug}/records.json
```
---
## The Ğ1 Coin Is a Utility, Not a Currency
A Scenario submission carries an optional `g1_tx_hash` field. When present, it is the transaction hash of a one-Ğ1 transfer the participant made to submit. This serves two purposes: it is a deliberate, cost-bearing act that discourages frivolous or automated submission, and it is a timestamped, third-party-anchored fact that exists independently of this node.
**The orchestrator does not require this field to accept or persist a record.** A Scenario record is valid, stored, and complete whether or not `g1_tx_hash` is present. The coin strengthens the record's evidentiary weight; its absence does not invalidate the record. This must remain true in every future version of the receiver contract. If a future change makes record acceptance conditional on a successful Ğ1 transaction, that change requires a deliberate architectural decision and sign-off — it is not an incidental tightening to slip into a validation patch.
---
## What the Orchestrator Does Not Decide
The orchestrator does not check whether a participant is allowed to submit a Scenario (the VS-Set-and-DSC-Set-must-exist-first prerequisite is enforced by the Hubzilla addon before the envelope is ever assembled). The orchestrator does not verify SASE standing. The orchestrator does not check Privacy Group membership. All of that happens upstream, in Hubzilla, before the POST is made. The orchestrator trusts the node that sends it a request with a valid token to have already done its job.
This mirrors the trust boundary above: the orchestrator trusts the node; within the node, the node trusts each addon to identify itself honestly. Two layers of trust, each scoped to what it can actually verify.
---
## Related Documents
- `receiver-spec.md` — the `/receive` endpoint contract, request/response shapes, validation rules per addon
- `../CODING-GUIDELINES.md` — rule 2 (named contracts) and rule 4 (Hubzilla/orchestrator boundary) govern this design
- `../hubzilla/addon/vs01/contracts/spool-v1.json` — the vs01 envelope contract (existing, predates this document)

View File

@@ -0,0 +1,126 @@
# Receiver Spec
**Endpoint:** `POST /receive`
**Contract version:** 1.0
**Status:** design complete, not yet implemented
This is the single endpoint every Hubzilla addon on this node POSTs to. There is no per-addon route. The `addon` field in the envelope body determines how the orchestrator validates and stores the request.
---
## Authentication
Every request must include:
```
X-Node-Token: {shared secret from /etc/civic-orchestrator/env_file}
```
A request with a missing or incorrect token is rejected with `401` before the body is parsed. See `README.md` for the trust model this implies — the token authenticates the node, not the specific addon.
---
## Request Envelope — Common Fields
Every envelope, regardless of `addon`, must include:
| Field | Type | Description |
|---|---|---|
| `addon` | string | One of `vs01`, `dsc01`, `scn01`. Identifies the record type and storage rule. |
| `contract_version` | string | Must be `"1.0"` for this spec version. |
| `association_slug` | string | URL slug of the association. Matches a key in that addon's `config.json` associations object. |
| `association_channel_id` | string | Hubzilla channel ID of the association, stored as string. |
| `participant_id` | string | The participant's Placekey (e.g. `SASE1-22c-at-5sb-8hm-54v`). This is the permanent, address-bound identifier — see `README.md` for why. |
| `submitted_at` | string | ISO 8601 timestamp of submission. |
| `standing` | string | One of `public`, `participant`, `professional`, `operator`. The access state at time of submission. |
Anything beyond these common fields is addon-specific and described below.
---
## `vs01` and `dsc01` — Overwrite-by-Placekey
### Additional field
| Field | Type | Description |
|---|---|---|
| `fields` | object | Keyed object of all field values for this submission. Keys match the field IDs defined in that addon's schema files. |
### Validation rules
- `fields` must be present and non-empty.
- The orchestrator does not validate individual field values against the VS or DSC schema files — that validation already happened on the Hubzilla side before the envelope was assembled. The orchestrator's job is to store what it receives, not to re-validate business rules it does not own a copy of.
- If `fields` is missing entirely, reject with `400`. An empty or malformed envelope is a bug on the sending side and should fail loudly, not be silently dropped or partially stored.
### Storage behavior
Write (overwrite, not append) to:
```
/srv/civic-orchestrator/data/{addon}/{association_slug}/{participant_id}.json
```
The entire file is replaced with the new envelope on every write. No merge, no partial update, no history retained by the orchestrator.
### Response
```json
{ "status": "ok", "addon": "vs01", "participant_id": "SASE1-22c-at-5sb-8hm-54v", "stored_at": "2026-06-15T10:00:00+00:00" }
```
---
## `scn01` — Append-Only
### Additional fields
| Field | Type | Description |
|---|---|---|
| `pinned_scenario_ids` | array of strings | 13 scenario IDs from `scenarios.json`, deduplicated. |
| `narrative` | string | Free text. |
| `vs_snapshot` | object | The participant's full VS fields object at time of submission. Embedded, not referenced. |
| `dsc_snapshot` | object | The participant's full DSC fields object at time of submission. Embedded, not referenced. |
| `g1_tx_hash` | string or null | Optional. Transaction hash if a Ğ1 payment was made for this submission. May be `null` or absent. **Absence does not invalidate the record — see README.md.** |
### Validation rules
- `pinned_scenario_ids` must have at least 1 and at most 3 entries.
- `narrative` must be non-empty.
- `vs_snapshot` and `dsc_snapshot` must both be present. A Scenario submitted without a snapshot of the participant's current VS and DSC understanding is rejected with `400` — this is the prerequisite enforced at the envelope level. (The Hubzilla addon enforces this before the form is even shown; the orchestrator enforces it again here as the backstop, per defense-in-depth, not as the primary gate.)
- `g1_tx_hash` is never required for acceptance.
### Storage behavior
Append the complete envelope as a new entry to the array at:
```
/srv/civic-orchestrator/data/scn01/{association_slug}/records.json
```
If the file does not exist, create it with a new array containing this one entry. Never overwrite or truncate existing entries. A write that would replace rather than append is a bug and must fail loudly rather than silently destroy prior records.
### Response
```json
{ "status": "ok", "addon": "scn01", "participant_id": "SASE1-22c-at-5sb-8hm-54v", "record_index": 14, "g1_tx_hash": null, "stored_at": "2026-06-15T10:00:00+00:00" }
```
`record_index` is the zero-based position of this entry in the association's records array, returned so the Hubzilla addon can display a confirmation reference if needed.
---
## Error Responses
| Code | Condition |
|---|---|
| `401` | Missing or incorrect `X-Node-Token`. |
| `400` | Missing required common field, missing addon-specific required field, or unrecognized `addon` value. |
| `500` | Storage write failed (disk, permissions, JSON encode failure). The orchestrator must log the full envelope to a separate failure log before returning this — a failed write must never silently lose the submission. |
Every error response includes a plain-language `message` field. No stack traces, no raw exception text.
---
## What This Spec Does Not Cover
Ğ1 transaction broadcast and verification (signing happens in g1wallet, broadcast relay happens in g1wallet's spool, the orchestrator only records the resulting hash if one is provided — it does not initiate or verify the transaction itself). That flow is a separate, not-yet-built piece of work and will get its own contract addendum when it is built.
Operator manage/review read endpoints (`GET` routes for the orchestrator's stored data) are also out of scope for this version. This spec covers writes only.