Skip to content

D5-S14 — Mcp: the frozen catalogue and its startup checks - #220

Merged
The-Running-Dev merged 2 commits into
mainfrom
slice/S14
Sep 7, 2026
Merged

The-Running-Dev merged 2 commits into
mainfrom
slice/S14

Conversation

@The-Running-Dev

@The-Running-Dev The-Running-Dev commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

Implements D5-S14: a product can offer tools to an AI client from two independent sources — a
manifest it ships and its own code — with neither privileged over the other. Nothing is offered to
anyone until someone explicitly says so, and a tool that could ask for a password never gets as far
as running.

Closes #182

  • New module SubZeroDev.Platform.Mcp:
    • ToolName, ToolProducerName, ToolDefinition, ToolRegistration, IToolProducer,
      IToolCatalogue — the contract's declared types (design/20-contract.md § Public surface 10).
    • McpOptions — the module's own exposure-configuration surface (the contract names the concept,
      "Mcp exposes tool producer registration and exposure configuration", without declaring its
      shape). Default closed: a tool absent from ExposedTools is registered but never listed or
      callable.
    • ToolCatalogue — the frozen IToolCatalogue, a holder replaced once at startup so a concurrent
      reader sees either the empty catalogue or the whole frozen one, never a half-populated set.
    • McpStartupValidation — runs every registered IToolProducer once, rejects a tool whose
      parameter schema names a redaction-marker parameter
      (HostStartupError.SensitiveToolParameter) or whose required permission no catalog declares
      (HostStartupError.UnregisteredPermission), then freezes the catalogue.
    • McpModule — registers the catalogue and the startup validator.
  • Two new HostStartupError variants in Hosting: SensitiveToolParameter and
    UnregisteredPermission. Mcp references Hosting directly for these — a module referencing a
    framework package, which ADR-006 rule 1 / I-C7 allow (only framework→module and module→module are
    forbidden) — since no other module could raise them on Mcp's behalf.
  • Extends PackageGraphTests with SdkReferenceGuard and SdkTypeSurfaceGuard, proving I-M9's
    containment half on the same fixture-first discipline as I-C6/I-C7/I-C8: ModelContextProtocol.*
    is referenced by no package other than Mcp, and no Platform public type's surface (base type,
    interfaces, public property/field/method signatures, generic arguments and array elements
    flattened in) names an SDK type.

S14.8 is only partially met. The criterion's full text is "ModelContextProtocol.* is referenced
by the Mcp module and by no other package." This PR proves the second half only — the actual
ModelContextProtocol.Core package reference is deferred to S15 ("the SDK transport and session"
per that slice's own Touches), since nothing in S14's acceptance criteria requires invoking the SDK
and an unused PackageReference produces no assembly reference for the architecture test to check
against. S15 adds the reference alongside its first real use, at which point the "referenced by Mcp"
half becomes checkable too.

Ordering note: McpStartupValidation is a plain IHostedService rather than
IHostedLifecycleService, deliberately — its work belongs in the StartAsync phase, which the
generic host runs for every hosted service only after every StartingAsync hook has completed. That
guarantees IPermissionCatalogRegistry is already populated and frozen by PlatformRegistryStartup's
own StartingAsync before EnsureDeclared is asked, regardless of DI registration order.

Acceptance criteria (design/30-slices.md, S14)

  • S14.1 A manifest-projecting producer and a product-owned fixed-table producer both
    register through the same interface, and the catalogue treats them identically.
  • S14.2 ToolDefinition declares no exposure member.
  • S14.3 A registered tool absent from exposure configuration answers identically to a name
    that was never registered.
  • S14.4 IToolCatalogue declares no member reaching an unexposed registration.
  • S14.5 A tool naming a redaction-marker parameter fails startup with
    HostStartupError.SensitiveToolParameter, naming the tool and the parameter.
  • S14.6 A tool requiring an undeclared permission fails startup with
    HostStartupError.UnregisteredPermission.
  • S14.7 Each producer's production runs once; the catalogue exposes no registration,
    unregistration or re-exposure member.
  • S14.8 Partially met — see note above. The "no other package references the SDK" half is
    proved; the "referenced by Mcp" half activates in S15.

Test plan

  • dotnet build SubZeroDev.Platform.slnx — clean build.
  • dotnet test tests/SubZeroDev.Platform.Tests — 442/442 passing, including the new
    McpTests and the extended PackageGraphTests.

Verified

Ran and passed:

  • Test — dotnet test --no-build --configuration Release --verbosity normal: Test Run Successful. Total tests: 442, Passed: 442, Total time: 1.2112 Minutes. Includes the new S14 McpTests and the extended PackageGraphTests (SdkReferenceGuard, SdkTypeSurfaceGuard).
  • Assert no framework project references a workload — pwsh ./build/Test-WorkloadIsolation.ps1: "No project under src/ or samples/ references workloads/ (46 project file(s) checked)." Exit code 0.
  • Typecheck — npm run typecheck (workloads/game-service): tsc --noEmit completed with no output and exit code 0.
  • Start the workload and the edge, the documented way — reproduced the documented commands manually against a Docker-provisioned PostgreSQL (docker compose up -d). Workload on 127.0.0.1:8080: GET /livez -> {"status":"healthy"}, GET /readyz -> {"status":"healthy"}. Edge built (dotnet build ... GameEdge.csproj -c Release) and started on 127.0.0.1:5080 against it: GET /health/live -> {"status":"Healthy","checks":[]}, GET /health/ready -> {"status":"Healthy","checks":[{"name":"game-workload","status":"Healthy"},{"name":"platform.audit.sink","status":"Healthy"}]}.
  • Run the replay against the workload — npx vitest run tests/replay.test.ts against a real Docker-provisioned PostgreSQL container: Test Files 1 passed (1), Tests 11 passed (11), including the byte-identity proof over a real hosted process (S5.2/S5.3).
  • Run the replay against the edge — npx vitest run tests/replay-edge.test.ts with GAME_EDGE_DLL pointed at the Release build: Test Files 1 passed (1), Tests 2 passed (2), both edge-fronted byte-identity comparisons (S7.8).
  • Migrate the schema to head, the documented way — GAME_SERVICE_DB_SCHEMA=ci_documented_migrate npm run migrate: "migrated ci_documented_migrate to head", exit code 0, against a real Docker-provisioned PostgreSQL instance.
  • Start the workload against the durable store, the documented way — started with GAME_SERVICE_STORAGE=durable and the documented connection string/schema (ci_documented_durable_start), then GET http://127.0.0.1:8081/readyz -> {"status":"healthy"}.
  • Run the one-instance contention proof — npx vitest run tests/contention-one-instance.test.ts: Test Files 1 passed (1), Tests 5 passed (5), including the two-concurrent-submissions-one-winner proof (S6.1/S6.6) and the unreachable-store-answers-503 perturbation case.
  • Run the two-instance contention proof — npx vitest run tests/contention-two-instances.test.ts: Test Files 1 passed (1), Tests 7 passed (7), including cross-instance session visibility (S7.2) and the one-winner-one-loser proof across two spawned instances (S7.3/S7.4).
  • Run the durable replay — npx vitest run tests/durable-replay.test.ts: Test Files 1 passed (1), Tests 7 passed (7), including the durable dump/in-process snapshot and golden-transcript comparisons (S8.2-S8.4) against real PostgreSQL.
  • Run the port-conformance suite — npx vitest run tests/conformance.test.ts: Test Files 1 passed (1), Tests 7 passed (7).
  • Test (game-service) — npm test (vitest run) against a real Docker-provisioned PostgreSQL, with GAME_EDGE_DLL set: Test Files 24 passed | 1 skipped (25); Tests 197 passed | 1 skipped (198); 0 failed. The one skip is tests/trace-evidence.test.ts, which self-skips without an OTEL_COLLECTOR_BIN pointing at a real OpenTelemetry Collector binary — that binary is a Linux-only download this build.yml job installs and this local Windows session did not reproduce; the corresponding CI run is authoritative for that one sub-test.
  • Validate Markdown links, terminology, and generated files — pwsh ./build/Test-Documentation.ps1: "Documentation checks passed across 127 Markdown file(s), with 2 warning(s)." Both warnings are pre-existing terminology notes in design/d3/90-decisions.md ("Use 'JSON' instead of 'Json'"), unrelated to this PR's changes; warnings do not fail the gate. Exit code 0.
  • Validate design/30-slices.md's Status markers — pwsh ./build/Test-SliceStatusMarkers.ps1: "Slice status markers consistent across 11 slice(s) in 'design/30-slices.md'." This PR does not touch design/30-slices.md.
  • Build and verify the status-page site (docs-ci verify job) — npm --prefix site ci && npm --prefix site run check: format:check, lint (oxlint, one non-blocking fast-refresh warning in src/shared.tsx), typecheck, test (6 files / 57 tests passed), test:build (site builds and both HTML entry points carry required static metadata), and test:merge ("Package merge preserves docs and rejects all protected-boundary violations") all succeeded.
  • Build and verify the status-page site (docs-deploy job) — identical command to the docs-ci verify job above, run once and reported under both step names since both jobs run the exact same script against the exact same site/ directory.

Ran and failed: none.

Did not run:

  • Run the sample in both roles — build/Test-SampleRoundTrip.ps1 documents itself as Linux-only: it signals processes via a P/Invoke to libc kill(2) and reads the store with the sqlite3 CLI, neither of which is available in this Windows session (no sqlite3 on PATH, no libc). The corresponding check on this pull request's build.yml run (ubuntu-latest) is authoritative for this gate.

Agent detail
  • Slice: S14 — design/30-slices.md § S14
  • Criteria met: S14.1, S14.2, S14.3, S14.4, S14.5, S14.6, S14.7
  • Left partial: S14.8 — the SDK-reference half is deferred to S15 (see note above)

The-Running-Dev and others added 2 commits September 7, 2026 08:56
Adds the new SubZeroDev.Platform.Mcp module: ToolName, ToolProducerName,
ToolDefinition, ToolRegistration, IToolProducer and IToolCatalogue per
design/20-contract.md § Public surface 10, plus McpOptions as the module's
own exposure-configuration surface (the contract names the concept without
declaring its shape).

McpStartupValidation runs every registered producer once at startup,
rejects a tool whose parameter schema names a redaction-marker parameter
(HostStartupError.SensitiveToolParameter) or whose required permission no
catalog declares (HostStartupError.UnregisteredPermission), and freezes
the catalogue — Mcp references Hosting directly for this (a module
referencing a framework package, allowed by ADR-006 rule 1/I-C7), since
the two new HostStartupError variants live there and no other module may
reach Mcp to raise them on its behalf. Ordering is safe by the generic
host's own phase separation: PlatformRegistryStartup populates and
freezes the permission catalog registry in the StartingAsync phase, which
completes for every hosted service before any StartAsync phase begins —
including McpStartupValidation's.

The SDK package reference itself is deferred to S15 ("the SDK transport
and session" per the slice's own Touches): nothing in S14's acceptance
criteria requires invoking it, and an unused PackageReference produces no
actual assembly reference to check against, so I-M9's architecture test
(PackageGraphTests) proves the containment property ahead of anything
crossing it, on the same fixture-first discipline as I-C6/I-C7/I-C8.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full discovered gate suite ran and passed except the Linux-only sample
round-trip script, which cannot run on this Windows session and defers
to build.yml's own ubuntu-latest run.
@The-Running-Dev
The-Running-Dev merged commit f44652c into main Sep 7, 2026
5 checks passed
@The-Running-Dev
The-Running-Dev deleted the slice/S14 branch September 7, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

D5-S14 — Mcp: the frozen catalogue and its startup checks

1 participant