1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
name: CI
on:
push:
branches:
pull_request:
branches:
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# Override .cargo/config.toml target-cpu=native to avoid SIGILL on CI runners
RUSTFLAGS: ""
# Pin matches .github/workflows/quality-badges.yml so badge and gate stay aligned (Phase 75 D-09).
PMAT_VERSION: "3.15.0"
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
# Remove large packages we don't need
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy, llvm-tools-preview
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install cargo-llvm-cov
run: |
if ! command -v cargo-llvm-cov &> /dev/null; then
cargo install cargo-llvm-cov
fi
- name: Format check
run: cargo fmt --all -- --check
- name: Clippy
run: |
cargo clippy --all-targets --all-features -- \
-D warnings \
-A clippy::module_name_repetitions \
-A clippy::must_use_candidate \
-A clippy::missing_errors_doc \
-A clippy::missing_const_for_fn \
-A clippy::return_self_not_must_use \
-A clippy::missing_fields_in_debug \
-A clippy::uninlined_format_args \
-A clippy::if_not_else \
-A clippy::result_large_err \
-A clippy::multiple_crate_versions \
-A clippy::implicit_hasher \
-A clippy::unused_async \
-A clippy::cast_lossless \
-A clippy::redundant_clone \
-A clippy::redundant_closure_for_method_calls \
-A clippy::significant_drop_tightening \
-A clippy::missing_panics_doc \
-A clippy::cast_possible_truncation \
-A clippy::cast_precision_loss \
-A clippy::option_if_let_else \
-A clippy::derive_partial_eq_without_eq \
-A clippy::redundant_else \
-A clippy::match_same_arms
- name: Build
run: cargo build --all-features --verbose
- name: Run tests
run: cargo test --all-features --verbose -- --test-threads=1
# NOTE: pmcp-package (workspace-EXCLUDED, own [workspace] table) is gated in
# the `quality-gate` job via `make quality-gate` -> `pmcp-package-gate`
# (fmt/clippy/test --manifest-path), which is the single source of truth and
# is reused by the local `make quality-gate`. It is intentionally NOT
# re-run here to avoid a duplicate from-scratch compile of the crate's tree.
- name: Run doctests
run: cargo test --doc --all-features --verbose
- name: Check examples
run: |
# Build every registered example with its declared required-features.
# Reading required-features from cargo metadata keeps this in sync with
# Cargo.toml automatically, so future renames or feature changes don't
# require workflow edits.
cargo metadata --no-deps --format-version 1 \
| python3 -c '
import json, sys
meta = json.load(sys.stdin)
pkg = next(p for p in meta["packages"] if p["name"] == "pmcp")
for t in pkg["targets"]:
if "example" in t["kind"]:
feats = ",".join(t.get("required-features") or [])
print(f"{t[\"name\"]}|{feats}")
' | while IFS='|' read -r name feats; do
if [[ -n "$feats" ]]; then
echo "Checking example: $name (features: $feats)"
cargo check --example "$name" --features "$feats"
else
echo "Checking example: $name (no required features)"
cargo check --example "$name" --features http
fi
done
- name: Clean up before coverage
run: |
echo "Disk usage before cleanup:"
df -h
echo "Cleaning cargo build artifacts..."
cargo clean
echo "Disk usage after cleanup:"
df -h
- name: Run coverage
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
feature-flags:
name: Feature Flag Verification
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-feature-flags-${{ hashFiles('**/Cargo.lock') }}
- name: Verify feature flag combinations
run: make test-feature-flags
quality-gate:
name: Quality Gate
runs-on: ubuntu-latest
# Phase 75 Wave 5 / D-07: when adding PMAT here, pin via `cargo install pmat --version =3.15.0 --locked`
# The pin matches .github/workflows/quality-badges.yml so badge and gate
# semantics stay aligned. See .planning/phases/75-fix-pmat-issues/
# 75-W0-SPIKE-RESULTS.md for the empirical justification (D-09).
steps:
- uses: actions/checkout@v7
- name: Free Disk Space
uses: jlumbroso/free-disk-space@main
with:
# Remove large packages we don't need
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-quality-${{ hashFiles('**/Cargo.lock') }}
- name: Install quality tools
run: |
if ! command -v cargo-llvm-cov &> /dev/null; then
cargo install cargo-llvm-cov
fi
if ! command -v cargo-nextest &> /dev/null; then
# Why: cargo-nextest 0.9.133+ embeds a `locked-tripwire` build-time
# compile_error! that aborts unless `--locked` is passed. Without
# this flag CI fails with "Nextest does not support being installed
# without --locked." See https://nexte.st/docs/installation/from-source/
cargo install --locked cargo-nextest
fi
# Force install latest cargo-audit to support CVSS 4.0
cargo install cargo-audit --force
- name: Install cargo-deny (make quality-gate chains purity-check, which needs it)
uses: taiki-e/install-action@v2.85.10
with:
# Pinned: the Makefile purity-check Layer 2 invocation uses cargo-deny
# 0.18.3's CLI ordering (`check --config deny.toml bans`). Newer cargo-deny
# moved `--config` to a global flag (before `check`) and rejects the old
# form with a usage error, silently breaking the gate. Pin like PMAT_VERSION.
tool: cargo-deny@0.18.3
- name: Check disk space before quality gate
run: df -h
- name: Check rustdoc zero-warnings
run: make doc-check
- name: Run quality gate
run: make quality-gate
- name: Install PMAT (cache-aware; reused across runs via ~/.cargo/bin)
run: |
if ! command -v pmat &>/dev/null || ! pmat --version | grep -qE "^pmat ${PMAT_VERSION}$"; then
cargo install pmat --version "=${PMAT_VERSION}" --locked
fi
- name: Run PMAT quality gate (complexity only — see CONTEXT.md D-01 / D-11-B)
run: pmat quality-gate --fail-on-violation --checks complexity
# Phase 109 Plan 08 (D-18): contract-first compliance for the team-servers
# bindings. `comply-ci` is FAIL-CLOSED (no `command -v pmat` guard) so a CI
# without pmat fails rather than passing vacuously; it runs the mandated
# `pmat comply check --path .` report and makes team-servers binding drift
# gate-blocking via a deterministic source-resolution check. Runs here AFTER
# PMAT is installed. `comply-negative` proves a broken binding is rejected.
- name: Run team-servers contract compliance (fail-closed)
run: make comply-ci
- name: Assert broken-binding rejection (negative compliance test)
run: make comply-negative
- name: Check disk space after quality gate
run: df -h
benchmarks:
name: Benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Run benchmarks
run: cargo bench --all-features --no-run
msrv:
name: Minimum Supported Rust Version (1.91)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust 1.91
uses: dtolnay/rust-toolchain@1.91
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.lock') }}
- name: Check MSRV
run: cargo check --all-features
# Phase 91 (WBRT-04) purity gate — fail-closed, per-crate, per-feature.
# Proves the Excel reader (umya/quick-xml/calamine) and the JS stack
# (swc_*/pmcp-code-mode) can never enter the reader-free served trees.
# `make purity-check` already loops crate × feature, fails closed, and runs the
# crate-local cargo-deny [bans] (Layer 2) — so cargo-deny must be installed here.
purity-check:
name: Purity Gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v6
with:
# No ~/.cargo/bin/ here — taiki-e/install-action fetches a prebuilt
# cargo-deny in seconds and caches it itself; restore-keys keeps the
# registry warm across Cargo.lock bumps.
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-purity-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-purity-
- name: Install cargo-deny (Layer 2 backstop)
uses: taiki-e/install-action@v2.85.10
with:
# Pinned to match the Makefile purity-check Layer 2 CLI form (see the
# quality-gate job's install step for the full rationale). Newer cargo-deny
# relocated `--config` and breaks `check --config deny.toml bans`.
tool: cargo-deny@0.18.3
- name: Run purity gate (per-crate, per-feature, fail-closed; Layers 1+2)
run: make purity-check
# Phase 117 (SMPL-01 / D-02) MCP-v1 severability gate.
#
# Proves that the v1 compatibility layer is CLEANLY SEVERABLE: the crate still
# builds, warning-free, with `v1-compat` off and everything else on. This job is
# wired into the `gate` aggregate below in all THREE required places (`needs:`,
# the `env:` binding, and the `if` chain), so a broken severance build is a red
# required check rather than a green-looking advisory one. It must never be made
# advisory-only, and it must not be folded into the `feature-flags` job: that job
# is absent from `gate.needs`, and `make test-feature-flags` (Makefile:310-341)
# checks `pmcp-tasks` combinations only — it touches zero root `pmcp` features,
# so its name is misleading.
#
# The build command carries FOUR fences. Each closes a specific false-green, and
# none of them is redundant — read this before "simplifying" the line:
#
# 1. `-p pmcp` — `pmcp` is both the root package AND the workspace root, and 20+
# members depend on it, several with `full` (which includes `v1-compat`). A
# workspace-wide build would unify `v1-compat` back on and prove nothing.
# `-p` restricts the build to that package plus its own dependency closure.
#
# 2. `--no-default-features` — `v1-compat` is a member of `default`, so without
# this it simply arrives via `default` and the build is vacuous.
#
# 3. `--features full-v2` (NOT a bare `--no-default-features`) — `default` also
# carries `logging`, and a bare `--no-default-features` strips `http` and
# `streamable-http` as well. That would "prove" severance by never compiling
# the transport at all. `full-v2` is `full` minus exactly `v1-compat`, and
# tests/v1_severability_tripwire.rs derives both lists from Cargo.toml so the
# two enumerated lists cannot silently drift apart.
#
# 4. `RUSTFLAGS="-D warnings"` — load-bearing. `make lint` passes `-D clippy::all`
# but NOT a bare `-D warnings`, so a helper left stranded after the v1 cut
# would emit a `dead_code` rustc lint and the build would still pass green.
#
# Two flags that must NEVER be added here:
#
# * `--all-features` can NEVER prove severance. Cargo features are additive, so
# it enables `full-v2` AND `v1-compat` simultaneously. This is why the `msrv`
# job (`cargo check --all-features`), the coverage step and `make build` are
# all incapable of substituting for this job, however similar they look.
#
# * `--all-targets` (likewise `--tests`, `--examples`) must not be added. The
# severance build is deliberately lib-only — `pmcp` declares no `[[bin]]`, so
# `cargo build` covers exactly the shipped library. Adding targets would drag
# in ~29 struct-literal sites in tests/examples for ZERO additional proof
# about the library that consumers actually link.
#
# The cache key is deliberately its own (`-cargo-severance-`): every other job
# builds a different feature set, and sharing a `target/` with an
# `--all-features` build makes timings confusing while cargo rebuilds anyway.
v1-severance:
name: v1 Severance Gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-severance-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-severance-
- name: Build pmcp with v1 severed (SMPL-01 severance proof)
run: RUSTFLAGS="-D warnings" cargo build -p pmcp --no-default-features --features full-v2
# The RUNTIME half of the same claim (SMPL-02). The build step above proves
# what does not EXIST in the severed library; this step proves what the
# severed server ANSWERS, by running the three proof files ON that build —
# and fails if any of them ran ZERO tests.
#
# Deliberately a SEPARATE step, not extra flags on the step above: the
# rationale block forbids `--all-targets` on the lib-only build precisely so
# that proof stays about the library consumers link. This step is the
# "separate job" that block tells you to add instead.
#
# The commands live in the script, not inline, so `tests/ci_severance_gate_wiring.rs`
# can pin them as DATA — including the zero-count guard, which cannot live
# inside a `#![cfg]`-selected test file (a test in a conditionally-compiled
# file can never police whether that file was compiled).
- name: Runtime severance proofs + severed test build (SMPL-02)
run: ./scripts/run-severance-proofs.sh
# Phase 108 (D-13) pmcp-agent target matrix. Two independent guarantees:
# 1. The LOOP + SEAMS + config path compiles for wasm32 under DEFAULT features
# (no url-connector/reqwest/StreamableHttpTransport). The agent-as-server
# adapter + SamplingSource are native-only (they ride native-only pmcp APIs)
# and are `cfg`-excluded on wasm32, so this gate proves target-cleanliness of
# the loop/seam path, NOT adapter-on-wasm.
# 2. The feature-gated HTTP sources (openai-compat/anthropic) AND the native-only
# UrlConnectorClientFactory (url-connector) compile on native.
pmcp-agent-targets:
name: pmcp-agent target matrix (wasm32 default + native features)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust (+ wasm32 target)
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-pmcp-agent-targets-${{ hashFiles('**/Cargo.lock') }}
- name: Build pmcp-agent for wasm32 (DEFAULT features — loop + seams + config)
run: |
rustup target add wasm32-unknown-unknown
cargo build -p pmcp-agent --target wasm32-unknown-unknown
- name: Build pmcp-agent native (HTTP sources + URL connector)
run: cargo build -p pmcp-agent --features openai-compat,anthropic,url-connector
# Phase 116 (D-06) wasm32 build fence for the UNGATED OAuth tier.
#
# `src/shared/oauth_validation.rs` (RFC 9207 `iss` + CSRF `state` decision
# table, SEP-2351 discovery-URL derivation) and `src/shared/credential_store.rs`
# (SEP-2352's `(issuer, account, server)` key, the document format, the schema
# 1 -> 2 migration and the CredentialStore/CredentialStoreAdmin traits) are
# ungated ON PURPOSE: a Cloudflare Workers or AWS Lambda platform must be able
# to reach them without the `oauth` feature, which pulls in native-only
# dependencies that do not build for wasm32. That property is what lets a
# platform redirect handler and the CLI share ONE implementation instead of
# two that drift.
#
# Nothing else in CI builds for wasm32, so without this job a future
# contributor could add a native-only dependency to either module and break
# the property with a green build. The fence is on the EXIT CODE, not the
# warning count: `make wasm-build` currently emits ~92 pre-existing dead-code
# warnings that are not this gate's business.
#
# The deliberately GATED counterpart is `src/shared/credential_file.rs` (the
# filesystem-backed CredentialStore). Its native-only imports are correct and
# it is excluded from the wasm build by feature/target gating — do not "fix"
# the split by ungating it or by gating the two modules above.
wasm32-purity:
name: wasm32 build fence (ungated OAuth tier, Phase 116 D-06)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install Rust (+ wasm32 target)
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-wasm32-purity-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-wasm32-purity-
# Explicit, even though the toolchain action already requested the target:
# RESEARCH assumption A5 records that the target's availability on a runner
# was never probed, so this must not be assumed. `rustup target add` is a
# no-op when the target is already installed.
- name: Add wasm32 target
run: rustup target add wasm32-unknown-unknown
# The Makefile target, NOT a bespoke cargo line, so the fence and the
# command a developer runs locally can never diverge.
- name: Build for wasm32 (make wasm-build)
run: make wasm-build
# Phase 118 (CONF-01 / D-21) official MCP conformance suite gate.
#
# Runs `@modelcontextprotocol/conformance` — the OFFICIAL, external referee —
# against ONE pmcp server process at BOTH spec revisions, and blocks merge on
# what genuinely holds. This job is wired into the `gate` aggregate below in
# all FOUR required places (`needs:`, the `env:` binding, the `if` chain and
# the failure echo), so a broken conformance surface is a red required check
# rather than a green-looking advisory one. It must never be made advisory.
#
# WHAT THIS GATE CLAIMS, stated up front so the CI log cannot be misread.
# Neither requirement set exits 0 today. Measured on this pin:
# --requirements 2025-11-25 -> 51 passed, 15 failed, exit 1
# --requirements 2026-07-28 -> 124 passed, 54 failed, exit 1
# Nine structural SDK gaps (G-1..G-9) explain those failures and are recorded,
# with source citations, in
# `.planning/phases/118-conformance-against-the-official-suite/118-CONFORMANCE-GAPS.md`.
# The answer to that measurement is NOT an allowlist (fence 5). The gate is
# SCOPED to surfaces that genuinely pass — the MRTR surface plus the executed
# check floors — and the script PRINTS the declared non-conformance on every
# run, so the claim this repo makes is exactly the claim it can defend.
#
# THE FENCES, numbered, each naming the false green it closes:
#
# 1. `actions/setup-node@v4` with `node-version: 22` — the FIRST `setup-node`
# in this repository, so there is no house style to inherit. The suite
# imports `globSync` from `node:fs` at MODULE SCOPE (Node >= 22) and the
# published package declares NO `engines` of its own, so a Node-20 runner
# would sail through a naive install and then die at load with
# `SyntaxError: The requested module 'fs' does not provide an export named
# 'globSync'` — several minutes in, from a stack trace naming neither Node
# nor this repo. `conformance/.npmrc` sets `engine-strict=true`, which
# makes the INSTALL itself fail on Node 20 (measured: `EBADENGINE`, exit 1,
# no `node_modules`). Pinning the runner's Node is the FIRST line of
# defence and the `.npmrc` is the SECOND; neither replaces the other,
# because a runner can arrive with a pre-populated `node_modules` tree.
#
# 2. `cache: 'npm'` keyed on `conformance/package-lock.json` — the lockfile is
# COMMITTED and exact, so the cache key is exact too. This removes a
# per-run network fetch and the flake that comes with it. It does NOT
# weaken the pin: `npm ci` still validates every integrity hash in the
# lockfile, and because the key IS the lockfile a cache hit can never
# serve a different dependency tree than the one committed.
#
# 3. `npm ci --ignore-scripts`, which lives INSIDE the script and not inline
# here. The committed lockfile is authoritative — `npm install` resolves at
# run time and can silently move the referee. `--ignore-scripts` is
# load-bearing because npm runs lifecycle scripts for the WHOLE dependency
# tree, not just the package named, so confirming the direct package's
# `postinstall` is empty is necessary and nowhere near sufficient.
#
# 4. THE COMMANDS LIVE IN `scripts/run-conformance-suite.sh`, NOT INLINE, so
# `tests/ci_conformance_gate_wiring.rs` can pin them AS DATA — the vacuity
# guards, the zero-check gate and the two check floors included. None of
# those can live inside a `#![cfg]`-selected test file: a test in a
# conditionally-compiled file can never police whether that file was
# compiled.
#
# 5. `--requirements`, never `--spec-version`, and NO `--expected-failures`.
# D-03 admits no known-fail allowlist in any shape, and D-21 makes that
# prohibition doubly binding now that the failures are known and named.
# D-14 fixes the gate's boundary at the suite's SCORED set. If a future
# edit finds itself writing a list of tolerated failures, that is the
# forbidden shape — close the SDK gap or narrow the surface instead.
#
# 6. ONE server process for BOTH runs (D-06), started from the BUILT BINARY
# and torn down by process GROUP. Two processes would prove "pmcp can serve
# v1" and "pmcp can serve v2" as separate facts; the milestone headline is
# that ONE binary does both, and only one process can demonstrate that (or
# expose cross-era state bleed). `cargo run` is a PARENT process, so
# trapping its pid can orphan the real server holding the port.
#
# 7. `if: always()` on the artifact upload — NOT `if: failure()`, which is the
# shape `.github/workflows/fuzz.yml` uses and the WRONG one to copy here.
# D-14 needs the not-scored `extension`/`pending` results reviewable on
# GREEN runs, which is precisely when a failure-conditioned upload skips.
# Those results are the input to the next re-pin decision.
#
# 8. `timeout-minutes: 75`, which is DELIBERATELY ABOVE the script's own
# `TOTAL_BUDGET_SECONDS=3600` (60 minutes). The script's budget is the
# DIAGNOSIS — it is checked at six named checkpoints and fails naming the
# step that hung; this job-level value is only the BACKSTOP for a hang the
# script's own checkpoints cannot reach. Setting the job timeout BELOW the
# script budget would invert that: the platform would cancel the job first
# and replace an actionable message with an opaque cancellation. Note that
# `v1-severance` above carries NO `timeout-minutes` at all — that is the
# template this job otherwise follows, and this divergence is deliberate,
# not an oversight: `v1-severance` runs a bounded build, while this job
# installs from a network registry and drives a live server over a port.
#
# 9. THIS JOB IS IN `gate.needs` AND THEREFORE BLOCKS MERGE. Measured
# precedent for why that must be checked rather than assumed: on PR #319
# `security_audit` and `workspace-test` were both RED while `gate` was
# GREEN, because neither is listed in `gate.needs`. Adding a job to this
# file does NOT make it block. Never move this job out of `gate.needs`,
# and never mark it advisory — no error-tolerating step attribute and no
# status-masking shell suffix. Those two spellings are deliberately not
# written out here: `ci.yml` is grepped for them literally, and prose
# forbidding a token must not be what makes that grep fire.
conformance-suite:
name: Official MCP Conformance Suite (CONF-01)
runs-on: ubuntu-latest
# Fence 8: above the script's own 3600 s budget, so the script's diagnosis
# wins the race and this is only the backstop.
timeout-minutes: 75
env:
# A fixed NON-PRODUCTION value. The target example reads this variable for
# PRESENCE only (examples/s54_v2_dual_conformance.rs:1403) to mint MRTR
# `requestState` tokens that live for the duration of one CI run against a
# loopback server. It protects nothing outside this job, it is not a
# credential for any system, and it must never be reused anywhere real.
# The value is never echoed and never placed on a command line.
PMCP_REQUEST_STATE_KEY: deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef
steps:
- uses: actions/checkout@v7
# Fences 1 and 2.
- name: Install Node 22 (suite requires >= 22; see fence 1)
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: 'conformance/package-lock.json'
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-conformance-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-conformance-
# Fences 3-6 all live inside the script, as DATA the wiring test pins.
- name: Run the official conformance suite at both revisions (CONF-01)
run: ./scripts/run-conformance-suite.sh
# Fence 7: `always()`, so the not-scored results stay reviewable on GREEN
# runs — which is exactly when `if: failure()` would skip.
- name: Upload conformance results
if: always()
uses: actions/upload-artifact@v7
with:
name: conformance-results-${{ github.run_id }}-${{ github.run_attempt }}
path: target/conformance-results/
# Phase 118 (CONF-02 / CONF-03) era-comparison matrix gate.
#
# Wired into the `gate` aggregate below in all FOUR required places, exactly as
# `conformance-suite` above.
#
# THE FENCES, numbered:
#
# 1. WHY THIS JOB EXISTS AT ALL: **no CI job runs
# `crates/pmcp-team-servers/tests/` today.** Measured, not assumed — three
# independent paths all miss it. `ci.yml`'s `test` job is scoped to the
# root `pmcp` package; `org-gate-checks.yml`'s `workspace-test` runs
# `--lib --bins`, which EXCLUDES `tests/`, and is itself absent from
# `gate.needs`; and `make quality-gate` never reaches that crate. The
# Phase-109 team-servers harness, the 33-case v1 fixture corpus, the era
# matrix and the baseline schema gate therefore execute only when a human
# types the command. Adding an era matrix to a harness nothing executes
# gates nothing — that is RESEARCH Pitfall 4, and the same shape Phase 117
# found for the severance proofs.
#
# 2. WHY THIS IS NOT WIRED BY WIDENING `workspace-test`: that job's absence
# from `gate.needs` is an explicitly DEFERRED item with its own scope, and
# D-15 keeps it deferred by giving CONF-02 a gate it OWNS. Promoting
# `workspace-test` here would silently pull an unrelated, unreviewed job
# into the required check.
#
# 3. THE DEV-DEPENDENCY-FREE BUILD FENCES AND THE NONZERO-TEST-COUNT GUARDS
# LIVE IN THE SCRIPT, outside the compilation unit. A `cfg!`-based guard
# written INSIDE a `#![cfg]`-selected file expands to `!false` — it cannot
# fail on any input, and on the build where it would be false the file does
# not compile, so the test does not exist to run. A test inside a
# conditionally-compiled file can never police whether that file was
# compiled, so the guard has to live outside it, which is the script.
#
# 4. `era_matrix` and `era_baseline` need `--features http`, which is NOT in
# this crate's default set. Omitting it is SILENT, not loud — measured:
# cargo test -p pmcp-team-servers --test era_matrix
# -> running 0 tests ... exit 0
# cargo test -p pmcp-team-servers --features http --test era_matrix
# -> running 4 tests ... exit 0
# The zero-count guard in the script is the only thing that catches it, and
# `MATRIX_TESTS` carries each target's flags AS DATA rather than leaving
# them to a caller's memory.
#
# 5. `timeout-minutes: 45`. Same PURPOSE as `conformance-suite` fence 8 — turn
# a hang into a bounded failure — but with an honest difference that must
# not be papered over: `scripts/run-era-matrix.sh` declares NO internal
# wall-clock budget of its own, so unlike the conformance job there is no
# script diagnosis for this value to sit above. Here the job timeout is the
# SOLE backstop. 45 minutes is well above the observed cost (two full
# `cargo build` fences over the team-servers tree plus three test targets),
# so it fires only on a genuine hang. If a script-level budget is ever
# added, raise this value above it so the script's diagnosis wins.
era-matrix:
name: Era Matrix (CONF-02 / CONF-03)
runs-on: ubuntu-latest
# Fence 5: the SOLE backstop — this script declares no budget of its own.
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-era-matrix-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-era-matrix-
# Fences 3 and 4 live inside the script, as DATA the wiring test pins.
- name: Run the era matrix on a dev-dependency-free build (CONF-02 / CONF-03)
run: ./scripts/run-era-matrix.sh
# Unified gate — single required check for org ruleset.
# Reports as "gate" to match the org ruleset's required status check.
gate:
runs-on: ubuntu-latest
needs:
if: always()
steps:
- name: Evaluate required checks
env:
TEST_RESULT: ${{ needs.test.result }}
QG_RESULT: ${{ needs.quality-gate.result }}
PURITY_RESULT: ${{ needs.purity-check.result }}
AGENT_TARGETS_RESULT: ${{ needs.pmcp-agent-targets.result }}
WASM32_RESULT: ${{ needs.wasm32-purity.result }}
SEVERANCE_RESULT: ${{ needs.v1-severance.result }}
CONFORMANCE_RESULT: ${{ needs.conformance-suite.result }}
ERA_MATRIX_RESULT: ${{ needs.era-matrix.result }}
run: |
if [[ "$TEST_RESULT" != "success" ]] || \
[[ "$QG_RESULT" != "success" ]] || \
[[ "$PURITY_RESULT" != "success" ]] || \
[[ "$AGENT_TARGETS_RESULT" != "success" ]] || \
[[ "$WASM32_RESULT" != "success" ]] || \
[[ "$SEVERANCE_RESULT" != "success" ]] || \
[[ "$CONFORMANCE_RESULT" != "success" ]] || \
[[ "$ERA_MATRIX_RESULT" != "success" ]]; then
echo "Required checks failed: test=$TEST_RESULT, quality-gate=$QG_RESULT, purity-check=$PURITY_RESULT, pmcp-agent-targets=$AGENT_TARGETS_RESULT, wasm32-purity=$WASM32_RESULT, v1-severance=$SEVERANCE_RESULT, conformance-suite=$CONFORMANCE_RESULT, era-matrix=$ERA_MATRIX_RESULT"
exit 1
fi
echo "All required checks passed."