Summary
grpc-transport.md is Stable with six MUSTs, a proto that calls itself abridged and defers its own messages to a v1.x that never came, no client in the suite, and zero advertisers — Axiom 1's exact failure. supportedTransports lists one baseline, one real transport nobody serves, and two embedded protocols that have richer first-class blocks; nothing reads its mcp or a2a members. The legacy A2A 0.3 and MCP 2025-06-18 profiles have dated sunsets (2027-03-12, 2027-08-12), one dual-era advertiser each, and removal recorded as "a v2 change." Version negotiation on both protocols is fail-closed only for an already-authenticated request, has no minimum-version policy in normative text, no refresh SLA, no audit event, and no threat model; its only witness is a seam a production host never mounts. v2 demotes gRPC, deletes supportedTransports, removes the legacy profiles at the cut, makes negotiation authenticated with an advertised floor, a refresh SLA, a ceiling on MRTR rounds and a content-free audit event, and writes the threat model both Stable documents cite.
Motivation
grpc-transport.md:3Stable;api/grpc/openwop.proto:14–20, 68–72"abridged … deferred to v1.x … a future v1.x minor expands every Struct";conformance/src/scenarios/grpc-transport.test.ts:5"SHAPE-ONLY by design … the suite ships no gRPC client";grpc-transport.md:202still says RFC 0094 is "in flight";extensions.json:662draft,witnessable-gatedon a scenario that dials nothing; nogrpcin INTEROP-MATRIX.capabilities.schema.json:370supportedTransports: ["rest","mcp","a2a","grpc"];profiles.md:86/102read onlyrest;a2a-integration.md:3andmcp-integration.md:3describe compositions, not transports;a2acarries its own transport list inside its block (G4) andmcp.serverMount.transportsis its own enum.- Legacy windows:
a2a-integration.md:333(2027-03-12; "Removal of the 0.3 code path from the corpus is a v2 change"),mcp-integration.md:251(2027-08-12; same); RFC 0152 G1 / 0153 G1 measured: one dual-era advertiser each (openwop-app), no reference host advertises versions; the-legacysuffix is baked into bothprofilesitem patterns (capabilities.schema.json:2908, 3400), so removal is a pattern change;mcp-integration.md:316lists the legacy-profile scenarios the cut deletes. - Negotiation:
a2a-integration.md:341andmcp-integration.md:260— "for an authenticated request the default is fail-closed"; the exchange itself is not required to be authenticated; "minimum-version policy" exists only as a mitigation phrase in RFC 0152/0153 R1; the refresh SLA is RFC 0147 R10, unwritten; the audit event is RFC 0152/0153 G7 (authorization.decidedas carrier, no dedicated type);host-sample-test-seams.md:954— the wire toward a peer is "what no black-box request to the host's own API can observe," so both R1 rows areblockedon every host. mcp-integration.md:283"the host MUST bound the number of rounds (host policy)" — a MUST with no number (G9). RFC 0100 UQ4 —auth-requiredin the persistedA2ATaskStateenum for the reverse direction, never emitted forward.SECURITY/threat-model-interop.mddoes not exist;a2a-integration.md:509andmcp-integration.md:331(G8) cite it; the G7/G8/G9 rows live only in the spec docs — the RFC registers stop at G7 and G5.
Proposal
§A. gRPC
§A.1 grpc-transport.md moves to spec/v2/ext/grpc-transport/ with header witness: unwitnessable (the suite ships no client) and adoption: none; its six MUSTs become SHOULDs of the extension; api/grpc/openwop.proto is retired from the canonical API set (api/v2/ carries OpenAPI and AsyncAPI only) and kept under ext/grpc-transport/openwop.proto as a non-normative sketch. If a Phase 3 contributor generates the proto from the C.2 declaration file and lands a suite client, the extension re-enters core/ by a v2.x additive RFC — the door is named, not left ajar. The capabilities.grpc block is removed from the closed root (an unwitnessable family may not be advertised, RFC 0169 §A.1); grpc-transport.md:202's "in flight" sentence is corrected now (editorial).
§B. supportedTransports is deleted
§B.1 REST is the wire; there is no transport advertisement. A2A and MCP are compositions advertised by their own facets (a2a.versions[], a2a.preferredVersion, a2a.minimumVersion, a2a.profiles[] without -legacy; mcp.revisions[], mcp.preferredVersion, mcp.minimumRevision, mcp.features[], mcp.serverMount.transports[]). RFC 0172 §B axes 11–12 are these facets.
§C. Legacy profiles gone at the cut
§C.1 a2a-0.3-legacy and mcp-2025-06-18-legacy do not exist in the v2 tree: the profiles[] item patterns lose the (-legacy)? alternative; the legacy code paths (a2a-integration.md §0.3 mapping, mcp-integration.md live-callback bridges) are absent from spec/v2/; the suite's dual-era A2A peer and MCP server lose their legacy halves; the four mcp-server-* legacy scenarios are deleted. The v1 sunset dates (2027-03-12, 2027-08-12) stand for the v1 tree through Phase 5. §C.2 The adopter inventories (RFC 0152 G1, 0153 G1) are closed as measured: one dual-era advertiser (openwop-app), no reference host; a third-party inventory is not obtainable and the RFCs say so. §C.3 The header-less card rule (a2a-integration.md:349) becomes unconditional: a v2 host serves the card of preferredVersion when no A2A-Version header is present.
§D. Negotiation is a protocol
§D.1 A version-negotiation exchange on either embedded protocol MUST be authenticated: the peer identity is the C.3 Subject of the caller (or the host's own outbound identity), and an unauthenticated negotiation MUST NOT lower the version below preferredVersion. §D.2 A host advertises minimumVersion (A2A) / minimumRevision (MCP); a negotiation that would land below the floor MUST fail closed (interop_version_unsupported, RFC 0152 §B's code, registered in C.4's errors.json) whether or not a policy permits explicit downgrade above the floor. §D.3 Every negotiation outcome — including the fail-closed one — emits negotiation.decided (new v2 event type: { protocol: a2a | mcp, peer: <origin digest>, requested, negotiated | null, outcome: accepted | downgraded | refused, reason }, content-free), replacing the authorization.decided carrier RFC 0152/0153 G7 recommended. §D.4 Refresh SLA (RFC 0147 R10): a host MUST re-evaluate its advertised versions[] / revisions[] against the upstream registry within an advertised refreshedAt window not exceeding 90 days; an advertisement older than its window is non-conformant. §D.5 The silent-downgrade invariants (a2a-version-no-silent-downgrade, mcp-version-no-silent-downgrade) are witnessed by the negotiation.decided event on the host's own event log — a normative surface — rather than only by the §22/§23 invoke seams; the seams stay in the C.1 seams profile for the wire-capture leg.
§E. MRTR and the durable-task projection
§E.1 mcp.mrtr.maxRounds (integer, 1–16, advertised) is the ceiling; the host MUST refuse a further input_required round beyond it (mcp_mrtr_rounds_exceeded, registered in errors.json); RFC 0153 G9 closes. The requestState MUSTs (mcp-integration.md:290–291) carry over unchanged. §E.2 RFC 0100 UQ4: auth-required stays in the persisted A2ATaskState enum for the reverse direction; the forward projection never emits it in v2 either — v2 has no auth interrupt kind, and inventing one here would be C.4's business; recorded as decided, not deferred (RFC 0152 G9 closes as "not in v2.0").
§F. The threat model
§F.1 SECURITY/threat-model-interop.md is written with the sibling template (§1–§8): downgrade, card/runtime drift, cross-tenant lookup through a peer, artifact leakage across the boundary, the anonymous end-user actor (RFC 0165 §B.6), and negotiation replay; its §5 invariants are the two silent-downgrade rows plus interop-negotiation-authenticated, interop-minimum-version-enforced, interop-peer-no-authority-escalation (already registered) — entering invariants.yaml with their tests in Phase 3 (RFC 0167 §C rule). RFC 0152 G8 and 0153 G8 close when the file exists.
Migration table
| Row | Kind | v1 | v2 | Codemod | Persisted data |
|---|---|---|---|---|---|
openwop.migration.C8.1 | remove | a2a-0.3-legacy / mcp-2025-06-18-legacy profiles and the (-legacy)? patterns | none | openwop.codemod.discovery-document-v2 (strips the ids and the legacy version when preferredVersion is not legacy; refuses when it is) | not-persisted |
openwop.migration.C8.2 | remove | supportedTransports[] | none — REST is the wire; A2A/MCP are facets | openwop.codemod.discovery-document-v2 | not-persisted |
openwop.migration.C8.3 | remove | capabilities.grpc block; grpc-transport.md in core | spec/v2/ext/grpc-transport/ (unwitnessable, adoption: none); block not advertisable | openwop.codemod.discovery-document-v2 (drops the block) | not-persisted |
openwop.migration.C8.4 | add | none | a2a.minimumVersion, mcp.minimumRevision, refreshedAt | — | not-persisted |
openwop.migration.C8.5 | add | authorization.decided as the recommended carrier | negotiation.decided event type | — (a new event; v1 logs carry none) | not-persisted |
openwop.migration.C8.6 | behavior | negotiation fail-closed only for authenticated requests; no floor; no SLA | authenticated exchange, floor, 90-day refresh SLA | — | not-persisted |
openwop.migration.C8.7 | add | "the host MUST bound the number of rounds (host policy)" | mcp.mrtr.maxRounds + mcp_mrtr_rounds_exceeded | — | not-persisted |
openwop.migration.C8.8 | behavior | auth-required never emitted forward; UQ4 open | decided: stays reverse-only; no v2 auth interrupt kind | — | unchanged (persisted A2ATaskState rows keep the enum member) |
Persisted-data disposition
| Store | v1 artifact | Disposition |
|---|---|---|
| Persisted A2A task state (openwop-app) | A2ATaskState.state incl. auth-required | unchanged |
| Event logs | no negotiation events under v1 | not-persisted (new type from the cut) |
| Discovery documents | legacy profile ids, supportedTransports, grpc block | not-persisted; the codemod rewrites a captured document |
| Suite fixtures (dual-era peer/server) | legacy halves | removed with suite 2.0.0 (C.1) |
Compatibility
breaking (v2). This PR changes no v1.x wire shape. The RFC 0152/0153 register additions copy rows that already exist in the spec documents' own gap tables (register drift, RFC 0174 §C); the grpc-transport.md:202 correction is editorial (RFC 0094 is Accepted).
Conformance
v2 scenarios (suite 2.0.0): no-transport-advertisement (unaided: supportedTransports and grpc absent), legacy-profiles-absent (unaided: no -legacy id validates), negotiation-decided-emitted (gated on a2a/mcp: the host's own event log carries negotiation.decided for a peer exchange driven through the seams profile), minimum-version-refused (gated: a peer offering below the floor is refused with interop_version_unsupported and the event says refused), negotiation-authenticated (gated: an unauthenticated exchange cannot lower the version), refresh-sla (unaided: refreshedAt within the window), mrtr-rounds-ceiling (gated on mcp: round maxRounds + 1 refused), threat-model-template (corpus, shared with C.6).
Falsifiability — one row per normative requirement
| Requirement | Observable | Who can cause the condition | Verdict |
|---|---|---|---|
§A.1 no grpc block; ext header declares class | discovery + corpus — openwop.requirement.0175.no-transport-advertisement | the suite, unaided; the corpus gate | witnessable — unaided |
§B.1 no supportedTransports | discovery — openwop.requirement.0175.no-transport-advertisement | the suite, unaided | witnessable — unaided |
§C.1 no -legacy id | discovery schema — openwop.requirement.0175.legacy-profiles-absent | the suite, unaided | witnessable — unaided |
| §D.1 unauthenticated exchange cannot downgrade | event log + peer capture — openwop.requirement.0175.negotiation-authenticated | the suite via the C.1 seams profile | seam-gated |
| §D.2 floor refused | interop_version_unsupported + event — openwop.requirement.0175.minimum-version-refused | the suite via the seams profile | seam-gated |
§D.3 negotiation.decided on every outcome | the host's event log — openwop.requirement.0175.negotiation-decided-emitted | the suite, gated on a2a/mcp (the exchange is driven through the seam, the event is read on the normative surface) | witnessable — gated |
| §D.4 refresh SLA | refreshedAt — openwop.requirement.0175.refresh-sla | the suite, unaided | witnessable — unaided |
| §E.1 MRTR ceiling | refusal at maxRounds + 1 — openwop.requirement.0175.mrtr-rounds-ceiling | the suite's fake MCP server, gated | witnessable — gated |
| §F.1 threat model present with the template | corpus gate | the corpus gate | witnessable — unaided (corpus) |
Adversarial review
- Demoting gRPC to
unwitnessablewhile agrpcblock exists in the schema contradicts RFC 0169 §A.1. Disposition: the block is removed from the v2 root (row C8.3); a demoted extension is documented, not advertised. - Deleting
supportedTransportsremoves the one field that let a client learn REST is served. Disposition: REST is the wire by definition (grpc-transport.md:3already says "REST + SSE remains the REQUIRED wire surface"); a field that can only sayrestsays nothing. - Removing the
-legacypattern before 2027-03-12 / 2027-08-12 breaks the v1 promise. Disposition: it does not — the v1 tree keeps the profiles and the dates through Phase 5; onlyspec/v2/lacks them, and a v2 host that still speaks A2A 0.3 does so as a non-advertised private behavior, which is what "removed from the corpus" means. - Authenticated negotiation is unwitnessable without the invoke seams. Disposition: the exchange is driven through the C.1 seams profile, but the record (
negotiation.decided) lands on the host's own event log, a normative surface; §D.3's leg iswitnessable — gated, §D.1/§D.2's peer-capture legs stay honestlyseam-gated. - A 90-day refresh SLA is arbitrary. Disposition: it is the shortest window both upstreams' own lifecycle policies exceed (A2A/MCP twelve-month deprecation minimums); recorded as G1 for re-measurement at the first refresh.
- RFC 0100 UQ4 is "decided" by not deciding. Disposition: the decision is that v2.0 has no
authinterrupt kind; a future additive RFC may add one; the enum member stays for reverse-direction fidelity; RFC 0152 G9 closes as "not in v2.0", which is a decision with a date. - The RFC 0152/0153 registers lack the G7–G9 rows the charter cites. Disposition: copied into the registers in this PR with the spec-doc rows as their sources (RFC 0174 §C register-drift rule); the spec docs' tables are retired into
gaps.jsonin Phase 3 (RFC 0174 §E.3).
Alternatives considered
- Generate the proto now and keep gRPC in core. Rejected: no client, no advertiser, and the declaration file (C.2) does not exist until Phase 3; the door is named in §A.1.
- Keep
supportedTransportsas["rest"]. Rejected: Axiom 2 — a field with one legal value is an alias for its own absence. - Keep the legacy profiles through the v1 dates in
spec/v2/. Rejected: the dates are v1's; v2 starts without the code paths, as both RFCs already say. - Do nothing. Rejected: two Stable documents cite a threat model that does not exist.
Unresolved questions
- Whether
negotiation.decidedshould carry the peer origin in clear or as a digest. Recommended: digest (content-free audit, RFC 0128 purpose label rides separately).
Implementation notes (non-normative)
openwop-app advertises both eras of both protocols today and drops the legacy halves in Phase 4; MyndHyve advertises neither A2A nor MCP versions and has no obligation under §D. The suite's fake peer and server (a2a-fake-peer.ts, mcp-fake-server.ts) are the negotiation-decided-emitted drivers.
Acceptance criteria
- [x]
Draft → Active: RFC text; rowsC8.1–C8.8; the discovery codemod extended with fixtures; RFC 0152/0153 register rows G7–G9 added;grpc-transport.md:202corrected; deprecation rowsupported-transports-field; ledger row; adversarial review. (This PR.) - [x]
Active → Accepted(Phase 3):spec/v2/ext/grpc-transport/; the facets and patterns inschemas/v2/capabilities.schema.json;spec/v2/core/interop.md;negotiation.decidedin the v2 event registry;SECURITY/threat-model-interop.md; the eight scenarios in suite 2.0.0. — evidence: the corpus half is present and green.reason:the openwop-app clause is dropped from the tick and recorded here instead, because three of its four ids cannot be witnessed on that host by construction. openwop-app advertises neither thea2anor themcpfacet, so:refresh-slarecordsinapplicable— "host advertises neither a2a nor mcp — no refreshedAt to check";legacy-profiles-absentrecordsinapplicablefor the same reason on the leg that needs a facet; andnegotiation-decided-emittedhas no row at all. Onlyno-transport-advertisementisexecuted-passthere (and on all three bundles). MyndHyve records the identicalinapplicablereasons — it advertises neither facet either. All eight §D/§E ids areexecuted-passtier-1 on the reference host, which advertises both. The remedy is a host advertisinga2a/mcpin production, not a change to this RFC.
Evidence correction (2026-09-23) — one acceptance row measured the harness, and §D.1's clause is unexercised
Status is unchanged and nothing here is flipped. This section records what one of the eight executed-pass rows cited in the acceptance tick above actually measured, because the row measured the test harness rather than the host, and finding that exposed a second, larger fact about §D.1.
1. The vacuous row. openwop.requirement.0175.negotiation-authenticated.mcp is executed-pass in the committed reference-host bundle (suite 2.35.0, build commit:8477d75f, witnessSha256 5562d7c57609…) and measured nothing about the host's negotiation decision. v2-negotiation-authenticated.test.ts establishes its condition by constructing a single-era McpFakeServer(['2025-06-18']) on an ephemeral port and handing the host hostFacingEndpoint(). Since suite 2.33.0 that method returns OPENWOP_MCP_FAKE_SERVER_URL — the operator's public front, which forwards to the pinned port the shared, setup-level fake server owns, and that server is dual-era and speaks the preferred revision. host-sample-test-seams.md §23 defines no request field that makes a server offer only a lower revision (requestVersion overrides what the host asks for, which is §D.2's floor case, and the leg does not pass it), so no lower offer ever reached the host: it discovered 2026-07-28, negotiated at it, and the leg asserted negotiatedVersion === preferredVersion — a property of the harness. The row's own assertion count records the moment it went hollow: 4 in every pre-front cut of this host (2.4.0, 2.4.1, the 2.4.2 acceptance witness, 2.4.5, 2.24.0, 2.25.0) and 3 in every cut since the fronts landed in suite 2.33.0 (2.33.1, 2.34.0, 2.35.0). The missing assertion is the wire leg, which iterates the scenario-owned server's invocations() — empty, because the host never reached that instance. Its three siblings (minimum-version-refused, minimum-version-refused.mcp, negotiation-authenticated) drove the host's decision genuinely through peerOffersOnly / requestVersion, and each of them refused as the RFC requires — but their wire half contributes zero assertions in the committed bundle too, for two different reasons. The MCP sibling lost its one wire assertion (8 → 7) when the front took the host away from the scenario's own server; the two A2A legs never had one, because the host refuses before it makes any non-GET call and the leg inspects only non-GET calls (the Agent Card GET is filtered out). A wire half that can only ever iterate an empty array is not a check.
2. What that hid: §D.1's clause has never run. The host refuses an exchange as unauthenticated only when the negotiated version would land below preferredVersion and at or above the floor. The only host that has ever witnessed these ids advertises a2a.preferredVersion === a2a.minimumVersion === 1.0 and mcp.preferredVersion === mcp.minimumRevision === 2026-07-28, so no such version exists on it and every lower offer refuses below-floor instead. Every reference-host cut in the file's history was checked (17): the nine that advertise the facets all carry that posture, the eight before them advertise neither facet and record 0 assertions on these ids, and the two production bundles record them inapplicable or omit them. No committed bundle has ever exercised §D.1's authentication clause, fronted or loopback. What the three genuine rows measured is §D.2's floor rule, twice.
3. The defect is the suite's, not the host's, and what now measures it. No host emitted anything it should not have; the reference host's decide() implements §D.1 as written, and the corpus cannot convict or acquit it on this evidence. The harness fix has since landed (openwop#1520, suite 2.37.0): conformance/src/lib/front-mux.ts nonce-paths each scenario-owned fake behind the one operator front, so the host now reaches this server — the one that offers only 2025-06-18, which is where the lower offer comes from, §23 still having no knob for it — and both legs now record blocked instead of a pass when the seam answers a success and the suite's own peer received no request. This PR touches no scenario; it records what the bundle already in the tree measured, which no code change can retract.
4. What the remaining evidence supports, stated plainly. §A, §B, §C, §D.2, §D.3, §D.4, §E.1 and §F.1 keep genuine tier-1 rows, so the acceptance is not withdrawn. §D.1 is, as of this entry, unwitnessed — not failed, not refuted, unwitnessed — and neither row may be cited as evidence that any host enforces it. Witnessing it needs a host whose advertised floor sits below its preferred version (or a suite lever producing that posture), which no host in INTEROP-MATRIX.md has today; recorded as openwop.gap.0175.6. The re-cut of the reference bundle owed after the harness fix is openwop.gap.0175.5, and the matrix row discloses that the recorded pass predates the fix.
Verified for this entry by reading the three committed bundles and all 17 reference-host cuts in the file's history, the scenario source, §22/§23's seam contract, resolvePublicFront + conformance/src/setup.ts, the cut script that sets the fronts, and the host's decide() at the exact build the bundle names. Not re-measured: the fronted path cannot be reproduced without opening public ingress, so which branch the 2.35.0 run took is read from the routing and the assertion arithmetic, not from a fresh run.
References
- RFC 0167 §A (Axioms 1–2), §C, §E.1 axes 11–12, §E.3; RFC 0094 §H; RFC 0100 UQ4; RFC 0152 §B, G1, G3, G7–G9, R1; RFC 0153 §A/§C, G1, G3, G7–G9, R1; RFC 0147 G11, G12, R10; RFC 0169 §A.1; RFC 0171 §A/§B; RFC 0172 §B; RFC 0165 §B.6.
spec/v1/grpc-transport.md,api/grpc/openwop.proto;spec/v1/a2a-integration.md§Discovery, §B, §Open spec gaps;spec/v1/mcp-integration.md§B, §C.1, §Open spec gaps;spec/v1/host-sample-test-seams.md§22–§23;spec/v1/capabilities.md:162;schemas/capabilities.schema.json(supportedTransports,a2a,mcp,grpc);conformance/src/scenarios/grpc-transport.test.ts;SECURITY/threat-model-*.md(the template).