Requirements 014: Resource Opinions over Local Agora Relay¶
Based on:
doc/project/20-memos/resource-opinions-and-discussion.mddoc/project/30-stories/story-008-cool-site-comment.mddoc/project/40-proposals/024-capability-passports-and-network-ledger-delegation.mddoc/project/40-proposals/026-resource-opinions-and-discussion-surfaces.mddoc/project/40-proposals/035-agora-topic-addressed-record-relay.mddoc/project/60-solutions/008-agora/008-agora.mddoc/project/60-solutions/008-agora/008-agora-caps.edndoc/project/60-solutions/008-agora/008-agora-topic-addressed-relay-impl.mddoc/project/50-requirements/requirements-010-middleware-executor.md
Date: 2026-04-16
Status: Draft
Executive Summary¶
Requirements 001–013 cover Node onboarding, federated answer procurement,
learning, archival, transcript curation, networking baseline, settlement,
organization subjects, capability_limited participants, middleware
execution, bundled exchange modules, service offers, and Monus. None of them
cover the MVP path for a participant publishing a signed opinion about an
external URL through the local Agora relay, discovered via the node's
capability-routing surface.
Story-008 exercises exactly that path end-to-end in the smallest possible shape:
- one author (operator's primary participant key),
- one URL resource (
resource/kind = "url"), - one content schema (
resource-opinion.v1), - one
agora-record.v1envelope, - one local relay discovered under capability id
agora.relay, - zero federation, zero delegation.
This requirements document fixes the contract of that path so that the smallest coherent Agora slice cannot regress even before the federated profile lands.
Functional Requirements¶
| ID | Requirement | Type | Source | Status |
|---|---|---|---|---|
| FR-001 | The system MUST support publishing a resource-opinion.v1 content payload as an agora-record.v1 envelope with record/kind = "opinion" and content/schema = "resource-opinion.v1". |
Fact | Proposal 035 §3 + Proposal 026 | implemented |
| FR-002 | The envelope MUST carry the resource reference in record/about as an array containing at least one { "resource/kind": "...", "resource/id": "..." } entry. |
Fact | Proposal 026 + Proposal 035 | implemented |
| FR-003 | For URL-scoped opinions the resource/kind MUST be the literal string "url" and resource/id MUST be the URL without any implementation-side normalization. |
Fact | Proposal 026 + Story-008 AC-4 | implemented |
| FR-004 | The MVP deployment MUST accept a topic/key of "ai.orbiplex.opinions/url" as a default catch-all bucket for URL opinions without requiring a per-domain or per-community split. |
Fact | Story-008 Step 3 notes + Proposal 046 | implemented |
| FR-005 | A Node UI flow that composes a resource opinion MUST NOT hold a hardcoded Agora endpoint. It MUST resolve the relay endpoint through the daemon's host capability API under capability id agora.relay. |
Fact | Story-008 AC-1 + Proposal 024 | implemented |
| FR-006 | When no agora.relay provider is registered locally, the UI MUST surface a clear "no local relay available" state and MUST NOT silently fall back to any non-local relay. |
Fact | Story-008 AC-2 | implemented |
| FR-007 | Signing MUST use KeyRef::PrimaryParticipant as the MVP default, with the signer returning an Ed25519 signature in the agora.record.v1 domain. |
Fact | Proposal 035 + signer-core contract | implemented |
| FR-008 | The signing path MUST verify that the public key returned by the host signer matches the author's participant:did:key fingerprint and MUST reject the record on mismatch before returning it to the caller. |
Fact | Proposal 035 + agora-core sign adapter | implemented |
| FR-009 | The envelope MUST self-verify (canonicalize → recompute record/id → verify signature.value in the agora.record.v1 domain) before it is persisted or federated. |
Fact | Proposal 035 §4 | implemented |
| FR-010 | Local ingest MUST be idempotent on record/id: a duplicate POST of the same signed envelope MUST return 200 OK rather than 409 or 201, without creating duplicate storage rows. |
Fact | Proposal 035 §5 + Story-008 AC-6 | implemented |
| FR-011 | The relay MUST serve the record back by record/id and MUST expose the subject-indexed listing keyed by {resource/kind, resource/id} so that later readers can retrieve all opinions attached to a given URL without scanning full topics. |
Fact | Proposal 026 + Proposal 035 | implemented |
| FR-012 | The MVP local deployment MUST be operable without any agora.relay capability passport. A node that never mints an agora.relay passport MUST still be able to sign, ingest, query, and subscribe on its own relay. |
Fact | Proposal 035 §5.7 + Story-008 AC-8 | implemented |
| FR-013 | The relay MUST record a full action trace for each accepted record covering: capability lookup, sign request, ingest, readback. This trace MUST be inspectable without re-running the flow. | Fact | Story-008 AC-7 | implemented |
| FR-014 | The relay MUST NOT interpret content beyond content/schema for ACL or retention purposes. Per-kind content semantics (opinion meaning, ranking, moderation) remain outside the relay contract. |
Fact | Solution doc (008-agora/008-agora.md) + Proposal 035 | implemented |
Non-Functional Requirements¶
| ID | Requirement | Type | Source | Status |
|---|---|---|---|---|
| NFR-001 | The smallest useful deployment (one node, one participant, one topic, no passport) MUST remain a first-class configuration rather than a degraded fallback. Local-only behavior SHALL NOT be gated on federation features. | Fact | Proposal 035 §5.7 | implemented |
| NFR-002 | The capability-lookup surface (GET /v1/host/capabilities/agora.relay) SHOULD return local providers with transport: "http-local", scope: "local", and passport: null; it SHOULD distinguish a known-but-unready capability (200 with empty providers) from an unknown capability (404 capability_unknown) and SHOULD NOT require a passport to be minted for a local response to be valid. |
Fact | Proposal 024 + Story-008 Step 2 | implemented |
| NFR-003 | The signing/ingest/query surfaces SHOULD be reachable exclusively on loopback in the MVP local-only profile; remote addresses MUST require explicit operator opt-in at the daemon config layer. | Inference | Operator safety + Proposal 035 | implemented |
| NFR-004 | The envelope used in local-only mode MUST be byte-identical to the envelope used in a federated, passport-gated, multi-relay deployment. No local shortcuts, no dialects. | Fact | Proposal 035 + Story-008 architectural significance | implemented |
| NFR-005 | The local trace for a published opinion SHOULD be available under a single trace namespace (e.g. trace/agora) and SHOULD correlate capability lookup, sign request, ingest, and readback by a common correlation identifier. |
Inference | Operability | implemented |
| NFR-006 | A deployment that later adds key-delegation.v1-backed proxy signing MUST NOT be required to change the envelope wire shape beyond the optional proof fields documented for the delegated-signing layer; archival readers that ignore delegation proofs MUST still be able to verify direct-signed records. |
Fact | Proposal 032 | implemented |
Failure Modes and Mitigations¶
| Failure Mode | Impact | Mitigation |
|---|---|---|---|---|
| Node UI hardcodes an Agora endpoint | Breakage when the relay moves ports; no fallback discovery; hidden coupling | FR-005: discovery must go through capability-routing; reject code that imports a hardcoded URL. |
| Signer returns a public key that disagrees with the author's participant id | Record whose signature verifies but whose author claim is a lie | FR-008: hard block in the sign adapter; SignAdapterError::PublicKeyMismatch. |
| Duplicate POST creates two storage rows with the same record/id | Subject-index and topic pagination become non-deterministic | FR-010: idempotent ingest keyed on record/id. |
| URL resource id is silently normalized (trailing slash, case, punycode) before hashing | Two users opining about the same URL end up on different subject-index buckets | FR-003: no normalization drift; byte-identical resource/id. |
| Local UI silently falls back to a public relay when no local provider is registered | Operator believes they are posting locally but is in fact publishing to the open web | FR-006: explicit "no local relay available" state, no silent fallback. |
| Operator attempts federation-only workflows on a node that never minted an agora.relay passport | Local publication blocked for no architectural reason | FR-012 + NFR-001: local-only mode is first-class. |
| Relay starts interpreting content for ACL or moderation | Layer violation; per-kind semantics leak into the substrate | FR-014: relay never reads beyond content/schema. |
Out of Scope¶
- federation topology, Seed Directory registration of
agora.relayproviders, cross-relay replication behavior, - delegated / proxy-key signing (see proposal 032),
- content moderation, reputation weighting, listener-side filtering,
- multi-subject opinions (
record/aboutas an array with more than one entry) and richer opinion metadata (rating,tags), - any opinion substrate keyed on non-URL resource kinds
(
resource/kind = "ean","node","org","gps", ...); those are covered by proposal 026 but not tested by story-008.
Notes¶
This requirements document is the "contract" side of the story-008 flow. It
deliberately mirrors story-008's acceptance criteria rather than restating
them, so that implementation work done against the capability catalog
(agora-caps.edn) has a checkable requirements trail even if story-008
itself evolves.
The substrate layer (ingest, query, subject-index, topic routing, relay
ACL) MUST remain open on resource/kind — it accepts any value
conforming to the resource-ref.v1 shape (^[a-z][a-z0-9-]*$, 1..64
chars), not only the literal "url". The URL-only scope present in
FR-003, FR-004, and the "Out of Scope" list above applies solely to the
MVP UI flow and the story-008 end-to-end test surface; it is a
product-scope decision, not a contract closure. Implementations MUST NOT
hardcode "url" below the UI layer, and a later requirements document
covering non-URL kinds will exercise the same substrate without
requiring schema, gate, or relay changes.
Future requirements documents may extend this one horizontally (requirements-015 for non-URL resource kinds, requirements-016 for delegated signing) rather than widening FR/NFR counts in place.
Redelivery semantics — what a relay returns on GET
/v1/agora/records/{id} when it once held a record and no longer does,
how a participant's node may re-ship from its local Memarium archive,
how custody between nodes is negotiated and proven, and how sondage
protects relays from flood — are out of scope for the MVP contract
frozen here and are covered by proposal 040 (custodial redelivery and
tombstones). The MVP path (local-only, one node, no federation) does
not exercise those semantics; a federated requirements document will
translate proposal 040 into FR/NFR form.