D5-S14 — Mcp: the frozen catalogue and its startup checks - #220
Merged
Merged
Conversation
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.
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
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
ExposedToolsis registered but never listed orcallable.
ToolCatalogue— the frozenIToolCatalogue, a holder replaced once at startup so a concurrentreader sees either the empty catalogue or the whole frozen one, never a half-populated set.
McpStartupValidation— runs every registeredIToolProduceronce, rejects a tool whoseparameter 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.HostStartupErrorvariants in Hosting:SensitiveToolParameterandUnregisteredPermission. Mcp references Hosting directly for these — a module referencing aframework 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.
PackageGraphTestswithSdkReferenceGuardandSdkTypeSurfaceGuard, proving I-M9'scontainment 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 referencedby the Mcp module and by no other package." This PR proves the second half only — the actual
ModelContextProtocol.Corepackage 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
PackageReferenceproduces no assembly reference for the architecture test to checkagainst. S15 adds the reference alongside its first real use, at which point the "referenced by Mcp"
half becomes checkable too.
Ordering note:
McpStartupValidationis a plainIHostedServicerather thanIHostedLifecycleService, deliberately — its work belongs in theStartAsyncphase, which thegeneric host runs for every hosted service only after every
StartingAsynchook has completed. Thatguarantees
IPermissionCatalogRegistryis already populated and frozen byPlatformRegistryStartup'sown
StartingAsyncbeforeEnsureDeclaredis asked, regardless of DI registration order.Acceptance criteria (design/30-slices.md, S14)
register through the same interface, and the catalogue treats them identically.
ToolDefinitiondeclares no exposure member.that was never registered.
IToolCataloguedeclares no member reaching an unexposed registration.HostStartupError.SensitiveToolParameter, naming the tool and the parameter.HostStartupError.UnregisteredPermission.unregistration or re-exposure member.
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 newMcpTestsand the extendedPackageGraphTests.Verified
Ran and passed:
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).pwsh ./build/Test-WorkloadIsolation.ps1: "No project under src/ or samples/ references workloads/ (46 project file(s) checked)." Exit code 0.npm run typecheck(workloads/game-service):tsc --noEmitcompleted with no output and exit code 0.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"}]}.npx vitest run tests/replay.test.tsagainst 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).npx vitest run tests/replay-edge.test.tswithGAME_EDGE_DLLpointed at the Release build: Test Files 1 passed (1), Tests 2 passed (2), both edge-fronted byte-identity comparisons (S7.8).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.GAME_SERVICE_STORAGE=durableand the documented connection string/schema (ci_documented_durable_start), thenGET http://127.0.0.1:8081/readyz->{"status":"healthy"}.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.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).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.npx vitest run tests/conformance.test.ts: Test Files 1 passed (1), Tests 7 passed (7).npm test(vitest run) against a real Docker-provisioned PostgreSQL, withGAME_EDGE_DLLset: Test Files 24 passed | 1 skipped (25); Tests 197 passed | 1 skipped (198); 0 failed. The one skip istests/trace-evidence.test.ts, which self-skips without anOTEL_COLLECTOR_BINpointing 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.pwsh ./build/Test-Documentation.ps1: "Documentation checks passed across 127 Markdown file(s), with 2 warning(s)." Both warnings are pre-existing terminology notes indesign/d3/90-decisions.md("Use 'JSON' instead of 'Json'"), unrelated to this PR's changes; warnings do not fail the gate. Exit code 0.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 touchdesign/30-slices.md.npm --prefix site ci && npm --prefix site run check: format:check, lint (oxlint, one non-blocking fast-refresh warning insrc/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.site/directory.Ran and failed: none.
Did not run:
build/Test-SampleRoundTrip.ps1documents itself as Linux-only: it signals processes via a P/Invoke to libckill(2)and reads the store with thesqlite3CLI, neither of which is available in this Windows session (no sqlite3 on PATH, no libc). The corresponding check on this pull request'sbuild.ymlrun (ubuntu-latest) is authoritative for this gate.Agent detail
design/30-slices.md§ S14