diff --git a/experiments/analysis-efficiency/evm-descriptor.md b/experiments/analysis-efficiency/evm-descriptor.md index 8e3611f282..0fcdf4d44b 100644 --- a/experiments/analysis-efficiency/evm-descriptor.md +++ b/experiments/analysis-efficiency/evm-descriptor.md @@ -179,3 +179,40 @@ interface question through an explicitly selected precise view, retain full bytecode/original Evidence under the existing lifetime, and preserve complete defaults. Discover support before sending new options to an older server. Include discovery, follow-up, failures and retries in the complete task cost. + +## Static contract admission verification + +The follow-up from main `c462a4ebd3a10e59253bcf38353a0914b176738b` tests the +pilot's actual static boundary, without adding a dynamic module registry or +changing production code. Complete-project TypeScript checking now verifies +that the inferred descriptor type rejects a WASM operation contract, a WASM +CLI command, a WASM service factory, an unrelated input schema and an unrelated +output schema. These are negative assignability assertions: their guarantee +comes from the compiler, not from executing an incompatible descriptor. +The existing named contract and CLI identity are also checked at runtime; +separate factory calls produce distinct service/provider owners. Invalid target +input remains an application error without launching a worker. + +The actual public in-memory MCP SDK test additionally spies on the provider: +duplicate EVM registration throws the named `already registered` error before +any provider call. Converted input/output JSON Schemas validate before the +first valid operation. Invalid client arguments are rejected before execution; +incompatible provider reply data is validated **after** execution and is not +presented as a pre-use contract guarantee. Existing healthy-peer, cancellation, +retained summary/default and cleanup-retry scenarios remain covered. + +Full project typecheck passes with the installed compiler's supported single +thread scheduling; it includes all source/test files. The two exact focused +files pass 12 runtime cases. The resource scopes sample 2,057,961,472 bytes for +typecheck and 2,426,290,176 bytes for required build plus focused tests, below the +unchanged 3 GiB early-stop/4 GiB hard ceiling. Final transient-cgroup counters +remain unknown. No new real-provider or performance improvement is claimed by +these compile/SDK checks; the earlier pinned real EVMole/WABT/lifecycle evidence +retains its original scope and source identity. + +This proves the one statically composed descriptor's named/schema/factory +compatibility and duplicate-registration guard. It does not implement runtime +admission of arbitrary JavaScript descriptors, third-party plugin ABI/version +negotiation or a general plugin SDK. Those remain deferred by the original plan. +Existing callers, full defaults, provider inputs, owned lifetimes and output +identities are unchanged. diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index 704af92c56..f3594dd891 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -28,8 +28,11 @@ import { it("advertises exact valid schemas and records inline EVM interface evidence without a binary target", async () => { const value = evmInterfaceFixture(); + const inspect = vi.fn(() => + Promise.resolve(ok(evmInterfaceExecution(value))), + ); const service = new EvmInterfaceService({ - inspect: () => Promise.resolve(ok(evmInterfaceExecution(value))), + inspect, }); const session = createTestBinarySession(() => { throw new Error("deep provider must not start"); @@ -71,6 +74,7 @@ it("advertises exact valid schemas and records inline EVM interface evidence wit expect(() => registerEvmTools(server, service, silentLogger)).toThrow( /inspect_evm_interface.*already registered/, ); + expect(inspect).not.toHaveBeenCalled(); expect( (await client.listTools()).tools.filter( (tool) => tool.name === "inspect_evm_interface", diff --git a/tests/composition/evmModuleContract.test.ts b/tests/composition/evmModuleContract.test.ts new file mode 100644 index 0000000000..b04a41d93b --- /dev/null +++ b/tests/composition/evmModuleContract.test.ts @@ -0,0 +1,62 @@ +import { expect, expectTypeOf, it, vi } from "vitest"; +import { z } from "zod"; +import { evmInterfaceModule } from "../../src/composition/evm.js"; +import { wasmArtifactModule } from "../../src/composition/wasm.js"; +import { CLI_COMMANDS } from "../../src/cliCommandNames.js"; +import { INSPECT_EVM_INTERFACE_CONTRACT } from "../../src/contracts/evm/evmToolContracts.js"; + +it("rejects incompatible static bindings through the complete project typecheck", () => { + // expect-type's negative assertions are checked by tsc, not the runtime + // test runner. No malformed descriptor is registered or executed here. + expectTypeOf({ + ...evmInterfaceModule, + contract: wasmArtifactModule.contract, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + cliCommand: CLI_COMMANDS.inspectWasmArtifact, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + create: wasmArtifactModule.create, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + contract: { + ...evmInterfaceModule.contract, + inputSchema: z.strictObject({ unrelated: z.string() }), + }, + }).not.toExtend(); + expectTypeOf({ + ...evmInterfaceModule, + contract: { + ...evmInterfaceModule.contract, + outputSchema: z.strictObject({ unrelated: z.string() }), + }, + }).not.toExtend(); +}); + +it("keeps the named binding and gives each caller a separate lazy service owner", async () => { + expect(evmInterfaceModule.contract).toBe(INSPECT_EVM_INTERFACE_CONTRACT); + expect(evmInterfaceModule.cliCommand).toBe(CLI_COMMANDS.inspectEvmInterface); + const first = evmInterfaceModule.create({}); + const second = evmInterfaceModule.create({}); + try { + expect(first).not.toBe(second); + expect(first.provider).not.toBe(second.provider); + await first.close(); + const inspect = vi.spyOn(second.provider, "inspect"); + const invalid = await second.inspect({ + path: "relative.hex", + encoding: "hex", + }); + expect(invalid).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); + expect(inspect).not.toHaveBeenCalled(); + } finally { + await first.close(); + await second.close(); + } +});