The broker (coordinator) reference implementation of the KOTVA standard — content-blind by construction, swappable, no token.
ephor — Greek for overseer: in Sparta, one of five magistrates elected annually to check the kings' power, never sovereign and never reappointed — the same shape here, a swappable service point that watches traffic pass without ever seeing inside it, hired for a term and replaced without ceremony.
Ephor is the single project that implements
coordinator/CONTRACT.md
— the KOTVA spec's contract for centralization that is hired, not depended-on. A
coordinator is any party providing a function the peer-to-peer substrate can't provide
reciprocally: a global view, a scarce resource, a legal anchor. Ephor houses every
coordinator kind behind that one contract (see The coordinator kinds
below).
Every kind is:
- Accountable — an attested identity plus a signed descriptor, not an anonymous relay.
- Swappable — leaving is a config change: zero migration, zero identity change.
- Self-hostable, with one disclosed exception class — scarce network reachability
(port-25 egress for
gateway, public ingress forreachability-adapter). - Declared content-visible at exactly one class × level, never silently downgraded.
Content-visibility is a checkable type, not a policy promise. broker-economics models it
as VisibilityClass (blind / blind-routing / terminating) × AssuranceLevel (structural
/ attested / declared) — CONTRACT §3 — and broker-conformance's COORD-1..8 harness
checks a coordinator's implementation against its own declaration rather than trusting prose.
Some clauses are decidable from the descriptor alone; COORD-5 (observed-vs-declared visibility)
is behavioral and can only be caught against real traffic, so the harness marks it
Outcome::Behavioral instead of falsely passing it — per-kind runtime tests then discharge it
where a real implementation exists (gateway, reachability-adapter today).
A coordinator authorizes from identity and rate; it never classifies content on a delivery
or canonical path — that judgement belongs to the recipient. No token. Economics are a
signed tariff plus signed usage receipts delivered to the payer (a one-directional audit — proves
a claimed operation happened, can't disconfirm a fabricated one, see
Billing & pricing). Settlement rides an existing stablecoin or fiat rail;
Ephor brokers none and takes no cut. Stake (where a kind requires skin-in-the-game — arbiter,
oracle) is verified on the settlement rail itself, never merely asserted.
Not the OS app-gateway. Routing
/app/<id>to a box's local app ports (with auth-token injection) is the VulOS shell's own internal reverse proxy — a separate concern that stays in the OS. Ephor crosses the network boundary (P2P, public exposure, mail egress), not the in-box one.
| Kind | Provides | Declared visibility | Status |
|---|---|---|---|
gateway |
Legacy mail bridge — MX, DKIM, SMTP/IMAP/POP3 | terminating — the one non-blind kind, disclosed |
built, 320 tests |
relay |
Mesh reachability for NAT'd peers, real libp2p 0.56 Circuit Relay v2 | blind / structural |
built, 12 tests, real two-peer loopback relay proof |
reachability-adapter |
ngrok-style public subdomains for box services, SNI-passthrough | blind-routing / structural(own-domain) or declared(vanity) |
built, 32 tests — REACH-2 key-auth done, control channel not yet Noise-encrypted (see Honest limits) |
media-relay |
Scales calls — orchestrates an external SFU (coturn/LiveKit), SFrame-sealed payload (RFC 9605) | blind-routing / structural |
built, 24 tests |
indexer |
Search/discovery over an opt-in public corpus | terminating / declared (attested-TEE option) — Gate::DerivedViewOnly (§4 carve-out) |
scaffold, 8 tests |
labeler |
Opt-in, subscribable moderation labels — §4's own named carve-out example | terminating / declared — Gate::DerivedViewOnly |
scaffold, 7 tests |
matcher |
Real-time supply/demand matching (rides, delivery) | terminating / declared (attested-TEE option) — Gate::DerivedViewOnly |
scaffold, 8 tests |
arbiter |
Dispute resolution over disclosed evidence, staked jury | terminating / declared — Gate::NoDeliveryPath |
scaffold, 7 tests |
oracle |
Physical-world/real-fact attestation (ORACLE ⊂ ATTEST, DIRECTION §2) |
terminating / declared — Gate::NoDeliveryPath |
scaffold, 7 tests |
compute |
Outsourced computation, provisional per CONTRACT §5's own table | terminating / declared (attested-TEE "blind compute" option) — Gate::NoDeliveryPath |
scaffold, 8 tests |
"scaffold" means a real broker_conformance::Coordinator implementation and a real
kotva-core-signed descriptor exist and are tested, but the kind's own function (ranking,
labeling, matching, arbitration, attestation, compute) is future work, disclosed in each crate's
docs — not silently stubbed. indexer / labeler / matcher never present their opt-in ranking
as an authoritative delivery path; arbiter / oracle / compute are neither a delivery path
nor a §4 derived view, so they declare Gate::NoDeliveryPath instead.
The Ephor operator console (console/) is the web UI an operator runs a coordinator with:
one Svelte 5 app fronting the admin crate's HTTP API — the coordinator-kind-
agnostic control plane for a descriptor, a tariff, metering/receipts, quota, and the operator's
signing keys. Six views behind one left-nav shell:
| # | View | What it's for |
|---|---|---|
| 01 | Overview | Kind + declared content-visibility badge, live COORD-1..8 strip, headline metrics (metered usage, prepaid balance, receipts issued, uptime) — the coordinator posture pictured above |
| 02 | Descriptor | View/edit operator policy + declared visibility, sign & publish; warns before a silent visibility downgrade (CONTRACT §3.2) and requires explicit disclosure to proceed |
| 03 | Pricing | Recommended cost-plus USD pricing (Hetzner/Vultr basis) as a reference only, plus your own editable, signable TariffSchedule — no token field anywhere in this UI (DIRECTION §5) |
| 04 | Billing | Prepaid credit balance per payer, top-up, metered usage, and signed receipts with the one-directional-audit caveat surfaced on every panel — see Billing & pricing |
| 05 | Keys | Current signing pubkey + rotate (re-signs the descriptor; old keys kept in history, never dropped) |
| 06 | Conformance | The full COORD-1..8 checklist — pass / behavioral / violation, with clause refs |
Run it locally in mock mode (no ephor-admin needed — full in-memory fixture data):
cd console && pnpm install && pnpm dev # http://localhost:5173, VITE_MOCK=1 by defaultTo point it at a live coordinator instead, build with VITE_MOCK=0 and VITE_API_BASE set to
your ephor-admin bind address (loopback by default, bearer-token gated, fail-closed if no
token is configured). See console/README.md for the full DTO-to-Rust
mapping and the screenshot pipeline (pnpm build && pnpm screenshot).
Prepaid top-up credit metered against usage is the primary model. An operator's payer tops
up a credit balance (via a settlement rail, see below); broker-billing::prepaid::PrepaidLedger
debits it against real metered usage and issues a signed receipt per debit
(BillingState::{Ok, LowBalance, Exhausted}). This preserves zero-lock-in (CONTRACT §2.2), fits
the anonymous-but-accountable posture SEC-7 allows, matches §6's continuous-metering model better
than an after-the-fact tab, and mirrors patala's own prepaid PostageProvider seam. Ephor
holds no funds — a credit is a claim backed by an on-rail funding reference, never custody.
A thin, optional monthly-card postpaid add-on (broker_billing::subscription::Subscription)
rides the exact same SettlementRail seam for operators who want a card-on-file experience
instead — secondary by design, not the default.
The console is responsive: on a phone the sidebar collapses to a drawer and the metric grid drops to a single column so no figure is truncated.
broker-billing::pricing::recommended_tariff turns a real infra cost profile into a starting
point, never a mandate — CONTRACT §6 is explicit that quotas, rates, and prices are operator
policy. The formula amortizes a HostingProfile's fixed VPS cost (plus, for the two
scarce-reachability kinds, a reachability premium) into a per-unit cost, then applies a 2.00x
cost-plus markup:
| Kind | Priced unit | Hetzner-like profile (~$5/mo, ~$0.01/GiB) | Scarce-reachability premium applied? |
|---|---|---|---|
relay / media-relay |
per GiB forwarded | recommended | no |
reachability-adapter |
per GiB forwarded | recommended, higher than relay for the same profile |
yes (+$5/mo) |
gateway |
per 1,000 messages | recommended | yes (+$5/mo) |
compute |
per 1,000 compute-seconds | recommended | no |
The exact cents come out of recommended_tariff against a chosen HostingProfile
(HETZNER_CX, VULTR_GENERIC, or a padded GENERIC_VPS) — see
crates/broker-billing/src/pricing.rs for the full
formula, the batching rationale (bandwidth/messages/compute-seconds are priced per-batch so
integer cents stay non-zero), and the sourcing caveat on the illustrative profile constants. USD
is the pricing/display currency only; settlement itself is stablecoin or fiat via the rail —
every number here is operator-overridable before it's ever signed.
Every billed operation gets a signed UsageReceipt (broker-billing::ReceiptLog) the payer can
verify. Read .verify() honestly: it proves the coordinator signed a claim, never that the
claim is true, and never that no unreceipted charge happened elsewhere. This is demonstrated
by a test — a receipt for a fabricated, never-metered operation verifies identically to a receipt
for a real one. It's a real accountability primitive, not a fraud-proof one; treat it as such.
broker-billing::SettlementRail is a provider-agnostic seam with one in-tree mock reference
adapter (InMemoryLedger, an explicit double-entry mock, no external custody). The optional
broker-billing-patala crate adapts it onto real
patala rails — patala-stellar shipped as the one
reference crypto top-up rail (Ed25519-native, so a coordinator's own substrate identity key can
double as the receiving wallet), patala-hyperswitch noted (not depended on) for the optional
card/monthly path. It is a real workspace member but kept out of default-members — a plain
cargo build/cargo test at the repo root never compiles a patala crate; only
cargo build -p broker-billing-patala (or an explicit --workspace) pulls it in. There is no
protocol token anywhere (DIRECTION §5) — Descriptor structurally cannot carry a stake field
or a price rank (CONTRACT §2.1); stake, where a kind requires it, is verified on the settlement
rail itself.
All-Rust, one crate per coordinator kind plus shared model/harness/billing/admin crates.
Substrate types (MOTE, envelope, identity/naming, PUB, SYNC, signing/DS-tags, CBOR, crypto) come
from kotva-core / kotva-mail — crates carved out of envoir, living in the kotva repo,
and pinned by tag (core-v0.2.0), never tracked at HEAD (the isango guardrail:
extracting this same gateway from envoir failed twice before against a moving core).
| Crate | Role | Status |
|---|---|---|
broker-economics |
Content-visibility model, coordinator-kinds table, signed descriptor/tariff/usage-receipt shapes | built |
broker-conformance |
The Coordinator trait + COORD-1..8 checklist harness |
built |
broker-billing |
Metering, TariffSchedule, prepaid ledger, signed receipts, SettlementRail/StakeVerifier seams, USD recommended pricing |
built |
broker-billing-patala |
Optional, non-default: SettlementRail over real patala rails |
built, isolated |
admin |
Kind-agnostic operator HTTP API (ephor-admin binary) |
built |
gateway, relay, reachability-adapter, media-relay |
The four built coordinator kinds | built |
indexer, labeler, matcher, arbiter, oracle, compute |
The six scaffolded kinds | scaffold |
Full crate map, per-crate detail, and the kotva-core pin mechanics:
crates/README.md.
cargo build --workspace
cargo test --workspace # 548 tests, clippy clean
cargo clippy --workspace --all-targetsThe Go tree (go build ./...) and the Rust workspace coexist at the repo root (Cargo.toml +
go.mod) — building one does not affect the other; see
Preserved Go implementation below.
Disclosed residuals, not silently fixed:
reachability-adapterREACH-2: the box↔adapter control channel is key-authenticated (challenge-response against the box's identity, replay-inert) but not yet Noise-encrypted — an on-path attacker can observe/DoS the control channel but not impersonate a box. Transport confidentiality/integrity is still open; do not expose it publicly until that lands.broker-billingreceipts are a one-directional audit (CONTRACT §6, R-6):UsageReceipt::verify()proves the coordinator signed a claim, never that the claim is true or that no unreceipted charge exists elsewhere.- Settlement and stake are seams, not rails:
SettlementRail/StakeVerifiereach ship with one mock reference adapter (InMemoryLedger; fail-closedNoStakeRail). The realbroker-billing-patalaadapter is untested against a live Stellar network — disclosed in-crate, not merely absent. indexer/labeler/matcher/arbiter/oracle/computeare scaffolds — a real signed descriptor and conformance posture exist; the kind's own function does not yet.- The Go reverse-tunnel relay + the
@vulos/relay-clientJS SDK remain the working implementation until the Rustrelayport is proven in production and this note is removed — see Status and Preserved Go implementation below. - The wire layout chosen for
Descriptor/Tariff/UsageReceipt(canonical §18.1.1 CBOR, Ed25519, DS-tagged) is logged toCOORDINATION.mdfor the spec session but not yet ratified intocoordinator/CONTRACT.mditself — treat it as this implementation's proposal, not yet normative. media-relayorchestrates an external SFU, it does not embed one — bind-don't-reinvent, by design; large-scale SFU capacity is out of scope for this crate.
Ephor is mid-rewrite. Read this before relying on any of it in production.
- The Rust workspace is in progress, not the shipping implementation yet. The
Go reverse-tunnel relay (
tunnel/,cmd/) and the@vulos/relay-clientJS SDK (client/) are the preserved, working implementation — keep using them until the Rust port is proven and this note is removed. See Preserved Go implementation below. reachability-adapter: not public-safe yet. REACH-2 key-auth is wired, but the control channel is still plain (unencrypted) TCP — see Honest limits.- Live, wave-by-wave status lives in BUILD-PLAN.md; decisions are logged append-only in DECISIONS.md.
Until the Rust relay crate is proven in production, this repo's working relay is the
original Go reverse-tunnel + the JS peer-fabric SDK. Both are frozen-but-maintained, not
deprecated — see Status.
Wires browser peers together with WebRTC peer-to-peer data channels, falling back
to a relay circuit when a direct connection can't be established. It's a client
only — it talks to its host app's /api/peering/* endpoints for signaling and ICE
credentials, and only ever speaks https/wss.
npm install @vulos/relay-clientimport { selectEndpoint } from '@vulos/relay-client/endpoints'
import { FabricClient } from '@vulos/relay-client/fabric'
import { PresenceManager } from '@vulos/relay-client/presence'
const base = await selectEndpoint() // LAN-direct → cloud → same-origin
const fabric = new FabricClient({
sessionId: 'doc-abc123',
peerId: currentUser.id,
signalingUrl: `${base.replace(/^http/, 'ws')}/api/peering/stream`,
iceUrl: `${base}/api/peering/ice`,
authToken: session.jwt, // optional Bearer JWT
})
fabric.addEventListener('message', ({ detail: { from, data } }) => console.log(from, data))
await fabric.join()
fabric.send(JSON.stringify({ op: 'insert', pos: 0, text: 'hello' }))
const presence = new PresenceManager({ fabric, localIdentity: { accountId: currentUser.id } })
presence.join()Key properties: E2E peer authentication (per-session ECDSA P-256, every
offer/answer/ICE frame and relay deposit signed, TOFU + replay cache),
endpoint failover (LAN ↔ cloud, 400 ms debounce), presence + live cursors
(React hooks), P2P mesh calls (createCall; the LiveKit SFU path was removed
before 1.0). Full option list: docs/CONFIGURATION.md; design:
docs/ARCHITECTURE.md; subpath map:
client/README.md.
A self-hosted replacement for frp / ngrok / Cloudflare Tunnel: a loopback-bound
box dials one outbound wss:// connection to a relay you control, which serves a
public URL and reverse-proxies HTTP + WebSocket back down it — no inbound ports, no
static IP, no third-party relay.
./scripts/install.sh --domain relay.example.com # one command, needs Docker Composeor run the binaries directly:
go run ./cmd/vulos-relayd -domain relay.example.com -tokens-file grants.json
go run ./cmd/vulos-relay-agent -server wss://relay.example.com -token SECRET1 -name box1 -local 127.0.0.1:8080go build ./...
go test -race ./...
go vet ./...Security posture — honest, no overclaim. The relay is a content-visible Layer-7
terminating proxy, not an end-to-end-encrypted pipe: it (or its fronting edge)
terminates the client's TLS, so the relay operator can read and modify all tunneled
HTTP. Confidentiality rests on who runs the relay: self-host it (you are the
operator), or use a verified direct endpoint (TLS runs client↔box, bypassing the
relay). This is exactly the honesty gap the Rust reachability-adapter's
SNI-passthrough transport is built to close for NAT'd boxes — see
Honest limits for why it isn't public-safe yet either.
Other hardening already in the Go relay: bearer-token agent auth (constant-time,
hashed at rest), token-bound names, an SSRF guard on the agent's forward target,
per-IP/per-tunnel/global rate limiting (429), over-quota cut (402), token
revocation (file/env list + runtime API + periodic sweep), a verified direct-IP
fast path (near-native latency, unmetered, bypasses the relay entirely), request
bounds (256 MiB body cap, slow-body ingestion deadline), a loopback/token-gated admin
listener for /metrics + /healthz + /readyz (never on the public tunnel), and a
geo-distributed pool with a CP-driven, make-before-break autoscaler (fully
CP-optional — a self-host relay runs none of it). Real-time media (RTP) never rides
the tunnel — it goes over ICE/TURN directly, preferring the box's verified direct
endpoint. Full trust model and deploy notes:
docs/SECURITY.md · docs/TUNNEL.md ·
docs/GETTING-STARTED.md.
The relay also serves two open, opt-in roles any conforming operator can run: a rendezvous role (signed, content-blind announce/resolve/signal/mailbox + ICE substrate for OS-free P2P signaling — docs/RENDEZVOUS.md), and a pubcache/pin role (a verifying read-through cache + durable pin store for public, self-verifying DMTAP-PUB objects, refusing anything that doesn't match its content address — docs/PUBCACHE.md, docs/PINNING.md). Pubcache is the one role that is not content-blind (it serves public plaintext by design) and is off by default, explicit opt-in.
| Document | Description |
|---|---|
| HANDOVER.md | The build brief: target architecture, guardrails, build order |
| BUILD-PLAN.md | Live wave-by-wave status of the Rust port |
| DECISIONS.md | Append-only decision log |
| COORDINATION.md | Cross-repo sync log with the kotva spec session |
| crates/README.md | Rust workspace map, per-crate status, the kotva-core seam |
| console/README.md | Operator console views, stack, mock mode, screenshot pipeline |
| brand/README.md | The Bronze comma mark — concept, palette, file map, usage rules |
| docs/GETTING-STARTED.md | Zero-to-reachable-box walkthrough (Go relay) |
| docs/SECURITY.md | Go relay trust model — what the operator can/cannot see |
| docs/TUNNEL.md | Full server flag/env reference & deploy notes (Go relay) |
| docs/TUNNEL-GUIDE.md | Protocol/lifecycle deep dive — wss+yamux, reconnects |
| docs/METERING-BILLING.md | How Go-relay transfer is metered (opt-in; unbilled self-host by default) |
| docs/RENDEZVOUS.md | The open rendezvous role — wire protocol, auth, signing |
| docs/PUBCACHE.md | The open cache/pin role — verification gate, bounds |
| docs/PINNING.md | Durable pin store — budget, refusal semantics, signed wire protocol |
| docs/ARCHITECTURE.md | JS SDK fabric / signaling / endpoint-failover design |
| docs/CONFIGURATION.md | All JS SDK options and constructor params |
| docs/TROUBLESHOOTING.md | Symptom → cause → fix field guide (Go relay) |
| client/README.md | JS SDK subpath exports + migration notes |
| ROADMAP.md | Planned directions for the JS SDK |
| CHANGELOG.md | Release history |
The KOTVA spec itself (coordinator/CONTRACT.md, DIRECTION.md, and the per-kind
profiles/) lives in the kotva repo and is owned by
that project, not duplicated here.
# Rust workspace
cargo build --workspace && cargo test --workspace
# Go relay (server + agent)
go build ./... && go test -race ./...
# JS SDK
cd client && npm ci && npm run build && npm test
# Operator console
cd console && pnpm install && pnpm build && pnpm checkCI (.github/workflows/ci.yml) builds and tests the JS
client on Node 20 and runs a Trivy filesystem scan (HIGH/CRITICAL gating). The
publishable JS package lives in client/; the repository root holds dev tooling
(screenshot capture, etc.) under scripts/.
# bump version in client/package.json first, then:
git tag v1.2.3 && git push origin v1.2.3The release workflow builds, tests, verifies the tag
matches client/package.json, and publishes to npm with OIDC provenance.
Vulnerability disclosure. Report via GitHub Security Advisories (preferred) or
security@vulos.org. In-scope areas include the Go relay's endpoint probe/cache
integrity, signaling session isolation, peer-auth bypass, and offline-queue integrity,
as well as the Rust workspace's coordinator conformance, content-visibility
enforcement, and billing/receipt integrity. Acknowledgement within 72 hours. Full
policy: SECURITY.md.
JS SDK peer authentication. Every FabricClient session generates an ephemeral
ECDSA P-256 key pair; the public key is published in the signaling join frame and
every outgoing offer/answer/ice frame and relay deposit is signed over its
canonical form. By default (requirePeerAuth: true) unsigned frames from unknown
peers are rejected (TOFU on first signed frame); replayed (from, nonce) pairs are
dropped (bounded FIFO cache). Details: docs/ARCHITECTURE.md.
See CONTRIBUTING.md for dev-environment setup, branch and commit conventions, and scope constraints.
MIT OR Apache-2.0 — © VulOS. Ephor is a VulOS project; source and issues at github.com/vul-os/ephor. Brand mark, palette, and usage rules: brand/README.md.
![]()
vulos — rooted in vula, the Zulu/Xhosa word for
open




