mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
docs(quartz): per-CEP compliance matrix and 5-tier test method for ContextVM
Widens the ContextVM scope from the 7 CEPs cordn exercises to the core spec plus all 12 CEPs, so :contextvm is a complete MCP-over-Nostr implementation rather than a cordn adapter, and adds the compliance surface that implies. Adds a 13-row matrix (core spec + 12 CEPs) with status, surface, a CVM-* rule id prefix per document, and the test tier each must clear. Rule ids follow the STORE-Fxx convention from the event-store-semantics skill so a future divergence can be named rather than described. Defines five test tiers, because the CEPs are symmetric and no single counterparty exercises them all: rule-derived unit tests, live integration against the reference coordinator, adversarial tests against our own fixture server, cross-implementation vector exchange, and wallet integration for CEP-8. Tier C is the one that does not exist yet and is now a first-class deliverable: no real server sends a non-monotonic progress, a stale pong nonce or a mismatched digest, yet all three are MUST-fail requirements. Also records a per-CEP test catalog with the negative cases called out explicitly, and two findings from reading CEP-8 and CEP-15 in full: both require RFC 8785 JSON canonicalization, which Quartz does not have (the canonicalize hits in the tree are IPv6, media types and relay URLs), and CEP-8 forbids silent fallback in both directions -- a payment handler that pays whatever it is asked violates the client half. Build list grows to 16 items, Stage 2 splits into 2a-2f gated on rule-id groups, and three open questions are added on the fixture server, CEP-8's real priority, and upstream conformance vectors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012BfD4txdnsaPRXmNXbup9n
This commit is contained in:
@@ -40,12 +40,18 @@ The engine is reusable but **not yet binding-agnostic**: it lives inside
|
||||
Extracting it is the prerequisite for this work and is worth doing on its own merits.
|
||||
|
||||
The transport has to be written from scratch: **ContextVM is MCP-over-Nostr and nothing in Quartz
|
||||
speaks it.** Reviewing the full spec set (§6) puts the scope at **7 of 12 CEPs** — the core spec,
|
||||
CEP-4/19 (encryption), CEP-6/17 (discovery), CEP-35 (stateless discovery), and the two big ones
|
||||
CEP-22 (bounded oversized transfer) and CEP-41 (open streams), which together are most of the work
|
||||
and nearly all of the risk. Payments (CEP-8) and common tool schemas (CEP-15) are confirmed
|
||||
unused by cordn. Only 3 of the 7 are **Final**; the rest, including both big ones, are **Draft**
|
||||
(§6.6). The result is a general MCP-over-Nostr client, reusable well beyond cordn.
|
||||
speaks it.** Scope is **the core spec plus all 12 CEPs** (§6.2), so `:contextvm` is a complete
|
||||
MCP-over-Nostr implementation rather than a cordn adapter — cordn only exercises 7 of the 13
|
||||
documents, but the rest are cheap next to the two large transfer profiles and several ride on NIPs
|
||||
we already have. Only **CEP-4, CEP-6 and CEP-16** are Final; the core spec and the other nine CEPs
|
||||
are Draft, including CEP-22 and CEP-41 (§6.7).
|
||||
|
||||
Because the CEPs are symmetric, compliance is not demonstrable against cordn alone. §6.4 defines
|
||||
five test tiers, and the one that does not exist yet is **Tier C: a Kotlin fixture server that
|
||||
misbehaves on demand.** No real server sends a non-monotonic `progress`, a stale `pong` nonce or a
|
||||
mismatched digest, yet those are MUST-fail requirements — so the fixture is a first-class Stage 2
|
||||
deliverable, not scaffolding. Two CEPs (8 and 15) also need **RFC 8785 JCS**, which Quartz does not
|
||||
have; it lands in `quartz/…/utils/` as a shared primitive.
|
||||
|
||||
Recommended sequencing: **Stage 0 (vectors) → Stage 1 (extract engine) → decide → Stage 2+.**
|
||||
Do not start Stage 2 before the §4.1 decision, because if it goes the wrong way every KeyPackage
|
||||
@@ -216,20 +222,22 @@ NIP-44 (and NIP-59) for the ContextVM gift wrap, NIP-19 bech32/TLV for `cordn1
|
||||
Blossom (`nipB7Blossom`), `INostrClient` and the `accessories/` one-shot helpers for relay I/O,
|
||||
and `NostrSigner` for both identities.
|
||||
|
||||
## 6. ContextVM: full surface review
|
||||
## 6. ContextVM: full surface review and compliance matrix
|
||||
|
||||
We have to write this from scratch — nothing in Quartz speaks it, and the only SDKs are TypeScript
|
||||
and Rust. This section is the complete protocol surface so Stage 2 can be scoped honestly.
|
||||
and Rust. **Scope decision: implement the whole CEP list**, not just the subset cordn exercises.
|
||||
That makes `:contextvm` a complete MCP-over-Nostr implementation rather than a cordn adapter, and
|
||||
it means compliance has to be demonstrable per CEP rather than "cordn works".
|
||||
|
||||
**Clean-room sourcing.** Everything below is derived from the specification documents in
|
||||
`ContextVM/contextvm-docs` (the `docs/contextvm-docs` submodule of the SDK, cloned at
|
||||
`ContextVM/contextvm-docs` (the `docs/contextvm-docs` submodule of the SDK, at
|
||||
`src/content/docs/reference/`), **not** from the LGPL SDK source (§7). Implementers should work
|
||||
from those documents. Note that the docs repo carries **no LICENSE file** — the protocol is free
|
||||
to implement, but do not paste spec prose into our repo; paraphrase.
|
||||
from those documents. The docs repo carries **no LICENSE file** — the protocol is free to
|
||||
implement, but do not paste spec prose into our repo; paraphrase.
|
||||
|
||||
### 6.1 The core spec is small
|
||||
|
||||
`spec/ctxvm-draft-spec.md` (352 lines, Draft) is nearly all it is:
|
||||
`spec/ctxvm-draft-spec.md` (352 lines, Draft) is nearly all of the base protocol:
|
||||
|
||||
- **One event kind, 25910**, ephemeral (NIP-01 range 20000–30000). `content` is the stringified
|
||||
MCP JSON-RPC message, preserved exactly. Nostr metadata lives only in tags: `p` addresses the
|
||||
@@ -243,33 +251,43 @@ client must already be subscribed when the response is published or the response
|
||||
there is no REQ-after-the-fact recovery. That shapes our subscription lifecycle more than anything
|
||||
else in the spec.
|
||||
|
||||
### 6.2 CEP inventory and scope
|
||||
### 6.2 Compliance matrix
|
||||
|
||||
Twelve CEPs exist. Seven are load-bearing for cordn; five are not.
|
||||
Thirteen documents: the core spec plus 12 CEPs. `Rules` is the rule-id prefix this plan assigns
|
||||
for citing individual requirements, following the `STORE-Fxx` convention the `event-store-semantics`
|
||||
skill established — so a future divergence can be named precisely instead of described. `Gate` is
|
||||
what has to pass before we claim compliance; the test catalog is §6.5.
|
||||
|
||||
| CEP | Status | What it adds | Needed |
|
||||
| --- | ------ | ------------ | ------ |
|
||||
| **4** Encryption Support | Final | NIP-44 encrypt the signed inner 25910 event, place it in a NIP-59 gift wrap (kind 1059) with **no rumor layer**. `support_encryption` tag. | **Yes** |
|
||||
| **19** Ephemeral Gift Wraps | Draft | Kind **21059**, identical structure/semantics to 1059 but in the ephemeral range so relays do not persist the envelope. `support_encryption_ephemeral`. | **Yes** — cordn-web pins `EPHEMERAL` |
|
||||
| **6** Public Server Announcements | Final | Addressable **11316** (server), **11317** tools, **11318** resources, **11319** resource templates, **11320** prompts. `content` is the stringified initialize/list result. Discovery tags: `name`, `about`, `picture`, `website`, `support_*`. | **Yes** (read side) |
|
||||
| **35** Stateless Session Discovery | Draft, Informational | Discovery tags ride the **first direct message each side sends** in a session — role-oriented, not initialize-oriented. Unknown discovery tags MUST be preserved. | **Yes** — cordn-web sets `isStateless: true` |
|
||||
| **22** Oversized Payload Transfer | Draft | Bounded reassembly over `notifications/progress`. `progressToken` is the transfer id. Frames `start`/`accept`/`chunk`/`end`/`abort`, `completionMode: "render"`, SHA-256 digest + `totalBytes` + `totalChunks`. | **Yes** — coordinator enables it; a large `msg_fetch_many` reply needs it |
|
||||
| **41** Open-Ended Streams | Draft | Long-lived streams over the same envelope. Frames `start`/`accept`/`chunk`/`ping`/`pong`/`close`/`abort`. | **Yes** — `msg_sub_many` is built on it |
|
||||
| **16** Client Public Key Injection | Final, Informational | Server transport injects `_meta.clientPubkey` into inbound requests. | **Server-side**, but load-bearing (§6.3.11) |
|
||||
| **17** Server Relay List Metadata | Draft | NIP-65 **kind 10002**, `r` tags, unmarked by default in the ContextVM profile. | Recommended — lets a `cordn1…` ref with no relay hints still resolve |
|
||||
| **23** Server Profile Metadata | Draft | Servers MAY publish **kind 0** and **kind 1**. | Optional — free win for a coordinator picker; we already render kind 0 |
|
||||
| **24** Server Reviews | Draft | NIP-22 **kind 1111** anchored to the `11316:<pubkey>:` `a` coordinate. | Optional — we already have NIP-22 |
|
||||
| **8** Capability Pricing and Payment | Draft | `cap` pricing tags, `pmi` payment-method ids, `payment_interaction` negotiation (`transparent` vs `explicit_gating`), payment notifications/errors. | **No** — verified zero payment references anywhere in cordn |
|
||||
| **15** Common Tool Schemas | Draft | RFC 8785 (JCS) hash of normalized tool schemas, `io.contextvm/common-schema` `_meta` namespace, NIP-73 discovery. | **No** — irrelevant to a client with 11 fixed tools |
|
||||
| **21** PMI Recommendations | Draft, Informational | Naming guidance for CEP-8 PMIs. | **No** (depends on CEP-8) |
|
||||
| Spec | Status | Surface | Rules | Gate |
|
||||
| ---- | ------ | ------- | ----- | ---- |
|
||||
| **Core** draft spec | Draft | Kind 25910, `content` = stringified JSON-RPC, `p`/`e` tags, optional MCP lifecycle | `CVM-CORE-*` | A + B |
|
||||
| **CEP-4** Encryption | **Final** | NIP-44 encrypt the *signed* inner 25910 event into a NIP-59 wrap (kind 1059), **no rumor layer**; `support_encryption` | `CVM-4-*` | A + B + **D** |
|
||||
| **CEP-19** Ephemeral Gift Wraps | Draft | Kind **21059**, identical semantics to 1059 but ephemeral; `support_encryption_ephemeral`; MUST fall back to 1059 | `CVM-19-*` | A + B |
|
||||
| **CEP-6** Public Announcements | **Final** | Addressable **11316** server, **11317** tools, **11318** resources, **11319** resource templates, **11320** prompts; discovery tags `name`/`about`/`picture`/`website`/`support_*` | `CVM-6-*` | A + B |
|
||||
| **CEP-17** Relay List Metadata | Draft | NIP-65 **kind 10002**, unmarked `r` tags in the ContextVM profile; bootstrap vs advertised relays are distinct | `CVM-17-*` | A + B |
|
||||
| **CEP-35** Stateless Discovery | Draft, Info | Discovery tags on the **first direct message each side sends**; unknown tags MUST be preserved; `p`/`e` excluded from the learned surface | `CVM-35-*` | A + C |
|
||||
| **CEP-22** Oversized Transfer | Draft | Bounded reassembly over `notifications/progress`; `progressToken` = transfer id; `start`/`accept`/`chunk`/`end`/`abort`; `completionMode: "render"`; SHA-256 digest + `totalBytes` + `totalChunks` | `CVM-22-*` | A + B + C |
|
||||
| **CEP-41** Open Streams | Draft | Long-lived streams, same envelope; `start`/`accept`/`chunk`/`ping`/`pong`/`close`/`abort`; per-sender `progress`; contiguous `chunkIndex` | `CVM-41-*` | A + B + C |
|
||||
| **CEP-16** Client Pubkey Injection | **Final**, Info | Server injects `_meta.clientPubkey` into inbound requests; opt-in, default off | `CVM-16-*` | A + C (server role) |
|
||||
| **CEP-8** Pricing and Payment | Draft | `cap`/`pmi`/`payment_interaction`/`direct_payment`/`change` tags; transparent notification lifecycle vs `explicit_gating` JSON-RPC errors (`-32042`, `-32043`, `-32602`); canonical invocation identity | `CVM-8-*` | A + C + **E** |
|
||||
| **CEP-15** Common Tool Schemas | Draft | RFC 8785 JCS hash of `{name, normalized inputSchema, normalized outputSchema?}`; `io.contextvm/common-schema` `_meta`; NIP-73 `i`/`k` tags | `CVM-15-*` | A + B |
|
||||
| **CEP-21** PMI Recommendations | Draft, Info | PMI naming conventions, `-direct` suffix for bearer settlement | `CVM-21-*` | A |
|
||||
| **CEP-23** Server Profile Metadata | Draft | Server-published **kind 0** and optional **kind 1** | `CVM-23-*` | A + B |
|
||||
| **CEP-24** Server Reviews | Draft | NIP-22 **kind 1111** anchored to the `11316:<pubkey>:` `a` coordinate | `CVM-24-*` | A + B |
|
||||
|
||||
Design the module so CEP-8 is not *precluded* — a priced coordinator is plausible later — but do
|
||||
not build it.
|
||||
Gate legend (method in §6.4): **A** rule-derived unit tests · **B** live integration against a
|
||||
real counterparty · **C** adversarial tests against our own fixture server · **D** cross-
|
||||
implementation vector exchange · **E** wallet integration.
|
||||
|
||||
### 6.3 The subtle parts
|
||||
Status reality check: only **CEP-4, CEP-6 and CEP-16** are Final. The core spec and the nine other
|
||||
CEPs are Draft, including both large transfer profiles. Per the CEP guidelines a CEP reaches Final
|
||||
only once its reference implementation lands, so "Draft" here means the spec text may still move,
|
||||
not that it is unimplemented. Budget for churn (§6.7).
|
||||
|
||||
These are where a naive implementation passes unit tests and then fails against the live
|
||||
coordinator. Each is a spec MUST and should become a test.
|
||||
### 6.3 The high-risk rules
|
||||
|
||||
These are where an implementation passes its own unit tests and then diverges against a real peer.
|
||||
Each is a spec MUST and each maps to a named test in §6.5.
|
||||
|
||||
1. **CEP-41 has two ordering fields, and they are not interchangeable.** `progress` orders *all*
|
||||
frames (control frames included) and is explicitly **not** a chunk counter. `chunkIndex` starts
|
||||
@@ -302,10 +320,10 @@ coordinator. Each is a spec MUST and should become a test.
|
||||
10. **The inner event is fully signed, not a rumor.** CEP-4's flow signs the 25910 event *first*,
|
||||
then NIP-44-encrypts the whole thing into the wrap. Receivers verify the inner signature, and
|
||||
response correlation uses the **inner** `id`, not the gift wrap's.
|
||||
11. **Three separate correlation identifiers coexist**: the nostr `e` tag (event level), the
|
||||
JSON-RPC `id` (MCP level), and `progressToken` (CEP-22/41 transfer level). Plus
|
||||
`_meta.clientPubkey` (CEP-16) as the coordinator's authenticated caller identity — which is the
|
||||
mechanism §4.2's KeyPackage binding ultimately rests on.
|
||||
11. **Four separate correlation identifiers coexist**: the nostr `e` tag (event level), the
|
||||
JSON-RPC `id` (MCP level), `progressToken` (CEP-22/41 transfer level), and CEP-8's canonical
|
||||
invocation identity (payment level). Plus `_meta.clientPubkey` (CEP-16) as the authenticated
|
||||
caller identity — the mechanism §4.2's KeyPackage binding ultimately rests on.
|
||||
12. **Ephemeral delivery has no replay.** Per §6.1, subscribe before you publish. Combined with
|
||||
per-identity subscriptions (§8.2), each identity needs its own live `#p` subscription on 25910
|
||||
plus both gift wrap kinds.
|
||||
@@ -314,54 +332,190 @@ coordinator. Each is a spec MUST and should become a test.
|
||||
14. **`close.lastChunkIndex` is optional and meaningful.** Present, it is a completeness bound and
|
||||
every index `0..lastChunkIndex` must have arrived; omitted, the stream was open-ended and no
|
||||
bound is asserted. Senders omit it for live feeds.
|
||||
15. **CEP-8 excludes `params._meta` from the canonical invocation identity, but forwards it at
|
||||
execution.** The exclusion exists because MCP clients regenerate `progressToken` per call, so
|
||||
without it two semantically identical invocations never match one paid authorization. Getting
|
||||
this backwards either breaks retry matching or strips transport metadata from the handler.
|
||||
16. **CEP-8 forbids silent fallback.** A server that will not accept `explicit_gating` MUST NOT
|
||||
quietly use the transparent lifecycle; and a client that required `explicit_gating` SHOULD NOT
|
||||
auto-satisfy transparent `payment_required` notifications. A naive payment handler that pays
|
||||
whatever it is asked to pay violates the client half of this.
|
||||
17. **CEP-15's hash is a verification target, not a label.** The whole point is that two servers
|
||||
documenting a tool differently produce the same hash. A client that trusts the advertised
|
||||
`schemaHash` without recomputing it from the tool definition gains nothing from the CEP.
|
||||
|
||||
### 6.4 Build list for `:contextvm`
|
||||
### 6.4 Conformance method
|
||||
|
||||
Five tiers, because the CEPs are symmetric and no single counterparty exercises all of them.
|
||||
|
||||
**Tier A — rule-derived unit tests.** Every MUST/MUST NOT in a CEP becomes a named test carrying
|
||||
its rule id, asserted against pure codecs and frame state machines with no network. This is where
|
||||
the negative cases live and it is the bulk of the value: the CEP-22/41 validation sections are
|
||||
written almost entirely as failure conditions. Offline, fast, runs in `commonTest`.
|
||||
|
||||
**Tier B — live integration against a real counterparty.** `ghcr.io/cordn-msg/cordn:latest` with
|
||||
`CORDN_STORAGE_BACKEND=memory` and `CORDN_ANNOUNCED=false` boots in one command and enables
|
||||
CEP-22 and CEP-41. It covers the core spec, CEP-4/19, CEP-6/17 and the happy paths of 22/41.
|
||||
`cordn/packages/test-utils/src/mockRelay.ts` exists if we want a relay stub instead of a public
|
||||
relay. Tagged as an integration suite, not run on every build.
|
||||
|
||||
**Tier C — adversarial tests against our own fixture server.** This is the tier that does not
|
||||
exist yet and has to be built: **a Kotlin `:contextvm` test fixture that can play the server role
|
||||
and misbehave on demand.** No real server will send a non-monotonic `progress`, a second `start`
|
||||
on a live token, a `pong` with a stale nonce, a digest that does not match, or a
|
||||
`payment_required` in a session where `explicit_gating` was accepted — but our client must handle
|
||||
all of them correctly, and several are outright MUST-fail requirements. The fixture is also the
|
||||
only practical way to test CEP-16 (a server-side obligation) and the server half of CEP-8.
|
||||
Building it is a first-class Stage 2 deliverable, not test scaffolding.
|
||||
|
||||
**Tier D — cross-implementation vector exchange.** For the crypto surface, agreeing with ourselves
|
||||
is not evidence. Generate CEP-4 wrap/unwrap vectors and CEP-22/41 frame sequences, check them in
|
||||
under `quartz/src/commonTest/resources/contextvm/`, and verify both directions against the
|
||||
reference implementation the way `TsMlsWelcomeInteropTest` does for MLS. Ask upstream to adopt them
|
||||
(§10) — a shared vector set helps every non-TypeScript implementation and is a cheap contribution.
|
||||
|
||||
**Tier E — wallet integration.** CEP-8's client role is a payment *handler*, so compliance is only
|
||||
demonstrable end to end against a real rail. `bitcoin-lightning-bolt11` is the one recommended PMI
|
||||
(CEP-21) and we already have NIP-47 NWC and NIP-57 zaps in Quartz, so this is integration, not new
|
||||
payment code. Regtest or a small-amount live wallet; gated behind a manual test tag.
|
||||
|
||||
**Shared prerequisite: RFC 8785 (JCS).** Both CEP-8 (canonical invocation identity) and CEP-15
|
||||
(schema hash) require it, and **Quartz has no JCS implementation** — I checked; the
|
||||
`canonicalize` hits in the tree are IPv6, media types and relay URLs, all unrelated. So JCS is its
|
||||
own build item with its own vector suite (the RFC's test vectors, plus the number-formatting edge
|
||||
cases that make JCS genuinely tricky: `1E30`, `-0`, very small and very large doubles). Put it in
|
||||
`quartz/…/utils/` rather than in `:contextvm` — it is a generic primitive and NIP work may want it.
|
||||
|
||||
### 6.5 Per-CEP test catalog
|
||||
|
||||
Tier A cases, grouped by rule prefix. Negative cases are marked ✗ — they are the majority by
|
||||
design, and a suite without them proves nothing.
|
||||
|
||||
**`CVM-CORE`** — round-trip every message class (request, response, error, notification);
|
||||
`content` is a *string*, not an embedded object (a plausible early bug); reject a non-25910 kind;
|
||||
`e`-tag correlation maps a response to its request; ✗ a response published before we subscribed is
|
||||
unrecoverable (asserts the §6.1 lifecycle rule rather than pretending it works); the
|
||||
`initialize` → `notifications/initialized` sequence; and the stateless path succeeding with no
|
||||
initialize at all.
|
||||
|
||||
**`CVM-4`** — inner event is signed and verifies; wrap `p` tag names the recipient; two wraps of
|
||||
the same payload have **different** outer pubkeys (fresh key per wrap); decrypt recovers the inner
|
||||
event byte-for-byte; correlation uses the inner `id`; ✗ inner signature invalid → reject; ✗
|
||||
unsupported wrap kind → reject; conversation-key symmetry both directions. Tier D vectors here.
|
||||
|
||||
**`CVM-19`** — prefer 21059 when both peers advertise `support_encryption_ephemeral`; MUST fall
|
||||
back to 1059 when the peer does not; 21059 and 1059 decode identically; subscription filters
|
||||
include both kinds.
|
||||
|
||||
**`CVM-6`** — parse each of 11316–11320 (`content` is a stringified initialize/list result);
|
||||
replaceable semantics keep the newest `created_at` per `(kind, pubkey)`; all discovery tags parsed;
|
||||
optional tags absent → no failure; discovery tags seen on a first direct message are treated as
|
||||
equivalent to announcement tags (the CEP-6/CEP-35 overlap).
|
||||
|
||||
**`CVM-17`** — unmarked `r` tag means read **and** write; `read`/`write` markers honored when
|
||||
present; latest-wins replacement; bootstrap relays are publication targets and MUST NOT be assumed
|
||||
operational; absent 10002 → fall back to configured relays.
|
||||
|
||||
**`CVM-35`** — client sends capability/negotiation tags on its first direct message and omits them
|
||||
after; server tags are learned from the first direct server→client message even when it is not an
|
||||
initialize result; **unknown tags preserved** and reachable via a raw accessor; `p` and `e` excluded
|
||||
from the learned surface; a feature tag on a later message is message-local and does not mutate the
|
||||
session baseline.
|
||||
|
||||
**`CVM-22`** — happy path reassembles and validates; out-of-order chunks inside the buffer window
|
||||
reassemble correctly by `progress`; nothing is surfaced upward before validation succeeds; ✗ digest
|
||||
mismatch; ✗ `totalBytes` mismatch; ✗ `totalChunks` mismatch; ✗ `chunk` before `accept` in a
|
||||
stateless flow; ✗ non-monotonic `progress`; ✗ `end` with unresolved gaps; ✗ unknown
|
||||
`completionMode`; ✗ declared totals over local policy rejected at `start`; ✗ transfer started for a
|
||||
request with no `progressToken`; `abort` is terminal.
|
||||
|
||||
**`CVM-41`** — happy path streams incrementally; zero-chunk stream (`close` straight after
|
||||
`start`) succeeds; `close` with `lastChunkIndex` and every index present succeeds; `close` without
|
||||
`lastChunkIndex` on an open-ended feed succeeds; the final JSON-RPC response is still required and
|
||||
delivered after `close`; idle → `ping` → `pong` keeps the stream alive; ✗ no `pong` before probe
|
||||
timeout fails the stream; ✗ `pong` with unknown, duplicate or expired nonce is not liveness
|
||||
evidence; ✗ nonce over 64 bytes rejected; ✗ second `start` on a live `progressToken`; ✗
|
||||
non-contiguous `chunkIndex` at `close`; ✗ `close` with `lastChunkIndex` and a missing index; ✗
|
||||
frames after `close` or `abort` ignored; `pong.progress` unrelated to `ping.progress` (asserts
|
||||
§6.3.2 explicitly).
|
||||
|
||||
**`CVM-16`** — the fixture server injects `_meta.clientPubkey` derived from the event pubkey;
|
||||
injection is off by default; our client never sends `clientPubkey` itself (a client-supplied value
|
||||
would be a spoof, and the coordinator's §4.2 binding depends on it being server-derived).
|
||||
|
||||
**`CVM-8`** — `cap` tag parses fixed (`"100"`) and range (`"100-1000"`) prices with the
|
||||
`tool:`/`prompt:`/`resource:` prefixes; PMI intersection selection picks a mutually supported
|
||||
method; absent `payment_interaction` means `transparent`; a requested `explicit_gating` accepted by
|
||||
the server is disclosed on the first direct response; ✗ requested `explicit_gating` not accepted
|
||||
MUST NOT silently become transparent, and our handler MUST NOT auto-pay transparent
|
||||
`payment_required` in that session (§6.3.16); `-32602` shape on an unsupported mode; `-32042`
|
||||
`Payment Required` carries one or more `payment_options`; `-32043` `Payment Pending` with
|
||||
`retry_after`; mid-session mode upsert re-discloses on transition to `explicit_gating`; canonical
|
||||
identity is stable across a changed JSON-RPC `id`, a changed outer event id, and a regenerated
|
||||
`progressToken` (the `_meta` exclusion); transparent idempotency — the same outer event id is not
|
||||
charged twice; `ttl` expiry; at most one `direct_payment` tag, first supported PMI wins; `change`
|
||||
tag parsed on `payment_accepted`.
|
||||
|
||||
**`CVM-15`** — normalization strips `title`/`description`/`examples`/`default`/`deprecated`/
|
||||
`readOnly`/`writeOnly` and `x-*` keys **at every nesting level**; the same tool documented
|
||||
differently yields the same hash (the CEP's whole purpose); adding an `outputSchema` changes the
|
||||
hash; `$ref` bundled into a self-contained representation, with ✗ no network resolution attempted;
|
||||
`i`/`k` NIP-73 tags emitted and parsed; and the key client-side rule — **recompute the hash from
|
||||
the tool definition and reject a mismatched advertised `schemaHash`** rather than trusting it.
|
||||
|
||||
**`CVM-21`** — PMI format matches `[a-z0-9-]+`; `-direct` suffix detected as bearer-settlement
|
||||
capable; unknown PMI degrades gracefully rather than failing the session.
|
||||
|
||||
**`CVM-23`** — parse a server `kind 0` as NIP-01 metadata (reuses existing Quartz code); `kind 1`
|
||||
notes from a server pubkey carry no special semantics.
|
||||
|
||||
**`CVM-24`** — top-level review builds both uppercase `A`/`K`/`P` and lowercase `a`/`k`/`p` with
|
||||
`k = 11316`; a reply keeps uppercase `A`/`K`/`P` at the root announcement while using lowercase
|
||||
`e` for the parent comment and `k = 1111`; the discovery filter returns reviews for a given server.
|
||||
|
||||
### 6.6 Build list for `:contextvm`
|
||||
|
||||
Ordered so each item is testable before the next depends on it:
|
||||
|
||||
| # | Component | Notes |
|
||||
| - | --------- | ----- |
|
||||
| 1 | Kinds, tags, frame types, JSON-RPC 2.0 codec | Pure data; port the constant set from the spec |
|
||||
| 2 | Minimal MCP client | `initialize`, `notifications/initialized`, `tools/call`, typed errors, `_meta`/`progressToken` plumbing. Not a full MCP SDK — cordn uses tools only |
|
||||
| 3 | CEP-4/19 gift wrap | NIP-44 + 1059/21059 on existing `nip44Encryption` and `nip59Giftwrap`. Pin `REQUIRED` (§8.6) |
|
||||
| 4 | Correlation + subscription lifecycle | `#p` subscriptions on 25910 + both wrap kinds, `e`-tag routing, the subscribe-before-publish rule of §6.1, per-identity scoping |
|
||||
| 5 | CEP-35 discovery-tag learning | First-message exchange each way; preserve unknown tags |
|
||||
| 6 | CEP-6/17 server discovery | 11316–11320 readers + NIP-65 10002 relay resolution. Reuses `INostrClient` `accessories/` one-shots |
|
||||
| 7 | CEP-22 receiver | Frame state machine, bounded reassembly, admission control on `totalBytes`/`totalChunks`, digest verify |
|
||||
| 8 | CEP-41 receiver + writer | The two-counter state machine, keepalive, incremental delivery as a `Flow`, the dual completion of §6.3.3 |
|
||||
| 9 | CEP-22 sender | Only needed if we ever post a >64 KiB `msg_post`; defer until a real case appears |
|
||||
| 10 | Dual-signer plumbing | Account `NostrSigner` for stable, local keypair for ephemeral |
|
||||
| # | Component | Gate | Notes |
|
||||
| - | --------- | ---- | ----- |
|
||||
| 1 | Kinds, tags, frame types, JSON-RPC 2.0 codec | A | Pure data |
|
||||
| 2 | Minimal MCP client | A + B | `initialize`, `notifications/initialized`, `tools/call`, `tools/list`, typed errors, `_meta`/`progressToken` plumbing |
|
||||
| 3 | CEP-4/19 gift wrap | A + B + D | On existing `nip44Encryption` + `nip59Giftwrap`. Pin `REQUIRED` (§8.6) |
|
||||
| 4 | Correlation + subscription lifecycle | A + B | `#p` subscriptions on 25910 + both wrap kinds, `e`-tag routing, subscribe-before-publish, per-identity scoping |
|
||||
| 5 | CEP-35 discovery-tag learning | A | First-message exchange, unknown-tag preservation, raw accessor |
|
||||
| 6 | CEP-6/17/23 server discovery | A + B | 11316–11320 + 10002 + kind 0. Reuses `INostrClient` `accessories/` one-shots |
|
||||
| 7 | **Fixture server (Tier C)** | — | Plays the server role, misbehaves on demand. Unblocks every adversarial test below |
|
||||
| 8 | CEP-22 receiver | A + B + C | Frame machine, bounded reassembly, admission control, digest verify |
|
||||
| 9 | CEP-41 receiver + writer | A + B + C | Two-counter machine, keepalive, `Flow` delivery, dual completion |
|
||||
| 10 | CEP-22 sender | A + C | Proactive fragmentation with relay-size margin |
|
||||
| 11 | RFC 8785 JCS | A | In `quartz/…/utils/`, not here — shared by 8 and 15 (§6.4) |
|
||||
| 12 | CEP-15 common tool schemas | A + B | Normalization, hash, `i`/`k` tags, recompute-and-verify |
|
||||
| 13 | CEP-8 + CEP-21 payments | A + C + E | Both lifecycles, canonical identity, PMI registry; handler on NIP-47 NWC |
|
||||
| 14 | CEP-16 injection (server role) | A + C | Only meaningful in the fixture server and any server we later expose |
|
||||
| 15 | CEP-24 reviews | A + B | Thin layer on existing NIP-22 |
|
||||
| 16 | Dual-signer plumbing | A + B | Account `NostrSigner` for stable, local keypair for ephemeral |
|
||||
|
||||
Sizing reference: the SDK's client-side surface (`src/core` + `src/transport/nostr-client` +
|
||||
oversized-transfer + open-stream, tests excluded) is ~5.4k lines of TypeScript. Items 7 and 8 are
|
||||
the bulk of it and the bulk of the risk.
|
||||
oversized-transfer + open-stream, tests excluded) is ~5.4k lines of TypeScript, and that excludes
|
||||
payments, the server role and the announcement manager. Items 8, 9 and 13 are the bulk of the work
|
||||
and the bulk of the risk.
|
||||
|
||||
Reusable beyond cordn: this is a general MCP-over-Nostr client. Any future Amethyst work that
|
||||
wants to call a remote MCP server — or expose one — lands here rather than in a feature module.
|
||||
Reusable beyond cordn: this is a general MCP-over-Nostr client *and* the beginnings of a server.
|
||||
Any future Amethyst work that wants to call a remote MCP server — or expose one — lands here
|
||||
rather than in a feature module.
|
||||
|
||||
### 6.5 Conformance strategy
|
||||
### 6.7 Spec stability risk
|
||||
|
||||
No official ContextVM test vectors exist as far as I can find (worth asking upstream — see §10).
|
||||
So conformance has to be built:
|
||||
Only CEP-4, CEP-6 and CEP-16 are **Final**. Everything else, including the core spec and both
|
||||
transfer profiles, is **Draft** — and CEP-8 (722 lines), CEP-41 (534) and CEP-15 (502) are the
|
||||
three largest documents. Per the CEP guidelines, Final requires a completed reference
|
||||
implementation, so Draft here means the text can still move.
|
||||
|
||||
- **Rule-derived unit tests.** Every MUST in §6.3 and in CEP-22/41's validation sections becomes a
|
||||
test against the frame state machines, including the negative cases: non-monotonic `progress`, a
|
||||
second `start` on a live token, `close` with unresolved gaps, a `pong` with an unknown nonce,
|
||||
digest mismatch, `totalBytes` mismatch.
|
||||
- **Live integration against the reference coordinator.** `ghcr.io/cordn-msg/cordn:latest` with
|
||||
`CORDN_STORAGE_BACKEND=memory` and `CORDN_ANNOUNCED=false` boots in one command and enables both
|
||||
CEP-22 and CEP-41. `cordn/packages/test-utils/src/mockRelay.ts` exists if we want a relay stub
|
||||
rather than a public one.
|
||||
- **Ask upstream for vectors**, and offer ours. A shared vector set for CEP-22/41 framing would
|
||||
benefit every non-TypeScript implementation and is a cheap contribution.
|
||||
|
||||
### 6.6 Spec stability risk
|
||||
|
||||
Only CEP-4, CEP-6 and CEP-16 are **Final**. Everything cordn actually depends on beyond the core —
|
||||
CEP-19, CEP-22, CEP-41, CEP-35 — is **Draft**, and the core spec itself is Draft. CEP-22 and CEP-41
|
||||
are also the two largest and most intricate documents. Budget for churn, keep the frame state
|
||||
machines isolated behind a narrow interface, and pin which CEP revision we implemented in the
|
||||
module's README so a future reader can diff.
|
||||
Mitigations: keep each CEP's rules behind a narrow interface so a revision is a localized change;
|
||||
record the implemented revision (commit hash of `contextvm-docs`) in the module README and in each
|
||||
rule-id group; and make the Tier A suite the tripwire — when a CEP revises, the diff against our
|
||||
named rules says exactly what to change.
|
||||
|
||||
## 7. Licensing
|
||||
|
||||
@@ -509,19 +663,27 @@ Sourcing discipline, restated because it constrains the whole stage: implement f
|
||||
specification documents in `ContextVM/contextvm-docs`, **not** from the LGPL SDK (§7). Whoever
|
||||
takes this should avoid reading the SDK source at all; §6 was written so they do not have to.
|
||||
|
||||
Substages, matching §6.4's build list:
|
||||
Substages, matching §6.6's build list. Each ships when its rule-id group in §6.5 is green at the
|
||||
tier §6.2 assigns it:
|
||||
|
||||
- **2a — wire layer.** Items 1–4: constants and codecs, the minimal MCP client, CEP-4/19 gift wrap
|
||||
pinned to `REQUIRED` (§8.6), and the correlation + subscription lifecycle. Ships when a
|
||||
`tools/list` round-trips against the reference coordinator in Docker.
|
||||
- **2b — discovery.** Items 5–6: CEP-35 first-message tag learning and CEP-6/17 announcement and
|
||||
`tools/list` round-trips against the reference coordinator in Docker and `CVM-CORE`/`CVM-4`/
|
||||
`CVM-19` pass, with CEP-4 vectors exchanged both ways (Tier D).
|
||||
- **2b — discovery.** Items 5–6: CEP-35 first-message tag learning, CEP-6/17/23 announcement and
|
||||
relay resolution. Ships when a coordinator pubkey alone is enough to connect.
|
||||
- **2c — transfer profiles.** Items 7–8: the CEP-22 receiver and the CEP-41 receiver/writer. This
|
||||
is the bulk of the work and the bulk of the risk; §6.3 items 1–7 and 13–14 all live here. Ships
|
||||
when `msg_sub_many` streams a live backlog and a >64 KiB `msg_fetch_many` reassembles with a
|
||||
verified digest.
|
||||
- **2d — deferred.** Item 9 (CEP-22 *sender*) until a real >64 KiB `msg_post` exists. Not CEP-8,
|
||||
not CEP-15 — but do not architect them out (§6.2).
|
||||
- **2c — fixture server.** Item 7, and the gate for everything after it. A Kotlin `:contextvm`
|
||||
test double that plays the server role and can be told to violate any rule in §6.5. Without it
|
||||
the adversarial half of CEP-22/41 and all of CEP-16 are untestable.
|
||||
- **2d — transfer profiles.** Items 8–10: CEP-22 receiver, CEP-41 receiver/writer, CEP-22 sender.
|
||||
The bulk of the work and the risk; §6.3 items 1–7, 13 and 14 all live here. Ships when
|
||||
`msg_sub_many` streams a live backlog, a >64 KiB `msg_fetch_many` reassembles with a verified
|
||||
digest, and every ✗ case in `CVM-22`/`CVM-41` fails the way the spec requires.
|
||||
- **2e — JCS and schemas.** Items 11–12: RFC 8785 in `quartz/…/utils/` against the RFC's own
|
||||
vectors plus number-formatting edge cases, then CEP-15 on top of it.
|
||||
- **2f — payments and the rest.** Items 13–16: CEP-8 both lifecycles with the NIP-47 NWC handler
|
||||
(Tier E), CEP-16 injection in the fixture, CEP-24 reviews on existing NIP-22, dual-signer
|
||||
plumbing. Lowest priority — cordn needs none of it — but it is what makes the module complete.
|
||||
|
||||
Two constraints that shape the API and are easy to discover too late:
|
||||
|
||||
@@ -569,8 +731,11 @@ CEP-22/41 are where an implementation that "works" quietly diverges.
|
||||
|
||||
- Multi-device (§4.6) — nothing interoperable to build.
|
||||
- Reusing any `Marmot*`/`Mip*` type for cordn.
|
||||
- A Kotlin ContextVM *server* — `cordn-rs` exists, is faster, and shares the SQLite schema. If we
|
||||
ever want a coordinator, run theirs.
|
||||
- A production Kotlin ContextVM *server*. The Tier C fixture (§6.4) plays the server role for
|
||||
tests only. For a real coordinator, `cordn-rs` exists, is faster, and shares the SQLite schema —
|
||||
run theirs. The fixture is deliberately not hardened for deployment.
|
||||
- A full MCP SDK. `:contextvm` implements the client surface the CEPs define plus the fixture's
|
||||
server role, not MCP's sampling/roots/elicitation breadth.
|
||||
- Bridging a Marmot group and a cordn group into one MLS group. Possible in principle once §4.1
|
||||
and §4.4 are resolved, but it is a separate design with its own trust questions.
|
||||
|
||||
@@ -587,8 +752,8 @@ CEP-22/41 are where an implementation that "works" quietly diverges.
|
||||
5. **Whose coordinator?** The privacy analysis reads very differently for a self-hosted
|
||||
per-community coordinator versus the shipped public default.
|
||||
6. **Are there ContextVM conformance vectors?** None found in `contextvm-docs` or the SDK. Ask
|
||||
upstream, and offer ours — a shared CEP-22/41 framing vector set helps every non-TypeScript
|
||||
implementation and is a cheap contribution (§6.5).
|
||||
upstream, and offer ours — a shared CEP-4 wrap and CEP-22/41 framing vector set helps every
|
||||
non-TypeScript implementation and is a cheap contribution (Tier D, §6.4).
|
||||
7. **How stable are CEP-22 and CEP-41?** Both are Draft, both are the largest CEPs, and both are
|
||||
mandatory for cordn. A breaking revision mid-implementation is the main schedule risk in
|
||||
Stage 2c. Worth asking whether either is close to Final.
|
||||
@@ -599,3 +764,11 @@ CEP-22/41 are where an implementation that "works" quietly diverges.
|
||||
9. **The ContextVM spec repo has no LICENSE.** Implementing a protocol from a published spec is
|
||||
normal and fine, but if we want to quote rule text into KDoc or the module README, ask upstream
|
||||
to add one (CC-BY or similar) rather than assuming.
|
||||
10. **Should the Tier C fixture server become a shared conformance harness?** It is the piece the
|
||||
ecosystem is missing — a counterparty that can violate any rule on demand. Offering it
|
||||
upstream would make it the de facto ContextVM test suite, which is influence worth having but
|
||||
also a maintenance commitment beyond our own needs.
|
||||
11. **Is CEP-8 worth implementing at all, or just not precluding?** It is 722 lines, needs Tier E
|
||||
wallet integration, and no coordinator we know of prices anything. The full-list decision says
|
||||
build it; if that is really "build it when someone charges", say so now and 2f drops to a
|
||||
stub that surfaces `-32042` to the user instead of paying.
|
||||
|
||||
@@ -10,7 +10,7 @@ _Audited 2026-09-17. 13 plans: 7 shipped (archived), 0 in-progress, 5 queued, 1
|
||||
| [2026-07-03-incremental-negentropy-storage.md](2026-07-03-incremental-negentropy-storage.md) | Always-current (created_at, id) index so cold NEG-OPENs stop paying a full scan + seal (~340 ms at 50k vs strfry's ~21 ms). |
|
||||
| [2026-07-04-small-req-floor.md](2026-07-04-small-req-floor.md) | Small-REQ dispatch floor: decomposed, inline fast path tried and reverted (no wire-level win); floor is transport-side. |
|
||||
| [2026-08-13-gpu-pow-mining.md](2026-08-13-gpu-pow-mining.md) | GPU NIP-13 mining declined (ARMv8 has SHA-256 in silicon, mobile GPUs do not). Midstate is ~3x on JVM targets; Android hinges on Conscrypt per-digest JNI cost, still unmeasured. created_at refresh while mining shipped. |
|
||||
| [2026-09-17-cordn-interop.md](2026-09-17-cordn-interop.md) | Cordn (cordn.net) is an alternative binding of MLS onto Nostr, not an alternative to MLS: same ciphersuite `0x0001`, byte-identical ChaCha20-Poly1305 seal and NIP-01 envelope, but the delivery service is an MCP server over ContextVM with no key-package event kind. Only the RFC 9420 engine is shareable, and it is not yet Marmot-clean (3 files, 10 imports, 3 hardcoded policies). ContextVM must be written from scratch: full spec review puts it at 7 of 12 CEPs, 4 of them Draft, with CEP-22 (oversized transfer) and CEP-41 (open streams) carrying the risk. Blocked on the credential-identity encoding (raw 32 bytes vs 64-byte hex ASCII). Includes a coordinator metadata-exposure analysis. |
|
||||
| [2026-09-17-cordn-interop.md](2026-09-17-cordn-interop.md) | Cordn (cordn.net) is an alternative binding of MLS onto Nostr, not an alternative to MLS: same ciphersuite `0x0001`, byte-identical ChaCha20-Poly1305 seal and NIP-01 envelope, but the delivery service is an MCP server over ContextVM with no key-package event kind. Only the RFC 9420 engine is shareable, and it is not yet Marmot-clean (3 files, 10 imports, 3 hardcoded policies). ContextVM must be written from scratch: full review of the spec + all 12 CEPs, with a per-CEP compliance matrix, `CVM-*` rule ids and a 5-tier test method (including a fixture server that misbehaves on demand, and RFC 8785 JCS which Quartz lacks). Blocked on the credential-identity encoding (raw 32 bytes vs 64-byte hex ASCII). Includes a coordinator metadata-exposure analysis. |
|
||||
| [2026-09-08-marmot-spec-resync.md](2026-09-08-marmot-spec-resync.md) | Marmot moved off the MIP-era spec (2026-07-02): group state split into `app_data_dictionary` components, account identity proof v2, and a convergence engine. Current MDK rejects our groups outright. Gap analysis + 8-stage plan; Stages 0-4 done (mdk interop reference, app_data_dictionary, identity proof v2, the six group components, transport corrections); lifecycle + branch selection landed. |
|
||||
|
||||
## Archived (shipped)
|
||||
|
||||
Reference in New Issue
Block a user