From 24b2ae49d79470f47bc7594680e18e6c8c512215 Mon Sep 17 00:00:00 2001 From: Daniel Ahn Date: Sun, 16 Aug 2026 00:45:25 -0700 Subject: [PATCH 1/5] DAN-855: the conformance suite's documented size was wrong in all three places it appeared MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `spec/vectors/` holds 167 vectors across 14 files. The repo said otherwise three different ways: README.md claimed 133, and docs/specification.md claimed 161 in two separate places. All three are now the real number. Two of those are cosmetic. The third is not. §13 "Conformance" criterion 5 is the NORMATIVE definition of what makes an implementation s402-conformant, so an implementer in Go, Python or Rust was told they conformed at 161 vectors — leaving six shipped vectors outside the definition of conformance. That clause now binds conformance to *every* shipped vector and carries the count as a version-anchored parenthetical rather than as the operative term. Also fixed: the README told readers to find the vectors by cloning the repo and running `ls test/conformance/vectors/`. That directory is generated by scripts/prepare-publish.sh at publish time and is git-ignored, so following the instructions in a fresh clone produced "No such file or directory". Clones are now pointed at `spec/vectors/`, and the relationship between the two paths is stated so the npm path stays right for package consumers. A count written into prose is a derived value maintained by hand — it rots silently on every vector added, and nothing fails when it does. Three independent wrong numbers is what that looks like once it has been running a while. So the fix is not only the numbers: test/spec-doc-counts.test.ts counts spec/vectors/ and compares it against every claim in both documents. The test is deliberately built to refuse a vacuous pass. If the wording changes so that no claim matches, it fails rather than silently verifying nothing — a check that cannot fail is not a check. It was watched to fail against four mutations before being kept: a wrong count in the spec, a wrong count in the README, a reworded claim, and a vector added without a doc update. It reads `spec/vectors/` and not `typescript/test/conformance/vectors/` on purpose. The latter is git-ignored build output; a check keyed there passes on any machine that has ever packed and is red on its first CI run. Also adds the README's missing "who this is for / who this is not for" and a falsification section naming four observable conditions that would retire the project — per the README convention's third question, which the doc set had nowhere to put. Co-Authored-By: Claude Opus 5 --- README.md | 55 ++++++++- docs/specification.md | 4 +- typescript/CHANGELOG.md | 29 +++++ typescript/test/spec-doc-counts.test.ts | 141 ++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 typescript/test/spec-doc-counts.test.ts diff --git a/README.md b/README.md index 383ec89..911caf1 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,48 @@ HTTP 402 ("Payment Required") has been reserved since 1999 — waiting for a pay **s402 is Sui-native by design.** These advantages come from Sui's object model, PTBs, and sub-second finality. They can't be replicated on EVM — and they don't need to be. x402 already handles EVM well. s402 handles Sui better. +## Who This Is For + +**Use s402 if** you are charging for an HTTP endpoint that AI agents call, and either you settle +on Sui, or you need a payment model x402 does not have — metered draw-down, streaming with rate +enforcement, escrow with an arbiter, or pay-to-decrypt. Also use it if you are implementing the +protocol in another language: the wire format is specified independently of this codebase and +ships with conformance vectors to check yourself against. + +**Do not use s402 if** you are EVM-only and `exact` covers you — x402 is the better fit and we +say so. Also skip it if you want a payments *product*: s402 is a wire format and a set of types +with zero runtime dependencies. It does not move money, custody funds, or run a facilitator for +you. Fiat and card rails are out of scope and will stay that way. + +## What Would Prove This Should Stop + +s402's bet is narrow enough to be wrong in public. Any of the following would falsify it, and we +would rather retire the project than defend it past its evidence: + +1. **x402 absorbs the superset.** This README claims x402's two-scheme governance envelope + cannot take on prepaid, stream, escrow and unlock without re-ratification. If x402 ratifies + equivalents, the reason to run a second protocol is gone — the correct response is to fold + the schemes upstream and delete this repo, not to compete. + +2. **The prepaid economics do not survive contact with production.** The case for micropayments + rests on batching 1,000 payments into 2 on-chain transactions. If real deployments do not land + near the claimed ~$0.014 per 1,000 calls — because of contention on shared objects, gas-price + regimes, or settlement patterns we have not modelled — then the central advantage over + per-call settlement is arithmetic that only works on paper. + +3. **No independent implementation ever passes the vectors.** s402 claims to be a protocol, not a + library. A protocol with exactly one implementation is a library wearing a specification. If + the conformance vectors go unclaimed by any second-language implementation, the honest + description is "the wire format of the s402 npm package," and the spec should be demoted to + internal documentation. + +4. **Agent commerce settles somewhere other than HTTP 402.** If agent-to-service payment + converges on mandate-passing over a non-402 channel, then the status code this protocol is + built around is the wrong integration point, and being excellent at it does not matter. + +Points 1 and 3 are the ones to watch: both are observable from outside this repo, and neither +depends on our own judgement of our own work. + ## Which Scheme Should I Use? | Your situation | Scheme | Gas per 1K calls | Latency | @@ -349,16 +391,21 @@ const requirements: s402PaymentRequirements = { ## Conformance Testing -s402 ships 133 machine-readable JSON test vectors for cross-language conformance. If you're implementing s402 in Go, Python, Rust, or any other language, use these vectors to verify your implementation matches the spec. +s402 ships machine-readable JSON test vectors for cross-language conformance — 167 vectors across 14 files. If you're implementing s402 in Go, Python, Rust, or any other language, use these vectors to verify your implementation matches the spec. ```bash -# Vectors are in the npm package +# From the npm package ls node_modules/s402/test/conformance/vectors/ -# Or clone the repo -ls test/conformance/vectors/ +# Or from a clone of this repo +ls spec/vectors/ ``` +The two paths hold the same files. `spec/vectors/` is the canonical, version-controlled +location; `scripts/prepare-publish.sh` copies it into `test/conformance/vectors/` at publish +time so it lands inside the npm tarball. **In a fresh clone only `spec/vectors/` exists** — +`test/conformance/vectors/` is generated and git-ignored. + See [`test/conformance/README.md`](./typescript/test/conformance/README.md) for the vector format, encoding scheme, and implementation guide. ## Related diff --git a/docs/specification.md b/docs/specification.md index 92dc0a1..e04e29f 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -8,7 +8,7 @@ description: s402 Wire Format Specification v1 — the formal, field-by-field de This document defines the s402 wire format — the exact encoding, field definitions, validation rules, and error semantics for the s402 HTTP 402 payment protocol. It is the authoritative reference for any implementation in any language. -The TypeScript reference implementation lives at [github.com/s402-protocol/core](https://github.com/s402-protocol/core). Machine-readable conformance test vectors ship in the npm package (161 vectors across 13 files). +The TypeScript reference implementation lives at [github.com/s402-protocol/core](https://github.com/s402-protocol/core). Machine-readable conformance test vectors ship in the npm package (167 vectors across 14 files). ## 1. Terminology @@ -510,7 +510,7 @@ An implementation is **s402-conformant** if it: 2. Validates all required fields per §4.1, §5.1, and §6 3. Rejects malformed input with the appropriate error code from §8 4. Strips unknown keys on decode per §10 -5. Passes the 161 machine-readable conformance test vectors shipped in the `s402` npm package +5. Passes **every** machine-readable conformance test vector shipped in the `s402` npm package (167 vectors across 14 files as of v0.9.0) The conformance vectors cover: encode, decode, body transport, x402 compat normalization, receipt format/parse, settlement verification, validation rejection, key stripping, and roundtrip identity. See the [Conformance Vectors guide](/guide/conformance) for the vector format and implementation instructions. diff --git a/typescript/CHANGELOG.md b/typescript/CHANGELOG.md index 306ad6d..6079bcf 100644 --- a/typescript/CHANGELOG.md +++ b/typescript/CHANGELOG.md @@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **The documented size of the conformance suite was wrong everywhere it appeared.** The repo + stated it three different ways — `README.md` said 133 vectors, `docs/specification.md` said + 161 in two places — while `spec/vectors/` holds **167 vectors across 14 files**. All three now + report the real number. The consequential one was §13 *Conformance*, criterion 5: that clause + defines what makes an implementation s402-conformant, so an implementer in Go, Python or Rust + reading the spec was told they conformed at 161 vectors, leaving six shipped vectors outside + the definition of conformance. +- **The README told you to look for the conformance vectors in a directory that does not exist + in a clone.** It pointed at `test/conformance/vectors/`, which is generated by + `scripts/prepare-publish.sh` at publish time and is git-ignored. Cloning the repo and following + the instructions produced *No such file or directory*. The README now points clones at + `spec/vectors/` — the canonical, version-controlled location — and explains how the two paths + relate, so the npm path stays correct for package consumers. + +### Added + +- **A regression test that fails when a documented vector count drifts from reality** + (`test/spec-doc-counts.test.ts`). It counts `spec/vectors/` and compares against every + `" vectors across files"` claim in `README.md` and `docs/specification.md`, and it + refuses to pass vacuously: if the wording changes so no claim is found, the test fails rather + than silently verifying nothing. Counts written into prose are derived values maintained by + hand, and three independent wrong numbers in one repo is what that looks like after a while. +- **The README now says who s402 is for, who it is not for, and what would prove it should + stop.** The last of those is a falsification section naming four observable conditions that + would retire the project — chiefly x402 absorbing the superset schemes, and no independent + implementation ever passing the conformance vectors. + ## [0.8.0] - 2026-06-28 The transport-abstraction release (ADR-011): **one seam, three carriers — HTTP, MCP, and A2A.** diff --git a/typescript/test/spec-doc-counts.test.ts b/typescript/test/spec-doc-counts.test.ts new file mode 100644 index 0000000..0f8e6f7 --- /dev/null +++ b/typescript/test/spec-doc-counts.test.ts @@ -0,0 +1,141 @@ +/** + * Doc consistency — every conformance-vector count printed in the repo's prose + * must match what spec/vectors/ actually contains. + * + * ── WHY THIS EXISTS ───────────────────────────────────────────────────────── + * + * On 2026-08-16 (DAN-855) this repo stated the size of its own conformance + * suite THREE different ways, and all three were wrong: + * + * README.md "133 machine-readable JSON test vectors" + * specification.md §0 "161 vectors across 13 files" + * specification.md §13 "the 161 machine-readable conformance test vectors" + * spec/vectors/ 167 vectors across 14 files <- the truth + * + * The first two are cosmetic. The third is NORMATIVE: §13 "Conformance" + * criterion 5 defines what makes an implementation s402-conformant, so a + * third-party implementer in Go or Rust was told they were conformant at 161 + * vectors, leaving six shipped vectors outside the definition of conformance. + * + * A count written into prose is a derived value stored by hand. It rots + * silently every time a vector is added, and nothing fails when it does. Three + * independent numbers in one repo is what that rot looks like once it has been + * running for a while. This test is the thing that fails. + * + * ── WHY THESE PATHS AND NOT THE OTHER ONES ────────────────────────────────── + * + * There are two vector directories and only one of them exists everywhere: + * + * spec/vectors/ tracked, canonical, in every clone + * typescript/test/conformance/vectors/ GITIGNORED — a copy that + * scripts/prepare-publish.sh makes for + * `npm pack` + * + * The prose says the vectors "ship in the npm package", which points at the + * copy — so the naive reading is to count the copy. A check keyed there passes + * on any machine that has ever packed and is red on its first CI run, because a + * fresh checkout correctly does not have that directory. Every path read below + * is tracked and present in every checkout. conformance.test.ts resolves + * spec/vectors/ the same way, for the same reason. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const REPO_ROOT = join(import.meta.dirname, '..', '..'); +const VECTORS_DIR = join(REPO_ROOT, 'spec', 'vectors'); + +/** + * The canonical claim phrasing, held as a SOURCE STRING rather than a shared + * RegExp object. A regex carrying /g is stateful (`lastIndex`), and + * `expect(...).toMatch(re)` calls `.test()` internally, which advances that + * state — sharing one global regex across `matchAll` and `toMatch` makes + * results depend on assertion order, which is an intermittent false green. + * Every use below builds a fresh regex. + * + * Keeping the phrasing identical in both documents is deliberate: it is what + * lets one narrow pattern cover them, instead of a loose pattern that would + * match numbers it was never meant to. + */ +const CLAIM_PATTERN = String.raw`(\d+)\s+vectors\s+across\s+(\d+)\s+files`; +const claimsIn = (text: string) => [...text.matchAll(new RegExp(CLAIM_PATTERN, 'g'))]; + +/** + * Documents that state the count, and the minimum number of times each must + * state it. The minimum is the anti-vacuum guard: without it, rewording a + * sentence yields zero matches, zero comparisons, and a green test that has + * verified nothing. + */ +const DOCS = [ + { label: 'docs/specification.md', rel: ['docs', 'specification.md'], minClaims: 2 }, + { label: 'README.md', rel: ['README.md'], minClaims: 1 }, +] as const; + +function actualCounts(): { files: number; vectors: number } { + const files = readdirSync(VECTORS_DIR) + .filter((f) => f.endsWith('.json')) + .sort(); + + let vectors = 0; + for (const f of files) { + const parsed: unknown = JSON.parse(readFileSync(join(VECTORS_DIR, f), 'utf8')); + if (!Array.isArray(parsed)) { + throw new Error( + `spec/vectors/${f} is not a top-level JSON array — this test counts array ` + + `entries and would silently undercount a different shape.`, + ); + } + vectors += parsed.length; + } + + return { files: files.length, vectors }; +} + +describe('conformance-vector counts stated in prose', () => { + const actual = actualCounts(); + + it('spec/vectors/ is non-empty (guards against counting nothing)', () => { + expect(actual.files).toBeGreaterThan(0); + expect(actual.vectors).toBeGreaterThan(0); + }); + + for (const doc of DOCS) { + const path = join(REPO_ROOT, ...doc.rel); + + it(`${doc.label} states the count at least ${doc.minClaims}x`, () => { + const claims = claimsIn(readFileSync(path, 'utf8')); + expect( + claims.length, + `expected the " vectors across files" claim at least ${doc.minClaims} ` + + `time(s) in ${doc.label}. If the wording changed deliberately, update ` + + `CLAIM_PATTERN in this file — do not delete the assertion, or this check ` + + `silently stops checking.`, + ).toBeGreaterThanOrEqual(doc.minClaims); + }); + + it(`${doc.label} counts match spec/vectors/ on disk`, () => { + const claims = claimsIn(readFileSync(path, 'utf8')).map((m) => ({ + vectors: Number(m[1]), + files: Number(m[2]), + })); + for (const claim of claims) { + expect(claim).toEqual({ vectors: actual.vectors, files: actual.files }); + } + }); + } + + it('specification.md §13 still binds conformance to the vector set', () => { + const doc = readFileSync(join(REPO_ROOT, 'docs', 'specification.md'), 'utf8'); + + // indexOf returning -1 must fail loudly. `slice(-1)` hands back the + // document's last character, which is a non-empty string, so a "not empty" + // assertion would PASS on a missing section. + const start = doc.indexOf('## 13. Conformance'); + expect(start, 'the "## 13. Conformance" heading is gone from the spec').toBeGreaterThan(-1); + + const section = doc.slice(start); + expect(section).toMatch(/conformance test vector/i); + expect(section).toMatch(new RegExp(CLAIM_PATTERN)); + }); +}); From fd10efbdd69120c3a5359805e831ffa74de85e12 Mon Sep 17 00:00:00 2001 From: Daniel Ahn Date: Sun, 16 Aug 2026 00:56:37 -0700 Subject: [PATCH 2/5] DAN-855: every ADR now records whether it was built, and one of them was claiming a CI check that never existed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Status: Accepted` has only ever meant DECIDED. It has never said BUILT — so an ADR that shipped and an ADR that was ratified and quietly never built were byte-identical on the page. All twelve records now carry an `Implementation:` field, each determined against the code rather than against the prose. The ratio this makes countable: 7 of 12 shipped, 1 upheld, 2 in-progress, 2 not-started. ADR-006 was the only record already carrying an `Implementation:` field, and it was false. It stated in the present tense that scripts/compute-scheme-digests.mjs "runs in CI and writes spec/scheme-digests.json", failing any PR that changed a scheme doc without updating the digests. Neither file appears in any commit on any branch, and CI's five jobs contain no such gate. So the project's single answer to "was this built?" was a claim that read as verified precisely because it sat in the slot reserved for verified things. The paragraph is now marked as an unbuilt plan. Two findings fell out of doing this honestly: ADR-004's extensions framework ships as the `s402/extensions` subpath in package.json while its Status still reads Proposed — built without the decision ever being accepted. Status is left untouched: flipping a governance field is a decision, not a cleanup. ADR-010's S16 is half-built. Searching for the label found one narrative mention; searching for the MECHANISM found the protocol version genuinely bound into the signed envelope and required at envelope.ts:268. Its scheme-digest half is blocked on ADR-006, which is not-started. Grepping for a rule's name finds where someone wrote the name; grepping for its mechanism finds where it is enforced. Where the evidence is a search rather than a proof, the field says so — the negatives are recorded as "no witness found", not as "proven absent". Co-Authored-By: Claude Opus 5 --- docs/adr/001-protocol-boundaries.md | 1 + docs/adr/002-s402-is-pure-protocol.md | 1 + docs/adr/003-upto-scheme.md | 1 + docs/adr/004-extensions-architecture.md | 2 ++ docs/adr/005-interop-superset-principle.md | 1 + ...006-version-negotiation-and-scheme-digests.md | 14 +++++++++++++- docs/adr/007-settlement-response-envelope.md | 1 + docs/adr/008-safety-invariants-s9-s13.md | 1 + docs/adr/009-open-gaps-rotation-reorg.md | 1 + docs/adr/010-safety-invariants-s15-s16.md | 1 + docs/adr/011-transport-abstraction.md | 1 + typescript/CHANGELOG.md | 16 ++++++++++++++++ 12 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/adr/001-protocol-boundaries.md b/docs/adr/001-protocol-boundaries.md index 096c8b0..001df05 100644 --- a/docs/adr/001-protocol-boundaries.md +++ b/docs/adr/001-protocol-boundaries.md @@ -1,6 +1,7 @@ # ADR-001: Protocol Boundaries — Facilitator Trust, Receipt Cardinality, Scheme Cap, and Extension Hygiene **Status:** Accepted +**Implementation:** shipped **Date:** 2026-04-11 **Supersedes:** (none — first ADR) diff --git a/docs/adr/002-s402-is-pure-protocol.md b/docs/adr/002-s402-is-pure-protocol.md index 6a8f263..0491c7f 100644 --- a/docs/adr/002-s402-is-pure-protocol.md +++ b/docs/adr/002-s402-is-pure-protocol.md @@ -1,6 +1,7 @@ # ADR-002: s402 is a Pure Protocol Repo — mcp-server Moves to SweeFi **Status:** Accepted +**Implementation:** shipped **Date:** 2026-04-11 **Supersedes:** (none — refines the boundary that ADR-001 § Decision 1 named but did not enforce at the repo level) diff --git a/docs/adr/003-upto-scheme.md b/docs/adr/003-upto-scheme.md index 9bd9512..9b6482e 100644 --- a/docs/adr/003-upto-scheme.md +++ b/docs/adr/003-upto-scheme.md @@ -1,6 +1,7 @@ # ADR-003: `upto` Scheme — Usage-Based Payments with Settlement Overrides **Status:** Accepted (implemented — see note) +**Implementation:** shipped **Date:** 2026-04-12 **Supersedes:** (none) **Linear:** DAN-284 diff --git a/docs/adr/004-extensions-architecture.md b/docs/adr/004-extensions-architecture.md index 8e55efe..f501e36 100644 --- a/docs/adr/004-extensions-architecture.md +++ b/docs/adr/004-extensions-architecture.md @@ -1,6 +1,8 @@ # ADR-004: Extensions Architecture — Typed, Lifecycle-Aware Plugin System **Status:** Proposed +**Implementation:** shipped — `src/extensions.ts` (10KB), exported as the `s402/extensions` subpath in `package.json`. +⚠️ **`Status: Proposed` above is stale and is left as-is deliberately** (found 2026-08-16, DAN-855): this was built without the ADR ever being moved to Accepted. Flipping a governance field is a decision, not a cleanup, so it is surfaced here rather than changed unilaterally. It is also the exact gap the `Implementation:` field exists to expose — under `Status` alone, "proposed and never built" and "proposed and shipped" were the same page. **Date:** 2026-04-12 **Supersedes:** (none — implements the framework that ADR-001 Decision 4 described as hygiene rules) **Linear:** DAN-285 diff --git a/docs/adr/005-interop-superset-principle.md b/docs/adr/005-interop-superset-principle.md index 3907404..bd339ad 100644 --- a/docs/adr/005-interop-superset-principle.md +++ b/docs/adr/005-interop-superset-principle.md @@ -1,6 +1,7 @@ # ADR-005: Interop When Possible, Superset When Wise **Status:** Accepted +**Implementation:** upheld **Date:** 2026-04-14 **Supersedes:** (none — formalizes informal positioning against x402 and MPP) **Linear:** DAN-313 diff --git a/docs/adr/006-version-negotiation-and-scheme-digests.md b/docs/adr/006-version-negotiation-and-scheme-digests.md index 5efe549..6d4371a 100644 --- a/docs/adr/006-version-negotiation-and-scheme-digests.md +++ b/docs/adr/006-version-negotiation-and-scheme-digests.md @@ -1,6 +1,7 @@ # ADR-006: Version Negotiation and Scheme Content-Hashing **Status:** Draft +**Implementation:** not-started **Date:** 2026-04-19 **Supersedes:** implicit version string in `s402Version` field **Related:** ADR-001 (Protocol Boundaries), ADR-002 (Protocol-Pure), ADR-004 (Extensions), ADR-005 (Interop-Superset), INVARIANTS S3 @@ -95,7 +96,18 @@ advertised = "sha256-" || base64url_no_pad(digest) **Target is the prose spec, not derived artifacts.** The markdown spec at `docs/schemes/.md` is normative — it is the human-readable law of the scheme. JSON Schema files and BCS type definitions are derivative; their content-hashes are advertised separately in `spec/scheme-digests.json` under a `derived` key but do not drive version negotiation. -**Implementation:** `scripts/compute-scheme-digests.mjs` runs in CI and writes `spec/scheme-digests.json`. Any PR that modifies `docs/schemes/**/*.md` without updating the digests file fails CI. +**Implementation plan — NOT BUILT (verified 2026-08-16, DAN-855).** The intent is that +`scripts/compute-scheme-digests.mjs` runs in CI and writes `spec/scheme-digests.json`, so any PR +modifying `docs/schemes/**/*.md` without updating the digests file fails CI. + +⚠️ **None of that exists.** Neither file appears in any commit on any branch (`git log --all -- +'**/compute-scheme-digests.mjs' '**/scheme-digests.json'` returns nothing), and CI's five jobs +contain no such gate. This paragraph previously asserted the mechanism in the present tense, which +made an unbuilt plan read as a shipped guarantee — in the one field the ADR convention reserves for +answering exactly that question. The header now carries `Implementation: not-started`. + +`docs/schemes/` itself is real (`escrow.md`, `exact.md`, `prepaid.md`), so the thing this would +guard exists; only the guard is missing. ### Amendment chain diff --git a/docs/adr/007-settlement-response-envelope.md b/docs/adr/007-settlement-response-envelope.md index 12c893e..13bd685 100644 --- a/docs/adr/007-settlement-response-envelope.md +++ b/docs/adr/007-settlement-response-envelope.md @@ -1,6 +1,7 @@ # ADR-007: Settlement Response Envelope **Status:** Draft (v2 — post /vet wave review) +**Implementation:** shipped **Date:** 2026-04-19 **Related:** ADR-001 (Protocol Boundaries), ADR-006 (Version Negotiation), ADR-008 (Safety Invariants S9-S13), ADR-009 (Open Gaps), INVARIANTS S7, S8 **Supersedes:** `s402SettleResponse` (legacy flat shape in `typescript/src/scheme.ts`) diff --git a/docs/adr/008-safety-invariants-s9-s13.md b/docs/adr/008-safety-invariants-s9-s13.md index e894782..651dbe2 100644 --- a/docs/adr/008-safety-invariants-s9-s13.md +++ b/docs/adr/008-safety-invariants-s9-s13.md @@ -1,6 +1,7 @@ # ADR-008: Safety Invariants S9–S14 **Status:** Draft (v2 — post /vet wave review; S14 added, S11 hardened) +**Implementation:** in-progress — **S11** and **S14** are built (`src/envelope.ts:77`, `:430`, `:443`; S14's constant-time comparison has its own test at `test/envelope.test.ts:429`). **S9, S10, S12 and S13** have no enforcement site found. Determined 2026-08-16 (DAN-855) by searching for the mechanisms, not only the labels — but treat the negatives as *no witness found*, not *proven absent*. **Date:** 2026-04-19 **Related:** INVARIANTS.md (S1–S8), ADR-004 (Extensions), ADR-007 (Settlement Envelope), ADR-009 (Open Gaps) diff --git a/docs/adr/009-open-gaps-rotation-reorg.md b/docs/adr/009-open-gaps-rotation-reorg.md index d124a6f..92b2adf 100644 --- a/docs/adr/009-open-gaps-rotation-reorg.md +++ b/docs/adr/009-open-gaps-rotation-reorg.md @@ -1,6 +1,7 @@ # ADR-009: Open Gaps — Facilitator Key Rotation, Chain Reorg Tolerance, Scheme Acceptance Process **Status:** Placeholder — tracked gaps, not yet resolved +**Implementation:** not-started **Date:** 2026-04-19 **Related:** ADR-006, ADR-007, ADR-008, INVARIANTS S8, S11 diff --git a/docs/adr/010-safety-invariants-s15-s16.md b/docs/adr/010-safety-invariants-s15-s16.md index aac5239..725121b 100644 --- a/docs/adr/010-safety-invariants-s15-s16.md +++ b/docs/adr/010-safety-invariants-s15-s16.md @@ -1,6 +1,7 @@ # ADR-010: Safety Invariants S15–S16 — Session Binding and Version Binding **Status:** Draft +**Implementation:** in-progress — **S16 is half-built**: the protocol version *is* bound into the signed envelope rather than only into transport headers (`src/envelope.ts:54`, `:173`, `:219`, and required at `:268`). Its *scheme-digest* half is blocked on ADR-006, which is `not-started`. **S15** (binding long-running scheme state to the mandate/capability object rather than the signer's key) has no enforcement site in this repo — consistent with ADR-002, since mandates live upstack. Determined 2026-08-16 (DAN-855) by searching for the mechanisms rather than the labels; a label-only search finds where someone wrote "S16", not where it is enforced. **Date:** 2026-04-21 **Related:** INVARIANTS.md (S1–S8), ADR-006 (Version Negotiation), ADR-007 (Settlement Envelope), ADR-008 (Safety Invariants S9–S14), ADR-009 (Open Gaps) diff --git a/docs/adr/011-transport-abstraction.md b/docs/adr/011-transport-abstraction.md index c799646..e38a747 100644 --- a/docs/adr/011-transport-abstraction.md +++ b/docs/adr/011-transport-abstraction.md @@ -1,6 +1,7 @@ # ADR-011: Transport Abstraction — Payment Rides Any Carrier **Status:** Accepted (2026-06-28; revised after blind-spot review; Chunks 1a-i, 1a-ii, 1a-iii, and 2 landed + verified — HTTP + MCP + A2A all behind one seam, 1075 tests green) +**Implementation:** shipped **Date:** 2026-06-28 **Supersedes:** (none) **Related:** ADR-005 (interop/superset), ADR-002 (s402 is pure protocol), S7 (chain-agnostic boundary) diff --git a/typescript/CHANGELOG.md b/typescript/CHANGELOG.md index 6079bcf..e739504 100644 --- a/typescript/CHANGELOG.md +++ b/typescript/CHANGELOG.md @@ -23,8 +23,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `spec/vectors/` — the canonical, version-controlled location — and explains how the two paths relate, so the npm path stays correct for package consumers. +- **An architecture decision record claimed a CI check that has never existed.** ADR-006 stated, + in the present tense, that `scripts/compute-scheme-digests.mjs` "runs in CI and writes + `spec/scheme-digests.json`", failing any PR that changed a scheme doc without updating digests. + Neither file appears in any commit on any branch, and CI has no such job. It was the only ADR + carrying an `Implementation:` field, so the project's single answer to "was this built?" was + false. The paragraph is now marked as an unbuilt plan and the record reads + `Implementation: not-started`. + ### Added +- **Every ADR now records whether it was actually built.** All twelve carry an `Implementation:` + field (`shipped` · `in-progress` · `not-started` · `upheld`), each determined against the code + rather than the prose. `Status: Accepted` only ever meant *decided* — so a decision that shipped + and one that was ratified and quietly never built were indistinguishable on the page. The + conceived-to-shipped ratio is now countable: **7 of 12 shipped.** Two findings fell straight out + of the exercise: ADR-004's extensions framework ships as the `s402/extensions` subpath while its + `Status` still reads *Proposed*, and ADR-010's S16 turns out to be half-built — version binding + is enforced in the envelope, its scheme-digest half blocked on ADR-006. - **A regression test that fails when a documented vector count drifts from reality** (`test/spec-doc-counts.test.ts`). It counts `spec/vectors/` and compares against every `" vectors across files"` claim in `README.md` and `docs/specification.md`, and it From 6969a397d77b0d394200dae720871c97e03a3784 Mon Sep 17 00:00:00 2001 From: Daniel Ahn Date: Sun, 16 Aug 2026 01:10:23 -0700 Subject: [PATCH 3/5] =?UTF-8?q?DAN-855/DAN-872:=20the=20demo=20cannot=20re?= =?UTF-8?q?ach=20the=20network=20=E2=80=94=20say=20so=20where=20a=20strang?= =?UTF-8?q?er=20meets=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sui has deprecated JSON-RPC on public fullnodes. Every core method returns -32601 on testnet AND mainnet, verified directly on both rather than taken from a report. mcp-demo/src/server.ts and src/agent.ts both construct a SuiClient over that transport, and docs/guide/tutorial.md's "Making It Real" snippet tells readers to do the same. This is a hole in the done-bar item I had just called satisfied. Item 1 is "a stranger can run the one demo in 10 minutes", and I had verified the README's CLAIMS — paths, counts — without ever running the demo. The grant-ready standard I authored says to enumerate every shipped surface; I applied that to test suites and missed the runnable artifact, which is the surface the stranger test is actually about. The fix here is honesty, not repair: both surfaces now state the blocker above the instructions, so nobody follows a dead path. The migration to gRPC/GraphQL is tracked separately and is not attempted inside a documentation pass. Both notices are to be removed by that work — a stale blocker warning is its own defect, so the removal is written into that ticket's done-bar. Worth recording what did NOT break: typescript/package.json has no Sui dependency and never had one (ADR-002, invariant S7, enforced by the S7 Chain-Agnostic Boundary CI job). The entire protocol layer — encoding, validation, conformance vectors, all 1108 tests — runs offline and is untouched. The blast radius is exactly one demo and one snippet. That containment is the pure-protocol decision paying out, and it is the strongest evidence for S7 the repo has produced so far. Raised cross-lane by agent-b g11 from the SweeFi pass; verified independently before acting. Co-Authored-By: Claude Opus 5 --- docs/guide/tutorial.md | 10 ++++++++++ mcp-demo/README.md | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/guide/tutorial.md b/docs/guide/tutorial.md index 98c033d..b9ef977 100644 --- a/docs/guide/tutorial.md +++ b/docs/guide/tutorial.md @@ -233,6 +233,16 @@ This is the complete s402 protocol flow. In production, step 3 uses the Sui SDK ## Making It Real +> 🛑 **The snippet below will not connect today.** Sui has deprecated JSON-RPC on public +> fullnodes — `https://fullnode.{testnet,mainnet}.sui.io` returns `-32601` for every core method +> (verified 2026-08-16). A `SuiClient` pointed at a public fullnode is dead on arrival, so treat +> this section as illustrating the *shape* of the integration, not as runnable today. Reaching a +> live network requires the gRPC or GraphQL transport; migration is tracked separately. +> +> Everything **above** this section is unaffected: the s402 protocol layer has no Sui dependency +> (ADR-002, invariant S7), so the encoding, validation and error handling you just worked through +> all still run offline. + To connect this to Sui, replace the mock payment in `client.ts` with real Sui SDK code: ```typescript diff --git a/mcp-demo/README.md b/mcp-demo/README.md index 0fb1454..0915c21 100644 --- a/mcp-demo/README.md +++ b/mcp-demo/README.md @@ -2,6 +2,30 @@ A minimal Sui-native MCP server that advertises **three coexisting payment protocols** (x402, s402, stripe-mpp) inside a single `payment[]` envelope. Built as a reference implementation for [MCP SEP-2007](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2007). +> ## 🛑 This demo cannot currently reach the network +> +> **Sui has deprecated JSON-RPC on public fullnodes.** Every core method now returns `-32601` +> on **both testnet and mainnet** (verified 2026-08-16): +> +> ``` +> curl -s -X POST https://fullnode.testnet.sui.io:443 -H 'Content-Type: application/json' \ +> -d '{"jsonrpc":"2.0","id":1,"method":"sui_getChainIdentifier","params":[]}' +> # {"error":{"code":-32601,"message":"Method not found. JSON-RPC on public fullnodes has been +> # deprecated. Please migrate to gRPC or GraphQL endpoints."}} +> ``` +> +> `src/server.ts` and `src/agent.ts` both construct a `SuiClient` over that transport, so the +> end-to-end run does not work today. The **protocol layer is unaffected** — `s402` itself has +> no Sui dependency and never had one (ADR-002, invariant S7), so the envelope shapes, encoding +> and conformance vectors this demo illustrates are all still correct and all still tested. +> +> **What still works offline:** reading the `tools/list` envelope below, and the `s402` package's +> own suite (`cd ../typescript && pnpm vitest run`). +> +> Migration to gRPC/GraphQL is tracked separately — it is a migration, not a doc fix, and it is +> not being attempted inside a documentation pass. **This warning is removed when that lands**; +> a stale blocker notice is its own defect. + ## The headline artifact The `tools/list` response advertises three payment options per tool: From d9905f49c02c66f5f239bec66077c3423077466e Mon Sep 17 00:00:00 2001 From: Daniel Ahn Date: Sun, 16 Aug 2026 01:37:53 -0700 Subject: [PATCH 4/5] DAN-872: the demo settles over gRPC again, and the verifier no longer rejects payments that landed Sui deprecated JSON-RPC on public fullnodes, which left mcp-demo dead at the network layer. Both touchpoints now use SuiGrpcClient. The endpoint is unchanged. gRPC is served from the same host and port, so this is a client swap and not an endpoint move. Requires @mysten/sui 2.x, and that is not incidental. On the 1.45.2 the caret had resolved to, executeTransaction is rejected by current fullnodes with "invalid read_mask path: transaction.transaction", and neither the gRPC nor the GraphQL client can resolve a transaction for building. The write path cannot be migrated on 1.x at all. Four shape changes, each of which type-checks and then misbehaves: - balanceChanges is present-but-undefined unless requested via `include`. Left out, the credit check runs over undefined, returns false, and the demo rejects every real payment while looking like a working payment gate. - Responses are protobuf-wrapped as a union discriminated on $kind; the payload is at .Transaction, and the failure arm carries it under another key. - Balance changes are flat {coinType,address,amount}; JSON-RPC nested the recipient under owner.AddressOwner. - Execution status is a boolean discriminant, not status.status === 'success'. Also adds a bounded retry to the verifying read. getTransaction returns NOT_FOUND immediately after an execute that already reported success, because the public endpoint is load-balanced and the read can reach a node that has not caught up. Observed: first read NOT_FOUND, second read ~4s later succeeds. Without the retry the demo intermittently rejects settled payments, and does so more often on a fast machine. Verified end-to-end on testnet, not compiled: agent settles 7cVD9Z7SirgNhJYQMgaUcjnsbivfGcWFp5Fnozxg4WpX effects.status success balanceChanges receiver +10000000, sender -11997880 (amount + gas) server verifies tool result returned negative control bogus digest rejected with -32402 The negative control matters: an earlier run passed against a stale envelope-only server, whose verifier returns true without touching the chain. That was a false green. The gate was re-proven able to fail before this was called done. typescript/src is untouched, so S7 holds. Co-Authored-By: Claude Opus 5 --- mcp-demo/package.json | 6 +- mcp-demo/src/agent.ts | 31 +++++-- mcp-demo/src/server.ts | 66 +++++++++++---- pnpm-lock.yaml | 179 ++++++++++++++++++++--------------------- 4 files changed, 165 insertions(+), 117 deletions(-) diff --git a/mcp-demo/package.json b/mcp-demo/package.json index 2d33756..ccccde6 100644 --- a/mcp-demo/package.json +++ b/mcp-demo/package.json @@ -12,10 +12,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "s402": "workspace:*", - "hono": "^4.6.0", "@hono/node-server": "^1.13.0", - "@mysten/sui": "^1.18.0" + "@mysten/sui": "^2.24.0", + "hono": "^4.6.0", + "s402": "workspace:*" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/mcp-demo/src/agent.ts b/mcp-demo/src/agent.ts index 22e2141..102312a 100644 --- a/mcp-demo/src/agent.ts +++ b/mcp-demo/src/agent.ts @@ -1,4 +1,4 @@ -import { SuiClient, getFullnodeUrl } from '@mysten/sui/client'; +import { SuiGrpcClient } from '@mysten/sui/grpc'; import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; import { Transaction } from '@mysten/sui/transactions'; @@ -7,8 +7,12 @@ import { s402PaymentObject, DEMO_PROVIDER_ADDRESS } from './protocols/s402.js'; const SERVER_URL = process.env.SERVER_URL ?? 'http://localhost:3000'; const REAL_SETTLEMENT = process.env.SUI_REAL_SETTLEMENT === '1'; const MNEMONIC = process.env.SUI_TESTNET_MNEMONIC; +const SUI_RPC = process.env.SUI_RPC_URL ?? 'https://fullnode.testnet.sui.io:443'; -const sui = new SuiClient({ url: getFullnodeUrl('testnet') }); +// gRPC, not JSON-RPC: Sui deprecated JSON-RPC on public fullnodes and every core +// method now answers -32601. Note the URL is unchanged — gRPC is served from the +// same host and port, so this is a client swap and not an endpoint move. +const sui = new SuiGrpcClient({ network: 'testnet', baseUrl: SUI_RPC }); async function rpc(method: string, params: Record = {}, headers: Record = {}): Promise { const res = await fetch(SERVER_URL, { @@ -28,17 +32,28 @@ async function settleViaS402(): Promise { const keypair = Ed25519Keypair.deriveKeypair(MNEMONIC); const tx = new Transaction(); + // The gRPC client has no signAndExecuteTransaction, so the sender is no longer + // implied by a signer argument and must be set explicitly before the build. + tx.setSender(keypair.toSuiAddress()); const [coin] = tx.splitCoins(tx.gas, [s402PaymentObject.amount]); tx.transferObjects([coin], DEMO_PROVIDER_ADDRESS); - const result = await sui.signAndExecuteTransaction({ - signer: keypair, - transaction: tx, - options: { showEffects: true } + const bytes = await tx.build({ client: sui }); + const { signature } = await keypair.signTransaction(bytes); + + const result = await sui.core.executeTransaction({ + transaction: bytes, + signatures: [signature] }); - console.log(` Sui testnet tx: ${result.digest}`); - return result.digest; + // TransactionResult is a union discriminated on $kind; the failure arm carries + // the transaction under a different key, so narrow rather than reach for .Transaction. + if (result.$kind !== 'Transaction') { + throw new Error(`Settlement did not execute: ${result.FailedTransaction.digest}`); + } + + console.log(` Sui testnet tx: ${result.Transaction.digest}`); + return result.Transaction.digest; } async function main() { diff --git a/mcp-demo/src/server.ts b/mcp-demo/src/server.ts index 89486fb..4fc0528 100644 --- a/mcp-demo/src/server.ts +++ b/mcp-demo/src/server.ts @@ -1,6 +1,6 @@ import { Hono } from 'hono'; import { serve } from '@hono/node-server'; -import { SuiClient } from '@mysten/sui/client'; +import { SuiGrpcClient } from '@mysten/sui/grpc'; import { summarizeTool, summarize } from './tools/summarize.js'; import { s402PaymentObject, DEMO_PROVIDER_ADDRESS } from './protocols/s402.js'; @@ -11,7 +11,27 @@ const PORT = Number(process.env.PORT ?? 3000); const SUI_RPC = process.env.SUI_RPC_URL ?? 'https://fullnode.testnet.sui.io:443'; const REAL_SETTLEMENT = process.env.SUI_REAL_SETTLEMENT === '1'; -const sui = new SuiClient({ url: SUI_RPC }); +// gRPC, not JSON-RPC: Sui deprecated JSON-RPC on public fullnodes. SUI_RPC is +// unchanged — gRPC is served from the same host and port. +const sui = new SuiGrpcClient({ network: 'testnet', baseUrl: SUI_RPC }); + +// The public endpoint is load-balanced, so the verifying read can land on a node +// that has not yet caught up to the transaction the agent just executed. Observed: +// the first read returns NOT_FOUND and the second, ~4s later, succeeds — for a +// transaction whose own execute call already reported success. Without this retry +// the demo intermittently rejects payments that actually settled, and it does so +// more often on a fast machine. +const SETTLEMENT_READ_ATTEMPTS = 6; +const SETTLEMENT_READ_DELAY_MS = 2_000; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +const isNotFound = (err: unknown): boolean => { + const code = (err as { code?: unknown })?.code; + if (code === 'NOT_FOUND') return true; + const message = err instanceof Error ? err.message : String(err); + return /not[ %]?found/i.test(message); +}; const paymentRequired = (id: number | string) => ({ jsonrpc: '2.0' as const, @@ -28,21 +48,35 @@ const paymentRequired = (id: number | string) => ({ async function verifyS402Settlement(txDigest: string): Promise { if (!REAL_SETTLEMENT) return true; - try { - const tx = await sui.getTransactionBlock({ - digest: txDigest, - options: { showEffects: true, showBalanceChanges: true } - }); - if (tx.effects?.status?.status !== 'success') return false; - const credited = tx.balanceChanges?.some( - (c) => c.owner && typeof c.owner === 'object' && 'AddressOwner' in c.owner && - c.owner.AddressOwner === DEMO_PROVIDER_ADDRESS && - BigInt(c.amount) >= BigInt(s402PaymentObject.amount) - ); - return Boolean(credited); - } catch { - return false; + + for (let attempt = 1; attempt <= SETTLEMENT_READ_ATTEMPTS; attempt++) { + try { + // balanceChanges is empty unless it is requested. Omitting `include` returns + // the field present and undefined, which would make the credit check below + // fail closed on every real payment. + const result = await sui.core.getTransaction({ + digest: txDigest, + include: { effects: true, balanceChanges: true } + }); + + if (result.$kind !== 'Transaction') return false; + const tx = result.Transaction; + if (!tx.effects.status.success) return false; + + // gRPC balance changes are flat — { coinType, address, amount } — where + // JSON-RPC nested the recipient under owner.AddressOwner. + return tx.balanceChanges.some( + (change) => + change.address === DEMO_PROVIDER_ADDRESS && + BigInt(change.amount) >= BigInt(s402PaymentObject.amount) + ); + } catch (err) { + if (!isNotFound(err) || attempt === SETTLEMENT_READ_ATTEMPTS) return false; + await sleep(SETTLEMENT_READ_DELAY_MS); + } } + + return false; } const app = new Hono(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 241fb39..2552a68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^1.13.0 version: 1.19.14(hono@4.12.18) '@mysten/sui': - specifier: ^1.18.0 - version: 1.45.2(typescript@5.9.3) + specifier: ^2.24.0 + version: 2.24.0(typescript@5.9.3) hono: specifier: ^4.6.0 version: 4.12.18 @@ -75,16 +75,16 @@ importers: packages: - '@0no-co/graphql.web@1.2.0': - resolution: {integrity: sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==} + '@0no-co/graphql.web@1.3.3': + resolution: {integrity: sha512-4gFGBdyaFmQ6n9euhp5JtIGS4ZeivwDr1tCPENUxTvy5wyv532yOtFCr9zzYAJh1s6uibgC+TRXUcay+mxzCoQ==} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 peerDependenciesMeta: graphql: optional: true - '@0no-co/graphqlsp@1.15.4': - resolution: {integrity: sha512-Nt1DVHcZ08lKRKwhiU0amXH77fSdrO6DzyjLE0DkCxfbM/N1SAs32d76y1xtCzM5H9eT0iDS7SdksgRXWJu05g==} + '@0no-co/graphqlsp@1.17.4': + resolution: {integrity: sha512-5OneBcUU0dVbyrTqyFzmC1TtXmA1PxRKLLBO3O9wqmB6dzkidea1d+mK3ciqYDSejq22LzBQ2lb829aArK22/g==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 typescript: ^5.0.0 || ^6.0.0 @@ -537,25 +537,25 @@ packages: cpu: [x64] os: [win32] - '@gql.tada/cli-utils@1.7.3': - resolution: {integrity: sha512-3iQY5E/jvv3Lnh6D1Mh7zr+Bb9C/TGk1DHkm+lbIjQBnZAu2m+BcTcr1e3spUt6Aa6HG/xAN2XxpbWw9oZALEg==} + '@gql.tada/cli-utils@1.9.3': + resolution: {integrity: sha512-P1TiXErpJwIi73sei5fzwGA/SOeCaIHFWFR4RdZPLwqxZzQN0T6MAUivzXBgKCORL67rvYnLaaPoWeqWq/61ug==} peerDependencies: - '@0no-co/graphqlsp': ^1.12.13 - '@gql.tada/svelte-support': 1.0.2 - '@gql.tada/vue-support': 1.0.2 + '@0no-co/graphqlsp': ^1.16.0 + '@gql.tada/svelte-support': 1.0.3 + '@gql.tada/vue-support': 1.0.3 graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 - typescript: ^5.0.0 || ^6.0.0 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@gql.tada/svelte-support': optional: true '@gql.tada/vue-support': optional: true - '@gql.tada/internal@1.0.9': - resolution: {integrity: sha512-Bp8yi+kLrzIJ3l5Dfxhz48H4OCH2LCX+pShaPcJgh+oiBt6clrjUKDYNDD3Z78aDQ3+Tyrxe4dd0MfLgpSLPPg==} + '@gql.tada/internal@1.2.2': + resolution: {integrity: sha512-4lZcElPP6MC8Ct8KN70LR2WQsHjbAsyAmTGG09VsOPhx738UFIYaL0S1XiI2pqq2tj/sDs/y3b9jvKoWGL7iuQ==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 || ^17.0.0 - typescript: ^5.0.0 || ^6.0.0 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 '@graphql-typed-document-node/core@3.2.0': resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} @@ -595,15 +595,15 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@mysten/bcs@1.9.2': - resolution: {integrity: sha512-kBk5xrxV9OWR7i+JhL/plQrgQ2/KJhB2pB5gj+w6GXhbMQwS3DPpOvi/zN0Tj84jwPvHMllpEl0QHj6ywN7/eQ==} + '@mysten/bcs@2.1.0': + resolution: {integrity: sha512-rIR/cDAqDfBDxmYEZXppN/on8gmh1OW0dYi/Bk2kMBZcYMTaBaEtEX1VR6g7HicVxuMbFAIP0/SQkdH7J9Y5dQ==} - '@mysten/sui@1.45.2': - resolution: {integrity: sha512-gftf7fNpFSiXyfXpbtP2afVEnhc7p2m/MEYc/SO5pov92dacGKOpQIF7etZsGDI1Wvhv+dpph+ulRNpnYSs7Bg==} - engines: {node: '>=18'} + '@mysten/sui@2.24.0': + resolution: {integrity: sha512-VmIkgnMulRVeA0mnBgHiTFTci5XKJH5/UB8Kr2M9+1nZnKC1RKsNGRXWBtox+2HYtwHNdUjHxYchfWnfZ9yHkQ==} + engines: {node: '>=22'} - '@mysten/utils@0.2.0': - resolution: {integrity: sha512-CM6kJcJHX365cK6aXfFRLBiuyXc5WSBHQ43t94jqlCAIRw8umgNcTb5EnEA9n31wPAQgLDGgbG/rCUISCTJ66w==} + '@mysten/utils@0.4.0': + resolution: {integrity: sha512-nHlXECBl9kE+AHF/aw5a7JVNizae8Kb71A4H18BV/sXd5/l2BaWNGWVatq05fzJo3hF78AWorDxa++1TgcBKWw==} '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} @@ -611,13 +611,13 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@noble/curves@1.9.4': - resolution: {integrity: sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==} - engines: {node: ^14.21.3 || >=16} + '@noble/curves@2.3.0': + resolution: {integrity: sha512-v7cY+4oWYPQszRj6ZFGzTVL7uP2TaLo1xMhWHzYC5wj0ZhOXQ5x+sBre8rF3hi8cAoi0bh1qXoovoOkdFtvqEg==} + engines: {node: '>= 20.19.0'} - '@noble/hashes@1.8.0': - resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} - engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} + engines: {node: '>= 20.19.0'} '@oxc-project/types@0.112.0': resolution: {integrity: sha512-m6RebKHIRsax2iCwVpYW2ErQwa4ywHJrE4sCK3/8JK8ZZAWOKXaRJFl/uP51gaVyyXlaS4+chU1nSCdzYf6QqQ==} @@ -961,14 +961,14 @@ packages: cpu: [x64] os: [win32] - '@scure/base@1.2.6': - resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/base@2.3.0': + resolution: {integrity: sha512-NsG6Y03tY6R5BUis4FdVtHVkur0U6FOzskgs9ZXNl78CUc9fkZ78HmENUle1nSOkCasDmbubmWD9qwB7mm4PZA==} - '@scure/bip32@1.7.0': - resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + '@scure/bip32@2.3.0': + resolution: {integrity: sha512-mMPjNcXxJsvNveIgRIXrnwd4omu0wdXo4JowAUZO7/82+/bpAwVltclx6MG6nv2M3dA6IbfVv4xwWxaREm3OcA==} - '@scure/bip39@1.6.0': - resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@scure/bip39@2.3.0': + resolution: {integrity: sha512-qdyWuxoYwi3+YmqIsfkpz1I029m980WkVPilj+kG7VxSm+gKQ2BmQru3nv3LbMtpSUXuyZwdFT65JkpKu5OHOQ==} '@shikijs/core@2.5.0': resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} @@ -1377,14 +1377,14 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true - gql.tada@1.9.2: - resolution: {integrity: sha512-QxRHVpxtrOVdYXz6oavq0lBM+Zdp0swapLGJcD4SLpXDcsD337BHDFrzqqjfkbepv0sSAiO0LGabu1kI5D5Gyg==} + gql.tada@1.11.3: + resolution: {integrity: sha512-5JCI4j2f0nug8ILaCQys/yjOP78QqqjVUf47OQsME63rZfemsHT3e5vcfbHsnZiG6vxyqpKUJArtXEVwSefUhw==} hasBin: true peerDependencies: - typescript: ^5.0.0 || ^6.0.0 + typescript: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - graphql@16.14.0: - resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} + graphql@16.14.2: + resolution: {integrity: sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} has-flag@4.0.0: @@ -1813,8 +1813,8 @@ packages: synckit: optional: true - valibot@1.4.0: - resolution: {integrity: sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==} + valibot@1.4.2: + resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: @@ -1934,14 +1934,14 @@ packages: snapshots: - '@0no-co/graphql.web@1.2.0(graphql@16.14.0)': + '@0no-co/graphql.web@1.3.3(graphql@16.14.2)': optionalDependencies: - graphql: 16.14.0 + graphql: 16.14.2 - '@0no-co/graphqlsp@1.15.4(graphql@16.14.0)(typescript@5.9.3)': + '@0no-co/graphqlsp@1.17.4(graphql@16.14.2)(typescript@5.9.3)': dependencies: - '@gql.tada/internal': 1.0.9(graphql@16.14.0)(typescript@5.9.3) - graphql: 16.14.0 + '@gql.tada/internal': 1.2.2(graphql@16.14.2)(typescript@5.9.3) + graphql: 16.14.2 typescript: 5.9.3 '@algolia/abtesting@1.15.2': @@ -2285,22 +2285,22 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@gql.tada/cli-utils@1.7.3(@0no-co/graphqlsp@1.15.4(graphql@16.14.0)(typescript@5.9.3))(graphql@16.14.0)(typescript@5.9.3)': + '@gql.tada/cli-utils@1.9.3(@0no-co/graphqlsp@1.17.4(graphql@16.14.2)(typescript@5.9.3))(graphql@16.14.2)(typescript@5.9.3)': dependencies: - '@0no-co/graphqlsp': 1.15.4(graphql@16.14.0)(typescript@5.9.3) - '@gql.tada/internal': 1.0.9(graphql@16.14.0)(typescript@5.9.3) - graphql: 16.14.0 + '@0no-co/graphqlsp': 1.17.4(graphql@16.14.2)(typescript@5.9.3) + '@gql.tada/internal': 1.2.2(graphql@16.14.2)(typescript@5.9.3) + graphql: 16.14.2 typescript: 5.9.3 - '@gql.tada/internal@1.0.9(graphql@16.14.0)(typescript@5.9.3)': + '@gql.tada/internal@1.2.2(graphql@16.14.2)(typescript@5.9.3)': dependencies: - '@0no-co/graphql.web': 1.2.0(graphql@16.14.0) - graphql: 16.14.0 + '@0no-co/graphql.web': 1.3.3(graphql@16.14.2) + graphql: 16.14.2 typescript: 5.9.3 - '@graphql-typed-document-node/core@3.2.0(graphql@16.14.0)': + '@graphql-typed-document-node/core@3.2.0(graphql@16.14.2)': dependencies: - graphql: 16.14.0 + graphql: 16.14.2 '@hono/node-server@1.19.14(hono@4.12.18)': dependencies: @@ -2337,36 +2337,36 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mysten/bcs@1.9.2': + '@mysten/bcs@2.1.0': dependencies: - '@mysten/utils': 0.2.0 - '@scure/base': 1.2.6 + '@mysten/utils': 0.4.0 + '@scure/base': 2.3.0 - '@mysten/sui@1.45.2(typescript@5.9.3)': + '@mysten/sui@2.24.0(typescript@5.9.3)': dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.0) - '@mysten/bcs': 1.9.2 - '@mysten/utils': 0.2.0 - '@noble/curves': 1.9.4 - '@noble/hashes': 1.8.0 + '@graphql-typed-document-node/core': 3.2.0(graphql@16.14.2) + '@mysten/bcs': 2.1.0 + '@mysten/utils': 0.4.0 + '@noble/curves': 2.3.0 + '@noble/hashes': 2.3.0 '@protobuf-ts/grpcweb-transport': 2.11.1 '@protobuf-ts/runtime': 2.11.1 '@protobuf-ts/runtime-rpc': 2.11.1 - '@scure/base': 1.2.6 - '@scure/bip32': 1.7.0 - '@scure/bip39': 1.6.0 - gql.tada: 1.9.2(graphql@16.14.0)(typescript@5.9.3) - graphql: 16.14.0 + '@scure/base': 2.3.0 + '@scure/bip32': 2.3.0 + '@scure/bip39': 2.3.0 + gql.tada: 1.11.3(graphql@16.14.2)(typescript@5.9.3) + graphql: 16.14.2 poseidon-lite: 0.2.1 - valibot: 1.4.0(typescript@5.9.3) + valibot: 1.4.2(typescript@5.9.3) transitivePeerDependencies: - '@gql.tada/svelte-support' - '@gql.tada/vue-support' - typescript - '@mysten/utils@0.2.0': + '@mysten/utils@0.4.0': dependencies: - '@scure/base': 1.2.6 + '@scure/base': 2.3.0 '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: @@ -2375,11 +2375,11 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@noble/curves@1.9.4': + '@noble/curves@2.3.0': dependencies: - '@noble/hashes': 1.8.0 + '@noble/hashes': 2.3.0 - '@noble/hashes@1.8.0': {} + '@noble/hashes@2.3.0': {} '@oxc-project/types@0.112.0': {} @@ -2575,18 +2575,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true - '@scure/base@1.2.6': {} + '@scure/base@2.3.0': {} - '@scure/bip32@1.7.0': + '@scure/bip32@2.3.0': dependencies: - '@noble/curves': 1.9.4 - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 + '@noble/curves': 2.3.0 + '@noble/hashes': 2.3.0 + '@scure/base': 2.3.0 - '@scure/bip39@1.6.0': + '@scure/bip39@2.3.0': dependencies: - '@noble/hashes': 1.8.0 - '@scure/base': 1.2.6 + '@noble/hashes': 2.3.0 '@shikijs/core@2.5.0': dependencies: @@ -3089,19 +3088,19 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - gql.tada@1.9.2(graphql@16.14.0)(typescript@5.9.3): + gql.tada@1.11.3(graphql@16.14.2)(typescript@5.9.3): dependencies: - '@0no-co/graphql.web': 1.2.0(graphql@16.14.0) - '@0no-co/graphqlsp': 1.15.4(graphql@16.14.0)(typescript@5.9.3) - '@gql.tada/cli-utils': 1.7.3(@0no-co/graphqlsp@1.15.4(graphql@16.14.0)(typescript@5.9.3))(graphql@16.14.0)(typescript@5.9.3) - '@gql.tada/internal': 1.0.9(graphql@16.14.0)(typescript@5.9.3) + '@0no-co/graphql.web': 1.3.3(graphql@16.14.2) + '@0no-co/graphqlsp': 1.17.4(graphql@16.14.2)(typescript@5.9.3) + '@gql.tada/cli-utils': 1.9.3(@0no-co/graphqlsp@1.17.4(graphql@16.14.2)(typescript@5.9.3))(graphql@16.14.2)(typescript@5.9.3) + '@gql.tada/internal': 1.2.2(graphql@16.14.2)(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - '@gql.tada/svelte-support' - '@gql.tada/vue-support' - graphql - graphql@16.14.0: {} + graphql@16.14.2: {} has-flag@4.0.0: {} @@ -3571,7 +3570,7 @@ snapshots: dependencies: rolldown: 1.0.0-rc.16 - valibot@1.4.0(typescript@5.9.3): + valibot@1.4.2(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 From 21d586e44cf5f2e79d73b8d357f6e454c5aa54f7 Mon Sep 17 00:00:00 2001 From: Daniel Ahn Date: Sun, 16 Aug 2026 01:40:24 -0700 Subject: [PATCH 5/5] DAN-872: remove the blocker notices, because the thing they warned about is fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notices in mcp-demo/README.md and docs/guide/tutorial.md said the demo could not reach the network and that they would be removed when the migration landed. It landed in the previous commit, so they go in the same PR — a stale blocker warning is its own defect, and this one would now be actively false. The tutorial snippet is updated to the transport that works: SuiGrpcClient, an explicit tx.setSender (the gRPC client has no signAndExecuteTransaction to infer it from), and a note that this needs @mysten/sui 2.x. Two README claims the migration made untrue are corrected: the declared dependency (^1.18.0 -> ^2.24.0) and the verification path, which is no longer SuiClient.getTransactionBlock. The retry on the verifying read is documented where someone reading the settlement flow will meet it. Deliberately NOT changed: "Demo returns a 501 JSON-RPC error" further down that README. That JSON-RPC is MCP's own wire protocol, not Sui's node RPC, and it is still correct. Only Sui's transport was deprecated. docs/guide/quickstart.md also mentions @mysten/sui and still carries no notice. That is correct rather than an oversight: it names no transport, constructs no client and points at no fullnode, so it stayed true throughout. The SDK did not die, the JSON-RPC transport did. Co-Authored-By: Claude Opus 5 --- docs/guide/tutorial.md | 31 ++++++++++++++++--------------- mcp-demo/README.md | 33 +++++++-------------------------- 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/docs/guide/tutorial.md b/docs/guide/tutorial.md index b9ef977..ba05893 100644 --- a/docs/guide/tutorial.md +++ b/docs/guide/tutorial.md @@ -233,30 +233,31 @@ This is the complete s402 protocol flow. In production, step 3 uses the Sui SDK ## Making It Real -> 🛑 **The snippet below will not connect today.** Sui has deprecated JSON-RPC on public -> fullnodes — `https://fullnode.{testnet,mainnet}.sui.io` returns `-32601` for every core method -> (verified 2026-08-16). A `SuiClient` pointed at a public fullnode is dead on arrival, so treat -> this section as illustrating the *shape* of the integration, not as runnable today. Reaching a -> live network requires the gRPC or GraphQL transport; migration is tracked separately. -> -> Everything **above** this section is unaffected: the s402 protocol layer has no Sui dependency -> (ADR-002, invariant S7), so the encoding, validation and error handling you just worked through -> all still run offline. - -To connect this to Sui, replace the mock payment in `client.ts` with real Sui SDK code: +To connect this to Sui, replace the mock payment in `client.ts` with real Sui SDK code. + +Use the **gRPC** client, not `SuiClient`: Sui has deprecated JSON-RPC on public fullnodes, and +every core method there now answers `-32601`. The URL is unchanged — gRPC is served from the same +host and port — so this is a client swap, not an endpoint move. It needs `@mysten/sui` **2.x**; +the 1.x gRPC client cannot execute against current fullnodes. ```typescript -import { SuiClient } from '@mysten/sui/client'; +import { SuiGrpcClient } from '@mysten/sui/grpc'; import { Transaction } from '@mysten/sui/transactions'; import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; -// Build a real payment +const keypair = new Ed25519Keypair(); // or load from keystore +const suiClient = new SuiGrpcClient({ + network: 'testnet', + baseUrl: 'https://fullnode.testnet.sui.io:443', +}); + +// Build a real payment. The sender must be set explicitly — there is no +// signAndExecuteTransaction on the gRPC client to infer it from a signer. const tx = new Transaction(); +tx.setSender(keypair.toSuiAddress()); const [coin] = tx.splitCoins(tx.gas, [requirements.amount]); tx.transferObjects([coin], requirements.payTo); -const keypair = new Ed25519Keypair(); // or load from keystore -const suiClient = new SuiClient({ url: 'https://fullnode.testnet.sui.io' }); const txBytes = await tx.build({ client: suiClient }); const { bytes, signature } = await keypair.signTransaction(txBytes); diff --git a/mcp-demo/README.md b/mcp-demo/README.md index 0915c21..21b9c1e 100644 --- a/mcp-demo/README.md +++ b/mcp-demo/README.md @@ -2,30 +2,6 @@ A minimal Sui-native MCP server that advertises **three coexisting payment protocols** (x402, s402, stripe-mpp) inside a single `payment[]` envelope. Built as a reference implementation for [MCP SEP-2007](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2007). -> ## 🛑 This demo cannot currently reach the network -> -> **Sui has deprecated JSON-RPC on public fullnodes.** Every core method now returns `-32601` -> on **both testnet and mainnet** (verified 2026-08-16): -> -> ``` -> curl -s -X POST https://fullnode.testnet.sui.io:443 -H 'Content-Type: application/json' \ -> -d '{"jsonrpc":"2.0","id":1,"method":"sui_getChainIdentifier","params":[]}' -> # {"error":{"code":-32601,"message":"Method not found. JSON-RPC on public fullnodes has been -> # deprecated. Please migrate to gRPC or GraphQL endpoints."}} -> ``` -> -> `src/server.ts` and `src/agent.ts` both construct a `SuiClient` over that transport, so the -> end-to-end run does not work today. The **protocol layer is unaffected** — `s402` itself has -> no Sui dependency and never had one (ADR-002, invariant S7), so the envelope shapes, encoding -> and conformance vectors this demo illustrates are all still correct and all still tested. -> -> **What still works offline:** reading the `tools/list` envelope below, and the `s402` package's -> own suite (`cd ../typescript && pnpm vitest run`). -> -> Migration to gRPC/GraphQL is tracked separately — it is a migration, not a doc fix, and it is -> not being attempted inside a documentation pass. **This warning is removed when that lands**; -> a stale blocker notice is its own defect. - ## The headline artifact The `tools/list` response advertises three payment options per tool: @@ -89,10 +65,13 @@ Look at the `package.json`. Four runtime dependencies: "s402": "workspace:*", "hono": "^4.6.0", "@hono/node-server": "^1.13.0", - "@mysten/sui": "^1.18.0" + "@mysten/sui": "^2.24.0" } ``` +`@mysten/sui` is pinned to 2.x deliberately. Settlement runs over gRPC because Sui deprecated +JSON-RPC on public fullnodes, and the 1.x gRPC client cannot execute against current fullnodes. + **No `@coinbase/x402` SDK. No Stripe SDK.** The s402 wire-format library natively emits both x402 V2 PaymentRequirements (via `s402/compat/x402`'s `toX402V2Requirements()`) AND MPP-shaped charge challenges (via `s402/compat/mpp`'s `toMppChargeChallenge()`). The x402 entry matches upstream `@x402/core/types/payments.ts` HEAD: slimmer V2 shape with required `extra: {}`, no per-requirement `x402Version`, no `maxAmountRequired` alias. The Stripe MPP entry's `request` field is real base64url-encoded JCS — decode it and you get the canonical MPP Charge Request JSON. Both write paths are roundtrip-stable against the read-path inverses (`fromX402Envelope`, `decodeMppChargeRequest`). The architectural argument — *s402 is a superset by chain-feature construction* — is visible in the import list, not pitched in prose. ## Quick start @@ -137,7 +116,9 @@ By default, the demo runs in **envelope-only** mode (no on-chain transactions). pnpm --filter s402-mcp-demo dev:agent ``` -The server (which must also be run with `SUI_REAL_SETTLEMENT=1`) verifies the on-chain settlement via `SuiClient.getTransactionBlock` and checks the balance change credits the demo provider address before executing the tool. +The server (which must also be run with `SUI_REAL_SETTLEMENT=1`) verifies the on-chain settlement via the gRPC client's `core.getTransaction` and checks the balance change credits the demo provider address before executing the tool. + +The verifying read retries on `NOT_FOUND`. The public endpoint is load-balanced, so the read can reach a fullnode that has not yet caught up to the transaction the agent just executed — without the retry the server would intermittently reject payments that actually settled. ## What this demo proves