From 5c53bcaed8e2d155454f0e968697950fc20e5c94 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 21:51:18 +0800 Subject: [PATCH] feat(mcp)!: retire the full schema profiles and declare schema facts as typed annotations (#10693) * test(mcp): check advertised schemas against a portable dialect in every profile * fix(mcp): re-emit advertised patterns in a portable regex grammar * fix(mcp): present discover_tools input through the selected profile The hot-cold catalog registered discover_tools directly with the SDK, so the default compact input profile still advertised its canonical schema, including page-size defaults clients apply on their own (#5322). Route every listed input schema, transport discovery included, through one server-owned presentation, and pass the selected presentation to the deferred server, which serves recorded tool schemas but not discovery. * fix(mcp): keep the Evidence union on described borrowed inputs borrowEvidenceInput installed its advertised projection on one schema instance. A call site's .describe() rebuilds the schema from its definition, so match_native_functions advertised left and right as unconstrained values in every input profile. Install the projection in the borrowed parser's constructor so derived schemas keep it, for both borrowed inputs and borrowed arrays. * fix(mcp): validate advertised dialect by selected profile * fix(mcp): preserve schema widening through references * feat(mcp): project compact input schemas into the portable advertised dialect One projection (advertisedDialectProjection.ts) owns the compact input advertisement: one type per node, items on every array, string literals as enums, oneOf widened to anyOf, allOf merged, tuples as element unions, exclusive bounds made inclusive, and every keyword outside the dialect dropped. Each rewrite is exact or widening; server validation is unchanged. The root description and sentences for dropped cross-field constraints move to the tool description, which Codex and Zed keep. The compact baseline falls to nullable values only (29), which the dialect cannot express without narrowing an input. The gateway shape repairs (typeArrayItemUnions, hasGatewayIncompatibleShape), the second dependentRequired-to-prose copy and the strict-gateway test denylist are removed; the dialect checker gates the inline presentation instead. * feat(mcp): omit output schemas from tools/list by default No surveyed model API reads an output schema, MCP client validators repeat REA's own result validation, and complete output schemas were about 92% of the default tools/list. REA_MCP_OUTPUT_SCHEMA_PROFILE now resolves to off through one owner (resolveMcpOutputSchemaProfile); compact and full remain selectable. structuredContent is still delivered, and the deferred server serves both off and full from the recorded catalog with the matching recorded identity. Tests and artifact verifiers that validate results against complete advertised output contracts select full explicitly. * docs: describe omitted output schemas as the default in README translations * fix(mcp): select schemas in verification workflows * fix(mcp): refuse zero-padded repeat bounds and record host-specific path patterns * fix(mcp): keep projected schemas permissive * fix(mcp): keep references valid when presentation widens their targets * fix(mcp): keep a single union branch's closure scoped to its properties * fix(mcp): select complete output schemas in the Electron verifier * fix(mcp): select complete output schemas in standalone verifier probes * fix(mcp): keep an allOf member's tuple tail with its own positions * fix(mcp): relax references into negations and conditionals presentation may remove * refactor(contracts): declare root discriminators as typed schema annotations * test(mcp): select the borrowed Evidence contract from its family * fix(mcp): keep astral literals and dynamic references widening in every mode * fix(mcp): omit astral literals and relax untracked references when presentation widens * fix(mcp): select complete output schemas in every verifier transport * fix(mcp): read reference pointers by schema position, not segment spelling * fix(mcp): protect references into collapsing boolean unions and keep referencing branches out of the collapse * fix(mcp): keep reduced schemas open when their root admits names by pattern * test(mcp): keep the reduced-schema pattern case outside the size-limited block * fix(mcp): omit patterns whose classes name astral characters * fix(mcp): decide union collapse and pointer protection from canonical branches * docs(mcp): document non-u astral matching and pin BMP agreement across V8 modes * fix(mcp): keep prototype-named properties as own properties in the dialect projection * test(mcp): select full output schemas in packet and signal schema checks * test(mcp): expect the advertised base64 schema to omit the unbounded pad-bit probe * fix(mcp): protect references into unevaluated keywords lost evaluation removes * fix(mcp): validate the doctor probe reply against the canonical output contract (#10655) * fix(mcp): validate the doctor probe reply against the canonical output contract * test(mcp): replay doctor probe validation under every output profile * test(mcp): accept typed-branch nullable unions in the advertised dialect (#10649) * test(mcp): select full output schemas in HTTP session workflows * fix(mcp): emit stack-safe classes with non-ASCII members and protect draft-07 reference siblings * fix(mcp): protect references into literal siblings of relaxed draft-07 references * feat(mcp): omit output schemas from tools/list by default (#10646) * feat(mcp): omit output schemas from tools/list by default No surveyed model API reads an output schema, MCP client validators repeat REA's own result validation, and complete output schemas were about 92% of the default tools/list. REA_MCP_OUTPUT_SCHEMA_PROFILE now resolves to off through one owner (resolveMcpOutputSchemaProfile); compact and full remain selectable. structuredContent is still delivered, and the deferred server serves both off and full from the recorded catalog with the matching recorded identity. Tests and artifact verifiers that validate results against complete advertised output contracts select full explicitly. * fix(mcp): select schemas in verification workflows * fix(mcp): select complete output schemas in the Electron verifier * fix(mcp): select complete output schemas in standalone verifier probes * fix(mcp): select complete output schemas in every verifier transport * test(mcp): select full output schemas in packet and signal schema checks * fix(mcp): validate the doctor probe reply against the canonical output contract (#10655) * fix(mcp): validate the doctor probe reply against the canonical output contract * test(mcp): replay doctor probe validation under every output profile * test(mcp): accept typed-branch nullable unions in the advertised dialect (#10649) * test(mcp): select full output schemas in HTTP session workflows * fix(mcp): keep the Evidence union on described borrowed inputs (#10597) * fix(mcp): keep the Evidence union on described borrowed inputs borrowEvidenceInput installed its advertised projection on one schema instance. A call site's .describe() rebuilds the schema from its definition, so match_native_functions advertised left and right as unconstrained values in every input profile. Install the projection in the borrowed parser's constructor so derived schemas keep it, for both borrowed inputs and borrowed arrays. * test(mcp): select the borrowed Evidence contract from its family * docs(mcp): admit baseline growth only for restored contract declarations * fix(mcp): state reduced multi-type properties as typed branches, not type lists The reduced fallback derived a property's types and emitted them as a type list, which Gemini, Moonshot and Ark reject. A property whose canonical schema is anyOf string and number therefore traded a typed union for a rejected construct when the inline projection fell back. Emit one type, or anyOf typed branches, so [T, null] becomes the dialect's nullable union. * feat(mcp)!: retire the full schema profiles and declare complete observations as a typed annotation (#10718) * refactor(mcp): declare complete observations as a typed annotation applied only on the wire * test(mcp): compare complete-observation declarations with their owning text * refactor(schema): couple Zod checks with their JSON Schema constraint keywords (#10725) * refactor(schema): couple Zod checks with their JSON Schema constraint keywords * refactor(schema): declare pattern approximations and object output roots as typed annotations (#10754) * refactor(schema): declare pattern approximations and object output roots as typed annotations * fix(schema): refuse pattern declarations JSON Schema cannot state * test(schema): select guided_workflows from its contract family * feat(mcp)!: retire the full input and output schema profiles (#10841) * feat(mcp)!: retire the full input and output schema profiles The compact input presentation is now the only advertised input schema, and output schemas are either omitted (`off`, the default) or bounded (`compact`). Catalog identity, the build-recorded digests and the recorded startup catalog collapse to those presentations: two digest profiles instead of six, one recorded catalog instead of two, and no input dialect baseline. Tests and verifiers that validated replies against listed full output schemas now validate against the canonical output contract, which REA already enforces server-side; tests that asserted canonical input restrictions read the canonical input schema instead of a full listing. BREAKING CHANGE: `REA_MCP_INPUT_SCHEMA_PROFILE=full` and `REA_MCP_OUTPUT_SCHEMA_PROFILE=full` are refused at startup and by `rea setup` with a configuration error naming the setting. Remove the setting from the client registration (or rerun setup with `REA_MCP_INPUT_SCHEMA_PROFILE=compact` / `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`). `REA_MCP_INPUT_SCHEMA_PROFILE` still accepts `compact`. Server-side validation is unchanged. * test(mcp): validate replies against canonical output contracts at every listed-schema reader tools/list omits output schemas by default, so fixtures, verifiers and the Android analysis runners that read a listed outputSchema now bind the canonical contract instead. connectLocalToolsMcp and advertisedMcpToolSchemas hand the SDK a definition carrying the canonical output schema, so the SDK client and caller-supplied validators check every successful reply. * test(mcp): resolve Android runners' output contracts from the selected repository The Android efficiency runners imported the canonical output-schema helper from the harness checkout while --repository selects the revision under test. Import it from the selected repository, as the runners already do for the SDK and Ajv. * test(mcp): let Android runners compare revisions before the canonical output helper A selected revision without scripts/lib/canonical-output-schema.mjs could not start the Android runners. Choose the output contracts in one harness helper: the selected revision's canonical contracts when it has them, otherwise its listed schemas under the full output profile it still supports. * fix(scripts): keep caller launch settings when the real Hopper verifier builds its server environment The verifier deleted REA_VERIFY_SERVER_COMMAND and REA_VERIFY_SERVER_ARGS_JSON from process.env itself before reading them, so a caller selecting an installed REA command silently ran the checkout's dist/main.js. Copy the environment, as main does, so only the child's environment changes. * test(mcp): validate whole replies against canonical output contracts wherever a listed definition is held tools/list omits output schemas by default and the full output profile is gone, so tests that handed the SDK a listed definition no longer checked complete replies. Attach the canonical output schema with withCanonicalOutputSchema at every held REA tool definition. Environment-served tools (not REA contracts), and the deferred-runtime and doctor tests that exercise per-profile listings, keep their listed definitions. * test(mcp): record the advertised catalog summary without the retired full profiles Retiring the full input and output profiles leaves only the compact input and compact output listings, so the summary drops the full-profile digests and every pattern only the full listings carried. Regenerated with npm run verify:advertised-schemas -- --update-summary. --- README.md | 12 +- docs/development/client-verification.md | 47 +- docs/development/testing.md | 28 +- docs/getting-started/client-configuration.md | 75 +- docs/getting-started/installation.md | 4 +- docs/mcp-catalog-evaluation.md | 4 - docs/readme/README_ar.md | 4 +- docs/readme/README_bg.md | 12 +- docs/readme/README_de.md | 4 +- docs/readme/README_el.md | 4 +- docs/readme/README_es.md | 4 +- docs/readme/README_fa.md | 4 +- docs/readme/README_fr.md | 4 +- docs/readme/README_id.md | 4 +- docs/readme/README_it.md | 4 +- docs/readme/README_ja.md | 4 +- docs/readme/README_ko.md | 4 +- docs/readme/README_ml.md | 4 +- docs/readme/README_pl.md | 4 +- docs/readme/README_pt-BR.md | 4 +- docs/readme/README_ro.md | 10 +- docs/readme/README_ru.md | 4 +- docs/readme/README_th.md | 4 +- docs/readme/README_tr.md | 4 +- docs/readme/README_uk.md | 4 +- docs/readme/README_ur-Latn.md | 11 +- docs/readme/README_vi.md | 4 +- docs/readme/README_zh-TW.md | 4 +- docs/readme/README_zh.md | 4 +- docs/reference/mcp-contracts.md | 56 +- scripts/generate-skill-metadata.mjs | 4 +- scripts/lib/artifact-e2e.mjs | 20 +- .../browser-capture-metadata-budget-e2e.mjs | 5 +- ...wser-scenario-storage-fingerprints-e2e.mjs | 3 +- scripts/lib/canonical-output-schema.d.mts | 5 + scripts/lib/canonical-output-schema.mjs | 24 + scripts/lib/catalog-builders.d.mts | 2 +- scripts/lib/catalog-builders.mjs | 2 +- .../lib/mcp-verification-environment.d.mts | 7 - scripts/lib/mcp-verification-environment.mjs | 6 - scripts/lib/native-value-e2e.mjs | 3 +- scripts/lib/product-catalog.mjs | 2 +- scripts/lib/real-ghidra-boundaries.mjs | 13 +- scripts/lib/real-hopper-boundaries.mjs | 13 +- scripts/lib/release-package-canary.mjs | 10 +- scripts/lib/verify-package-core.d.mts | 1 - scripts/lib/verify-package-core.mjs | 17 +- scripts/record-catalog-digests.mjs | 11 +- scripts/record-mcp-catalog.mjs | 17 +- scripts/verify-apk-signature.mjs | 13 +- scripts/verify-claude-client.mjs | 14 +- scripts/verify-copilot-client.mjs | 26 +- scripts/verify-gemini-client.mjs | 4 - scripts/verify-go-binary.mjs | 3 +- scripts/verify-http3-network-captures.mjs | 10 +- scripts/verify-package-mcp.mjs | 6 +- scripts/verify-package-pack.mjs | 5 +- scripts/verify-packet-capture.mjs | 31 +- scripts/verify-qlog-captures.mjs | 10 +- scripts/verify-real-browser.mjs | 5 +- scripts/verify-real-ghidra-com.mjs | 20 +- scripts/verify-real-ghidra-raw.mjs | 13 +- scripts/verify-real-ghidra-riscv.mjs | 15 +- scripts/verify-real-hopper-callers.mjs | 5 +- scripts/verify-real-hopper-fat.mjs | 15 +- scripts/verify-real-hopper.mjs | 3 +- scripts/verify-recorded-crash.mjs | 10 +- scripts/verify-signal-recording.mjs | 27 +- scripts/verify-wasm-artifact.mjs | 25 +- scripts/verify-windows-package.mjs | 4 +- .../verify/advertised-schemas/summary.json | 2560 ++--------------- scripts/verify/android/external-invokes.mjs | 13 +- .../verify/architecture-guard-baselines.json | 4 + scripts/verify/architecture-guards.mjs | 14 + scripts/verify/electron/tool-contracts.mjs | 14 +- src/application/DirectAnalysisStatus.ts | 6 +- src/catalogIdentity.ts | 147 +- src/config/mcpInputSchemaProfile.ts | 34 +- src/config/mcpOutputSchemaProfile.ts | 25 +- src/config/parseConfig.ts | 3 - src/config/types.ts | 2 - src/contracts/advertisedDialectBaseline.ts | 55 - src/contracts/artifactToolContracts.ts | 13 +- src/contracts/browserToolContracts.ts | 10 +- .../bytecodeDeliveryContracts.test.ts | 4 +- src/contracts/compactInputSchema.ts | 7 +- src/contracts/erlang/beamToolContracts.ts | 10 +- src/contracts/guidedWorkflowSchemas.ts | 12 +- .../inputObservationPresentation.test.ts | 135 +- src/contracts/inputSchemaPresentation.ts | 30 +- .../javascript/electronToolContracts.ts | 23 +- .../javascriptRouteDeliveryContracts.test.ts | 3 +- src/contracts/managed/managedToolContracts.ts | 28 +- .../python/pythonPycToolContracts.ts | 10 +- src/contracts/toolSchemaMetadata.test.ts | 16 + src/contracts/toolSchemaMetadata.ts | 62 +- src/contracts/webScriptToolContracts.ts | 10 +- .../analysisView/environmentManifestView.ts | 12 +- src/domain/apiContract/apiContractQuery.ts | 10 +- src/domain/browserScenarioDiffValues.ts | 17 +- src/domain/desktop/appBuild.ts | 15 +- src/domain/desktop/buildComputerSettings.ts | 7 +- src/domain/flutter/flutterBuildAnalysis.ts | 20 +- src/domain/il2cpp/il2cppMetadata.ts | 42 +- src/domain/native/functionDossier.ts | 152 +- src/domain/portableBundleName.ts | 27 +- src/domain/schemaAnnotations.test.ts | 46 + src/domain/schemaAnnotations.ts | 102 + src/domain/schemaConstraints.test.ts | 188 ++ src/domain/schemaConstraints.ts | 174 ++ src/domain/sqlite/sqliteDatabase.ts | 55 +- src/domain/stringPatterns.ts | 32 +- src/domain/webCaptureDiffSchemas.ts | 12 +- src/main.ts | 8 +- src/main/deferredRuntime.ts | 39 +- src/mcpDoctor.ts | 10 - src/recordedCatalogDigests.ts | 7 +- src/recordedMcpCatalog.ts | 5 +- src/server/BundleResourcesMcp.test.ts | 5 +- src/server/DeferredEvidenceMcpServer.test.ts | 15 +- src/server/DeferredEvidenceMcpServer.ts | 8 +- src/server/EvidenceMcpServer.ts | 12 +- src/server/EvidenceMcpServerCore.ts | 43 +- src/server/ProgressiveToolCatalog.test.ts | 2 +- src/server/ProgressiveToolCatalog.ts | 2 +- src/server/createServer.ts | 9 +- src/server/registerGuidedWorkflowTool.test.ts | 2 +- src/setupEnvironment.test.ts | 4 +- tests/acceptance/analysisSettingsMcp.test.ts | 11 +- tests/acceptance/apkSignatureV1Cli.test.ts | 8 +- .../applications/apiContract.test.ts | 7 +- .../applicationPromiseHandleCliMcp.test.ts | 5 +- .../applications/firmwareComparison.test.ts | 1 - .../applications/graphqlContract.test.ts | 7 +- .../applications/peResources.test.ts | 5 +- .../applications/playwrightTrace.test.ts | 7 +- .../progressiveAnalysisViews.test.ts | 8 +- tests/acceptance/applications/runtime.test.ts | 9 +- .../staticObservedEndpoints.test.ts | 10 +- .../artifactExtractionSelection.test.ts | 10 +- .../acceptance/harmonyArtifactJourney.test.ts | 3 +- tests/acceptance/javascriptRoutesCli.test.ts | 2 +- .../acceptance/managedMemberRecovery.test.ts | 8 +- .../jebConfigurationIsolation.test.ts | 5 +- .../retainedComparisonAggregation.test.ts | 2 +- tests/acceptance/setup/dshClientSetup.test.ts | 5 +- .../setup/launcherAliasesCli.test.ts | 3 +- .../registrationRuntimeEnvironmentCli.test.ts | 4 +- tests/acceptance/toolFamiliesMcp.test.ts | 2 +- .../tools/nativeFunctionMatching.test.ts | 4 +- .../wasmInterfaceComparison.test.ts | 8 +- tests/boundary/browser/sazWebCapture.test.ts | 6 +- .../cli/artifactComparisonSources.test.ts | 4 +- .../boundary/cli/directAnalysisStatus.test.ts | 1 - .../cli/workflowInputIdentities.test.ts | 10 +- .../filesystem/codexClientPolicy.test.ts | 2 +- .../filesystem/dependencyInstall.test.ts | 2 +- .../filesystem/generatedSkill.test.ts | 2 +- .../kimiClientConfiguration.test.ts | 68 +- .../lmStudioClientConfiguration.test.ts | 8 +- .../javascriptSemanticTraceEvidence.test.ts | 7 +- .../mcp/absoluteLocalPathSchema.test.ts | 16 +- .../boundary/mcp/adbDeviceProperties.test.ts | 5 +- tests/boundary/mcp/adbMcp.test.ts | 7 +- .../mcp/addressContextCapacity.test.ts | 8 +- .../mcp/addressContextInputErrors.test.ts | 9 +- tests/boundary/mcp/advertisedDialect.test.ts | 139 +- .../mcp/advertisedPatternsVMode.test.ts | 6 +- tests/boundary/mcp/analysisViewMcp.test.ts | 44 +- .../mcp/androidDescriptorAdmissionMcp.test.ts | 2 +- .../mcp/androidPackageSummaryMcp.test.ts | 13 +- tests/boundary/mcp/apktoolMcp.test.ts | 2 +- tests/boundary/mcp/appleIpsMcp.test.ts | 7 +- .../mcp/applicationEvidenceMcp.test.ts | 5 +- .../mcp/applicationExportShapeMcp.test.ts | 16 +- .../mcp/artifactInspectionCoverageMcp.test.ts | 7 +- .../mcp/artifactMcp.inlineInventory.test.ts | 14 +- .../mcp/batchDecompileViewsMcp.test.ts | 27 +- tests/boundary/mcp/binaryHardeningMcp.test.ts | 5 +- tests/boundary/mcp/browserMcp.test.ts | 18 +- ...browserScenarioInputDiagnosticsMcp.test.ts | 2 +- tests/boundary/mcp/browserScenarioMcp.test.ts | 20 +- .../mcp/browserSourceCoordinates.test.ts | 8 +- .../mcp/compactInputSchemaMcp.test.ts | 165 +- .../mcp/contextEvidenceOverflow.test.ts | 7 +- .../boundary/mcp/contractPresentation.test.ts | 21 +- tests/boundary/mcp/dartAotSummaryMcp.test.ts | 11 +- tests/boundary/mcp/deferredRuntime.test.ts | 143 +- tests/boundary/mcp/electronFusesMcp.test.ts | 7 +- tests/boundary/mcp/electronMcp.test.ts | 5 +- .../enhancedTools.callPathsAndBatches.test.ts | 8 +- .../boundary/mcp/environmentBundleMcp.test.ts | 2 +- .../mcp/errorEvidenceLifecycleMcp.test.ts | 2 +- tests/boundary/mcp/espFlashImageMcp.test.ts | 2 +- tests/boundary/mcp/espImageMcp.test.ts | 2 +- .../evidenceAtomicRetentionRecovery.test.ts | 2 - .../mcp/evidenceInputContractsMcp.test.ts | 13 +- .../mcp/evidenceLinkInspectionMcp.test.ts | 15 +- .../mcp/evidenceRecordSelectionMcp.test.ts | 9 +- .../mcp/evidenceRetentionRecovery.test.ts | 5 +- tests/boundary/mcp/evmInterfaceMcp.test.ts | 14 +- .../boundary/mcp/featureTraceViewsMcp.test.ts | 18 +- .../mcp/firmwareContainersMcp.test.ts | 2 +- tests/boundary/mcp/fitImageMcp.test.ts | 2 +- tests/boundary/mcp/flutterMcp.test.ts | 19 +- tests/boundary/mcp/fridaMcp.test.ts | 31 +- .../mcp/functionCallDirectionsMcp.test.ts | 4 +- .../mcp/functionComparisonCoverageMcp.test.ts | 9 +- .../mcp/functionComparisonMcp.test.ts | 6 +- ...unctionComparisonReferenceFactsMcp.test.ts | 7 +- .../mcp/gamecubeDolSessionMcp.test.ts | 6 +- .../mcp/ghidraAmbiguousProcedureMcp.test.ts | 4 +- .../mcp/ghidraContainingListingMcp.test.ts | 4 +- tests/boundary/mcp/ghidraEvidenceMcp.test.ts | 3 +- tests/boundary/mcp/ghidraNativeApiMcp.test.ts | 12 +- .../mcp/ghidraRegexDiagnosticsMcp.test.ts | 2 +- tests/boundary/mcp/goBinaryMcp.test.ts | 7 +- tests/boundary/mcp/goBinaryViews.test.ts | 3 +- tests/boundary/mcp/godotPckMcp.test.ts | 25 +- tests/boundary/mcp/guidedWorkflowsMcp.test.ts | 8 +- tests/boundary/mcp/hexRecordsMcp.test.ts | 4 +- .../mcp/javascriptFactsViewMcp.test.ts | 7 +- .../mcp/javascriptRecoveryMcp.test.ts | 10 +- .../javascriptRuntimeObservationMcp.test.ts | 7 +- .../mcp/javascriptSourceSelection.test.ts | 14 +- tests/boundary/mcp/jebMcp.test.ts | 4 +- tests/boundary/mcp/kernelModuleMcp.test.ts | 6 +- tests/boundary/mcp/largeToolResult.test.ts | 7 +- .../mcp/managedArtifactSummaryMcp.test.ts | 8 +- .../mcp/managedFieldConstantsMcp.test.ts | 11 +- tests/boundary/mcp/managedMcp.test.ts | 7 +- .../mcp/managedMemberSummaryMcp.test.ts | 14 +- tests/boundary/mcp/mcpDoctor.test.ts | 92 +- .../mcp/nativeAddressContextMcp.test.ts | 6 +- .../mcp/nativeFeatureTraceViews.test.ts | 2 +- .../nativeInstructionReferencesView.test.ts | 9 +- .../mcp/nativeInventoryViewsMcp.test.ts | 15 +- .../mcp/nativeMachoSummaryMcp.test.ts | 11 +- .../mcp/nativeObservationViewsMcp.test.ts | 2 +- .../mcp/nativeProcedureReferenceViews.test.ts | 4 +- .../mcp/nativeValueFlowAvailability.test.ts | 3 - .../mcp/nativeValueTraceCompleteness.test.ts | 21 +- .../mcp/nativeViewPredicateMcp.test.ts | 13 +- tests/boundary/mcp/nodeSeaMcp.test.ts | 2 +- .../boundary/mcp/outputSchemaProfile.test.ts | 62 +- .../outputSchemaSemanticConstraints.test.ts | 12 +- .../mcp/packageCatalogVerification.test.ts | 19 +- tests/boundary/mcp/packetCaptureMcp.test.ts | 10 +- tests/boundary/mcp/peCertificatesMcp.test.ts | 7 +- tests/boundary/mcp/peLoadConfigMcp.test.ts | 7 +- .../mcp/processCaptureCleanupSchema.test.ts | 3 +- tests/boundary/mcp/promptMcp.test.ts | 2 +- .../boundary/mcp/providerSelectionMcp.test.ts | 10 +- .../mcp/pyinstallerArtifactMcp.test.ts | 5 +- tests/boundary/mcp/pythonPycMcp.test.ts | 9 +- tests/boundary/mcp/rawBinaryMcp.test.ts | 6 +- tests/boundary/mcp/recordedCrashMcp.test.ts | 7 +- tests/boundary/mcp/releaseExposureMcp.test.ts | 5 +- .../retainedApplicationEvidenceMcp.test.ts | 11 +- .../mcp/sameFunctionObservation.test.ts | 8 +- tests/boundary/mcp/serverIdentity.test.ts | 70 +- tests/boundary/mcp/serverWorkflowMcp.test.ts | 10 +- tests/boundary/mcp/signalRecordingMcp.test.ts | 10 +- .../mcp/sourcePublicationContext.test.ts | 12 +- .../mcp/sourcePublicationRecovery.test.ts | 2 +- .../mcp/sourceToBundleViewsMcp.test.ts | 13 +- .../mcp/stdioFrameDiagnostics.test.ts | 4 +- tests/boundary/mcp/streamableHttpMcp.test.ts | 3 +- .../mcp/toolErrorSchemaDelivery.test.ts | 5 +- tests/boundary/mcp/toolFamiliesMcp.test.ts | 2 +- .../mcp/toolInputCapacityHttp.test.ts | 2 +- .../mcp/traceFeatureSeedGuidanceMcp.test.ts | 4 +- tests/boundary/mcp/uImageMcp.test.ts | 2 +- tests/boundary/mcp/unityFsMcp.test.ts | 2 +- tests/boundary/mcp/universalSliceMcp.test.ts | 7 +- tests/boundary/mcp/wasmArtifactMcp.test.ts | 9 +- tests/boundary/mcp/webBundleViewsMcp.test.ts | 2 +- tests/boundary/mcp/webModuleTraceMcp.test.ts | 5 +- .../boundary/mcp/webNetworkCaptureMcp.test.ts | 7 +- tests/boundary/mcp/webRuntimeMcp.test.ts | 9 +- .../boundary/mcp/webSourceLocationMcp.test.ts | 5 +- .../mcp/workflowUnknownPublication.test.ts | 2 +- tests/boundary/mcp/wxapkgMcp.test.ts | 2 +- .../process/androidAnalysisMcp.test.ts | 5 +- .../process/androidMappingMcp.test.ts | 13 +- .../process/artifactMcpVerifierOutput.test.ts | 17 +- .../process/processSessionMcp.test.ts | 5 +- .../process/releaseArtifactCanary.test.ts | 3 +- .../kunaDirectoryIndependentRetryMcp.test.ts | 2 +- .../kuna/kunaIndependentDecompileMcp.test.ts | 2 +- .../contracts/mcpInputSchemaProfile.test.ts | 39 +- .../contracts/mcpOutputSchemaProfile.test.ts | 40 +- tests/fixtures/advertisedMcpTool.ts | 21 +- tests/fixtures/artifactMcpVerifierProbe.mjs | 30 +- tests/fixtures/buildComputerMcp.ts | 1 - tests/fixtures/canonicalOutputSchema.ts | 43 + tests/fixtures/localToolsMcp.ts | 18 +- .../processCaptureFilenameBytes.test.ts | 9 +- .../analysis-efficiency/android-jni.mjs | 15 +- .../analysis-efficiency/android-mapping.mjs | 13 +- .../analysis-efficiency/catalog-eval.py | 11 +- .../selected-output-contracts.mjs | 27 + .../support/reverseDebuggerMcpTestHelpers.ts | 5 +- vitest.config.ts | 4 - 304 files changed, 2780 insertions(+), 4523 deletions(-) create mode 100644 scripts/lib/canonical-output-schema.d.mts create mode 100644 scripts/lib/canonical-output-schema.mjs delete mode 100644 src/contracts/advertisedDialectBaseline.ts create mode 100644 src/domain/schemaAnnotations.test.ts create mode 100644 src/domain/schemaAnnotations.ts create mode 100644 src/domain/schemaConstraints.test.ts create mode 100644 src/domain/schemaConstraints.ts create mode 100644 tests/fixtures/canonicalOutputSchema.ts create mode 100644 tests/support/analysis-efficiency/selected-output-contracts.mjs diff --git a/README.md b/README.md index 013e80836a..5ea33378b6 100644 --- a/README.md +++ b/README.md @@ -96,17 +96,15 @@ Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build and manual MCP registration, Claude Code/Codex plugins, and the Cursor Marketplace plugin. -MCP uses compact input schemas by default to reduce context overhead. Set -`REA_MCP_INPUT_SCHEMA_PROFILE=full` in your MCP registration when you need the -complete advertised field guidance and nested validation details. Both profiles -keep the full tool set and canonical server validation. See -"Compact input schema profile" in the MCP contracts guide. +MCP advertises compact input schemas to reduce context overhead; guidance a +compact schema omits follows the tool description. The full tool set and +canonical server validation are unchanged. See "Compact input schema profile" +in the MCP contracts guide. Output schemas are omitted from `tools/list` by default: no model API reads them, and REA validates every result itself. Set `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` to advertise each output schema within -a 4,096-byte budget, or `full` for complete output schemas (for MCP clients -that validate results). All settings keep canonical server-side validation and +a 4,096-byte budget. Both settings keep canonical server-side validation and complete structured results. See "Output schemas" in the MCP contracts guide. ### Ask your agent diff --git a/docs/development/client-verification.md b/docs/development/client-verification.md index da7a232ccb..946dfba896 100644 --- a/docs/development/client-verification.md +++ b/docs/development/client-verification.md @@ -30,12 +30,10 @@ declaration renderers rendered the full `compare_web_captures` input as `args: unknown`: its generated TypeScript type exceeds Pi's 16,000-character default. REA keeps the full advertisement's fields by inlining references when the shared form would exceed Pi's 32-reference expansion limit; retaining that -shared form would leave later fields unknown instead. The existing -`REA_MCP_INPUT_SCHEMA_PROFILE=compact` profile lets callers stay below -its character limit by advertising reduced nested validation structure, while -REA continues to validate calls against the complete canonical input schema. -Compact is the default for the server's advertised inputs; an explicit `full` -choice is preserved. See the [compact input schema profile](../reference/mcp-contracts.md#compact-input-schema-profile) +shared form would leave later fields unknown instead. The compact +input presentation keeps advertised inputs below its character limit by +reducing nested validation structure, while REA continues to validate calls +against the complete canonical input schema. See the [compact input schema profile](../reference/mcp-contracts.md#compact-input-schema-profile) for details of its reduced forms. Custom renderer hosts can raise `inputMaxChars` through `renderToolSignature` or @@ -208,39 +206,30 @@ ID). Every actual HTTP request's model is checked and recorded separately. Both `HOME` and `USERPROFILE` point to the disposable account; native `skill add` still selects the installed skill directory explicitly. -The same native client passed full-profile chat and analysis with the actual -wire model `gpt-4.1` under a distinct BYOK model ID: +The same native client passed chat and analysis under REA's former full input +profile with the actual wire model `gpt-4.1` under a distinct BYOK model ID: ```bash REA_VERIFY_COPILOT_MODEL=rea-gpt-4.1 REA_VERIFY_COPILOT_WIRE_MODEL=gpt-4.1 npm run verify:copilot-client -- chat REA_VERIFY_COPILOT_MODEL=rea-gpt-4.1 REA_VERIFY_COPILOT_WIRE_MODEL=gpt-4.1 npm run verify:copilot-client -- call ``` -These runs retain full canonical input/output schemas, all tools, installed-skill -loading and complete Evidence readback. They use the fixture's requested -one-million-token BYOK prompt setting; effective capacity and live API acceptance -remain unknown. With the built-in `gpt-4.1` model ID, the verified client still -blocks the full input-schema profile before HTTP with -`compaction_static_context_blocked`, even when requesting a larger BYOK prompt -capacity. The effective capacity is unknown. The default verifier preserves -the complete catalog and full schemas. To verify the existing compact profile: +Those runs used the fixture's requested one-million-token BYOK prompt setting; +effective capacity and live API acceptance remain unknown. With the built-in +`gpt-4.1` model ID, the verified client blocked the former full input-schema +profile before HTTP with `compaction_static_context_blocked`. That profile was +removed, so the verifier now compares the input schemas forwarded by Copilot +with REA's compact advertisement: ```bash -REA_VERIFY_COPILOT_MODEL=gpt-4.1 REA_VERIFY_COPILOT_SCHEMA_PROFILE=compact npm run verify:copilot-client -- chat -REA_VERIFY_COPILOT_MODEL=gpt-4.1 REA_VERIFY_COPILOT_SCHEMA_PROFILE=compact npm run verify:copilot-client -- call +REA_VERIFY_COPILOT_MODEL=gpt-4.1 npm run verify:copilot-client -- chat +REA_VERIFY_COPILOT_MODEL=gpt-4.1 npm run verify:copilot-client -- call ``` -The verifier explicitly writes either selected profile into its disposable -registration and compares the input schemas forwarded by Copilot with that -profile's advertised schemas. A run must not infer `full` from an unset setting: -REA's runtime default is now `compact`. The recorded client results above remain -scoped to their executed revisions; this verifier correction is not a new client run. - -Both native workflows passed with the complete tool inventory. The verifier selects the -profile only in its disposable Copilot registration and records it in the -receipt. Compact advertisements can reduce nested validation structure; the -server still validates complete canonical inputs. This is a tested alternate -workflow, not a fix for the full-profile client/model limit tracked in #1554. +The recorded client results above remain scoped to their executed revisions; +this verifier change is not a new client run. Compact native workflows passed +with the complete tool inventory. Compact advertisements can reduce nested +validation structure; the server still validates complete canonical inputs. Live model APIs, native Windows and default OS-home skill discovery remain unverified. diff --git a/docs/development/testing.md b/docs/development/testing.md index 3a8c6ade65..acc0004589 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -111,9 +111,9 @@ API dependency, and independent fixture compiler oracles keep their existing TypeScript versions. `npm run build:cached` compiles the CLI/MCP runtime and its packaged skill. -The build also records `dist/mcp-catalog.json` (compact inputs) and -`dist/mcp-catalog-full.json` from the canonical SDK registrations and binds -each to the runtime with its own SHA-256 digest. These are +The build also records `dist/mcp-catalog.json` (compact inputs) from the +canonical SDK registrations and binds it to the runtime with its SHA-256 +digest. These are runtime artifacts, separate from the test catalog below. Default target-free MCP startup serves its schemas without constructing analysis validators; the first tool or guided-workflow request materializes the canonical runtime @@ -294,12 +294,10 @@ profile. Meta-schema validity does not establish client acceptance or useful rendered declarations. `src/contracts/advertisedDialect.ts` owns the portable advertised dialect: the keyword allowlist, size and depth limits, and the regex grammar that compiles in V8, Python, RE2 and Rust. `advertisedDialect.test.ts` -lists every input, output and catalog profile combination and compares the -violation counts with `advertisedDialectBaseline.ts`, which may only shrink, -except when a runtime analysis setting adds its `update_analysis_settings` -branch, or a fix restores a contract declaration the presentation had lost and -the PR names it; the packaged catalog check applies the same rules to the -default profiles. +lists every output and catalog profile combination and requires zero dialect +violations; there is no baseline, so a construct outside the dialect fails the +test by name. The packaged catalog check applies the same rule to the default +profiles. Advertised patterns, input and output, are enforced at zero: `portablePattern.ts` re-emits each one exactly, widens it, or omits it, and the presentation drops any enclosing `not` or `if` and turns `oneOf` into `anyOf` @@ -760,12 +758,14 @@ to reach their original memory, depth, cycle or representation boundary; retain those stronger assertions. When a matrix already holds a named tool definition from live `tools/list`, -pass that definition through the SDK's `toolDefinition` call option. This keeps -the selected output-schema validation without eagerly compiling unrelated -catalog schemas. Re-read definitions if the server advertises a changed list. +pass that definition through the SDK's `toolDefinition` call option, with the +canonical output schema attached (`tools/list` omits output schemas by default). +This keeps output-schema validation without eagerly compiling unrelated catalog +schemas. Re-read definitions if the server advertises a changed list. -For exact nested input constraints, select the full profile explicitly or use -the full-SDK conformance suite that owns the field. Compact workflow tests +For exact nested input constraints, assert the canonical input schema +(`advertisedInputJsonSchema`) or use the full-SDK conformance suite that owns +the field. Compact workflow tests should assert canonical failure categories and provider-call controls where rejection before provider work matters. Do not make today's omission of a constraint a compact-profile invariant. diff --git a/docs/getting-started/client-configuration.md b/docs/getting-started/client-configuration.md index 3485ec60f6..6bfc1f0fff 100644 --- a/docs/getting-started/client-configuration.md +++ b/docs/getting-started/client-configuration.md @@ -62,26 +62,26 @@ the registration's file does not carry as a ## Setting launch variables in PowerShell -Shell examples that prefix a command with `REA_MCP_INPUT_SCHEMA_PROFILE=compact` +Shell examples that prefix a command with `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` use POSIX shell syntax. In PowerShell, set the process environment before launching REA: ```powershell -$env:REA_MCP_INPUT_SCHEMA_PROFILE = "compact" +$env:REA_MCP_OUTPUT_SCHEMA_PROFILE = "compact" rea mcp doctor --json ``` The assignment applies to the current PowerShell process and commands it launches. Restore the previous value when finished, or use -`Remove-Item Env:REA_MCP_INPUT_SCHEMA_PROFILE` if it was previously unset. It does +`Remove-Item Env:REA_MCP_OUTPUT_SCHEMA_PROFILE` if it was previously unset. It does not update an already running MCP server or desktop client. -To select this profile for an agent, set `REA_MCP_INPUT_SCHEMA_PROFILE` in that +To select this profile for an agent, set `REA_MCP_OUTPUT_SCHEMA_PROFILE` in that client's existing REA registration `env` object (`environment` for OpenCode) and restart the client, preserving its other settings. A successful terminal doctor run does not verify the client's launch environment. See [launch-environment verification](../development/client-verification.md#launch-environment-verification) and the client-specific instructions below. REA setup forwards supported selections, -including `REA_MCP_INPUT_SCHEMA_PROFILE`, from its environment into the registration; +including `REA_MCP_OUTPUT_SCHEMA_PROFILE`, from its environment into the registration; it does not copy unrelated environment values. Restoring the shell's previous value does not undo a selection already saved by setup. @@ -179,7 +179,6 @@ during `tools/list`, merge these settings into the **REA server process's** environment, then reconnect: ```text -REA_MCP_INPUT_SCHEMA_PROFILE=compact REA_MCP_OUTPUT_SCHEMA_PROFILE=compact REA_MCP_TOOL_CATALOG_PROFILE=full ``` @@ -232,23 +231,20 @@ aligned configuration; doctor does not widen it to expose every REA tool. See [Copilot's MCP configuration](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers) for tool-selection settings. -For GitHub Copilot CLI 1.0.95, `gpt-4.1` model metadata can block ordinary -chat with REA's full input-schema profile before sending a model request +For GitHub Copilot CLI 1.0.95, `gpt-4.1` model metadata blocked ordinary +chat with REA's former full input-schema profile before sending a model request (`compaction_static_context_blocked`; [#1554](https://github.com/morluto/rea/issues/1554)). -For the native offline OpenAI-compatible BYOK adapter, full-profile chat and -analysis also passed with a distinct `COPILOT_PROVIDER_MODEL_ID` such as +That profile was removed. For the native offline OpenAI-compatible BYOK adapter, +chat and analysis with it also passed with a distinct `COPILOT_PROVIDER_MODEL_ID` such as `rea-gpt-4.1`, `COPILOT_PROVIDER_WIRE_MODEL=gpt-4.1`, and that distinct ID selected through `copilot --model rea-gpt-4.1`. The local fixture requested `COPILOT_PROVIDER_MAX_PROMPT_TOKENS=1000000`; configure the limit supported by your backend. This keeps every full schema and the actual wire model intact. It does not establish the effective client budget or fix the built-in-ID path. -The existing compact profile passed native chat and analysis with the complete -tool inventory on Linux using a loopback model fixture. After setup, add -`"REA_MCP_INPUT_SCHEMA_PROFILE": "compact"` to the `env` object of the -`mcpServers.rea` entry in Copilot's `mcp-config.json`, preserving its other -settings, then restart the client. Compact advertisements can reduce nested -schema constraints; REA still validates complete canonical inputs. Full-profile -`gpt-5.4` metadata is also verified locally. Live model API acceptance and +The compact input schemas passed native chat and analysis with the complete +tool inventory on Linux using a loopback model fixture. Compact advertisements +can reduce nested schema constraints; REA still validates complete canonical +inputs. Live model API acceptance and Windows remain unverified; see [native client verification](../development/testing.md). ## Claude Desktop @@ -354,11 +350,6 @@ and Claude Code reports that autocompact is thrashing. To avoid this: to restore deferred loading. DeepSeek's Anthropic API guide does not list `tool_reference` among its supported content blocks, so this setting may cause its requests to fail. -- To shrink the upfront catalog, add - `"REA_MCP_INPUT_SCHEMA_PROFILE": "compact"` to the `env` object of the - `mcpServers.rea` entry in `~/.claude.json` (`$CLAUDE_CONFIG_DIR/.claude.json` - when that variable is set), then restart Claude Code. Setup keeps this - override. - To remove entire tool families from the upfront catalog, run `rea setup --client claude_code --tool-families ` with the families the workflow needs (see [the advertising scope contract](../reference/mcp-contracts.md#tool-family-advertising-scope)). @@ -372,13 +363,9 @@ The sizes above come from REA's own `tools/list`. A live Claude Code session against DeepSeek's API is unverified. Output schemas are omitted by default (`"REA_MCP_OUTPUT_SCHEMA_PROFILE": "off"`). -If you selected `"full"` and a client's stdio reader rejects the complete -catalog frame, return to the default or select `"compact"`, which bounds every -advertised output schema, in REA's `env` object, then restart the client. -Every profile keeps canonical result validation and complete structured -results; `compact` keeps a widened root shape and `off` advertises nothing. On catalogs large -enough that full input schemas alone approach the frame limit, also keep the -input profile at its `compact` default. See [MCP output schema +`"compact"` bounds every advertised output schema. Both profiles keep canonical +result validation and complete structured results; `compact` keeps a widened +root shape and `off` advertises nothing. See [MCP output schema profiles](../reference/mcp-contracts.md#output-schemas). ## Kimi Code @@ -429,12 +416,10 @@ client environment overrides remain intact unless setup supplies the same key. You can edit `startupTimeoutMs` and `toolTimeoutMs` there; doctor keeps positive caller-selected values aligned and reports a non-blocking advisory when a tool timeout is shorter than the known provider allowance. -The server advertises compact input schemas by default, which avoids a Moonshot -Kimi Code 2.2.0 failure observed with the full catalog: its request was rejected +The server advertises compact input schemas, which avoids a Moonshot Kimi Code +2.2.0 failure observed with uncompacted schemas: its request was rejected because a single tool schema exceeded the provider's size limit. The limit is -provider-specific and not an MCP protocol limit. Setup preserves an explicit -`REA_MCP_INPUT_SCHEMA_PROFILE` from the stored registration or the setup -environment; an explicit `full` profile can still exceed Moonshot's limit. +provider-specific and not an MCP protocol limit. Kimi Code also discovers personal skills in `~/.agents/skills` and project-local `.agents/skills`, so no separate skill root is required; setup still installs and verifies the matching skill files through the normal shared-skill flow. @@ -450,8 +435,7 @@ the pointer file. LM Studio parses `mcp.json` as strict JSON, so setup rejects a file with comments, trailing commas, or a byte order mark without a backup or a write. The entry carries `command`, `args`, and `env`, and setup keeps every other server. Local models often have small context windows; the default -compact input-schema profile applies unless the stored entry or the setup -environment selects `REA_MCP_INPUT_SCHEMA_PROFILE` explicitly. LM Studio 0.4.26 on macOS loaded +compact input schemas keep each advertised tool within a bounded size. LM Studio 0.4.26 on macOS loaded this registration, and a local model called an REA tool through it. Linux and Windows remain unverified. @@ -640,8 +624,8 @@ workload needs more time. Devin honors `XDG_CONFIG_HOME` for its An older REA input containing acyclic local references was rejected by a Responses-compatible Kimi gateway as “infinite recursion”; expanding those -references succeeded. A separate full-profile input failed the provider's -schema-size limit. These are distinct reported failures, not proof that the +references succeeded. A separate input under REA's former full input profile +failed the provider's schema-size limit. These are distinct reported failures, not proof that the canonical schema is recursive. Current REA inputs are already self-contained; see [schema advertisement](../reference/mcp-contracts.md#schema-advertisement). @@ -649,17 +633,10 @@ For this failure, inspect the active MCP launch command and refresh the REA installation that command actually runs. A configured package selector or the installed skill version alone does not identify a cached or still-running server. Follow [installation and update guidance](installation.md#diagnose-update-and-remove). -Then merge this setting into the REA entry in `$CODEX_HOME/config.toml` -(`~/.codex/config.toml` by default), preserving its launcher and other settings: - -```toml -[mcp_servers.rea.env] -REA_MCP_INPUT_SCHEMA_PROFILE = "compact" -``` - -If that table already exists, add or update the key there. Restart/reconnect -Codex so REA reads the new environment. Compact is now REA's default, but an -explicit `full` override remains effective until changed. It preserves the +REA advertises compact input schemas. A registration that still sets +`REA_MCP_INPUT_SCHEMA_PROFILE = "full"` in `$CODEX_HOME/config.toml` +(`~/.codex/config.toml` by default) fails at startup, so remove that key and +restart/reconnect Codex. Compact advertisement preserves the tool inventory and canonical server validation while reducing advertised input detail. The probes in [#5160](https://github.com/morluto/rea/issues/5160) covered individual tools through one gateway, not the entire current catalog or a diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 53a7cb4114..b119a7af6d 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -98,7 +98,7 @@ approve replacing it. A different existing installation path is foreign to the current entrypoint; a path that cannot be resolved remains unknown. Cordis uses its stable REA entry ID instead of launcher identity. Setup also retains valid environment overrides in REA's existing server entry, -such as `REA_MCP_INPUT_SCHEMA_PROFILE`, `REA_MCP_OUTPUT_SCHEMA_PROFILE`, and +such as `REA_MCP_OUTPUT_SCHEMA_PROFILE`, `REA_MCP_MAX_RESPONSE_BYTES`, and `REA_MCP_TOOL_FAMILIES`. Detected provider settings replace only matching environment keys. Setup manages REA's launcher, selected provider settings, and recommended timeout @@ -352,7 +352,7 @@ remove it with `claude mcp remove --scope local rea` from the same directory. `rea update` doesn't refresh it either, so it continues using the selected installation. To move it to a newer release, first back up the project entry in `~/.claude.json` and record its environment and other metadata. In particular, -preserve caller-selected overrides such as `REA_MCP_INPUT_SCHEMA_PROFILE` and +preserve caller-selected overrides such as `REA_MCP_OUTPUT_SCHEMA_PROFILE` and `REA_MCP_MAX_RESPONSE_BYTES`, and `REA_MCP_TOOL_FAMILIES`. Run `claude mcp remove --scope local rea` from the same directory, then recreate it with the platform-appropriate command above and the new version. Reapply every recorded environment value with Claude diff --git a/docs/mcp-catalog-evaluation.md b/docs/mcp-catalog-evaluation.md index 1e2713a8f1..c47c59a190 100644 --- a/docs/mcp-catalog-evaluation.md +++ b/docs/mcp-catalog-evaluation.md @@ -63,10 +63,6 @@ invalidate that profile even when the model still emits the expected marker. Useful controls: ```sh -# Compare full input guidance with the default compact input presentation. -npm run eval:mcp-catalog -- --model gpt-6.1-sol \ - --output "$HOME/.cache/rea-catalog-full" --input-profile full --repetitions 3 - # Choose exact tool names in the working host mode. npm run eval:mcp-catalog -- --model gpt-6.1-sol \ --output "$HOME/.cache/rea-catalog-selected" --modes client-code \ diff --git a/docs/readme/README_ar.md b/docs/readme/README_ar.md index c315c54d7c..c430d07432 100644 --- a/docs/readme/README_ar.md +++ b/docs/readme/README_ar.md @@ -82,11 +82,11 @@ npx rea-agents setup يدعم الإعداد Claude Code وCodex وCursor وGemini CLI وGrok Build و[وكلاء آخرين](../getting-started/installation.md#supported-agents). راجع [التثبيت والإعداد](../getting-started/installation.md) لتهيئة موفّري التحليل وتسجيل MCP يدويًا. -يستخدم MCP مخططات إدخال مختصرة افتراضيًا. للحصول على إرشادات الحقول الكاملة وتفاصيل التحقق المتداخل، اضبط `REA_MCP_INPUT_SCHEMA_PROFILE=full` في تسجيل MCP. يحتفظ الخياران بجميع الأدوات والتحقق القياسي نفسه على الخادم؛ راجع دليل عقود MCP. +يستخدم MCP مخططات إدخال مختصرة افتراضيًا. تنتقل الإرشادات التي يحذفها المخطط المختصر إلى وصف الأداة. تبقى جميع الأدوات والتحقق القياسي على الخادم دون تغيير؛ راجع دليل عقود MCP. يسجّل الإعداد بيئة التشغيل المحددة وملف دخول REA مباشرةً. عند استخدام مشغّل حزم، احتفظ بإدخال ذاكرة التخزين المؤقت وبيئة التشغيل؛ ونفّذ تنزيل الحزمة من دليل موثوق خارج المشروع المستهدف. راجع دليل التثبيت والإعداد. -تُحذف مخططات الإخراج من `tools/list` افتراضيًا: لا تقرؤها أي واجهة برمجة لنموذج، ويتحقق REA من كل نتيجة بنفسه. اضبط `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` في تسجيل MCP لإعلان كل مخطط إخراج ضمن حد 4096 بايت، أو `full` لمخططات الإخراج الكاملة (لعملاء MCP الذين يتحققون من النتائج)؛ يظل التحقق والخروج المنظّم كاملين على الخادم في كل الإعدادات. راجع قسم مخططات الإخراج في دليل عقود MCP. +تُحذف مخططات الإخراج من `tools/list` افتراضيًا: لا تقرؤها أي واجهة برمجة لنموذج، ويتحقق REA من كل نتيجة بنفسه. اضبط `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` في تسجيل MCP لإعلان كل مخطط إخراج ضمن حد 4096 بايت؛ يظل التحقق والخروج المنظّم كاملين على الخادم في كل الإعدادات. راجع قسم مخططات الإخراج في دليل عقود MCP. ### اطلب من وكيلك diff --git a/docs/readme/README_bg.md b/docs/readme/README_bg.md index 7de1a00e03..81b60e90e1 100644 --- a/docs/readme/README_bg.md +++ b/docs/readme/README_bg.md @@ -99,18 +99,16 @@ runner), не изтривайте записа му в кеша. Изтегля доставчици на анализ, ръчна регистрация на MCP и инсталиране на плъгина за Claude Code/Codex. -По подразбиране MCP използва компактни входни схеми, за да заема по-малко от -контекста. Задайте `REA_MCP_INPUT_SCHEMA_PROFILE=full` в MCP регистрацията си, -когато са ви нужни пълните описания на полетата и подробностите за вложената -валидация. И двата профила запазват пълния набор от инструменти и каноничната -валидация на сървъра. Вижте „Compact input schema profile“ в ръководството за MCP +MCP обявява компактни входни схеми, за да заема по-малко от контекста; +указанията, които компактната схема пропуска, следват описанието на +инструмента. Пълният набор от инструменти и каноничната валидация на сървъра +остават непроменени. Вижте „Compact input schema profile“ в ръководството за MCP договорите. По подразбиране изходните схеми се пропускат от `tools/list`: никой API на модел не ги чете, а REA сам валидира всеки резултат. Задайте `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`, за да обявите всяка изходна схема в -рамките на 4096 байта, или `full` за пълни изходни схеми (за MCP клиенти, които -валидират резултатите). Всички настройки запазват каноничната валидация от +рамките на 4096 байта. Всички настройки запазват каноничната валидация от страна на сървъра и пълните структурирани резултати. Вижте „Output schemas“ в ръководството за MCP договорите. diff --git a/docs/readme/README_de.md b/docs/readme/README_de.md index aa792cc4b4..dd034238c8 100644 --- a/docs/readme/README_de.md +++ b/docs/readme/README_de.md @@ -82,11 +82,11 @@ Wähle deine Agenten aus, prüfe die geplanten Änderungen und bestätige sie. D Die Einrichtung unterstützt Claude Code, Codex, Cursor, Gemini CLI, Grok Build und [weitere Agenten](../getting-started/installation.md#supported-agents). Informationen zur Provider-Konfiguration und manuellen MCP-Registrierung findest du unter [Installation und Einrichtung](../getting-started/installation.md). -MCP verwendet standardmäßig kompakte Eingabeschemas. Für vollständige Feldhinweise und Details zur verschachtelten Validierung setze `REA_MCP_INPUT_SCHEMA_PROFILE=full` in der MCP-Registrierung. Beide Profile behalten alle Tools und dieselbe kanonische Servervalidierung; siehe MCP-Vertragsleitfaden. +MCP verwendet standardmäßig kompakte Eingabeschemas. Hinweise, die ein kompaktes Schema auslässt, folgen der Tool-Beschreibung. Alle Tools und die kanonische Servervalidierung bleiben unverändert; siehe MCP-Vertragsleitfaden. Die Einrichtung registriert die gewählte Laufzeit und das REA-Einstiegsskript direkt. Bewahre bei einem Paket-Runner dessen Cache-Eintrag und die Laufzeit auf; lade das Paket aus einem vertrauenswürdigen Verzeichnis außerhalb des Zielprojekts. Siehe die Installationsanleitung. -Ausgabeschemas werden standardmäßig nicht in `tools/list` angekündigt: Keine Modell-API liest sie, und REA validiert jedes Ergebnis selbst. Setze in der MCP-Registrierung `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`, um jedes Ausgabeschema innerhalb von 4096 Byte anzukündigen, oder `full` für vollständige Ausgabeschemas (für MCP-Clients, die Ergebnisse validieren). Servervalidierung und strukturierte Ergebnisse bleiben in jeder Einstellung vollständig. Siehe „Output schemas“ im MCP-Vertragsleitfaden. +Ausgabeschemas werden standardmäßig nicht in `tools/list` angekündigt: Keine Modell-API liest sie, und REA validiert jedes Ergebnis selbst. Setze in der MCP-Registrierung `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`, um jedes Ausgabeschema innerhalb von 4096 Byte anzukündigen. Servervalidierung und strukturierte Ergebnisse bleiben in jeder Einstellung vollständig. Siehe „Output schemas“ im MCP-Vertragsleitfaden. ### Deinen Agenten fragen diff --git a/docs/readme/README_el.md b/docs/readme/README_el.md index cf95fd20d1..6d2621a2f5 100644 --- a/docs/readme/README_el.md +++ b/docs/readme/README_el.md @@ -84,11 +84,11 @@ npx rea-agents setup Υποστηρίζονται τα Claude Code, Codex, Cursor, Gemini CLI, Grok Build και [άλλοι agents](../getting-started/installation.md#supported-agents). Για τη ρύθμιση των παρόχων ανάλυσης και τη μη αυτόματη καταχώριση MCP, δες τον οδηγό [εγκατάστασης και ρύθμισης](../getting-started/installation.md). -Το MCP χρησιμοποιεί συμπαγή σχήματα εισόδου από προεπιλογή. Για πλήρεις οδηγίες πεδίων και ένθετης επικύρωσης, όρισε `REA_MCP_INPUT_SCHEMA_PROFILE=full` στην καταχώριση MCP. Και τα δύο προφίλ διατηρούν όλα τα εργαλεία και την ίδια κανονική επικύρωση διακομιστή· δες τον οδηγό συμβολαίων MCP. +Το MCP χρησιμοποιεί συμπαγή σχήματα εισόδου από προεπιλογή. Οι οδηγίες που παραλείπει ένα συμπαγές σχήμα ακολουθούν την περιγραφή του εργαλείου. Όλα τα εργαλεία και η κανονική επικύρωση διακομιστή παραμένουν ίδια· δες τον οδηγό συμβολαίων MCP. Η εγκατάσταση καταχωρίζει απευθείας τον επιλεγμένο runtime και το αρχικό script του REA. Αν χρησιμοποιείς package runner, διατήρησε την καταχώριση cache και τον runtime· απόκτησε το πακέτο από έμπιστο κατάλογο έξω από το έργο-στόχο. Δες τον οδηγό εγκατάστασης. -Τα σχήματα εξόδου παραλείπονται από το `tools/list` από προεπιλογή: κανένα API μοντέλου δεν τα διαβάζει και το REA επικυρώνει μόνο του κάθε αποτέλεσμα. Όρισε `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` στην καταχώριση MCP για να ανακοινώνεται κάθε σχήμα εξόδου έως 4096 byte, ή `full` για πλήρη σχήματα εξόδου (για clients MCP που επικυρώνουν αποτελέσματα). Η επικύρωση του διακομιστή και τα δομημένα αποτελέσματα παραμένουν πλήρη σε κάθε ρύθμιση. Δες το «Output schemas» στον οδηγό συμβάσεων MCP. +Τα σχήματα εξόδου παραλείπονται από το `tools/list` από προεπιλογή: κανένα API μοντέλου δεν τα διαβάζει και το REA επικυρώνει μόνο του κάθε αποτέλεσμα. Όρισε `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` στην καταχώριση MCP για να ανακοινώνεται κάθε σχήμα εξόδου έως 4096 byte. Η επικύρωση του διακομιστή και τα δομημένα αποτελέσματα παραμένουν πλήρη σε κάθε ρύθμιση. Δες το «Output schemas» στον οδηγό συμβάσεων MCP. ### Ζήτησε από τον agent σου diff --git a/docs/readme/README_es.md b/docs/readme/README_es.md index c15b4e5267..f71a667390 100644 --- a/docs/readme/README_es.md +++ b/docs/readme/README_es.md @@ -82,11 +82,11 @@ Elige tus agentes, revisa los cambios propuestos y apruébalos. La configuració La configuración admite Claude Code, Codex, Cursor, Gemini CLI, Grok Build y [otros agentes](../getting-started/installation.md#supported-agents). Consulta [instalación y configuración](../getting-started/installation.md) para configurar proveedores y registrar MCP manualmente. -MCP usa esquemas de entrada compactos por defecto. Para ver todas las instrucciones de campos y la validación anidada, establece `REA_MCP_INPUT_SCHEMA_PROFILE=full` en el registro MCP. Ambos perfiles conservan todas las herramientas y la misma validación canónica del servidor; consulta la guía de contratos MCP. +MCP usa esquemas de entrada compactos por defecto. Las indicaciones que omite un esquema compacto siguen a la descripción de la herramienta. Todas las herramientas y la validación canónica del servidor no cambian; consulta la guía de contratos MCP. La configuración registra directamente el entorno de ejecución elegido y el script de entrada de REA. Si usas un ejecutor de paquetes, conserva su entrada de caché y el entorno de ejecución; descarga el paquete desde un directorio de confianza fuera del proyecto objetivo. Consulta la guía de instalación. -Los esquemas de salida se omiten de `tools/list` por defecto: ninguna API de modelos los lee y REA valida cada resultado por sí mismo. Establece `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` en el registro MCP para anunciar cada esquema de salida dentro de 4096 bytes, o `full` para los esquemas de salida completos (para clientes MCP que validan resultados). En todos los casos, la validación del servidor y los resultados estructurados siguen siendo completos. Consulta «Output schemas» en la guía de contratos MCP. +Los esquemas de salida se omiten de `tools/list` por defecto: ninguna API de modelos los lee y REA valida cada resultado por sí mismo. Establece `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` en el registro MCP para anunciar cada esquema de salida dentro de 4096 bytes. En todos los casos, la validación del servidor y los resultados estructurados siguen siendo completos. Consulta «Output schemas» en la guía de contratos MCP. ### Pregunta a tu agente diff --git a/docs/readme/README_fa.md b/docs/readme/README_fa.md index 3e8842ab18..4449ab3c84 100644 --- a/docs/readme/README_fa.md +++ b/docs/readme/README_fa.md @@ -74,11 +74,11 @@ npx rea-agents setup راه‌اندازی از Claude Code، Codex، Cursor، Gemini CLI، Grok Build و [ایجنت‌های دیگر](../getting-started/installation.md#supported-agents) پشتیبانی می‌کند. برای پیکربندی ارائه‌دهنده و ثبت دستی MCP، [نصب و راه‌اندازی](../getting-started/installation.md) را ببینید. -MCP به‌طور پیش‌فرض از schema ورودی فشرده استفاده می‌کند. برای راهنمای کامل فیلدها و اعتبارسنجی تو‌در‌تو، در ثبت MCP مقدار `REA_MCP_INPUT_SCHEMA_PROFILE=full` را تنظیم کنید. هر دو حالت همهٔ ابزارها و اعتبارسنجی استاندارد یکسان سرور را نگه می‌دارند؛ راهنمای قراردادهای MCP را ببینید. +MCP به‌طور پیش‌فرض از schema ورودی فشرده استفاده می‌کند. راهنمایی‌هایی که schema فشرده حذف می‌کند پس از توضیح ابزار می‌آیند. همهٔ ابزارها و اعتبارسنجی استاندارد سرور بدون تغییر می‌مانند؛ راهنمای قراردادهای MCP را ببینید. راه‌اندازی، محیط اجرای انتخاب‌شده و اسکریپت ورودی REA را مستقیماً ثبت می‌کند. اگر از اجراکنندهٔ بسته استفاده می‌کنید، ورودی حافظهٔ پنهان و محیط اجرا را حفظ کنید؛ بسته را از پوشه‌ای قابل اعتماد خارج از پروژهٔ هدف دریافت کنید. راهنمای نصب را ببینید. -طرح‌های خروجی به‌طور پیش‌فرض از `tools/list` حذف می‌شوند: هیچ API مدلی آن‌ها را نمی‌خواند و REA خودش هر نتیجه را اعتبارسنجی می‌کند. در ثبت MCP مقدار `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` را برای اعلام هر طرح خروجی در سقف 4096 بایت، یا `full` را برای طرح‌های خروجی کامل (برای کلاینت‌های MCP که نتایج را اعتبارسنجی می‌کنند) تنظیم کنید. در همهٔ تنظیمات، اعتبارسنجی سرور و نتایج ساخت‌یافته کامل می‌مانند. بخش «Output schemas» در راهنمای قراردادهای MCP را ببینید. +طرح‌های خروجی به‌طور پیش‌فرض از `tools/list` حذف می‌شوند: هیچ API مدلی آن‌ها را نمی‌خواند و REA خودش هر نتیجه را اعتبارسنجی می‌کند. در ثبت MCP مقدار `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` را برای اعلام هر طرح خروجی در سقف 4096 بایت تنظیم کنید. در همهٔ تنظیمات، اعتبارسنجی سرور و نتایج ساخت‌یافته کامل می‌مانند. بخش «Output schemas» در راهنمای قراردادهای MCP را ببینید. ### از ایجنت خود بپرسید diff --git a/docs/readme/README_fr.md b/docs/readme/README_fr.md index 9d4abbf0cf..328a3fe581 100644 --- a/docs/readme/README_fr.md +++ b/docs/readme/README_fr.md @@ -82,11 +82,11 @@ Choisissez vos agents, vérifiez les modifications proposées et approuvez-les. Setup prend en charge Claude Code, Codex, Cursor, Gemini CLI, Grok Build et [d'autres agents](../getting-started/installation.md#supported-agents). Consultez [Installation et configuration](../getting-started/installation.md) pour la configuration des fournisseurs et l'enregistrement MCP manuel. -MCP utilise par défaut des schémas d’entrée compacts. Pour les descriptions complètes des champs et de la validation imbriquée, définissez `REA_MCP_INPUT_SCHEMA_PROFILE=full` dans l’enregistrement MCP. Les deux profils conservent tous les outils et la même validation canonique du serveur ; voir le guide des contrats MCP. +MCP utilise par défaut des schémas d’entrée compacts. Les indications qu’un schéma compact omet suivent la description de l’outil. Tous les outils et la validation canonique du serveur restent inchangés ; voir le guide des contrats MCP. La configuration enregistre directement le runtime choisi et le script d’entrée de REA. Avec un exécuteur de paquets, conservez son entrée de cache et le runtime ; récupérez le paquet depuis un répertoire de confiance hors du projet cible. Consultez le guide d’installation. -Les schémas de sortie sont omis de `tools/list` par défaut : aucune API de modèle ne les lit, et REA valide lui-même chaque résultat. Définissez `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` dans l’enregistrement MCP pour annoncer chaque schéma de sortie dans un budget de 4096 octets, ou `full` pour des schémas de sortie complets (pour les clients MCP qui valident les résultats). Dans tous les cas, la validation du serveur et les résultats structurés restent complets. Consultez « Output schemas » dans le guide des contrats MCP. +Les schémas de sortie sont omis de `tools/list` par défaut : aucune API de modèle ne les lit, et REA valide lui-même chaque résultat. Définissez `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` dans l’enregistrement MCP pour annoncer chaque schéma de sortie dans un budget de 4096 octets. Dans tous les cas, la validation du serveur et les résultats structurés restent complets. Consultez « Output schemas » dans le guide des contrats MCP. ### Interroger votre agent diff --git a/docs/readme/README_id.md b/docs/readme/README_id.md index 4532c68207..7da7c60d45 100644 --- a/docs/readme/README_id.md +++ b/docs/readme/README_id.md @@ -82,11 +82,11 @@ Pilih agen yang ingin digunakan, tinjau perubahan yang diusulkan, lalu setujui p Penyiapan mendukung Claude Code, Codex, Cursor, Gemini CLI, Grok Build, dan [agen lainnya](../getting-started/installation.md#supported-agents). Lihat [instalasi dan penyiapan](../getting-started/installation.md) untuk konfigurasi penyedia dan pendaftaran MCP secara manual. -MCP menggunakan skema input ringkas secara default. Untuk petunjuk bidang lengkap dan validasi bertingkat, atur `REA_MCP_INPUT_SCHEMA_PROFILE=full` pada pendaftaran MCP. Kedua profil mempertahankan semua alat dan validasi server kanonis yang sama; lihat panduan kontrak MCP. +MCP menggunakan skema input ringkas secara default. Petunjuk yang dihilangkan skema ringkas mengikuti deskripsi alat. Semua alat dan validasi server kanonis tidak berubah; lihat panduan kontrak MCP. Setup mencatat runtime yang dipilih dan skrip masuk REA secara langsung. Jika memakai package runner, pertahankan entri cache dan runtime tersebut; unduh paket dari direktori tepercaya di luar proyek target. Lihat panduan instalasi. -Skema keluaran tidak disertakan dalam `tools/list` secara default: tidak ada API model yang membacanya, dan REA memvalidasi setiap hasil sendiri. Atur `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` dalam registrasi MCP untuk mengumumkan setiap skema keluaran dalam batas 4096 byte, atau `full` untuk skema keluaran lengkap (bagi klien MCP yang memvalidasi hasil). Di semua pengaturan, validasi server dan hasil terstruktur tetap lengkap. Lihat «Output schemas» di panduan kontrak MCP. +Skema keluaran tidak disertakan dalam `tools/list` secara default: tidak ada API model yang membacanya, dan REA memvalidasi setiap hasil sendiri. Atur `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` dalam registrasi MCP untuk mengumumkan setiap skema keluaran dalam batas 4096 byte. Di semua pengaturan, validasi server dan hasil terstruktur tetap lengkap. Lihat «Output schemas» di panduan kontrak MCP. ### Minta agen Anda diff --git a/docs/readme/README_it.md b/docs/readme/README_it.md index 8ecf123279..3bcb74c464 100644 --- a/docs/readme/README_it.md +++ b/docs/readme/README_it.md @@ -82,11 +82,11 @@ Scegli i tuoi agenti, rivedi le modifiche proposte e approvale. La configurazion La configurazione supporta Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [altri agenti](../getting-started/installation.md#supported-agents). Vedi [installazione e configurazione](../getting-started/installation.md) per la configurazione dei provider e la registrazione manuale MCP. -MCP usa schemi di input compatti per impostazione predefinita. Per tutte le indicazioni sui campi e la validazione annidata, imposta `REA_MCP_INPUT_SCHEMA_PROFILE=full` nella registrazione MCP. Entrambi i profili mantengono tutti gli strumenti e la stessa validazione canonica del server; vedi la guida ai contratti MCP. +MCP usa schemi di input compatti per impostazione predefinita. Le indicazioni omesse da uno schema compatto seguono la descrizione dello strumento. Tutti gli strumenti e la validazione canonica del server restano invariati; vedi la guida ai contratti MCP. La configurazione registra direttamente il runtime scelto e lo script di avvio di REA. Se usi un package runner, conserva la sua voce nella cache e il runtime; scarica il pacchetto da una directory attendibile fuori dal progetto di destinazione. Consulta la guida di installazione. -Gli schemi di output sono omessi da `tools/list` per impostazione predefinita: nessuna API di modelli li legge e REA valida ogni risultato da sé. Imposta `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` nella registrazione MCP per dichiarare ogni schema di output entro 4096 byte, oppure `full` per gli schemi di output completi (per i client MCP che validano i risultati). In ogni caso la validazione del server e i risultati strutturati restano completi. Consulta «Output schemas» nella guida ai contratti MCP. +Gli schemi di output sono omessi da `tools/list` per impostazione predefinita: nessuna API di modelli li legge e REA valida ogni risultato da sé. Imposta `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` nella registrazione MCP per dichiarare ogni schema di output entro 4096 byte. In ogni caso la validazione del server e i risultati strutturati restano completi. Consulta «Output schemas» nella guida ai contratti MCP. ### Chiedi al tuo agente diff --git a/docs/readme/README_ja.md b/docs/readme/README_ja.md index c661c07cb8..8de2fbb890 100644 --- a/docs/readme/README_ja.md +++ b/docs/readme/README_ja.md @@ -82,11 +82,11 @@ npx rea-agents setup セットアップは Claude Code、Codex、Cursor、Gemini CLI、Grok Build、および[その他のエージェント](../getting-started/installation.md#supported-agents)に対応しています。プロバイダーの設定と MCP の手動登録については、[インストールとセットアップ](../getting-started/installation.md)を参照してください。 -MCP は既定でコンパクトな入力スキーマを使用します。完全なフィールド説明と入れ子の検証情報が必要な場合は、MCP 登録に `REA_MCP_INPUT_SCHEMA_PROFILE=full` を設定してください。どちらも全ツールと同じ正規サーバー検証を維持します。詳細は MCP 契約ガイドを参照してください。 +MCP は既定でコンパクトな入力スキーマを使用します。コンパクトなスキーマが省略する説明はツールの説明の後に続きます。全ツールと正規のサーバー検証は変わりません。詳細は MCP 契約ガイドを参照してください。 セットアップは選択したランタイムと REA のエントリースクリプトを直接登録します。パッケージランナーを使う場合は、そのキャッシュ項目とランタイムを残し、対象プロジェクト外の信頼できるディレクトリからパッケージを取得してください。 インストールガイドを参照してください。 -出力スキーマはデフォルトで `tools/list` から省略されます。モデル API は出力スキーマを読まず、REA 自身がすべての結果を検証するためです。MCP 登録で `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` を設定すると各出力スキーマを 4096 バイト以内で公開し、`full` を設定すると完全な出力スキーマを公開します(結果を検証する MCP クライアント向け)。どの設定でもサーバー側の検証と構造化結果は完全なままです。 MCP 契約ガイドの「Output schemas」を参照してください。 +出力スキーマはデフォルトで `tools/list` から省略されます。モデル API は出力スキーマを読まず、REA 自身がすべての結果を検証するためです。MCP 登録で `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` を設定すると各出力スキーマを 4096 バイト以内で公開します。どの設定でもサーバー側の検証と構造化結果は完全なままです。 MCP 契約ガイドの「Output schemas」を参照してください。 ### エージェントに依頼する diff --git a/docs/readme/README_ko.md b/docs/readme/README_ko.md index 0b172e492c..083e572746 100644 --- a/docs/readme/README_ko.md +++ b/docs/readme/README_ko.md @@ -82,11 +82,11 @@ npx rea-agents setup 설정은 Claude Code, Codex, Cursor, Gemini CLI, Grok Build 및 [다른 에이전트](../getting-started/installation.md#supported-agents)를 지원합니다. 제공자 설정과 수동 MCP 등록은 [설치 및 설정](../getting-started/installation.md)을 참고하세요. -MCP는 기본적으로 간결한 입력 스키마를 사용합니다. 전체 필드 안내와 중첩 검증 정보가 필요하면 MCP 등록에서 `REA_MCP_INPUT_SCHEMA_PROFILE=full`을 설정하세요. 두 프로필 모두 전체 도구와 동일한 정규 서버 검증을 유지합니다. MCP 계약 가이드를 참고하세요. +MCP는 기본적으로 간결한 입력 스키마를 사용합니다. 간결한 스키마가 생략한 안내는 도구 설명 뒤에 이어집니다. 전체 도구와 정규 서버 검증은 그대로 유지됩니다. MCP 계약 가이드를 참고하세요. 설정은 선택한 런타임과 REA 진입 스크립트를 직접 등록합니다. 패키지 실행기를 사용한다면 해당 캐시 항목과 런타임을 유지하고, 대상 프로젝트 밖의 신뢰할 수 있는 디렉터리에서 패키지를 받으세요. 설치 가이드를 참고하세요. -기본적으로 출력 스키마는 `tools/list`에서 생략됩니다. 모델 API는 출력 스키마를 읽지 않으며 REA가 모든 결과를 직접 검증합니다. MCP 등록에서 `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`로 설정하면 각 출력 스키마를 4096바이트 이내로 제공하고, `full`로 설정하면 완전한 출력 스키마를 제공합니다(결과를 검증하는 MCP 클라이언트용). 어떤 설정에서도 서버 검증과 구조화된 결과는 완전하게 유지됩니다. MCP 계약 가이드의 «Output schemas»를 참고하세요. +기본적으로 출력 스키마는 `tools/list`에서 생략됩니다. 모델 API는 출력 스키마를 읽지 않으며 REA가 모든 결과를 직접 검증합니다. MCP 등록에서 `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`로 설정하면 각 출력 스키마를 4096바이트 이내로 제공합니다. 어떤 설정에서도 서버 검증과 구조화된 결과는 완전하게 유지됩니다. MCP 계약 가이드의 «Output schemas»를 참고하세요. ### 에이전트에게 요청 diff --git a/docs/readme/README_ml.md b/docs/readme/README_ml.md index b87505ec1a..24639c228a 100644 --- a/docs/readme/README_ml.md +++ b/docs/readme/README_ml.md @@ -84,11 +84,11 @@ npx rea-agents setup Claude Code, Codex, Cursor, Gemini CLI, Grok Build, കൂടാതെ [മറ്റ് agents](../getting-started/installation.md#supported-agents) എന്നിവയെ setup പിന്തുണയ്ക്കുന്നു. provider configuration-നും manual MCP registration-നും [installation and setup](../getting-started/installation.md) കാണുക. -MCP സാധാരണയായി compact input schemas ഉപയോഗിക്കുന്നു. പൂർണ്ണ field guidance-ഉം nested validation details-ഉം വേണമെങ്കിൽ MCP registration-ൽ `REA_MCP_INPUT_SCHEMA_PROFILE=full` സജ്ജമാക്കുക. രണ്ടു profiles-ലും എല്ലാ tools-ഉം ഒരേ canonical server validation-ഉം നിലനിൽക്കും; MCP contracts guide കാണുക. +MCP സാധാരണയായി compact input schemas ഉപയോഗിക്കുന്നു. Compact schema ഒഴിവാക്കുന്ന guidance tool description-ന് ശേഷം വരും. എല്ലാ tools-ഉം canonical server validation-ഉം മാറ്റമില്ലാതെ നിലനിൽക്കും; MCP contracts guide കാണുക. തിരഞ്ഞെടുത്ത runtime-ഉം REA entry script-ഉം setup നേരിട്ട് രജിസ്റ്റർ ചെയ്യുന്നു. Package runner ഉപയോഗിക്കുമ്പോൾ അതിന്റെ cache entry-യും runtime-ഉം നിലനിർത്തുക; target project-ന് പുറത്തുള്ള വിശ്വസനീയമായ ഡയറക്ടറിയിൽ നിന്ന് package നേടുക. ഇൻസ്റ്റലേഷൻ ഗൈഡ് കാണുക. -സ്ഥിരസ്ഥിതിയായി output schema-കൾ `tools/list`-ൽ നിന്ന് ഒഴിവാക്കുന്നു: ഒരു model API-യും അവ വായിക്കുന്നില്ല, ഓരോ result-ഉം REA തന്നെ validate ചെയ്യുന്നു. ഓരോ output schema-യും 4096 byte പരിധിക്കുള്ളിൽ പരസ്യപ്പെടുത്താൻ MCP registration-ൽ `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`, പൂർണ്ണ output schema-കൾക്ക് `full` (result-കൾ validate ചെയ്യുന്ന MCP client-കൾക്ക്) സജ്ജമാക്കുക. എല്ലാ ക്രമീകരണങ്ങളിലും Server validation-ഉം structured result-കളും പൂർണ്ണമായി നിലനിൽക്കും. MCP കരാർ ഗൈഡിലെ «Output schemas» കാണുക. +സ്ഥിരസ്ഥിതിയായി output schema-കൾ `tools/list`-ൽ നിന്ന് ഒഴിവാക്കുന്നു: ഒരു model API-യും അവ വായിക്കുന്നില്ല, ഓരോ result-ഉം REA തന്നെ validate ചെയ്യുന്നു. ഓരോ output schema-യും 4096 byte പരിധിക്കുള്ളിൽ പരസ്യപ്പെടുത്താൻ MCP registration-ൽ `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` സജ്ജമാക്കുക. എല്ലാ ക്രമീകരണങ്ങളിലും Server validation-ഉം structured result-കളും പൂർണ്ണമായി നിലനിൽക്കും. MCP കരാർ ഗൈഡിലെ «Output schemas» കാണുക. ### നിങ്ങളുടെ agent-നോട് ആവശ്യപ്പെടുക diff --git a/docs/readme/README_pl.md b/docs/readme/README_pl.md index b517597e9a..bd9c7318b1 100644 --- a/docs/readme/README_pl.md +++ b/docs/readme/README_pl.md @@ -82,11 +82,11 @@ Wybierz swoich agentów, sprawdź proponowane zmiany i zatwierdź je. Konfigurat Konfigurator obsługuje Claude Code, Codex, Cursor, Gemini CLI, Grok Build i [innych agentów](../getting-started/installation.md#supported-agents). Konfigurację dostawców analizy i ręczną rejestrację MCP opisuje przewodnik [instalacji i konfiguracji](../getting-started/installation.md). -MCP domyślnie używa zwartych schematów wejściowych. Pełne opisy pól i zagnieżdżonej walidacji włącza `REA_MCP_INPUT_SCHEMA_PROFILE=full` w rejestracji MCP. Oba profile zachowują wszystkie narzędzia i tę samą kanoniczną walidację serwera; zobacz przewodnik kontraktów MCP. +MCP domyślnie używa zwartych schematów wejściowych. Wskazówki pominięte w zwartym schemacie trafiają do opisu narzędzia. Wszystkie narzędzia i kanoniczna walidacja serwera pozostają bez zmian; zobacz przewodnik kontraktów MCP. Konfiguracja zapisuje bezpośrednio wybrane środowisko uruchomieniowe i skrypt wejściowy REA. Przy użyciu narzędzia uruchamiającego pakiety zachowaj jego wpis w pamięci podręcznej i środowisko; pobierz pakiet z zaufanego katalogu poza projektem docelowym. Zobacz instrukcję instalacji. -Schematy wyjściowe są domyślnie pomijane w `tools/list`: żadne API modelu ich nie czyta, a REA samo waliduje każdy wynik. Ustaw `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` w rejestracji MCP, aby udostępniać każdy schemat wyjściowy w limicie 4096 bajtów, lub `full`, aby udostępniać pełne schematy wyjściowe (dla klientów MCP, które walidują wyniki). W każdym ustawieniu walidacja serwera i wyniki strukturalne pozostają pełne. Zobacz „Output schemas” w przewodniku po kontraktach MCP. +Schematy wyjściowe są domyślnie pomijane w `tools/list`: żadne API modelu ich nie czyta, a REA samo waliduje każdy wynik. Ustaw `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` w rejestracji MCP, aby udostępniać każdy schemat wyjściowy w limicie 4096 bajtów. W każdym ustawieniu walidacja serwera i wyniki strukturalne pozostają pełne. Zobacz „Output schemas” w przewodniku po kontraktach MCP. ### Zapytaj agenta diff --git a/docs/readme/README_pt-BR.md b/docs/readme/README_pt-BR.md index cee4799197..08208788bd 100644 --- a/docs/readme/README_pt-BR.md +++ b/docs/readme/README_pt-BR.md @@ -82,11 +82,11 @@ Escolha seus agentes, revise as alterações propostas e aprove-as. A configura A configuração oferece suporte ao Claude Code, Codex, Cursor, Gemini CLI, Grok Build e [outros agentes](../getting-started/installation.md#supported-agents). Consulte [instalação e configuração](../getting-started/installation.md) para configurar provedores e registrar o MCP manualmente. -O MCP usa esquemas de entrada compactos por padrão. Para obter todas as orientações de campos e validação aninhada, defina `REA_MCP_INPUT_SCHEMA_PROFILE=full` no registro MCP. Ambos os perfis mantêm todas as ferramentas e a mesma validação canônica do servidor; veja o guia dos contratos MCP. +O MCP usa esquemas de entrada compactos por padrão. As orientações que um esquema compacto omite seguem a descrição da ferramenta. Todas as ferramentas e a validação canônica do servidor permanecem iguais; veja o guia dos contratos MCP. A configuração registra diretamente o runtime escolhido e o script de entrada do REA. Ao usar um executor de pacotes, mantenha sua entrada de cache e o runtime; obtenha o pacote em um diretório confiável fora do projeto alvo. Consulte o guia de instalação. -Os esquemas de saída são omitidos de `tools/list` por padrão: nenhuma API de modelo os lê, e o REA valida cada resultado por conta própria. Defina `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` no registro MCP para anunciar cada esquema de saída dentro de 4096 bytes, ou `full` para esquemas de saída completos (para clientes MCP que validam resultados). Em todas as configurações, a validação do servidor e os resultados estruturados continuam completos. Consulte “Output schemas” no guia de contratos MCP. +Os esquemas de saída são omitidos de `tools/list` por padrão: nenhuma API de modelo os lê, e o REA valida cada resultado por conta própria. Defina `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` no registro MCP para anunciar cada esquema de saída dentro de 4096 bytes. Em todas as configurações, a validação do servidor e os resultados estruturados continuam completos. Consulte “Output schemas” no guia de contratos MCP. ### Peça ao seu agente diff --git a/docs/readme/README_ro.md b/docs/readme/README_ro.md index 96cb5c4515..5a8e9a8ebc 100644 --- a/docs/readme/README_ro.md +++ b/docs/readme/README_ro.md @@ -92,16 +92,14 @@ Programul de configurare acceptă Claude Code, Codex, Cursor, Gemini CLI, Grok B [instalarea și configurarea](../getting-started/installation.md) pentru configurarea motoarelor de analiză și înregistrarea manuală a serverului MCP. -MCP folosește implicit scheme de intrare compacte, pentru a reduce spațiul ocupat -în context. Setează `REA_MCP_INPUT_SCHEMA_PROFILE=full` în înregistrarea MCP când -ai nevoie de îndrumările complete pentru câmpurile expuse și de detaliile -validării structurilor imbricate. Ambele profiluri păstrează toate uneltele și -validarea canonică a serverului. Consultă secțiunea „Compact input schema profile” +MCP anunță scheme de intrare compacte, pentru a reduce spațiul ocupat în +context; îndrumările omise de o schemă compactă urmează descrierea uneltei. +Toate uneltele și validarea canonică a serverului rămân neschimbate. Consultă secțiunea „Compact input schema profile” din ghidul contractelor MCP. Configurarea înregistrează direct mediul de execuție ales și scriptul de intrare REA. Dacă folosești un executor de pachete, păstrează intrarea sa din cache și mediul de execuție; obține pachetul dintr-un director de încredere din afara proiectului țintă. Consultă ghidul de instalare. -Schemele de ieșire sunt omise implicit din `tools/list`: niciun API de model nu le citește, iar REA validează singur fiecare rezultat. Setează `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` în înregistrarea MCP pentru a anunța fiecare schemă de ieșire în limita a 4096 de octeți, sau `full` pentru scheme de ieșire complete (pentru clienții MCP care validează rezultatele). În toate setările, validarea serverului și rezultatele structurate rămân complete. Consultă „Output schemas” din ghidul contractelor MCP. +Schemele de ieșire sunt omise implicit din `tools/list`: niciun API de model nu le citește, iar REA validează singur fiecare rezultat. Setează `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` în înregistrarea MCP pentru a anunța fiecare schemă de ieșire în limita a 4096 de octeți. În toate setările, validarea serverului și rezultatele structurate rămân complete. Consultă „Output schemas” din ghidul contractelor MCP. ### Cere-i agentului diff --git a/docs/readme/README_ru.md b/docs/readme/README_ru.md index 353f78fc08..455763db88 100644 --- a/docs/readme/README_ru.md +++ b/docs/readme/README_ru.md @@ -82,11 +82,11 @@ npx rea-agents setup Настройка поддерживает Claude Code, Codex, Cursor, Gemini CLI, Grok Build и [других агентов](../getting-started/installation.md#supported-agents). Настройка провайдеров и ручная регистрация MCP описаны в разделе [установка и настройка](../getting-started/installation.md). -MCP по умолчанию использует компактные входные схемы. Для полного описания полей и вложенной валидации задайте `REA_MCP_INPUT_SCHEMA_PROFILE=full` в регистрации MCP. Оба профиля сохраняют все инструменты и одинаковую каноническую проверку на сервере; см. руководство по контрактам MCP. +MCP по умолчанию использует компактные входные схемы. Указания, которые компактная схема опускает, следуют за описанием инструмента. Все инструменты и каноническая проверка на сервере не меняются; см. руководство по контрактам MCP. Настройка напрямую регистрирует выбранную среду выполнения и входной скрипт REA. При использовании средства запуска пакетов сохраняйте его запись в кэше и среду выполнения; получайте пакет из доверенного каталога вне целевого проекта. См. руководство по установке. -По умолчанию схемы вывода не включаются в `tools/list`: ни один API моделей их не читает, а REA сам проверяет каждый результат. Задайте `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` в регистрации MCP, чтобы объявлять каждую схему вывода в пределах 4096 байт, или `full` для полных схем вывода (для MCP-клиентов, которые проверяют результаты). При любой настройке проверка на сервере и структурированные результаты остаются полными. См. раздел «Output schemas» в руководстве по контрактам MCP. +По умолчанию схемы вывода не включаются в `tools/list`: ни один API моделей их не читает, а REA сам проверяет каждый результат. Задайте `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` в регистрации MCP, чтобы объявлять каждую схему вывода в пределах 4096 байт. При любой настройке проверка на сервере и структурированные результаты остаются полными. См. раздел «Output schemas» в руководстве по контрактам MCP. ### Задайте вопрос агенту diff --git a/docs/readme/README_th.md b/docs/readme/README_th.md index 277c5372ad..a28cf63730 100644 --- a/docs/readme/README_th.md +++ b/docs/readme/README_th.md @@ -82,11 +82,11 @@ npx rea-agents setup ขั้นตอนตั้งค่ารองรับ Claude Code, Codex, Cursor, Gemini CLI, Grok Build และ[เอเจนต์อื่น ๆ](../getting-started/installation.md#supported-agents) ดู[การติดตั้งและตั้งค่า](../getting-started/installation.md) สำหรับการตั้งค่าผู้ให้บริการวิเคราะห์และการลงทะเบียน MCP ด้วยตนเอง -MCP ใช้สคีมาอินพุตแบบกระชับโดยปริยาย หากต้องการคำอธิบายฟิลด์และการตรวจสอบแบบซ้อนทั้งหมด ให้ตั้ง `REA_MCP_INPUT_SCHEMA_PROFILE=full` ในการลงทะเบียน MCP ทั้งสองแบบคงเครื่องมือครบและใช้การตรวจสอบมาตรฐานฝั่งเซิร์ฟเวอร์เดียวกัน ดูคู่มือสัญญา MCP +MCP ใช้สคีมาอินพุตแบบกระชับโดยปริยาย คำแนะนำที่สคีมาแบบกระชับละไว้จะต่อท้ายคำอธิบายเครื่องมือ เครื่องมือทั้งหมดและการตรวจสอบมาตรฐานฝั่งเซิร์ฟเวอร์ยังคงเหมือนเดิม ดูคู่มือสัญญา MCP การตั้งค่าจะลงทะเบียนรันไทม์ที่เลือกและสคริปต์เริ่มต้นของ REA โดยตรง หากใช้ตัวรันแพ็กเกจ ให้เก็บรายการแคชและรันไทม์ไว้ และรับแพ็กเกจจากไดเรกทอรีที่เชื่อถือได้นอกโปรเจกต์เป้าหมาย ดูคู่มือการติดตั้ง -ตามค่าเริ่มต้น ระบบจะไม่ใส่สคีมาเอาต์พุตใน `tools/list` เพราะไม่มี API ของโมเดลใดอ่านสคีมาเหล่านี้ และ REA ตรวจสอบผลลัพธ์ทุกรายการเอง ตั้ง `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` ในการลงทะเบียน MCP เพื่อประกาศสคีมาเอาต์พุตแต่ละรายการภายใน 4096 ไบต์ หรือ `full` เพื่อประกาศสคีมาเอาต์พุตฉบับเต็ม (สำหรับไคลเอนต์ MCP ที่ตรวจสอบผลลัพธ์) ทุกการตั้งค่ายังคงการตรวจสอบฝั่งเซิร์ฟเวอร์และผลลัพธ์แบบมีโครงสร้างครบถ้วน ดูหัวข้อ “Output schemas” ในคู่มือสัญญา MCP +ตามค่าเริ่มต้น ระบบจะไม่ใส่สคีมาเอาต์พุตใน `tools/list` เพราะไม่มี API ของโมเดลใดอ่านสคีมาเหล่านี้ และ REA ตรวจสอบผลลัพธ์ทุกรายการเอง ตั้ง `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` ในการลงทะเบียน MCP เพื่อประกาศสคีมาเอาต์พุตแต่ละรายการภายใน 4096 ไบต์ ทุกการตั้งค่ายังคงการตรวจสอบฝั่งเซิร์ฟเวอร์และผลลัพธ์แบบมีโครงสร้างครบถ้วน ดูหัวข้อ “Output schemas” ในคู่มือสัญญา MCP ### ถามเอเจนต์ diff --git a/docs/readme/README_tr.md b/docs/readme/README_tr.md index 6f962c2af9..ce09551aa9 100644 --- a/docs/readme/README_tr.md +++ b/docs/readme/README_tr.md @@ -82,11 +82,11 @@ Ajanlarınızı seçin, önerilen değişiklikleri inceleyin ve onaylayın. Kuru Kurulum Claude Code, Codex, Cursor, Gemini CLI, Grok Build ve [diğer ajanları](../getting-started/installation.md#supported-agents) destekler. Sağlayıcı yapılandırması ve elle MCP kaydı için [yükleme ve kurulum](../getting-started/installation.md) belgesine bakın. -MCP varsayılan olarak kompakt girdi şemaları kullanır. Tam alan açıklamaları ve iç içe doğrulama ayrıntıları için MCP kaydında `REA_MCP_INPUT_SCHEMA_PROFILE=full` ayarlayın. Her iki profil tüm araçları ve aynı kanonik sunucu doğrulamasını korur; MCP sözleşme rehberine bakın. +MCP varsayılan olarak kompakt girdi şemaları kullanır. Kompakt şemanın atladığı açıklamalar araç açıklamasının ardından gelir. Tüm araçlar ve kanonik sunucu doğrulaması değişmez; MCP sözleşme rehberine bakın. Kurulum, seçilen çalışma zamanını ve REA giriş betiğini doğrudan kaydeder. Paket çalıştırıcısı kullanıyorsanız önbellek girdisini ve çalışma zamanını koruyun; paketi hedef projenin dışında güvenilir bir dizinden edinin. Kurulum rehberine bakın. -Çıktı şemaları varsayılan olarak `tools/list` içinde yer almaz: hiçbir model API'si bunları okumaz ve REA her sonucu kendisi doğrular. Her çıktı şemasını 4096 baytlık sınır içinde duyurmak için MCP kaydında `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`, tam çıktı şemaları için `full` (sonuçları doğrulayan MCP istemcileri için) ayarlayın. Tüm ayarlarda sunucu doğrulaması ve yapılandırılmış sonuçlar tam kalır. MCP sözleşmeleri rehberindeki “Output schemas” bölümüne bakın. +Çıktı şemaları varsayılan olarak `tools/list` içinde yer almaz: hiçbir model API'si bunları okumaz ve REA her sonucu kendisi doğrular. Her çıktı şemasını 4096 baytlık sınır içinde duyurmak için MCP kaydında `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` ayarlayın. Tüm ayarlarda sunucu doğrulaması ve yapılandırılmış sonuçlar tam kalır. MCP sözleşmeleri rehberindeki “Output schemas” bölümüne bakın. ### Ajanınıza sorun diff --git a/docs/readme/README_uk.md b/docs/readme/README_uk.md index 936f2e7e17..661a576f6e 100644 --- a/docs/readme/README_uk.md +++ b/docs/readme/README_uk.md @@ -82,11 +82,11 @@ npx rea-agents setup Налаштування підтримує Claude Code, Codex, Cursor, Gemini CLI, Grok Build та [інших агентів](../getting-started/installation.md#supported-agents). Конфігурацію провайдерів і ручну реєстрацію MCP описано в розділі [встановлення й налаштування](../getting-started/installation.md). -MCP типово використовує компактні вхідні схеми. Для повного опису полів і вкладеної перевірки задайте `REA_MCP_INPUT_SCHEMA_PROFILE=full` у реєстрації MCP. Обидва профілі зберігають усі інструменти й однакову канонічну перевірку сервера; див. посібник контрактів MCP. +MCP типово використовує компактні вхідні схеми. Вказівки, які компактна схема пропускає, йдуть після опису інструмента. Усі інструменти й канонічна перевірка сервера не змінюються; див. посібник контрактів MCP. Налаштування безпосередньо реєструє вибране середовище виконання та стартовий скрипт REA. Якщо ви використовуєте засіб запуску пакетів, збережіть його запис у кеші та середовище виконання; отримуйте пакет із довіреного каталогу поза цільовим проєктом. Див. посібник зі встановлення. -За замовчуванням схеми виводу не включаються до `tools/list`: жоден API моделей їх не читає, а REA сам перевіряє кожен результат. Задайте `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` у реєстрації MCP, щоб оголошувати кожну схему виводу в межах 4096 байтів, або `full` для повних схем виводу (для MCP-клієнтів, які перевіряють результати). За будь-якого налаштування перевірка сервера та структуровані результати залишаються повними. Див. розділ «Output schemas» у посібнику з контрактів MCP. +За замовчуванням схеми виводу не включаються до `tools/list`: жоден API моделей їх не читає, а REA сам перевіряє кожен результат. Задайте `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` у реєстрації MCP, щоб оголошувати кожну схему виводу в межах 4096 байтів. За будь-якого налаштування перевірка сервера та структуровані результати залишаються повними. Див. розділ «Output schemas» у посібнику з контрактів MCP. ### Запитайте агента diff --git a/docs/readme/README_ur-Latn.md b/docs/readme/README_ur-Latn.md index 842250d63e..aad3d45ced 100644 --- a/docs/readme/README_ur-Latn.md +++ b/docs/readme/README_ur-Latn.md @@ -92,16 +92,15 @@ Setup Claude Code, Codex, Cursor, Gemini CLI, Grok Build aur configuration aur manual MCP registration ke liye [installation aur setup](../getting-started/installation.md) dekhein. -Context overhead kam rakhne ke liye MCP default mein compact input schemas use -karta hai. Agar tamam advertised fields ki guidance aur nested validation details -chahiye hon to apni MCP registration mein `REA_MCP_INPUT_SCHEMA_PROFILE=full` -set karein. Dono profiles mein tamam tools aur canonical server validation -maujood rehte hain. MCP contracts guide ka "Compact input schema profile" +Context overhead kam rakhne ke liye MCP compact input schemas advertise karta +hai; jo guidance compact schema chhor deta hai woh tool description ke baad aati +hai. Tamam tools aur canonical server validation baghair tabdeeli ke maujood +rehte hain. MCP contracts guide ka "Compact input schema profile" section dekhein. Setup chuna hua runtime aur REA entry script seedha register karta hai. Package runner use karne par uski cache entry aur runtime barqarar rakhein; package target project se bahar kisi bharosemand directory se hasil karein. Installation guide dekhein. -Default mein output schemas `tools/list` se nikal diye jate hain: koi model API inhein nahin parhti, aur REA har result ko khud validate karta hai. Har output schema ko 4096 bytes ki had mein advertise karne ke liye MCP registration mein `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact`, ya mukammal output schemas ke liye `full` (results validate karne wale MCP clients ke liye) set karein. Har setting mein server validation aur structured results mukammal rehte hain. MCP contracts guide ka “Output schemas” section dekhein. +Default mein output schemas `tools/list` se nikal diye jate hain: koi model API inhein nahin parhti, aur REA har result ko khud validate karta hai. Har output schema ko 4096 bytes ki had mein advertise karne ke liye MCP registration mein `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` set karein. Har setting mein server validation aur structured results mukammal rehte hain. MCP contracts guide ka “Output schemas” section dekhein. ### Apne agent se kahein diff --git a/docs/readme/README_vi.md b/docs/readme/README_vi.md index d0429b24f4..53c7a8ff8d 100644 --- a/docs/readme/README_vi.md +++ b/docs/readme/README_vi.md @@ -82,11 +82,11 @@ Chọn các tác nhân, xem lại những thay đổi được đề xuất và Quá trình thiết lập hỗ trợ Claude Code, Codex, Cursor, Gemini CLI, Grok Build và [các tác nhân khác](../getting-started/installation.md#supported-agents). Xem [cài đặt và thiết lập](../getting-started/installation.md) để cấu hình trình phân tích và đăng ký MCP thủ công. -MCP mặc định dùng schema đầu vào gọn. Để có hướng dẫn đầy đủ về trường và xác thực lồng nhau, đặt `REA_MCP_INPUT_SCHEMA_PROFILE=full` trong đăng ký MCP. Cả hai cấu hình giữ toàn bộ công cụ và cùng xác thực chuẩn ở máy chủ; xem hướng dẫn hợp đồng MCP. +MCP mặc định dùng schema đầu vào gọn. Hướng dẫn mà schema gọn lược bỏ được nối sau mô tả công cụ. Toàn bộ công cụ và xác thực chuẩn ở máy chủ không thay đổi; xem hướng dẫn hợp đồng MCP. Thiết lập đăng ký trực tiếp runtime đã chọn và script khởi chạy REA. Khi dùng trình chạy gói, hãy giữ mục cache và runtime đó; tải gói từ một thư mục đáng tin cậy bên ngoài dự án đích. Xem hướng dẫn cài đặt. -Theo mặc định, schema đầu ra được lược khỏi `tools/list`: không API mô hình nào đọc chúng, và REA tự xác thực mọi kết quả. Đặt `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` trong cấu hình MCP để công bố từng schema đầu ra trong giới hạn 4096 byte, hoặc `full` để công bố schema đầu ra đầy đủ (cho các client MCP có xác thực kết quả). Với mọi thiết lập, xác thực phía server và kết quả có cấu trúc vẫn đầy đủ. Xem mục “Output schemas” trong hướng dẫn hợp đồng MCP. +Theo mặc định, schema đầu ra được lược khỏi `tools/list`: không API mô hình nào đọc chúng, và REA tự xác thực mọi kết quả. Đặt `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` trong cấu hình MCP để công bố từng schema đầu ra trong giới hạn 4096 byte. Với mọi thiết lập, xác thực phía server và kết quả có cấu trúc vẫn đầy đủ. Xem mục “Output schemas” trong hướng dẫn hợp đồng MCP. ### Hỏi tác nhân diff --git a/docs/readme/README_zh-TW.md b/docs/readme/README_zh-TW.md index 0997964972..2eab04af92 100644 --- a/docs/readme/README_zh-TW.md +++ b/docs/readme/README_zh-TW.md @@ -82,11 +82,11 @@ npx rea-agents setup 設定流程支援 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他 AI 代理](../getting-started/installation.md#supported-agents)。提供者組態和手動註冊 MCP 的方法見[安裝與設定](../getting-started/installation.md)。 -MCP 預設使用精簡輸入 Schema,以減少上下文開銷。需要完整欄位說明和巢狀驗證細節時,請在 MCP 註冊中設定 `REA_MCP_INPUT_SCHEMA_PROFILE=full`。兩種設定都保留完整工具集和標準的伺服器驗證;見 MCP 合約指南。 +MCP 預設使用精簡輸入 Schema,以減少上下文開銷。精簡 Schema 省略的說明會接在工具描述之後。完整工具集和標準的伺服器驗證維持不變;見 MCP 合約指南。 設定會直接登記所選執行環境和 REA 入口指令碼。使用套件執行器時,請保留其快取項目及執行環境,並從目標專案以外的可信任目錄取得套件。 請參閱安裝指南。 -預設會從 `tools/list` 省略輸出 Schema:沒有任何模型 API 會讀取它們,且 REA 會自行驗證每個結果。在 MCP 登記中設定 `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` 可在 4096 位元組內公布每個輸出 Schema,設定 `full` 則公布完整輸出 Schema(供會驗證結果的 MCP 用戶端使用)。所有設定下,伺服器驗證和結構化結果仍保持完整。 請參閱 MCP 合約指南中的「Output schemas」。 +預設會從 `tools/list` 省略輸出 Schema:沒有任何模型 API 會讀取它們,且 REA 會自行驗證每個結果。在 MCP 登記中設定 `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` 可在 4096 位元組內公布每個輸出 Schema。所有設定下,伺服器驗證和結構化結果仍保持完整。 請參閱 MCP 合約指南中的「Output schemas」。 ### 詢問 AI 代理 diff --git a/docs/readme/README_zh.md b/docs/readme/README_zh.md index 03617fd130..b7da70f1dd 100644 --- a/docs/readme/README_zh.md +++ b/docs/readme/README_zh.md @@ -82,11 +82,11 @@ npx rea-agents setup 设置流程支持 Claude Code、Codex、Cursor、Gemini CLI、Grok Build 和[其他智能体](../getting-started/installation.md#supported-agents)。提供方配置和手动注册 MCP 的方法见[安装与设置](../getting-started/installation.md)。 -MCP 默认使用紧凑输入 Schema 来减少上下文开销。需要完整字段说明和嵌套验证细节时,在 MCP 注册中设置 `REA_MCP_INPUT_SCHEMA_PROFILE=full`。两种配置都保留完整工具集和规范的服务端验证;见 MCP 合约指南。 +MCP 默认使用紧凑输入 Schema 来减少上下文开销。紧凑 Schema 省略的说明会接在工具描述之后。完整工具集和规范的服务端验证保持不变;见 MCP 合约指南。 设置会直接注册所选运行时和 REA 入口脚本。使用包运行器时,请保留其缓存条目和运行时,并从目标项目以外的可信目录获取包。 请参阅安装指南。 -默认从 `tools/list` 中省略输出 Schema:没有任何模型 API 读取它们,REA 会自行验证每个结果。在 MCP 注册中设置 `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` 可在 4096 字节内公布每个输出 Schema,设置 `full` 则公布完整输出 Schema(供验证结果的 MCP 客户端使用)。所有设置下,服务端验证和结构化结果仍保持完整。 请参阅 MCP 合约指南中的“Output schemas”。 +默认从 `tools/list` 中省略输出 Schema:没有任何模型 API 读取它们,REA 会自行验证每个结果。在 MCP 注册中设置 `REA_MCP_OUTPUT_SCHEMA_PROFILE=compact` 可在 4096 字节内公布每个输出 Schema。所有设置下,服务端验证和结构化结果仍保持完整。 请参阅 MCP 合约指南中的“Output schemas”。 ### 询问智能体 diff --git a/docs/reference/mcp-contracts.md b/docs/reference/mcp-contracts.md index f4345c1fe7..63642a9f75 100644 --- a/docs/reference/mcp-contracts.md +++ b/docs/reference/mcp-contracts.md @@ -89,10 +89,9 @@ managed method page with `view: {"kind":"managed-members","collection":"methods","offset":0,"limit":64}`. `view: {"kind":"summary"}` has no facet. These are examples, not all supported families; use the producer and its guide for collection-specific fields and -selection. The full input schema exposes accepted variants, while the default -compact profile can omit nested shapes and parameter descriptions. Refreshing -the same compact catalog does not restore those missing shapes; do not invent -selectors from an opaque object declaration. +selection. The compact input schema can omit nested shapes and parameter +descriptions. Refreshing the catalog does not restore those missing shapes; do +not invent selectors from an opaque object declaration. Text pages use UTF-16 code units; row pages count the selected retained collection. Source coverage and retained-page coverage remain distinct. A @@ -2084,33 +2083,32 @@ rejects. The MCP handler still validates every call with the canonical contract. The Gemini CLI verifier checks the outgoing `parametersJsonSchema` and canonical contract examples against that declaration using a loopback model fixture; it does not establish live Google API or -OpenCode client behavior. Set `REA_VERIFY_GEMINI_SCHEMA_PROFILE=compact` to -verify the compact advertisement through the same client boundary. +OpenCode client behavior. ### Compact input schema profile Some function-calling providers reject a request while any single tool's `parameters` schema exceeds a literal serialized size cap; Moonshot clients such as kimi-cli have reported failures with the full catalog attached -([#1484](https://github.com/morluto/rea/issues/1484)). Setting the server's -`REA_MCP_INPUT_SCHEMA_PROFILE` environment variable to `compact` (the default) -renders every advertised input schema within a 13,312-byte budget, measured to sit below +([#1484](https://github.com/morluto/rea/issues/1484)). REA therefore renders +every advertised input schema within a 13,312-byte budget, measured to sit below Moonshot's observed cap (community measurements: a 13.7 KB schema is accepted -while a ~15 KB one is rejected). The setting is read at startup; restart the -server to apply changes, as with `REA_MCP_MAX_RESPONSE_BYTES`. Explicitly select -`full` to retain complete advertised input guidance and nested validation detail. -Existing explicit profile choices are preserved. Compact presentation +while a ~15 KB one is rejected). This compact presentation is the only input +advertisement. `REA_MCP_INPUT_SCHEMA_PROFILE` accepts only `compact`; the former +`full` profile was removed, and a registration that still selects it fails at +startup with a configuration error naming the setting. Remove the setting, or +set it to `compact`. Compact presentation also reduces schemas whose expanded JSON tree exceeds ten levels, the limit reported by strict-depth MCP clients in [#2813](https://github.com/morluto/rea/issues/2813). -WorkBuddy/CodeBuddy gateways have also rejected full REA catalogs with an -opaque `11133` error while accepting the compact profile +WorkBuddy/CodeBuddy gateways have also rejected uncompacted REA catalogs with +an opaque `11133` error while accepting the compact profile ([#2999](https://github.com/morluto/rea/issues/2999)); compatibility depends on the selected gateway and model. That report used ZCode through a gateway to a Kimi-K3 backend, not the WorkBuddy MCP client. Set the profile on REA's registration in the actual MCP host; the host's name alone does not identify the downstream model or its schema constraints. -The compact profile also projects every advertised input schema into the +The compact presentation also projects every advertised input schema into the portable advertised dialect declared in `src/contracts/advertisedDialect.ts`: the subset of JSON Schema that the clients and gateways REA has met accept, with one rule per observed rejection (for example strict gateways in @@ -2170,11 +2168,10 @@ dialect checker, keep them valid JSON Schema that still accepts every canonical example. Client verification also checks provider-facing sizes and canonical examples after conversion by the actual Kimi Code CLI; local captures do not establish Moonshot acceptance. -The SDK catalog test checks that full-profile schemas retain nested ADB extra, -managed selection, and WASM timeout and detail restrictions. Path-contract tests -check full-profile local-path rules. Compact MCP workflow tests verify canonical -rejection of invalid inputs before provider work, independent of how much detail -the selected advertisement retains. +The SDK catalog test checks that the canonical input schemas retain nested ADB +extra, managed selection, and WASM timeout and detail restrictions. Compact MCP +workflow tests verify canonical rejection of invalid inputs before provider +work, independent of how much detail the advertisement retains. @@ -2307,8 +2304,8 @@ preserved. This keeps every tool and validation rule in discovery and does not change the SDK's catalog invalidation or availability checks. The server identity's tool-schema digest includes the selected output-schema -profile and the advertised form that profile produces. `full`, `compact`, and -`off` therefore have distinct identities. The `compact` digest covers the +profile and the advertised form that profile produces. `compact` and `off` +therefore have distinct identities. The `compact` digest covers the bounded schemas clients receive; `off` keeps the canonical output contracts in the digest while advertising nothing. @@ -2319,9 +2316,9 @@ validation, and complete output schemas were about 92% of a default `tools/list` (4.3 of 4.6 MB). Some clients also reject a large `tools/list` frame or HTTP SSE event before they load any tools (the measured pi-web-ui failure is in [#2512](https://github.com/morluto/rea/issues/2512)). Select -`compact` to bound every advertised output schema to 4,096 bytes, or `full` -for complete output schemas when an MCP client should validate results. Every -profile keeps the same named canonical tool contract for server-side result +`compact` to bound every advertised output schema to 4,096 bytes. The former +`full` output profile was removed; a registration that still selects it fails +at startup with a configuration error naming the setting. Both profiles keep the same named canonical tool contract for server-side result validation and still returns complete `structuredContent` and text results; the MCP specification does not require an output schema for `structuredContent`. @@ -2339,12 +2336,7 @@ within the budget, so the advertised form is self-contained and omits the canonical contract's content-bound `$id`. The projection only widens: a valid canonical result still satisfies the advertised schema, but a client validating against it can accept a result the canonical contract rejects. REA still -validates every result before delivery. On catalogs large enough that full -input schemas alone approach the frame limit, keep the input profile at its -`compact` default as well; with full input schemas the advertisement is -dominated by the input side. -The full input profile has no one-megabyte catalog-frame guarantee, including -when output schemas are omitted. +validates every result before delivery. Only client-side schema discovery and validation are reduced or omitted, so hosts using these profiles should rely on REA's validation and retain their diff --git a/scripts/generate-skill-metadata.mjs b/scripts/generate-skill-metadata.mjs index 6c5a348900..ff10c160ba 100644 --- a/scripts/generate-skill-metadata.mjs +++ b/scripts/generate-skill-metadata.mjs @@ -23,7 +23,7 @@ const current = ( await readFile(join(sourceRoot, "SKILL.md"), "utf8") ).replaceAll("\r\n", "\n"); const cacheBust = String(Date.now()); -const { FULL_CATALOG_IDENTITY } = await import( +const { DEFAULT_CATALOG_IDENTITY } = await import( `${pathToFileURL(join(root, "dist/catalogIdentity.js")).href}?${cacheBust}` ); const { PRODUCT_IDENTITY } = await import( @@ -34,7 +34,7 @@ const latestSpecifier = PRODUCT_IDENTITY.packageSpecifier; const pinnedSpecifier = PRODUCT_IDENTITY.registrationPackageSpecifier; const generated = addSkillManifestToolCount( current, - FULL_CATALOG_IDENTITY.counts.mcp_tools, + DEFAULT_CATALOG_IDENTITY.counts.mcp_tools, ); if (!generated.ok) throw generated.error; const source = generated.value.replaceAll(latestSpecifier, pinnedSpecifier); diff --git a/scripts/lib/artifact-e2e.mjs b/scripts/lib/artifact-e2e.mjs index e476760fcf..f164375ae1 100644 --- a/scripts/lib/artifact-e2e.mjs +++ b/scripts/lib/artifact-e2e.mjs @@ -4,10 +4,8 @@ import { Client } from "@modelcontextprotocol/client"; import { createVerifierCommands } from "./verify-commands.mjs"; import { createOwnedMcpStdioTransport } from "./owned-mcp-stdio.mjs"; import { withVerifierCleanup } from "./verifier-cleanup.mjs"; -import { - INLINE_EVIDENCE_MCP_ENVIRONMENT, - schemaVerifierEnvironment, -} from "./mcp-verification-environment.mjs"; +import { INLINE_EVIDENCE_MCP_ENVIRONMENT } from "./mcp-verification-environment.mjs"; +import { withCanonicalOutputSchema } from "./canonical-output-schema.mjs"; // Bind parsed definitions to this connection. Hot/cold catalogs may add tools; // refresh a missing name once without throwing away already validated bindings. @@ -59,7 +57,7 @@ const advertisedTool = async (client, name) => { return tool; }; -/** Call an advertised tool with SDK output validation of its actual schema. */ +/** Call an advertised tool with SDK output validation against its canonical contract. */ export async function artifactMcpCall(client, parameters, options = {}) { options.signal?.throwIfAborted(); assert.equal( @@ -67,11 +65,9 @@ export async function artifactMcpCall(client, parameters, options = {}) { undefined, "Artifact verification owns the advertised schema; pass request options only", ); - const toolDefinition = await advertisedTool(client, parameters.name); - assert.notEqual( - toolDefinition.outputSchema, - undefined, - `MCP tool omitted its output schema: ${parameters.name}`, + // tools/list omits output schemas by default; bind the canonical contract. + const toolDefinition = withCanonicalOutputSchema( + await advertisedTool(client, parameters.name), ); options.signal?.throwIfAborted(); return client.callTool(parameters, { ...options, toolDefinition }); @@ -98,7 +94,7 @@ const environment = () => { const compileCache = process.env.NODE_COMPILE_CACHE ?? process.env.REA_ARTIFACT_NODE_COMPILE_CACHE; - return schemaVerifierEnvironment({ + return { PATH: process.env.PATH ?? "/usr/bin:/bin", ...(compileCache && process.env.NODE_V8_COVERAGE === undefined && @@ -109,7 +105,7 @@ const environment = () => { REA_ANALYSIS_PROVIDER: "auto", // Artifact workflows have no deep-provider prerequisite or GUI authority. HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper", - }); + }; }; /** Execute the compiled CLI with production providers and no test adapters. */ diff --git a/scripts/lib/browser-capture-metadata-budget-e2e.mjs b/scripts/lib/browser-capture-metadata-budget-e2e.mjs index fbf5fb91e2..07293fa637 100644 --- a/scripts/lib/browser-capture-metadata-budget-e2e.mjs +++ b/scripts/lib/browser-capture-metadata-budget-e2e.mjs @@ -4,7 +4,6 @@ import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { artifactCliEvidence, artifactMcpResult } from "./artifact-e2e.mjs"; import { webSourceMapsSchema } from "../../dist/domain/webBundleAnalysis.js"; -import { schemaVerifierEnvironment } from "./mcp-verification-environment.mjs"; const ANNOTATION_BYTES = 2 * 1024 * 1024; const SCRIPT_COUNT = 5; @@ -107,10 +106,10 @@ export async function verifyBrowserCaptureMetadataBudget(endpoint) { const transport = new StdioClientTransport({ command: process.execPath, args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"], - env: schemaVerifierEnvironment({ + env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent", - }), + }, stderr: "pipe", }); const client = new Client({ name: "browser-metadata-e2e", version: "1" }); diff --git a/scripts/lib/browser-scenario-storage-fingerprints-e2e.mjs b/scripts/lib/browser-scenario-storage-fingerprints-e2e.mjs index f2a41181f3..3d25385477 100644 --- a/scripts/lib/browser-scenario-storage-fingerprints-e2e.mjs +++ b/scripts/lib/browser-scenario-storage-fingerprints-e2e.mjs @@ -1,4 +1,3 @@ -import { schemaVerifierEnvironment } from "./mcp-verification-environment.mjs"; import assert from "node:assert/strict"; import { requireMcpEvidenceBundle } from "./mcp-verifier-results.mjs"; import { createHash } from "node:crypto"; @@ -138,7 +137,7 @@ export async function verifyScenarioStorageFingerprints(executable, origin) { const transport = new StdioClientTransport({ command: process.execPath, args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"], - env: schemaVerifierEnvironment(process.env), + env: process.env, stderr: "pipe", }); const client = new Client({ diff --git a/scripts/lib/canonical-output-schema.d.mts b/scripts/lib/canonical-output-schema.d.mts new file mode 100644 index 0000000000..de4ddde4f9 --- /dev/null +++ b/scripts/lib/canonical-output-schema.d.mts @@ -0,0 +1,5 @@ +import type { Tool } from "@modelcontextprotocol/client"; +export function canonicalOutputSchema(name: string): Record; +export function withCanonicalOutputSchema>( + tool: T, +): T & { outputSchema: Record }; diff --git a/scripts/lib/canonical-output-schema.mjs b/scripts/lib/canonical-output-schema.mjs new file mode 100644 index 0000000000..8e54810533 --- /dev/null +++ b/scripts/lib/canonical-output-schema.mjs @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { DISCOVER_TOOLS } from "../../dist/catalogDiscovery.js"; +import { TOOL_CONTRACTS } from "../../dist/contracts/toolContracts.js"; +import { advertisedOutputJsonSchema } from "../../dist/contracts/toolSchemaMetadata.js"; + +/** + * The canonical output JSON Schema for one tool. `tools/list` omits output + * schemas by default (or bounds them), so verifiers validate replies against + * the compiled contract instead of a listed definition. + */ +export const canonicalOutputSchema = (name) => { + // Transport discovery is registered from its own schema, not a tool contract. + if (name === DISCOVER_TOOLS.name) + return z.toJSONSchema(DISCOVER_TOOLS.outputSchema, { io: "output" }); + const contract = TOOL_CONTRACTS.find((candidate) => candidate.name === name); + if (contract === undefined) throw new Error(`No tool contract named ${name}`); + return advertisedOutputJsonSchema(contract); +}; + +/** A listed tool definition whose output schema the SDK client validates against. */ +export const withCanonicalOutputSchema = (tool) => ({ + ...tool, + outputSchema: canonicalOutputSchema(tool.name), +}); diff --git a/scripts/lib/catalog-builders.d.mts b/scripts/lib/catalog-builders.d.mts index 4992fd31ee..5deeb36b28 100644 --- a/scripts/lib/catalog-builders.d.mts +++ b/scripts/lib/catalog-builders.d.mts @@ -40,7 +40,7 @@ export function toolFamilyCatalog(sources: { readonly TOOL_CONTRACTS: readonly unknown[]; }; readonly catalogIdentity: { - readonly FULL_CATALOG_IDENTITY: { + readonly DEFAULT_CATALOG_IDENTITY: { readonly counts: { readonly mcp_tools: number }; }; }; diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 02a66923f9..f9a8442b1c 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -17,7 +17,7 @@ export const toolFamilyCatalog = (sources) => { if ( familyTotal !== sources.toolContracts.TOOL_CONTRACTS.length || familyTotal !== - sources.catalogIdentity.FULL_CATALOG_IDENTITY.counts.mcp_tools + sources.catalogIdentity.DEFAULT_CATALOG_IDENTITY.counts.mcp_tools ) throw new Error("Tool family inventory does not match TOOL_CONTRACTS"); return { total: familyTotal, families }; diff --git a/scripts/lib/mcp-verification-environment.d.mts b/scripts/lib/mcp-verification-environment.d.mts index 7020c94ec4..8bc6d2a8b0 100644 --- a/scripts/lib/mcp-verification-environment.d.mts +++ b/scripts/lib/mcp-verification-environment.d.mts @@ -1,10 +1,3 @@ export const INLINE_EVIDENCE_MCP_ENVIRONMENT: { readonly REA_MCP_MAX_INPUT_ELEMENTS: "65536"; }; - -/** Select complete output contracts when validating advertised schemas. */ -export function schemaVerifierEnvironment< - Environment extends Record, ->( - environment: Environment, -): Environment & { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }; diff --git a/scripts/lib/mcp-verification-environment.mjs b/scripts/lib/mcp-verification-environment.mjs index de028cb049..602719f1b6 100644 --- a/scripts/lib/mcp-verification-environment.mjs +++ b/scripts/lib/mcp-verification-environment.mjs @@ -4,9 +4,3 @@ export const INLINE_EVIDENCE_MCP_ENVIRONMENT = Object.freeze({ REA_MCP_MAX_INPUT_ELEMENTS: "65536", }); - -/** Select complete output contracts when validating advertised schemas. */ -export const schemaVerifierEnvironment = (environment) => ({ - ...environment, - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", -}); diff --git a/scripts/lib/native-value-e2e.mjs b/scripts/lib/native-value-e2e.mjs index c16ca0763a..e26815c1f6 100644 --- a/scripts/lib/native-value-e2e.mjs +++ b/scripts/lib/native-value-e2e.mjs @@ -1,4 +1,3 @@ -import { schemaVerifierEnvironment } from "./mcp-verification-environment.mjs"; import assert from "node:assert/strict"; import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -237,7 +236,7 @@ async function verifyNativeValueAdapters( const transport = await createOwnedMcpStdioTransport({ command: process.execPath, arguments: [entrypoint, "mcp"], - hostEnvironment: schemaVerifierEnvironment(env), + hostEnvironment: env, cwd: process.cwd(), expectedCommand: null, sweepTokenOwnedProcesses: true, diff --git a/scripts/lib/product-catalog.mjs b/scripts/lib/product-catalog.mjs index 159a55a5d8..60b1933108 100644 --- a/scripts/lib/product-catalog.mjs +++ b/scripts/lib/product-catalog.mjs @@ -43,7 +43,7 @@ export const createProductCatalog = async (root) => { aliases: cli.aliases, }, runtime_catalog: { - counts: sources.catalogIdentity.FULL_CATALOG_IDENTITY.counts, + counts: sources.catalogIdentity.DEFAULT_CATALOG_IDENTITY.counts, digests: { // Full runtime schema digests belong to server identity, not this facts projection. providers_sha256: digest(providers), diff --git a/scripts/lib/real-ghidra-boundaries.mjs b/scripts/lib/real-ghidra-boundaries.mjs index 04263bfae8..08747cfa28 100644 --- a/scripts/lib/real-ghidra-boundaries.mjs +++ b/scripts/lib/real-ghidra-boundaries.mjs @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "./canonical-output-schema.mjs"; import { requireMcpToolError, mcpTextValue, @@ -43,15 +44,19 @@ export async function verifyGhidraBoundaries( addFormats(ajv); const validators = new Map(); for (const tool of catalog.tools) { - for (const key of ["inputSchema", "outputSchema"]) + const outputSchema = canonicalOutputSchema(tool.name); + for (const [key, schema] of [ + ["inputSchema", tool.inputSchema], + ["outputSchema", outputSchema], + ]) assert.ok( - ajv.validateSchema(tool[key]), + ajv.validateSchema(schema), `${tool.name} ${key}: ${JSON.stringify(ajv.errors)}`, ); validators.set( tool.name, - (tool.outputSchema?.$id && ajv.getSchema(tool.outputSchema.$id)) || - ajv.compile(tool.outputSchema), + (outputSchema.$id && ajv.getSchema(outputSchema.$id)) || + ajv.compile(outputSchema), ); } let successfulCalls = 0; diff --git a/scripts/lib/real-hopper-boundaries.mjs b/scripts/lib/real-hopper-boundaries.mjs index 837a81a97a..47c22e499f 100644 --- a/scripts/lib/real-hopper-boundaries.mjs +++ b/scripts/lib/real-hopper-boundaries.mjs @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "./canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { copyFile, mkdtemp, readFile, realpath, rm } from "node:fs/promises"; @@ -40,15 +41,19 @@ export async function verifyHopperBoundaryContracts( ajv.validateSchema(tool.inputSchema), `${tool.name} input schema`, ); - assert.ok(tool.outputSchema, `${tool.name} omitted its output schema`); assert.ok( - ajv.validateSchema(tool.outputSchema), + canonicalOutputSchema(tool.name), + `${tool.name} omitted its output schema`, + ); + assert.ok( + ajv.validateSchema(canonicalOutputSchema(tool.name)), `${tool.name} output schema`, ); validators.set( tool.name, - (tool.outputSchema?.$id && ajv.getSchema(tool.outputSchema.$id)) || - ajv.compile(tool.outputSchema), + (canonicalOutputSchema(tool.name)?.$id && + ajv.getSchema(canonicalOutputSchema(tool.name).$id)) || + ajv.compile(canonicalOutputSchema(tool.name)), ); } let successfulCalls = 0; diff --git a/scripts/lib/release-package-canary.mjs b/scripts/lib/release-package-canary.mjs index 1ae3f66ec1..6e1d023504 100644 --- a/scripts/lib/release-package-canary.mjs +++ b/scripts/lib/release-package-canary.mjs @@ -5,7 +5,7 @@ import { withVerifierCleanup } from "./verifier-cleanup.mjs"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; -import { FULL_CATALOG_IDENTITY } from "../../dist/catalogIdentity.js"; +import { DEFAULT_CATALOG_IDENTITY } from "../../dist/catalogIdentity.js"; import { verifyCompleteToolCatalog } from "./verify-package-core.mjs"; /** Install the selected archive or registry specifier with an owned ten-minute deadline. */ @@ -79,14 +79,10 @@ export async function verifyInstalledCanaryPackage( return withVerifierCleanup(async () => { try { await owner.open(client, transport); - const toolNames = await verifyCompleteToolCatalog( - client, - undefined, - environment, - ); + const toolNames = await verifyCompleteToolCatalog(client); return { mcpTools: toolNames.length, - canonicalMcpTools: FULL_CATALOG_IDENTITY.counts.mcp_tools, + canonicalMcpTools: DEFAULT_CATALOG_IDENTITY.counts.mcp_tools, }; } catch (cause) { throw new Error(`Installed package MCP verification failed: ${stderr}`, { diff --git a/scripts/lib/verify-package-core.d.mts b/scripts/lib/verify-package-core.d.mts index db565c8bc1..88d04b9142 100644 --- a/scripts/lib/verify-package-core.d.mts +++ b/scripts/lib/verify-package-core.d.mts @@ -2,6 +2,5 @@ import type { Client } from "@modelcontextprotocol/client"; export function verifyCompleteToolCatalog( client: Pick, options?: Parameters[1], - environment?: Readonly, ): Promise; export function pathExists(path: string): Promise; diff --git a/scripts/lib/verify-package-core.mjs b/scripts/lib/verify-package-core.mjs index ceb27fd5e9..2f5c65e599 100644 --- a/scripts/lib/verify-package-core.mjs +++ b/scripts/lib/verify-package-core.mjs @@ -5,23 +5,15 @@ import { Ajv2020 } from "ajv/dist/2020.js"; * Verify canonical names, converted JSON Schemas, and session catalog metadata. * Only this operation needs built contracts; path checks remain source-only. */ -export const verifyCompleteToolCatalog = async ( - client, - options, - environment = {}, -) => { +export const verifyCompleteToolCatalog = async (client, options) => { const [ { requireMcpLifecycleResult, sortVerifierNames }, { TOOL_CONTRACTS }, dialect, - { ADVERTISED_INPUT_DIALECT_BASELINE }, - { mcpInputSchemaProfileSchema }, ] = await Promise.all([ import("./mcp-verifier-results.mjs"), import("../../dist/contracts/toolContracts.js"), import("../../dist/contracts/advertisedDialect.js"), - import("../../dist/contracts/advertisedDialectBaseline.js"), - import("../../dist/config/mcpInputSchemaProfile.js"), ]); const listed = await client.listTools(undefined, options); const canonical = sortVerifierNames(TOOL_CONTRACTS.map(({ name }) => name)); @@ -48,16 +40,13 @@ export const verifyCompleteToolCatalog = async ( ); } } - // Compare the advertisement with the profiles actually passed to the server. - const inputProfile = mcpInputSchemaProfileSchema.parse( - environment.REA_MCP_INPUT_SCHEMA_PROFILE ?? "compact", - ); + // The compact input presentation is enforced at zero, like every output pattern. const regressions = [ ...dialect.advertisedDialectRegressions( dialect.countAdvertisedDialectViolations( listed.tools.flatMap(dialect.advertisedInputDialectViolations), ), - ADVERTISED_INPUT_DIALECT_BASELINE[inputProfile], + {}, ), ...dialect.advertisedDialectRegressions( dialect.countAdvertisedDialectViolations( diff --git a/scripts/record-catalog-digests.mjs b/scripts/record-catalog-digests.mjs index dc7f0e78d2..d04db9199a 100644 --- a/scripts/record-catalog-digests.mjs +++ b/scripts/record-catalog-digests.mjs @@ -13,16 +13,11 @@ const { createCatalogDigestProfiles } = await import( pathToFileURL(join(compiledRoot, "catalogIdentity.js")).href ); // Retain only hashes, with every shared record frozen. Schemas remain owned by -// the compiled contracts and are converted once for all six identities. +// the compiled contracts and are converted once for both identities. const profiles = Object.entries(createCatalogDigestProfiles()) .map( - ([input, outputs]) => - `${JSON.stringify(input)}: Object.freeze({${Object.entries(outputs) - .map( - ([output, digests]) => - `${JSON.stringify(output)}: Object.freeze(${JSON.stringify(digests)})`, - ) - .join(",")}})`, + ([output, digests]) => + `${JSON.stringify(output)}: Object.freeze(${JSON.stringify(digests)})`, ) .join(","); await writeFile( diff --git a/scripts/record-mcp-catalog.mjs b/scripts/record-mcp-catalog.mjs index eeb9c7f890..a81a11ada0 100644 --- a/scripts/record-mcp-catalog.mjs +++ b/scripts/record-mcp-catalog.mjs @@ -28,11 +28,10 @@ const [ read("domain/evidenceInputContract.js"), read("contracts/toolSchemaMetadata.js"), ]); -const recordCatalog = async (profile) => { +const recordCatalog = async () => { // Catalog generation is independent of the host account and user configuration. const home = await mkdtemp(join(tmpdir(), "rea-mcp-catalog-")); const environment = { - REA_MCP_INPUT_SCHEMA_PROFILE: profile, HOME: home, USERPROFILE: home, XDG_CONFIG_HOME: join(home, "config"), @@ -97,13 +96,7 @@ const recordCatalog = async (profile) => { }; const encoded = JSON.stringify(catalog); const digest = createHash("sha256").update(encoded).digest("hex"); - await writeFile( - join( - compiled, - profile === "full" ? "mcp-catalog-full.json" : "mcp-catalog.json", - ), - encoded, - ); + await writeFile(join(compiled, "mcp-catalog.json"), encoded); return digest; } catch (cause) { producerFailure = { cause }; @@ -132,10 +125,8 @@ const recordCatalog = async (profile) => { } }; -const digests = {}; -for (const profile of ["compact", "full"]) - digests[profile] = await recordCatalog(profile); +const digest = await recordCatalog(); await writeFile( join(compiled, "recordedMcpCatalog.js"), - `/** Recorded from the compiled runtime; do not edit. */\nexport const RECORDED_MCP_CATALOG_SHA256 = ${JSON.stringify(digests)};\n`, + `/** Recorded from the compiled runtime; do not edit. */\nexport const RECORDED_MCP_CATALOG_SHA256 = ${JSON.stringify(digest)};\n`, ); diff --git a/scripts/verify-apk-signature.mjs b/scripts/verify-apk-signature.mjs index 81875710ed..97b88453e3 100644 --- a/scripts/verify-apk-signature.mjs +++ b/scripts/verify-apk-signature.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -224,7 +224,7 @@ const client = new Client({ name: "apk-signature-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(environment), + env: environment, stderr: "pipe", }); // The MCP server child reads files under the workspace until it has exited. @@ -238,11 +238,11 @@ try { const advertised = (await client.listTools()).tools.find( ({ name }) => name === "inspect_apk_signature", ); - assert.ok(advertised?.outputSchema, "tool must advertise an output schema"); + assert.ok(advertised, "tool must advertise an output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); assert.ok(ajv.validateSchema(advertised.inputSchema), "input schema invalid"); assert.ok( - ajv.validateSchema(advertised.outputSchema), + ajv.validateSchema(canonicalOutputSchema(advertised.name)), "output schema invalid", ); @@ -264,7 +264,10 @@ try { ); assert.notEqual(reply.isError, true, mcpTextValue(reply)); assert.ok( - ajv.validate(advertised.outputSchema, reply.structuredContent), + ajv.validate( + canonicalOutputSchema(advertised.name), + reply.structuredContent, + ), JSON.stringify(ajv.errors), ); const mcp = parseEvidence(requireMcpOutput(reply, "inspect_apk_signature")); diff --git a/scripts/verify-claude-client.mjs b/scripts/verify-claude-client.mjs index 0ea9219b11..db83813eed 100644 --- a/scripts/verify-claude-client.mjs +++ b/scripts/verify-claude-client.mjs @@ -5,10 +5,6 @@ import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import { createCatalogIdentity } from "../dist/catalogIdentity.js"; -import { - compactPresentationForMcpInputSchemaProfile, - parseMcpInputSchemaProfile, -} from "../dist/config/mcpInputSchemaProfile.js"; import { parseMcpOutputSchemaProfile, resolveMcpOutputSchemaProfile, @@ -191,26 +187,18 @@ await withVerifierCleanup(async () => { ...parsedStatus, result: expandMcpSessionStatus(parsedStatus.result), }; - const inputProfile = parseMcpInputSchemaProfile( - environment.REA_MCP_INPUT_SCHEMA_PROFILE, - ); const outputProfile = parseMcpOutputSchemaProfile( environment.REA_MCP_OUTPUT_SCHEMA_PROFILE, ); - assert.ok(inputProfile.ok); assert.ok(outputProfile.ok); assert.deepEqual( status.result.server_identity.catalog, createCatalogIdentity({ - compactPresentation: - compactPresentationForMcpInputSchemaProfile( - inputProfile.value, - ), outputSchemaProfile: resolveMcpOutputSchemaProfile( outputProfile.value, ), }), - "binary_session must return the selected input/output profile's catalog identity", + "binary_session must return the selected output profile's catalog identity", ); assert.equal( Object.hasOwn(status.result.server_identity.catalog, "tools"), diff --git a/scripts/verify-copilot-client.mjs b/scripts/verify-copilot-client.mjs index acdf667085..8c8992924f 100644 --- a/scripts/verify-copilot-client.mjs +++ b/scripts/verify-copilot-client.mjs @@ -10,7 +10,7 @@ import { } from "../dist/contracts/toolContracts.js"; import { advertisedInputJsonSchema } from "../dist/contracts/toolSchemaMetadata.js"; import { presentAdvertisedInputSchema } from "../dist/contracts/compactInputSchema.js"; -import { compactPresentationForMcpInputSchemaProfile } from "../dist/config/mcpInputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../dist/config/mcpInputSchemaProfile.js"; import { isPathWithinRoot } from "../dist/domain/localPath.js"; import { createVerifierCommands } from "./lib/verify-commands.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; @@ -37,12 +37,6 @@ const command = process.env.REA_VERIFY_COPILOT_COMMAND ?? "copilot"; const mode = process.argv[2] ?? "call"; const model = process.env.REA_VERIFY_COPILOT_MODEL ?? "gpt-5.4"; const wireModel = process.env.REA_VERIFY_COPILOT_WIRE_MODEL ?? model; -const inputSchemaProfile = - process.env.REA_VERIFY_COPILOT_SCHEMA_PROFILE ?? "full"; -assert( - ["full", "compact"].includes(inputSchemaProfile), - "REA_VERIFY_COPILOT_SCHEMA_PROFILE must be full or compact", -); assert( ["chat", "call"].includes(mode), "Usage: verify-copilot-client.mjs [chat|call]", @@ -176,9 +170,9 @@ await withVerifierCleanup(async () => { advertisedInputJsonSchema( toolContract(tool.name.slice(prefix.length)), ), - compactPresentationForMcpInputSchemaProfile(inputSchemaProfile), + COMPACT_INPUT_SCHEMA_PRESENTATION, ), - `${tool.name}: Copilot must forward the selected ${inputSchemaProfile} input schema`, + `${tool.name}: Copilot must forward the compact input schema`, ); schemasChecked++; } @@ -329,15 +323,6 @@ await withVerifierCleanup(async () => { assert.deepEqual(JSON.parse(repeated.stdout).appliedActions, []); assert.equal(await readFile(configPath, "utf8"), configured); assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original); - // Bind both choices explicitly: the server's default can change independently - // of the profile this verifier promises to exercise and records in its receipt. - const customized = JSON.parse(configured); - assert(customized.mcpServers.rea); - customized.mcpServers.rea.env = { - ...customized.mcpServers.rea.env, - REA_MCP_INPUT_SCHEMA_PROFILE: inputSchemaProfile, - }; - await writeFile(configPath, JSON.stringify(customized, null, 2)); const installedSkill = await readFile( join(skillDirectory, "SKILL.md"), "utf8", @@ -442,7 +427,6 @@ await withVerifierCleanup(async () => { mode, model, wireModel, - inputSchemaProfile, passed, artifacts: lab, runtimeRoot, @@ -456,9 +440,7 @@ await withVerifierCleanup(async () => { skillDiscovery: "native COPILOT_HOME MCP configuration and caller-added isolated skill directory; default OS-home discovery unverified", capacityBoundary: - inputSchemaProfile === "compact" - ? "Explicit compact profile: complete inventory and canonical server validation; advertised schemas may be reduced. This does not fix the full-profile gpt-4.1 context boundary." - : "Full advertised input schemas, checked at the client request boundary. Client model ID and wire model are recorded separately; effective token capacity and live provider compatibility remain unknown.", + "Compact input schemas, checked at the client request boundary: complete inventory and canonical server validation; advertised schemas may be reduced. Client model ID and wire model are recorded separately; effective token capacity and live provider compatibility remain unknown.", catalogSize, schemasChecked, spillReadRequested, diff --git a/scripts/verify-gemini-client.mjs b/scripts/verify-gemini-client.mjs index b90ceabbb1..f5c404f0f4 100644 --- a/scripts/verify-gemini-client.mjs +++ b/scripts/verify-gemini-client.mjs @@ -34,8 +34,6 @@ const repo = fileURLToPath(new URL("..", import.meta.url)); const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo; const command = process.env.REA_VERIFY_GEMINI_COMMAND ?? "gemini"; const mode = process.argv[2] ?? "call"; -const schemaProfile = process.env.REA_VERIFY_GEMINI_SCHEMA_PROFILE ?? "full"; -assert(["full", "compact"].includes(schemaProfile)); // npm installs of Gemini CLI provide a JavaScript entry point; on Windows only a gemini.cmd shim reaches // PATH, which Node does not spawn without a shell, so a selected .js entry runs under this Node. It is // resolved once, because the preflight runs from the caller's directory and the client from the workspace. @@ -99,7 +97,6 @@ await withVerifierCleanup(async () => { GEMINI_CLI_NO_RELAUNCH: "1", GEMINI_CLI_TRUST_WORKSPACE: "true", REA_PROCESS_RUN_ID: verifier.run_id, - REA_MCP_INPUT_SCHEMA_PROFILE: schemaProfile, }; try { clientVersion = ( @@ -417,7 +414,6 @@ await withVerifierCleanup(async () => { client: "Gemini CLI", clientVersion, mode, - schemaProfile, schemasChecked, examplesChecked, passed, diff --git a/scripts/verify-go-binary.mjs b/scripts/verify-go-binary.mjs index eaa8f544dd..cf9bc1327d 100644 --- a/scripts/verify-go-binary.mjs +++ b/scripts/verify-go-binary.mjs @@ -1,5 +1,4 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; import assert from "node:assert/strict"; import { isUtf8 } from "node:buffer"; import { createHash } from "node:crypto"; @@ -82,7 +81,7 @@ try { transport = await createOwnedMcpStdioTransport({ command: process.execPath, arguments: [entrypoint, "mcp"], - hostEnvironment: schemaVerifierEnvironment(environment), + hostEnvironment: environment, cwd: process.cwd(), expectedCommand: null, sweepTokenOwnedProcesses: true, diff --git a/scripts/verify-http3-network-captures.mjs b/scripts/verify-http3-network-captures.mjs index da5fea9f43..54c5e0e215 100644 --- a/scripts/verify-http3-network-captures.mjs +++ b/scripts/verify-http3-network-captures.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; @@ -65,7 +65,7 @@ const client = new Client({ const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(environment), + env: environment, stderr: "pipe", }); const contract = toolContract("inspect_web_network_capture"); @@ -213,11 +213,11 @@ try { const advertised = (await client.listTools()).tools.find( (tool) => tool.name === contract.name, ); - assert.ok(advertised?.outputSchema, "Missing named advertised schema"); + assert.ok(advertised, "Missing named advertised schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); assert.ok(ajv.validateSchema(advertised.inputSchema)); - assert.ok(ajv.validateSchema(advertised.outputSchema)); - validateOutput = ajv.compile(advertised.outputSchema); + assert.ok(ajv.validateSchema(canonicalOutputSchema(advertised.name))); + validateOutput = ajv.compile(canonicalOutputSchema(advertised.name)); const inputValidator = ajv.compile(advertised.inputSchema); assert.ok( inputValidator({ diff --git a/scripts/verify-package-mcp.mjs b/scripts/verify-package-mcp.mjs index d77235befa..6487f7b3f9 100644 --- a/scripts/verify-package-mcp.mjs +++ b/scripts/verify-package-mcp.mjs @@ -17,8 +17,8 @@ import * as prompts from "./verify-package-prompts.mjs"; import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs"; import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs"; -const verifyMcpToolsAndPrompts = async (client, mcpOptions, environment) => { - await verifyCompleteToolCatalog(client, mcpOptions, environment); +const verifyMcpToolsAndPrompts = async (client, mcpOptions) => { + await verifyCompleteToolCatalog(client, mcpOptions); await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names); await prompts.verifyPromptCompletion(client, mcpOptions, []); }; @@ -283,7 +283,7 @@ export async function verifyPackageMcp({ try { await client.connect(transport); const mcpOptions = { timeout: 15_000 }; - await verifyMcpToolsAndPrompts(client, mcpOptions, environment); + await verifyMcpToolsAndPrompts(client, mcpOptions); await verifyMcpTargetFree(client, mcpOptions); await verifyMcpUnknownProvider(client, mcpOptions); await verifyMcpBinaryLifecycle(client, mcpOptions); diff --git a/scripts/verify-package-pack.mjs b/scripts/verify-package-pack.mjs index 3f70297943..0e4b26b184 100644 --- a/scripts/verify-package-pack.mjs +++ b/scripts/verify-package-pack.mjs @@ -2,7 +2,7 @@ import { importVerifierSource } from "./lib/verifier-source-loader.mjs"; import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import { FULL_CATALOG_IDENTITY } from "../dist/catalogIdentity.js"; +import { DEFAULT_CATALOG_IDENTITY } from "../dist/catalogIdentity.js"; import { parseTarListing } from "./lib/verify-package-listing.mjs"; import { verifyPackedBridge } from "./verify-packed-bridge.mjs"; @@ -213,7 +213,8 @@ export async function verifyPackagePack({ root, workspace, commands }) { const skillMetadata = parseSkillManifestMetadata(skill); if ( !skillMetadata.ok || - skillMetadata.value.toolCount !== FULL_CATALOG_IDENTITY.counts.mcp_tools || + skillMetadata.value.toolCount !== + DEFAULT_CATALOG_IDENTITY.counts.mcp_tools || skillMetadata.value.hasCatalogDigest ) throw new Error( diff --git a/scripts/verify-packet-capture.mjs b/scripts/verify-packet-capture.mjs index e3c3806763..6d0fceddfc 100644 --- a/scripts/verify-packet-capture.mjs +++ b/scripts/verify-packet-capture.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { requireMcpEvidenceBundle, @@ -46,7 +46,7 @@ const client = new Client({ name: "packet-capture-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(environment), + env: environment, stderr: "pipe", }); @@ -120,17 +120,20 @@ await withVerifierCleanup(async () => { const tool = (await client.listTools()).tools.find( ({ name }) => name === "inspect_packet_capture", ); - assert.ok(tool?.outputSchema); + assert.ok(tool); const ajv = new Ajv2020({ strict: false, validateFormats: false }); assert.ok(ajv.validateSchema(tool.inputSchema)); - assert.ok(ajv.validateSchema(tool.outputSchema)); + assert.ok(ajv.validateSchema(canonicalOutputSchema(tool.name))); const inspect = async (input) => { const response = await client.callTool({ name: tool.name, arguments: input, }); assert.ok( - ajv.validate(tool.outputSchema, response.structuredContent), + ajv.validate( + canonicalOutputSchema(tool.name), + response.structuredContent, + ), JSON.stringify(ajv.errors), ); const evidence = parseEvidence( @@ -276,10 +279,10 @@ await withVerifierCleanup(async () => { const pcapTool = (await client.listTools()).tools.find( (t) => t.name === "inspect_pcap_capture", ); - assert.ok(pcapTool?.outputSchema); + assert.ok(pcapTool); const pcapAjv = new Ajv2020({ strict: false, validateFormats: false }); assert.ok(pcapAjv.validateSchema(pcapTool.inputSchema)); - assert.ok(pcapAjv.validateSchema(pcapTool.outputSchema)); + assert.ok(pcapAjv.validateSchema(canonicalOutputSchema(pcapTool.name))); for (const capturePath of [path, bluetooth]) { const capinfosOutput = ( await execute( @@ -316,7 +319,10 @@ await withVerifierCleanup(async () => { arguments: { path: capturePath, detail: "summary" }, }); assert.ok( - pcapAjv.validate(pcapTool.outputSchema, response.structuredContent), + pcapAjv.validate( + canonicalOutputSchema(pcapTool.name), + response.structuredContent, + ), pcapAjv.errorsText(), ); const summary = parseEvidence( @@ -393,9 +399,9 @@ await withVerifierCleanup(async () => { const discovery = (await client.listTools()).tools.find( ({ name }) => name === "inspect_packet_fields", ); - assert.ok(discovery?.outputSchema); + assert.ok(discovery); assert.ok(ajv.validateSchema(discovery.inputSchema)); - assert.ok(ajv.validateSchema(discovery.outputSchema)); + assert.ok(ajv.validateSchema(canonicalOutputSchema(discovery.name))); const nativeProfile = join(root.path, "native-profile"); await mkdir(nativeProfile); const native = ( @@ -422,7 +428,10 @@ await withVerifierCleanup(async () => { arguments: input, }); assert.ok( - ajv.validate(discovery.outputSchema, response.structuredContent), + ajv.validate( + canonicalOutputSchema(discovery.name), + response.structuredContent, + ), JSON.stringify(ajv.errors), ); const evidence = parseEvidence( diff --git a/scripts/verify-qlog-captures.mjs b/scripts/verify-qlog-captures.mjs index d0ada1879a..fcea83edfb 100644 --- a/scripts/verify-qlog-captures.mjs +++ b/scripts/verify-qlog-captures.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import { closeVerifierResources, withVerifierCleanup, @@ -41,7 +41,7 @@ const environment = { const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(environment), + env: environment, stderr: "pipe", }); let diagnostics = ""; @@ -60,10 +60,10 @@ const report = await withVerifierCleanup(async () => { const tool = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_web_network_capture", ); - assert.ok(tool?.outputSchema); - for (const schema of [tool.inputSchema, tool.outputSchema]) + assert.ok(tool); + for (const schema of [tool.inputSchema, canonicalOutputSchema(tool.name)]) assert.ok(ajv.validateSchema(schema)); - const validateOutput = ajv.compile(tool.outputSchema); + const validateOutput = ajv.compile(canonicalOutputSchema(tool.name)); for (const [lane, path] of [ ["control", controlPath], ["loss", lossPath], diff --git a/scripts/verify-real-browser.mjs b/scripts/verify-real-browser.mjs index 164a2ff987..b6f73f2c22 100644 --- a/scripts/verify-real-browser.mjs +++ b/scripts/verify-real-browser.mjs @@ -51,7 +51,6 @@ import { verifyScenarioStorage } from "./lib/browser-scenario-storage-e2e.mjs"; import { verifyScenarioStorageFingerprints } from "./lib/browser-scenario-storage-fingerprints-e2e.mjs"; import { verifyScenarioServiceWorkers } from "./lib/browser-scenario-service-workers-e2e.mjs"; import { verifyBrowserUrlUserInfo } from "./lib/browser-url-userinfo-e2e.mjs"; -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret"; const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret"; @@ -448,10 +447,10 @@ async function verifyPublicBundle(endpoint, target, origin) { const transport = new StdioClientTransport({ command: process.execPath, args: [fileURLToPath(new URL("./rea.mjs", import.meta.url)), "mcp"], - env: schemaVerifierEnvironment({ + env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent", - }), + }, stderr: "pipe", }); const client = new Client({ name: "browser-bundle-e2e", version: "1" }); diff --git a/scripts/verify-real-ghidra-com.mjs b/scripts/verify-real-ghidra-com.mjs index f1aa635c11..a38bf8dce6 100644 --- a/scripts/verify-real-ghidra-com.mjs +++ b/scripts/verify-real-ghidra-com.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import { closeVerifierResources } from "./lib/verifier-cleanup.mjs"; import assert from "node:assert/strict"; import { @@ -92,7 +92,7 @@ const env = { const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(env), + env: env, stderr: "pipe", }); const client = new Client({ name: "rea-real-com-proof", version: "1" }); @@ -215,11 +215,17 @@ await withVerifierFixture(workspaceFixture, async () => { const advertised = tools.find( (tool) => tool.name === "inspect_native_instruction", ); - assert.ok(advertised?.outputSchema); + assert.ok(advertised); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - assert.equal(ajv.validateSchema(advertised.outputSchema), true); assert.equal( - ajv.validate(advertised.outputSchema, unlistedResponse.structuredContent), + ajv.validateSchema(canonicalOutputSchema(advertised.name)), + true, + ); + assert.equal( + ajv.validate( + canonicalOutputSchema(advertised.name), + unlistedResponse.structuredContent, + ), true, JSON.stringify(ajv.errors), ); @@ -234,7 +240,7 @@ await withVerifierFixture(workspaceFixture, async () => { const contextDefinition = tools.find( (tool) => tool.name === "inspect_address_context", ); - assert.ok(contextDefinition?.outputSchema); + assert.ok(contextDefinition); const documents = await call("list_documents"); assert.equal(documents.length, 1); const document = documents[0]; @@ -314,7 +320,7 @@ await withVerifierFixture(workspaceFixture, async () => { // Ghidra, rather than assuming the provider exposes direct comment methods. const contextCall = async (name, args) => { const definition = tools.find((tool) => tool.name === name); - assert.ok(definition?.outputSchema, `${name} must be advertised`); + assert.ok(definition, `${name} must be advertised`); const response = await client.callTool( { name, arguments: args }, { timeout: 240000, toolDefinition: definition }, diff --git a/scripts/verify-real-ghidra-raw.mjs b/scripts/verify-real-ghidra-raw.mjs index 12771410ac..1955471a15 100644 --- a/scripts/verify-real-ghidra-raw.mjs +++ b/scripts/verify-real-ghidra-raw.mjs @@ -1,4 +1,4 @@ -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { withCanonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -97,7 +97,7 @@ const env = { const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(env), + env: env, stderr: "pipe", }); const client = new Client({ name: "rea-real-raw-proof", version: "1" }); @@ -135,10 +135,11 @@ await withVerifierFixture(workspaceFixture, async () => { try { await client.connect(transport); connected = true; - searchDefinition = (await client.listTools()).tools.find( + const listedSearch = (await client.listTools()).tools.find( (tool) => tool.name === "search_bytes", ); - assert.ok(searchDefinition?.outputSchema); + assert.ok(listedSearch); + searchDefinition = withCanonicalOutputSchema(listedSearch); const profiles = []; const sessions = []; for (const entryPoint of ["0x08000000", "0x8000000", "0x09000000"]) { @@ -734,7 +735,7 @@ async function verifyCrossBlockProject(target, targetSha256) { command: process.execPath, args: [entrypoint, "mcp"], env: { - ...schemaVerifierEnvironment(env), + ...env, REA_GHIDRA_PROJECT_PATH: join(projectDirectory, "Boundary.gpr"), }, stderr: "pipe", @@ -748,7 +749,7 @@ async function verifyCrossBlockProject(target, targetSha256) { const definition = (await projectClient.listTools()).tools.find( (tool) => tool.name === "search_bytes", ); - assert.ok(definition?.outputSchema); + assert.ok(definition); const projectCall = async (name, args) => { const reply = await projectClient.callTool( { name, arguments: args }, diff --git a/scripts/verify-real-ghidra-riscv.mjs b/scripts/verify-real-ghidra-riscv.mjs index 6345bdeebd..5083f46ebe 100644 --- a/scripts/verify-real-ghidra-riscv.mjs +++ b/scripts/verify-real-ghidra-riscv.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -90,7 +90,7 @@ for (const key of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(env), + env: env, stderr: "pipe", }); const client = new Client({ name: "rea-real-riscv-proof", version: "1" }); @@ -132,14 +132,17 @@ try { const validators = new Map(); for (const tool of catalog.tools) { assert.ok( - ajv.validateSchema(tool.outputSchema), + ajv.validateSchema(canonicalOutputSchema(tool.name)), `${tool.name}: invalid output schema`, ); const existing = - typeof tool.outputSchema.$id === "string" - ? ajv.getSchema(tool.outputSchema.$id) + typeof canonicalOutputSchema(tool.name).$id === "string" + ? ajv.getSchema(canonicalOutputSchema(tool.name).$id) : undefined; - validators.set(tool.name, existing ?? ajv.compile(tool.outputSchema)); + validators.set( + tool.name, + existing ?? ajv.compile(canonicalOutputSchema(tool.name)), + ); } const typedSoft = await buildRiscvFixture(workspace, 32, "soft"); diff --git a/scripts/verify-real-hopper-callers.mjs b/scripts/verify-real-hopper-callers.mjs index e4386751a6..a5922ad093 100644 --- a/scripts/verify-real-hopper-callers.mjs +++ b/scripts/verify-real-hopper-callers.mjs @@ -1,4 +1,3 @@ -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; // Run from the repository root after `npm run build:cached` on macOS arm64, // with Hopper and the Xcode command-line tools installed. No target is executed. import assert from "node:assert/strict"; @@ -68,12 +67,12 @@ const transport = new StdioClientTransport({ command: process.execPath, args: ["dist/main.js"], cwd: root, - env: schemaVerifierEnvironment({ + env: { ...process.env, REA_ANALYSIS_PROVIDER: "hopper", REA_LOG_LEVEL: "silent", REA_HOPPER_CALLER_SCAN_BUDGET_MS: "5000", - }), + }, stderr: "pipe", }); let stderr = ""; diff --git a/scripts/verify-real-hopper-fat.mjs b/scripts/verify-real-hopper-fat.mjs index 5a6c78a565..f094c40e49 100644 --- a/scripts/verify-real-hopper-fat.mjs +++ b/scripts/verify-real-hopper-fat.mjs @@ -1,4 +1,4 @@ -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; @@ -82,11 +82,11 @@ const client = new Client({ name: "hopper-fat-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, args: ["dist/main.js"], - env: schemaVerifierEnvironment({ + env: { ...process.env, REA_ANALYSIS_PROVIDER: "hopper", HOPPER_LOADER_ARGS_JSON: "[]", - }), + }, stderr: "pipe", }); transport.stderr?.on("data", (chunk) => process.stderr.write(chunk)); @@ -259,10 +259,11 @@ await withVerifierFixture(workspace, async () => { { timeout: 180_000 }, ); const result = requireMcpOperationResult(reply, name); - const schema = catalog.tools.find( - (tool) => tool.name === name, - )?.outputSchema; - assert.ok(schema, `${name} omitted its output schema`); + assert.ok( + catalog.tools.some((tool) => tool.name === name), + `${name} is not advertised`, + ); + const schema = canonicalOutputSchema(name); assert.equal( ajv.validate(schema, reply.structuredContent), true, diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs index 2768d7bf31..ca183ffd0b 100644 --- a/scripts/verify-real-hopper.mjs +++ b/scripts/verify-real-hopper.mjs @@ -1,4 +1,3 @@ -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; import { execFile } from "node:child_process"; import { tmpdir } from "node:os"; import { promisify } from "node:util"; @@ -234,7 +233,7 @@ const targetB = fixtureTargets.secondary.path; const largeTarget = fixtureTargets.large.path; const targetHashA = fixtureTargets.primary.sha256; const targetHashB = fixtureTargets.secondary.sha256; -const serverEnvironment = schemaVerifierEnvironment(process.env); +const serverEnvironment = { ...process.env }; serverEnvironment.REA_ANALYSIS_PROVIDER = "auto"; delete serverEnvironment.HOPPER_TARGET_PATH; delete serverEnvironment.HOPPER_SECOND_TARGET_PATH; diff --git a/scripts/verify-recorded-crash.mjs b/scripts/verify-recorded-crash.mjs index cba8b21ecc..901861b759 100644 --- a/scripts/verify-recorded-crash.mjs +++ b/scripts/verify-recorded-crash.mjs @@ -96,13 +96,9 @@ try { advertised.filter((tool) => tool.name === "inspect_recorded_crash").length, 1, ); - const catalog = await verifyCompleteToolCatalog( - basic.client, - { - timeout: 90_000, - }, - environment, - ); + const catalog = await verifyCompleteToolCatalog(basic.client, { + timeout: 90_000, + }); for (const name of [ "inspect_binary_layout", "inspect_recorded_crash", diff --git a/scripts/verify-signal-recording.mjs b/scripts/verify-signal-recording.mjs index d5526a995d..5abc58e5c9 100644 --- a/scripts/verify-signal-recording.mjs +++ b/scripts/verify-signal-recording.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { requireMcpEvidenceBundle, @@ -80,7 +80,7 @@ const assertRejectedSelector = ( const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment(environment), + env: environment, stderr: "pipe", }); // The MCP server child reads files under the root until it has exited. @@ -119,10 +119,10 @@ await withVerifierFixture(root, async () => { const advertised = (await client.listTools()).tools.find( ({ name }) => name === "decode_signal_recording", ); - assert.ok(advertised?.outputSchema); + assert.ok(advertised); const ajv = new Ajv2020({ strict: false, validateFormats: false }); assert.ok(ajv.validateSchema(advertised.inputSchema)); - assert.ok(ajv.validateSchema(advertised.outputSchema)); + assert.ok(ajv.validateSchema(canonicalOutputSchema(advertised.name))); for (const extra of [ { input_format: "vcd", annotations: "uart=rx-data" }, {}, @@ -133,7 +133,10 @@ await withVerifierFixture(root, async () => { arguments: input, }); assert.ok( - ajv.validate(advertised.outputSchema, response.structuredContent), + ajv.validate( + canonicalOutputSchema(advertised.name), + response.structuredContent, + ), JSON.stringify(ajv.errors), ); const evidence = parseEvidence( @@ -249,7 +252,10 @@ await withVerifierFixture(root, async () => { arguments: input, }); assert.ok( - ajv.validate(advertised.outputSchema, response.structuredContent), + ajv.validate( + canonicalOutputSchema(advertised.name), + response.structuredContent, + ), JSON.stringify(ajv.errors), ); const evidence = parseEvidence( @@ -325,9 +331,9 @@ await withVerifierFixture(root, async () => { const discoveryTool = (await client.listTools()).tools.find( ({ name }) => name === "inspect_signal_decoders", ); - assert.ok(discoveryTool?.outputSchema); + assert.ok(discoveryTool); assert.ok(ajv.validateSchema(discoveryTool.inputSchema)); - assert.ok(ajv.validateSchema(discoveryTool.outputSchema)); + assert.ok(ajv.validateSchema(canonicalOutputSchema(discoveryTool.name))); const nativeHelp = ( await execute( process.env.REA_SIGROK_COMMAND, @@ -379,7 +385,10 @@ await withVerifierFixture(root, async () => { arguments: input, }); assert.ok( - ajv.validate(discoveryTool.outputSchema, response.structuredContent), + ajv.validate( + canonicalOutputSchema(discoveryTool.name), + response.structuredContent, + ), JSON.stringify(ajv.errors), ); const evidence = parseEvidence( diff --git a/scripts/verify-wasm-artifact.mjs b/scripts/verify-wasm-artifact.mjs index 75148ded8e..861583866a 100644 --- a/scripts/verify-wasm-artifact.mjs +++ b/scripts/verify-wasm-artifact.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { schemaVerifierEnvironment } from "./lib/mcp-verification-environment.mjs"; +import { canonicalOutputSchema } from "./lib/canonical-output-schema.mjs"; import assert from "node:assert/strict"; import { execFile } from "node:child_process"; import { createHash } from "node:crypto"; @@ -90,12 +90,12 @@ const callTool = (request) => const transport = new StdioClientTransport({ command: process.execPath, args: [entrypoint, "mcp"], - env: schemaVerifierEnvironment({ + env: { ...environment, TMPDIR: childTempRoot, TMP: childTempRoot, TEMP: childTempRoot, - }), + }, stderr: "pipe", }); let cases = 0; @@ -110,10 +110,10 @@ try { const advertised = (await client.listTools()).tools.find( ({ name }) => name === "inspect_wasm_artifact", ); - assert.ok(advertised?.outputSchema, "WASM tool must advertise output schema"); + assert.ok(advertised, "WASM tool must advertise output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); assert.ok(ajv.validateSchema(advertised.inputSchema)); - assert.ok(ajv.validateSchema(advertised.outputSchema)); + assert.ok(ajv.validateSchema(canonicalOutputSchema(advertised.name))); const inspectCli = async (input) => { try { return JSON.parse( @@ -280,7 +280,10 @@ try { }); assert.notEqual(reply.isError, true, mcpTextValue(reply)); assert.ok( - ajv.validate(advertised.outputSchema, reply.structuredContent), + ajv.validate( + canonicalOutputSchema(advertised.name), + reply.structuredContent, + ), JSON.stringify(ajv.errors), ); const mcp = parseEvidence(requireMcpOutput(reply, "inspect_wasm_artifact")); @@ -521,7 +524,10 @@ try { }); assert.notEqual(reply.isError, true, mcpTextValue(reply)); assert.ok( - ajv.validate(advertised.outputSchema, reply.structuredContent), + ajv.validate( + canonicalOutputSchema(advertised.name), + reply.structuredContent, + ), JSON.stringify(ajv.errors), ); const mcp = parseEvidence(requireMcpOutput(reply, "inspect_wasm_artifact")); @@ -587,7 +593,10 @@ try { }); assert.notEqual(reply.isError, true, mcpTextValue(reply)); assert.ok( - ajv.validate(advertised.outputSchema, reply.structuredContent), + ajv.validate( + canonicalOutputSchema(advertised.name), + reply.structuredContent, + ), JSON.stringify(ajv.errors), ); const mcp = parseEvidence(requireMcpOutput(reply, "inspect_wasm_artifact")); diff --git a/scripts/verify-windows-package.mjs b/scripts/verify-windows-package.mjs index 5739b2b55f..532fcaf02c 100644 --- a/scripts/verify-windows-package.mjs +++ b/scripts/verify-windows-package.mjs @@ -105,9 +105,7 @@ try { let toolCount = 0; await withVerifierCleanup(async () => { await client.connect(server); - toolCount = ( - await verifyCompleteToolCatalog(client, undefined, environment) - ).length; + toolCount = (await verifyCompleteToolCatalog(client)).length; }, [ async () => { try { diff --git a/scripts/verify/advertised-schemas/summary.json b/scripts/verify/advertised-schemas/summary.json index 8374a42226..d4a21d49f8 100644 --- a/scripts/verify/advertised-schemas/summary.json +++ b/scripts/verify/advertised-schemas/summary.json @@ -1,150 +1,27 @@ { "patterns": { - "^(0|[1-9][0-9]*)$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_web_network_capture output" - ], - "^(?:(?:[0-9][0-9][2468][048]|[0-9][0-9][13579][26]|[0-9][0-9]0[48]|[02468][048]00|[13579][26]00)-02-29|[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|(?:02)-(?:0[1-9]|1[0-9]|2[0-8])))T(?:[01][0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9](?:\\.[0-9]+)?)?(?:Z|[+\\-](?:[01][0-9]|2[0-3]):[0-5][0-9])$": [ - "evaluate_reconstruction_coverage input" - ], - "^(?:(?:[0-9][0-9][2468][048]|[0-9][0-9][13579][26]|[0-9][0-9]0[48]|[02468][048]00|[13579][26]00)-02-29|[0-9]{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01])|(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30)|(?:02)-(?:0[1-9]|1[0-9]|2[0-8])))T(?:[01][0-9]|2[0-3]):[0-5][0-9](?::[0-5][0-9](?:\\.[0-9]+)?)?Z$": [ - "binary_session output", - "capture_browser_scenario output", - "capture_process_scenario output", - "capture_web_screenshot output", - "inspect_analysis_view output", - "inspect_electron_page output", - "inspect_web_event_listeners output", - "inspect_web_page output", - "observe_web_execution output", - "observe_web_session output" - ], - "^(?:(?:[\\-.0-9A-Z_a-z~]|%[0-9A-F]{2})+:)?0x(?:0|[1-9a-f][0-9a-f]*)$": [ - "build_call_path output", - "compare_functions output" - ], - "^(?:(?:[\\-.0-9A-Z_a-z~]|%[0-9A-F]{2})+:)?0x[0-9a-f]+$": [ - "inspect_analysis_view output", - "inspect_native_load_image output" - ], - "^(?:(?:[\\-.0-9A-Z_a-z~]|%[0-9A-Fa-f]{2})+:)?0[Xx][0-9A-Fa-f]+$": [ - "build_call_path input" - ], - "^(?:/[^\\x0a\\x0d]*)?$": ["capture_browser_scenario input"], - "^(?:0|-?[1-9][0-9]*)$": [ - "inspect_analysis_view output", - "inspect_android_class output", - "inspect_artifact output", - "inspect_ntfs_journal output" - ], - "^(?:0|[1-9][0-9]*)$": [ - "compare_wasm_interfaces output", - "inspect_analysis_view output", - "inspect_apple_ips output", - "inspect_artifact output", - "inspect_elf_unwind_ranges output", - "inspect_gpu_code_objects output", - "inspect_model_artifact output", - "inspect_ntfs_journal output", - "inspect_wasm_artifact output", - "inspect_windows_shortcut output" - ], - "^(?:NaN|-?Infinity|-?[0-9]+\\.[0-9]+(?:E-?[0-9]+)?)$": [ - "inspect_analysis_view output", - "inspect_android_class output" - ], - "^(?:[0-9]+|0x[0-9a-f]*)$": [ - "export_environment_bundle input", - "measure_build_task input" - ], - "^(?:[0-9a-f]{2})*$": [ - "extract_firmware output", - "inspect_analysis_view output", - "inspect_apk_signature output", - "inspect_evm_interface output", - "inspect_macho output", - "inspect_managed_members output", - "inspect_native_load_image output", - "inspect_sqlite_database output", - "read_bytes output" - ], - "^(?:[0-9a-f]{2})+$": [ - "capture_process_scenario output", - "identify_flutter_build output" - ], - "^(?:[0-9a-f]{2}){0,252}$": [ - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^(?:[0-9a-f]{2}){1,16}$": [ - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^(?:[0-9a-f]{2}){1,63}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_pcap_capture output" - ], - "^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$": ["inspect_windows_shortcut output"], - "^(?:[0-9a-f]{40}|[0-9a-f]{64})$": ["compare_source_to_bundle input"], - "^(?:[0-9a-f]{4})*$": ["inspect_pe_resources output"], - "^(?:[0-9a-f]{64}|[0-9a-f]{96})$": [ - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^(?:[0-9a-f]{8}|[0-9a-f]{16})$": ["inspect_kernel_module output"], "^(?:[^\\x00.\\x2f\\\\][^\\x00\\x2f\\\\]*|\\.[^\\x00.\\x2f\\\\][^\\x00\\x2f\\\\]*|\\.\\.[^\\x00\\x2f\\\\]+)(?:/(?:[^\\x00.\\x2f\\\\][^\\x00\\x2f\\\\]*|\\.[^\\x00.\\x2f\\\\][^\\x00\\x2f\\\\]*|\\.\\.[^\\x00\\x2f\\\\]+))*$": [ "trace_dylib_resolution input" ], - "^(?:[^\\x00=R][^\\x00=]*|R(?:[^\\x00=E][^\\x00=]*|E(?:[^\\x00=A][^\\x00=]*|A(?:[^\\x00=_][^\\x00=]*|_(?:[^\\x00=P][^\\x00=]*|P(?:[^\\x00=R][^\\x00=]*|R(?:[^\\x00=O][^\\x00=]*|O(?:[^\\x00=C][^\\x00=]*|C(?:[^\\x00=E][^\\x00=]*|E(?:[^\\x00=S][^\\x00=]*|S(?:[^\\x00=S][^\\x00=]*|S(?:[^\\x00=_][^\\x00=]*|_(?:[^\\x00=R][^\\x00=]*|R(?:[^\\x00=U][^\\x00=]*|U(?:[^\\x00=N][^\\x00=]*|N(?:[^\\x00=_][^\\x00=]*|_(?:[^\\x00=I][^\\x00=]*|I(?:[^\\x00=D][^\\x00=]*|D[^\\x00=]+)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)?)$": [ - "capture_process_scenario input", - "capture_process_scenario output" - ], - "^(?:[^\\x00=_][^\\x00=]*|_(?:[^\\x00=_][^\\x00=]*|_(?:[^\\x00=p][^\\x00=]*|p(?:[^\\x00=r][^\\x00=]*|r(?:[^\\x00=o][^\\x00=]*|o(?:[^\\x00=t][^\\x00=]*|t(?:[^\\x00=o][^\\x00=]*|o(?:[^\\x00=_][^\\x00=]*|_(?:[^\\x00=_][^\\x00=]*|_[^\\x00=]+)?)?)?)?)?)?)?)?)$": [ - "capture_process_scenario input", - "capture_process_scenario output" - ], - "^(?:[^\\x0a\\x0d][^\\x0a\\x0d][^\\x0a\\x0d][^\\x0a\\x0d])*$": [ - "inspect_analysis_view output", - "inspect_binary_layout output", - "inspect_go_binary output", - "inspect_il2cpp_metadata output", - "inspect_native_exports output", - "inspect_recorded_crash output", - "inspect_web_network_capture output" - ], "^(?:[a-z]{2,3}(?:-(?:r?[A-Z]{2}|[0-9]{3}))?|b\\+[a-z]{2,3}(?:\\+[0-9A-Za-z]{2,8})*)$": [ "decode_android_resources input" ], - "^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$": [ - "export_environment_bundle input", - "measure_build_task input" - ], "^([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[1-8][0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$": [ "act_on_build_computer input", "act_on_build_computer output", - "binary_session output", - "check_build_task input", - "check_build_task output", "close_build_computer input", "close_build_computer output", "close_frida_session input", "close_gdb_session input", "close_rizin_debug_session input", "cutter_command input", - "export_build_environment input", "export_build_environment output", "frida_session_status input", - "frida_session_status output", "gdb_console input", "gdb_session_status input", "gdb_session_status output", - "inspect_analysis_view output", - "instrument_with_frida output", "list_cutter_sessions output", "load_frida_script input", - "load_frida_script output", "mark_build_checkpoint input", "mark_build_checkpoint output", "record_build_consent input", @@ -160,589 +37,55 @@ "start_gdb_session output", "start_rizin_debug_session output", "stop_build_capture input", - "stop_build_capture output", "unload_frida_script input" ], - "^(true|false)$": ["start_adb_activity input"], - "^-?(?:0|[1-9][0-9]*)$": [ - "inspect_analysis_view output", - "inspect_apple_ips output", - "inspect_evm_interface output", - "inspect_sqlite_database output" - ], - "^-?(?:0|[1-9][0-9]{0,18})$": ["inspect_binary_layout output"], - "^-?[0-9]+$": [ - "analyze_function output", - "annotate_native_function output", - "get_objc_classes output", - "inspect_adb_package output", - "inspect_native_dispatch_metadata output", - "procedure_info output", - "start_adb_activity input" - ], - "^/(?:[^\\x2f\\\\]|$)[^\\x00]*(?:\\x00[^\\x00]*)*$": [ - "capture_browser_scenario input" - ], + "^-?[0-9]+$": ["start_adb_activity input"], "^/[^\\x00]*$": [ "list_adb_directory input", "pull_adb_file input", "push_adb_file input" ], - "^/[^\\x00]*(?:\\x00[^\\x00]*)*$": [ - "analyze_javascript_application input", - "analyze_javascript_application output", - "analyze_web_bundle output", - "audit_release_exposure input", - "binary_session input", - "capture_browser_scenario input", - "capture_electron_scenario input", - "check_build_task input", - "close_binary input", - "compare_artifacts input", - "compare_bundles input", - "compare_source_to_bundle input", - "decode_signal_recording input", - "decompile_all input", - "export_build_environment input", - "export_environment_bundle input", - "export_evidence_bundle input", - "export_web_scripts input", - "extract_artifact input", - "extract_firmware input", - "import_evidence_bundle input", - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_android_class input", - "inspect_android_method input", - "inspect_api_contract input", - "inspect_apk_signature input", - "inspect_apple_ips input", - "inspect_binary_hardening input", - "inspect_binary_hardening output", - "inspect_binary_layout input", - "inspect_byte_entropy input", - "inspect_desktop_application input", - "inspect_devicetree input", - "inspect_electron_fuses input", - "inspect_elf_notes input", - "inspect_elf_unwind_ranges input", - "inspect_erlang_beam input", - "inspect_evm_interface input", - "inspect_firmware_regions input", - "inspect_go_binary input", - "inspect_gpu_code_objects input", - "inspect_il2cpp_metadata input", - "inspect_kernel_build_config input", - "inspect_kernel_module input", - "inspect_managed_artifact input", - "inspect_managed_members input", - "inspect_managed_native_boundaries input", - "inspect_model_artifact input", - "inspect_native_exports input", - "inspect_native_initializers input", - "inspect_native_initializers output", - "inspect_ntfs_journal input", - "inspect_ntfs_journal output", - "inspect_packet_capture input", - "inspect_pe_certificates input", - "inspect_pe_imports input", - "inspect_pe_load_config input", - "inspect_pe_resources input", - "inspect_playwright_trace input", - "inspect_python_pyc input", - "inspect_recorded_crash input", - "inspect_rust_binary input", - "inspect_sqlite_database input", - "inspect_v8_code_cache input", - "inspect_v8_code_cache output", - "inspect_wasm_artifact input", - "inspect_web_network_capture input", - "inspect_windows_shortcut input", - "measure_build_task input", - "open_binary input", - "open_bundle_resources input", - "recover_javascript_sources input", - "reset_investigation input", - "start_build_computer input", - "trace_web_module_imports input", - "trace_web_source_location input" - ], - "^0x(?:0|[1-9a-f][0-9a-f]{0,15})$": [ - "inspect_binary_layout output", - "inspect_recorded_crash output" - ], - "^0x[0-9A-Fa-f]+$": [ - "inspect_analysis_view output", - "inspect_macho output" - ], "^0x[0-9A-Fa-f]{1,16}$": [ - "binary_session output", "execute_rizin_command input", - "inspect_analysis_view input", - "inspect_analysis_view output", "open_binary input", "open_binary output" ], "^0x[0-9A-Fa-f]{1,8}$": ["open_binary input"], - "^0x[0-9a-f]+$": [ - "inspect_analysis_view output", - "inspect_android_jni_bindings output", - "inspect_apple_ips output", - "inspect_elf_notes output", - "inspect_elf_unwind_ranges output", - "inspect_macho output", - "inspect_native_data_type output", - "inspect_native_exports output", - "inspect_native_initializers output", - "inspect_native_load_image output", - "inspect_pe_load_config output", - "observe_native_calls output" - ], - "^0x[0-9a-f]{1,16}$": [ - "inspect_analysis_view output", - "inspect_il2cpp_metadata output" - ], - "^0x[0-9a-f]{4}$": [ - "inspect_analysis_view output", - "inspect_managed_artifact output", - "inspect_managed_native_boundaries output" - ], - "^0x[0-9a-f]{8}$": [ - "compare_managed_members output", - "import_managed_reconstruction input", - "import_managed_reconstruction output", - "inspect_analysis_view output", - "inspect_evm_interface output", - "inspect_gpu_code_objects output", - "inspect_managed_artifact output", - "inspect_managed_members output", - "inspect_managed_native_boundaries output", - "verify_managed_native_boundaries output" - ], - "^1\\.0\\.[0-9]+$": [ - "inspect_analysis_view output", - "inspect_wasm_artifact output" - ], - "^CONFIG_[0-9A-Z_a-z]+$": [ - "inspect_kernel_build_config input", - "inspect_kernel_build_config output" - ], - "^INVOKE_[^\\x00]*(?:\\x00[^\\x00]*)*$": [ - "trace_android_references output" - ], - "^REA_[0-9A-Z_]{1,60}$": ["start_build_computer input"], - "^[ +\\x2d-\\x3a\\x40-Z_a-z]+$": ["compare_source_to_bundle input"], - "^[!\\x23-\\x27*+\\-.0-9A-Z\\x5e-z\\|~]+$": [ - "capture_browser_scenario input" - ], - "^[+\\-0-9A-Z_a-z]+$": [ - "inspect_signal_decoders input", - "inspect_signal_decoders output" - ], - "^[+\\-0-9A-Z_a-z]+(?:/[+\\-0-9A-Z_a-z]+)*$": [ - "capture_browser_scenario input" - ], - "^[+\\-0-9\\x40-Z_a-z][+\\-.0-9\\x40-Z_a-z]*(?:/[+\\-0-9\\x40-Z_a-z][+\\-.0-9\\x40-Z_a-z]*)*$": [ - "start_build_computer input" - ], - "^[0-9A-F]{8}$": ["inspect_windows_shortcut output"], - "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$": [ - "inspect_apple_ips output", - "inspect_windows_shortcut output" - ], - "^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$": [ - "compare_managed_members output", - "import_managed_reconstruction output", - "inspect_analysis_view output", - "inspect_managed_artifact output", - "inspect_managed_members output", - "inspect_managed_native_boundaries output", - "verify_managed_native_boundaries output" - ], - "^[0-9A-Za-z]+(?:-[0-9A-Za-z]+)*$": ["capture_browser_scenario input"], + "^CONFIG_[0-9A-Z_a-z]+$": ["inspect_kernel_build_config input"], + "^[+\\-0-9A-Z_a-z]+$": ["inspect_signal_decoders input"], "^[0-9A-Za-z][+\\-.0-9A-Z_a-z]*$": ["execute_rizin_command input"], - "^[0-9A-Za-z][\\-.0-9A-Z_a-z]*$": ["start_build_computer input"], - "^[0-9A-Za-z][\\x2d-9A-Z_a-z]*$": ["start_build_computer input"], - "^[0-9]+$": [ - "inspect_analysis_view output", - "inspect_native_exports output" - ], - "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$": [ - "inspect_analysis_view output", - "inspect_managed_artifact output", - "inspect_managed_members output" - ], - "^[0-9a-f]*$": [ - "inspect_analysis_view output", - "inspect_devicetree output", - "inspect_elf_notes output", - "inspect_kernel_module output", - "inspect_pe_certificates output" - ], - "^[0-9a-f]+$": [ - "analyze_function output", - "annotate_native_function output", - "inspect_analysis_view output", - "inspect_artifact output", - "inspect_pe_imports output", - "trace_native_values output" - ], - "^[0-9a-f]{16}$": [ - "inspect_analysis_view output", - "inspect_managed_artifact output", - "inspect_managed_members output", - "inspect_python_pyc output" - ], - "^[0-9a-f]{32}$": [ - "analyze_javascript_application output", - "analyze_swift_types output", - "batch_decompile output", - "extract_artifact output", - "get_call_graph output", - "identify_flutter_build output", - "inspect_analysis_settings output", - "inspect_analysis_view output", - "inspect_artifact output", - "inspect_dart_aot output", - "inspect_pe_imports output", - "trace_call_path output", - "update_analysis_settings output" - ], - "^[0-9a-f]{40}$": [ - "inspect_analysis_view output", - "inspect_artifact output", - "inspect_rust_binary output" - ], "^[0-9a-f]{64}$": [ - "address_name output", - "address_to_file_offset output", - "analyze_function output", - "analyze_javascript_application output", - "analyze_swift_types output", - "analyze_web_bundle output", - "annotate_native_function output", - "audit_release_exposure output", - "batch_decompile output", - "binary_overview output", "binary_session input", - "binary_session output", - "build_call_path input", - "build_call_path output", - "build_reconstruction_obligation_ledger input", - "build_reconstruction_obligation_ledger output", - "capture_adb_screen output", - "capture_browser_scenario output", - "capture_electron_scenario output", - "capture_native_ui_scenario output", - "capture_process_scenario output", - "capture_web_screenshot output", - "collect_adb_bugreport output", - "comment output", - "compare_application_versions input", - "compare_application_versions output", - "compare_artifacts input", - "compare_artifacts output", - "compare_bundles output", - "compare_functions input", - "compare_functions output", - "compare_javascript_export_shapes input", - "compare_javascript_export_shapes output", - "compare_managed_members input", - "compare_managed_members output", - "compare_process_captures input", - "compare_process_captures output", - "compare_source_to_bundle input", - "compare_source_to_bundle output", - "compare_static_and_observed_endpoints input", - "compare_static_and_observed_endpoints output", - "compare_wasm_interfaces input", - "compare_wasm_interfaces output", - "compare_web_captures output", "compare_web_screenshots input", - "compare_web_screenshots output", - "correlate_static_and_runtime input", - "correlate_static_and_runtime output", - "current_address output", - "current_document output", - "current_procedure output", - "cutter_command output", - "decode_android_resources output", - "decode_interface_builder output", - "decode_signal_recording output", - "decompile_all output", - "decompile_jeb_item output", - "demangle_swift output", - "discover_webmcp_tools output", - "evaluate_reconstruction_coverage input", - "execute_rizin_command output", - "export_web_scripts output", - "extract_artifact output", - "extract_firmware output", - "find_changed_behavior input", - "find_changed_behavior output", - "find_xrefs_to_name output", - "frida_session_status output", - "gdb_console output", - "get_call_graph output", "get_evidence_bundle input", - "get_evidence_bundle output", - "get_objc_classes output", - "get_objc_protocols output", - "goto_address output", - "identify_flutter_build output", - "import_managed_reconstruction input", - "import_managed_reconstruction output", - "inline_comment output", - "inspect_adb_client output", - "inspect_adb_device output", - "inspect_adb_display output", - "inspect_adb_package output", - "inspect_adb_window output", - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_android_class output", - "inspect_android_jni_bindings output", - "inspect_android_method output", - "inspect_android_package output", - "inspect_api_contract output", - "inspect_apk_signature output", - "inspect_apktool_client output", - "inspect_apple_ips output", - "inspect_artifact output", - "inspect_asset_catalog output", - "inspect_binary_hardening output", - "inspect_binary_layout output", - "inspect_byte_entropy output", - "inspect_dart_aot output", - "inspect_desktop_application output", - "inspect_devicetree output", - "inspect_electron_fuses output", - "inspect_electron_page output", - "inspect_elf_notes output", - "inspect_elf_unwind_ranges output", - "inspect_erlang_beam output", - "inspect_evm_interface output", - "inspect_firmware_regions output", - "inspect_go_binary output", "inspect_gpu_code_objects input", - "inspect_gpu_code_objects output", - "inspect_il2cpp_metadata output", - "inspect_jeb_client output", - "inspect_kernel_build_config output", - "inspect_kernel_module output", - "inspect_keyed_archive output", - "inspect_macho output", - "inspect_managed_artifact output", - "inspect_managed_members output", - "inspect_managed_native_boundaries output", - "inspect_model_artifact output", - "inspect_native_api output", - "inspect_native_data_type output", - "inspect_native_dispatch_metadata output", - "inspect_native_exports output", - "inspect_native_initializers output", - "inspect_native_instruction output", - "inspect_native_load_image output", - "inspect_ntfs_journal output", - "inspect_packet_capture output", - "inspect_packet_fields output", - "inspect_pcap_capture output", - "inspect_pcap_client output", - "inspect_pe_certificates output", - "inspect_pe_imports output", - "inspect_pe_load_config output", - "inspect_pe_resources output", - "inspect_playwright_trace output", - "inspect_plist output", - "inspect_python_pyc output", - "inspect_recorded_crash output", - "inspect_rust_binary output", - "inspect_signal_decoders output", - "inspect_signature output", - "inspect_sqlite_database output", - "inspect_v8_code_cache output", - "inspect_wasm_artifact output", - "inspect_web_event_listeners output", - "inspect_web_network_capture output", - "inspect_web_page output", - "inspect_windows_shortcut output", - "inspect_with_objdump output", - "install_adb_package output", - "instrument_with_frida output", - "list_adb_devices output", - "list_adb_directory output", - "list_adb_features output", - "list_adb_packages output", - "list_adb_processes output", - "list_adb_services output", - "list_architectures output", - "list_bookmarks output", - "list_browser_targets output", - "list_documents output", - "list_electron_targets output", - "list_javascript_runtime_targets output", - "list_jeb_units output", - "list_names output", - "list_procedures output", - "list_segments output", - "list_strings output", - "list_unknowns output", - "load_frida_script output", - "match_native_functions input", - "match_native_functions output", - "next_address output", - "observe_javascript_runtime output", - "observe_native_calls output", - "observe_native_ui output", - "observe_web_execution output", - "observe_web_session output", "open_binary output", - "open_bundle_resources output", - "open_jeb_project output", - "prev_address output", - "procedure_address output", - "procedure_assembly output", - "procedure_callees output", - "procedure_callers output", - "procedure_info output", - "procedure_pseudo_code output", - "procedure_references output", - "project_android_application_graph input", - "project_android_application_graph output", - "project_apple_application_graph input", - "project_apple_application_graph output", - "project_harmony_application_graph input", - "project_harmony_application_graph output", - "project_managed_application_graph input", - "project_managed_application_graph output", - "pull_adb_file output", - "pull_adb_package output", - "push_adb_file output", - "read_adb_logcat output", - "read_adb_setting output", - "read_bytes output", - "read_function_instructions output", - "reconcile_javascript_runtime input", - "reconcile_javascript_runtime output", - "record_unknown output", - "recover_javascript_sources output", - "resolve_adb_packages output", - "resolve_containing_procedure output", - "resolve_native_call_targets output", - "rizin_debug_command output", - "search_android_classes output", - "search_bytes output", - "search_procedures output", - "search_strings output", - "set_address_name output", - "set_addresses_names output", - "set_bookmark output", - "set_comment output", - "set_inline_comment output", - "start_adb_activity output", - "start_adb_app output", - "start_build_computer input", - "stop_adb_app output", - "trace_android_references output", - "trace_application_feature input", - "trace_application_feature output", - "trace_call_path output", - "trace_dylib_resolution output", - "trace_feature output", - "trace_javascript_semantics input", - "trace_javascript_semantics output", - "trace_native_ui_action output", - "trace_native_values output", - "trace_web_module_imports output", - "trace_web_source_location output", - "uninstall_adb_package output", - "unset_bookmark output", - "update_unknown output", - "verify_managed_native_boundaries input", - "verify_managed_native_boundaries output", - "verify_reconstruction output", - "verify_unknown_resolution output", - "xrefs output" - ], - "^[0-9a-f]{6}$": [ - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^[0-9a-f]{8}$": [ - "analyze_javascript_application output", - "analyze_swift_types output", - "batch_decompile output", - "extract_artifact output", - "get_call_graph output", - "inspect_analysis_settings output", - "inspect_analysis_view output", - "inspect_artifact output", - "trace_call_path output", - "update_analysis_settings output" + "open_bundle_resources output" ], "^[0-9a-z]+(?:-[0-9a-z]+)*$": [ "export_environment_bundle input", "measure_build_task input" ], - "^[0-9a-z][+\\-.0-9a-z]+$": ["start_build_computer input"], "^[0-9a-z][\\-.0-9_a-z]{0,62}$": [ "act_on_build_computer input", - "export_build_environment input", - "mark_build_checkpoint input", - "start_build_computer input" - ], - "^[0-9a-z][\\-0-9a-z]{0,63}$": [ - "check_build_task input", - "export_build_environment input" - ], - "^[1-9][0-9]*$": [ - "inspect_packet_capture output", - "list_adb_devices output" + "mark_build_checkpoint input" ], "^[A-Za-z][0-9A-Z_a-z]*(\\.[A-Za-z][0-9A-Z_a-z]*)*$": [ "inspect_adb_package input", - "inspect_adb_package output", - "list_adb_packages output", "pull_adb_package input", - "pull_adb_package output", - "resolve_adb_packages output", "start_adb_app input", - "start_adb_app output", "stop_adb_app input", - "stop_adb_app output", - "uninstall_adb_package input", - "uninstall_adb_package output" - ], - "^[A-Za-z][\\-.0-9A-Z_a-z]*$": [ - "capture_browser_scenario input", - "compare_process_captures input", - "compare_process_captures output", - "start_adb_activity input", - "verify_reconstruction input", - "verify_reconstruction output" - ], - "^[A-Za-z][\\x2d-\\x3aA-Z_a-z]*$": [ - "build_reconstruction_obligation_ledger input", - "build_reconstruction_obligation_ledger output" + "uninstall_adb_package input" ], + "^[A-Za-z][\\-.0-9A-Z_a-z]*$": ["start_adb_activity input"], "^[A-Za-z][\\x2d-\\x3aA-Z_a-z]{0,199}$": [ - "evaluate_reconstruction_coverage input", "evaluate_reconstruction_coverage output" ], "^[\\-.0-9A-Z_a-z]+$": [ "read_adb_setting input", - "start_adb_activity input", - "start_build_computer input" - ], - "^[\\-.0-9A-Z_a-z]+/[$\\-.0-9A-Z_a-z]+$": ["start_adb_activity input"], - "^[\\x01-\\x20]*(?:\\+|-)?(?:NaN|Infinity|(?:(?:[0-9]+(?:\\.[0-9]*)?|\\.[0-9]+)(?:[Ee](?:\\+|-)?[0-9]+)?[DFdf]?|0[Xx](?:[0-9A-Fa-f]+(?:\\.[0-9A-Fa-f]*)?|\\.[0-9A-Fa-f]+)[Pp](?:\\+|-)?[0-9]+[DFdf]?))[\\x01-\\x20]*$": [ "start_adb_activity input" ], - "^[^\\x00-\\x08\\x0a-\\x1f\\x7f]*$": ["capture_browser_scenario input"], - "^[^\\x00=]+$": [ - "observe_native_calls input", - "observe_native_calls output" - ], + "^[\\-.0-9A-Z_a-z]+/[$\\-.0-9A-Z_a-z]+$": ["start_adb_activity input"], "^[^\\x00\\x09-\\x0d \"']+$": [ "capture_adb_screen input", "collect_adb_bugreport input", @@ -769,2496 +112,1143 @@ ], "^[^\\x00\\x09-\\x0d ]+$": ["start_adb_activity input"], "^[^\\x00]*$": [ - "address_name output", - "address_to_file_offset output", - "analyze_function output", - "analyze_javascript_application input", - "analyze_javascript_application output", - "analyze_swift_types output", - "analyze_web_bundle output", - "annotate_native_function output", "audit_release_exposure input", - "audit_release_exposure output", - "batch_decompile output", - "binary_overview output", "binary_session input", - "build_call_path input", - "build_call_path output", - "build_reconstruction_obligation_ledger input", - "build_reconstruction_obligation_ledger output", "capture_adb_screen input", - "capture_adb_screen output", - "capture_browser_scenario input", - "capture_browser_scenario output", "capture_electron_scenario input", - "capture_electron_scenario output", - "capture_native_ui_scenario output", "capture_process_scenario input", - "capture_process_scenario output", - "capture_web_screenshot output", - "check_build_task input", "close_binary input", "collect_adb_bugreport input", - "collect_adb_bugreport output", - "comment output", - "compare_application_versions input", - "compare_application_versions output", - "compare_artifacts input", - "compare_artifacts output", "compare_bundles input", - "compare_bundles output", - "compare_functions input", - "compare_functions output", - "compare_javascript_export_shapes input", - "compare_javascript_export_shapes output", - "compare_managed_members input", - "compare_managed_members output", - "compare_process_captures input", - "compare_process_captures output", - "compare_source_to_bundle input", - "compare_source_to_bundle output", - "compare_static_and_observed_endpoints input", - "compare_static_and_observed_endpoints output", - "compare_wasm_interfaces input", - "compare_wasm_interfaces output", - "compare_web_captures output", - "compare_web_screenshots output", - "correlate_static_and_runtime input", - "correlate_static_and_runtime output", - "current_address output", - "current_document output", - "current_procedure output", - "cutter_command output", "decode_android_resources input", - "decode_android_resources output", - "decode_interface_builder output", "decode_signal_recording input", - "decode_signal_recording output", "decompile_all input", - "decompile_all output", - "decompile_jeb_item output", - "demangle_swift input", - "demangle_swift output", - "discover_webmcp_tools output", - "evaluate_reconstruction_coverage input", "execute_rizin_command input", - "execute_rizin_command output", - "export_build_environment input", "export_environment_bundle input", "export_evidence_bundle input", "export_web_scripts input", - "export_web_scripts output", "extract_artifact input", - "extract_artifact output", "extract_firmware input", - "extract_firmware output", - "find_changed_behavior input", - "find_changed_behavior output", - "find_xrefs_to_name output", - "frida_session_status output", - "gdb_console output", - "get_call_graph output", - "get_evidence_bundle output", - "get_objc_classes output", - "get_objc_protocols output", - "goto_address output", "identify_flutter_build input", - "identify_flutter_build output", "import_evidence_bundle input", - "import_managed_reconstruction input", - "import_managed_reconstruction output", - "inline_comment output", - "inspect_adb_client output", - "inspect_adb_device output", - "inspect_adb_display output", - "inspect_adb_package output", - "inspect_adb_window output", - "inspect_analysis_view input", - "inspect_analysis_view output", "inspect_android_class input", - "inspect_android_class output", "inspect_android_jni_bindings input", - "inspect_android_jni_bindings output", "inspect_android_method input", - "inspect_android_method output", "inspect_android_package input", - "inspect_android_package output", - "inspect_api_contract input", - "inspect_api_contract output", "inspect_apk_signature input", - "inspect_apk_signature output", - "inspect_apktool_client output", "inspect_apple_ips input", - "inspect_apple_ips output", - "inspect_artifact output", - "inspect_asset_catalog output", "inspect_binary_hardening input", - "inspect_binary_hardening output", "inspect_binary_layout input", - "inspect_binary_layout output", "inspect_byte_entropy input", - "inspect_byte_entropy output", "inspect_dart_aot input", - "inspect_dart_aot output", "inspect_desktop_application input", - "inspect_desktop_application output", "inspect_devicetree input", - "inspect_devicetree output", "inspect_electron_fuses input", - "inspect_electron_fuses output", - "inspect_electron_page output", "inspect_elf_notes input", - "inspect_elf_notes output", "inspect_elf_unwind_ranges input", - "inspect_elf_unwind_ranges output", "inspect_erlang_beam input", - "inspect_erlang_beam output", "inspect_evm_interface input", - "inspect_evm_interface output", "inspect_firmware_regions input", - "inspect_firmware_regions output", "inspect_go_binary input", - "inspect_go_binary output", "inspect_gpu_code_objects input", - "inspect_gpu_code_objects output", "inspect_il2cpp_metadata input", - "inspect_il2cpp_metadata output", - "inspect_jeb_client output", "inspect_kernel_build_config input", - "inspect_kernel_build_config output", "inspect_kernel_module input", - "inspect_kernel_module output", "inspect_keyed_archive input", - "inspect_keyed_archive output", - "inspect_macho output", "inspect_managed_artifact input", - "inspect_managed_artifact output", "inspect_managed_members input", - "inspect_managed_members output", "inspect_managed_native_boundaries input", - "inspect_managed_native_boundaries output", "inspect_model_artifact input", - "inspect_model_artifact output", - "inspect_native_api output", - "inspect_native_data_type output", - "inspect_native_dispatch_metadata output", "inspect_native_exports input", - "inspect_native_exports output", "inspect_native_initializers input", - "inspect_native_initializers output", - "inspect_native_instruction output", - "inspect_native_load_image output", "inspect_ntfs_journal input", - "inspect_ntfs_journal output", "inspect_packet_capture input", - "inspect_packet_capture output", - "inspect_packet_fields output", "inspect_pcap_capture input", - "inspect_pcap_capture output", - "inspect_pcap_client output", "inspect_pe_certificates input", - "inspect_pe_certificates output", "inspect_pe_imports input", - "inspect_pe_imports output", "inspect_pe_load_config input", - "inspect_pe_load_config output", "inspect_pe_resources input", - "inspect_pe_resources output", "inspect_playwright_trace input", - "inspect_playwright_trace output", "inspect_plist input", - "inspect_plist output", "inspect_python_pyc input", - "inspect_python_pyc output", "inspect_recorded_crash input", - "inspect_recorded_crash output", "inspect_rust_binary input", - "inspect_rust_binary output", - "inspect_signal_decoders output", - "inspect_signature output", "inspect_sqlite_database input", - "inspect_sqlite_database output", "inspect_v8_code_cache input", - "inspect_v8_code_cache output", "inspect_wasm_artifact input", - "inspect_wasm_artifact output", - "inspect_web_event_listeners output", "inspect_web_network_capture input", - "inspect_web_network_capture output", - "inspect_web_page output", "inspect_windows_shortcut input", - "inspect_windows_shortcut output", "inspect_with_objdump input", - "inspect_with_objdump output", "install_adb_package input", - "install_adb_package output", "instrument_with_frida input", - "instrument_with_frida output", - "list_adb_devices output", - "list_adb_directory output", - "list_adb_features output", - "list_adb_packages output", - "list_adb_processes output", - "list_adb_services output", - "list_architectures output", - "list_bookmarks output", - "list_browser_targets output", - "list_documents output", - "list_electron_targets output", - "list_javascript_runtime_targets output", - "list_jeb_units output", - "list_names output", - "list_procedures output", - "list_segments output", - "list_strings output", "load_frida_script input", - "load_frida_script output", - "match_native_functions input", - "match_native_functions output", "measure_build_task input", - "next_address output", - "observe_javascript_runtime output", "observe_native_calls input", - "observe_native_calls output", - "observe_native_ui output", - "observe_web_execution output", - "observe_web_session output", "open_binary input", "open_bundle_resources input", "open_jeb_project input", - "open_jeb_project output", - "prev_address output", - "procedure_address output", - "procedure_assembly output", - "procedure_callees output", - "procedure_callers output", - "procedure_info output", - "procedure_pseudo_code output", - "procedure_references output", - "project_android_application_graph input", - "project_android_application_graph output", - "project_apple_application_graph input", - "project_apple_application_graph output", - "project_harmony_application_graph input", - "project_harmony_application_graph output", - "project_managed_application_graph input", - "project_managed_application_graph output", "pull_adb_file input", - "pull_adb_file output", "pull_adb_package input", - "pull_adb_package output", "push_adb_file input", - "push_adb_file output", - "read_adb_logcat output", - "read_adb_setting output", - "read_bytes output", - "read_function_instructions output", - "reconcile_javascript_runtime input", - "reconcile_javascript_runtime output", "recover_javascript_sources input", - "recover_javascript_sources output", "reset_investigation input", - "resolve_adb_packages output", - "resolve_containing_procedure output", - "resolve_native_call_targets output", - "rizin_debug_command output", "search_android_classes input", - "search_android_classes output", - "search_bytes output", - "search_procedures output", - "search_strings output", - "set_address_name output", - "set_addresses_names output", - "set_bookmark output", - "set_comment output", - "set_inline_comment output", "start_adb_activity input", - "start_adb_activity output", - "start_adb_app output", - "start_build_computer input", "start_frida_session input", "start_gdb_session input", "start_rizin_debug_session input", - "stop_adb_app output", "trace_android_references input", - "trace_android_references output", - "trace_application_feature input", - "trace_application_feature output", - "trace_call_path output", - "trace_dylib_resolution output", - "trace_feature output", - "trace_javascript_semantics input", - "trace_javascript_semantics output", - "trace_native_ui_action output", - "trace_native_values output", "trace_web_module_imports input", - "trace_web_module_imports output", - "trace_web_source_location input", - "trace_web_source_location output", - "uninstall_adb_package output", - "unset_bookmark output", - "verify_managed_native_boundaries input", - "verify_managed_native_boundaries output", - "verify_reconstruction output", - "xrefs output" - ], - "^[^\\x09-\\x0d \\[\\]]+$": [ - "observe_native_calls input", - "observe_native_calls output" + "trace_web_source_location input" ], "^[^\\x0a]*$": ["demangle_swift input"], "^[^\\x2f]*$": ["inspect_dart_aot input"], "^[a-z][\\-0-9a-z]*$": ["open_binary input"], "^\\((?:\\[{0,255}(?:[B-DFIJSZ]|L[^\\(\\);]+;))*\\)(?:V|\\[{0,255}(?:[B-DFIJSZ]|L[^\\(\\);]+;))$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_android_class output", - "inspect_android_jni_bindings input", - "inspect_android_jni_bindings output", - "inspect_android_method output", - "trace_android_references output" - ], - "^\\[{0,255}(?:[B-DFIJSZ]|L[^\\(\\);]+;)$": [ - "inspect_analysis_view output", - "inspect_android_class output", - "inspect_android_method output" - ], - "^aap_[0-9a-f]{64}$": ["project_apple_application_graph output"], - "^adp_[0-9a-f]{64}$": ["project_android_application_graph output"], - "^aex_[0-9a-f]{64}$": ["extract_artifact output"], - "^agm_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "compare_artifacts output", - "extract_artifact output", - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^ai_[0-9a-f]{64}$": ["inspect_artifact output"], - "^aib_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^aic_[0-9a-f]{64}$": ["inspect_artifact output"], - "^aih_[0-9a-f]{64}$": ["inspect_artifact output"], - "^aio_[0-9a-f]{64}$": ["inspect_artifact output"], - "^ais_[0-9a-f]{64}$": ["inspect_artifact output"], - "^api_contract_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_api_contract output" - ], - "^api_contract_diagnostic_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_api_contract output" - ], - "^api_operation_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^api_operation_key_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^api_path_reference_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^art_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "compare_application_versions output", - "compare_artifacts output", - "extract_artifact output", - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_artifact output", - "project_android_application_graph output", - "project_apple_application_graph output", - "project_harmony_application_graph output", - "project_managed_application_graph output", - "reconcile_javascript_runtime output", - "trace_application_feature output", - "trace_javascript_semantics output" - ], - "^cor_[0-9a-f]{64}$": ["correlate_static_and_runtime output"], - "^dsc_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_desktop_application output" - ], - "^dsf_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_desktop_application output" - ], - "^dsm_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_desktop_application output" - ], - "^dso_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_desktop_application output" - ], - "^dsp_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_desktop_application output" - ], - "^dsr_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_desktop_application output" - ], - "^edge_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^electron_resource_[0-9a-f]{64}$": ["inspect_electron_page output"], - "^electron_script_[0-9a-f]{64}$": ["inspect_electron_page output"], - "^endpoint_comparison_[0-9a-f]{64}$": [ - "compare_static_and_observed_endpoints output", - "inspect_analysis_view output" - ], - "^endpoint_match_[0-9a-f]{64}$": [ - "compare_static_and_observed_endpoints output", - "inspect_analysis_view output" + "inspect_android_jni_bindings input" ], "^ev_[0-9a-f]{64}$": [ - "address_name output", - "address_to_file_offset output", - "analyze_function output", - "analyze_javascript_application output", - "analyze_swift_types output", - "analyze_web_bundle output", - "annotate_native_function output", - "audit_release_exposure output", - "batch_decompile output", - "binary_overview output", - "build_call_path input", - "build_call_path output", - "build_reconstruction_obligation_ledger input", - "build_reconstruction_obligation_ledger output", - "capture_adb_screen output", - "capture_browser_scenario output", - "capture_electron_scenario output", - "capture_native_ui_scenario output", - "capture_process_scenario output", - "capture_web_screenshot output", - "collect_adb_bugreport output", - "comment output", - "compare_application_versions input", - "compare_application_versions output", - "compare_artifacts input", - "compare_artifacts output", "compare_bundles input", - "compare_bundles output", - "compare_functions input", - "compare_functions output", - "compare_javascript_export_shapes input", - "compare_javascript_export_shapes output", - "compare_managed_members input", - "compare_managed_members output", - "compare_process_captures input", - "compare_process_captures output", - "compare_source_to_bundle input", - "compare_source_to_bundle output", - "compare_static_and_observed_endpoints input", - "compare_static_and_observed_endpoints output", - "compare_wasm_interfaces input", - "compare_wasm_interfaces output", - "compare_web_captures output", - "compare_web_screenshots output", - "correlate_static_and_runtime input", - "correlate_static_and_runtime output", - "current_address output", - "current_document output", - "current_procedure output", - "cutter_command output", - "decode_android_resources output", - "decode_interface_builder output", - "decode_signal_recording output", - "decompile_all output", - "decompile_jeb_item output", - "demangle_swift output", - "discover_webmcp_tools output", - "evaluate_reconstruction_coverage input", "evaluate_reconstruction_coverage output", - "execute_rizin_command output", "export_environment_bundle input", - "export_environment_bundle output", - "export_web_scripts output", - "extract_artifact output", - "extract_firmware output", - "find_changed_behavior input", - "find_changed_behavior output", - "find_xrefs_to_name output", - "frida_session_status output", - "gdb_console output", - "get_call_graph output", "get_evidence_bundle input", - "get_evidence_bundle output", - "get_objc_classes output", - "get_objc_protocols output", - "goto_address output", - "identify_flutter_build output", - "import_managed_reconstruction input", - "import_managed_reconstruction output", - "inline_comment output", - "inspect_adb_client output", - "inspect_adb_device output", - "inspect_adb_display output", - "inspect_adb_package output", - "inspect_adb_window output", - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_android_class output", - "inspect_android_jni_bindings output", - "inspect_android_method output", - "inspect_android_package output", - "inspect_api_contract output", - "inspect_apk_signature output", - "inspect_apktool_client output", - "inspect_apple_ips output", - "inspect_artifact output", - "inspect_asset_catalog output", - "inspect_binary_hardening output", - "inspect_binary_layout output", - "inspect_byte_entropy output", - "inspect_dart_aot output", - "inspect_desktop_application output", - "inspect_devicetree output", - "inspect_electron_fuses output", - "inspect_electron_page output", - "inspect_elf_notes output", - "inspect_elf_unwind_ranges output", - "inspect_erlang_beam output", - "inspect_evm_interface output", - "inspect_firmware_regions output", - "inspect_go_binary output", - "inspect_gpu_code_objects output", - "inspect_il2cpp_metadata output", - "inspect_jeb_client output", - "inspect_kernel_build_config output", - "inspect_kernel_module output", - "inspect_keyed_archive output", - "inspect_macho output", - "inspect_managed_artifact output", - "inspect_managed_members output", - "inspect_managed_native_boundaries output", - "inspect_model_artifact output", - "inspect_native_api output", - "inspect_native_data_type output", - "inspect_native_dispatch_metadata output", - "inspect_native_exports output", - "inspect_native_initializers output", - "inspect_native_instruction output", - "inspect_native_load_image output", - "inspect_ntfs_journal output", - "inspect_packet_capture output", - "inspect_packet_fields output", - "inspect_pcap_capture output", - "inspect_pcap_client output", - "inspect_pe_certificates output", - "inspect_pe_imports output", - "inspect_pe_load_config output", - "inspect_pe_resources output", - "inspect_playwright_trace output", - "inspect_plist output", - "inspect_python_pyc output", - "inspect_recorded_crash output", - "inspect_rust_binary output", - "inspect_signal_decoders output", - "inspect_signature output", - "inspect_sqlite_database output", - "inspect_v8_code_cache output", - "inspect_wasm_artifact output", - "inspect_web_event_listeners output", - "inspect_web_network_capture output", - "inspect_web_page output", - "inspect_windows_shortcut output", - "inspect_with_objdump output", - "install_adb_package output", - "instrument_with_frida output", - "list_adb_devices output", - "list_adb_directory output", - "list_adb_features output", - "list_adb_packages output", - "list_adb_processes output", - "list_adb_services output", - "list_architectures output", - "list_bookmarks output", - "list_browser_targets output", - "list_documents output", - "list_electron_targets output", - "list_javascript_runtime_targets output", - "list_jeb_units output", - "list_names output", - "list_procedures output", - "list_segments output", - "list_strings output", - "list_unknowns output", - "load_frida_script output", - "match_native_functions input", - "match_native_functions output", - "next_address output", - "observe_javascript_runtime output", - "observe_native_calls output", - "observe_native_ui output", - "observe_web_execution output", - "observe_web_session output", - "open_jeb_project output", - "prev_address output", - "procedure_address output", - "procedure_assembly output", - "procedure_callees output", - "procedure_callers output", - "procedure_info output", - "procedure_pseudo_code output", - "procedure_references output", - "project_android_application_graph input", - "project_android_application_graph output", - "project_apple_application_graph input", - "project_apple_application_graph output", - "project_harmony_application_graph input", - "project_harmony_application_graph output", - "project_managed_application_graph input", - "project_managed_application_graph output", - "pull_adb_file output", - "pull_adb_package output", - "push_adb_file output", - "read_adb_logcat output", - "read_adb_setting output", - "read_bytes output", - "read_function_instructions output", - "reconcile_javascript_runtime input", - "reconcile_javascript_runtime output", "record_unknown input", - "record_unknown output", - "recover_javascript_sources output", - "resolve_adb_packages output", - "resolve_containing_procedure output", - "resolve_native_call_targets output", - "rizin_debug_command output", - "search_android_classes output", - "search_bytes output", - "search_procedures output", - "search_strings output", - "set_address_name output", - "set_addresses_names output", - "set_bookmark output", - "set_comment output", - "set_inline_comment output", - "start_adb_activity output", - "start_adb_app output", - "stop_adb_app output", - "trace_android_references output", - "trace_application_feature input", - "trace_application_feature output", - "trace_call_path output", - "trace_dylib_resolution output", - "trace_feature output", - "trace_javascript_semantics input", - "trace_javascript_semantics output", - "trace_native_ui_action output", - "trace_native_values output", - "trace_web_module_imports output", - "trace_web_source_location output", - "uninstall_adb_package output", - "unset_bookmark output", - "update_unknown input", - "update_unknown output", - "verify_managed_native_boundaries input", - "verify_managed_native_boundaries output", - "verify_reconstruction input", - "verify_reconstruction output", - "verify_unknown_resolution output", - "xrefs output" - ], - "^graphql_contract_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_api_contract output" - ], - "^graphql_definition_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^graphql_diagnostic_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_api_contract output" - ], - "^graphql_field_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^graphql_reference_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^graphql_spread_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_api_contract output" - ], - "^graphql_type_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_api_contract input", - "inspect_api_contract output" - ], - "^hmp_[0-9a-f]{64}$": ["project_harmony_application_graph output"], - "^https?://[^\\x00]*(?:\\x00[^\\x00]*)*$": ["start_build_computer input"], - "^ic_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "extract_artifact output", - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^jag_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "compare_application_versions output", - "compare_javascript_export_shapes output", - "compare_source_to_bundle output", - "compare_static_and_observed_endpoints output", - "inspect_analysis_view output", - "project_managed_application_graph output", - "reconcile_javascript_runtime output", - "trace_application_feature output" - ], - "^jag_edge_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "compare_application_versions output", - "inspect_analysis_view output", - "project_managed_application_graph output", - "reconcile_javascript_runtime output", - "trace_application_feature output" - ], - "^jag_node_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "build_reconstruction_obligation_ledger input", - "build_reconstruction_obligation_ledger output", - "compare_application_versions output", - "compare_javascript_export_shapes output", - "compare_source_to_bundle output", - "compare_static_and_observed_endpoints output", - "inspect_analysis_view input", - "inspect_analysis_view output", - "project_managed_application_graph output", - "reconcile_javascript_runtime output", - "trace_application_feature input", - "trace_application_feature output", - "trace_javascript_semantics input", - "trace_javascript_semantics output" - ], - "^jag_observation_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "compare_application_versions output", - "compare_static_and_observed_endpoints output", - "inspect_analysis_view output", - "project_managed_application_graph output", - "reconcile_javascript_runtime output", - "trace_application_feature output" - ], - "^jatp_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "trace_application_feature output" - ], - "^jatr_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "trace_application_feature output" - ], - "^javc_[0-9a-f]{64}$": ["compare_application_versions output"], - "^javc_item_[0-9a-f]{64}$": ["compare_application_versions output"], - "^jesc_[0-9a-f]{64}$": ["compare_javascript_export_shapes output"], - "^jesc_change_[0-9a-f]{64}$": ["compare_javascript_export_shapes output"], - "^jrl_[0-9a-f]{64}$": ["reconcile_javascript_runtime output"], - "^jrr_[0-9a-f]{64}$": ["reconcile_javascript_runtime output"], - "^jrr_item_[0-9a-f]{64}$": ["reconcile_javascript_runtime output"], - "^jsg:[0-9a-f]{32}$": [ - "analyze_javascript_application output", - "analyze_web_bundle output", - "inspect_analysis_view output" - ], - "^jso:[0-9a-f]{32}$": [ - "analyze_javascript_application output", - "analyze_web_bundle output", - "inspect_analysis_view output" - ], - "^jsr:[0-9a-f]{32}$": [ - "analyze_javascript_application output", - "analyze_web_bundle output", - "inspect_analysis_view output" - ], - "^jsrg_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "inspect_analysis_view output", - "trace_application_feature output", - "trace_javascript_semantics output" - ], - "^jsrg_evidence_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "trace_javascript_semantics output" - ], - "^jsrg_fingerprint_[0-9a-f]{64}$": [ - "analyze_javascript_application output" - ], - "^jsrg_node_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "build_reconstruction_obligation_ledger input", - "build_reconstruction_obligation_ledger output", - "inspect_analysis_view output", - "trace_application_feature output", - "trace_javascript_semantics input", - "trace_javascript_semantics output" - ], - "^jsrg_relation_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "trace_javascript_semantics output" - ], - "^jsrg_unknown_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "trace_javascript_semantics output" - ], - "^jsri:[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "analyze_web_bundle output", - "inspect_analysis_view output" - ], - "^jsrq_[0-9a-f]{64}$": ["trace_javascript_semantics output"], - "^jss:[0-9a-f]{32}$": [ - "analyze_javascript_application output", - "analyze_web_bundle output", - "inspect_analysis_view output" - ], - "^magp_[0-9a-f]{64}$": ["project_managed_application_graph output"], - "^match_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "match_native_functions output" - ], - "^mmc_[0-9a-f]{64}$": ["compare_managed_members output"], - "^mmc_field_[0-9a-f]{64}$": ["compare_managed_members output"], - "^mmc_method_[0-9a-f]{64}$": ["compare_managed_members output"], - "^mnv_[0-9a-f]{64}$": ["verify_managed_native_boundaries output"], - "^mnv_pinvoke_[0-9a-f]{64}$": ["verify_managed_native_boundaries output"], - "^mre_[0-9a-f]{64}$": ["import_managed_reconstruction output"], - "^observed_endpoint_[0-9a-f]{64}$": [ - "compare_static_and_observed_endpoints input", - "compare_static_and_observed_endpoints output", - "inspect_analysis_view input", - "inspect_analysis_view output" - ], - "^occ_[0-9a-f]{64}$": [ - "analyze_javascript_application output", - "compare_artifacts output", - "extract_artifact input", - "extract_artifact output", - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_artifact output" - ], - "^playwright_trace_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_playwright_trace output" - ], - "^res_[0-9a-f]{64}$": ["inspect_web_page output"], - "^rol_[0-9a-f]{64}$": ["build_reconstruction_obligation_ledger output"], - "^scr_[0-9a-f]{64}(?:_[0-9]+)?$": ["inspect_web_page output"], - "^static_endpoint_[0-9a-f]{64}$": [ - "compare_static_and_observed_endpoints input", - "compare_static_and_observed_endpoints output", - "inspect_analysis_view input", - "inspect_analysis_view output" - ], - "^stbc_[0-9a-f]{64}$": [ - "compare_source_to_bundle output", - "inspect_analysis_view output" - ], - "^stbc_item_[0-9a-f]{64}$": [ - "compare_source_to_bundle input", - "compare_source_to_bundle output", - "inspect_analysis_view input", - "inspect_analysis_view output" - ], - "^trace_diagnostic_[0-9a-f]{64}$": [ - "inspect_analysis_view output", - "inspect_playwright_trace output" - ], - "^trace_event_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_playwright_trace input", - "inspect_playwright_trace output" - ], - "^trace_member_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_playwright_trace input", - "inspect_playwright_trace output" - ], - "^trace_stream_[0-9a-f]{64}$": [ - "inspect_analysis_view input", - "inspect_analysis_view output", - "inspect_playwright_trace input", - "inspect_playwright_trace output" + "update_unknown input" ], + "^occ_[0-9a-f]{64}$": ["extract_artifact input"], + "^trace_event_[0-9a-f]{64}$": ["inspect_playwright_trace input"], + "^trace_member_[0-9a-f]{64}$": ["inspect_playwright_trace input"], + "^trace_stream_[0-9a-f]{64}$": ["inspect_playwright_trace input"], "^unk_[0-9a-f]{64}$": [ - "build_reconstruction_obligation_ledger input", - "evaluate_reconstruction_coverage input", - "get_evidence_bundle output", - "list_unknowns output", "record_unknown input", - "record_unknown output", "update_unknown input", - "update_unknown output", - "verify_reconstruction output", - "verify_unknown_resolution input", - "verify_unknown_resolution output" - ], - "^v8_script_[0-9a-f]{64}$": ["observe_javascript_runtime output"], - "^webmcp_[0-9a-f]{64}$": ["discover_webmcp_tools output"] + "verify_unknown_resolution input" + ] }, "tools": { "act_on_build_computer": { "input compact": "492ec1ac2a1230c5", - "input full": "fe0ca8b863da0bf1", - "output compact": "9bd80958780175fe", - "output full": "2ad61e3ecab3d881" + "output compact": "9bd80958780175fe" }, "address_name": { "input compact": "f1b0a54d8c702be5", - "input full": "dbc2a763429ebf8f", - "output compact": "9e24f29540c4da8a", - "output full": "83872009723f45ea" + "output compact": "9e24f29540c4da8a" }, "address_to_file_offset": { "input compact": "c9d8a29b5ea9eaf2", - "input full": "0ab8a9a24f970cbf", - "output compact": "9087cdd7aaaf031b", - "output full": "748b8df6220513cb" + "output compact": "9087cdd7aaaf031b" }, "analyze_function": { "input compact": "02ac81c718b49410", - "input full": "573c4bf9ff99f946", - "output compact": "9087cdd7aaaf031b", - "output full": "fb02b58bbf1dea56" + "output compact": "9087cdd7aaaf031b" }, "analyze_javascript_application": { "input compact": "990f691d36bfeeec", - "input full": "463072c41fe38945", - "output compact": "9087cdd7aaaf031b", - "output full": "9658186a6c224c9a" + "output compact": "9087cdd7aaaf031b" }, "analyze_swift_types": { "input compact": "954672370cc76451", - "input full": "4b996c516df3e4c4", - "output compact": "9087cdd7aaaf031b", - "output full": "25e016be8abacc53" + "output compact": "9087cdd7aaaf031b" }, "analyze_web_bundle": { "input compact": "3d7959f5cdca863d", - "input full": "1b2a3ffc89e2ce0e", - "output compact": "9087cdd7aaaf031b", - "output full": "b2368fb3eedde37c" + "output compact": "9087cdd7aaaf031b" }, "annotate_native_function": { "input compact": "70c17ea61092a8c3", - "input full": "5bc1398c10cd90e3", - "output compact": "9087cdd7aaaf031b", - "output full": "47791123e7cadf27" + "output compact": "9087cdd7aaaf031b" }, "audit_release_exposure": { "input compact": "d4ebd6a35bc062aa", - "input full": "5055ede7d1f3450f", - "output compact": "9087cdd7aaaf031b", - "output full": "49ac2a8610170a58" + "output compact": "9087cdd7aaaf031b" }, "batch_decompile": { "input compact": "cd55cbd9191fb397", - "input full": "35e3b8c0e37d488c", - "output compact": "9087cdd7aaaf031b", - "output full": "e422f6fcbe4846d2" + "output compact": "9087cdd7aaaf031b" }, "binary_overview": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "ce6b6a6b3e76650b" + "output compact": "9087cdd7aaaf031b" }, "binary_session": { "input compact": "ea2ae40d88bad772", - "input full": "bdc9ef39d01dc6af", - "output compact": "9cda19dfa1310afa", - "output full": "88501cffa8a4622d" + "output compact": "9cda19dfa1310afa" }, "build_call_path": { "input compact": "5b291bcc2323c4d0", - "input full": "591fe3b23aa24195", - "output compact": "9087cdd7aaaf031b", - "output full": "b465a94a856ee163" + "output compact": "9087cdd7aaaf031b" }, "build_reconstruction_obligation_ledger": { "input compact": "a7cd5ce32e774439", - "input full": "fcb5b278ee29d4a6", - "output compact": "9087cdd7aaaf031b", - "output full": "f43613afd95c3750" + "output compact": "9087cdd7aaaf031b" }, "capture_adb_screen": { "input compact": "5270421b8c21c650", - "input full": "d8e07a01fdcfbde6", - "output compact": "9087cdd7aaaf031b", - "output full": "16e20fe396e71218" + "output compact": "9087cdd7aaaf031b" }, "capture_browser_scenario": { "input compact": "b6594631753958b4", - "input full": "ec0e1087368b32b1", - "output compact": "9087cdd7aaaf031b", - "output full": "bdc0e92ab339d9f0" + "output compact": "9087cdd7aaaf031b" }, "capture_electron_scenario": { "input compact": "1966487f98e87f51", - "input full": "b70e8d23758a4f2a", - "output compact": "9087cdd7aaaf031b", - "output full": "7d7f5efdd61e3b3a" + "output compact": "9087cdd7aaaf031b" }, "capture_native_ui_scenario": { "input compact": "699d21a37250979b", - "input full": "9980fd16980030d9", - "output compact": "9087cdd7aaaf031b", - "output full": "a5491cbb15a68153" + "output compact": "9087cdd7aaaf031b" }, "capture_process_scenario": { "input compact": "b16f62e36c31a0cc", - "input full": "f12f6aaec6b7b255", - "output compact": "9087cdd7aaaf031b", - "output full": "8e18236d4810c499" + "output compact": "9087cdd7aaaf031b" }, "capture_web_screenshot": { "input compact": "7ba00cbe6d75c7d9", - "input full": "105e53d45b26ca8f", - "output compact": "9087cdd7aaaf031b", - "output full": "a494fd22fad4c52a" + "output compact": "9087cdd7aaaf031b" }, "check_build_task": { "input compact": "2e039048620b8a11", - "input full": "ce5d4c3ad1f07904", - "output compact": "86cabbcac4bdb308", - "output full": "7570e201f7392892" + "output compact": "86cabbcac4bdb308" }, "close_binary": { "input compact": "1650d4dd6dfc3b5e", - "input full": "13c14d6d73723f3f", - "output compact": "67aadfbdedfd0c6e", - "output full": "8de052ee6c183d96" + "output compact": "67aadfbdedfd0c6e" }, "close_build_computer": { "input compact": "3b3f0b6ed9a02890", - "input full": "9adc1d883ed94c75", - "output compact": "b384409a7d0f96ed", - "output full": "994b57fa6b0ba714" + "output compact": "b384409a7d0f96ed" }, "close_frida_session": { "input compact": "d5defc56611ce19d", - "input full": "4b71db780c1a3f28", - "output compact": "9ca208e0b4a3ca6f", - "output full": "9e736438339d5d4b" + "output compact": "9ca208e0b4a3ca6f" }, "close_gdb_session": { "input compact": "d5defc56611ce19d", - "input full": "4b71db780c1a3f28", - "output compact": "377e3b6f9b0db8e3", - "output full": "37fd22b655998a1d" + "output compact": "377e3b6f9b0db8e3" }, "close_rizin_debug_session": { "input compact": "d5defc56611ce19d", - "input full": "4b71db780c1a3f28", - "output compact": "9ca208e0b4a3ca6f", - "output full": "9e736438339d5d4b" + "output compact": "9ca208e0b4a3ca6f" }, "collect_adb_bugreport": { "input compact": "5270421b8c21c650", - "input full": "126e7335caba09a9", - "output compact": "9087cdd7aaaf031b", - "output full": "e5a1c40ee2b0552b" + "output compact": "9087cdd7aaaf031b" }, "comment": { "input compact": "f1b0a54d8c702be5", - "input full": "dbc2a763429ebf8f", - "output compact": "9e24f29540c4da8a", - "output full": "83872009723f45ea" + "output compact": "9e24f29540c4da8a" }, "compare_application_versions": { "input compact": "6e86c4697a074d94", - "input full": "a476cd134b7fd2f2", - "output compact": "9087cdd7aaaf031b", - "output full": "61c8381862f56ebf" + "output compact": "9087cdd7aaaf031b" }, "compare_artifacts": { "input compact": "633f07e1c25fb0e9", - "input full": "d8be0c2cd573ba1d", - "output compact": "9087cdd7aaaf031b", - "output full": "98c6ffc654611bf4" + "output compact": "9087cdd7aaaf031b" }, "compare_bundles": { "input compact": "65c9f2cad6fee49f", - "input full": "cf71f82072833270", - "output compact": "9087cdd7aaaf031b", - "output full": "0a58e04d9c80a831" + "output compact": "9087cdd7aaaf031b" }, "compare_functions": { "input compact": "633f07e1c25fb0e9", - "input full": "49082f494d551ebd", - "output compact": "9087cdd7aaaf031b", - "output full": "db50e6fc0e036b08" + "output compact": "9087cdd7aaaf031b" }, "compare_javascript_export_shapes": { "input compact": "169fa0bb1123cfed", - "input full": "ee0493e1d87ebbb8", - "output compact": "9087cdd7aaaf031b", - "output full": "d7c3906507c145c4" + "output compact": "9087cdd7aaaf031b" }, "compare_managed_members": { "input compact": "633f07e1c25fb0e9", - "input full": "e95d4c348b60dcc9", - "output compact": "9087cdd7aaaf031b", - "output full": "5d237ec5c0048a4a" + "output compact": "9087cdd7aaaf031b" }, "compare_process_captures": { "input compact": "09de9679075c918d", - "input full": "4ebd05582fe3252b", - "output compact": "9087cdd7aaaf031b", - "output full": "e1d49f0a80706861" + "output compact": "9087cdd7aaaf031b" }, "compare_source_to_bundle": { "input compact": "53e35e56039bbec8", - "input full": "6e99d392d341fb5e", - "output compact": "9087cdd7aaaf031b", - "output full": "f24053aae7e1e599" + "output compact": "9087cdd7aaaf031b" }, "compare_static_and_observed_endpoints": { "input compact": "87bd558a19485f40", - "input full": "32707dc01645362f", - "output compact": "9087cdd7aaaf031b", - "output full": "8b13c007fbc604d7" + "output compact": "9087cdd7aaaf031b" }, "compare_wasm_interfaces": { "input compact": "762370ff0063b96c", - "input full": "9886ebad78aa5342", - "output compact": "9087cdd7aaaf031b", - "output full": "f14fc6b047e054a9" + "output compact": "9087cdd7aaaf031b" }, "compare_web_captures": { "input compact": "9a27667fef6bb852", - "input full": "22e4552549eba9cd", - "output compact": "9087cdd7aaaf031b", - "output full": "6513f71988070aff" + "output compact": "9087cdd7aaaf031b" }, "compare_web_screenshots": { "input compact": "eff327dda5c10d9a", - "input full": "9a2e7804cb4c51a1", - "output compact": "9087cdd7aaaf031b", - "output full": "79bc0e517b7b90a2" + "output compact": "9087cdd7aaaf031b" }, "correlate_static_and_runtime": { "input compact": "24360903f54081d4", - "input full": "b66f3e8b56ac6886", - "output compact": "9087cdd7aaaf031b", - "output full": "587854817049ba58" + "output compact": "9087cdd7aaaf031b" }, "current_address": { "input compact": "4df1857d0f25b9b1", - "input full": "8d7280bca0b14cf2", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "current_document": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "current_procedure": { "input compact": "4df1857d0f25b9b1", - "input full": "8d7280bca0b14cf2", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "cutter_command": { "input compact": "b91fd6378ec89ada", - "input full": "b2498419df0799a6", - "output compact": "9087cdd7aaaf031b", - "output full": "3ad6e2b547bea0b2" + "output compact": "9087cdd7aaaf031b" }, "decode_android_resources": { "input compact": "a1b45d95750ca8ef", - "input full": "a431c83aa943b8ae", - "output compact": "9087cdd7aaaf031b", - "output full": "305a0073811ca5db" + "output compact": "9087cdd7aaaf031b" }, "decode_interface_builder": { "input compact": "0395893cc47e3a89", - "input full": "0a3e8cd205422160", - "output compact": "9087cdd7aaaf031b", - "output full": "1350c0e95cd079a2" + "output compact": "9087cdd7aaaf031b" }, "decode_signal_recording": { "input compact": "57f1e31eab5d6150", - "input full": "3421d68afea3e305", - "output compact": "9087cdd7aaaf031b", - "output full": "034b694447d662c9" + "output compact": "9087cdd7aaaf031b" }, "decompile_all": { "input compact": "f9475f6ca1476ae0", - "input full": "8edf4f9728334112", - "output compact": "9087cdd7aaaf031b", - "output full": "027c85c124f3eb94" + "output compact": "9087cdd7aaaf031b" }, "decompile_jeb_item": { "input compact": "365c0a3f3e7ee733", - "input full": "550314d3566b4d06", - "output compact": "9087cdd7aaaf031b", - "output full": "9eaba827b9c0df95" + "output compact": "9087cdd7aaaf031b" }, "demangle_swift": { "input compact": "ce0ed2a63eb8849e", - "input full": "c00ce4105952db46", - "output compact": "9087cdd7aaaf031b", - "output full": "c7e9a6485b9f2511" + "output compact": "9087cdd7aaaf031b" }, "discover_tools": { "input compact": "fdb69f3d5123e80e", - "input full": "db7d3a192d019fd3", - "output compact": "3e22807c622e44b2", - "output full": "32f3a4bae160d629" + "output compact": "3e22807c622e44b2" }, "discover_webmcp_tools": { "input compact": "4425590849a07d7f", - "input full": "4c1c00b38167c522", - "output compact": "9087cdd7aaaf031b", - "output full": "6e44435e7574e13a" + "output compact": "9087cdd7aaaf031b" }, "evaluate_reconstruction_coverage": { "input compact": "4c824f77c8039956", - "input full": "7939aaf37e3ed764", - "output compact": "6eb93bf85f43f04d", - "output full": "d6d551e9c2d87f25" + "output compact": "6eb93bf85f43f04d" }, "execute_rizin_command": { "input compact": "abd06cd2d1a16823", - "input full": "2f94236f15541ad1", - "output compact": "9087cdd7aaaf031b", - "output full": "6641582cc244716a" + "output compact": "9087cdd7aaaf031b" }, "export_build_environment": { "input compact": "f2e2ecfc4671d550", - "input full": "62bc3e2d0ab04e94", - "output compact": "c9f69f0d4856f6f4", - "output full": "390dad80870631cc" + "output compact": "c9f69f0d4856f6f4" }, "export_environment_bundle": { "input compact": "61eb02cacceb0fc4", - "input full": "744ea588b4eead6a", - "output compact": "31f5a06d7bc1ebba", - "output full": "dceeebaf6160555e" + "output compact": "31f5a06d7bc1ebba" }, "export_evidence_bundle": { "input compact": "fc434da7d0233b71", - "input full": "0108de34746f6550", - "output compact": "66a3eb6c5dc33577", - "output full": "3e62cab0931831f4" + "output compact": "66a3eb6c5dc33577" }, "export_web_scripts": { "input compact": "b18c35aaccafe15a", - "input full": "1184064cfab19a5e", - "output compact": "9087cdd7aaaf031b", - "output full": "e9fb40af65d5281d" + "output compact": "9087cdd7aaaf031b" }, "extract_artifact": { "input compact": "5ecb2020ae1f8f42", - "input full": "01fa5d96d1d7ec30", - "output compact": "9087cdd7aaaf031b", - "output full": "ccc9da0f11f369f8" + "output compact": "9087cdd7aaaf031b" }, "extract_firmware": { "input compact": "6ca4f62a458ff3d7", - "input full": "5532b56827f00c58", - "output compact": "9087cdd7aaaf031b", - "output full": "3f1934751041ebc3" + "output compact": "9087cdd7aaaf031b" }, "find_changed_behavior": { "input compact": "a8a946736b36ddb8", - "input full": "f4ee35765ec19d05", - "output compact": "9087cdd7aaaf031b", - "output full": "33ae23e1449da56d" + "output compact": "9087cdd7aaaf031b" }, "find_xrefs_to_name": { "input compact": "ba6030423e62d130", - "input full": "a71994c97b288b3f", - "output compact": "9087cdd7aaaf031b", - "output full": "8f1b014112cb21c0" + "output compact": "9087cdd7aaaf031b" }, "frida_session_status": { "input compact": "31cc6bc09d1bb29d", - "input full": "1e54d48e2c836204", - "output compact": "9087cdd7aaaf031b", - "output full": "d21c77dcda2c79e7" + "output compact": "9087cdd7aaaf031b" }, "gdb_console": { "input compact": "3a493db9e13845e2", - "input full": "f881edec7b3deaad", - "output compact": "9087cdd7aaaf031b", - "output full": "81a18d0c5a98d9e8" + "output compact": "9087cdd7aaaf031b" }, "gdb_session_status": { "input compact": "d5defc56611ce19d", - "input full": "4b71db780c1a3f28", - "output compact": "c29682a2312eb267", - "output full": "b966c84f3b4307ce" + "output compact": "c29682a2312eb267" }, "get_call_graph": { "input compact": "5d40cd3080063c1d", - "input full": "d7ee8a225352e1b9", - "output compact": "9087cdd7aaaf031b", - "output full": "54b6a9dbb64e9097" + "output compact": "9087cdd7aaaf031b" }, "get_evidence_bundle": { "input compact": "3c840e5bbd9e9894", - "input full": "da9006c880020e5e", - "output compact": "9cda19dfa1310afa", - "output full": "a26c3bd621203a0d" + "output compact": "9cda19dfa1310afa" }, "get_navigation_context": { "input compact": "4c9f333a998b4999", - "input full": "3e8f619285b8a44a", - "output compact": "f496722f55251654", - "output full": "268e4988bdfe1284" + "output compact": "f496722f55251654" }, "get_objc_classes": { "input compact": "e2e179a75a6a8189", - "input full": "f2dad16a8fa59c65", - "output compact": "9087cdd7aaaf031b", - "output full": "0f0a8280b706c3ec" + "output compact": "9087cdd7aaaf031b" }, "get_objc_protocols": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "2294d4cd7b528639" + "output compact": "9087cdd7aaaf031b" }, "goto_address": { "input compact": "c9d8a29b5ea9eaf2", - "input full": "0ab8a9a24f970cbf", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "guided_workflows": { "input compact": "eab443f9a403d9f4", - "input full": "7b82ba652be17879", - "output compact": "2fefdacc1c7b9233", - "output full": "bab03929a6e8fea7" + "output compact": "2fefdacc1c7b9233" }, "identify_flutter_build": { "input compact": "18d266261424a97c", - "input full": "a0d01d4361373cc0", - "output compact": "9087cdd7aaaf031b", - "output full": "cfabff00f232456d" + "output compact": "9087cdd7aaaf031b" }, "import_evidence_bundle": { "input compact": "18d266261424a97c", - "input full": "f9275b31e76e709a", - "output compact": "6c6c35e1ceaeb7ae", - "output full": "00f86c62d562bdc9" + "output compact": "6c6c35e1ceaeb7ae" }, "import_managed_reconstruction": { "input compact": "32ffbbfc8df34d78", - "input full": "3192e92a7311cdfb", - "output compact": "9087cdd7aaaf031b", - "output full": "8bb0832e69f9b84c" + "output compact": "9087cdd7aaaf031b" }, "inline_comment": { "input compact": "f1b0a54d8c702be5", - "input full": "dbc2a763429ebf8f", - "output compact": "9e24f29540c4da8a", - "output full": "83872009723f45ea" + "output compact": "9e24f29540c4da8a" }, "inspect_adb_client": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "cfb880acbdaebae1" + "output compact": "9087cdd7aaaf031b" }, "inspect_adb_device": { "input compact": "0eefd4f4245ea66b", - "input full": "809345a31c21bb43", - "output compact": "9087cdd7aaaf031b", - "output full": "ca55e6f468904bc1" + "output compact": "9087cdd7aaaf031b" }, "inspect_adb_display": { "input compact": "0eefd4f4245ea66b", - "input full": "809345a31c21bb43", - "output compact": "9087cdd7aaaf031b", - "output full": "0ca5f240faf27b0e" + "output compact": "9087cdd7aaaf031b" }, "inspect_adb_package": { "input compact": "2d6b5bb69d217de1", - "input full": "18d1a418dd45188f", - "output compact": "9087cdd7aaaf031b", - "output full": "578e9aa061ba090c" + "output compact": "9087cdd7aaaf031b" }, "inspect_adb_window": { "input compact": "0eefd4f4245ea66b", - "input full": "809345a31c21bb43", - "output compact": "9087cdd7aaaf031b", - "output full": "cddb436877d72abe" + "output compact": "9087cdd7aaaf031b" }, "inspect_address_context": { "input compact": "9dd6c6df7b9ea95f", - "input full": "cf19092234e1cb71", - "output compact": "9cda19dfa1310afa", - "output full": "5ec565e258d7f01d" + "output compact": "9cda19dfa1310afa" }, "inspect_analysis_settings": { - "input compact": "60b44d23dd57ba69", - "input full": "1f5d7c73cf8a5b40", - "output compact": "b3a429110120aff1", - "output full": "d98ea6dc62763c7b" + "input compact": "7020930a4af1f37c", + "output compact": "b3a429110120aff1" }, "inspect_analysis_view": { "input compact": "99dbe34c28cd04dd", - "input full": "9f3548ab0d96918c", - "output compact": "9087cdd7aaaf031b", - "output full": "eb1b9cf3a1cb1101" + "output compact": "9087cdd7aaaf031b" }, "inspect_android_class": { "input compact": "fb8be61f22dca985", - "input full": "2acc22cf59776739", - "output compact": "9087cdd7aaaf031b", - "output full": "d81ad5ccc4d224b0" + "output compact": "9087cdd7aaaf031b" }, "inspect_android_jni_bindings": { "input compact": "e5d2cfec1580148e", - "input full": "62232046e59e04f7", - "output compact": "9087cdd7aaaf031b", - "output full": "0518be07f91f63f2" + "output compact": "9087cdd7aaaf031b" }, "inspect_android_method": { "input compact": "f4d383779c829cc1", - "input full": "a306408bc7c4174a", - "output compact": "9087cdd7aaaf031b", - "output full": "a53e146e0e939917" + "output compact": "9087cdd7aaaf031b" }, "inspect_android_package": { "input compact": "6f391991d856198a", - "input full": "30021dcd33a85198", - "output compact": "9087cdd7aaaf031b", - "output full": "5546a2f4315e37be" + "output compact": "9087cdd7aaaf031b" }, "inspect_api_contract": { "input compact": "79519ee02b61df07", - "input full": "a3bab8e17fe60041", - "output compact": "9087cdd7aaaf031b", - "output full": "d1df13b6284b5600" + "output compact": "9087cdd7aaaf031b" }, "inspect_apk_signature": { "input compact": "dec153ba3fc1960c", - "input full": "4b9794bc1798c807", - "output compact": "9087cdd7aaaf031b", - "output full": "3e82b1caf1d35728" + "output compact": "9087cdd7aaaf031b" }, "inspect_apktool_client": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "dd7496e2a2b6c25c" + "output compact": "9087cdd7aaaf031b" }, "inspect_apple_ips": { "input compact": "c55c03a1cfb12bf4", - "input full": "21914ef23fb008e9", - "output compact": "9087cdd7aaaf031b", - "output full": "596616d03d2149a0" + "output compact": "9087cdd7aaaf031b" }, "inspect_artifact": { "input compact": "aabcc05db1232b21", - "input full": "aab2ccd8777bd3dc", - "output compact": "9087cdd7aaaf031b", - "output full": "60fa986d5d03bc5e" + "output compact": "9087cdd7aaaf031b" }, "inspect_asset_catalog": { "input compact": "72d0fff774075e7c", - "input full": "e75dd6946f5ebbd4", - "output compact": "9087cdd7aaaf031b", - "output full": "58c7e5f89c6f5e41" + "output compact": "9087cdd7aaaf031b" }, "inspect_binary_hardening": { "input compact": "c2417cfbfd1a4976", - "input full": "a443f7ff71920d2b", - "output compact": "9087cdd7aaaf031b", - "output full": "0e8485f4b878f5d9" + "output compact": "9087cdd7aaaf031b" }, "inspect_binary_layout": { "input compact": "59748ce45f667d12", - "input full": "8c2f01ee4713b3aa", - "output compact": "9087cdd7aaaf031b", - "output full": "c183ea17727dde13" + "output compact": "9087cdd7aaaf031b" }, "inspect_byte_entropy": { "input compact": "fdd4a3a111b4308b", - "input full": "59cf0e12bc3c108e", - "output compact": "9087cdd7aaaf031b", - "output full": "de5a7d222a981da3" + "output compact": "9087cdd7aaaf031b" }, "inspect_dart_aot": { "input compact": "a048bd94aa23c39e", - "input full": "84b7a4d7885725d2", - "output compact": "9087cdd7aaaf031b", - "output full": "e5366bda21abaa94" + "output compact": "9087cdd7aaaf031b" }, "inspect_desktop_application": { "input compact": "f7cf5551a6ffd825", - "input full": "56c058e26af84fce", - "output compact": "9087cdd7aaaf031b", - "output full": "55ffcf71322c47e6" + "output compact": "9087cdd7aaaf031b" }, "inspect_devicetree": { "input compact": "02a13a32cf55d458", - "input full": "5e4ff085d8caedb1", - "output compact": "9087cdd7aaaf031b", - "output full": "37cd8c96dac7d44c" + "output compact": "9087cdd7aaaf031b" }, "inspect_electron_fuses": { "input compact": "18d266261424a97c", - "input full": "6c021241d8cd9bba", - "output compact": "9087cdd7aaaf031b", - "output full": "86c725cca41b2e8b" + "output compact": "9087cdd7aaaf031b" }, "inspect_electron_page": { "input compact": "4bfd2a08afbc54fc", - "input full": "e7e98605fd861bd2", - "output compact": "9087cdd7aaaf031b", - "output full": "ddf3b275b9e2f853" + "output compact": "9087cdd7aaaf031b" }, "inspect_elf_notes": { "input compact": "8b30e04cd3ac0805", - "input full": "72951828309d7890", - "output compact": "9087cdd7aaaf031b", - "output full": "09e1374b000c473f" + "output compact": "9087cdd7aaaf031b" }, "inspect_elf_unwind_ranges": { "input compact": "9a9a8438a4b6e9f0", - "input full": "de4fdb0b32bba220", - "output compact": "9087cdd7aaaf031b", - "output full": "cb6cb8ce3d696fc6" + "output compact": "9087cdd7aaaf031b" }, "inspect_erlang_beam": { "input compact": "c14ca0aea789c645", - "input full": "e8105c1c3831969f", - "output compact": "9087cdd7aaaf031b", - "output full": "71f853fdc1ce0f71" + "output compact": "9087cdd7aaaf031b" }, "inspect_evm_interface": { "input compact": "f8a87e80e0ca66fe", - "input full": "eea3525bc6145792", - "output compact": "9087cdd7aaaf031b", - "output full": "c016c2f159ef8331" + "output compact": "9087cdd7aaaf031b" }, "inspect_firmware_regions": { "input compact": "ac72671d7bdc9ed4", - "input full": "077121200c6230d6", - "output compact": "9087cdd7aaaf031b", - "output full": "3494a60126706e43" + "output compact": "9087cdd7aaaf031b" }, "inspect_go_binary": { "input compact": "f5cc9dc3caf9edf4", - "input full": "4cc2529f61181257", - "output compact": "9087cdd7aaaf031b", - "output full": "70261e5c2f965682" + "output compact": "9087cdd7aaaf031b" }, "inspect_gpu_code_objects": { "input compact": "01c7ceeb7a6ac4d4", - "input full": "57828f2d970d5ec6", - "output compact": "9087cdd7aaaf031b", - "output full": "a6509e3b5d7bbb86" + "output compact": "9087cdd7aaaf031b" }, "inspect_il2cpp_metadata": { "input compact": "7181073b85e89641", - "input full": "d1f0b5d95f44b979", - "output compact": "9087cdd7aaaf031b", - "output full": "13b593523657b92f" + "output compact": "9087cdd7aaaf031b" }, "inspect_jeb_client": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "d46072274de39a08" + "output compact": "9087cdd7aaaf031b" }, "inspect_kernel_build_config": { "input compact": "67f91f8c76d696c7", - "input full": "90f801f16bcee24f", - "output compact": "9087cdd7aaaf031b", - "output full": "87e30d96fe5ec329" + "output compact": "9087cdd7aaaf031b" }, "inspect_kernel_module": { "input compact": "9f3ed49a3c380bd6", - "input full": "428edf5db3df7824", - "output compact": "9087cdd7aaaf031b", - "output full": "d8bd0171ffc09440" + "output compact": "9087cdd7aaaf031b" }, "inspect_keyed_archive": { "input compact": "fe4818885a05bfb0", - "input full": "3022b9844d558fd9", - "output compact": "9087cdd7aaaf031b", - "output full": "a40af63f27b8f0a5" + "output compact": "9087cdd7aaaf031b" }, "inspect_macho": { "input compact": "d50348f9415e6058", - "input full": "57d78ef0e5807969", - "output compact": "9087cdd7aaaf031b", - "output full": "2c2d4b041b83c16f" + "output compact": "9087cdd7aaaf031b" }, "inspect_managed_artifact": { "input compact": "b64e74c7116cc99b", - "input full": "39d3d9b27bfbb2bc", - "output compact": "9087cdd7aaaf031b", - "output full": "6c679ed6b00ecd30" + "output compact": "9087cdd7aaaf031b" }, "inspect_managed_members": { "input compact": "2c6c9865b3119ae2", - "input full": "4e2d466049d3db8a", - "output compact": "9087cdd7aaaf031b", - "output full": "c1398e2053c19bb2" + "output compact": "9087cdd7aaaf031b" }, "inspect_managed_native_boundaries": { "input compact": "ab5465849952e847", - "input full": "16fcf2208d2649c1", - "output compact": "9087cdd7aaaf031b", - "output full": "c8128dae7beafee6" + "output compact": "9087cdd7aaaf031b" }, "inspect_model_artifact": { "input compact": "0a9a2b524be0ce15", - "input full": "77c23542b6c0cc91", - "output compact": "9087cdd7aaaf031b", - "output full": "9fccb1bf3712547e" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_api": { "input compact": "e1341c240f08c350", - "input full": "797d8e00e4516641", - "output compact": "9087cdd7aaaf031b", - "output full": "d7c2553d6e22f8f1" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_data_type": { "input compact": "957e0a1d7fa1bd33", - "input full": "0106a28c68a6c1f4", - "output compact": "9087cdd7aaaf031b", - "output full": "a147305b1437aa33" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_dispatch_metadata": { "input compact": "743838105118ca29", - "input full": "7471befefb6ecc03", - "output compact": "9087cdd7aaaf031b", - "output full": "de464d9fb160bf5a" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_exports": { "input compact": "70adce0beddda5eb", - "input full": "ca56759b24315a92", - "output compact": "9087cdd7aaaf031b", - "output full": "27b6fa8a5cb77430" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_initializers": { "input compact": "546bf32e9f7eb381", - "input full": "8dae0031df746862", - "output compact": "9087cdd7aaaf031b", - "output full": "5c34881386230019" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_instruction": { "input compact": "940657f284593726", - "input full": "874cb6dd1433a2b9", - "output compact": "9087cdd7aaaf031b", - "output full": "fe091fab701e055f" + "output compact": "9087cdd7aaaf031b" }, "inspect_native_load_image": { "input compact": "f23a678531aa373a", - "input full": "b4f17638fe058818", - "output compact": "9087cdd7aaaf031b", - "output full": "fd83c093241df901" + "output compact": "9087cdd7aaaf031b" }, "inspect_ntfs_journal": { "input compact": "2e70facfe9d6f4f4", - "input full": "27993fb75f380d3c", - "output compact": "9087cdd7aaaf031b", - "output full": "dcf67a22f25e2efa" + "output compact": "9087cdd7aaaf031b" }, "inspect_packet_capture": { "input compact": "6b084c215c2391a6", - "input full": "83e69e1840a1b76f", - "output compact": "9087cdd7aaaf031b", - "output full": "fa4cbed82a20db32" + "output compact": "9087cdd7aaaf031b" }, "inspect_packet_fields": { "input compact": "7c1e0a2a80f4c87c", - "input full": "352f41bad20373eb", - "output compact": "9087cdd7aaaf031b", - "output full": "952deccd5027ec69" + "output compact": "9087cdd7aaaf031b" }, "inspect_pcap_capture": { "input compact": "5655059de2dcc830", - "input full": "37c0dee810ad43ba", - "output compact": "9087cdd7aaaf031b", - "output full": "ab4a25c8a892c33f" + "output compact": "9087cdd7aaaf031b" }, "inspect_pcap_client": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "3300dea89b1f7e6d" + "output compact": "9087cdd7aaaf031b" }, "inspect_pe_certificates": { "input compact": "eaa9f9a271f19ee9", - "input full": "42cfc75628887310", - "output compact": "9087cdd7aaaf031b", - "output full": "c90224285c2b9eaa" + "output compact": "9087cdd7aaaf031b" }, "inspect_pe_imports": { "input compact": "ad8b37fcf0adb19e", - "input full": "5b14dc7c54013f45", - "output compact": "9087cdd7aaaf031b", - "output full": "956b1b81feabfce5" + "output compact": "9087cdd7aaaf031b" }, "inspect_pe_load_config": { "input compact": "ea4a836a963d167b", - "input full": "1627e15cbccf40eb", - "output compact": "9087cdd7aaaf031b", - "output full": "57ca664436841319" + "output compact": "9087cdd7aaaf031b" }, "inspect_pe_resources": { "input compact": "2caac0f860526546", - "input full": "27aac58a9f10fc00", - "output compact": "9087cdd7aaaf031b", - "output full": "146513ec0ac0f950" + "output compact": "9087cdd7aaaf031b" }, "inspect_playwright_trace": { "input compact": "8211b6c65f46d420", - "input full": "b75133a3a13dde4a", - "output compact": "9087cdd7aaaf031b", - "output full": "76b7a1d70a4056fa" + "output compact": "9087cdd7aaaf031b" }, "inspect_plist": { "input compact": "c22855860d96f2a7", - "input full": "cb3c96882c8847c0", - "output compact": "9087cdd7aaaf031b", - "output full": "7c6fbc3e7d8e5b05" + "output compact": "9087cdd7aaaf031b" }, "inspect_python_pyc": { "input compact": "5969a9c826c2c055", - "input full": "fbc4308060a7efc2", - "output compact": "9087cdd7aaaf031b", - "output full": "ac4617fc12b74383" + "output compact": "9087cdd7aaaf031b" }, "inspect_recorded_crash": { "input compact": "3460ff3dfbfb1b2a", - "input full": "5229ff71304da517", - "output compact": "9087cdd7aaaf031b", - "output full": "35d1105e6420123f" + "output compact": "9087cdd7aaaf031b" }, "inspect_rust_binary": { "input compact": "55e64710a49b9fa2", - "input full": "50b4214d76713128", - "output compact": "9087cdd7aaaf031b", - "output full": "48bdf3e1453ddc77" + "output compact": "9087cdd7aaaf031b" }, "inspect_signal_decoders": { "input compact": "3748645472ab43e5", - "input full": "8d84ddfb4b2d7bd1", - "output compact": "9087cdd7aaaf031b", - "output full": "f07b92837cd184ee" + "output compact": "9087cdd7aaaf031b" }, "inspect_signature": { "input compact": "fcb6d54fec138c1e", - "input full": "b339e1884abe4fb2", - "output compact": "9087cdd7aaaf031b", - "output full": "04563e76e07f5d22" + "output compact": "9087cdd7aaaf031b" }, "inspect_sqlite_database": { "input compact": "e85331872a5525e6", - "input full": "dff8cc156179e435", - "output compact": "9087cdd7aaaf031b", - "output full": "53687605d9bfd0bd" + "output compact": "9087cdd7aaaf031b" }, "inspect_v8_code_cache": { "input compact": "17341f97702a0422", - "input full": "de55f188be526d0f", - "output compact": "9087cdd7aaaf031b", - "output full": "08e94e6a93710d59" + "output compact": "9087cdd7aaaf031b" }, "inspect_wasm_artifact": { "input compact": "7a138da90fc5e650", - "input full": "c4c28d5c089c03b7", - "output compact": "9087cdd7aaaf031b", - "output full": "cef0b0922a1b7685" + "output compact": "9087cdd7aaaf031b" }, "inspect_web_event_listeners": { "input compact": "2ef444a0a29f50e4", - "input full": "ce8cd07db8c7e59b", - "output compact": "9087cdd7aaaf031b", - "output full": "429e5c77ee7040be" + "output compact": "9087cdd7aaaf031b" }, "inspect_web_network_capture": { "input compact": "9a171b304737ce3d", - "input full": "a73cb946587e663b", - "output compact": "9087cdd7aaaf031b", - "output full": "bec94829c8c29d08" + "output compact": "9087cdd7aaaf031b" }, "inspect_web_page": { "input compact": "ae6db425047c0a8f", - "input full": "48fbe489c80a476d", - "output compact": "9087cdd7aaaf031b", - "output full": "b205593aafb06b34" + "output compact": "9087cdd7aaaf031b" }, "inspect_windows_shortcut": { "input compact": "f7055432bf97761a", - "input full": "4d754a10fbaca4ca", - "output compact": "9087cdd7aaaf031b", - "output full": "b05303d2b14c9bb9" + "output compact": "9087cdd7aaaf031b" }, "inspect_with_objdump": { "input compact": "3f51a78954949124", - "input full": "afcfcee5af05febd", - "output compact": "9087cdd7aaaf031b", - "output full": "aedf30bb7dbbdc00" + "output compact": "9087cdd7aaaf031b" }, "install_adb_package": { "input compact": "46f07cf88578ac7a", - "input full": "17ea522d18ea46f7", - "output compact": "9087cdd7aaaf031b", - "output full": "84b6fbf378d1da19" + "output compact": "9087cdd7aaaf031b" }, "instrument_with_frida": { "input compact": "6d62658ca139ecb4", - "input full": "0f976284ab4ee551", - "output compact": "9087cdd7aaaf031b", - "output full": "4442bf95ef4f9c2c" + "output compact": "9087cdd7aaaf031b" }, "list_adb_devices": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "9087cdd7aaaf031b", - "output full": "dfd224b0e6e55d48" + "output compact": "9087cdd7aaaf031b" }, "list_adb_directory": { "input compact": "5560c011cdc77714", - "input full": "6da0d1c6b41aee8d", - "output compact": "9087cdd7aaaf031b", - "output full": "6b6c0c769d846493" + "output compact": "9087cdd7aaaf031b" }, "list_adb_features": { "input compact": "0eefd4f4245ea66b", - "input full": "809345a31c21bb43", - "output compact": "9087cdd7aaaf031b", - "output full": "77b63c8f6ff19cac" + "output compact": "9087cdd7aaaf031b" }, "list_adb_packages": { "input compact": "5cd991375a9f0a8f", - "input full": "88925c7f277cf06e", - "output compact": "9087cdd7aaaf031b", - "output full": "4bf7a3ce82b77b7a" + "output compact": "9087cdd7aaaf031b" }, "list_adb_processes": { "input compact": "0eefd4f4245ea66b", - "input full": "809345a31c21bb43", - "output compact": "9087cdd7aaaf031b", - "output full": "505d82df4c35a8bc" + "output compact": "9087cdd7aaaf031b" }, "list_adb_services": { "input compact": "0eefd4f4245ea66b", - "input full": "809345a31c21bb43", - "output compact": "9087cdd7aaaf031b", - "output full": "6cb0e05a69b2581c" + "output compact": "9087cdd7aaaf031b" }, "list_architectures": { "input compact": "fcb6d54fec138c1e", - "input full": "b339e1884abe4fb2", - "output compact": "9087cdd7aaaf031b", - "output full": "1db9b1172c5f67a1" + "output compact": "9087cdd7aaaf031b" }, "list_bookmarks": { "input compact": "4df1857d0f25b9b1", - "input full": "8d7280bca0b14cf2", - "output compact": "ef2ab2008a66fdd8", - "output full": "2f1381be676687e2" + "output compact": "ef2ab2008a66fdd8" }, "list_browser_targets": { "input compact": "cfdc32cb7dc51cda", - "input full": "c931fc88e338c96c", - "output compact": "9087cdd7aaaf031b", - "output full": "7c97ab3784537f37" + "output compact": "9087cdd7aaaf031b" }, "list_cutter_sessions": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "a61f3d77d190e70d", - "output full": "9d3c28981f007665" + "output compact": "a61f3d77d190e70d" }, "list_documents": { "input compact": "32062bdb9024160d", - "input full": "4ca60b6725ad78fa", - "output compact": "49662c31e39ea6a1", - "output full": "cd9580c4d8d853b3" + "output compact": "49662c31e39ea6a1" }, "list_electron_targets": { "input compact": "c32eb20cbe48cefa", - "input full": "745a189166918d0e", - "output compact": "9087cdd7aaaf031b", - "output full": "b02c41d5e62368a9" + "output compact": "9087cdd7aaaf031b" }, "list_frida_devices": { "input compact": "4478f0c5fb8ad186", - "input full": "a9c8cc732879c69b", - "output compact": "f1344e6ff37d2aa3", - "output full": "d4ebd1bf7ee78089" + "output compact": "f1344e6ff37d2aa3" }, "list_frida_processes": { "input compact": "b04d2099435f3cac", - "input full": "0c7186de2af40a75", - "output compact": "f685f02d537b3bdf", - "output full": "646612a99438b324" + "output compact": "f685f02d537b3bdf" }, "list_javascript_runtime_targets": { "input compact": "2b836a5a8a6c41d0", - "input full": "73b1f9e25fc8df9f", - "output compact": "9087cdd7aaaf031b", - "output full": "2c3b9a18aa07277a" + "output compact": "9087cdd7aaaf031b" }, "list_jeb_units": { "input compact": "4b137397fb16524d", - "input full": "e247faf31fe0ec63", - "output compact": "9087cdd7aaaf031b", - "output full": "f61ff906072d2f88" + "output compact": "9087cdd7aaaf031b" }, "list_names": { "input compact": "0ab7d5f507e24ad6", - "input full": "5ef5bc20130695cc", - "output compact": "2622a47474da6c03", - "output full": "79049d31807466ab" + "output compact": "2622a47474da6c03" }, "list_procedures": { "input compact": "96e4c1b335588f47", - "input full": "41b8cbb8e9fe9c11", - "output compact": "2622a47474da6c03", - "output full": "e549351f44938a72" + "output compact": "2622a47474da6c03" }, "list_segments": { "input compact": "4df1857d0f25b9b1", - "input full": "8d7280bca0b14cf2", - "output compact": "ef2ab2008a66fdd8", - "output full": "345e36b01e114028" + "output compact": "ef2ab2008a66fdd8" }, "list_strings": { "input compact": "0ab7d5f507e24ad6", - "input full": "5ef5bc20130695cc", - "output compact": "2622a47474da6c03", - "output full": "e72ac73a4407c596" + "output compact": "2622a47474da6c03" }, "list_unknowns": { "input compact": "8e887061536632ee", - "input full": "e28dd673bdb76694", - "output compact": "9cda19dfa1310afa", - "output full": "f5d81bfa0cecc921" + "output compact": "9cda19dfa1310afa" }, "load_frida_script": { "input compact": "ed20157c00980757", - "input full": "bec28cb50b2736f8", - "output compact": "9087cdd7aaaf031b", - "output full": "70b2c62ad8ca8b3f" + "output compact": "9087cdd7aaaf031b" }, "mark_build_checkpoint": { "input compact": "f2e13f2c20cc0620", - "input full": "437c94722fb6250a", - "output compact": "8a71b3f0e157e74f", - "output full": "bd77a1a7bd66b777" + "output compact": "8a71b3f0e157e74f" }, "match_native_functions": { "input compact": "149e91c2ce2cc2f3", - "input full": "e537462c725377ac", - "output compact": "9087cdd7aaaf031b", - "output full": "6139fce8f156f9ae" + "output compact": "9087cdd7aaaf031b" }, "measure_build_task": { "input compact": "e7f4c82c15a6ce96", - "input full": "ecb0fd8a3eca02d3", - "output compact": "d368534f377ef13b", - "output full": "61de1405afa96929" + "output compact": "d368534f377ef13b" }, "next_address": { "input compact": "f1b0a54d8c702be5", - "input full": "dbc2a763429ebf8f", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "observe_javascript_runtime": { "input compact": "e04053c4c8250c81", - "input full": "7eb7e5f05efc1d0d", - "output compact": "9087cdd7aaaf031b", - "output full": "c18514ef9e3c9a14" + "output compact": "9087cdd7aaaf031b" }, "observe_native_calls": { "input compact": "3ef1f34cab7e4387", - "input full": "5407f3241aa3941e", - "output compact": "9087cdd7aaaf031b", - "output full": "5840fd691f7aaba1" + "output compact": "9087cdd7aaaf031b" }, "observe_native_ui": { "input compact": "79890b6c44be3b32", - "input full": "6ebf031c232e994c", - "output compact": "9087cdd7aaaf031b", - "output full": "a5491cbb15a68153" + "output compact": "9087cdd7aaaf031b" }, "observe_web_execution": { "input compact": "d59167e845f9c262", - "input full": "eeb43f4b6574b727", - "output compact": "9087cdd7aaaf031b", - "output full": "892b41f6f9d9e563" + "output compact": "9087cdd7aaaf031b" }, "observe_web_session": { "input compact": "b68bdfd126b58c24", - "input full": "0c12d3bc050e7373", - "output compact": "9087cdd7aaaf031b", - "output full": "7670fb2eb98a4054" + "output compact": "9087cdd7aaaf031b" }, "open_binary": { "input compact": "706f1985ca397688", - "input full": "7b5cf7f1c39a70d2", - "output compact": "4cbb79604f9b9e14", - "output full": "114aaa1c3966c025" + "output compact": "4cbb79604f9b9e14" }, "open_bundle_resources": { "input compact": "18d266261424a97c", - "input full": "bd6f2ec781ec07b7", - "output compact": "57c40955388cd718", - "output full": "4f65f0daedac8b5b" + "output compact": "57c40955388cd718" }, "open_jeb_project": { "input compact": "18d266261424a97c", - "input full": "45deac1b582a89bf", - "output compact": "9087cdd7aaaf031b", - "output full": "6cdf0ade70ee4052" + "output compact": "9087cdd7aaaf031b" }, "prev_address": { "input compact": "f1b0a54d8c702be5", - "input full": "dbc2a763429ebf8f", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "procedure_address": { "input compact": "fd653e0720583704", - "input full": "0220946446d6b330", - "output compact": "4f6bd8217751f707", - "output full": "99a5a4082b2c7c12" + "output compact": "4f6bd8217751f707" }, "procedure_assembly": { "input compact": "7bbe36435a4e3ba8", - "input full": "c544b2488ecdbd55", - "output compact": "ffd2432283f65468", - "output full": "e5ab6e33086533ac" + "output compact": "ffd2432283f65468" }, "procedure_callees": { "input compact": "fd653e0720583704", - "input full": "0220946446d6b330", - "output compact": "3558a96c0cb0a314", - "output full": "e99ea10ce7e3c9cf" + "output compact": "3558a96c0cb0a314" }, "procedure_callers": { "input compact": "fd653e0720583704", - "input full": "0220946446d6b330", - "output compact": "3558a96c0cb0a314", - "output full": "aa32f77b9e522cf4" + "output compact": "3558a96c0cb0a314" }, "procedure_info": { "input compact": "7bbe36435a4e3ba8", - "input full": "c544b2488ecdbd55", - "output compact": "9087cdd7aaaf031b", - "output full": "d1a6d4604ffcb626" + "output compact": "9087cdd7aaaf031b" }, "procedure_pseudo_code": { "input compact": "6dc93a3e8a3e48e4", - "input full": "7143429e3ffe5e47", - "output compact": "a6a19eed5331b2af", - "output full": "d7512367f6208022" + "output compact": "a6a19eed5331b2af" }, "procedure_references": { "input compact": "dabb0c95aeeef564", - "input full": "dba2839ba1f62d34", - "output compact": "9087cdd7aaaf031b", - "output full": "1999fbe7856f8535" + "output compact": "9087cdd7aaaf031b" }, "project_android_application_graph": { "input compact": "af88743b51732378", - "input full": "934423de251b174e", - "output compact": "9087cdd7aaaf031b", - "output full": "863039e9b66e1d3e" + "output compact": "9087cdd7aaaf031b" }, "project_apple_application_graph": { "input compact": "af88743b51732378", - "input full": "8ebfa0d5f06423db", - "output compact": "9087cdd7aaaf031b", - "output full": "26c803059444fa84" + "output compact": "9087cdd7aaaf031b" }, "project_harmony_application_graph": { "input compact": "af88743b51732378", - "input full": "3c3d8cbfe057f1fb", - "output compact": "9087cdd7aaaf031b", - "output full": "bbde759881630604" + "output compact": "9087cdd7aaaf031b" }, "project_managed_application_graph": { "input compact": "926cbfd4477960cc", - "input full": "fdeaae10fff78bb7", - "output compact": "9087cdd7aaaf031b", - "output full": "ceb31fc3b096dab1" + "output compact": "9087cdd7aaaf031b" }, "pull_adb_file": { "input compact": "da6ee35a3849c53b", - "input full": "99b24dd80720988a", - "output compact": "9087cdd7aaaf031b", - "output full": "2ebe2c9149b53f9d" + "output compact": "9087cdd7aaaf031b" }, "pull_adb_package": { "input compact": "7f3087dd0cb3722b", - "input full": "46f398b6b0b928f3", - "output compact": "9087cdd7aaaf031b", - "output full": "838f72ec8feb0646" + "output compact": "9087cdd7aaaf031b" }, "push_adb_file": { "input compact": "09c89b1cb90f857b", - "input full": "2f39ec0c4540c53a", - "output compact": "9087cdd7aaaf031b", - "output full": "bcffe5c89f08af58" + "output compact": "9087cdd7aaaf031b" }, "read_adb_logcat": { "input compact": "827ccf60f59990d4", - "input full": "ead67c8e90f04068", - "output compact": "9087cdd7aaaf031b", - "output full": "969fbb7080d84294" + "output compact": "9087cdd7aaaf031b" }, "read_adb_setting": { "input compact": "ffd694dc45e55a70", - "input full": "a915c9414568e616", - "output compact": "9087cdd7aaaf031b", - "output full": "359be0b9452b3604" + "output compact": "9087cdd7aaaf031b" }, "read_bytes": { "input compact": "b271d6570e8bd906", - "input full": "8a052cfeb89c38eb", - "output compact": "9087cdd7aaaf031b", - "output full": "7fbea0b4ae9a2865" + "output compact": "9087cdd7aaaf031b" }, "read_function_instructions": { "input compact": "7bbe36435a4e3ba8", - "input full": "0c8dbba7f23902ea", - "output compact": "9087cdd7aaaf031b", - "output full": "8ac1d838a28a4a84" + "output compact": "9087cdd7aaaf031b" }, "read_guidance_section": { "input compact": "56957ce06b265ede", - "input full": "0312c29de650355f", - "output compact": "be25041cd42b10ec", - "output full": "320ca631319ece3a" + "output compact": "be25041cd42b10ec" }, "reconcile_javascript_runtime": { "input compact": "12dcdff55b84d363", - "input full": "37c3cf2d88cd6cb9", - "output compact": "9087cdd7aaaf031b", - "output full": "76f4021b991965d5" + "output compact": "9087cdd7aaaf031b" }, "record_build_consent": { "input compact": "a37dd3fbb6304940", - "input full": "ef23c3cff6b30a5c", - "output compact": "ed09e287cbf98b85", - "output full": "7c7a6076afade6c9" + "output compact": "ed09e287cbf98b85" }, "record_unknown": { "input compact": "685294c0ddc0e020", - "input full": "06abdaa6ca6d7a2a", - "output compact": "9cda19dfa1310afa", - "output full": "447bbb45a3d5dcdb" + "output compact": "9cda19dfa1310afa" }, "recover_javascript_sources": { "input compact": "8ac2f60d560cb8e9", - "input full": "4bb7bbafc1f87be6", - "output compact": "9087cdd7aaaf031b", - "output full": "6e20d1403b6e0973" + "output compact": "9087cdd7aaaf031b" }, "reset_investigation": { "input compact": "9fff69030d873c44", - "input full": "cecc491efc696c69", - "output compact": "0d2c6c6d5a286267", - "output full": "56fc10e0667d4def" + "output compact": "0d2c6c6d5a286267" }, "resolve_adb_packages": { "input compact": "16856277f255380d", - "input full": "c2a94402cc0d7f89", - "output compact": "9087cdd7aaaf031b", - "output full": "a7c549aa307132e3" + "output compact": "9087cdd7aaaf031b" }, "resolve_containing_procedure": { "input compact": "1e9a46a46d409356", - "input full": "dfc23cdc75cf036e", - "output compact": "9087cdd7aaaf031b", - "output full": "8df8f36e2efca85f" + "output compact": "9087cdd7aaaf031b" }, "resolve_native_call_targets": { "input compact": "940657f284593726", - "input full": "874cb6dd1433a2b9", - "output compact": "9087cdd7aaaf031b", - "output full": "3f1eea2e2d1d7b8c" + "output compact": "9087cdd7aaaf031b" }, "resume_frida_session": { "input compact": "d5defc56611ce19d", - "input full": "4b71db780c1a3f28", - "output compact": "f9a0c56f36f58265", - "output full": "e5dcac1611a3d10e" + "output compact": "f9a0c56f36f58265" }, "rizin_debug_command": { "input compact": "c70e42c90db7a9f1", - "input full": "b91945bbd6c0be2c", - "output compact": "9087cdd7aaaf031b", - "output full": "16d75d349481eb33" + "output compact": "9087cdd7aaaf031b" }, "rizin_debug_session_status": { "input compact": "d5defc56611ce19d", - "input full": "4b71db780c1a3f28", - "output compact": "cdbade1a97bbb088", - "output full": "f1aae35aa55e3da5" + "output compact": "cdbade1a97bbb088" }, "search_android_classes": { "input compact": "db67ac2566308239", - "input full": "8f628f88bdbac5cf", - "output compact": "9087cdd7aaaf031b", - "output full": "fc8143d248f073b6" + "output compact": "9087cdd7aaaf031b" }, "search_bytes": { "input compact": "05bc71d6612c74db", - "input full": "7a607295b002658b", - "output compact": "9087cdd7aaaf031b", - "output full": "d52579638295d756" + "output compact": "9087cdd7aaaf031b" }, "search_guidance": { "input compact": "e08dca23a03ab73b", - "input full": "1868228067cec6e0", - "output compact": "ccc9d9f4009e2c90", - "output full": "398153c9580881b6" + "output compact": "ccc9d9f4009e2c90" }, "search_procedures": { "input compact": "05ff4a8d2a0fa1cf", - "input full": "4e94f61b4077bfd5", - "output compact": "2622a47474da6c03", - "output full": "24b013c84aba7ea0" + "output compact": "2622a47474da6c03" }, "search_strings": { "input compact": "05ff4a8d2a0fa1cf", - "input full": "374db74e89cad759", - "output compact": "2622a47474da6c03", - "output full": "88252b743be5c8de" + "output compact": "2622a47474da6c03" }, "set_address_name": { "input compact": "b15641bedfef5010", - "input full": "9ece3e63bfaa6459", - "output compact": "3c3f5b2578e1a632", - "output full": "60ca06761be3cddf" + "output compact": "3c3f5b2578e1a632" }, "set_addresses_names": { "input compact": "9dabf376fc823a5d", - "input full": "cf5fade07eb19019", - "output compact": "9087cdd7aaaf031b", - "output full": "feff0de1f3f54d7c" + "output compact": "9087cdd7aaaf031b" }, "set_bookmark": { "input compact": "04b55352b608b7e7", - "input full": "37bcbd457d943922", - "output compact": "3c3f5b2578e1a632", - "output full": "60ca06761be3cddf" + "output compact": "3c3f5b2578e1a632" }, "set_comment": { "input compact": "1989be9d5b720d84", - "input full": "8314e8673cacea5a", - "output compact": "3c3f5b2578e1a632", - "output full": "60ca06761be3cddf" + "output compact": "3c3f5b2578e1a632" }, "set_inline_comment": { "input compact": "1989be9d5b720d84", - "input full": "f9e7a04c33c1038b", - "output compact": "3c3f5b2578e1a632", - "output full": "60ca06761be3cddf" + "output compact": "3c3f5b2578e1a632" }, "start_adb_activity": { "input compact": "1457e8daaa15b8ce", - "input full": "828047e0681e9c47", - "output compact": "9087cdd7aaaf031b", - "output full": "5ad14fede3638967" + "output compact": "9087cdd7aaaf031b" }, "start_adb_app": { "input compact": "2d6b5bb69d217de1", - "input full": "2adc09597440e823", - "output compact": "9087cdd7aaaf031b", - "output full": "227c093d87a52c0f" + "output compact": "9087cdd7aaaf031b" }, "start_build_capture": { "input compact": "fb22413ed630198e", - "input full": "1672c5f59bcccbed", - "output compact": "ec7acbbccb1d199b", - "output full": "bfce0e1a8518dfdc" + "output compact": "ec7acbbccb1d199b" }, "start_build_computer": { "input compact": "f1976a463d3143e7", - "input full": "10cbe3174fe9a542", - "output compact": "e4fa205faaf0e82e", - "output full": "3d7a08bf5d14f8d1" + "output compact": "e4fa205faaf0e82e" }, "start_frida_session": { "input compact": "849dbbcd225a233a", - "input full": "b960b01d88e54588", - "output compact": "2565468480ec3144", - "output full": "6dbf34b783fe654b" + "output compact": "2565468480ec3144" }, "start_gdb_session": { "input compact": "21c503e369a05f57", - "input full": "7f12f941cd880d81", - "output compact": "a14bf3365e44977e", - "output full": "9c295f8d31b186bc" + "output compact": "a14bf3365e44977e" }, "start_rizin_debug_session": { "input compact": "ab6163e1a603d534", - "input full": "408e848e06f64118", - "output compact": "8679774ab9ec07a1", - "output full": "b37765d288071337" + "output compact": "8679774ab9ec07a1" }, "stop_adb_app": { "input compact": "2d6b5bb69d217de1", - "input full": "070694a1c822fc8c", - "output compact": "9087cdd7aaaf031b", - "output full": "5f8c3fb8b378299a" + "output compact": "9087cdd7aaaf031b" }, "stop_build_capture": { "input compact": "3b3f0b6ed9a02890", - "input full": "9adc1d883ed94c75", - "output compact": "6ad23bedc3a584a2", - "output full": "c0418b4d2e05e883" + "output compact": "6ad23bedc3a584a2" }, "trace_android_references": { "input compact": "bed8f76edd347cb6", - "input full": "b6378abd45badfbc", - "output compact": "9087cdd7aaaf031b", - "output full": "3785f1ee85a22a75" + "output compact": "9087cdd7aaaf031b" }, "trace_application_feature": { "input compact": "6da6ada4587d6b1e", - "input full": "30c955768a4061fc", - "output compact": "9087cdd7aaaf031b", - "output full": "10f538ed7befb508" + "output compact": "9087cdd7aaaf031b" }, "trace_call_path": { "input compact": "eb86d04cd6c8d08e", - "input full": "70a468c2d2a8a351", - "output compact": "9087cdd7aaaf031b", - "output full": "9de3cae3d8c6d978" + "output compact": "9087cdd7aaaf031b" }, "trace_dylib_resolution": { "input compact": "fba037864c570ea9", - "input full": "45f2af8917ab7595", - "output compact": "9087cdd7aaaf031b", - "output full": "de680209d061dfd9" + "output compact": "9087cdd7aaaf031b" }, "trace_feature": { "input compact": "6e6c01e5a00f75ec", - "input full": "e6da33780049b8c7", - "output compact": "9087cdd7aaaf031b", - "output full": "7eedc13b0f055d59" + "output compact": "9087cdd7aaaf031b" }, "trace_javascript_semantics": { "input compact": "86bc47efe9065d21", - "input full": "941e1bdc205d6cb3", - "output compact": "9087cdd7aaaf031b", - "output full": "336ce73fb6d5a84c" + "output compact": "9087cdd7aaaf031b" }, "trace_native_ui_action": { "input compact": "5f7e5def746a8c3d", - "input full": "258357b193ca6d4e", - "output compact": "9087cdd7aaaf031b", - "output full": "2e0a96227678cd8f" + "output compact": "9087cdd7aaaf031b" }, "trace_native_values": { "input compact": "2d2afa5eefb4be64", - "input full": "7c4c10ea873d67ba", - "output compact": "9087cdd7aaaf031b", - "output full": "f6db546cf92a3c41" + "output compact": "9087cdd7aaaf031b" }, "trace_web_module_imports": { "input compact": "fac6a749ccacd74f", - "input full": "0f1bc7937a81be35", - "output compact": "9087cdd7aaaf031b", - "output full": "43f60c8ee46f6cbd" + "output compact": "9087cdd7aaaf031b" }, "trace_web_source_location": { "input compact": "ae3268db6e89a16b", - "input full": "dc4353623267beba", - "output compact": "9087cdd7aaaf031b", - "output full": "986e34494d9a6ae2" + "output compact": "9087cdd7aaaf031b" }, "uninstall_adb_package": { "input compact": "2d6b5bb69d217de1", - "input full": "6a2815a86f5dcd95", - "output compact": "9087cdd7aaaf031b", - "output full": "5f8c3fb8b378299a" + "output compact": "9087cdd7aaaf031b" }, "unload_frida_script": { "input compact": "2b7c1fe308fe6475", - "input full": "4e1590464e35482a", - "output compact": "433541d12d2cce0f", - "output full": "bb9f2449493fc50b" + "output compact": "433541d12d2cce0f" }, "unset_bookmark": { "input compact": "c9d8a29b5ea9eaf2", - "input full": "0ab8a9a24f970cbf", - "output compact": "3c3f5b2578e1a632", - "output full": "60ca06761be3cddf" + "output compact": "3c3f5b2578e1a632" }, "update_analysis_settings": { - "input compact": "a16933f98bedfc3a", - "input full": "6b1deb63e4e36def", - "output compact": "39446aa1012006cc", - "output full": "5270e4654d984a2b" + "input compact": "2144cc02dc3d9c72", + "output compact": "39446aa1012006cc" }, "update_unknown": { "input compact": "9c58fa7f6dfaccdb", - "input full": "2407902ba1c82558", - "output compact": "9cda19dfa1310afa", - "output full": "447bbb45a3d5dcdb" + "output compact": "9cda19dfa1310afa" }, "verify_managed_native_boundaries": { "input compact": "6b357d640e0a98f5", - "input full": "a3fb480008d0f2b0", - "output compact": "9087cdd7aaaf031b", - "output full": "34d3bba9d7e0d642" + "output compact": "9087cdd7aaaf031b" }, "verify_reconstruction": { "input compact": "8b4cfb9f381ccb12", - "input full": "b31389c983ee1d64", - "output compact": "9087cdd7aaaf031b", - "output full": "e78a6a9596829597" + "output compact": "9087cdd7aaaf031b" }, "verify_unknown_resolution": { "input compact": "09378f40aaf4fa55", - "input full": "af205e3e45797ae3", - "output compact": "9cda19dfa1310afa", - "output full": "0d598a592d2d90e8" + "output compact": "9cda19dfa1310afa" }, "xrefs": { "input compact": "f1b0a54d8c702be5", - "input full": "dbc2a763429ebf8f", - "output compact": "49662c31e39ea6a1", - "output full": "cd9580c4d8d853b3" + "output compact": "49662c31e39ea6a1" } } } diff --git a/scripts/verify/android/external-invokes.mjs b/scripts/verify/android/external-invokes.mjs index e05394a87e..e15712b4b8 100644 --- a/scripts/verify/android/external-invokes.mjs +++ b/scripts/verify/android/external-invokes.mjs @@ -5,10 +5,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { parseEvidence } from "../../../dist/domain/evidence.js"; import { artifactMcpCall } from "../../lib/artifact-e2e.mjs"; import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs"; -import { - INLINE_EVIDENCE_MCP_ENVIRONMENT, - schemaVerifierEnvironment, -} from "../../lib/mcp-verification-environment.mjs"; +import { INLINE_EVIDENCE_MCP_ENVIRONMENT } from "../../lib/mcp-verification-environment.mjs"; import { requireMcpOutput, requireMcpToolError, @@ -107,10 +104,10 @@ const verifyApk = async (options, path, dexEntry) => { command: process.execPath, args: [options.entrypoint, "mcp"], cwd: options.repository, - env: schemaVerifierEnvironment({ + env: { ...options.environment, ...INLINE_EVIDENCE_MCP_ENVIRONMENT, - }), + }, stderr: "pipe", }); const client = new Client({ @@ -256,10 +253,10 @@ const verifySiteBudget = async (options, path, control) => { command: process.execPath, args: [options.entrypoint, "mcp"], cwd: options.repository, - env: schemaVerifierEnvironment({ + env: { ...options.environment, ...INLINE_EVIDENCE_MCP_ENVIRONMENT, - }), + }, stderr: "pipe", }); const client = new Client({ name: "rea-external-site-budget", version: "1" }); diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index bfff3749d4..5785d4ec13 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -155,6 +155,10 @@ "src/pcap/PcapDnsWire.ts": 5, "src/v8/V8CodeCacheParser.ts": 1 }, + "raw-json-schema-meta": { + "src/cli/jsonInputArgument.ts": 2, + "src/cli/utilityCommands.ts": 1 + }, "range-error-interpretation": { "src/application/JsonInputFile.ts": 1, "src/artifacts/nodeSea/NodeSeaBlob.ts": 1, diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index 837d18dc4d..4d648bf311 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -535,6 +535,20 @@ checkCounted( [], ); +// Raw JSON Schema keywords and `x-rea-*` markers in `.meta()` bypass the +// canonical Zod checks and reach every JSON Schema projection, including the +// advertised one. Presentation facts are typed annotations +// (src/domain/schemaAnnotations); constraints belong in Zod checks, or in a +// src/domain/schemaConstraints helper that adds the check and its keyword +// together. +checkCounted( + "raw JSON Schema keyword or x-rea marker in Zod .meta() (use a typed annotation from src/domain/schemaAnnotations, a Zod check, or a src/domain/schemaConstraints helper)", + "raw-json-schema-meta", + files, + /\.meta\(\s*\{\s*"?(?:x-rea-[\w-]+|\$ref|not|allOf|anyOf|oneOf|if|then|else|dependentRequired|dependentSchemas|dependencies|uniqueItems|(?:min|max)(?:Length|Items|Properties|imum)|exclusive(?:Min|Max)imum|multipleOf|type|const|enum|format|pattern|patternProperties|properties|required|additionalProperties|items|prefixItems|contains|content(?:MediaType|Encoding|Schema)|propertyNames)"?\s*:/gu, + ["src/domain/schemaConstraints.ts"], +); + // A RangeError can mean a stack overflow, a host string or buffer limit, a // raw read or write past a buffer, or whatever its thrower intended. Only // known host limits are classified; parsers throw a ParseFailureError. diff --git a/scripts/verify/electron/tool-contracts.mjs b/scripts/verify/electron/tool-contracts.mjs index 0fc3646ce8..e8717aee7a 100644 --- a/scripts/verify/electron/tool-contracts.mjs +++ b/scripts/verify/electron/tool-contracts.mjs @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../lib/canonical-output-schema.mjs"; import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs"; import { VerifierFixture, @@ -5,7 +6,6 @@ import { withVerifierFixture, } from "../../lib/verifier-fixture.mjs"; import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs"; -import { schemaVerifierEnvironment } from "../../lib/mcp-verification-environment.mjs"; import assert from "node:assert/strict"; import { execFile, spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; @@ -83,7 +83,7 @@ bridge.exposeInMainWorld("fixture", { command: process.execPath, args: [entrypoint, "mcp"], // Results are validated against the listed complete output contracts. - env: schemaVerifierEnvironment({ + env: { ...Object.fromEntries( ["DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY"].flatMap((name) => process.env[name] === undefined ? [] : [[name, process.env[name]]], @@ -92,7 +92,7 @@ bridge.exposeInMainWorld("fixture", { PATH: process.env.PATH, REA_LOG_LEVEL: "silent", HOPPER_LAUNCHER_PATH: "/rea-unconfigured/hopper", - }), + }, stderr: "pipe", }); const client = new Client({ name: "electron-real-e2e", version: "1" }); @@ -146,10 +146,10 @@ bridge.exposeInMainWorld("fixture", { ajv.compile(tool.inputSchema); return [ tool.name, - tool.outputSchema - ? (tool.outputSchema.$id && - ajv.getSchema(tool.outputSchema.$id)) || - ajv.compile(tool.outputSchema) + canonicalOutputSchema(tool.name) + ? (canonicalOutputSchema(tool.name).$id && + ajv.getSchema(canonicalOutputSchema(tool.name).$id)) || + ajv.compile(canonicalOutputSchema(tool.name)) : undefined, ]; }), diff --git a/src/application/DirectAnalysisStatus.ts b/src/application/DirectAnalysisStatus.ts index ce90d6cbd6..d89e180a2a 100644 --- a/src/application/DirectAnalysisStatus.ts +++ b/src/application/DirectAnalysisStatus.ts @@ -9,7 +9,7 @@ import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js"; import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js"; import { catalogIdentityForToolFamilies } from "../catalogIdentity.js"; import { progressiveCatalogIdentity } from "../catalogDiscovery.js"; -import { compactPresentationForMcpInputSchemaProfile } from "../config/mcpInputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../config/mcpInputSchemaProfile.js"; import { resolveMcpOutputSchemaProfile } from "../config/mcpOutputSchemaProfile.js"; import { directAnalysisCleanupFailure, @@ -34,9 +34,7 @@ export const runSessionStatus = async ( async () => { const status = jsonObjectSchema.parse(await session.status()); const catalog = catalogIdentityForToolFamilies( - compactPresentationForMcpInputSchemaProfile( - config.value.mcpInputSchemaProfile, - ), + COMPACT_INPUT_SCHEMA_PRESENTATION, resolveMcpOutputSchemaProfile(config.value.mcpOutputSchemaProfile), config.value.mcpToolFamilies === undefined ? undefined diff --git a/src/catalogIdentity.ts b/src/catalogIdentity.ts index 3a76481ed2..2cab268a88 100644 --- a/src/catalogIdentity.ts +++ b/src/catalogIdentity.ts @@ -19,21 +19,13 @@ import { presentAdvertisedInput, type CompactInputSchemaPresentation, } from "./contracts/compactInputSchema.js"; +import { presentAdvertisedOutputSchema } from "./contracts/compactOutputSchema.js"; import { - presentAdvertisedOutputSchema, - type CompactOutputSchemaPresentation, -} from "./contracts/compactOutputSchema.js"; -import { - COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES, - compactPresentationForMcpOutputSchemaProfile, + COMPACT_OUTPUT_SCHEMA_PRESENTATION, resolveMcpOutputSchemaProfile, type McpOutputSchemaProfile, } from "./config/mcpOutputSchemaProfile.js"; -import { - COMPACT_INPUT_SCHEMA_BUDGET_BYTES, - compactPresentationForMcpInputSchemaProfile, - type McpInputSchemaProfile, -} from "./config/mcpInputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "./config/mcpInputSchemaProfile.js"; import { RECORDED_CATALOG_DIGEST_PROFILES } from "./recordedCatalogDigests.js"; /** @@ -68,27 +60,29 @@ const presentedInput = ( }; }; +/** Advertised input for every tool; output stays canonical unless bounded. */ const presentToolCatalog = ( catalog: ReturnType, - presentation: CompactInputSchemaPresentation | undefined, - outputPresentation: CompactOutputSchemaPresentation | undefined = undefined, + presentation: CompactInputSchemaPresentation, ) => - presentation === undefined && outputPresentation === undefined - ? catalog - : catalog.map((tool) => ({ + catalog.map((tool) => ({ + ...tool, + ...presentedInput(tool.description, tool.input_schema, presentation), + })); + +const presentOutputSchemas = ( + catalog: ReturnType, + outputSchemaProfile: McpOutputSchemaProfile, +) => + outputSchemaProfile === "compact" + ? catalog.map((tool) => ({ ...tool, - ...(presentation === undefined - ? {} - : presentedInput(tool.description, tool.input_schema, presentation)), - ...(outputPresentation === undefined - ? {} - : { - output_schema: presentAdvertisedOutputSchema( - tool.output_schema, - outputPresentation, - ), - }), - })); + output_schema: presentAdvertisedOutputSchema( + tool.output_schema, + COMPACT_OUTPUT_SCHEMA_PRESENTATION, + ), + })) + : catalog; const createPromptCatalog = (contracts: readonly PromptContract[]) => contracts @@ -117,13 +111,13 @@ const digest = (value: unknown): string => { }; const digestCatalogs = ( - catalog: ReturnType, + catalog: ReturnType, prompts: ReturnType, outputSchemaProfile: McpOutputSchemaProfile, ) => { const tools = { output_schema_profile: outputSchemaProfile, - catalog, + catalog: presentOutputSchemas(catalog, outputSchemaProfile), }; return { tools_sha256: digest(tools), @@ -139,19 +133,20 @@ const digestCatalogs = ( /** * Catalog digests over the given tool contracts. Exported so tests can prove * which contract fields the identity covers; production uses - * {@link FULL_CATALOG_IDENTITY}. + * {@link DEFAULT_CATALOG_IDENTITY}. */ export const createCatalogDigests = ( contracts: readonly ToolContract[] = TOOL_CONTRACTS, promptContracts: readonly PromptContract[] = PROMPT_CONTRACTS, - compactPresentation: CompactInputSchemaPresentation | undefined = undefined, - outputSchemaProfile: McpOutputSchemaProfile = "full", + compactPresentation: CompactInputSchemaPresentation = COMPACT_INPUT_SCHEMA_PRESENTATION, + outputSchemaProfile: McpOutputSchemaProfile = resolveMcpOutputSchemaProfile( + undefined, + ), ) => digestCatalogs( presentToolCatalog( createToolCatalog(sortByName(contracts)), compactPresentation, - compactPresentationForMcpOutputSchemaProfile(outputSchemaProfile), ), createPromptCatalog(promptContracts), outputSchemaProfile, @@ -159,35 +154,23 @@ export const createCatalogDigests = ( export type CatalogDigests = ReturnType; export type CatalogDigestProfiles = Readonly< - Record< - McpInputSchemaProfile, - Readonly> - > + Record >; -/** Build-time identities share schema conversion across the six supported profiles. */ +/** Build-time identities share schema conversion across both output profiles. */ export const createCatalogDigestProfiles = ( contracts: readonly ToolContract[] = TOOL_CONTRACTS, promptContracts: readonly PromptContract[] = PROMPT_CONTRACTS, ): CatalogDigestProfiles => { - const full = createToolCatalog(sortByName(contracts)); - const compact = presentToolCatalog(full, { - budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, - }); + const catalog = presentToolCatalog( + createToolCatalog(sortByName(contracts)), + COMPACT_INPUT_SCHEMA_PRESENTATION, + ); const prompts = createPromptCatalog(promptContracts); - const compactOutput: CompactOutputSchemaPresentation = { - budgetBytes: COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES, - }; - const outputProfiles = (catalog: typeof full) => ({ - full: digestCatalogs(catalog, prompts, "full"), - compact: digestCatalogs( - presentToolCatalog(catalog, undefined, compactOutput), - prompts, - "compact", - ), + return { off: digestCatalogs(catalog, prompts, "off"), - }); - return { full: outputProfiles(full), compact: outputProfiles(compact) }; + compact: digestCatalogs(catalog, prompts, "compact"), + }; }; /** One catalog identity: advertised presentation, contract and prompt scope, output profile. */ @@ -200,31 +183,22 @@ export interface CatalogIdentityOptions { export const createCatalogIdentity = (options: CatalogIdentityOptions = {}) => { const { - compactPresentation, + compactPresentation = COMPACT_INPUT_SCHEMA_PRESENTATION, contracts = TOOL_CONTRACTS, - outputSchemaProfile = "full", + outputSchemaProfile = resolveMcpOutputSchemaProfile(undefined), promptContracts = PROMPT_CONTRACTS, } = options; - const selectedPresentation = - compactPresentation === undefined - ? undefined - : { budgetBytes: compactPresentation.budgetBytes }; - // Only the exact built-in presentations have build-time identities. Source - // runs and custom budgets still compute from their selected schemas lazily. - const inputProfile = - selectedPresentation === undefined - ? "full" - : selectedPresentation.budgetBytes === COMPACT_INPUT_SCHEMA_BUDGET_BYTES - ? "compact" - : undefined; - // Recorded digests describe the complete default catalog only; a scoped tool - // or prompt set always computes from its own inputs. + const selectedPresentation = { budgetBytes: compactPresentation.budgetBytes }; + // Recorded digests describe the complete catalog at the built-in budget + // only; a scoped tool or prompt set, or a custom budget, computes from its + // own inputs on first use. let catalogDigests: CatalogDigests | undefined = - inputProfile === undefined || + selectedPresentation.budgetBytes !== + COMPACT_INPUT_SCHEMA_PRESENTATION.budgetBytes || contracts !== TOOL_CONTRACTS || promptContracts !== PROMPT_CONTRACTS ? undefined - : RECORDED_CATALOG_DIGEST_PROFILES?.[inputProfile][outputSchemaProfile]; + : RECORDED_CATALOG_DIGEST_PROFILES?.[outputSchemaProfile]; return { counts: { cli_commands: CLI_COMMAND_NAMES.length, @@ -242,17 +216,8 @@ export const createCatalogIdentity = (options: CatalogIdentityOptions = {}) => { } as const; }; -/** - * Full input/output advertisement identity. Digests are recorded at build time - * or computed on first use; this is independent of the configured default. - */ -export const FULL_CATALOG_IDENTITY = createCatalogIdentity(); - -/** Default MCP advertisement; explicit full/custom selections have their own identity. */ -export const DEFAULT_CATALOG_IDENTITY = createCatalogIdentity({ - compactPresentation: compactPresentationForMcpInputSchemaProfile(undefined), - outputSchemaProfile: resolveMcpOutputSchemaProfile(undefined), -}); +/** Default MCP advertisement: compact input, output schemas omitted. */ +export const DEFAULT_CATALOG_IDENTITY = createCatalogIdentity(); /** * Identity whose digests cover exactly the contracts a registration advertises. @@ -260,19 +225,17 @@ export const DEFAULT_CATALOG_IDENTITY = createCatalogIdentity({ * paths derive the same value from the same scope. */ export const catalogIdentityForToolFamilies = ( - compactPresentation: CompactInputSchemaPresentation | undefined, + compactPresentation: CompactInputSchemaPresentation, outputSchemaProfile: McpOutputSchemaProfile, advertisedToolFamilies: ReadonlySet | undefined, ): ReturnType => - compactPresentation === undefined && - outputSchemaProfile === "full" && + compactPresentation.budgetBytes === + COMPACT_INPUT_SCHEMA_PRESENTATION.budgetBytes && + outputSchemaProfile === resolveMcpOutputSchemaProfile(undefined) && advertisedToolFamilies === undefined - ? FULL_CATALOG_IDENTITY + ? DEFAULT_CATALOG_IDENTITY : createCatalogIdentity({ - compactPresentation: - compactPresentation === undefined - ? undefined - : { budgetBytes: compactPresentation.budgetBytes }, + compactPresentation: { budgetBytes: compactPresentation.budgetBytes }, contracts: contractsForToolFamilies(advertisedToolFamilies), outputSchemaProfile, promptContracts: promptsForToolFamilies(advertisedToolFamilies), diff --git a/src/config/mcpInputSchemaProfile.ts b/src/config/mcpInputSchemaProfile.ts index 9e156b1909..1e2a2e114f 100644 --- a/src/config/mcpInputSchemaProfile.ts +++ b/src/config/mcpInputSchemaProfile.ts @@ -1,30 +1,32 @@ import { z } from "zod"; import { ADVERTISED_INPUT_SCHEMA_MAX_BYTES } from "../contracts/advertisedDialect.js"; +import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { err, ok, type Result } from "../domain/result.js"; -/** The compact profile fits the portable dialect's per-tool size cap. */ +/** The compact presentation fits the portable dialect's per-tool size cap. */ export const COMPACT_INPUT_SCHEMA_BUDGET_BYTES = ADVERTISED_INPUT_SCHEMA_MAX_BYTES; -export const mcpInputSchemaProfileSchema = z.enum(["full", "compact"]); - -/** Resolve the selected advertisement at the configuration boundary. */ -export const resolveMcpInputSchemaProfile = ( - profile: McpInputSchemaProfile | undefined, -): McpInputSchemaProfile => profile ?? "compact"; - -/** Select the default compact advertisement, preserving an explicit full choice. */ -export const compactPresentationForMcpInputSchemaProfile = ( - profile: "full" | "compact" | undefined, -): { readonly budgetBytes: number } | undefined => - resolveMcpInputSchemaProfile(profile) === "full" - ? undefined - : { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES }; +/** + * The one advertised input presentation. Every server, identity and recorded + * catalog presents input schemas through it; canonical validators still + * check every call. + */ +export const COMPACT_INPUT_SCHEMA_PRESENTATION: CompactInputSchemaPresentation = + Object.freeze({ budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES }); const profileConstraint = - 'REA_MCP_INPUT_SCHEMA_PROFILE must be "compact" (default) or "full". The compact profile advertises bounded input schemas with fewer nested annotations; full preserves complete advertised input guidance.'; + 'REA_MCP_INPUT_SCHEMA_PROFILE accepts only "compact", the default. The "full" input profile was removed: remove the setting from the client registration, or set it to "compact". Canonical server-side validation still checks every call.'; + +/** + * Only `compact` remains so registrations that spell the default keep + * starting; any other value, including the removed `full`, fails closed. + */ +export const mcpInputSchemaProfileSchema = z.enum(["compact"], { + error: profileConstraint, +}); export type McpInputSchemaProfile = z.infer; diff --git a/src/config/mcpOutputSchemaProfile.ts b/src/config/mcpOutputSchemaProfile.ts index bb7a8a3875..dd76479084 100644 --- a/src/config/mcpOutputSchemaProfile.ts +++ b/src/config/mcpOutputSchemaProfile.ts @@ -15,8 +15,13 @@ import { err, ok, type Result } from "../domain/result.js"; */ export const COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES = 4 * 1024; -/** Select whether tools/list carries the large output contracts. */ -export const mcpOutputSchemaProfileSchema = z.enum(["full", "compact", "off"]); +const profileConstraint = + 'REA_MCP_OUTPUT_SCHEMA_PROFILE must be "off" (default) or "compact". The off profile omits output schemas; compact advertises every output schema within a 4,096-byte budget. The "full" output profile was removed: remove the setting or select "compact". Both keep canonical server-side validation and complete structured content.'; + +/** Select whether tools/list carries bounded output contracts. */ +export const mcpOutputSchemaProfileSchema = z.enum(["off", "compact"], { + error: profileConstraint, +}); export type McpOutputSchemaProfile = z.infer< typeof mcpOutputSchemaProfileSchema @@ -26,22 +31,16 @@ export type McpOutputSchemaProfile = z.infer< * Resolve the selected advertisement at the configuration boundary. No model * API reads an output schema; MCP client validators re-check what REA already * validates, and complete output schemas were about 92% of `tools/list`. They - * are therefore omitted unless a caller selects `compact` or `full`. + * are therefore omitted unless a caller selects `compact`. */ export const resolveMcpOutputSchemaProfile = ( profile: McpOutputSchemaProfile | undefined, ): McpOutputSchemaProfile => profile ?? "off"; -/** Resolve the compact output advertisement, preserving every other choice. */ -export const compactPresentationForMcpOutputSchemaProfile = ( - profile: McpOutputSchemaProfile | undefined, -): { readonly budgetBytes: number } | undefined => - profile === "compact" - ? { budgetBytes: COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES } - : undefined; - -const profileConstraint = - 'REA_MCP_OUTPUT_SCHEMA_PROFILE must be "off" (default), "compact", or "full". The off profile omits output schemas; compact advertises every output schema within a 4,096-byte budget; full advertises complete output schemas. All keep canonical server-side validation and complete structured content.'; +/** The one bounded output advertisement, selected by `compact`. */ +export const COMPACT_OUTPUT_SCHEMA_PRESENTATION: { + readonly budgetBytes: number; +} = Object.freeze({ budgetBytes: COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES }); /** Parse the output schema advertisement selection at server composition. */ export const parseMcpOutputSchemaProfile = ( diff --git a/src/config/parseConfig.ts b/src/config/parseConfig.ts index 5e7b148ca9..3b2df5a693 100644 --- a/src/config/parseConfig.ts +++ b/src/config/parseConfig.ts @@ -152,9 +152,6 @@ export const parseConfig = ( ...(env.REA_MCP_MAX_INPUT_ELEMENTS === undefined ? {} : { mcpMaxInputElements: env.REA_MCP_MAX_INPUT_ELEMENTS }), - ...(env.REA_MCP_INPUT_SCHEMA_PROFILE === undefined - ? {} - : { mcpInputSchemaProfile: env.REA_MCP_INPUT_SCHEMA_PROFILE }), ...(env.REA_MCP_OUTPUT_SCHEMA_PROFILE === undefined ? {} : { mcpOutputSchemaProfile: env.REA_MCP_OUTPUT_SCHEMA_PROFILE }), diff --git a/src/config/types.ts b/src/config/types.ts index dc67dca197..979e01010a 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -5,7 +5,6 @@ import type { ReferenceSourcePolicy } from "../domain/referenceSourcePolicy.js"; import type { AnalysisProviderSelector } from "../domain/providerSelection.js"; import type { GhidraLanguageOverride } from "./ghidraLanguageOverride.js"; -import type { McpInputSchemaProfile } from "./mcpInputSchemaProfile.js"; import type { McpOutputSchemaProfile } from "./mcpOutputSchemaProfile.js"; import type { McpToolCatalogProfile } from "./mcpToolCatalogProfile.js"; import type { BridgeMaxRequestBytes } from "./bridgeMaxRequestBytes.js"; @@ -20,7 +19,6 @@ export interface AppConfig { readonly mcpToolFamilies?: readonly McpToolFamilies[number][]; readonly mcpMaxResponseBytes?: number; readonly mcpMaxInputElements?: number; - readonly mcpInputSchemaProfile?: McpInputSchemaProfile; readonly mcpOutputSchemaProfile?: McpOutputSchemaProfile; readonly mcpToolCatalogProfile?: McpToolCatalogProfile; readonly analysisProvider: AnalysisProviderSelector; diff --git a/src/contracts/advertisedDialectBaseline.ts b/src/contracts/advertisedDialectBaseline.ts deleted file mode 100644 index 66a63940f4..0000000000 --- a/src/contracts/advertisedDialectBaseline.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Advertised constructs outside the portable dialect that the presentation -// does not yet project away, counted per profile over every distinct tool -// definition the server can list (all catalog and output profiles). Counts may -// only shrink: the catalog test requires an exact match, so removing a -// construct lowers its count here, and any key not listed is enforced at zero. -// Two exceptions grow them. Each runtime analysis setting adds a branch to -// update_analysis_settings with two `const` members (`setting`, `action`), and -// the full profile also counts the Evidence bundle's required `evidence_format` -// literal (src/domain/evidenceFormat.ts) in the inputs that carry a whole -// bundle. A fix that restores a contract declaration the presentation had lost -// (for example a borrowed Evidence union advertised as an unconstrained value) -// counts its constructs again, and the PR names the restored declaration. A -// new contract or presentation construct never raises a count. - -// Every advertised pattern, input and output, is enforced at zero: the -// presentation re-emits each one in the portable grammar. - -/** Input-side counts, keyed by `REA_MCP_INPUT_SCHEMA_PROFILE`. */ -export const ADVERTISED_INPUT_DIALECT_BASELINE: Readonly< - Record<"compact" | "full", Readonly>> -> = { - // The compact projection (`advertisedDialectProjection.ts`) leaves nothing - // outside the dialect. - compact: {}, - full: { - "input any-of fewer-than-two-branches": 176, - "input any-of untyped-branch": 142, - "input boolean-schema": 1, - "input bound maxItems": 2, - "input bound maxLength": 30, - "input description-length": 1, - "input enum not-on-string-type": 1, - "input input-bytes": 23, - "input input-depth": 33, - "input keyword $schema": 256, - "input keyword allOf": 99, - "input keyword const": 1917, - "input keyword default": 803, - "input keyword else": 6, - "input keyword examples": 253, - "input keyword exclusiveMinimum": 335, - "input keyword format": 22, - "input keyword if": 7, - "input keyword minProperties": 12, - "input keyword not": 10, - "input keyword oneOf": 225, - "input keyword prefixItems": 267, - "input keyword propertyNames": 55, - "input keyword then": 7, - "input keyword uniqueItems": 5, - "input type type-list": 53, - "input type type:null": 6, - "input type untyped": 1256, - }, -}; diff --git a/src/contracts/artifactToolContracts.ts b/src/contracts/artifactToolContracts.ts index 9584a781ef..f669d21b58 100644 --- a/src/contracts/artifactToolContracts.ts +++ b/src/contracts/artifactToolContracts.ts @@ -7,6 +7,7 @@ import type { ToolContract } from "./toolContractTypes.js"; import { artifactOutputSchemas } from "./toolOutputSchemaGroups.js"; import { artifactIntegrityPolicySchema } from "../domain/artifactIntegrityPolicy.js"; import { jsonValueSchema } from "../domain/jsonValue.js"; +import { distinctItems } from "../domain/schemaConstraints.js"; import { toolContractMetadata } from "./toolEffects.js"; import { appleAssetCatalogInputSchema } from "../domain/apple/appleAssetCatalog.js"; import { dylibResolutionInputSchema } from "../domain/apple/dylibResolution.js"; @@ -44,14 +45,10 @@ export const artifactExtractionInputSchema = z.strictObject({ .describe( "Absolute destination path on the REA host. It must not exist and its parent must exist. Omit to use a fresh temporary directory. Successful output persists after the call.", ), - occurrence_ids: z - .array(artifactOccurrenceSchema.shape.occurrence_id) - .min(1) - .refine( - (ids) => new Set(ids).size === ids.length, - "Occurrence IDs must be unique", - ) - .meta({ uniqueItems: true }) + occurrence_ids: distinctItems( + z.array(artifactOccurrenceSchema.shape.occurrence_id).min(1), + "Occurrence IDs must be unique", + ) .exactOptional() .describe( "Exact regular-file or slice occurrence IDs from the current inventory. Omit to extract all regular entries exposed by the active reader. Directories, links, the root and members inside nested archives cannot be selected.", diff --git a/src/contracts/browserToolContracts.ts b/src/contracts/browserToolContracts.ts index 4152cd6c01..8c02fa5390 100644 --- a/src/contracts/browserToolContracts.ts +++ b/src/contracts/browserToolContracts.ts @@ -42,15 +42,17 @@ import { } from "../domain/webScreenshot.js"; import { toolContractMetadata } from "./toolEffects.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; +import { presentRootDiscriminator } from "../domain/schemaAnnotations.js"; const listOutputSchema = evidenceResultOf(browserTargetListSchema); const inspectionOutputSchema = evidenceResultOf(webPageInspectionSchema); -export const analyzeWebBundleRequestSchema = z - .discriminatedUnion("detail", [ +export const analyzeWebBundleRequestSchema = presentRootDiscriminator( + z.discriminatedUnion("detail", [ analyzeWebBundleInputSchema.extend(javaScriptRoutesCompleteDeliveryShape), analyzeWebBundleInputSchema.extend(webRoutesSummaryDeliveryShape), - ]) - .meta({ "x-rea-root-discriminator": "detail" }); + ]), + "detail", +); const bundleOutputSchema = evidenceResultOf( z.union([ webBundleAnalysisSchema, diff --git a/src/contracts/bytecodeDeliveryContracts.test.ts b/src/contracts/bytecodeDeliveryContracts.test.ts index 6b91f4e753..9c0713faf8 100644 --- a/src/contracts/bytecodeDeliveryContracts.test.ts +++ b/src/contracts/bytecodeDeliveryContracts.test.ts @@ -80,8 +80,10 @@ it.each( : new Ajv({ strict: false, validateFormats: false }); expect(ajv.validateSchema(projected)).toBe(true); expect(projected).toMatchObject({ type: "object" }); - for (const key of ["anyOf", "oneOf", "allOf", "x-rea-root-discriminator"]) + for (const key of ["anyOf", "oneOf", "allOf"]) expect(projected).not.toHaveProperty(key); + // The typed root discriminator selects the alternative. + expect(projected).toMatchObject({ if: { required: ["detail"] } }); const validate = ajv.compile(projected); for (const [delivery, valid] of [ [{}, true], diff --git a/src/contracts/compactInputSchema.ts b/src/contracts/compactInputSchema.ts index 065e02255a..70b978c290 100644 --- a/src/contracts/compactInputSchema.ts +++ b/src/contracts/compactInputSchema.ts @@ -10,7 +10,7 @@ import { stripAnnotations, } from "./compactJsonSchema.js"; -// The compact input profile: every advertised input schema is self-contained +// The compact input presentation: every advertised input schema is self-contained // (clients such as Kimi expand references before sending tools to Moonshot), // inside the per-tool byte budget, and in the portable advertised dialect. // Canonical server-side validation remains unchanged. @@ -112,9 +112,8 @@ const compacted = new WeakMap>(); /** The advertised input for the selected presentation, with its guidance. */ export const presentAdvertisedInput = ( schema: Record, - compactPresentation: CompactInputSchemaPresentation | undefined, + compactPresentation: CompactInputSchemaPresentation, ): Pick => { - if (compactPresentation === undefined) return { schema, guidance: [] }; const { budgetBytes } = compactPresentation; let byBudget = compacted.get(schema); if (byBudget === undefined) compacted.set(schema, (byBudget = new Map())); @@ -130,7 +129,7 @@ export const presentAdvertisedInput = ( /** Return the JSON schema clients receive for the selected input presentation. */ export const presentAdvertisedInputSchema = ( schema: Record, - compactPresentation: CompactInputSchemaPresentation | undefined, + compactPresentation: CompactInputSchemaPresentation, ): Record => presentAdvertisedInput(schema, compactPresentation).schema; diff --git a/src/contracts/erlang/beamToolContracts.ts b/src/contracts/erlang/beamToolContracts.ts index e96778ba93..bc3884d54a 100644 --- a/src/contracts/erlang/beamToolContracts.ts +++ b/src/contracts/erlang/beamToolContracts.ts @@ -14,14 +14,16 @@ import { erlangBeamCompleteDeliveryShape, erlangBeamSummaryDeliveryShape, } from "../../domain/analysisView/erlangBeamView.js"; +import { presentRootDiscriminator } from "../../domain/schemaAnnotations.js"; /** Delivery selection does not change provider inputs or complete Evidence identity. */ -export const inspectErlangBeamRequestSchema = z - .discriminatedUnion("detail", [ +export const inspectErlangBeamRequestSchema = presentRootDiscriminator( + z.discriminatedUnion("detail", [ inspectErlangBeamInputSchema.extend(erlangBeamCompleteDeliveryShape), inspectErlangBeamInputSchema.extend(erlangBeamSummaryDeliveryShape), - ]) - .meta({ "x-rea-root-discriminator": "detail" }); + ]), + "detail", +); /** Static BEAM inspection is independent of active native-analysis providers. */ export const ERLANG_BEAM_TOOL_CONTRACTS = [ diff --git a/src/contracts/guidedWorkflowSchemas.ts b/src/contracts/guidedWorkflowSchemas.ts index 6ac064225c..329c439184 100644 --- a/src/contracts/guidedWorkflowSchemas.ts +++ b/src/contracts/guidedWorkflowSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { presentObjectRoot } from "../domain/schemaAnnotations.js"; + /** Transport-independent request grammar; rendering never executes a workflow. */ export const guidedWorkflowsInputSchema = z.discriminatedUnion("action", [ z.strictObject({ action: z.literal("list") }), @@ -28,9 +30,9 @@ const guidedWorkflowItemSchema = z.strictObject({ }), ), }); -// Every alternative is an object; declare that root for older MCP codecs too. -export const guidedWorkflowsOutputSchema = z - .discriminatedUnion("action", [ +// Every alternative is an object; the advertised root says so for older MCP codecs. +export const guidedWorkflowsOutputSchema = presentObjectRoot( + z.discriminatedUnion("action", [ z.strictObject({ action: z.literal("list"), workflows: z.array(guidedWorkflowItemSchema), @@ -40,6 +42,6 @@ export const guidedWorkflowsOutputSchema = z workflow_id: z.string(), rendered_prompt: z.string(), }), - ]) - .meta({ type: "object" }); + ]), +); export type GuidedWorkflowsResult = z.infer; diff --git a/src/contracts/inputObservationPresentation.test.ts b/src/contracts/inputObservationPresentation.test.ts index a9ff8d5898..f6793746ef 100644 --- a/src/contracts/inputObservationPresentation.test.ts +++ b/src/contracts/inputObservationPresentation.test.ts @@ -2,72 +2,77 @@ import { Ajv } from "ajv"; import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it } from "vitest"; -import { presentInputJsonSchema } from "./inputSchemaPresentation.js"; +import { z } from "zod"; -it.each(["draft-2020-12", "draft-07"] as const)( - "summarizes only declared observation schemas, preserving literal data in %s", - (target) => { - const literal = { - type: "object", - "x-rea-complete-observation": true, - properties: { producer_data: { type: "string" } }, - }; - const controls = { - type: "object", - properties: { match: { type: "string", minLength: 1 } }, - required: ["match"], - additionalProperties: false, - }; - const canonical = { - $schema: - target === "draft-07" - ? "http://json-schema.org/draft-07/schema#" - : "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - observation: { - type: "object", - description: "Complete result from the producer.", - "x-rea-complete-observation": true, - properties: { id: { type: "string" } }, - required: ["id"], - }, - controls, - literal: { const: literal }, - }, - required: ["observation", "controls", "literal"], - additionalProperties: false, - }; - const presented = presentInputJsonSchema(canonical, () => undefined); - expect(presented).toHaveProperty("properties.observation", { - type: "object", - description: expect.stringContaining("complete canonical observation"), - }); - expect(presented).toHaveProperty("properties.controls", controls); - expect(presented).toHaveProperty("properties.literal.const", literal); - const ajv = - target === "draft-07" - ? new Ajv({ strict: false }) - : new Ajv2020({ strict: false }); - const validate = ajv.compile(presented); - expect( - validate({ observation: {}, controls: { match: "x" }, literal }), - ).toBe(true); - expect( - validate({ observation: "{}", controls: { match: "x" }, literal }), - ).toBe(false); - expect( - validate({ observation: {}, controls: { match: "" }, literal }), - ).toBe(false); - expect( - ajv.compile(canonical)({ - observation: {}, - controls: { match: "x" }, - literal, - }), - ).toBe(false); - }, -); +import { BROWSER_TOOL_CONTRACTS } from "./browserToolContracts.js"; +import { presentInputJsonSchema } from "./inputSchemaPresentation.js"; +import { + advertisedInputJsonSchema, + COMPLETE_OBSERVATION_GUIDANCE, + toolOutputSchemaWithMetadata, +} from "./toolSchemaMetadata.js"; +import { compareBrowserScenariosInputSchema } from "../domain/browserScenarioDiffValues.js"; +import { captureSnapshotSchema } from "../domain/webCaptureDiffSchemas.js"; + +const browserContract = (name: string) => { + const found = BROWSER_TOOL_CONTRACTS.find( + (candidate) => candidate.name === name, + ); + if (found === undefined) throw new Error(`Missing ${name}`); + return found; +}; + +it("advertises complete observations as opaque objects only on the wire", () => { + const comparison = browserContract("compare_web_captures"); + const advertised = advertisedInputJsonSchema(comparison); + // Each opaque declaration is the field's own description, if any, followed + // by the shared instruction. + const declared = (description: string | undefined) => ({ + type: "object", + description: [description, COMPLETE_OBSERVATION_GUIDANCE] + .filter((text) => text !== undefined) + .join(" "), + }); + expect(advertised).toHaveProperty( + "properties.before.properties.inspection", + declared(captureSnapshotSchema.shape.inspection.description), + ); + expect(advertised).toHaveProperty( + "properties.before.properties.webmcp.anyOf.0", + declared(undefined), + ); + expect(advertised).toHaveProperty( + "properties.before_scenario", + declared( + compareBrowserScenariosInputSchema.shape.before_scenario.description, + ), + ); + // Analyst-authored controls stay explicit. + expect(advertised).toHaveProperty( + "properties.normalization.properties.rules.items.required", + ["rule_id", "artifacts", "match", "replacement"], + ); + const validate = new Ajv2020({ strict: false }).compile(advertised); + expect(validate({ before_scenario: {}, after_scenario: {} })).toBe(true); + expect(validate({ before_scenario: "{}", after_scenario: {} })).toBe(false); + + // Every other projection of the same schemas stays canonical: the plain + // conversion behind `--schema`, and the producer's own output schema. + const plain = JSON.stringify( + z.toJSONSchema(comparison.inputSchema, { io: "input" }), + ); + expect(plain).not.toContain(COMPLETE_OBSERVATION_GUIDANCE); + expect(plain).not.toContain("x-rea-"); + // The full scenario capture declaration, including its step list. + expect(plain).toContain('"steps":{'); + expect( + JSON.stringify( + toolOutputSchemaWithMetadata(browserContract("inspect_web_page"))[ + "~standard" + ].jsonSchema.output({ target: "draft-2020-12" }), + ), + ).not.toContain(COMPLETE_OBSERVATION_GUIDANCE); +}); const referenceWideningCases: readonly { name: string; diff --git a/src/contracts/inputSchemaPresentation.ts b/src/contracts/inputSchemaPresentation.ts index 0fac9ce34a..82beaa3ac1 100644 --- a/src/contracts/inputSchemaPresentation.ts +++ b/src/contracts/inputSchemaPresentation.ts @@ -7,6 +7,7 @@ import { isDeepStrictEqual } from "node:util"; import { advertisedSchemaPatterns } from "./advertisedDialect.js"; import { dependencyGuidance } from "./advertisedDialectProjection.js"; import { projectPortablePattern } from "./portablePattern.js"; +import type { SchemaAnnotations } from "../domain/schemaAnnotations.js"; import { schemaArrays, schemaChildren, @@ -50,22 +51,6 @@ const describeSchema = ( propertyDescription?: string, ): unknown => { if (!isObject(schema)) return schema; - // These operands are complete producer results, not fields an agent builds. - // Their owner keeps full canonical validation; only their wire declaration - // omits the repeated result schema. Analyst-authored controls stay explicit. - if (schema.type === "object" && schema["x-rea-complete-observation"] === true) - return { - type: "object", - description: [ - propertyDescription ?? - (typeof schema.description === "string" - ? schema.description - : undefined), - "Pass the complete returned normalized_result unchanged. Its fields are omitted from this declaration; the server still validates the complete canonical observation.", - ] - .filter((text) => text !== undefined) - .join(" "), - }; const scope = references.enter(schema, inherited); const described = Object.fromEntries( Object.entries(schema).map(([key, value]) => { @@ -895,13 +880,12 @@ const flattenRootObjectUnion = ( /** Preserve opt-in discriminator relationships without a root union. */ const presentDiscriminatedRoot = ( - root: Record, + canonical: Record, + discriminator: string | undefined, ): Record => { - const { "x-rea-root-discriminator": discriminator, ...canonical } = root; const references = new JsonSchemaReferences(canonical); const flattened = flattenRootObjectUnion(canonical, references); - if (typeof discriminator !== "string" || flattened === canonical) - return flattened; + if (discriminator === undefined || flattened === canonical) return flattened; const alternatives = canonical.anyOf ?? canonical.oneOf; if (!Array.isArray(alternatives) || alternatives.length !== 2) throw new TypeError( @@ -1000,8 +984,12 @@ const presentDiscriminatedRoot = ( export const presentInputJsonSchema = ( root: Record, describeProperty: (property: string) => string | undefined, + annotations: Pick = {}, ): Record => { - const flattened = presentDiscriminatedRoot(root); + const flattened = presentDiscriminatedRoot( + root, + annotations.rootDiscriminator, + ); const references = new JsonSchemaReferences(flattened); const described = describeSchema( flattened, diff --git a/src/contracts/javascript/electronToolContracts.ts b/src/contracts/javascript/electronToolContracts.ts index 2bd145aeeb..b1e3467c79 100644 --- a/src/contracts/javascript/electronToolContracts.ts +++ b/src/contracts/javascript/electronToolContracts.ts @@ -31,6 +31,7 @@ import { import { javascriptRuntimeReconciliationResultSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; import { reconcileJavaScriptRuntimeRequestSchema } from "./applicationWorkflowInputContracts.js"; import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "./javascriptRuntimeReconciliationExample.js"; +import { presentRootDiscriminator } from "../../domain/schemaAnnotations.js"; const listOutputSchema = evidenceResultOf(electronTargetListSchema); const inspectionOutputSchema = evidenceResultOf(electronPageInspectionSchema); @@ -41,16 +42,18 @@ const javascriptApplicationMcpInputSchema = ), }); /** Caller-selected delivery of a completed JavaScript application analysis. */ -export const analyzeJavaScriptApplicationRequestSchema = z - .discriminatedUnion("detail", [ - javascriptApplicationMcpInputSchema.extend( - javaScriptRoutesCompleteDeliveryShape, - ), - javascriptApplicationMcpInputSchema.extend( - javaScriptRoutesSummaryDeliveryShape, - ), - ]) - .meta({ "x-rea-root-discriminator": "detail" }); +export const analyzeJavaScriptApplicationRequestSchema = + presentRootDiscriminator( + z.discriminatedUnion("detail", [ + javascriptApplicationMcpInputSchema.extend( + javaScriptRoutesCompleteDeliveryShape, + ), + javascriptApplicationMcpInputSchema.extend( + javaScriptRoutesSummaryDeliveryShape, + ), + ]), + "detail", + ); const applicationOutputSchema = analysisDetailOutputSchemaOf( javascriptApplicationAnalysisResultSchema, z.union([ diff --git a/src/contracts/javascriptRouteDeliveryContracts.test.ts b/src/contracts/javascriptRouteDeliveryContracts.test.ts index 0d1f57432a..fa83216583 100644 --- a/src/contracts/javascriptRouteDeliveryContracts.test.ts +++ b/src/contracts/javascriptRouteDeliveryContracts.test.ts @@ -35,7 +35,8 @@ describe.each(cases)("%s delivery contract", (name, base) => { expect(projected).not.toHaveProperty("anyOf"); expect(projected).not.toHaveProperty("oneOf"); expect(projected).not.toHaveProperty("allOf"); - expect(projected).not.toHaveProperty("x-rea-root-discriminator"); + // The typed root discriminator selects the alternative. + expect(projected).toMatchObject({ if: { required: ["detail"] } }); const inputs = [ [{}, true], [{ detail: "complete" }, true], diff --git a/src/contracts/managed/managedToolContracts.ts b/src/contracts/managed/managedToolContracts.ts index 6c088dcd88..9eae027e87 100644 --- a/src/contracts/managed/managedToolContracts.ts +++ b/src/contracts/managed/managedToolContracts.ts @@ -5,6 +5,7 @@ import type { ToolContract } from "../toolContractTypes.js"; import { managedOutputSchemas } from "../toolOutputSchemaGroups.js"; import { toolContractMetadata } from "../toolEffects.js"; import { absoluteLocalPathStringSchema } from "../../domain/localPath.js"; +import { distinctItems } from "../../domain/schemaConstraints.js"; import { analysisDetailSchema } from "../analysisDetail.js"; import { managedInspectionBudgetShape } from "../../domain/managed/managedInspectionLimits.js"; @@ -25,20 +26,19 @@ export const managedArtifactInputSchema = managedTargetInputSchema.extend({ /** Member inspection may select declaring types before members are decoded. */ export const managedMembersInputSchema = managedArtifactInputSchema.extend({ - type_names: z - .array( - z - .string() - .min(1) - .describe( - "Exact declaring-type full_name: namespace-qualified for top-level types (System.Threading.Monitor); nested types report their bare name, so it selects every nested type of that name", - ), - ) - .min(1) - .refine((names) => new Set(names).size === names.length, { - message: "type_names must not repeat a name", - }) - .meta({ uniqueItems: true }) + type_names: distinctItems( + z + .array( + z + .string() + .min(1) + .describe( + "Exact declaring-type full_name: namespace-qualified for top-level types (System.Threading.Monitor); nested types report their bare name, so it selects every nested type of that name", + ), + ) + .min(1), + "type_names must not repeat a name", + ) .describe( "Inspect only the fields, methods, call edges, and field accesses declared by these exact type full names, and the user strings their method bodies load; omit to inspect every type. Unmatched names are reported in selection.unmatched_type_names.", ) diff --git a/src/contracts/python/pythonPycToolContracts.ts b/src/contracts/python/pythonPycToolContracts.ts index 71f85fec14..8caba70bc5 100644 --- a/src/contracts/python/pythonPycToolContracts.ts +++ b/src/contracts/python/pythonPycToolContracts.ts @@ -14,14 +14,16 @@ import { pythonPycCompleteDeliveryShape, pythonPycSummaryDeliveryShape, } from "../../domain/analysisView/pythonPycView.js"; +import { presentRootDiscriminator } from "../../domain/schemaAnnotations.js"; /** Delivery selection does not change provider inputs or complete Evidence identity. */ -export const inspectPythonPycRequestSchema = z - .discriminatedUnion("detail", [ +export const inspectPythonPycRequestSchema = presentRootDiscriminator( + z.discriminatedUnion("detail", [ inspectPythonPycInputSchema.extend(pythonPycCompleteDeliveryShape), inspectPythonPycInputSchema.extend(pythonPycSummaryDeliveryShape), - ]) - .meta({ "x-rea-root-discriminator": "detail" }); + ]), + "detail", +); /** Static CPython pyc inspection is independent of active native-analysis providers. */ export const PYTHON_PYC_TOOL_CONTRACTS = [ diff --git a/src/contracts/toolSchemaMetadata.test.ts b/src/contracts/toolSchemaMetadata.test.ts index 58864f282c..ed16fb3f7c 100644 --- a/src/contracts/toolSchemaMetadata.test.ts +++ b/src/contracts/toolSchemaMetadata.test.ts @@ -1,10 +1,12 @@ import { expect, it } from "vitest"; +import { z } from "zod"; import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js"; import { toolInputSchemaWithMetadata, toolOutputSchemaWithMetadata, } from "./toolSchemaMetadata.js"; +import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js"; // Each MCP server registers every contract. The SDK reprojects registered // schemas on every tools/list, so a wrapper rebuilt per server would repeat @@ -30,3 +32,17 @@ it("shares one advertised wrapper and projection per contract across registratio firstOutput["~standard"].jsonSchema.output(options), ); }); + +it("declares an object root only on the advertised output of an object union", () => { + const contract = APPLICATION_TOOL_CONTRACTS.find( + ({ name }) => name === "guided_workflows", + ); + if (contract === undefined) throw new Error("Missing guided_workflows"); + const advertised = toolOutputSchemaWithMetadata(contract)[ + "~standard" + ].jsonSchema.output({ target: "draft-2020-12" }); + expect(advertised).toMatchObject({ type: "object" }); + expect(advertised).toHaveProperty("oneOf"); + // The canonical projection keeps only what parsing enforces. + expect(z.toJSONSchema(contract.outputSchema)).not.toHaveProperty("type"); +}); diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts index 3ccd010897..b21df0282d 100644 --- a/src/contracts/toolSchemaMetadata.ts +++ b/src/contracts/toolSchemaMetadata.ts @@ -20,6 +20,7 @@ import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import { shareOutputDefinitions } from "./outputSchemaDefinitions.js"; import { getEvidenceInputContract } from "../domain/evidenceInputContract.js"; import { isRecord } from "../domain/isRecord.js"; +import { schemaAnnotations } from "../domain/schemaAnnotations.js"; const PROPERTY_DESCRIPTIONS: Readonly> = { addresses: "Ordered provider-normalized procedure addresses to analyze.", @@ -89,6 +90,49 @@ const oncePerContract = < return built; }; +/** The instruction an advertised complete observation carries after its description. */ +export const COMPLETE_OBSERVATION_GUIDANCE = + "Pass the complete returned normalized_result unchanged. Its fields are omitted from this declaration; the server still validates the complete canonical observation."; + +/** + * Apply typed presentation annotations while Zod projects an advertised input. + * Running inside this projection only keeps `--schema`, output schemas and + * every other JSON Schema conversion of the same Zod schema canonical. + */ +const presentAnnotatedInput = ({ + zodSchema, + jsonSchema, +}: { + readonly zodSchema: z.core.$ZodType; + readonly jsonSchema: Record; +}): void => { + if (schemaAnnotations(zodSchema)?.completeObservation !== true) return; + // These operands are complete producer results, not fields an agent builds, + // so the wire declaration omits the repeated result schema. + if (jsonSchema.type !== "object") + throw new TypeError( + "A complete observation annotation requires an object schema", + ); + const description = [jsonSchema.description, COMPLETE_OBSERVATION_GUIDANCE] + .filter((text) => typeof text === "string") + .join(" "); + for (const key of Object.keys(jsonSchema)) delete jsonSchema[key]; + Object.assign(jsonSchema, { type: "object", description }); +}; + +/** + * Declare `type: "object"` on a union root whose alternatives are all objects, + * after its combinator, where Zod places root metadata. + */ +const objectRoot = ({ + $defs, + ...root +}: Record): Record => ({ + ...root, + type: "object", + ...($defs === undefined ? {} : { $defs }), +}); + /** Attach caller guidance to a canonical schema for the SDK wire projection. */ export const toolInputSchemaWithMetadata = ( contract: Contract, @@ -100,12 +144,17 @@ export const toolInputSchemaWithMetadata = ( (project) => (options) => { const inline = project({ ...options, - libraryOptions: { ...options.libraryOptions, reused: "inline" }, + libraryOptions: { + ...options.libraryOptions, + reused: "inline", + override: presentAnnotatedInput, + }, }); const examples = advertisedInputExamples(contract); const presented = presentInputJsonSchema( inline, fallbackPropertyDescription, + schemaAnnotations(contract.inputSchema), ); const sourceGuidance = getEvidenceInputContract(contract.inputSchema) .hasEvidence @@ -146,12 +195,15 @@ export const toolOutputSchemaWithMetadata = ( contract.outputSchema, "output", (project) => (options) => { + const projected = project({ + ...options, + libraryOptions: { reused: "ref", ...options.libraryOptions }, + }); const shared = shareOutputDefinitions( presentOutputJsonSchema( - project({ - ...options, - libraryOptions: { reused: "ref", ...options.libraryOptions }, - }), + schemaAnnotations(contract.outputSchema)?.objectRoot === true + ? objectRoot(projected) + : projected, ), ); // An explicit ID owns the reference base. Relative external references diff --git a/src/contracts/webScriptToolContracts.ts b/src/contracts/webScriptToolContracts.ts index fbe318f8fa..47b5695e28 100644 --- a/src/contracts/webScriptToolContracts.ts +++ b/src/contracts/webScriptToolContracts.ts @@ -24,13 +24,15 @@ import { webSourceLocationInputSchema, webSourceLocationResultSchema, } from "../domain/webSourceLocation.js"; +import { presentRootDiscriminator } from "../domain/schemaAnnotations.js"; -export const webSourceLocationRequestSchema = z - .discriminatedUnion("detail", [ +export const webSourceLocationRequestSchema = presentRootDiscriminator( + z.discriminatedUnion("detail", [ webSourceLocationInputSchema.extend(webSourceLocationCompleteDeliveryShape), webSourceLocationInputSchema.extend(webSourceLocationSummaryDeliveryShape), - ]) - .meta({ "x-rea-root-discriminator": "detail" }); + ]), + "detail", +); /** Local captured-script publication contracts shared by CLI and MCP. */ export const WEB_SCRIPT_TOOL_CONTRACTS = [ diff --git a/src/domain/analysisView/environmentManifestView.ts b/src/domain/analysisView/environmentManifestView.ts index 347aadde3e..1c6e902aed 100644 --- a/src/domain/analysisView/environmentManifestView.ts +++ b/src/domain/analysisView/environmentManifestView.ts @@ -4,6 +4,7 @@ import { AnalysisUnsupportedTargetError } from "../analysisErrorCore.js"; import type { AnalysisError } from "../analysisErrorBase.js"; import { uniqueSorted } from "../canonicalOrdering.js"; import { err, ok, type Result } from "../result.js"; +import { distinctItems } from "../schemaConstraints.js"; import type { AnalysisViewParent } from "./analysisViewParent.js"; import { ownedView, type AdmittedView } from "./analysisViewShape.js"; import { EnvironmentManifestAccumulator } from "./EnvironmentManifestAccumulator.js"; @@ -19,13 +20,10 @@ import { type ScenarioResponseBodyRetention, } from "./scenarioCaptureRecords.js"; /** Opt-in template rules, each listed at most once; shared by every view that groups tools. */ -export const templateRulesSchema = z - .array(z.enum(TEMPLATE_RULES)) - .refine((rules) => new Set(rules).size === rules.length, { - message: "template_rules must not repeat a rule", - }) - .meta({ uniqueItems: true }) - .default([]); +export const templateRulesSchema = distinctItems( + z.array(z.enum(TEMPLATE_RULES)), + "template_rules must not repeat a rule", +).default([]); export const environmentManifestViewRequestSchema = z.strictObject({ kind: z.literal("environment-manifest"), diff --git a/src/domain/apiContract/apiContractQuery.ts b/src/domain/apiContract/apiContractQuery.ts index 1e2cf14965..c0fd72559a 100644 --- a/src/domain/apiContract/apiContractQuery.ts +++ b/src/domain/apiContract/apiContractQuery.ts @@ -7,6 +7,7 @@ import { apiContractOperationSchema, apiContractPathReferenceSchema, } from "./apiContract.js"; +import { presentRootDiscriminator } from "../schemaAnnotations.js"; const page = { kind: z.literal("api-contract"), @@ -67,8 +68,8 @@ const commonInput = { }; const selectionDescription = "Return this selected page immediately; complete Evidence is retained and view takes precedence over detail"; -export const inspectApiContractInputSchema = z - .union([ +export const inspectApiContractInputSchema = presentRootDiscriminator( + z.union([ z.strictObject({ ...commonInput, format: z.literal("openapi-json").default("openapi-json"), @@ -88,8 +89,9 @@ export const inspectApiContractInputSchema = z .optional() .describe(selectionDescription), }), - ]) - .meta({ "x-rea-root-discriminator": "format" }); + ]), + "format", +); export type InspectApiContractInput = z.output< typeof inspectApiContractInputSchema >; diff --git a/src/domain/browserScenarioDiffValues.ts b/src/domain/browserScenarioDiffValues.ts index a46166fdba..138b16ec3b 100644 --- a/src/domain/browserScenarioDiffValues.ts +++ b/src/domain/browserScenarioDiffValues.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { retainedBrowserScenarioCaptureSchema } from "./browserScenarioCapture.js"; +import { presentCompleteObservation } from "./schemaAnnotations.js"; /** Scenario artifact families compared after step alignment. */ export const browserScenarioArtifactKindSchema = z.enum([ @@ -74,14 +75,16 @@ const normalizationInputSchema = z /** Input for step-aligned comparison of two browser scenario captures. */ export const compareBrowserScenariosInputSchema = z.strictObject({ - before_scenario: retainedBrowserScenarioCaptureSchema - .describe( + before_scenario: presentCompleteObservation( + retainedBrowserScenarioCaptureSchema.describe( "Earlier complete normalized_result from capture_browser_scenario.", - ) - .meta({ "x-rea-complete-observation": true }), - after_scenario: retainedBrowserScenarioCaptureSchema - .describe("Later complete normalized_result from capture_browser_scenario.") - .meta({ "x-rea-complete-observation": true }), + ), + ), + after_scenario: presentCompleteObservation( + retainedBrowserScenarioCaptureSchema.describe( + "Later complete normalized_result from capture_browser_scenario.", + ), + ), normalization: normalizationInputSchema .default({ rules: [] }) .describe("Exact-literal rules committed in the comparison result."), diff --git a/src/domain/desktop/appBuild.ts b/src/domain/desktop/appBuild.ts index de7341f3a2..ff0d73951d 100644 --- a/src/domain/desktop/appBuild.ts +++ b/src/domain/desktop/appBuild.ts @@ -1,7 +1,6 @@ import { BUILD_COMPUTER_SETTINGS, BUNDLE_SETTING_VARIABLE, - isBundleSettingVariable, } from "./buildComputerSettings.js"; import { buildSecretHandleSchema } from "./buildSecrets.js"; import { createHash } from "node:crypto"; @@ -9,6 +8,7 @@ import { createHash } from "node:crypto"; import { z } from "zod"; import { absoluteLocalPathStringSchema } from "../localPath.js"; +import { excludedValues } from "../schemaConstraints.js"; import { appModuleFile, aptSources, @@ -149,14 +149,11 @@ export const buildSourceSchema = z.discriminatedUnion("kind", [ ), settings: z .record( - z - .string() - .regex(BUNDLE_SETTING_VARIABLE) - .refine( - isBundleSettingVariable, - "Build host control names are reserved.", - ) - .meta({ not: { enum: [...BUILD_COMPUTER_SETTINGS] } }), + excludedValues( + z.string().regex(BUNDLE_SETTING_VARIABLE), + BUILD_COMPUTER_SETTINGS, + "Build host control names are reserved.", + ), buildSecretHandleSchema, ) .optional() diff --git a/src/domain/desktop/buildComputerSettings.ts b/src/domain/desktop/buildComputerSettings.ts index e9954cfe4b..0686032f58 100644 --- a/src/domain/desktop/buildComputerSettings.ts +++ b/src/domain/desktop/buildComputerSettings.ts @@ -5,10 +5,5 @@ export const BUILD_COMPUTER_SETTINGS = [ "REA_BUILD_COMPUTER_REA_ARCHIVE", ] as const; -/** Bundle variable names; schemas advertise the host-control exclusion with `not`. */ +/** Bundle variable names; host-control names are excluded separately. */ export const BUNDLE_SETTING_VARIABLE = /^REA_[A-Z0-9_]{1,60}$/u; - -/** Whether a bundle setting name is portable and not a host-control name. */ -export const isBundleSettingVariable = (name: string): boolean => - BUNDLE_SETTING_VARIABLE.test(name) && - !(BUILD_COMPUTER_SETTINGS as readonly string[]).includes(name); diff --git a/src/domain/flutter/flutterBuildAnalysis.ts b/src/domain/flutter/flutterBuildAnalysis.ts index 1859a0bcb1..2eea5c646a 100644 --- a/src/domain/flutter/flutterBuildAnalysis.ts +++ b/src/domain/flutter/flutterBuildAnalysis.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { maxCodePoints } from "../schemaConstraints.js"; import { localPathStringSchema } from "../localPath.js"; import { digestSchema } from "../digests.js"; @@ -29,20 +30,11 @@ const ABI_SELECTOR_MAX_CHARACTERS = */ /** Shared refusal for an ABI label that is not a lib/ directory name. */ const ABI_SELECTOR_MESSAGE = `ABI must be the exact lib/ directory label: 1–${String(ABI_SELECTOR_MAX_CHARACTERS)} Unicode characters without '/'.`; -const abiSelectorSchema = z - .string() - .min(1, ABI_SELECTOR_MESSAGE) - .refine( - (label) => { - const characters = label[Symbol.iterator](); - for (let count = 0; count <= ABI_SELECTOR_MAX_CHARACTERS; count += 1) - if (characters.next().done) return true; - return false; - }, - { message: ABI_SELECTOR_MESSAGE, abort: true }, - ) - .regex(/^[^\x2f]*$/u, ABI_SELECTOR_MESSAGE) - .meta({ maxLength: ABI_SELECTOR_MAX_CHARACTERS }); +const abiSelectorSchema = maxCodePoints( + z.string().min(1, ABI_SELECTOR_MESSAGE), + ABI_SELECTOR_MAX_CHARACTERS, + ABI_SELECTOR_MESSAGE, +).regex(/^[^\x2f]*$/u, ABI_SELECTOR_MESSAGE); /** Bounded printable-run selection, independent of snapshot structural coverage. */ const DART_POOL_DEFAULTS = { diff --git a/src/domain/il2cpp/il2cppMetadata.ts b/src/domain/il2cpp/il2cppMetadata.ts index a2fe0f12b5..b59c22a0b8 100644 --- a/src/domain/il2cpp/il2cppMetadata.ts +++ b/src/domain/il2cpp/il2cppMetadata.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { requirePresence } from "../schemaConstraints.js"; import { digestSchema } from "../digests.js"; import { absoluteLocalPathStringSchema, @@ -106,8 +107,8 @@ const optionalIndex = z.number().int().min(-1); const indexWidth = z.union([z.literal(1), z.literal(2), z.literal(4)]); /** One explicit local file; detail selects delivery, never decoding coverage. */ -export const inspectIl2cppMetadataInputSchema = z - .strictObject({ +export const inspectIl2cppMetadataInputSchema = requirePresence( + z.strictObject({ path: absoluteLocalPathStringSchema.describe( "Absolute filesystem path to a Unity IL2CPP global-metadata.dat file", ), @@ -134,30 +135,21 @@ export const inspectIl2cppMetadataInputSchema = z .describe( "Also link generic method instances (genericMethodTable rows) to code addresses with Il2CppDumper-style Type$$Method names; needs binary_path", ), - }) - .superRefine((input, context) => { - if (input.binary_path !== undefined) return; - if (input.binary_architecture !== undefined) - context.addIssue({ - code: "custom", - path: ["binary_architecture"], - message: "binary_architecture selects a slice of binary_path.", - }); - if (input.generic_instances === true) - context.addIssue({ - code: "custom", - path: ["generic_instances"], - message: "generic_instances links generic methods in binary_path.", - }); - }) - .meta({ - dependentRequired: { binary_architecture: ["binary_path"] }, - if: { - properties: { generic_instances: { const: true } }, - required: ["generic_instances"], + }), + [ + { + when: "binary_architecture", + requires: "binary_path", + message: "binary_architecture selects a slice of binary_path.", }, - then: { required: ["binary_path"] }, - }); + { + when: "generic_instances", + equals: true, + requires: "binary_path", + message: "generic_instances links generic methods in binary_path.", + }, + ], +); export type IL2cppMetadataSectionName = z.infer< typeof il2cppMetadataSectionSchema diff --git a/src/domain/native/functionDossier.ts b/src/domain/native/functionDossier.ts index 46851c0fe7..18cce63786 100644 --- a/src/domain/native/functionDossier.ts +++ b/src/domain/native/functionDossier.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { requirePresence } from "../schemaConstraints.js"; import { err, ok, type Result } from "../result.js"; import { AnalysisOutputError } from "../analysisErrorCore.js"; import type { JsonValue } from "../jsonValue.js"; @@ -46,84 +47,85 @@ export const functionFacetAvailabilitySchema = z }) .strict(); -export const functionDossierSchema = z - .object({ - procedure: procedureIdentitySchema.safeExtend({ - signature: z.string().nullable(), - locals: z.array(localVariableSchema), - }), - pseudocode: z.string(), - assembly: z.array(z.string()), - comments: z.array( - z - .object({ - address: z.string(), - kind: z.enum(["comment", "inline"]), - text: z.string(), - }) - .strict(), - ), - callers: z.array(procedureIdentitySchema), - callees: z.array(procedureIdentitySchema), - incoming_references: z.array(referenceEdgeSchema), - outgoing_references: z.array(referenceEdgeSchema), - unresolved_calls: z.array(unresolvedCallSchema).exactOptional(), - referenced_strings: z.array( - analysisStringSchema.extend({ source_address: z.string() }), - ), - referenced_names: z.array( - z - .object({ - address: z.string(), - value: z.string(), - source_address: z.string(), - }) - .strict(), - ), - swift_string_literals: z - .array(swiftStringLiteralSchema) - .exactOptional() - .describe( - "Swift String literals decoded from constant register values. Absent when the provider does not decode them for this target.", +const SWIFT_LITERALS_TOGETHER = + "Swift literals and their omissions must be recorded together"; + +export const functionDossierSchema = requirePresence( + z + .object({ + procedure: procedureIdentitySchema.safeExtend({ + signature: z.string().nullable(), + locals: z.array(localVariableSchema), + }), + pseudocode: z.string(), + assembly: z.array(z.string()), + comments: z.array( + z + .object({ + address: z.string(), + kind: z.enum(["comment", "inline"]), + text: z.string(), + }) + .strict(), ), - swift_string_literal_omissions: z - .array(swiftStringLiteralOmissionSchema) - .exactOptional() - .describe( - "Literal candidates omitted for capacity or unresolved control flow. Present exactly when swift_string_literals is present; empty when all examined use sites were classified.", + callers: z.array(procedureIdentitySchema), + callees: z.array(procedureIdentitySchema), + incoming_references: z.array(referenceEdgeSchema), + outgoing_references: z.array(referenceEdgeSchema), + unresolved_calls: z.array(unresolvedCallSchema).exactOptional(), + referenced_strings: z.array( + analysisStringSchema.extend({ source_address: z.string() }), ), - basic_blocks: z.array( - z - .object({ - start: z.string(), - end: z.string(), - successors: z.array(z.string()), - }) - .strict(), - ), - native_api: nativeApiBoundarySchema.nullable(), - native_value_flow: nativeValueFlowSchema.nullable(), - facet_availability: functionFacetAvailabilitySchema.exactOptional(), - limitations: z.array(z.string()), - }) - .strict() - .superRefine((value, context) => { - if ( - (value.swift_string_literals === undefined) !== - (value.swift_string_literal_omissions === undefined) - ) - context.addIssue({ - code: "custom", - path: ["swift_string_literal_omissions"], - message: "Swift literals and their omissions must be recorded together", - }); - }) - .meta({ - dependentRequired: { - swift_string_literals: ["swift_string_literal_omissions"], - swift_string_literal_omissions: ["swift_string_literals"], + referenced_names: z.array( + z + .object({ + address: z.string(), + value: z.string(), + source_address: z.string(), + }) + .strict(), + ), + swift_string_literals: z + .array(swiftStringLiteralSchema) + .exactOptional() + .describe( + "Swift String literals decoded from constant register values. Absent when the provider does not decode them for this target.", + ), + swift_string_literal_omissions: z + .array(swiftStringLiteralOmissionSchema) + .exactOptional() + .describe( + "Literal candidates omitted for capacity or unresolved control flow. Present exactly when swift_string_literals is present; empty when all examined use sites were classified.", + ), + basic_blocks: z.array( + z + .object({ + start: z.string(), + end: z.string(), + successors: z.array(z.string()), + }) + .strict(), + ), + native_api: nativeApiBoundarySchema.nullable(), + native_value_flow: nativeValueFlowSchema.nullable(), + facet_availability: functionFacetAvailabilitySchema.exactOptional(), + limitations: z.array(z.string()), + }) + .strict(), + [ + { + when: "swift_string_literals", + requires: "swift_string_literal_omissions", + message: SWIFT_LITERALS_TOGETHER, + path: "swift_string_literal_omissions", }, - }); + { + when: "swift_string_literal_omissions", + requires: "swift_string_literals", + message: SWIFT_LITERALS_TOGETHER, + }, + ], +); /** Strict analyzed-function dossier shared by provider and comparison boundaries. */ export type FunctionDossier = z.infer; diff --git a/src/domain/portableBundleName.ts b/src/domain/portableBundleName.ts index 1dd7e37e20..9a8b47742a 100644 --- a/src/domain/portableBundleName.ts +++ b/src/domain/portableBundleName.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { normalizeHttpHostname } from "./httpHostname.js"; +import { declaredByPatterns } from "./schemaConstraints.js"; import { err, ok, type Result } from "./result.js"; /** Agentenv folder and local-id spelling, also used for authored task files. */ @@ -35,15 +36,17 @@ export const isPortableBundleName = (name: string): boolean => parsePortableBundleName(name).ok; /** Lookup names share portable identity; authoring reservations are separate. */ -export const portableBundleNameSchema = z - .string() - .regex(PORTABLE_BUNDLE_NAME_PATTERN) - .max(PORTABLE_BUNDLE_NAME_MAX_LENGTH) - .refine(isPortableBundleName, `Expected ${PORTABLE_BUNDLE_NAME_CONSTRAINT}`) - // Advertise both reservations without look-around, which strict clients reject. - .meta({ - allOf: [ - { not: { pattern: WINDOWS_RESERVED_NAME.source } }, - { not: { pattern: NUMERIC_HOSTNAME_NAME.source } }, - ], - }); +export const portableBundleNameSchema = declaredByPatterns( + z + .string() + .regex(PORTABLE_BUNDLE_NAME_PATTERN) + .max(PORTABLE_BUNDLE_NAME_MAX_LENGTH), + isPortableBundleName, + // Both reservations without look-around, which strict clients reject. The + // hostname check also refuses names these patterns admit. + { + excludes: [WINDOWS_RESERVED_NAME, NUMERIC_HOSTNAME_NAME], + relation: "wider", + }, + { message: `Expected ${PORTABLE_BUNDLE_NAME_CONSTRAINT}` }, +); diff --git a/src/domain/schemaAnnotations.test.ts b/src/domain/schemaAnnotations.test.ts new file mode 100644 index 0000000000..c335e5fd60 --- /dev/null +++ b/src/domain/schemaAnnotations.test.ts @@ -0,0 +1,46 @@ +import { expect, it } from "vitest"; +import { z } from "zod"; + +import { + presentCompleteObservation, + presentRootDiscriminator, + schemaAnnotations, +} from "./schemaAnnotations.js"; + +const alternatives = () => + [ + z.strictObject({ mode: z.literal("a"), a: z.string() }), + z.strictObject({ mode: z.literal("b").optional(), b: z.string() }), + ] as const; + +it("keeps a root discriminator out of the canonical JSON Schema", () => { + const schema = presentRootDiscriminator(z.union(alternatives()), "mode"); + expect(schemaAnnotations(schema)).toEqual({ rootDiscriminator: "mode" }); + expect(z.toJSONSchema(schema)).toEqual( + z.toJSONSchema(z.union(alternatives())), + ); +}); + +it("follows clones of the declaring schema, not other schemas", () => { + const schema = presentRootDiscriminator(z.union(alternatives()), "mode"); + expect(schemaAnnotations(schema.describe("described"))).toEqual({ + rootDiscriminator: "mode", + }); + expect(schemaAnnotations(schema.optional())).toBeUndefined(); + expect(schemaAnnotations(z.union(alternatives()))).toBeUndefined(); +}); + +it("annotates a copy as a complete observation, leaving the source canonical", () => { + const source = z.object({ id: z.string() }).describe("Producer result."); + const operand = presentCompleteObservation(source); + expect(schemaAnnotations(operand)).toEqual({ completeObservation: true }); + expect(schemaAnnotations(source)).toBeUndefined(); + expect(z.toJSONSchema(operand)).toEqual(z.toJSONSchema(source)); + expect(operand.parse({ id: "x" })).toEqual({ id: "x" }); + expect(() => operand.parse({})).toThrow(); +}); + +it("accepts only a property the alternatives declare", () => { + // @ts-expect-error The alternatives do not declare this property. + presentRootDiscriminator(z.union(alternatives()), "missing"); +}); diff --git a/src/domain/schemaAnnotations.ts b/src/domain/schemaAnnotations.ts new file mode 100644 index 0000000000..fdeac723f9 --- /dev/null +++ b/src/domain/schemaAnnotations.ts @@ -0,0 +1,102 @@ +import { z } from "zod"; + +// Typed presentation annotations on canonical Zod schemas. The advertised +// schema presenter reads them by schema identity; they never enter the +// canonical JSON Schema, so runtime validation and every other projection of +// the schema are unchanged. + +/** Presentation facts a canonical schema declares for its advertised form. */ +export interface SchemaAnnotations { + /** + * Present this two-object root union as one object whose `if`/`then`/`else` + * selects the alternative by this property's literal value. + */ + readonly rootDiscriminator?: string; + /** + * This object is a complete producer result the caller passes back + * unchanged. Its advertised form omits the repeated result fields and keeps + * only its description plus that instruction; runtime validation still + * parses the complete canonical observation. + */ + readonly completeObservation?: true; + /** + * How a pattern-only JSON Schema declaration relates to the runtime + * predicate it stands for: `exact` accepts the same strings, `wider` also + * admits strings the predicate rejects. Declaration checks compare the + * accepted sets accordingly. + */ + readonly patternDeclaration?: "exact" | "wider"; + /** + * Every alternative of this union is an object, so its advertised output + * root also declares `type: "object"` for MCP codecs that read only the + * root type. + */ + readonly objectRoot?: true; +} + +const annotations = z.registry(); + +/** The annotations declared on this schema or the schema it was cloned from. */ +export const schemaAnnotations = ( + schema: z.core.$ZodType, +): SchemaAnnotations | undefined => annotations.get(schema); + +type ObjectAlternatives = readonly [z.core.$ZodObject, z.core.$ZodObject]; + +/** + * Advertise a root union of two strict objects as one object keyed on + * `discriminator`: the alternative requiring it, otherwise the other one. + */ +export const presentRootDiscriminator = < + Schema extends z.ZodUnion, +>( + schema: Schema, + discriminator: keyof z.input & string, +): Schema => { + annotations.add(schema, { + ...annotations.get(schema), + rootDiscriminator: discriminator, + }); + return schema; +}; + +/** + * Advertise a copy of `schema` as a complete producer result to pass back + * unchanged. The shared result schema itself keeps its full advertised form + * wherever else it appears, such as the producer's output schema. + */ +export const presentCompleteObservation = ( + schema: Schema, +): Schema => { + // The clone inherits the source's annotations and description. + const annotated = schema.clone(); + annotations.add(annotated, { completeObservation: true }); + return annotated; +}; + +/** + * Record how `schema`'s pattern-only declaration relates to its runtime + * predicate. `schemaConstraints.ts` owns the declaration itself. + */ +export const annotatePatternDeclaration = ( + schema: Schema, + relation: "exact" | "wider", +): Schema => { + annotations.add(schema, { + ...annotations.get(schema), + patternDeclaration: relation, + }); + return schema; +}; + +type ObjectUnion = + | z.ZodUnion + | z.ZodDiscriminatedUnion; + +/** Advertise an output union of objects with an object root type. */ +export const presentObjectRoot = ( + schema: Schema, +): Schema => { + annotations.add(schema, { ...annotations.get(schema), objectRoot: true }); + return schema; +}; diff --git a/src/domain/schemaConstraints.test.ts b/src/domain/schemaConstraints.test.ts new file mode 100644 index 0000000000..1c2c602e33 --- /dev/null +++ b/src/domain/schemaConstraints.test.ts @@ -0,0 +1,188 @@ +import { Ajv2020 } from "ajv/dist/2020.js"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { canonicalBase64Schema } from "./stringPatterns.js"; +import { portableBundleNameSchema } from "./portableBundleName.js"; +import { schemaAnnotations } from "./schemaAnnotations.js"; +import { + declaredByPatterns, + distinctItems, + excludedValues, + maxCodePoints, + requirePresence, +} from "./schemaConstraints.js"; + +/** + * The declared JSON Schema and the Zod parser accept the same values, or, for + * a declaration annotated as a wider pattern approximation, the declaration + * accepts every value parsing does. + */ +const expectAgreement = (schema: z.ZodType, samples: readonly unknown[]) => { + const declared = new Ajv2020({ strict: false }).compile( + z.toJSONSchema(schema, { io: "input" }), + ); + const wider = schemaAnnotations(schema)?.patternDeclaration === "wider"; + for (const sample of samples) { + const parsed = schema.safeParse(sample).success; + if (wider && !parsed) continue; + expect(declared(sample), JSON.stringify(sample)).toBe(parsed); + } +}; + +describe("schema constraints", () => { + it("declares distinct items as uniqueItems", () => { + const schema = distinctItems(z.array(z.string()), "repeated"); + expect(z.toJSONSchema(schema)).toHaveProperty("uniqueItems", true); + expectAgreement(schema, [[], ["a", "b"], ["a", "a"]]); + expect(schema.safeParse(["a", "a"]).error?.issues[0]?.message).toBe( + "repeated", + ); + }); + + it("declares excluded values as not-enum", () => { + const schema = excludedValues(z.string(), ["RESERVED"], "reserved"); + expect(z.toJSONSchema(schema)).toHaveProperty("not", { + enum: ["RESERVED"], + }); + expectAgreement(schema, ["OTHER", "RESERVED"]); + }); + + it("bounds code points, the unit JSON Schema maxLength counts", () => { + const schema = maxCodePoints(z.string(), 2, "too long"); + expect(z.toJSONSchema(schema)).toHaveProperty("maxLength", 2); + // Two astral code points are four UTF-16 code units. + expectAgreement(schema, ["", "ab", "abc", "\u{1F600}\u{1F600}"]); + expect(schema.safeParse("\u{1F600}\u{1F600}").success).toBe(true); + }); + + it("declares presence rules as dependentRequired and if/then", () => { + const schema = requirePresence( + z.strictObject({ + target: z.string().optional(), + offset: z.int().optional(), + mode: z.enum(["all", "one"]).optional(), + }), + [ + { when: "offset", requires: "target", message: "offset needs target" }, + { + when: "mode", + equals: "one", + requires: "target", + message: "one needs target", + }, + ], + ); + expect(z.toJSONSchema(schema)).toMatchObject({ + dependentRequired: { offset: ["target"] }, + if: { properties: { mode: { const: "one" } }, required: ["mode"] }, + then: { required: ["target"] }, + }); + expectAgreement(schema, [ + {}, + { offset: 1 }, + { offset: 1, target: "t" }, + { mode: "all" }, + { mode: "one" }, + { mode: "one", target: "t" }, + ]); + expect( + schema.safeParse({ offset: 1, mode: "one" }).error?.issues, + ).toMatchObject([ + { path: ["offset"], message: "offset needs target" }, + { path: ["mode"], message: "one needs target" }, + ]); + }); + + it("combines several value-keyed rules under allOf", () => { + const schema = requirePresence( + z.strictObject({ + target: z.string().optional(), + first: z.boolean().optional(), + second: z.boolean().optional(), + }), + [ + { when: "first", equals: true, requires: "target", message: "first" }, + { when: "second", equals: true, requires: "target", message: "second" }, + ], + ); + expect(z.toJSONSchema(schema)).toHaveProperty("allOf.length", 2); + expectAgreement(schema, [ + { first: false, second: false }, + { first: true }, + { second: true }, + { first: true, second: true, target: "t" }, + ]); + }); + + it("declares a predicate by patterns built from the same expressions", () => { + const schema = declaredByPatterns( + z.string(), + (value) => value.startsWith("a") && !/b/u.test(value), + { matches: [/^a/u], excludes: [/b/u], relation: "exact" }, + { message: "rejected" }, + ); + expect(z.toJSONSchema(schema)).toHaveProperty("allOf", [ + { pattern: "^a" }, + { not: { pattern: "b" } }, + ]); + expect(schemaAnnotations(schema)?.patternDeclaration).toBe("exact"); + expectAgreement(schema, ["", "a", "ab", "ac", "b"]); + }); + + it("refuses pattern declarations JSON Schema cannot state", () => { + const declare = (declaration: Parameters[2]) => + declaredByPatterns(z.string(), () => true, declaration, { + message: "rejected", + }); + expect(() => declare({ matches: [/^app$/iu], relation: "exact" })).toThrow( + TypeError, + ); + expect(() => declare({ excludes: [/a/gu], relation: "wider" })).toThrow( + TypeError, + ); + expect(() => declare({ relation: "wider" })).toThrow(TypeError); + expect(() => + declare({ matches: [], excludes: [], relation: "exact" }), + ).toThrow(TypeError); + }); + + it("declares canonical base64 exactly", () => { + expect(schemaAnnotations(canonicalBase64Schema)?.patternDeclaration).toBe( + "exact", + ); + expectAgreement(canonicalBase64Schema, [ + "", + "AA==", + "AB==", + "AAA=", + "AAB=", + "AAAA", + "AAAAAA==", + "A===", + "AA=A", + "A", + "AA\nA", + "\u{1F600}AA=", + ]); + }); + + it("declares portable bundle names by a wider pattern approximation", () => { + expect( + schemaAnnotations(portableBundleNameSchema)?.patternDeclaration, + ).toBe("wider"); + expectAgreement(portableBundleNameSchema, [ + "app", + "my-app-2", + "con", + "lpt1", + "123", + "0x1f", + "0x", + "1e5", + "Upper", + "a".repeat(63), + "a".repeat(64), + ]); + }); +}); diff --git a/src/domain/schemaConstraints.ts b/src/domain/schemaConstraints.ts new file mode 100644 index 0000000000..b113d18c79 --- /dev/null +++ b/src/domain/schemaConstraints.ts @@ -0,0 +1,174 @@ +import { z } from "zod"; + +import { annotatePatternDeclaration } from "./schemaAnnotations.js"; + +// Constraints Zod has no native check for. Each helper adds the runtime check +// and the JSON Schema keyword that declares it in one place, so every JSON +// Schema projection states exactly what parsing enforces. + +type Primitive = string | number | boolean; + +/** Reject repeated items; declared as `uniqueItems`. */ +export const distinctItems = >>( + schema: Schema, + message: string, +): Schema => + schema + .refine((items) => new Set(items).size === items.length, { message }) + .meta({ uniqueItems: true }); + +/** Reject these exact values; declared as `not: { enum }`. */ +export const excludedValues = >( + schema: Schema, + values: readonly Primitive[], + message: string, +): Schema => + schema + .refine((value) => !values.includes(value), { message }) + .meta({ not: { enum: [...values] } }); + +/** + * Bound a string by Unicode code points, the unit JSON Schema `maxLength` + * counts; Zod's `.max()` counts UTF-16 code units. Aborts on failure, so later + * checks never see an oversized value. + */ +export const maxCodePoints = ( + schema: z.ZodString, + maximum: number, + message: string, +): z.ZodString => + schema + .refine( + (value) => { + const characters = value[Symbol.iterator](); + for (let count = 0; count <= maximum; count += 1) + if (characters.next().done) return true; + return false; + }, + { message, abort: true }, + ) + .meta({ maxLength: maximum }); + +/** One property whose presence requires another. */ +export interface PresenceRule { + /** The property that, when present, triggers the rule. */ + readonly when: Field; + /** Trigger only when `when` holds exactly this value. */ + readonly equals?: Primitive; + readonly requires: Field; + readonly message: string; + /** Issue path; defaults to `when`. */ + readonly path?: Field; +} + +type Declaration = Record; + +const declarePresence = ( + rules: readonly PresenceRule[], +): Declaration => { + const dependentRequired: Record = {}; + const conditionals: Declaration[] = []; + for (const rule of rules) { + if (rule.equals === undefined) { + (dependentRequired[rule.when] ??= []).push(rule.requires); + continue; + } + conditionals.push({ + if: { + properties: { [rule.when]: { const: rule.equals } }, + required: [rule.when], + }, + then: { required: [rule.requires] }, + }); + } + return { + ...(Object.keys(dependentRequired).length === 0 + ? {} + : { dependentRequired }), + ...(conditionals.length === 1 + ? conditionals[0] + : conditionals.length > 1 + ? { allOf: conditionals } + : {}), + }; +}; + +/** + * Require properties together; declared as `dependentRequired` and, for a + * rule keyed on a value, `if`/`then`. Issues follow the rule order. + */ +export const requirePresence = < + Schema extends z.ZodObject, + Field extends keyof z.output & string, +>( + schema: Schema, + rules: readonly PresenceRule[], +): Schema => + schema + .superRefine((parsed, context) => { + const value = parsed as Record; + for (const rule of rules) { + const trigger = value[rule.when]; + if ( + trigger === undefined || + (rule.equals !== undefined && trigger !== rule.equals) || + value[rule.requires] !== undefined + ) + continue; + context.addIssue({ + code: "custom", + path: [rule.path ?? rule.when], + message: rule.message, + }); + } + }) + .meta(declarePresence(rules)); + +/** Patterns that declare a string predicate JSON Schema cannot state directly. */ +export interface PatternDeclaration { + /** Patterns every accepted string matches. */ + readonly matches?: readonly RegExp[]; + /** Patterns no accepted string matches. */ + readonly excludes?: readonly RegExp[]; + /** + * `exact` when the patterns accept exactly what `predicate` does; `wider` + * when `predicate` also rejects strings the patterns admit. + */ + readonly relation: "exact" | "wider"; +} + +/** A pattern JSON Schema states with the same meaning: no flag but `u`. */ +const declaredPattern = (expression: RegExp): string => { + if (expression.flags !== "" && expression.flags !== "u") + throw new TypeError( + `Pattern /${expression.source}/${expression.flags} has flags JSON Schema cannot declare`, + ); + return expression.source; +}; + +/** + * Check `predicate` and declare it as an `allOf` of patterns built from the + * same expressions, annotated with how the declaration relates to it. + */ +export const declaredByPatterns = ( + schema: Schema, + predicate: (value: string) => boolean, + declaration: PatternDeclaration, + check: { readonly message: string; readonly abort?: boolean }, +): Schema => { + const allOf = [ + ...(declaration.matches ?? []).map((expression) => ({ + pattern: declaredPattern(expression), + })), + ...(declaration.excludes ?? []).map((expression) => ({ + not: { pattern: declaredPattern(expression) }, + })), + ]; + // An empty `allOf` is invalid JSON Schema and declares nothing. + if (allOf.length === 0) + throw new TypeError("A pattern declaration needs at least one pattern"); + return annotatePatternDeclaration( + schema.refine(predicate, check).meta({ allOf }) as Schema, + declaration.relation, + ); +}; diff --git a/src/domain/sqlite/sqliteDatabase.ts b/src/domain/sqlite/sqliteDatabase.ts index 904e2e4488..fb40acfa42 100644 --- a/src/domain/sqlite/sqliteDatabase.ts +++ b/src/domain/sqlite/sqliteDatabase.ts @@ -1,5 +1,6 @@ import { timerDurationMsSchema } from "../analysisResourceLimits.js"; import { z } from "zod"; +import { requirePresence } from "../schemaConstraints.js"; import { absoluteLocalPathStringSchema } from "../localPath.js"; import type { JsonValue } from "../jsonValue.js"; @@ -33,8 +34,8 @@ export const SQLITE_TABLE_REQUIRED_SELECTION_FIELDS = [ ] as const; /** Shared row/schema selection constraints, independent of path transport. */ -export const sqliteTableSelectionSchema = z - .strictObject({ +export const sqliteTableSelectionSchema = requirePresence( + z.strictObject({ table: z .string() .optional() @@ -68,43 +69,21 @@ export const sqliteTableSelectionSchema = z .describe( "Rows to skip before the retained window; default 0; requires table", ), - }) - .superRefine((input, context) => { - if (input.row_limit !== undefined && input.table === undefined) - context.addIssue({ - code: "custom", - path: ["row_limit"], - message: "row_limit requires an explicit table selection", - }); - if (input.row_offset !== undefined && input.table === undefined) - context.addIssue({ - code: "custom", - path: ["row_offset"], - message: "row_offset requires an explicit table selection", - }); - if (input.columns !== undefined && input.table === undefined) - context.addIssue({ - code: "custom", - path: ["columns"], - message: "columns requires an explicit table selection", - }); - if (input.schema_scope === "selected_table" && input.table === undefined) - context.addIssue({ - code: "custom", - path: ["schema_scope"], - message: "selected_table schema scope requires table", - }); - }) - .meta({ - dependentRequired: Object.fromEntries( - SQLITE_TABLE_REQUIRED_SELECTION_FIELDS.map((field) => [field, ["table"]]), - ), - if: { - properties: { schema_scope: { const: "selected_table" } }, - required: ["schema_scope"], + }), + [ + ...SQLITE_TABLE_REQUIRED_SELECTION_FIELDS.map((field) => ({ + when: field, + requires: "table" as const, + message: `${field} requires an explicit table selection`, + })), + { + when: "schema_scope", + equals: "selected_table", + requires: "table", + message: "selected_table schema scope requires table", }, - then: { required: ["table"] }, - }); + ], +); /** Inspect one frozen database and its sibling WAL, with optional ordinary-table rows. */ export const inspectSqliteDatabaseInputSchema = sqliteTableSelectionSchema diff --git a/src/domain/stringPatterns.ts b/src/domain/stringPatterns.ts index 71424570f0..8ae5462f29 100644 --- a/src/domain/stringPatterns.ts +++ b/src/domain/stringPatterns.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { declaredByPatterns } from "./schemaConstraints.js"; + /** Portable ASCII identifier starting with a letter. */ export const IDENTIFIER_PATTERN = /^[A-Za-z][A-Za-z0-9._\x2d]*$/u; @@ -54,22 +56,18 @@ export const isCanonicalBase64 = (value: string): boolean => isPaddedBase64(value) && !INVALID_CANONICAL_BASE64_PAD_BITS_PATTERN.test(value); -const paddedBase64SchemaRules = [ - { pattern: BASE64_QUARTET_LENGTH_PATTERN.source }, - { not: { pattern: INVALID_BASE64_CHARACTER_PATTERN.source } }, - { not: { pattern: INVALID_BASE64_PADDING_PATTERN.source } }, -]; - /** Canonical padded base64 with zero pad bits; invalid bytes never enter enclosing range checks. */ -export const canonicalBase64Schema = z - .string() - .refine(isCanonicalBase64, { - message: "Expected canonical padded base64", - abort: true, - }) - .meta({ - allOf: [ - ...paddedBase64SchemaRules, - { not: { pattern: INVALID_CANONICAL_BASE64_PAD_BITS_PATTERN.source } }, +export const canonicalBase64Schema = declaredByPatterns( + z.string(), + isCanonicalBase64, + { + matches: [BASE64_QUARTET_LENGTH_PATTERN], + excludes: [ + INVALID_BASE64_CHARACTER_PATTERN, + INVALID_BASE64_PADDING_PATTERN, + INVALID_CANONICAL_BASE64_PAD_BITS_PATTERN, ], - }); + relation: "exact", + }, + { message: "Expected canonical padded base64", abort: true }, +); diff --git a/src/domain/webCaptureDiffSchemas.ts b/src/domain/webCaptureDiffSchemas.ts index 025405f8a7..19e977a9be 100644 --- a/src/domain/webCaptureDiffSchemas.ts +++ b/src/domain/webCaptureDiffSchemas.ts @@ -1,18 +1,18 @@ import { z } from "zod"; import { emptyArraySchema } from "./emptyArraySchema.js"; +import { presentCompleteObservation } from "./schemaAnnotations.js"; import { webPageInspectionSchema } from "./browserObservationSchemas.js"; import { webMcpDiscoverySchema } from "./webMcpDiscovery.js"; /** One normalized passive page snapshot accepted by capture comparison. */ export const captureSnapshotSchema = z.object({ - inspection: webPageInspectionSchema - .describe( + inspection: presentCompleteObservation( + webPageInspectionSchema.describe( "Complete normalized_result from inspect_web_page, including capture identity, observations, completeness, and limitations.", - ) - .meta({ "x-rea-complete-observation": true }), - webmcp: webMcpDiscoverySchema - .meta({ "x-rea-complete-observation": true }) + ), + ), + webmcp: presentCompleteObservation(webMcpDiscoverySchema) .nullable() .default(null) .describe( diff --git a/src/main.ts b/src/main.ts index 20cef815d4..f236de436d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -99,12 +99,10 @@ export const run = async ( dependencies.writeStderr(`${projectAnalysisError(config.error).message}\n`); return 1; } - // The recorded catalog carries complete output schemas, which the deferred - // server either lists (full) or omits (off, the default). + // The deferred server omits the recorded output schemas from listings, the + // default `off` profile; `compact` composes its bounded presentation. const recordedOutputCatalog = - environment.REA_MCP_OUTPUT_SCHEMA_PROFILE === undefined || - environment.REA_MCP_OUTPUT_SCHEMA_PROFILE === "off" || - environment.REA_MCP_OUTPUT_SCHEMA_PROFILE === "full"; + config.value.mcpOutputSchemaProfile !== "compact"; // The recorded catalog holds the complete inventory and its full-catalog // identity, so only an unscoped registration can be served from it. A // scoped one composes the canonical scope itself instead of advertising diff --git a/src/main/deferredRuntime.ts b/src/main/deferredRuntime.ts index 91ef2d823e..3ee51ae975 100644 --- a/src/main/deferredRuntime.ts +++ b/src/main/deferredRuntime.ts @@ -9,12 +9,7 @@ import { type RecordedMcpCatalog, } from "../recordedMcpCatalog.js"; import type { AppConfig } from "../config/types.js"; -import { - compactPresentationForMcpInputSchemaProfile, - resolveMcpInputSchemaProfile, - type McpInputSchemaProfile, -} from "../config/mcpInputSchemaProfile.js"; -import { resolveMcpOutputSchemaProfile } from "../config/mcpOutputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../config/mcpInputSchemaProfile.js"; import { RECORDED_CATALOG_DIGEST_PROFILES } from "../recordedCatalogDigests.js"; import { DeferredEvidenceMcpServer } from "../server/DeferredEvidenceMcpServer.js"; import { ToolResultDelivery } from "../server/ToolResultDelivery.js"; @@ -26,16 +21,11 @@ import { reportMcpTransportError } from "./reportMcpTransportError.js"; import { OwnedMcpServers } from "./OwnedMcpServers.js"; /** Verify the build-owned artifact before trusting its exact typed projection. */ -const readCatalog = (profile: McpInputSchemaProfile): RecordedMcpCatalog => { - const bytes = readFileSync( - new URL( - profile === "full" ? "../mcp-catalog-full.json" : "../mcp-catalog.json", - import.meta.url, - ), - ); +const readCatalog = (): RecordedMcpCatalog => { + const bytes = readFileSync(new URL("../mcp-catalog.json", import.meta.url)); if ( createHash("sha256").update(bytes).digest("hex") !== - RECORDED_MCP_CATALOG_SHA256?.[profile] + RECORDED_MCP_CATALOG_SHA256 ) throw new Error(SERVER_START_FAILED); // The digest binds these bytes to the same compiled producer and SDK converter. @@ -138,13 +128,9 @@ export const runDeferred = async ( const serverLogger = createLogger("mcp", config.logLevel).child({ layer: "server", }); - const profile = resolveMcpInputSchemaProfile(config.mcpInputSchemaProfile); - // `main.ts` defers only the profiles the recorded catalog can serve. - const outputSchemaProfile = - resolveMcpOutputSchemaProfile(config.mcpOutputSchemaProfile) === "full" - ? "full" - : "off"; - let firstCatalog: RecordedMcpCatalog | undefined = readCatalog(profile); + // `main.ts` defers only the default output profile, which omits output + // schemas from listings; the recorded catalog keeps them for validation. + let firstCatalog: RecordedMcpCatalog | undefined = readCatalog(); const servers = new OwnedMcpServers(); let handle: McpServerHandle | undefined; let attempt: Promise | undefined; @@ -204,7 +190,7 @@ export const runDeferred = async ( handle = await dependencies.serve( (context) => { const server = servers.create(() => { - const catalog = firstCatalog ?? readCatalog(profile); + const catalog = firstCatalog ?? readCatalog(); firstCatalog = undefined; const startedAt = new Date().toISOString(); const server = new DeferredEvidenceMcpServer( @@ -225,15 +211,12 @@ export const runDeferred = async ( }), { maximumInputElements: config.mcpMaxInputElements, - compactPresentation: - compactPresentationForMcpInputSchemaProfile(profile), - outputSchemaProfile, + compactPresentation: COMPACT_INPUT_SCHEMA_PRESENTATION, identity: { ...catalog.identity, digests: - RECORDED_CATALOG_DIGEST_PROFILES?.[profile][ - outputSchemaProfile - ] ?? catalog.identity.digests, + RECORDED_CATALOG_DIGEST_PROFILES?.off ?? + catalog.identity.digests, }, }, ); diff --git a/src/mcpDoctor.ts b/src/mcpDoctor.ts index db28b59b1e..440d631591 100644 --- a/src/mcpDoctor.ts +++ b/src/mcpDoctor.ts @@ -12,10 +12,6 @@ import { missingCliOutputValueMessage, renderCliDocument, } from "./cliOutput.js"; -import { - compactPresentationForMcpInputSchemaProfile, - parseMcpInputSchemaProfile, -} from "./config/mcpInputSchemaProfile.js"; import { parseMcpOutputSchemaProfile, resolveMcpOutputSchemaProfile, @@ -610,9 +606,6 @@ const targetFreeEnvironment = ( const doctorExpectationForEnvironment = ( environment: Readonly>, ): DoctorCatalogExpectation => { - const inputProfile = parseMcpInputSchemaProfile( - environment.REA_MCP_INPUT_SCHEMA_PROFILE, - ); const outputProfile = parseMcpOutputSchemaProfile( environment.REA_MCP_OUTPUT_SCHEMA_PROFILE, ); @@ -631,9 +624,6 @@ const doctorExpectationForEnvironment = ( ); const progressive = catalogProfile.ok && catalogProfile.value === "hot-cold"; const catalog = createCatalogIdentity({ - compactPresentation: compactPresentationForMcpInputSchemaProfile( - inputProfile.ok ? inputProfile.value : undefined, - ), contracts, outputSchemaProfile: resolveMcpOutputSchemaProfile( outputProfile.ok ? outputProfile.value : undefined, diff --git a/src/recordedCatalogDigests.ts b/src/recordedCatalogDigests.ts index b0fccc34ce..81057b1fe0 100644 --- a/src/recordedCatalogDigests.ts +++ b/src/recordedCatalogDigests.ts @@ -1,9 +1,10 @@ import type { CatalogDigestProfiles } from "./catalogIdentity.js"; /** - * Digests of the six built-in input/output schema profiles, recorded - * from the compiled contracts by `scripts/record-catalog-digests.mjs` after - * compilation. Source runs leave this undefined and compute them on first use. + * Digests of the two built-in output schema profiles over the compact input + * presentation, recorded from the compiled contracts by + * `scripts/record-catalog-digests.mjs` after compilation. Source runs leave + * this undefined and compute them on first use. */ export const RECORDED_CATALOG_DIGEST_PROFILES: | CatalogDigestProfiles diff --git a/src/recordedMcpCatalog.ts b/src/recordedMcpCatalog.ts index 57e399332a..f0c5318e19 100644 --- a/src/recordedMcpCatalog.ts +++ b/src/recordedMcpCatalog.ts @@ -6,7 +6,6 @@ import type { } from "@modelcontextprotocol/server"; import type { createCatalogIdentity } from "./catalogIdentity.js"; import type { PromptArgumentContract } from "./contracts/promptContracts.js"; -import type { McpInputSchemaProfile } from "./config/mcpInputSchemaProfile.js"; /** Build-owned discovery data; canonical validators remain the runtime authority. */ export interface RecordedMcpCatalog { @@ -25,6 +24,4 @@ export interface RecordedMcpCatalog { } /** Replaced after compilation by the recorder; source runs use eager composition. */ -export const RECORDED_MCP_CATALOG_SHA256: - | Readonly> - | undefined = undefined; +export const RECORDED_MCP_CATALOG_SHA256: string | undefined = undefined; diff --git a/src/server/BundleResourcesMcp.test.ts b/src/server/BundleResourcesMcp.test.ts index 125a4d1b38..80f6c3eac9 100644 --- a/src/server/BundleResourcesMcp.test.ts +++ b/src/server/BundleResourcesMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../tests/fixtures/canonicalOutputSchema.js"; import type { Client } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import type { AnySchema } from "ajv"; @@ -440,9 +441,7 @@ const expectResourceSchemaBranches = async (client: Client): Promise => { contract.outputSchema.safeParse(malformedEvidence).success, `${name} canonical rejects missing resource_root`, ).toBe(false); - const advertised = advertisedTools.get(name)?.outputSchema; - expect(advertised, `${name} is advertised`).toBeDefined(); - if (advertised === undefined) continue; + const advertised = canonicalOutputSchema(name); const validateAdvertised = new Ajv2020({ strict: false, validateFormats: false, diff --git a/src/server/DeferredEvidenceMcpServer.test.ts b/src/server/DeferredEvidenceMcpServer.test.ts index c26b53c192..eb0869c30e 100644 --- a/src/server/DeferredEvidenceMcpServer.test.ts +++ b/src/server/DeferredEvidenceMcpServer.test.ts @@ -275,7 +275,10 @@ it("discovers without initialization, then shares one canonical runtime and pres const hold = barrier(); const f = await fixture({ hold }); const before = await f.client.listTools(); - expect(before.tools).toEqual(catalog.tools); + // Listings omit the recorded output schemas (the default `off` profile). + expect(before.tools).toEqual( + catalog.tools.map(({ outputSchema: _output, ...tool }) => tool), + ); expect((await f.client.listPrompts()).prompts).toEqual( catalog.prompts.map(({ parameters: _parameters, ...prompt }) => prompt), ); @@ -322,13 +325,9 @@ it.each(["legacy", "auto"] as const)( async (mode) => { const valid = await fixture({ unionOutput: true, mode }); const before = await valid.client.listTools(); - const schema = before.tools.find( - ({ name }) => name === "fixture", - )?.outputSchema; - if (schema === undefined) throw new Error("Missing union output schema"); - expect(schema).toHaveProperty("anyOf"); - expect(schema).not.toHaveProperty("properties.result"); - if (mode === "legacy") expect(schema.type).toBe("object"); + expect( + before.tools.find(({ name }) => name === "fixture")?.outputSchema, + ).toBeUndefined(); expect(valid.loads()).toBe(0); const success = await valid.client.callTool({ name: "fixture", diff --git a/src/server/DeferredEvidenceMcpServer.ts b/src/server/DeferredEvidenceMcpServer.ts index 66ced19ab2..ebe28fafb5 100644 --- a/src/server/DeferredEvidenceMcpServer.ts +++ b/src/server/DeferredEvidenceMcpServer.ts @@ -66,9 +66,6 @@ export class DeferredEvidenceMcpServer extends EvidenceMcpServerCore { // Recorded tools carry their presented schemas. The selected presentation // still applies to transport discovery, which is not recorded. readonly compactPresentation?: CompactInputSchemaPresentation | undefined; - // Recorded tools carry complete output schemas; `off` omits them from - // listings while the runtime still validates structured results. - readonly outputSchemaProfile?: "full" | "off" | undefined; readonly identity?: RecordedMcpCatalog["identity"] | undefined; } = {}, ) { @@ -84,7 +81,10 @@ export class DeferredEvidenceMcpServer extends EvidenceMcpServerCore { undefined, delivery, options.compactPresentation, - options.outputSchemaProfile ?? "full", + // Recorded tools carry complete output schemas; the default `off` + // profile omits them from listings while the runtime still validates + // structured results. + "off", options.identity ?? catalog.identity, ); for (const name of catalog.evidenceReferenceAlternatives) diff --git a/src/server/EvidenceMcpServer.ts b/src/server/EvidenceMcpServer.ts index f79caa9049..3c1d70e3b6 100644 --- a/src/server/EvidenceMcpServer.ts +++ b/src/server/EvidenceMcpServer.ts @@ -10,7 +10,11 @@ import { } from "../catalogIdentity.js"; import { toolFamilyId } from "../contracts/toolFamilies.js"; import type { ToolResultDelivery } from "./ToolResultDelivery.js"; -import type { McpOutputSchemaProfile } from "../config/mcpOutputSchemaProfile.js"; +import { + resolveMcpOutputSchemaProfile, + type McpOutputSchemaProfile, +} from "../config/mcpOutputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../config/mcpInputSchemaProfile.js"; import { EvidenceMcpServerCore } from "./EvidenceMcpServerCore.js"; import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js"; @@ -21,8 +25,10 @@ export class EvidenceMcpServer extends EvidenceMcpServerCore { options: McpServerOptions, recordEvidence: EvidenceWriter["recordEvidence"] | undefined, delivery: ToolResultDelivery, - compactPresentation: CompactInputSchemaPresentation | undefined = undefined, - outputSchemaProfile: McpOutputSchemaProfile = "full", + compactPresentation: CompactInputSchemaPresentation = COMPACT_INPUT_SCHEMA_PRESENTATION, + outputSchemaProfile: McpOutputSchemaProfile = resolveMcpOutputSchemaProfile( + undefined, + ), identity?: ReturnType, advertisedToolFamilies: ReadonlySet | undefined = undefined, ) { diff --git a/src/server/EvidenceMcpServerCore.ts b/src/server/EvidenceMcpServerCore.ts index 49a73c65d6..f851bcd3a6 100644 --- a/src/server/EvidenceMcpServerCore.ts +++ b/src/server/EvidenceMcpServerCore.ts @@ -36,9 +36,11 @@ import { } from "../contracts/compactInputSchema.js"; import { presentAdvertisedOutputSchema } from "../contracts/compactOutputSchema.js"; import { - compactPresentationForMcpOutputSchemaProfile, + COMPACT_OUTPUT_SCHEMA_PRESENTATION, + resolveMcpOutputSchemaProfile, type McpOutputSchemaProfile, } from "../config/mcpOutputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../config/mcpInputSchemaProfile.js"; import type { createCatalogIdentity } from "../catalogIdentity.js"; import { transformAdvertisedInputJsonSchema } from "../contracts/toolSchemaMetadata.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; @@ -244,22 +246,19 @@ const presentOutputSchemasForToolsList = ( compactedSchemas: WeakMap, Record>, ): JSONRPCMessage => { if ( - profile === "full" || !isJSONRPCResultResponse(message) || message.id !== requestId || !isRecord(message.result) || !Array.isArray(message.result.tools) ) return message; - const compactPresentation = - compactPresentationForMcpOutputSchemaProfile(profile); return { ...message, result: { ...message.result, tools: message.result.tools.map((tool: unknown) => { if (!isRecord(tool)) return tool; - if (compactPresentation === undefined) { + if (profile === "off") { const { outputSchema: _outputSchema, ...advertisedTool } = tool; return advertisedTool; } @@ -268,7 +267,7 @@ const presentOutputSchemasForToolsList = ( if (outputSchema === undefined) { outputSchema = presentAdvertisedOutputSchema( tool.outputSchema, - compactPresentation, + COMPACT_OUTPUT_SCHEMA_PRESENTATION, ); compactedSchemas.set(tool.outputSchema, outputSchema); } @@ -395,9 +394,7 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer { ): RegisteredPrompt { return super.registerPrompt(name, config, callback); } - private readonly compactPresentation: - | CompactInputSchemaPresentation - | undefined; + private readonly compactPresentation: CompactInputSchemaPresentation; private readonly maximumInputElements: number; private readonly evidenceReferenceAlternatives = new Set(); @@ -410,8 +407,10 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer { options: McpServerOptions, private recordEvidence: EvidenceWriter["recordEvidence"] | undefined, public delivery: ToolResultDelivery, - compactPresentation: CompactInputSchemaPresentation | undefined = undefined, - private readonly outputSchemaProfile: McpOutputSchemaProfile = "full", + compactPresentation: CompactInputSchemaPresentation = COMPACT_INPUT_SCHEMA_PRESENTATION, + private readonly outputSchemaProfile: McpOutputSchemaProfile = resolveMcpOutputSchemaProfile( + undefined, + ), identity?: ReturnType, readonly advertisedToolFamilies: | ReadonlySet @@ -421,10 +420,9 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer { options.maxToolInputElements ?? DEFAULT_MCP_MAX_INPUT_ELEMENTS; super(info, { ...options, maxToolInputElements: maximumInputElements }); this.maximumInputElements = maximumInputElements; - this.compactPresentation = - compactPresentation === undefined - ? undefined - : { budgetBytes: compactPresentation.budgetBytes }; + this.compactPresentation = { + budgetBytes: compactPresentation.budgetBytes, + }; this.identity = identity; } @@ -513,8 +511,9 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer { /** * The input schema and tool description this server advertises for one - * registration: the selected compact presentation, whose guidance joins - * the description, or both unchanged. Every listed tool, including + * registration: the compact presentation, whose guidance joins the + * description. A registration without an input schema is unchanged. Every + * listed tool, including * transport discovery, takes this one path. */ private presentInput( @@ -523,11 +522,7 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer { ): { description?: string; inputSchema?: Schema } { const { description, inputSchema } = config; const presentation = this.compactPresentation; - if ( - presentation === undefined || - inputSchema === undefined || - !hasAdvertisedInputJsonSchema(inputSchema) - ) + if (inputSchema === undefined || !hasAdvertisedInputJsonSchema(inputSchema)) return config; const present = (projected: Record) => presentAdvertisedInput(projected, presentation); @@ -785,9 +780,7 @@ class ErrorEvidenceTransport implements Transport { : (this.pendingValidation.get(id) ?? (scopedRequest?.id === id ? scopedRequest : undefined)); const rewritesOutputSchemas = - this.outputSchemaProfile !== "full" && - id !== undefined && - this.pendingToolLists.has(id); + id !== undefined && this.pendingToolLists.has(id); try { const publish = async (): Promise => { const recovered = this.recoverProtocolError(message, tool); diff --git a/src/server/ProgressiveToolCatalog.test.ts b/src/server/ProgressiveToolCatalog.test.ts index 03ef853e2d..c69b43b3d5 100644 --- a/src/server/ProgressiveToolCatalog.test.ts +++ b/src/server/ProgressiveToolCatalog.test.ts @@ -24,7 +24,7 @@ const connect = async ( undefined, new ToolResultDelivery(options.replyBytes ?? 1024 * 1024), undefined, - "full", + undefined, { counts: { cli_commands: 0, diff --git a/src/server/ProgressiveToolCatalog.ts b/src/server/ProgressiveToolCatalog.ts index 719e32c0d8..55b5ff685f 100644 --- a/src/server/ProgressiveToolCatalog.ts +++ b/src/server/ProgressiveToolCatalog.ts @@ -145,7 +145,7 @@ export class ProgressiveToolCatalog { }, { remediationAction: - "Reduce limit or request fewer exact names, then retry. For one oversized input contract, reconnect with REA_MCP_INPUT_SCHEMA_PROFILE=compact.", + "Reduce limit or request fewer exact names, then retry.", }, ), false, diff --git a/src/server/createServer.ts b/src/server/createServer.ts index 704e0b444f..91bf849d02 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -61,7 +61,7 @@ import { type McpOutputSchemaProfile, } from "../config/mcpOutputSchemaProfile.js"; import { - compactPresentationForMcpInputSchemaProfile, + COMPACT_INPUT_SCHEMA_PRESENTATION, parseMcpInputSchemaProfile, } from "../config/mcpInputSchemaProfile.js"; import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js"; @@ -129,7 +129,7 @@ export interface CreateServerOptions extends CapabilityServiceOptions { const createMcpServer = ( session: BinarySessionPort | undefined, delivery: ToolResultDelivery, - compactPresentation: CompactInputSchemaPresentation | undefined, + compactPresentation: CompactInputSchemaPresentation, outputSchemaProfile: McpOutputSchemaProfile, advertisedToolFamilies: ReadonlySet | undefined, inputElementBudget: string | undefined, @@ -927,14 +927,15 @@ const outputSchemaProfile = ( return resolveMcpOutputSchemaProfile(configured.value); }; +/** Only `compact` remains; the setting is still parsed so a removed value fails closed. */ const selectCompactInputSchemaPresentation = ( environment: Readonly, -): CompactInputSchemaPresentation | undefined => { +): CompactInputSchemaPresentation => { const configured = parseMcpInputSchemaProfile( environment.REA_MCP_INPUT_SCHEMA_PROFILE, ); if (!configured.ok) throw configured.error; - return compactPresentationForMcpInputSchemaProfile(configured.value); + return COMPACT_INPUT_SCHEMA_PRESENTATION; }; const createSessionRecorders = ( diff --git a/src/server/registerGuidedWorkflowTool.test.ts b/src/server/registerGuidedWorkflowTool.test.ts index e237ca208f..6cb72220b6 100644 --- a/src/server/registerGuidedWorkflowTool.test.ts +++ b/src/server/registerGuidedWorkflowTool.test.ts @@ -28,7 +28,7 @@ const fixture = async (mcp: TestMcp, families?: ReadonlySet) => { undefined, new ToolResultDelivery(1024 * 1024), undefined, - "full", + undefined, undefined, families, ); diff --git a/src/setupEnvironment.test.ts b/src/setupEnvironment.test.ts index 2f79c0d2c1..65f6aff9ab 100644 --- a/src/setupEnvironment.test.ts +++ b/src/setupEnvironment.test.ts @@ -198,7 +198,7 @@ it.each([ REA_BRIDGE_MAX_REQUEST_BYTES: "33554432", REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700000", REA_HOPPER_STARTUP_TIMEOUT_MS: "120000", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", + REA_MCP_INPUT_SCHEMA_PROFILE: "compact", REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold", REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "256", REA_REGEX_MATCHING_DEADLINE_MS: "10000", @@ -242,7 +242,7 @@ it.each([ REA_BRIDGE_MAX_REQUEST_BYTES: "33554432", REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700000", REA_HOPPER_STARTUP_TIMEOUT_MS: "120000", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", + REA_MCP_INPUT_SCHEMA_PROFILE: "compact", REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold", REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "256", REA_REGEX_MATCHING_DEADLINE_MS: "10000", diff --git a/tests/acceptance/analysisSettingsMcp.test.ts b/tests/acceptance/analysisSettingsMcp.test.ts index 5e2bb51de0..6ec5afd93e 100644 --- a/tests/acceptance/analysisSettingsMcp.test.ts +++ b/tests/acceptance/analysisSettingsMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { analysisSettingsResultSchema } from "../../src/contracts/analysisSettingsContracts.js"; import { expect, it } from "vitest"; import type { AnySchema } from "ajv"; @@ -111,7 +112,9 @@ it("advertises exact contracts and recovers a full large-index archive over one const tool = catalog.tools.find((tool) => tool.name === name)!; expect(tool).toBeDefined(); expect(ajv.validateSchema(tool.inputSchema as AnySchema)).toBe(true); - expect(ajv.validateSchema(tool.outputSchema as AnySchema)).toBe(true); + expect( + ajv.validateSchema(canonicalOutputSchema(tool.name) as AnySchema), + ).toBe(true); } const initial = await invoke("inspect_analysis_settings", { settings: ["asar.max_header_bytes"], @@ -232,9 +235,11 @@ it.each(["0", "2147483648"])( const tool = (await client.listTools()).tools.find( (entry) => entry.name === "inspect_analysis_settings", ); - expect(tool?.outputSchema).toBeDefined(); + expect(tool).toBeDefined(); const ajv = new Ajv2020({ strict: false }); - const validate = ajv.compile(tool!.outputSchema as AnySchema); + const validate = ajv.compile( + canonicalOutputSchema(tool!.name) as AnySchema, + ); expect( validate(response.structuredContent), JSON.stringify(validate.errors), diff --git a/tests/acceptance/apkSignatureV1Cli.test.ts b/tests/acceptance/apkSignatureV1Cli.test.ts index 1f6bd1a33b..b937ff5f77 100644 --- a/tests/acceptance/apkSignatureV1Cli.test.ts +++ b/tests/acceptance/apkSignatureV1Cli.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { apkSignatureResultSchema } from "../../src/domain/apkSignature/apkSignatureReport.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -223,10 +224,9 @@ cliTest( const tool = (await client.listTools()).tools.find( (t) => t.name === "inspect_apk_signature", ); - const outputSchema: Record | undefined = - tool?.outputSchema; - if (tool === undefined || outputSchema === undefined) - throw new Error("inspect_apk_signature must advertise an output schema"); + if (tool === undefined) + throw new Error("inspect_apk_signature must be advertised"); + const outputSchema = canonicalOutputSchema(tool.name); const ajv = new Ajv2020({ strict: false, validateFormats: false }); for (const [path, fromCli] of [ [v1Only, cliV1], diff --git a/tests/acceptance/applications/apiContract.test.ts b/tests/acceptance/applications/apiContract.test.ts index d5b69828cb..67649bc158 100644 --- a/tests/acceptance/applications/apiContract.test.ts +++ b/tests/acceptance/applications/apiContract.test.ts @@ -228,16 +228,17 @@ cliTest( ); await writeFile(path, bytes); const { client, call } = await connectLocalToolsMcp({ - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_TOOL_FAMILIES: "application,session", }); const schemas = await advertisedMcpToolSchemas( client, "inspect_api_contract", ); - expect(schemas.ajv.validate(schemas.inputSchema, { path })).toBe(true); + expect(schemas.ajv.validate(schemas.canonicalInputSchema, { path })).toBe( + true, + ); expect( - schemas.ajv.validate(schemas.inputSchema, { + schemas.ajv.validate(schemas.canonicalInputSchema, { path, view: { kind: "api-contract", diff --git a/tests/acceptance/applications/applicationPromiseHandleCliMcp.test.ts b/tests/acceptance/applications/applicationPromiseHandleCliMcp.test.ts index 6e4d110028..2e9d63aa2d 100644 --- a/tests/acceptance/applications/applicationPromiseHandleCliMcp.test.ts +++ b/tests/acceptance/applications/applicationPromiseHandleCliMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalInputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Ajv2020 } from "ajv/dist/2020.js"; import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; @@ -14,8 +15,6 @@ cliTest.for(["promise", "awaited", "reassigned", "opaque"] as const)( async (mode, { cli }) => { const { client, call } = await connectLocalToolsMcp({ REA_MCP_TOOL_FAMILIES: "electron", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }); const definition = (await client.listTools()).tools.find( ({ name }) => name === "analyze_javascript_application", @@ -25,7 +24,7 @@ cliTest.for(["promise", "awaited", "reassigned", "opaque"] as const)( const validate = new Ajv2020({ strict: false, validateFormats: false, - }).compile(definition.inputSchema); + }).compile(canonicalInputSchema(definition.name)); const root = await createTestTempDirectory("rea-promise-handle-"); const bodies = { promise: 'const handle = open("input.txt", "r");', diff --git a/tests/acceptance/applications/firmwareComparison.test.ts b/tests/acceptance/applications/firmwareComparison.test.ts index e3dc9fb2fc..cd2a8e7119 100644 --- a/tests/acceptance/applications/firmwareComparison.test.ts +++ b/tests/acceptance/applications/firmwareComparison.test.ts @@ -80,7 +80,6 @@ cliTest( await writeFile(rightPath, rightBytes); const { call } = await connectLocalToolsMcp({ REA_MCP_TOOL_FAMILIES: "session,artifact,application", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", }); const left = await inspect(call, leftPath), right = await inspect(call, rightPath); diff --git a/tests/acceptance/applications/graphqlContract.test.ts b/tests/acceptance/applications/graphqlContract.test.ts index 0813b95bb7..d41f91527e 100644 --- a/tests/acceptance/applications/graphqlContract.test.ts +++ b/tests/acceptance/applications/graphqlContract.test.ts @@ -127,7 +127,7 @@ const checkAdvertisedSchema = async ( client, "inspect_api_contract", ); - expect(schemas.ajv.validate(schemas.inputSchema, input)).toBe(true); + expect(schemas.ajv.validate(schemas.canonicalInputSchema, input)).toBe(true); for (const invalid of [ { ...input, format: "openapi-json" }, { ...input, view: { kind: "api-contract", collection: "operations" } }, @@ -140,7 +140,9 @@ const checkAdvertisedSchema = async ( }, }, ]) - expect(schemas.ajv.validate(schemas.inputSchema, invalid)).toBe(false); + expect(schemas.ajv.validate(schemas.canonicalInputSchema, invalid)).toBe( + false, + ); }; cliTest( @@ -156,7 +158,6 @@ cliTest( await writeFile(path, bytes); await writeFile(operationsPath, operations); const { client, call } = await connectLocalToolsMcp({ - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_TOOL_FAMILIES: "application,session", }); const input = { diff --git a/tests/acceptance/applications/peResources.test.ts b/tests/acceptance/applications/peResources.test.ts index 29560a42c8..af76a8676f 100644 --- a/tests/acceptance/applications/peResources.test.ts +++ b/tests/acceptance/applications/peResources.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client } from "@modelcontextprotocol/client"; import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; @@ -435,7 +436,7 @@ cliTest( const call: PeResourceCall = (arguments_) => client.callTool( { name: definition.name, arguments: arguments_ }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); const sessionDefinition = inventory.tools.find( ({ name }) => name === "binary_session", @@ -444,7 +445,7 @@ cliTest( throw new Error("Missing binary_session tool contract"); const session = await client.callTool( { name: sessionDefinition.name, arguments: {} }, - { toolDefinition: sessionDefinition }, + { toolDefinition: withCanonicalOutputSchema(sessionDefinition) }, ); expect(session.isError, JSON.stringify(session)).not.toBe(true); const status = toolContract("binary_session").outputSchema.parse( diff --git a/tests/acceptance/applications/playwrightTrace.test.ts b/tests/acceptance/applications/playwrightTrace.test.ts index 9c3b72ae25..99f016c1c6 100644 --- a/tests/acceptance/applications/playwrightTrace.test.ts +++ b/tests/acceptance/applications/playwrightTrace.test.ts @@ -164,7 +164,6 @@ cliTest( const fixture = await playwrightTraceFixture({ resources: true }); const { client, call } = await connectLocalToolsMcp({ REA_MCP_TOOL_FAMILIES: "application,session", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", PLAYWRIGHT_BROWSERS_PATH: "/rea-no-browser-required", }); const schemas = await advertisedMcpToolSchemas( @@ -172,10 +171,12 @@ cliTest( "inspect_playwright_trace", ); expect( - schemas.ajv.validate(schemas.inputSchema, { path: fixture.path }), + schemas.ajv.validate(schemas.canonicalInputSchema, { + path: fixture.path, + }), ).toBe(true); expect( - schemas.ajv.validate(schemas.inputSchema, { + schemas.ajv.validate(schemas.canonicalInputSchema, { path: fixture.path, view: { kind: "playwright-trace", diff --git a/tests/acceptance/applications/progressiveAnalysisViews.test.ts b/tests/acceptance/applications/progressiveAnalysisViews.test.ts index 9118adf944..7dd06618a9 100644 --- a/tests/acceptance/applications/progressiveAnalysisViews.test.ts +++ b/tests/acceptance/applications/progressiveAnalysisViews.test.ts @@ -7,8 +7,6 @@ import { parseEvidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.js"; import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; -import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { advertisedOutputJsonSchema } from "../../../src/contracts/toolSchemaMetadata.js"; it("loads retained analysis views only when needed, preserving complete schemas, source facts and Evidence", async () => { const root = await createTestTempDirectory("rea-progressive-api-"); @@ -60,9 +58,9 @@ it("loads retained analysis views only when needed, preserving complete schemas, const loaded = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_analysis_view", ); - expect(loaded?.outputSchema).toEqual( - advertisedOutputJsonSchema(toolContract("inspect_analysis_view")), - ); + expect(loaded).toBeDefined(); + // The default output profile omits output schemas from every listing. + expect(loaded?.outputSchema).toBeUndefined(); expect(changed).toHaveBeenCalledTimes(2); const source = { kind: "retained-evidence", diff --git a/tests/acceptance/applications/runtime.test.ts b/tests/acceptance/applications/runtime.test.ts index f201124f23..0bf957ac37 100644 --- a/tests/acceptance/applications/runtime.test.ts +++ b/tests/acceptance/applications/runtime.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { fileURLToPath } from "node:url"; import { Client } from "@modelcontextprotocol/client"; @@ -5,7 +6,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { afterAll, describe, expect, it } from "vitest"; import { z } from "zod"; -import { FULL_CATALOG_IDENTITY } from "../../../src/catalogIdentity.js"; +import { DEFAULT_CATALOG_IDENTITY } from "../../../src/catalogIdentity.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { evidenceResultOf, @@ -38,7 +39,7 @@ const expectAvailableToolInventory = async ( throw new Error("The compiled catalog omitted binary_session"); const status = await client.callTool( { name: "binary_session", arguments: {} }, - { toolDefinition: binarySession }, + { toolDefinition: withCanonicalOutputSchema(binarySession) }, ); expect(status.isError).not.toBe(true); const inventory = z @@ -56,7 +57,7 @@ const expectAvailableToolInventory = async ( expect( availability.length, "compiled tool inventory is stale; run npm run build:cached", - ).toBe(FULL_CATALOG_IDENTITY.counts.mcp_tools); + ).toBe(DEFAULT_CATALOG_IDENTITY.counts.mcp_tools); expect(new Set(listed.tools.map(({ name }) => name))).toEqual( new Set(TOOL_CONTRACTS.map(({ name }) => name)), ); @@ -228,7 +229,7 @@ describe("production stdio runtime", () => { throw new Error("The compiled catalog omitted close_binary"); const closed = await client.callTool( { name: "close_binary", arguments: {} }, - { toolDefinition: closeBinary }, + { toolDefinition: withCanonicalOutputSchema(closeBinary) }, ); expect(closed.isError).not.toBe(true); } finally { diff --git a/tests/acceptance/applications/staticObservedEndpoints.test.ts b/tests/acceptance/applications/staticObservedEndpoints.test.ts index 9905ce3b2a..ca5c89f33e 100644 --- a/tests/acceptance/applications/staticObservedEndpoints.test.ts +++ b/tests/acceptance/applications/staticObservedEndpoints.test.ts @@ -298,16 +298,20 @@ cliTest( ); expect(JSON.stringify(report)).not.toMatch(/kept-source-only|fragment|KEY/); const { client, call } = await connectLocalToolsMcp({ - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_TOOL_FAMILIES: "application,session", }); const schemas = await advertisedMcpToolSchemas( client, "compare_static_and_observed_endpoints", ); - expect(schemas.ajv.validate(schemas.inputSchema, input)).toBe(true); + expect(schemas.ajv.validate(schemas.canonicalInputSchema, input)).toBe( + true, + ); expect( - schemas.ajv.validate(schemas.inputSchema, { ...input, detail: "wrong" }), + schemas.ajv.validate(schemas.canonicalInputSchema, { + ...input, + detail: "wrong", + }), ).toBe(false); const retained = { application: { diff --git a/tests/acceptance/artifactExtractionSelection.test.ts b/tests/acceptance/artifactExtractionSelection.test.ts index b8223a7d5a..365e3c3c76 100644 --- a/tests/acceptance/artifactExtractionSelection.test.ts +++ b/tests/acceptance/artifactExtractionSelection.test.ts @@ -20,7 +20,6 @@ import { artifactInspectionResultSchema } from "../../src/domain/artifactInspect import { parseEvidence } from "../../src/domain/evidence.js"; import { advertisedMcpToolSchemas } from "../fixtures/advertisedMcpTool.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; -import { schemaVerifierEnvironment } from "../../scripts/lib/mcp-verification-environment.mjs"; const execute = promisify(execFile); const cli = fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url)); @@ -123,11 +122,10 @@ it("keeps CLI/MCP selection identity, invalid-input refusals and committed outpu const transport = new StdioClientTransport({ command: process.execPath, args: [cli, "mcp"], - env: schemaVerifierEnvironment({ + env: { ...data.environment, - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_TOOL_FAMILIES: "artifact,session", - }), + }, stderr: "pipe", }); const client = new Client({ @@ -145,14 +143,14 @@ it("keeps CLI/MCP selection identity, invalid-input refusals and committed outpu const schemas = await advertisedMcpToolSchemas(client, "extract_artifact"); for (const occurrence_ids of [[id], [id, data.occurrence("lib/other.so")]]) expect( - schemas.ajv.validate(schemas.inputSchema, { + schemas.ajv.validate(schemas.canonicalInputSchema, { output_root: output, occurrence_ids, }), JSON.stringify(schemas.ajv.errors), ).toBe(true); expect( - schemas.ajv.validate(schemas.inputSchema, { + schemas.ajv.validate(schemas.canonicalInputSchema, { output_root: output, occurrence_ids: [id, id], }), diff --git a/tests/acceptance/harmonyArtifactJourney.test.ts b/tests/acceptance/harmonyArtifactJourney.test.ts index a0f93c5f01..571e2cd27c 100644 --- a/tests/acceptance/harmonyArtifactJourney.test.ts +++ b/tests/acceptance/harmonyArtifactJourney.test.ts @@ -16,7 +16,6 @@ import { type HarmonyApplicationProjectionResult, } from "../../src/domain/harmony/harmonyApplication.js"; import { workspaceCliTest as test } from "../support/cli/workspaceCliFixture.js"; -import { schemaVerifierEnvironment } from "../../scripts/lib/mcp-verification-environment.mjs"; const expectProjectionParity = ( graph: HarmonyApplicationProjectionResult, @@ -126,7 +125,7 @@ test("preserves Harmony archive formats through the public CLI/MCP lifecycle", a const transport = new StdioClientTransport({ command: process.execPath, args: [resolve("scripts/rea.mjs"), "mcp"], - env: schemaVerifierEnvironment(environment), + env: environment, stderr: "pipe", }); try { diff --git a/tests/acceptance/javascriptRoutesCli.test.ts b/tests/acceptance/javascriptRoutesCli.test.ts index dfb744b66e..24df71797a 100644 --- a/tests/acceptance/javascriptRoutesCli.test.ts +++ b/tests/acceptance/javascriptRoutesCli.test.ts @@ -334,7 +334,7 @@ cliTest( } }, ); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "serves retained route batches through the %s production SDK schemas", async (profile) => { const path = await fixture(), diff --git a/tests/acceptance/managedMemberRecovery.test.ts b/tests/acceptance/managedMemberRecovery.test.ts index 9ec070eb41..c183595130 100644 --- a/tests/acceptance/managedMemberRecovery.test.ts +++ b/tests/acceptance/managedMemberRecovery.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { artifactMcpCall } from "../../scripts/lib/artifact-e2e.mjs"; import { createHash } from "node:crypto"; import { unlink, writeFile } from "node:fs/promises"; @@ -26,7 +27,6 @@ const createTransport = () => env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", ...(process.platform === "win32" ? { SystemRoot: process.env.SystemRoot ?? "", @@ -55,9 +55,11 @@ cliTest( (tool) => tool.name === "inspect_managed_members", ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - if (definition?.outputSchema === undefined) + if (definition === undefined) throw new Error("Managed member schema is absent"); - const outputSchema: Record = definition.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); expect(ajv.validateSchema(definition.inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const output = await cli.run({ diff --git a/tests/acceptance/providers/jebConfigurationIsolation.test.ts b/tests/acceptance/providers/jebConfigurationIsolation.test.ts index e2e3e80e3f..713367e5e2 100644 --- a/tests/acceptance/providers/jebConfigurationIsolation.test.ts +++ b/tests/acceptance/providers/jebConfigurationIsolation.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { withVerifierCleanup } from "../../../scripts/lib/verifier-cleanup.mjs"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; @@ -92,7 +93,7 @@ test("retains the selected endpoint when a direct JEB connection fails", async ( const refusal = parseMcpToolError( await client.callTool( { name: "inspect_jeb_client", arguments: {} }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ), ); expect(refusal.error).toMatchObject({ @@ -169,7 +170,7 @@ test("starts production stdio MCP and retains peer requests around a JEB configu const refusal = parseMcpToolError( await client.callTool( { name: "inspect_jeb_client", arguments: {} }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ), ); expect(refusal.error).toMatchObject({ diff --git a/tests/acceptance/retainedComparisonAggregation.test.ts b/tests/acceptance/retainedComparisonAggregation.test.ts index 0e7792e7a4..35b6181467 100644 --- a/tests/acceptance/retainedComparisonAggregation.test.ts +++ b/tests/acceptance/retainedComparisonAggregation.test.ts @@ -87,7 +87,7 @@ it("recovers a missing retained closure by importing the originating bundle", as expect(parseEvidence(retained.structuredContent)).toEqual(aggregate); }); -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "aggregates retained observations within the %s input profile budget", async (profile) => { // Distinct authenticated observations may have identical derived contents. diff --git a/tests/acceptance/setup/dshClientSetup.test.ts b/tests/acceptance/setup/dshClientSetup.test.ts index f350df1460..e8cd54e0fc 100644 --- a/tests/acceptance/setup/dshClientSetup.test.ts +++ b/tests/acceptance/setup/dshClientSetup.test.ts @@ -8,7 +8,6 @@ import { parse } from "yaml"; import { z } from "zod"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; -import { schemaVerifierEnvironment } from "../../../scripts/lib/mcp-verification-environment.mjs"; cliTest.for(["setup", "uninstall"] as const)( "DSH CLI %s refuses a multi-document patch without losing caller content", @@ -309,10 +308,10 @@ async function verifyGeneratedLauncher( command: registration.command, args: registration.args, cwd: home, - env: schemaVerifierEnvironment({ + env: { PATH: process.env.PATH ?? "", ...environment, - }), + }, stderr: "pipe", }); const client = new Client({ name: "dsh-setup-lifecycle", version: "1" }); diff --git a/tests/acceptance/setup/launcherAliasesCli.test.ts b/tests/acceptance/setup/launcherAliasesCli.test.ts index fe0fff184a..8864b3923f 100644 --- a/tests/acceptance/setup/launcherAliasesCli.test.ts +++ b/tests/acceptance/setup/launcherAliasesCli.test.ts @@ -9,7 +9,6 @@ import { z } from "zod"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; -import { schemaVerifierEnvironment } from "../../../scripts/lib/mcp-verification-environment.mjs"; cliTest.skipIf(process.platform === "win32")( "migrates an owned shebang registration and starts its generated MCP command without a runtime on PATH", @@ -106,7 +105,7 @@ cliTest.skipIf(process.platform === "win32")( command: registration.command, args: registration.args, cwd: home, - env: schemaVerifierEnvironment({ ...environment, ...registration.env }), + env: { ...environment, ...registration.env }, stderr: "pipe", }); const client = new Client({ name: "setup-interpreter", version: "1" }); diff --git a/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts b/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts index d06eaa754b..89e3013109 100644 --- a/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts +++ b/tests/acceptance/setup/registrationRuntimeEnvironmentCli.test.ts @@ -681,7 +681,7 @@ cliTest( REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700001", REA_HOPPER_STARTUP_TIMEOUT_MS: "2800002", REA_JEB_MCP_TIMEOUT_MS: "2900003", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", + REA_MCP_INPUT_SCHEMA_PROFILE: "compact", REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "256", REA_REGEX_MATCHING_DEADLINE_MS: "10000", REA_REGEX_WORKER_HEAP_REQUEST_MIB: "256", @@ -742,7 +742,7 @@ cliTest( REA_GHIDRA_STARTUP_TIMEOUT_MS: "2700001", REA_HOPPER_STARTUP_TIMEOUT_MS: "2800002", REA_JEB_MCP_TIMEOUT_MS: "2900003", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", + REA_MCP_INPUT_SCHEMA_PROFILE: "compact", REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "256", REA_REGEX_MATCHING_DEADLINE_MS: "10000", REA_REGEX_WORKER_HEAP_REQUEST_MIB: "256", diff --git a/tests/acceptance/toolFamiliesMcp.test.ts b/tests/acceptance/toolFamiliesMcp.test.ts index 0accc4cb53..8f967f924e 100644 --- a/tests/acceptance/toolFamiliesMcp.test.ts +++ b/tests/acceptance/toolFamiliesMcp.test.ts @@ -3,7 +3,7 @@ import { expect, it } from "vitest"; import { toolContract } from "../../src/contracts/toolContracts.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; -it.each(["full", "compact", "off"] as const)( +it.each(["compact", "off"] as const)( "keeps real stdio discovery, status identity and disabled calls in the same family scope (%s)", async (outputProfile) => { const { client, call } = await connectLocalToolsMcp({ diff --git a/tests/acceptance/tools/nativeFunctionMatching.test.ts b/tests/acceptance/tools/nativeFunctionMatching.test.ts index d13cda3ee6..995722cc39 100644 --- a/tests/acceptance/tools/nativeFunctionMatching.test.ts +++ b/tests/acceptance/tools/nativeFunctionMatching.test.ts @@ -238,7 +238,7 @@ const verifyPortableCli = async ( }); }; -cliTest.for([{ profile: "full" }, { profile: "compact" }] as const)( +cliTest.for([{ profile: "compact" }] as const)( "shared public CLI/MCP matching retains sources, pages exact facts, and feeds selected dossier pairs directly to compare_functions ($profile)", async ({ profile }, { cli }) => { const fixture = await createMatchingFixture(); @@ -692,7 +692,7 @@ cliTest( }, ); -cliTest.for([{ profile: "full" }, { profile: "compact" }] as const)( +cliTest.for([{ profile: "compact" }] as const)( "ties byte qualifications to selected observations through paged and offline MCP ($profile)", async ({ profile }) => { const left = nativeMatchingInventory("1", [ diff --git a/tests/acceptance/wasmInterfaceComparison.test.ts b/tests/acceptance/wasmInterfaceComparison.test.ts index 75a39c7745..1b54fdf0db 100644 --- a/tests/acceptance/wasmInterfaceComparison.test.ts +++ b/tests/acceptance/wasmInterfaceComparison.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -98,9 +99,10 @@ cliTest( const tool = (await client.listTools()).tools.find( (t) => t.name === "compare_wasm_interfaces", ); - if (tool?.outputSchema === undefined) - throw new Error("Missing comparison schema"); - const outputSchema: Record = tool.outputSchema; + if (tool === undefined) throw new Error("Missing comparison schema"); + const outputSchema: Record = canonicalOutputSchema( + tool.name, + ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validate(tool.inputSchema, input)).toBe(true); expect(ajv.validate(tool.inputSchema, { left, right })).toBe(false); diff --git a/tests/boundary/browser/sazWebCapture.test.ts b/tests/boundary/browser/sazWebCapture.test.ts index 4ebe004e19..3a228c83ab 100644 --- a/tests/boundary/browser/sazWebCapture.test.ts +++ b/tests/boundary/browser/sazWebCapture.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { createHash } from "node:crypto"; import { execFile } from "node:child_process"; import { readFile, unlink, writeFile } from "node:fs/promises"; @@ -515,15 +516,14 @@ it("inspects native SAZ through the public CLI and MCP with equal evidence, byte const tool = (await client.listTools()).tools.find( (item) => item.name === "inspect_web_network_capture", ); - if (tool?.outputSchema === undefined) - throw new Error("Missing capture schema"); + if (tool === undefined) throw new Error("Missing capture schema"); const args = { capture_path: path, format: "saz", record_ordinals: [1, 0] }; const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validate(tool.inputSchema, args)).toBe(true); const response = await call("inspect_web_network_capture", args); expect(response.isError, JSON.stringify(response)).not.toBe(true); expect(getValidator).toHaveBeenCalled(); - const expectedSchema = JSON.stringify(tool.outputSchema); + const expectedSchema = JSON.stringify(canonicalOutputSchema(tool.name)); expect( getValidator.mock.calls.some( ([schema]) => JSON.stringify(schema) === expectedSchema, diff --git a/tests/boundary/cli/artifactComparisonSources.test.ts b/tests/boundary/cli/artifactComparisonSources.test.ts index fdfc33b756..3ecd058b8e 100644 --- a/tests/boundary/cli/artifactComparisonSources.test.ts +++ b/tests/boundary/cli/artifactComparisonSources.test.ts @@ -29,7 +29,7 @@ const retained = (evidenceId: string) => ({ /** Two archives inspected in one stdio MCP session. */ const inspectedPair = async ( prefix: string, - profile: "full" | "compact" = "compact", + profile: "compact" = "compact", ) => { const root = await createTestTempDirectory(prefix); const one = join(root, "one.zip"); @@ -58,7 +58,7 @@ const inspectedPair = async ( return { root, one, call, client, inspect, left, right }; }; -cliTest.for(["full", "compact"] as const)( +cliTest.for(["compact"] as const)( "gives one comparison for retained, inline, file and mixed sources through MCP and the CLI (%s)", async (profile, { cli }) => { const { root, one, call, client, inspect, left, right } = diff --git a/tests/boundary/cli/directAnalysisStatus.test.ts b/tests/boundary/cli/directAnalysisStatus.test.ts index 28ff7c949c..5ef68f2063 100644 --- a/tests/boundary/cli/directAnalysisStatus.test.ts +++ b/tests/boundary/cli/directAnalysisStatus.test.ts @@ -127,7 +127,6 @@ it.each(["capabilities", "providers"])( readConfiguration: () => parseConfig({ REA_MCP_TOOL_FAMILIES: " session,session ", - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_OUTPUT_SCHEMA_PROFILE: "compact", }), createBinarySession: () => session, diff --git a/tests/boundary/cli/workflowInputIdentities.test.ts b/tests/boundary/cli/workflowInputIdentities.test.ts index 1c092dd8f7..06bddf4a78 100644 --- a/tests/boundary/cli/workflowInputIdentities.test.ts +++ b/tests/boundary/cli/workflowInputIdentities.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { INLINE_EVIDENCE_MCP_ENVIRONMENT } from "../../../scripts/lib/mcp-verification-environment.mjs"; @@ -54,7 +55,6 @@ const runJson = async ( cwd: root, environment: { REA_LOG_LEVEL: "silent", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, }); }; @@ -76,7 +76,6 @@ const connectMcp = async (root: string) => { XDG_CONFIG_HOME: join(root, "config"), XDG_CACHE_HOME: join(root, "cache"), REA_LOG_LEVEL: "silent", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", ...INLINE_EVIDENCE_MCP_ENVIRONMENT, }, }); @@ -376,10 +375,11 @@ cliTest( "trace_application_feature", ]) { const definition = tools.get(name); - if (definition?.outputSchema === undefined) - throw new Error("Missing workflow schema"); + if (definition === undefined) throw new Error("Missing workflow schema"); expect(ajv.validateSchema(definition.inputSchema)).toBe(true); - const { $schema, ...outputKeywords } = definition.outputSchema; + const { $schema, ...outputKeywords } = canonicalOutputSchema( + definition.name, + ); expect( ajv.validateSchema( $schema === undefined diff --git a/tests/boundary/filesystem/codexClientPolicy.test.ts b/tests/boundary/filesystem/codexClientPolicy.test.ts index d55ed655c5..3a9809bf30 100644 --- a/tests/boundary/filesystem/codexClientPolicy.test.ts +++ b/tests/boundary/filesystem/codexClientPolicy.test.ts @@ -48,7 +48,7 @@ it.each(["tables", "inline table"] as const)( currentClientRegistrationCommand(), ), ).toEqual({ status: "already_current" }); - const overrides = { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }; + const overrides = { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" }; expect( await configureClientConfiguration( client, diff --git a/tests/boundary/filesystem/dependencyInstall.test.ts b/tests/boundary/filesystem/dependencyInstall.test.ts index 3132de1a6d..aea4fada1a 100644 --- a/tests/boundary/filesystem/dependencyInstall.test.ts +++ b/tests/boundary/filesystem/dependencyInstall.test.ts @@ -234,7 +234,7 @@ const createBuildCacheFixture = async (): Promise => { await writeFile("resource-controls.json", JSON.stringify(Object.fromEntries(["GOMAXPROCS", "GOMEMLIMIT", "UV_THREADPOOL_SIZE", "REA_TEST_UNLISTED_ENV"].map((name) => [name, process.env[name] ?? null])))); await mkdir("dist", { recursive: true }); await writeFile("dist/cache-fixture.js", "built"); -await writeFile("dist/catalogIdentity.js", "export const FULL_CATALOG_IDENTITY = { counts: { mcp_tools: 1 } };\\n"); +await writeFile("dist/catalogIdentity.js", "export const DEFAULT_CATALOG_IDENTITY = { counts: { mcp_tools: 1 } };\\n"); await writeFile("dist/identity.js", ${JSON.stringify(`export const PRODUCT_IDENTITY = ${JSON.stringify({ packageSpecifier: `${packageJson.name}@latest`, registrationPackageSpecifier: `${packageJson.name}@${packageJson.version}` })};\n`)}); `, ); diff --git a/tests/boundary/filesystem/generatedSkill.test.ts b/tests/boundary/filesystem/generatedSkill.test.ts index 1bfbc298b3..7837d59b29 100644 --- a/tests/boundary/filesystem/generatedSkill.test.ts +++ b/tests/boundary/filesystem/generatedSkill.test.ts @@ -47,7 +47,7 @@ async function fixture(eol = "\n") { const identity = async (count: number, digest: string) => writeFile( join(root, "dist/catalogIdentity.js"), - `export const FULL_CATALOG_IDENTITY = ${JSON.stringify({ counts: { mcp_tools: count }, digests: { combined_sha256: digest } })};\n`, + `export const DEFAULT_CATALOG_IDENTITY = ${JSON.stringify({ counts: { mcp_tools: count }, digests: { combined_sha256: digest } })};\n`, ); await identity(139, "a".repeat(64)); const generate = (...args: string[]) => diff --git a/tests/boundary/filesystem/kimiClientConfiguration.test.ts b/tests/boundary/filesystem/kimiClientConfiguration.test.ts index e5f8fbbf15..6d890a16f2 100644 --- a/tests/boundary/filesystem/kimiClientConfiguration.test.ts +++ b/tests/boundary/filesystem/kimiClientConfiguration.test.ts @@ -39,7 +39,7 @@ const exists = async (path: string): Promise => { const expectedRegistration = ( launcher: readonly string[], - profile?: "full" | "compact", + profile?: "compact", toolTimeoutMs = 450_000, ) => ({ command: launcher[0], @@ -240,27 +240,41 @@ describe("Kimi Code input-schema profile selection", () => { return { home, client }; }; - it("preserves an explicitly stored full profile", async () => { - const { home, client } = await kimiClient(); - await writeFile( - client.configPath, - JSON.stringify({ - mcpServers: { - rea: { - command: "rea", - args: ["mcp"], - env: { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, - }, + it("refuses to preserve a stored removed full profile", async () => { + const { client } = await kimiClient(); + const stored = JSON.stringify({ + mcpServers: { + rea: { + command: "rea", + args: ["mcp"], + env: { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, }, - }), - ); + }, + }); + await writeFile(client.configPath, stored); + // The removed value would fail at server startup, so setup fails closed + // instead of rewriting a registration that cannot start. expect( (await configureClientConfiguration(client, {}, command)).status, + ).toBe("failed"); + expect(await readFile(client.configPath, "utf8")).toBe(stored); + }); + + it("uses the caller's explicit compact profile", async () => { + const { home, client } = await kimiClient(); + expect( + ( + await configureClientConfiguration( + client, + { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" }, + command, + ) + ).status, ).toBe("configured"); expect( JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea, - ).toEqual(expectedRegistration(command, "full")); + ).toEqual(expectedRegistration(command, "compact")); expect( ( await readClientRegistrationStatuses(home, undefined, { @@ -271,29 +285,7 @@ describe("Kimi Code input-schema profile selection", () => { ).toMatchObject({ state: "aligned", command }); }); - it.each(["full", "compact"] as const)( - "uses the caller's explicit %s profile", - async (profile) => { - const { client } = await kimiClient(); - expect( - ( - await configureClientConfiguration( - client, - { REA_MCP_INPUT_SCHEMA_PROFILE: profile }, - command, - ) - ).status, - ).toBe("configured"); - expect( - JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea, - ).toEqual(expectedRegistration(command, profile)); - }, - ); - - it.each([ - ["full", "compact"], - ["compact", "full"], - ] as const)( + it.each([["compact", "full"]] as const)( "lets explicit setup %s replace stored %s", async (overrideProfile, storedProfile) => { const { client } = await kimiClient(); diff --git a/tests/boundary/filesystem/lmStudioClientConfiguration.test.ts b/tests/boundary/filesystem/lmStudioClientConfiguration.test.ts index 99a3e75266..b09e378a77 100644 --- a/tests/boundary/filesystem/lmStudioClientConfiguration.test.ts +++ b/tests/boundary/filesystem/lmStudioClientConfiguration.test.ts @@ -31,7 +31,7 @@ const exists = async (path: string): Promise => { } }; -const expectedRegistration = (profile?: "full" | "compact") => ({ +const expectedRegistration = (profile?: "compact") => ({ command: command[0], args: command.slice(1), ...(profile === undefined @@ -161,7 +161,7 @@ describe("LM Studio registration lifecycle", () => { }); }); - it("keeps an explicit full profile and reports an omitted profile aligned", async () => { + it("keeps an explicit compact profile and reports an omitted profile aligned", async () => { const { home, client } = await installedHome("default"); await writeFile( client.configPath, @@ -175,14 +175,14 @@ describe("LM Studio registration lifecycle", () => { ( await configureClientConfiguration( client, - { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" }, command, ) ).status, ).toBe("configured"); expect( JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea, - ).toEqual(expectedRegistration("full")); + ).toEqual(expectedRegistration("compact")); expect(await configureClientConfiguration(client, {}, command)).toEqual({ status: "unchanged", }); diff --git a/tests/boundary/javascriptSemanticTraceEvidence.test.ts b/tests/boundary/javascriptSemanticTraceEvidence.test.ts index 77a68663fe..9327c7d9f1 100644 --- a/tests/boundary/javascriptSemanticTraceEvidence.test.ts +++ b/tests/boundary/javascriptSemanticTraceEvidence.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -411,7 +412,7 @@ const assertSuccessfulTraceParity = async ( const advertised = tools.find( ({ name }) => name === "trace_javascript_semantics", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Semantic trace SDK output schema is missing"); const ajv = new Ajv2020({ strict: false, @@ -419,7 +420,9 @@ const assertSuccessfulTraceParity = async ( inlineRefs: false, code: { optimize: false }, }); - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(outputSchema)).toBe(true); expect(ajv.validate(outputSchema, mcpResponse.structuredContent)).toBe(true); const mcpEvidence = toolContract( diff --git a/tests/boundary/mcp/absoluteLocalPathSchema.test.ts b/tests/boundary/mcp/absoluteLocalPathSchema.test.ts index d10339bff1..8f29a3ea3f 100644 --- a/tests/boundary/mcp/absoluteLocalPathSchema.test.ts +++ b/tests/boundary/mcp/absoluteLocalPathSchema.test.ts @@ -1,3 +1,4 @@ +import { canonicalInputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { resolve } from "node:path"; @@ -16,10 +17,7 @@ interface PathInputCase { readonly issuePath: readonly string[]; } -const createMcp = async ( - firmwareCalls: string[], - profile?: "full" | "compact", -) => { +const createMcp = async (firmwareCalls: string[], profile?: "compact") => { const session = createTestBinarySession(() => { throw new Error("Invalid path input must not acquire a binary provider"); }); @@ -246,7 +244,7 @@ const inputCases = ( it("advertises full host-absolute path constraints after SDK schema conversion", async () => { const firmwareCalls: string[] = []; - const { client, close } = await createMcp(firmwareCalls, "full"); + const { client, close } = await createMcp(firmwareCalls, "compact"); try { const listed = await client.listTools(); const ajv = new Ajv2020({ strict: false, validateFormats: false }); @@ -284,12 +282,10 @@ it("advertises full host-absolute path constraints after SDK schema conversion", ]; const checked: string[] = []; for (const { name, field, valid, invalid } of schemaCases) { - const schema = listed.tools.find( - (tool) => tool.name === name, - )?.inputSchema; - if (schema === undefined) + if (!listed.tools.some((tool) => tool.name === name)) throw new Error(`Missing advertised tool ${name}`); - const validate = ajv.compile(schema); + // Path rules belong to the canonical input contract. + const validate = ajv.compile(canonicalInputSchema(name)); expect(validate(invalid), `${name}.${field}`).toBe(false); expect( validate(valid), diff --git a/tests/boundary/mcp/adbDeviceProperties.test.ts b/tests/boundary/mcp/adbDeviceProperties.test.ts index 179d572a23..3e31f1f0af 100644 --- a/tests/boundary/mcp/adbDeviceProperties.test.ts +++ b/tests/boundary/mcp/adbDeviceProperties.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { chmod } from "node:fs/promises"; import { Ajv2020 } from "ajv/dist/2020.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -109,9 +110,7 @@ exit 1 await client.connect(clientTransport); const { tools } = await client.listTools(); const advertised = tools.find(({ name }) => name === scenario.tool); - const outputSchema = z - .record(z.string(), z.unknown()) - .parse(advertised?.outputSchema); + const outputSchema = canonicalOutputSchema(advertised!.name); const ajv = new Ajv2020({ strict: false, allErrors: true }); expect(ajv.validateSchema(outputSchema)).toBe(true); const validate = ajv.compile(outputSchema); diff --git a/tests/boundary/mcp/adbMcp.test.ts b/tests/boundary/mcp/adbMcp.test.ts index eb8d0f5d33..eb315ef82f 100644 --- a/tests/boundary/mcp/adbMcp.test.ts +++ b/tests/boundary/mcp/adbMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it, onTestFinished } from "vitest"; @@ -367,11 +368,7 @@ it("advertises exact valid ADB schemas and records inline evidence through MCP", { serial: "exact-serial", source_token: "18446744073709551615" }, ], }); - const listingSchema = tools.find( - ({ name }) => name === "list_adb_devices", - )?.outputSchema; - if (listingSchema === undefined) - throw new Error("Missing listing output schema"); + const listingSchema = canonicalOutputSchema("list_adb_devices"); const validation = createMcpSchemaValidation(); onTestFinished(() => validation.clear()); expect(validation.assertions.validateSchema(listingSchema)).toBe(true); diff --git a/tests/boundary/mcp/addressContextCapacity.test.ts b/tests/boundary/mcp/addressContextCapacity.test.ts index 9d37ec9610..2fb13afb81 100644 --- a/tests/boundary/mcp/addressContextCapacity.test.ts +++ b/tests/boundary/mcp/addressContextCapacity.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -14,7 +15,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { SESSION_TOOL_CONTRACTS } from "../../../src/contracts/sessionToolContracts.js"; import { createServer } from "../../../src/server/createServer.js"; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "preserves retained siblings and capacity recovery (%s schema)", async (profile) => { const identity = { @@ -84,7 +85,10 @@ it.each(["compact", "full"] as const)( const toolDefinition = definitions.get(name); if (toolDefinition === undefined) throw new Error(`Tool ${name} was not advertised`); - return client.callTool({ name, arguments: args }, { toolDefinition }); + return client.callTool( + { name, arguments: args }, + { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, + ); }; expect( ( diff --git a/tests/boundary/mcp/addressContextInputErrors.test.ts b/tests/boundary/mcp/addressContextInputErrors.test.ts index 02ecb7473a..f533d7afc9 100644 --- a/tests/boundary/mcp/addressContextInputErrors.test.ts +++ b/tests/boundary/mcp/addressContextInputErrors.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -101,7 +102,7 @@ const createContextRecoverySession = () => { })); }; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "offers a conditional containing-function comment route (%s)", async (profile) => { const directory = await createTestTempDirectory("rea-context-comment-"); @@ -238,9 +239,7 @@ it.each(["compact", "full"] as const)( it.each([ ["compact", false, "comment"], ["compact", true, "comment"], - ["full", false, "comment"], - ["full", true, "comment"], - ["full", false, "list_bookmarks"], + ["compact", false, "list_bookmarks"], ] as const)( "preserves a local input refusal alongside successful facets (%s, conforms=%s, operation=%s)", async (profile, conforms, rejectedOperation) => { @@ -324,7 +323,7 @@ it.each([ name: "inspect_address_context", arguments: { address: "0x401001", document: "fixture" }, }, - { toolDefinition: contextDefinition }, + { toolDefinition: withCanonicalOutputSchema(contextDefinition) }, ); expect(response.isError).not.toBe(true); const contract = SESSION_TOOL_CONTRACTS.find( diff --git a/tests/boundary/mcp/advertisedDialect.test.ts b/tests/boundary/mcp/advertisedDialect.test.ts index 846a870bd9..93f9235ad6 100644 --- a/tests/boundary/mcp/advertisedDialect.test.ts +++ b/tests/boundary/mcp/advertisedDialect.test.ts @@ -10,8 +10,6 @@ import { portablePatternViolations, type AdvertisedToolDefinition, } from "../../../src/contracts/advertisedDialect.js"; -import { ADVERTISED_INPUT_DIALECT_BASELINE } from "../../../src/contracts/advertisedDialectBaseline.js"; -import { mcpInputSchemaProfileSchema } from "../../../src/config/mcpInputSchemaProfile.js"; import { mcpOutputSchemaProfileSchema } from "../../../src/config/mcpOutputSchemaProfile.js"; import { mcpToolCatalogProfileSchema } from "../../../src/config/mcpToolCatalogProfile.js"; import { createServer } from "../../../src/server/createServer.js"; @@ -174,22 +172,13 @@ const listTools = async (environment: Readonly) => { return (await client.listTools()).tools; }; -it("presents transport discovery through the selected input profile", async () => { - const discovery = async (profile: "compact" | "full") => - ( - await listTools({ - ...process.env, - REA_MCP_INPUT_SCHEMA_PROFILE: profile, - REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold", - }) - ).find(({ name }) => name === "discover_tools")?.inputSchema; - const full = await discovery("full"); - // The canonical page defaults remain declared in the full profile only. - expect(full?.properties).toMatchObject({ - offset: { default: 0 }, - limit: { default: 3 }, - }); - const compact = await discovery("compact"); +it("presents transport discovery through the compact input presentation", async () => { + const compact = ( + await listTools({ + ...process.env, + REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold", + }) + ).find(({ name }) => name === "discover_tools")?.inputSchema; expect(compact?.properties).toEqual({ query: { type: "string" }, names: { @@ -208,79 +197,61 @@ const distinct = ( definition: AdvertisedToolDefinition, ) => into.set(JSON.stringify(definition), definition); -it("advertises every profile within the portable dialect or its recorded baseline", async () => { - const inputs = new Map>(); +it("advertises every profile within the portable dialect", async () => { + const inputs = new Map(); const outputViolations: string[] = []; const uncompilable: string[] = []; - for (const input of mcpInputSchemaProfileSchema.options) - for (const output of mcpOutputSchemaProfileSchema.options) - for (const catalog of mcpToolCatalogProfileSchema.options) { - const tools = await listTools({ - ...process.env, - REA_MCP_INPUT_SCHEMA_PROFILE: input, - REA_MCP_OUTPUT_SCHEMA_PROFILE: output, - REA_MCP_TOOL_CATALOG_PROFILE: catalog, + for (const output of mcpOutputSchemaProfileSchema.options) + for (const catalog of mcpToolCatalogProfileSchema.options) { + const tools = await listTools({ + ...process.env, + REA_MCP_OUTPUT_SCHEMA_PROFILE: output, + REA_MCP_TOOL_CATALOG_PROFILE: catalog, + }); + const combination = `${output}/${catalog}`; + // Every combination advertises local path patterns, so the grammar + // check is never vacuous. + expect( + tools.flatMap(({ inputSchema }) => + advertisedSchemaPatterns(inputSchema), + ).length, + combination, + ).toBeGreaterThan(0); + for (const tool of tools) { + distinct(inputs, { + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema, }); - const combination = `${input}/${output}/${catalog}`; - // Every combination advertises local path patterns, so the grammar - // check is never vacuous. - expect( - tools.flatMap(({ inputSchema }) => - advertisedSchemaPatterns(inputSchema), - ).length, - combination, - ).toBeGreaterThan(0); - for (const tool of tools) { - if (!inputs.has(input)) inputs.set(input, new Map()); - distinct(inputs.get(input)!, { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema, - }); - // SDK clients compile every listed output schema. - for (const violation of advertisedOutputPatternViolations(tool)) - outputViolations.push( - `${combination} ${tool.name} ${violation.path} ${violation.detail ?? ""}`, - ); - // The grammar was checked against seven engines offline; this keeps - // V8's own modes checked in CI. - for (const schema of [tool.inputSchema, tool.outputSchema]) - for (const { path, pattern } of advertisedSchemaPatterns(schema)) - for (const flags of ["", "u", "v"]) - try { - new RegExp(pattern, flags); - } catch (cause: unknown) { - uncompilable.push( - `${combination} ${tool.name} ${path} /${pattern}/${flags}: ${String(cause)}`, - ); - } - } + // SDK clients compile every listed output schema. + for (const violation of advertisedOutputPatternViolations(tool)) + outputViolations.push( + `${combination} ${tool.name} ${violation.path} ${violation.detail ?? ""}`, + ); + // The grammar was checked against seven engines offline; this keeps + // V8's own modes checked in CI. + for (const schema of [tool.inputSchema, tool.outputSchema]) + for (const { path, pattern } of advertisedSchemaPatterns(schema)) + for (const flags of ["", "u", "v"]) + try { + new RegExp(pattern, flags); + } catch (cause: unknown) { + uncompilable.push( + `${combination} ${tool.name} ${path} /${pattern}/${flags}: ${String(cause)}`, + ); + } } + } expect(uncompilable).toEqual([]); expect(outputViolations).toEqual([]); - const observed = Object.fromEntries( - [...inputs].map(([profile, tools]) => [ - profile, - countAdvertisedDialectViolations( - [...tools.values()].flatMap(advertisedInputDialectViolations), - ), - ]), - ); - // New constructs fail first with a named key; a count that dropped below - // the baseline must be recorded so it cannot grow back. + // Every input construct outside the dialect fails with a named key. expect( - Object.fromEntries( - Object.entries(observed).map(([profile, counts]) => [ - profile, - advertisedDialectRegressions( - counts, - ADVERTISED_INPUT_DIALECT_BASELINE[ - profile as keyof typeof ADVERTISED_INPUT_DIALECT_BASELINE - ], - ), - ]), + advertisedDialectRegressions( + countAdvertisedDialectViolations( + [...inputs.values()].flatMap(advertisedInputDialectViolations), + ), + {}, ), - ).toEqual({ compact: [], full: [] }); - expect(observed).toEqual(ADVERTISED_INPUT_DIALECT_BASELINE); + ).toEqual([]); }); diff --git a/tests/boundary/mcp/advertisedPatternsVMode.test.ts b/tests/boundary/mcp/advertisedPatternsVMode.test.ts index 7ba9109e94..90ea46a524 100644 --- a/tests/boundary/mcp/advertisedPatternsVMode.test.ts +++ b/tests/boundary/mcp/advertisedPatternsVMode.test.ts @@ -12,6 +12,7 @@ import { INVALID_CANONICAL_BASE64_PAD_BITS_PATTERN, } from "../../../src/domain/stringPatterns.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; // Pattern portability across every profile is owned by // advertisedDialect.test.ts. This file keeps the composed canonical base64 @@ -73,9 +74,10 @@ it("advertises base64 without rejecting valid inputs or overflowing V8", async ( const tools = await listAdvertisedTools(); const patterns: string[] = []; const canonicalBase64Leaves: Record[] = []; + // tools/list omits output schemas by default; a validating client checks + // replies against the canonical output contract, which carries the pattern. for (const tool of tools) - for (const schema of [tool.inputSchema, tool.outputSchema]) { - if (schema === undefined) continue; + for (const schema of [tool.inputSchema, canonicalOutputSchema(tool.name)]) { patterns.push( ...advertisedSchemaPatterns(schema).map(({ pattern }) => pattern), ); diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index a3b8919ce4..c0d982fb2e 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -1,3 +1,7 @@ +import { + canonicalInputSchema, + canonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js"; import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { API_SURFACE_EXAMPLE } from "../../../src/contracts/analysisViewApiSurfaceExample.js"; @@ -87,7 +91,6 @@ const connect = async ( // These schema conformance cases exercise full nested constraints. environment: { ...process.env, - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_MAX_INPUT_ELEMENTS: maximumInputElements === undefined ? process.env.REA_MCP_MAX_INPUT_ELEMENTS @@ -343,7 +346,11 @@ it("advertises typed module references and preserves portable/retained Evidence ); const recorded = session.recordEvidence(parent); if (!recorded.ok) throw recorded.error; - const { ajv, inputSchema, toolDefinition } = await advertisedMcpToolSchemas( + const { + ajv, + canonicalInputSchema: inputSchema, + toolDefinition, + } = await advertisedMcpToolSchemas( client, "inspect_analysis_view", schemaValidation.assertions, @@ -591,7 +598,11 @@ it("advertises exact schemas and projects one layout section from retained Evide inspect: () => Promise.resolve(ok(layout)), }); const { client, session } = await connect(service); - const { ajv, inputSchema, toolDefinition } = await advertisedMcpToolSchemas( + const { + ajv, + canonicalInputSchema: inputSchema, + toolDefinition, + } = await advertisedMcpToolSchemas( client, "inspect_analysis_view", schemaValidation.assertions, @@ -976,11 +987,12 @@ it("delivers a bounded native dossier view over MCP without a Ghidra provider or const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_analysis_view", ); - if (advertised?.outputSchema === undefined) - throw new Error("missing view schemas"); + if (advertised === undefined) throw new Error("missing view schemas"); const ajv = schemaValidation.assertions; - const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const inputSchema = canonicalInputSchema(advertised.name); + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); const arguments_ = { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "native", facet: "pseudocode", offset: 0, limit: 500 }, @@ -1726,7 +1738,11 @@ it("advertises Mach-O pages whose selected facet owns their exact rows", async ( it("extracts an environment state identically from retained, portable and file MCP Evidence", async () => { // Full inline capture conformance explicitly selects its transport allowance. const { client, session } = await connect(undefined, undefined, 8_192); - const { ajv, inputSchema, toolDefinition } = await advertisedMcpToolSchemas( + const { + ajv, + canonicalInputSchema: inputSchema, + toolDefinition, + } = await advertisedMcpToolSchemas( client, "inspect_analysis_view", schemaValidation.assertions, @@ -1803,7 +1819,11 @@ it("extracts an environment state identically from retained, portable and file M it("reports environment coverage identically from retained, portable and file MCP Evidence", async () => { const { client, session } = await connect(undefined, undefined, 8_192); - const { ajv, inputSchema, toolDefinition } = await advertisedMcpToolSchemas( + const { + ajv, + canonicalInputSchema: inputSchema, + toolDefinition, + } = await advertisedMcpToolSchemas( client, "inspect_analysis_view", schemaValidation.assertions, @@ -1913,7 +1933,11 @@ it("accounts for every safe HTTP method in environment state and coverage", asyn it("projects an environment manifest identically from retained, portable and file MCP Evidence", async () => { const { client, session } = await connect(undefined, undefined, 8_192); - const { ajv, inputSchema, toolDefinition } = await advertisedMcpToolSchemas( + const { + ajv, + canonicalInputSchema: inputSchema, + toolDefinition, + } = await advertisedMcpToolSchemas( client, "inspect_analysis_view", schemaValidation.assertions, diff --git a/tests/boundary/mcp/androidDescriptorAdmissionMcp.test.ts b/tests/boundary/mcp/androidDescriptorAdmissionMcp.test.ts index dbbe215650..86b3d22e92 100644 --- a/tests/boundary/mcp/androidDescriptorAdmissionMcp.test.ts +++ b/tests/boundary/mcp/androidDescriptorAdmissionMcp.test.ts @@ -3,7 +3,7 @@ import { expect, it, vi } from "vitest"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; -it.each(["compact", "full"])( +it.each(["compact"])( "enforces DEX dimensions with %s input-schema delivery", async (profile) => { const execute = vi.fn(() => { diff --git a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts index b50fa35a23..ac150abd2e 100644 --- a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts +++ b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { unlink } from "node:fs/promises"; import { join } from "node:path"; import { @@ -179,7 +180,6 @@ const createJniSummaryContractFixture = async () => { }, environment: { REA_MCP_TOOL_FAMILIES: "android,application,session", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, }, }); @@ -193,8 +193,7 @@ it("defaults JNI MCP inspection to a retained summary while preserving explicit const definition = catalog.find( (tool) => tool.name === "inspect_android_jni_bindings", ); - if (definition?.outputSchema === undefined) - throw new Error("Missing JNI output schema"); + if (definition === undefined) throw new Error("Missing JNI output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const request = { path: apk, ...selection }; expect( @@ -286,7 +285,6 @@ it.skipIf(process.platform === "win32")( androidAnalysis: provider, environment: { REA_MCP_TOOL_FAMILIES: "android,application,session", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, }, }); @@ -296,9 +294,10 @@ it.skipIf(process.platform === "win32")( const definition = catalog.find( (tool) => tool.name === "inspect_android_package", ); - if (definition?.outputSchema === undefined) - throw new Error("Missing APK schema"); - const outputSchema: Record = definition.outputSchema; + if (definition === undefined) throw new Error("Missing APK schema"); + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(definition.inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); diff --git a/tests/boundary/mcp/apktoolMcp.test.ts b/tests/boundary/mcp/apktoolMcp.test.ts index 751df14149..7ff4e4aa5a 100644 --- a/tests/boundary/mcp/apktoolMcp.test.ts +++ b/tests/boundary/mcp/apktoolMcp.test.ts @@ -86,7 +86,7 @@ it("advertises exact valid Apktool schemas and records inline evidence through M { kind: "session", session }, { apktoolAnalysis: provider, - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, ); onTestFinished(() => server.close()); diff --git a/tests/boundary/mcp/appleIpsMcp.test.ts b/tests/boundary/mcp/appleIpsMcp.test.ts index 60d7c0f26a..0832983b3f 100644 --- a/tests/boundary/mcp/appleIpsMcp.test.ts +++ b/tests/boundary/mcp/appleIpsMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -13,10 +14,12 @@ it("advertises and validates IPS facts and failures through production stdio MCP const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_apple_ips", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing advertised IPS output schema"); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(advertised?.annotations).toMatchObject({ readOnlyHint: false, diff --git a/tests/boundary/mcp/applicationEvidenceMcp.test.ts b/tests/boundary/mcp/applicationEvidenceMcp.test.ts index 242aaacfee..aad1ae6e3d 100644 --- a/tests/boundary/mcp/applicationEvidenceMcp.test.ts +++ b/tests/boundary/mcp/applicationEvidenceMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import type { Client } from "@modelcontextprotocol/client"; import { describe, expect, it } from "vitest"; @@ -141,10 +142,10 @@ async function runUnicodeSourceMapComparison( const advertised = (await harness.client.listTools()).tools.find( ({ name }) => name === "compare_source_to_bundle", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Comparison tool output schema is missing"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const outputSchema = advertised.outputSchema as AnySchema; + const outputSchema = canonicalOutputSchema(advertised.name) as AnySchema; expect(ajv.validateSchema(outputSchema)).toBe(true); const validateOutput = ajv.compile(outputSchema); expect(validateOutput(reply.structuredContent)).toBe(true); diff --git a/tests/boundary/mcp/applicationExportShapeMcp.test.ts b/tests/boundary/mcp/applicationExportShapeMcp.test.ts index 2146aed223..beaebf55a8 100644 --- a/tests/boundary/mcp/applicationExportShapeMcp.test.ts +++ b/tests/boundary/mcp/applicationExportShapeMcp.test.ts @@ -1,3 +1,7 @@ +import { + canonicalInputSchema, + canonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { describe, expect, it, onTestFinished } from "vitest"; import { copyFile, mkdir, rm, writeFile } from "node:fs/promises"; import { join, resolve, sep } from "node:path"; @@ -27,7 +31,7 @@ it("executes the advertised export presence example with matching analyzed Evide throw new Error("Missing advertised presence example"); const { client, close } = await createApplicationMcpHarness({ serverOptions: { - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, }); onTestFinished(close); @@ -35,7 +39,9 @@ it("executes the advertised export presence example with matching analyzed Evide const advertised = listing.tools.find( ({ name }) => name === "compare_javascript_export_shapes", ); - const examples = z.array(z.unknown()).parse(advertised?.inputSchema.examples); + const examples = z + .array(z.unknown()) + .parse(canonicalInputSchema("compare_javascript_export_shapes").examples); expect(new Set(examples.map((input) => JSON.stringify(input))).size).toBe( examples.length, ); @@ -220,16 +226,14 @@ describe("application workflow MCP parity", () => { const advertised = (await client.listTools()).tools.find( ({ name }) => name === "compare_javascript_export_shapes", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing advertised export-shape output schema"); const validate = new Ajv2020({ strict: false, validateFormats: false, inlineRefs: false, code: { optimize: false }, - }).compile( - z.record(z.string(), z.unknown()).parse(advertised.outputSchema), - ); + }).compile(canonicalOutputSchema(advertised.name)); expect(validate(full.structuredContent)).toBe(true); expect(session.exportEvidenceBundle().records).toEqual( expect.arrayContaining([ diff --git a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts index 2c19a30ad7..22e4b44ab6 100644 --- a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts +++ b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { execFileSync } from "node:child_process"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -50,8 +51,10 @@ const connect = async ( const definition = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_analysis_view", ); - expect(definition?.outputSchema).toBeDefined(); - const outputSchema: Record = definition!.outputSchema!; + expect(definition).toBeDefined(); + const outputSchema: Record = canonicalOutputSchema( + definition!.name, + ); return { client, outputSchema }; }; diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index bf58eed860..fdabbc11b1 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -1,3 +1,4 @@ +import { canonicalInputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { INLINE_EVIDENCE_MCP_ENVIRONMENT } from "../../../scripts/lib/mcp-verification-environment.mjs"; import { readFile, unlink, writeFile } from "node:fs/promises"; @@ -194,12 +195,12 @@ mcpTest( const createInventoryViewReader = async ( client: Client, parent: Evidence, - profile: "full" | "compact", + profile: "compact", ) => { const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_analysis_view", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing advertised view schemas"); const ajv = schemaValidation.assertions; const inputSchema: Record = advertised.inputSchema; @@ -207,8 +208,11 @@ const createInventoryViewReader = async ( source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, view: { kind: "inventory", collection: "nodes", logical_paths: [] }, }; - if (profile === "full") - expect(ajv.validate(inputSchema, malformed)).toBe(false); + // The canonical contract refuses the empty selection; the compact + // advertisement may not, so the server's refusal below is what callers see. + expect(ajv.validate(canonicalInputSchema(advertised.name), malformed)).toBe( + false, + ); const rejected = await client.callTool({ name: "inspect_analysis_view", arguments: malformed, @@ -377,7 +381,7 @@ const assertOccurrencePages = async ( }); }; -mcpTest.for(["full", "compact"] as const)( +mcpTest.for(["compact"] as const)( "keeps complete ZIP output and reads retained inventory views after the source is removed (%s)", async (profile, { mcp, onTestFinished }) => { const directory = await createTestTempDirectory("rea-artifact-inline-"); diff --git a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts index b3709b2837..185bc945e2 100644 --- a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts +++ b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -105,7 +106,7 @@ afterAll(() => schemaValidation.clear()); const ADVERTISED_OUTPUTS = ["batch_decompile", "inspect_analysis_view"]; const connect = async ( - profile: "full" | "compact", + profile: "compact", options: { readonly budget?: number; readonly bodies?: typeof BODIES; @@ -132,7 +133,7 @@ const connect = async ( const advertisedTools = new Map( ADVERTISED_OUTPUTS.map((name) => { const advertised = tools.find((tool) => tool.name === name); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error(`${name} advertises no output schema`); return [name, advertised] as const; }), @@ -144,7 +145,9 @@ const connect = async ( .parse( await harness.mcp.callTool( { name, arguments: args }, - toolDefinition === undefined ? {} : { toolDefinition }, + toolDefinition === undefined + ? {} + : { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, ), ); }; @@ -224,7 +227,7 @@ const invalidIssue = (message: string) => ({ }, }); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "selects retained batch rows and bodies from a summary without repeating analysis (%s)", async (profile) => { const harness = await connect(profile); @@ -366,7 +369,7 @@ it.each(["full", "compact"] as const)( ); it("counts both forms of an unavailable entry label", async () => { - const harness = await connect("full"); + const harness = await connect("compact"); const parent = ( await harness.call("batch_decompile", { addresses: ["main", "unlabeled", "label-failure"], @@ -399,7 +402,7 @@ it("counts both forms of an unavailable entry label", async () => { }); it("pages one body exactly without splitting a Unicode character", async () => { - const harness = await connect("full"); + const harness = await connect("compact"); const { evidence } = await harness.call("batch_decompile", { addresses: SELECTORS, }); @@ -450,7 +453,7 @@ it("pages one body exactly without splitting a Unicode character", async () => { it("recovers oversized batches and pages through smaller selections of the parent", async () => { const budget = 64 * 1024; const large = `int main(void) { /*${"x".repeat(budget * 2)}*/ }`; - const harness = await connect("full", { + const harness = await connect("compact", { budget, bodies: { "0x401000": large, "0x402000": large }, }); @@ -534,7 +537,7 @@ it("recovers oversized batches and pages through smaller selections of the paren }); it("reads the same rows from a bundle and an Evidence file without a provider", async () => { - const harness = await connect("full"); + const harness = await connect("compact"); const parent = ( await harness.call("batch_decompile", { addresses: SELECTORS }) ).evidence; @@ -585,7 +588,7 @@ it("reads the same rows from a bundle and an Evidence file without a provider", }); it("reads historical batch Evidence with the prior null-pseudocode error", async () => { - const harness = await connect("full"); + const harness = await connect("compact"); const savedItem = { address: "bodyless", procedure: { status: "resolved", address: "0x403000", name: "bodyless" }, @@ -787,7 +790,7 @@ it("delivers no summary without retention, on a ledger failure or after cancella ).toMatchObject({ code: "capability_unavailable" }); expect(calls).toBe(0); - const ledger = await connect("full"); + const ledger = await connect("compact"); vi.spyOn(ledger.session, "recordEvidence").mockReturnValue( err(new AnalysisOutputError("record_evidence", "ledger refused the batch")), ); @@ -797,7 +800,7 @@ it("delivers no summary without retention, on a ledger failure or after cancella }); expect(parseMcpToolError(refused).error.code).toBe("unreadable_output"); - const cancelled = await connect("full", { + const cancelled = await connect("compact", { execute: (operation, input) => Promise.resolve( operation === "procedure_pseudo_code" @@ -818,7 +821,7 @@ it("delivers no summary without retention, on a ledger failure or after cancella }); it("binds views to their parent and each page to its own collection", async () => { - const harness = await connect("full"); + const harness = await connect("compact"); const summary = viewOf( ( await harness.call("batch_decompile", { diff --git a/tests/boundary/mcp/binaryHardeningMcp.test.ts b/tests/boundary/mcp/binaryHardeningMcp.test.ts index 307ae2c03b..cde8f01c40 100644 --- a/tests/boundary/mcp/binaryHardeningMcp.test.ts +++ b/tests/boundary/mcp/binaryHardeningMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; @@ -39,7 +40,7 @@ mcpTest( await writeFile(path, bytes); const response = await client.callTool( { name: "inspect_binary_hardening", arguments: { path } }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(response.isError, JSON.stringify(response.content)).not.toBe(true); const evidence = toolContract( @@ -57,7 +58,7 @@ mcpTest( name: "inspect_binary_hardening", arguments: { path: "relative" }, }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(bad.isError).toBe(true); } finally { diff --git a/tests/boundary/mcp/browserMcp.test.ts b/tests/boundary/mcp/browserMcp.test.ts index fe15e434e8..10ca044574 100644 --- a/tests/boundary/mcp/browserMcp.test.ts +++ b/tests/boundary/mcp/browserMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { crc32, deflateSync } from "node:zlib"; import { parseEvidence } from "../../../src/domain/evidence.js"; @@ -97,7 +98,7 @@ it("preserves Link parameter order and context through the advertised MCP contra const advertised = (await connected.client.listTools()).tools.find( ({ name }) => name === "inspect_web_page", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("inspect_web_page must advertise its output schema"); const inspected = await connected.client.callTool( @@ -113,7 +114,9 @@ it("preserves Link parameter order and context through the advertised MCP contra ); expect(inspected.isError).not.toBe(true); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); const normalized = webPageInspectionSchema.parse( normalizedResultOf(inspected.structuredContent), @@ -792,19 +795,12 @@ cliTest( }, ); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "records a browser-socket endpoint redacted and reconciles its Evidence (%s)", async (profile) => { const browser = await startFakeCdpBrowser(); retainBrowser(browser); const connected = await connectBrowser(profile); - const tool = (await connected.client.listTools()).tools.find( - ({ name }) => name === "inspect_web_page", - ); - if (profile === "full") - expect(tool?.inputSchema.properties?.cdp_endpoint).toMatchObject({ - description: expect.stringContaining("/devtools/browser/ID"), - }); const listed = await connected.client.callTool({ name: "list_browser_targets", arguments: { @@ -954,7 +950,7 @@ it("projects legal oversized PNG dimensions as a memory constraint", async () => }); }); -const connectBrowser = async (profile?: "full" | "compact") => { +const connectBrowser = async (profile?: "compact") => { const session = createTestBinarySession(() => ({ execute: () => Promise.resolve(observed(null)), close: () => Promise.resolve(resultOk(null)), diff --git a/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts b/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts index b704c20343..0ee01f2f1f 100644 --- a/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/browserScenarioInputDiagnosticsMcp.test.ts @@ -81,7 +81,7 @@ const scenarios = [ }, ] as const; -cliTest.for(["full", "compact"] as const)( +cliTest.for(["compact"] as const)( "projects browser scenario input diagnostics over stdio MCP (%s)", async (profile, { cli }) => { const home = await createTestTempDirectory( diff --git a/tests/boundary/mcp/browserScenarioMcp.test.ts b/tests/boundary/mcp/browserScenarioMcp.test.ts index 171c96e386..b6108263c5 100644 --- a/tests/boundary/mcp/browserScenarioMcp.test.ts +++ b/tests/boundary/mcp/browserScenarioMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { describe, expect, test } from "vitest"; import { Ajv2020 } from "ajv/dist/2020.js"; import { z } from "zod"; @@ -177,7 +178,7 @@ const scenario = (origin = "https://app.example.test") => ({ ], }); -test.each(["full", "compact"] as const)( +test.each(["compact"] as const)( "advertises portable browser action patterns (%s)", async (profile) => { const provider = new FakeBrowserScenarioProvider(); @@ -216,17 +217,6 @@ test.each(["full", "compact"] as const)( pending.push({ value, where }); } } - if (profile === "full") { - expect( - patterns.some(({ where }) => where.endsWith("/path/pattern")), - ).toBe(true); - expect( - patterns.some( - ({ where }) => - where.includes("/headers/") && where.endsWith("/name/pattern"), - ), - ).toBe(true); - } const failures = patterns.flatMap(({ where, pattern }) => ["", "u", "v"].flatMap((flags) => { try { @@ -442,13 +432,11 @@ test("delivers a retained summary and pages its exact steps and events", async ( const advertised = tools.find( ({ name }) => name === "capture_browser_scenario", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing advertised scenario output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const validateInput = ajv.compile(advertised.inputSchema); - const validateOutput = ajv.compile( - z.record(z.string(), z.unknown()).parse(advertised.outputSchema), - ); + const validateOutput = ajv.compile(canonicalOutputSchema(advertised.name)); const request = { ...minimalScenario(), detail: "summary" }; expect(validateInput(request), JSON.stringify(validateInput.errors)).toBe( true, diff --git a/tests/boundary/mcp/browserSourceCoordinates.test.ts b/tests/boundary/mcp/browserSourceCoordinates.test.ts index dcb4c4223c..4d30bb0692 100644 --- a/tests/boundary/mcp/browserSourceCoordinates.test.ts +++ b/tests/boundary/mcp/browserSourceCoordinates.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { expect, it, onTestFinished } from "vitest"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; @@ -68,9 +69,12 @@ const inspectSourceCoordinates = async (position: { const definitions = (await client.listTools()).tools; const call = async (name: "inspect_web_page" | "discover_webmcp_tools") => { const toolDefinition = definitions.find((tool) => tool.name === name); - if (toolDefinition?.outputSchema === undefined) + if (toolDefinition === undefined) throw new Error(`Missing advertised output schema: ${name}`); - return client.callTool({ name, arguments: args }, { toolDefinition }); + return client.callTool( + { name, arguments: args }, + { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, + ); }; const page = await call("inspect_web_page"); const tools = await call("discover_webmcp_tools"); diff --git a/tests/boundary/mcp/compactInputSchemaMcp.test.ts b/tests/boundary/mcp/compactInputSchemaMcp.test.ts index 70ad810b1d..c1178005fb 100644 --- a/tests/boundary/mcp/compactInputSchemaMcp.test.ts +++ b/tests/boundary/mcp/compactInputSchemaMcp.test.ts @@ -66,100 +66,66 @@ const compactServer = (): EvidenceMcpServer => { capabilities: {} }, undefined, createToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE), - { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES }, ); -const fullServer = (): EvidenceMcpServer => - new EvidenceMcpServer( - { name: "full-profile", version: "0" }, - { capabilities: {} }, - undefined, - createToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE), - ); +/** Canonical input contracts, before the compact presentation. */ +const canonicalTools = (): readonly AdvertisedTool[] => + TOOL_CONTRACTS.map((contract) => ({ + name: contract.name, + inputSchema: advertisedInputJsonSchema(contract), + })); describe("MCP compact input schema profile", () => { - it("matches profile digests to tools/list and excludes stripped annotations", async () => { + it("matches the catalog digest to tools/list and excludes stripped annotations", async () => { const compactPresentation = { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, }; - const full = fullServer(); const compact = compactServer(); - registerCatalog(full); registerCatalog(compact); - const [fullTools, compactTools] = await Promise.all([ - listToolsThroughTransport(full), - listToolsThroughTransport(compact), - ]); - const toolsByName = [ - new Map(fullTools.map((tool) => [tool.name, tool])), - new Map(compactTools.map((tool) => [tool.name, tool])), - ]; - for (const [index, presentation] of [ - undefined, - compactPresentation, - ].entries()) { - for (const contract of TOOL_CONTRACTS) { - expect(toolsByName[index]?.get(contract.name)?.inputSchema).toEqual( - presentAdvertisedInputSchema( - advertisedInputJsonSchema(contract), - presentation, - ), - ); - } - } - - const fullIdentity = createCatalogDigests(); - const compactIdentity = createCatalogDigests( - TOOL_CONTRACTS, - PROMPT_CONTRACTS, - compactPresentation, + const compactTools = new Map( + (await listToolsThroughTransport(compact)).map((tool) => [ + tool.name, + tool, + ]), ); - expect(full.catalogIdentity.digests).toEqual(fullIdentity); + for (const contract of TOOL_CONTRACTS) + expect(compactTools.get(contract.name)?.inputSchema).toEqual( + presentAdvertisedInputSchema( + advertisedInputJsonSchema(contract), + compactPresentation, + ), + ); + + const compactIdentity = createCatalogDigests(); expect(compact.catalogIdentity.digests).toEqual(compactIdentity); - expect(compactIdentity.tools_sha256).not.toBe(fullIdentity.tools_sha256); expect( createCatalogDigests(TOOL_CONTRACTS, PROMPT_CONTRACTS, { budgetBytes: 1, }).tools_sha256, ).not.toBe(compactIdentity.tools_sha256); + // Examples are not advertised, so they do not change the identity. const withoutExamples = TOOL_CONTRACTS.map((contract) => contract.examples.length === 0 ? contract : { ...contract, examples: [] }, ); - expect(createCatalogDigests(withoutExamples).tools_sha256).not.toBe( - fullIdentity.tools_sha256, + expect(createCatalogDigests(withoutExamples).tools_sha256).toBe( + compactIdentity.tools_sha256, ); - expect( - createCatalogDigests( - withoutExamples, - PROMPT_CONTRACTS, - compactPresentation, - ).tools_sha256, - ).toBe(compactIdentity.tools_sha256); }); }); -it("keeps output schemas and extends tool descriptions only with input guidance", async () => { +it("extends tool descriptions only with input guidance", async () => { const compact = compactServer(); - const full = fullServer(); registerCatalog(compact); - registerCatalog(full); - const [compactTools, fullTools] = await Promise.all([ - listToolsThroughTransport(compact), - listToolsThroughTransport(full), - ]); - const fullByName = new Map(fullTools.map((tool) => [tool.name, tool])); - expect(compactTools.length).toBe(fullTools.length); + const compactTools = await listToolsThroughTransport(compact); + expect(compactTools.length).toBe(TOOL_CONTRACTS.length); for (const tool of compactTools) { - const reference = fullByName.get(tool.name); - if (reference === undefined) throw new Error(`Missing ${tool.name}`); - expect(tool.outputSchema, tool.name).toEqual(reference.outputSchema); // Guidance the compact schema omits follows the unchanged description. const contract = TOOL_CONTRACTS.find(({ name }) => name === tool.name); if (contract === undefined) throw new Error(`Missing ${tool.name}`); expect(tool.description, tool.name).toBe( describeAdvertisedTool( - reference.description, + toolRegistrationOptions(contract).description, presentAdvertisedInput(advertisedInputJsonSchema(contract), { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, }).guidance, @@ -197,8 +163,8 @@ it("preserves arbitrary nested JSON values at the converted SDK boundary", async const value = { deeply_nested: [[[[null, true, 7, "text"]]], { mixed: [[], [false]] }], }; - for (const profile of ["full", "compact"] as const) { - const server = profile === "full" ? fullServer() : compactServer(); + { + const server = compactServer(); server.registerTool( contract.name, toolRegistrationOptions(contract), @@ -213,16 +179,20 @@ it("preserves arbitrary nested JSON values at the converted SDK boundary", async validateFormats: false, }).compile(tool.inputSchema); expect(contract.inputSchema.safeParse({ value }).success).toBe(true); - expect(validate({ value }), `${profile} profile`).toBe(true); + expect(validate({ value })).toBe(true); } }); -it("keeps SDK-converted schemas valid and accepting canonical examples", async () => { - let checkedFullRestrictions = false; - for (const profile of ["full", "compact"] as const) { - const server = profile === "full" ? fullServer() : compactServer(); - registerCatalog(server); - const tools = await listToolsThroughTransport(server); +it("keeps SDK-converted and canonical schemas valid and accepting canonical examples", async () => { + let checkedCanonicalRestrictions = false; + for (const profile of ["canonical", "compact"] as const) { + let tools: readonly AdvertisedTool[]; + if (profile === "canonical") tools = canonicalTools(); + else { + const server = compactServer(); + registerCatalog(server); + tools = await listToolsThroughTransport(server); + } const ajv = new Ajv2020({ strict: false, validateFormats: false }); const contractsByName = new Map( TOOL_CONTRACTS.map((contract) => [contract.name, contract]), @@ -247,7 +217,7 @@ it("keeps SDK-converted schemas valid and accepting canonical examples", async ( `${profile} profile, ${tool.name}: ${example.title}`, ).toBe(true); } - if (profile === "full") { + if (profile === "canonical") { const adb = tools.find(({ name }) => name === "start_adb_activity"); const managed = tools.find( ({ name }) => name === "inspect_managed_members", @@ -321,22 +291,17 @@ it("keeps SDK-converted schemas valid and accepting canonical examples", async ( validateWasm({ path: "/artifacts/program", command_timeout_ms }), String(command_timeout_ms), ).toBe(false); - checkedFullRestrictions = true; + checkedCanonicalRestrictions = true; } } - expect(checkedFullRestrictions).toBe(true); + expect(checkedCanonicalRestrictions).toBe(true); }); -it("states dependency keywords as prose in both catalog profiles", async () => { - const full = fullServer(); +it("states dependency keywords as prose in the canonical and compact schemas", async () => { const compact = compactServer(); - registerCatalog(full); registerCatalog(compact); - const [fullTools, compactTools] = await Promise.all([ - listToolsThroughTransport(full), - listToolsThroughTransport(compact), - ]); - const sqlite = fullTools.find( + const compactTools = await listToolsThroughTransport(compact); + const sqlite = canonicalTools().find( ({ name }) => name === "inspect_sqlite_database", ); expect(sqlite?.inputSchema.description).toContain( @@ -484,9 +449,8 @@ describe("MCP compact input schema canonical enforcement", () => { }); }); -it.each(["full", "compact"] as const)( - "preserves nested resource validation through the %s SDK catalog and canonical handler", - async (profile) => { +it("preserves nested resource validation through the SDK catalog and canonical handler", async () => { + { const source = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://example.test/root.json", @@ -524,7 +488,7 @@ it.each(["full", "compact"] as const)( : { issues: [{ message: "Invalid resource value" }] }, }, }; - const server = profile === "compact" ? compactServer() : fullServer(); + const server = compactServer(); let calls = 0; server.registerTool("resource_fixture", { inputSchema }, async () => { calls++; @@ -553,18 +517,13 @@ it.each(["full", "compact"] as const)( expect(result.isError === true).toBe(!canonical(args)); } expect(calls).toBe(1); - }, -); + } +}); it("keeps root detail guidance so callers can choose bounded summaries", async () => { - const full = fullServer(); const compact = compactServer(); - registerCatalog(full); registerCatalog(compact); - const [fullTools, compactTools] = await Promise.all([ - listToolsThroughTransport(full), - listToolsThroughTransport(compact), - ]); + const compactTools = await listToolsThroughTransport(compact); const detailGuidance = (tools: readonly AdvertisedTool[]) => Object.fromEntries( tools.flatMap(({ name, inputSchema }) => { @@ -576,7 +535,7 @@ it("keeps root detail guidance so callers can choose bounded summaries", async ( : []; }), ); - const expected = detailGuidance(fullTools); + const expected = detailGuidance(canonicalTools()); expect(Object.keys(expected)).toEqual( expect.arrayContaining(["inspect_artifact", "inspect_macho"]), ); @@ -598,21 +557,11 @@ it.each([ { bundle: "/bundle", model: "test-model", budget_usd: 1 }, ], ] as const)( - "advertises portable names for %s through the SDK", - async (name, field, base) => { + "declares portable names for %s in the canonical input schema", + (name, field, base) => { const contract = toolContract(name); - const server = fullServer(); - server.registerTool( - contract.name, - toolRegistrationOptions(contract), - async () => ({ content: [] }), - ); - const tool = (await listToolsThroughTransport(server)).find( - ({ name }) => name === contract.name, - ); - expect(tool).toBeDefined(); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const validate = ajv.compile(tool!.inputSchema); + const validate = ajv.compile(advertisedInputJsonSchema(contract)); for (const [name, accepted] of [ [undefined, field === "name"], ["replay-2", true], diff --git a/tests/boundary/mcp/contextEvidenceOverflow.test.ts b/tests/boundary/mcp/contextEvidenceOverflow.test.ts index 02240ac2fd..1c3e538d7a 100644 --- a/tests/boundary/mcp/contextEvidenceOverflow.test.ts +++ b/tests/boundary/mcp/contextEvidenceOverflow.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -96,7 +97,6 @@ const verifyContext = ( it.each([ ["compact", false], - ["full", false], ["compact", true], ] as const)( "keeps context and smaller partials when unretained observations exceed the MCP budget (%s, two large=%s)", @@ -181,7 +181,10 @@ it.each([ const toolDefinition = definitions.get(name); if (toolDefinition === undefined) throw new Error(`Tool ${name} was not advertised`); - return client.callTool({ name, arguments: args }, { toolDefinition }); + return client.callTool( + { name, arguments: args }, + { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, + ); }; expect( ( diff --git a/tests/boundary/mcp/contractPresentation.test.ts b/tests/boundary/mcp/contractPresentation.test.ts index d1db7f3d79..11e6e36be3 100644 --- a/tests/boundary/mcp/contractPresentation.test.ts +++ b/tests/boundary/mcp/contractPresentation.test.ts @@ -1,3 +1,5 @@ +import { presentAdvertisedInputSchema } from "../../../src/contracts/compactInputSchema.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../../../src/config/mcpInputSchemaProfile.js"; import { randomUUID } from "node:crypto"; import { fileURLToPath } from "node:url"; import { isCallToolResult } from "@modelcontextprotocol/server"; @@ -53,7 +55,7 @@ const resultSchema = z.object({ z.object({ name: z.string(), inputSchema: z.record(z.string(), z.unknown()), - outputSchema: z.record(z.string(), z.unknown()), + outputSchema: z.record(z.string(), z.unknown()).optional(), }), ), responses: z.array( @@ -61,7 +63,7 @@ const resultSchema = z.object({ ), }); -it("preserves full advertised schemas and handler meaning when presentation order changes", async () => { +it("preserves advertised schemas and handler meaning when presentation order changes", async () => { const cancellation = new AbortController(); const command = runOwnedCommand( { @@ -70,13 +72,11 @@ it("preserves full advertised schemas and handler meaning when presentation orde runId: randomUUID(), env: { ...process.env, - REA_MCP_INPUT_SCHEMA_PROFILE: "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, sweepTokenOwnedProcesses: true, }, { - // A prepared full/full catalog run needs about 32s on two CPU cores. + // A prepared catalog run needed about 32s on two CPU cores. timeoutMs: 60_000, diagnosticBytes: 16 * 1024 * 1024, }, @@ -97,8 +97,15 @@ it("preserves full advertised schemas and handler meaning when presentation orde const canonical = GENERATED_MCP_TOOL_CATALOG.find( ({ name }) => name === advertised.name, ); - expect(advertised.inputSchema).toEqual(canonical?.inputSchema); - expect(advertised.outputSchema).toEqual(canonical?.outputSchema); + if (canonical === undefined) throw new Error(`Missing ${advertised.name}`); + expect(advertised.inputSchema).toEqual( + presentAdvertisedInputSchema( + canonical.inputSchema, + COMPACT_INPUT_SCHEMA_PRESENTATION, + ), + ); + // The default output profile omits output schemas. + expect(advertised.outputSchema).toBeUndefined(); } expect(result.responses.map(({ name }) => name).sort()).toEqual( [ diff --git a/tests/boundary/mcp/dartAotSummaryMcp.test.ts b/tests/boundary/mcp/dartAotSummaryMcp.test.ts index 6cb72cb4d8..9830c96af2 100644 --- a/tests/boundary/mcp/dartAotSummaryMcp.test.ts +++ b/tests/boundary/mcp/dartAotSummaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { unlink } from "node:fs/promises"; import { createHash } from "node:crypto"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -46,8 +47,7 @@ it("retains the selected pool policy through MCP summary projection", async () = const definition = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_dart_aot", ); - if (definition?.outputSchema === undefined) - throw new Error("Missing AOT output schema"); + if (definition === undefined) throw new Error("Missing AOT output schema"); const filteredReply = await client.callTool( { name: "inspect_dart_aot", @@ -81,9 +81,10 @@ it("honors explicit ABI and old full requests, validates SDK schemas and reuses const { client, session } = harness; const catalog = (await client.listTools()).tools; const definition = catalog.find((tool) => tool.name === "inspect_dart_aot"); - if (definition?.outputSchema === undefined) - throw new Error("Missing AOT output schema"); - const outputSchema: Record = definition.outputSchema; + if (definition === undefined) throw new Error("Missing AOT output schema"); + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(definition.inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); diff --git a/tests/boundary/mcp/deferredRuntime.test.ts b/tests/boundary/mcp/deferredRuntime.test.ts index cfc19e47bf..53e3c083bd 100644 --- a/tests/boundary/mcp/deferredRuntime.test.ts +++ b/tests/boundary/mcp/deferredRuntime.test.ts @@ -16,7 +16,6 @@ import { artifactMcpCall } from "../../../scripts/lib/artifact-e2e.mjs"; import { closeVerifierResources } from "../../../scripts/lib/verifier-cleanup.mjs"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js"; -import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js"; import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js"; import { TOOL_FAMILY_DEFINITIONS } from "../../../src/contracts/toolFamilies.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; @@ -208,16 +207,6 @@ it.each([ environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "compact" }, eager: false, }, - { - name: "full deferred", - environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, - eager: false, - }, - { - name: "output-full deferred", - environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }, - eager: false, - }, { name: "output-compact eager", environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "compact" }, @@ -290,33 +279,18 @@ registerHooks({ }, ); -it.each([ - { mode: "legacy", profile: "full" }, - { mode: "auto", profile: "full" }, - { mode: "legacy", profile: "compact" }, - { mode: "auto", profile: "compact" }, -] as const)( - "serves $profile discovery and canonical handlers over $mode negotiation", - async ({ mode, profile }) => { +it.each([{ mode: "legacy" }, { mode: "auto" }] as const)( + "serves discovery and canonical handlers over $mode negotiation", + async ({ mode }) => { const recorded = JSON.parse( - await readFile( - profile === "full" - ? "dist/mcp-catalog-full.json" - : "dist/mcp-catalog.json", - "utf8", - ), + await readFile("dist/mcp-catalog.json", "utf8"), ); - const { client, stderr } = await connect(mode, { - REA_MCP_INPUT_SCHEMA_PROFILE: profile, - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", - }); + const { client, stderr } = await connect(mode, {}); const before = await client.listTools(); // Compare independently composed canonical SDK registrations as well as // recorded artifacts. Listing every explicit family selects eager startup // with the same inventory, without target analysis or provider acquisition. const eager = await connect(mode, { - REA_MCP_INPUT_SCHEMA_PROFILE: profile, - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", REA_MCP_TOOL_FAMILIES: TOOL_FAMILY_DEFINITIONS.map(({ id }) => id).join( ",", ), @@ -331,44 +305,10 @@ it.each([ expect(before.tools.map(withoutOutput)).toEqual( recorded.tools.map(withoutOutput), ); - const outputs = new Map( - (mode === "auto" ? recorded.tools : GENERATED_MCP_TOOL_CATALOG).map( - (tool: { name: string; outputSchema?: unknown }) => [ - tool.name, - tool.outputSchema, - ], - ), - ); - for (const tool of before.tools) - expect(tool.outputSchema, tool.name).toEqual(outputs.get(tool.name)); - const contractProjection = (tool: { - name: string; - title?: string | undefined; - description?: string | undefined; - inputSchema: unknown; - outputSchema?: unknown; - annotations?: unknown; - }) => ({ - name: tool.name, - title: tool.title, - description: tool.description, - inputSchema: tool.inputSchema, - outputSchema: tool.outputSchema, - annotations: tool.annotations, - }); - if (profile === "full" && mode === "legacy") - expect( - new Map( - before.tools.map((tool) => [tool.name, contractProjection(tool)]), - ), - ).toEqual( - new Map( - GENERATED_MCP_TOOL_CATALOG.map((tool) => [ - tool.name, - contractProjection(tool), - ]), - ), - ); + // The default output profile omits the recorded output schemas. + expect( + before.tools.filter(({ outputSchema }) => outputSchema !== undefined), + ).toEqual([]); expect((await client.listPrompts()).prompts).toEqual( PROMPT_CONTRACTS.map((prompt) => ({ name: prompt.name, @@ -456,7 +396,6 @@ it.each(["legacy", "auto"] as const)( it("composes a scoped registration instead of serving the recorded catalog", async () => { const { client, stderr } = await connect("legacy", { REA_MCP_TOOL_FAMILIES: "session", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }); const advertised = (await client.listTools()).tools .map(({ name }) => name) @@ -491,11 +430,7 @@ globalThis.Date = class extends NativeDate { } }; `)}`; - const { client } = await connect( - "legacy", - { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }, - [`--import=${hook}`], - ); + const { client } = await connect("legacy", {}, [`--import=${hook}`]); await client.listTools(); await writeFile(clock, "2021-01-01T00:00:00.000Z"); const first = await artifactMcpCall(client, { @@ -816,39 +751,30 @@ console.log("LATE_PUBLICATION_REFUSED"); expect(child.stderr).toBe(""); }); -it.each([ - {}, - { REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" }, - { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }, -])("preserves the configured catalog profile %j", async (environment) => { - const { client } = await connect("legacy", environment); - const before = await client.listTools(); - const catalog = JSON.parse(await readFile("dist/mcp-catalog.json", "utf8")); - expect(before.tools).toHaveLength(catalog.tools.length); - // Output schemas are omitted unless `full` is selected. - const expected = - environment.REA_MCP_OUTPUT_SCHEMA_PROFILE !== "full" - ? catalog.tools.map( - ({ outputSchema: _output, ...tool }: Record) => tool, - ) - : catalog.tools.map((tool: { name: string }) => ({ - ...tool, - outputSchema: GENERATED_MCP_TOOL_CATALOG.find( - ({ name }) => name === tool.name, - )?.outputSchema, - })); - expect(before.tools).toEqual(expected); - const toolDefinition = before.tools.find( - ({ name }) => name === "list_unknowns", - ); - if (toolDefinition === undefined) throw new Error("Missing list_unknowns"); - const result = await client.callTool( - { name: "list_unknowns", arguments: {} }, - { toolDefinition }, - ); - expect(result.isError).not.toBe(true); - expect(await client.listTools()).toEqual(before); -}); +it.each([{}, { REA_MCP_OUTPUT_SCHEMA_PROFILE: "off" }])( + "preserves the configured catalog profile %j", + async (environment) => { + const { client } = await connect("legacy", environment); + const before = await client.listTools(); + const catalog = JSON.parse(await readFile("dist/mcp-catalog.json", "utf8")); + expect(before.tools).toHaveLength(catalog.tools.length); + // The deferred server omits the recorded output schemas from listings. + const expected = catalog.tools.map( + ({ outputSchema: _output, ...tool }: Record) => tool, + ); + expect(before.tools).toEqual(expected); + const toolDefinition = before.tools.find( + ({ name }) => name === "list_unknowns", + ); + if (toolDefinition === undefined) throw new Error("Missing list_unknowns"); + const result = await client.callTool( + { name: "list_unknowns", arguments: {} }, + { toolDefinition }, + ); + expect(result.isError).not.toBe(true); + expect(await client.listTools()).toEqual(before); + }, +); it("runs the real static artifact workflow after deferred initialization", async () => { const { client } = await connect("legacy"); @@ -1200,7 +1126,6 @@ it("distinguishes an exhausted production discovery page from an unmatched query "auto", { REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, ["--max-old-space-size=1024"], ); diff --git a/tests/boundary/mcp/electronFusesMcp.test.ts b/tests/boundary/mcp/electronFusesMcp.test.ts index 01b1b2daaf..345c45e76e 100644 --- a/tests/boundary/mcp/electronFusesMcp.test.ts +++ b/tests/boundary/mcp/electronFusesMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import type { Client } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { writeFile } from "node:fs/promises"; @@ -64,11 +65,13 @@ fuseMcpTest( const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_electron_fuses", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Electron fuse inspection must publish both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); expect(advertised.annotations).toMatchObject({ diff --git a/tests/boundary/mcp/electronMcp.test.ts b/tests/boundary/mcp/electronMcp.test.ts index 51f7f95901..3c9e09cf22 100644 --- a/tests/boundary/mcp/electronMcp.test.ts +++ b/tests/boundary/mcp/electronMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; import { electronActiveObservationInputSchema } from "../../../src/domain/javascript/electronActiveObservation.js"; @@ -509,12 +510,12 @@ it("runs active Electron scenarios with selected paths and inferred working dire const advertised = (await client.listTools()).tools.find( ({ name }) => name === "capture_electron_scenario", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Active Electron output schema was not advertised"); const validateOutput = new Ajv2020({ strict: false, validateFormats: false, - }).compile(z.record(z.string(), z.unknown()).parse(advertised.outputSchema)); + }).compile(canonicalOutputSchema(advertised.name)); const captured = await client.callTool({ name: "capture_electron_scenario", arguments: { diff --git a/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts b/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts index 74544340bc..4004b7fc31 100644 --- a/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts +++ b/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts @@ -1,3 +1,4 @@ +import { canonicalInputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { afterEach, describe, expect, it } from "vitest"; import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; @@ -473,9 +474,10 @@ describe("enhanced MCP batches", () => { }, ); const { tools } = await client.listTools(); - expect( - tools.find(({ name }) => name === "batch_decompile")?.inputSchema, - ).toMatchObject({ + expect(tools.some(({ name }) => name === "batch_decompile")).toBe(true); + // The budget is a canonical input constraint; the server enforces it + // whatever the compact advertisement retains. + expect(canonicalInputSchema("batch_decompile")).toMatchObject({ properties: { addresses: { maxItems: MAX_ENHANCED_RECORDS } }, }); diff --git a/tests/boundary/mcp/environmentBundleMcp.test.ts b/tests/boundary/mcp/environmentBundleMcp.test.ts index c3ab6b8d1e..53e0e57062 100644 --- a/tests/boundary/mcp/environmentBundleMcp.test.ts +++ b/tests/boundary/mcp/environmentBundleMcp.test.ts @@ -32,7 +32,7 @@ const connect = async () => { const server = createServer( { kind: "session", session }, { - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, ); const client = new Client({ name: "environment-bundle-mcp", version: "1" }); diff --git a/tests/boundary/mcp/errorEvidenceLifecycleMcp.test.ts b/tests/boundary/mcp/errorEvidenceLifecycleMcp.test.ts index 5a4ccbb8ed..85c84c1fad 100644 --- a/tests/boundary/mcp/errorEvidenceLifecycleMcp.test.ts +++ b/tests/boundary/mcp/errorEvidenceLifecycleMcp.test.ts @@ -92,7 +92,7 @@ const customFixture = () => { mcpServer: server, delivery, logger: silentLogger, - environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: {}, }, }), }; diff --git a/tests/boundary/mcp/espFlashImageMcp.test.ts b/tests/boundary/mcp/espFlashImageMcp.test.ts index 5c90bc5e83..47f2c445c3 100644 --- a/tests/boundary/mcp/espFlashImageMcp.test.ts +++ b/tests/boundary/mcp/espFlashImageMcp.test.ts @@ -68,7 +68,7 @@ const completeBootloaderSchema = z.object({ // The compiled stdio fixture checks advertised schemas against their dialect // and validates actual tool responses. -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "reads a flash dump at the caller's origin and keeps inputs in its identity (%s)", async (profile) => { const root = await createTestTempDirectory("rea-esp-flash-mcp-"); diff --git a/tests/boundary/mcp/espImageMcp.test.ts b/tests/boundary/mcp/espImageMcp.test.ts index 1d1082bf7e..3a632090d4 100644 --- a/tests/boundary/mcp/espImageMcp.test.ts +++ b/tests/boundary/mcp/espImageMcp.test.ts @@ -19,7 +19,7 @@ import { analysisViewResultSchema } from "../../../src/domain/analysisView/analy // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. // Full-profile retained-view schema work needs the compiled MCP boundary budget. -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "exposes ESP image facts and segment extraction through MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-esp-mcp-"); diff --git a/tests/boundary/mcp/evidenceAtomicRetentionRecovery.test.ts b/tests/boundary/mcp/evidenceAtomicRetentionRecovery.test.ts index 11e5abe76d..ae9bfaf6ef 100644 --- a/tests/boundary/mcp/evidenceAtomicRetentionRecovery.test.ts +++ b/tests/boundary/mcp/evidenceAtomicRetentionRecovery.test.ts @@ -14,8 +14,6 @@ import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchem it.each([ { profile: "compact", preexisting: false }, { profile: "compact", preexisting: true }, - { profile: "full", preexisting: false }, - { profile: "full", preexisting: true }, ] as const)( "preserves the completed comparison on atomic retention failure ($profile schema, preexisting: $preexisting)", async ({ profile, preexisting }) => { diff --git a/tests/boundary/mcp/evidenceInputContractsMcp.test.ts b/tests/boundary/mcp/evidenceInputContractsMcp.test.ts index 47013815af..0ee2d9dc27 100644 --- a/tests/boundary/mcp/evidenceInputContractsMcp.test.ts +++ b/tests/boundary/mcp/evidenceInputContractsMcp.test.ts @@ -63,18 +63,16 @@ describe("Evidence input SDK boundaries", () => { expect(properties.left_observations).toMatchObject({ type: "array" }); }); - mcpTest.for(["full", "compact"] as const)( - "advertises valid reference inputs after actual SDK conversion (%s)", - async (profile, { mcp }) => { + mcpTest( + "advertises valid reference inputs after actual SDK conversion", + async ({ mcp }) => { const contract = toolContract("build_call_path"); const server = new EvidenceMcpServer( { name: "evidence-contract", version: "1" }, { capabilities: {} }, undefined, createToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE), - profile === "compact" - ? { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES } - : undefined, + { budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES }, ); server.registerTool( contract.name, @@ -90,8 +88,7 @@ describe("Evidence input SDK boundaries", () => { const tool = tools.find(({ name }) => name === contract.name); if (tool === undefined) throw new Error("named contract not advertised"); // Compact input guidance is part of the tool description. - const guidance = - profile === "compact" ? tool.description : tool.inputSchema.description; + const guidance = tool.description; expect(guidance).toContain( '{"kind":"retained-evidence","evidence_id":"ev_..."}', ); diff --git a/tests/boundary/mcp/evidenceLinkInspectionMcp.test.ts b/tests/boundary/mcp/evidenceLinkInspectionMcp.test.ts index 7da3a8ff1f..54f3d7ba4c 100644 --- a/tests/boundary/mcp/evidenceLinkInspectionMcp.test.ts +++ b/tests/boundary/mcp/evidenceLinkInspectionMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterAll, expect, it } from "vitest"; @@ -48,7 +49,7 @@ const PARTIAL = [top, middle, leaf, unrelated]; const connect = async ( options: { - readonly profile?: "full" | "compact"; + readonly profile?: "compact"; readonly records?: readonly ReturnType[]; readonly maximumResponseBytes?: number; } = {}, @@ -63,8 +64,8 @@ const connect = async ( serverOptions: { environment: { ...process.env, - REA_MCP_INPUT_SCHEMA_PROFILE: options.profile ?? "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "full", + REA_MCP_INPUT_SCHEMA_PROFILE: options.profile ?? "compact", + REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "compact", }, ...(options.maximumResponseBytes === undefined ? {} @@ -76,7 +77,7 @@ const connect = async ( // Every delivered result must satisfy what this profile actually advertises. const { tools } = await client.listTools(); const advertised = tools.find(({ name }) => name === "get_evidence_bundle"); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("get_evidence_bundle advertises no output schema"); const reply = (args: Record) => client.callTool( @@ -84,7 +85,9 @@ const connect = async ( { toolDefinition: advertised }, ); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); const validation = schemaValidation.assertions; expect(validation.validateSchema(inputSchema)).toBe(true); expect(validation.validateSchema(outputSchema)).toBe(true); @@ -113,7 +116,7 @@ const exactly = (record_: { readonly evidence_id: string }) => ({ filters: { evidence_id: record_.evidence_id }, }); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "reads direct membership against the whole session, not the selection (%s)", async (profile) => { const { links, summary } = await connect({ profile }); diff --git a/tests/boundary/mcp/evidenceRecordSelectionMcp.test.ts b/tests/boundary/mcp/evidenceRecordSelectionMcp.test.ts index afb9aec137..7cbbd39986 100644 --- a/tests/boundary/mcp/evidenceRecordSelectionMcp.test.ts +++ b/tests/boundary/mcp/evidenceRecordSelectionMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterAll, expect, it } from "vitest"; @@ -60,7 +61,7 @@ const canonical = (records: readonly Record_[]) => const connect = async ( options: { - readonly profile?: "full" | "compact"; + readonly profile?: "compact"; readonly maximumResponseBytes?: number; readonly records?: readonly ReturnType[]; } = {}, @@ -75,7 +76,7 @@ const connect = async ( serverOptions: { environment: { ...process.env, - REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "full", + REA_MCP_OUTPUT_SCHEMA_PROFILE: options.profile ?? "compact", }, ...(options.maximumResponseBytes === undefined ? {} @@ -93,7 +94,7 @@ const connect = async ( const reply = (args: Record) => client.callTool( { name: "get_evidence_bundle", arguments: args }, - { toolDefinition }, + { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, ); const select = async (args: Record) => { const result = await reply({ detail: "records", ...args }); @@ -108,7 +109,7 @@ const connect = async ( return { client, session, reply, select }; }; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "returns selected complete records with retained identities (%s)", async (profile) => { const { client, select } = await connect({ profile }); diff --git a/tests/boundary/mcp/evidenceRetentionRecovery.test.ts b/tests/boundary/mcp/evidenceRetentionRecovery.test.ts index 0d30615509..4b3202e9be 100644 --- a/tests/boundary/mcp/evidenceRetentionRecovery.test.ts +++ b/tests/boundary/mcp/evidenceRetentionRecovery.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -19,7 +20,7 @@ import { createServer } from "../../../src/server/createServer.js"; import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; import { createToolResultDelivery } from "../../../src/server/toolResult.js"; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "delivers authenticated but unretained provider Evidence inline (%s schema)", async (profile) => { const identity = { @@ -73,7 +74,7 @@ it.each(["compact", "full"] as const)( name: "address_name", arguments: { address: "0x100", document: "fixture" }, }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(result.isError).toBe(true); const error = parseMcpToolError(result).error; diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index 240b3bc0d0..0a1b98d3ad 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; import { advertisedMcpToolSchemas, @@ -70,10 +71,8 @@ mcpTest( name: "evm-detail", version: "1", }); - const { inputSchema } = await advertisedMcpToolSchemas( - client, - "inspect_evm_interface", - ); + const { canonicalInputSchema: inputSchema } = + await advertisedMcpToolSchemas(client, "inspect_evm_interface"); expect(inputSchema).toHaveProperty(["properties", "detail"], { type: "string", enum: ["complete", "summary"], @@ -362,11 +361,12 @@ it("returns the exact inferred interface summary while preserving complete defau const definition = (await harness.client.listTools()).tools.find( (tool) => tool.name === "inspect_evm_interface", ); - if (definition?.outputSchema === undefined) - throw new Error("Missing EVM output schema"); + if (definition === undefined) throw new Error("Missing EVM output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = definition.inputSchema; - const outputSchema: Record = definition.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const input = { path: value.artifact.path, encoding: "hex" }; diff --git a/tests/boundary/mcp/featureTraceViewsMcp.test.ts b/tests/boundary/mcp/featureTraceViewsMcp.test.ts index 98190b91b4..5e70888d6f 100644 --- a/tests/boundary/mcp/featureTraceViewsMcp.test.ts +++ b/tests/boundary/mcp/featureTraceViewsMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -68,10 +69,7 @@ contextBridge.exposeInMainWorld("api",{${CHANNELS.map( return root; }; -const connect = async ( - profile: "full" | "compact", - maximumResponseBytes?: number, -) => { +const connect = async (profile: "compact", maximumResponseBytes?: number) => { const application = await analyzeJavaScriptApplication({ input_path: await writeApplication(), }); @@ -117,7 +115,7 @@ const bytes = (value: unknown): number => type Harness = Awaited>; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "returns selected feature-trace facts and retains the complete trace (%s)", async (profile) => { const harness = await connect(profile); @@ -169,7 +167,9 @@ it.each(["full", "compact"] as const)( ({ name }) => name === "trace_application_feature", )!; expect(ajv.validateSchema(traceTool.inputSchema)).toBe(true); - const outputSchema: Record = traceTool.outputSchema!; + const outputSchema: Record = canonicalOutputSchema( + traceTool.name, + ); expect(ajv.validateSchema(outputSchema)).toBe(true); const request = { kind: "feature-trace", @@ -341,7 +341,7 @@ const assertRetainedTracePages = async ( }; it("keeps a no-match trace's discovery and empty collections explicit", async () => { - const { inputs, call } = await connect("full"); + const { inputs, call } = await connect("compact"); const missing = { ...inputs, seed: { kind: "string", value: "absent-literal", match: "exact" }, @@ -408,7 +408,7 @@ it("refuses a selected view before tracing when the server retains no Evidence", it("recovers an oversized completed trace through its retained paths page", async () => { const budget = 64 * 1024; - const { session, inputs, call, reply } = await connect("full", budget); + const { session, inputs, call, reply } = await connect("compact", budget); const failed = await reply("trace_application_feature", inputs); const error = parseMcpToolError(failed).error; expect(error.code).toBe("resource_constraint"); @@ -460,7 +460,7 @@ it("recovers an oversized completed trace through its retained paths page", asyn it("names the retained trace and a smaller page when a selected page is oversized", async () => { const budget = 64 * 1024; - const { session, inputs, reply } = await connect("full", budget); + const { session, inputs, reply } = await connect("compact", budget); const selection = { kind: "feature-trace", collection: "nodes", diff --git a/tests/boundary/mcp/firmwareContainersMcp.test.ts b/tests/boundary/mcp/firmwareContainersMcp.test.ts index 577dac3213..7320be56d4 100644 --- a/tests/boundary/mcp/firmwareContainersMcp.test.ts +++ b/tests/boundary/mcp/firmwareContainersMcp.test.ts @@ -28,7 +28,7 @@ const ABSOLUTE_REGION = "family-0xe48bff57/0x10ffff00-0x10ffffff.bin"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "exposes UF2 facts, retained regions and extraction through MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-uf2-mcp-"); diff --git a/tests/boundary/mcp/fitImageMcp.test.ts b/tests/boundary/mcp/fitImageMcp.test.ts index 6d41900333..ba7d3b468f 100644 --- a/tests/boundary/mcp/fitImageMcp.test.ts +++ b/tests/boundary/mcp/fitImageMcp.test.ts @@ -15,7 +15,7 @@ import { analysisViewResultSchema } from "../../../src/domain/analysisView/analy // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "exposes FIT facts and image extraction through MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-fit-mcp-"); diff --git a/tests/boundary/mcp/flutterMcp.test.ts b/tests/boundary/mcp/flutterMcp.test.ts index 6e1bb2e303..ababb8993d 100644 --- a/tests/boundary/mcp/flutterMcp.test.ts +++ b/tests/boundary/mcp/flutterMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalInputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { expect, it, onTestFinished } from "vitest"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -90,8 +91,10 @@ it.each([ ); if (tool === undefined) throw new Error("Missing AOT tool"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - expect(ajv.validateSchema(tool.inputSchema)).toBe(true); - expect(ajv.validate(tool.inputSchema, { path, abi: label })).toBe(true); + expect(ajv.validateSchema(canonicalInputSchema(tool.name))).toBe(true); + expect( + ajv.validate(canonicalInputSchema(tool.name), { path, abi: label }), + ).toBe(true); }, ); @@ -213,7 +216,7 @@ it("advertises and enforces the same Unicode selector grammar through MCP", asyn const harness = await createApplicationMcpHarness({ serverOptions: { flutterAnalysis: fakeProvider(), - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, }); onTestFinished(harness.close); @@ -222,7 +225,7 @@ it("advertises and enforces the same Unicode selector grammar through MCP", asyn ); if (tool === undefined) throw new Error("Missing AOT tool"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - expect(ajv.validateSchema(tool.inputSchema)).toBe(true); + expect(ajv.validateSchema(canonicalInputSchema(tool.name))).toBe(true); for (const abi of [ "😀".repeat(1010), "l".repeat(1010), @@ -232,7 +235,9 @@ it("advertises and enforces the same Unicode selector grammar through MCP", asyn ), ]) { const arguments_ = { path: "/fixture.apk", abi }; - expect(ajv.validate(tool.inputSchema, arguments_)).toBe(true); + expect(ajv.validate(canonicalInputSchema(tool.name), arguments_)).toBe( + true, + ); expect( ( await harness.client.callTool({ @@ -251,7 +256,9 @@ it("advertises and enforces the same Unicode selector grammar through MCP", asyn ), ] as const) { const arguments_ = { path: "/fixture.apk", abi }; - expect(ajv.validate(tool.inputSchema, arguments_)).toBe(false); + expect(ajv.validate(canonicalInputSchema(tool.name), arguments_)).toBe( + false, + ); const refused = await harness.client.callTool({ name: "inspect_dart_aot", arguments: arguments_, diff --git a/tests/boundary/mcp/fridaMcp.test.ts b/tests/boundary/mcp/fridaMcp.test.ts index 3c5ef01d45..2ab376a613 100644 --- a/tests/boundary/mcp/fridaMcp.test.ts +++ b/tests/boundary/mcp/fridaMcp.test.ts @@ -1,3 +1,7 @@ +import { + canonicalOutputSchema, + withCanonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import type { AnySchema } from "ajv"; @@ -200,10 +204,12 @@ it("delivers Frida status summaries with complete retained Evidence for both tru ); const tools = (await client.listTools()).tools; const tool = tools.find((t) => t.name === "frida_session_status"); - if (!tool?.outputSchema) throw new Error("Missing Frida status schema"); + if (tool === undefined) throw new Error("Missing Frida status schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(tool.inputSchema)).toBe(true); - const outputSchema: Record = tool.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + tool.name, + ); expect(ajv.validateSchema(outputSchema)).toBe(true); for (const truncated of [false, true]) { statusTruncated = truncated; @@ -228,7 +234,7 @@ it("delivers Frida status summaries with complete retained Evidence for both tru expect(explicit.structuredContent).toEqual(parent); const summary = await client.callTool( { name: tool.name, arguments: { session_id: id, detail: "summary" } }, - { toolDefinition: tool }, + { toolDefinition: withCanonicalOutputSchema(tool) }, ); expect(summary.isError).not.toBe(true); const evidence = toolContract("frida_session_status").outputSchema.parse( @@ -416,14 +422,13 @@ it("advertises Frida MCP schemas and keeps remote credentials out of results", a const tools = (await client.listTools()).tools; const advertised = tools.find((tool) => tool.name === "list_frida_devices"); - if ( - advertised?.inputSchema === undefined || - advertised.outputSchema === undefined - ) - throw new Error("Missing advertised Frida schemas"); + if (advertised === undefined) + throw new Error("Missing advertised Frida definition"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const response = await client.callTool( @@ -433,7 +438,7 @@ it("advertises Frida MCP schemas and keeps remote credentials out of results", a remote: { address: "127.0.0.1:27042", token: "synthetic-secret-token" }, }, }, - { toolDefinition: advertised }, + { toolDefinition: withCanonicalOutputSchema(advertised) }, ); expect(response.isError).not.toBe(true); expect(JSON.stringify(response.structuredContent)).not.toContain( @@ -541,10 +546,10 @@ it("advertises Frida MCP schemas and keeps remote credentials out of results", a const oneShotContract = tools.find( (tool) => tool.name === "instrument_with_frida", ); - if (oneShotContract?.outputSchema === undefined) - throw new Error("Missing one-shot Frida output schema"); + if (oneShotContract === undefined) + throw new Error("Missing one-shot Frida definition"); const validateOneShot = ajv.compile( - oneShotContract.outputSchema as AnySchema, + canonicalOutputSchema(oneShotContract.name) as AnySchema, ); expect(validateOneShot(oneShot.structuredContent)).toBe(true); diff --git a/tests/boundary/mcp/functionCallDirectionsMcp.test.ts b/tests/boundary/mcp/functionCallDirectionsMcp.test.ts index 119d32694a..bd181aacdf 100644 --- a/tests/boundary/mcp/functionCallDirectionsMcp.test.ts +++ b/tests/boundary/mcp/functionCallDirectionsMcp.test.ts @@ -36,7 +36,7 @@ const observe = (relationshipCoverage: RelationshipCoverage) => { const schemaValidation = createMcpSchemaValidation(); afterAll(() => schemaValidation.clear()); -const profiles = ["full", "compact"] as const; +const profiles = ["compact"] as const; it.each(profiles)("preserves directional gaps (%s)", async (profile) => { const session = createTestBinarySession(() => { throw new Error("Offline call paths must not launch a provider"); @@ -116,8 +116,6 @@ it.each(profiles)("preserves directional gaps (%s)", async (profile) => { }, ], }; - if (profile === "full") - expect(ajv.validate(advertised.inputSchema, malformed)).toBe(false); const rejected = await client.callTool({ name: "build_call_path", arguments: malformed, diff --git a/tests/boundary/mcp/functionComparisonCoverageMcp.test.ts b/tests/boundary/mcp/functionComparisonCoverageMcp.test.ts index 8d0971e09c..4c29ef5829 100644 --- a/tests/boundary/mcp/functionComparisonCoverageMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonCoverageMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it } from "vitest"; @@ -43,9 +44,11 @@ const expectAdvertisedComparisonSchema = async ( const advertised = (await client.listTools()).tools.find( ({ name }) => name === "compare_functions", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("compare_functions did not advertise its output schema"); - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); const validator = new Ajv2020({ strict: false, validateFormats: false }); expect(validator.validateSchema(outputSchema)).toBe(true); const validate = validator.compile(outputSchema); @@ -234,7 +237,7 @@ it.each(["complete", "partial"] as const)( }, ); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "qualifies observed calls and verifies discovery independently (%s)", async (profile) => { for (const [label, coverage, status] of [ diff --git a/tests/boundary/mcp/functionComparisonMcp.test.ts b/tests/boundary/mcp/functionComparisonMcp.test.ts index 7220b86e9c..fcd59d4cf3 100644 --- a/tests/boundary/mcp/functionComparisonMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Ajv2020 } from "ajv/dist/2020.js"; import type { AnySchema } from "ajv"; import { functionSnapshotFixture } from "../../../src/domain/functionDossierEvidence.fixture.js"; @@ -551,12 +552,11 @@ it("advertises Swift literal coverage as a pair through the MCP SDK", async () = const tool = (await harness.client.listTools()).tools.find( ({ name }) => name === "analyze_function", ); - if (tool?.outputSchema === undefined) - throw new Error("Missing output schema"); + if (tool === undefined) throw new Error("Missing output schema"); const validate = new Ajv2020({ strict: false, validateFormats: false, - }).compile(tool.outputSchema as AnySchema); + }).compile(canonicalOutputSchema(tool.name) as AnySchema); const original = FUNCTION_COMPARISON_EXAMPLE.left; for (const [literals, omissions, valid] of [ [false, false, true], diff --git a/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts b/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts index bf4929c70f..da12bbdb86 100644 --- a/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonReferenceFactsMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import type { NativeSymbolMetadata } from "../../../src/domain/native/nativeSymbol.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { McpServer } from "@modelcontextprotocol/server"; @@ -1053,11 +1054,9 @@ it("distinguishes same-entry renames from different entries through retained MCP const advertised = (await schemaClient.listTools()).tools.find( ({ name }) => name === "compare_functions", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("compare_functions output schema is missing"); - const outputSchema = z - .record(z.string(), z.unknown()) - .parse(advertised.outputSchema); + const outputSchema = canonicalOutputSchema(advertised.name); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(outputSchema)).toBe(true); const validate = ajv.compile(outputSchema); diff --git a/tests/boundary/mcp/gamecubeDolSessionMcp.test.ts b/tests/boundary/mcp/gamecubeDolSessionMcp.test.ts index fc654fede1..19420a6c64 100644 --- a/tests/boundary/mcp/gamecubeDolSessionMcp.test.ts +++ b/tests/boundary/mcp/gamecubeDolSessionMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -25,12 +26,11 @@ const validateAdvertised = ( response: CallToolResult, ) => { const tool = tools.find((candidate) => candidate.name === name); - if (tool?.outputSchema === undefined) - throw new Error(`missing ${name} output schema`); + if (tool === undefined) throw new Error(`missing ${name} output schema`); const validate = new Ajv2020({ strict: false, validateFormats: false, - }).compile(z.record(z.string(), z.unknown()).parse(tool.outputSchema)); + }).compile(canonicalOutputSchema(tool.name)); expect( validate(response.structuredContent), JSON.stringify(validate.errors), diff --git a/tests/boundary/mcp/ghidraAmbiguousProcedureMcp.test.ts b/tests/boundary/mcp/ghidraAmbiguousProcedureMcp.test.ts index d302ab8f2d..db033bddb9 100644 --- a/tests/boundary/mcp/ghidraAmbiguousProcedureMcp.test.ts +++ b/tests/boundary/mcp/ghidraAmbiguousProcedureMcp.test.ts @@ -4,7 +4,7 @@ import { GhidraSessionError } from "../../../src/ghidra/GhidraSessionError.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { connectGhidraMcp } from "./ghidraMcpHarness.js"; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "preserves actionable Ghidra procedure ambiguity over MCP (%s)", async (profile) => { const message = @@ -60,7 +60,7 @@ it.each(["compact", "full"] as const)( }, ); -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "keeps file-offset mapping ambiguity distinct from function identity (%s)", async (profile) => { const message = diff --git a/tests/boundary/mcp/ghidraContainingListingMcp.test.ts b/tests/boundary/mcp/ghidraContainingListingMcp.test.ts index 772ff05ed4..b6aa4b0658 100644 --- a/tests/boundary/mcp/ghidraContainingListingMcp.test.ts +++ b/tests/boundary/mcp/ghidraContainingListingMcp.test.ts @@ -4,7 +4,7 @@ import { compactInstruction } from "../../../src/domain/native/nativeInstruction import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "keeps the selected interior address and containing instruction separate (%s)", async (profile) => { const observed = { @@ -127,7 +127,7 @@ it.each([ }, ); -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "retains a containing data identity without selecting a field (%s)", async (profile) => { const observed = { diff --git a/tests/boundary/mcp/ghidraEvidenceMcp.test.ts b/tests/boundary/mcp/ghidraEvidenceMcp.test.ts index 086c142cca..6b5ef33e3c 100644 --- a/tests/boundary/mcp/ghidraEvidenceMcp.test.ts +++ b/tests/boundary/mcp/ghidraEvidenceMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { describe, expect, it } from "vitest"; import { z } from "zod"; @@ -339,7 +340,7 @@ const connectNamingMcp = async (name: string) => { if (toolDefinition === undefined) throw new Error(`Missing ${tool}`); return harness.mcp.callTool( { name: tool, arguments: arguments_ }, - { toolDefinition }, + { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, ); }; return { harness, requests, bridge, call }; diff --git a/tests/boundary/mcp/ghidraNativeApiMcp.test.ts b/tests/boundary/mcp/ghidraNativeApiMcp.test.ts index fdfeb8f423..626bf77962 100644 --- a/tests/boundary/mcp/ghidraNativeApiMcp.test.ts +++ b/tests/boundary/mcp/ghidraNativeApiMcp.test.ts @@ -1,3 +1,7 @@ +import { + canonicalOutputSchema, + withCanonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { describe, expect, it } from "vitest"; import { connectGhidraMcp, sessionEvidence } from "./ghidraMcpHarness.js"; import { @@ -47,14 +51,16 @@ it("retains complete load-image facts and pages selected relocations through the input: Record, ) => { const definition = catalog.tools.find((tool) => tool.name === name); - if (!definition?.outputSchema) throw new Error("Missing named schema"); + if (definition === undefined) throw new Error("Missing named schema"); const inputSchema: Record = definition.inputSchema; - const outputSchema: Record = definition.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const reply = await harness.mcp.callTool( { name, arguments: input }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(reply.isError).not.toBe(true); return sessionEvidence(harness.session, reply.structuredContent); diff --git a/tests/boundary/mcp/ghidraRegexDiagnosticsMcp.test.ts b/tests/boundary/mcp/ghidraRegexDiagnosticsMcp.test.ts index 54276721f8..1d979c7552 100644 --- a/tests/boundary/mcp/ghidraRegexDiagnosticsMcp.test.ts +++ b/tests/boundary/mcp/ghidraRegexDiagnosticsMcp.test.ts @@ -4,7 +4,7 @@ import { GhidraSessionError } from "../../../src/ghidra/GhidraSessionError.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { connectGhidraMcp } from "./ghidraMcpHarness.js"; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "identifies the invalid Ghidra regex field and permits correction (%s)", async (profile) => { const message = diff --git a/tests/boundary/mcp/goBinaryMcp.test.ts b/tests/boundary/mcp/goBinaryMcp.test.ts index e3dc205f97..2882e506b1 100644 --- a/tests/boundary/mcp/goBinaryMcp.test.ts +++ b/tests/boundary/mcp/goBinaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { goBinarySchema } from "../../../src/domain/go/goBinary.js"; import type { Client } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -315,11 +316,13 @@ goMcpTest( const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_go_binary", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Go metadata must publish both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); expect( diff --git a/tests/boundary/mcp/goBinaryViews.test.ts b/tests/boundary/mcp/goBinaryViews.test.ts index b2a2e65579..f25b6d1d63 100644 --- a/tests/boundary/mcp/goBinaryViews.test.ts +++ b/tests/boundary/mcp/goBinaryViews.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { readFile, unlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterAll, expect } from "vitest"; @@ -67,7 +68,7 @@ const goViewTest = mcpTest.extend<{ tool.name === "inspect_analysis_view", ) .map((tool) => { - const output = tool.outputSchema; + const output = canonicalOutputSchema(tool.name); if (output === undefined) throw new Error(`Missing ${tool.name} output schema`); expect(output.$schema).toBe( diff --git a/tests/boundary/mcp/godotPckMcp.test.ts b/tests/boundary/mcp/godotPckMcp.test.ts index 35b85033b8..00edc7a6ae 100644 --- a/tests/boundary/mcp/godotPckMcp.test.ts +++ b/tests/boundary/mcp/godotPckMcp.test.ts @@ -1,3 +1,8 @@ +import { + canonicalOutputSchema, + canonicalInputSchema, + withCanonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { writeFile, unlink } from "node:fs/promises"; import { createHash } from "node:crypto"; import { join } from "node:path"; @@ -33,14 +38,18 @@ const viewValidators = async (client: Client) => { const contract = catalog.tools.find( (tool) => tool.name === "inspect_analysis_view", ); - if (contract?.outputSchema === undefined) + if (contract === undefined) throw new Error("Missing named view output contract"); const ajv = schemaValidation.assertions; - const inputSchema: Record = contract.inputSchema; - const outputSchema: Record = contract.outputSchema; + const inputSchema = canonicalInputSchema(contract.name); + const outputSchema = canonicalOutputSchema(contract.name); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); - return { ajv, inputSchema, toolDefinition: contract }; + return { + ajv, + inputSchema, + toolDefinition: withCanonicalOutputSchema(contract), + }; }; const retainedInventoryView = @@ -117,7 +126,7 @@ const expectedPckMembers = ( return expected; }; -const profiles = ["full", "compact"] as const; +const profiles = ["compact"] as const; const members = [ { path: "res://a.txt", data: "a" }, { path: "res://b.txt", data: "b" }, @@ -184,11 +193,7 @@ it.for(inventoryCases)( ).toBe(nested.normalized_result); const graph = nested.normalized_result; const view = retainedInventoryView(client, parent.evidence_id, validators); - await assertInvalidNodeField( - client, - parent.evidence_id, - profile === "full" ? validators : undefined, - ); + await assertInvalidNodeField(client, parent.evidence_id, undefined); const wanted = graph.occurrences.filter((row) => row.logical_path !== "."); const expectedMembers = expectedPckMembers(members, wanted); const occurrences = await view({ diff --git a/tests/boundary/mcp/guidedWorkflowsMcp.test.ts b/tests/boundary/mcp/guidedWorkflowsMcp.test.ts index 1f4799ba61..dc3cebd513 100644 --- a/tests/boundary/mcp/guidedWorkflowsMcp.test.ts +++ b/tests/boundary/mcp/guidedWorkflowsMcp.test.ts @@ -6,13 +6,11 @@ import { promptsForToolFamilies } from "../../../src/contracts/promptToolFamilyS import { toolFamilyId } from "../../../src/contracts/toolFamilies.js"; it.each([ - ["full", ""], ["compact", ""], - ["full", ",native,artifact,android,managed"], ["compact", ",native,artifact,android,managed"], - ["full", ",native"], - ["full", ",artifact"], - ["full", ",android"], + ["compact", ",native"], + ["compact", ",artifact"], + ["compact", ",android"], ])( "preserves workflow contracts through the production %s MCP server with extra families '%s'", async (profile, extraFamilies) => { diff --git a/tests/boundary/mcp/hexRecordsMcp.test.ts b/tests/boundary/mcp/hexRecordsMcp.test.ts index 0f481def0e..441935fe40 100644 --- a/tests/boundary/mcp/hexRecordsMcp.test.ts +++ b/tests/boundary/mcp/hexRecordsMcp.test.ts @@ -12,7 +12,7 @@ import { analysisViewResultSchema } from "../../../src/domain/analysisView/analy // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "preserves S28 high address bytes through MCP inventory and extraction (%s)", async (profile) => { const root = await createTestTempDirectory("rea-s28-mcp-"); @@ -62,7 +62,7 @@ it.each(["full", "compact"] as const)( }, ); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "exposes S-record facts and region extraction through MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-hex-mcp-"); diff --git a/tests/boundary/mcp/javascriptFactsViewMcp.test.ts b/tests/boundary/mcp/javascriptFactsViewMcp.test.ts index 44ca4d2125..a9e8669ef9 100644 --- a/tests/boundary/mcp/javascriptFactsViewMcp.test.ts +++ b/tests/boundary/mcp/javascriptFactsViewMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; @@ -41,11 +42,11 @@ const facts = async ( const matches = (await harness.client.listTools()).tools.filter( (tool) => tool.name === "inspect_analysis_view", ); - if (matches.length !== 1 || matches[0]?.outputSchema === undefined) - throw new Error("Expected one advertised analysis-view output schema"); + if (matches.length !== 1 || matches[0] === undefined) + throw new Error("Expected one advertised analysis-view definition"); const wire = await harness.client.callTool( { name: "inspect_analysis_view", arguments: { source, view } }, - { toolDefinition: matches[0] }, + { toolDefinition: withCanonicalOutputSchema(matches[0]) }, ); expect(wire.isError).not.toBe(true); const evidence = parseEvidence(wire.structuredContent); diff --git a/tests/boundary/mcp/javascriptRecoveryMcp.test.ts b/tests/boundary/mcp/javascriptRecoveryMcp.test.ts index b9eaf79dc1..b2711c2318 100644 --- a/tests/boundary/mcp/javascriptRecoveryMcp.test.ts +++ b/tests/boundary/mcp/javascriptRecoveryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { chmod, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -42,11 +43,16 @@ it const advertised = tools.tools.find( (tool) => tool.name === "recover_javascript_sources", ); - if (advertised === undefined || advertised.outputSchema === undefined) + if ( + advertised === undefined || + canonicalOutputSchema(advertised.name) === undefined + ) throw new Error("recovery tool must advertise both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const additional = join(fixture.root, "chunk.js"); diff --git a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts index 02853783ef..3be527c855 100644 --- a/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts +++ b/tests/boundary/mcp/javascriptRuntimeObservationMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { ok as resultOk } from "../../../src/domain/result.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -297,10 +298,12 @@ const assertSdkObservationSchema = async ( const observeTool = (await client.listTools()).tools.find( ({ name }) => name === "observe_javascript_runtime", ); - if (observeTool?.outputSchema === undefined) + if (observeTool === undefined) throw new Error("Missing Inspector observation output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const schema: Record = observeTool.outputSchema; + const schema: Record = canonicalOutputSchema( + observeTool.name, + ); expect(ajv.validateSchema(schema)).toBe(true); expect(ajv.validate(schema, content), JSON.stringify(ajv.errors)).toBe(true); }; diff --git a/tests/boundary/mcp/javascriptSourceSelection.test.ts b/tests/boundary/mcp/javascriptSourceSelection.test.ts index 27665d6b19..481bc8c995 100644 --- a/tests/boundary/mcp/javascriptSourceSelection.test.ts +++ b/tests/boundary/mcp/javascriptSourceSelection.test.ts @@ -1,3 +1,7 @@ +import { + canonicalInputSchema, + canonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { Client } from "@modelcontextprotocol/client"; import { createPackageWithOptions } from "@electron/asar"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -41,8 +45,6 @@ const createSelectionClient = () => { PATH: process.env.PATH ?? "", NODE_OPTIONS: "--max-old-space-size=512 --max-semi-space-size=8", // Exact advertised constraints require the complete schema profile. - REA_MCP_INPUT_SCHEMA_PROFILE: "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, stderr: "pipe", }); @@ -143,7 +145,7 @@ it("advertises exact source selection and retains its scope through summary, Evi const validate = new Ajv2020({ strict: false, validateFormats: false, - }).compile(schema); + }).compile(canonicalInputSchema("analyze_javascript_application")); expect(validate({ input_path: root, source_paths: ["main.mjs"] })).toBe( true, ); @@ -158,14 +160,12 @@ it("advertises exact source selection and retains its scope through summary, Evi }, }); expect(selected.isError, JSON.stringify(selected.content)).not.toBe(true); - if (definition?.outputSchema === undefined) + if (definition === undefined) throw new Error("Missing advertised application output contract"); const validateOutput = new Ajv2020({ strict: false, validateFormats: false, - }).compile( - z.record(z.string(), z.unknown()).parse(definition.outputSchema), - ); + }).compile(canonicalOutputSchema(definition.name)); expect( validateOutput(selected.structuredContent), JSON.stringify(validateOutput.errors), diff --git a/tests/boundary/mcp/jebMcp.test.ts b/tests/boundary/mcp/jebMcp.test.ts index ea8cb1aedd..a54ea7139e 100644 --- a/tests/boundary/mcp/jebMcp.test.ts +++ b/tests/boundary/mcp/jebMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Ajv2020 } from "ajv/dist/2020.js"; import { afterAll, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; @@ -230,7 +231,8 @@ it("advertises exact valid JEB schemas and records inline evidence through MCP", throw new Error("JEB tools must publish both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(advertised.inputSchema)).toBe(true); - const outputSchema: Record = advertised.outputSchema ?? {}; + const outputSchema: Record = + canonicalOutputSchema(advertised.name) ?? {}; expect(ajv.validateSchema(outputSchema)).toBe(true); expect(ajv.validate(advertised.inputSchema, {})).toBe(true); expect(ajv.validate(advertised.inputSchema, { unexpected: 1 })).toBe(false); diff --git a/tests/boundary/mcp/kernelModuleMcp.test.ts b/tests/boundary/mcp/kernelModuleMcp.test.ts index 1eb0b8dd8c..d423d316f2 100644 --- a/tests/boundary/mcp/kernelModuleMcp.test.ts +++ b/tests/boundary/mcp/kernelModuleMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { Ajv2020 } from "ajv/dist/2020.js"; import type { AnySchema } from "ajv"; @@ -149,10 +150,7 @@ mcpTest( arguments: { path }, }); expect(response.isError, JSON.stringify(response.content)).not.toBe(true); - const outputSchema = (await client.listTools()).tools.find( - (tool) => tool.name === "inspect_kernel_module", - )?.outputSchema; - if (outputSchema === undefined) throw new Error("Missing output schema"); + const outputSchema = canonicalOutputSchema("inspect_kernel_module"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(outputSchema as AnySchema)).toBe(true); expect( diff --git a/tests/boundary/mcp/largeToolResult.test.ts b/tests/boundary/mcp/largeToolResult.test.ts index 023fe7f66b..be8e956823 100644 --- a/tests/boundary/mcp/largeToolResult.test.ts +++ b/tests/boundary/mcp/largeToolResult.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Ajv2020 } from "ajv/dist/2020.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { @@ -89,7 +90,7 @@ const callPseudoCode = async ( const advertised = (await client.listTools()).tools.find( ({ name }) => name === "procedure_pseudo_code", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing named procedure_pseudo_code output schema"); const result = await client.callTool({ name: "procedure_pseudo_code", @@ -99,7 +100,9 @@ const callPseudoCode = async ( result, responseWire, records, - outputSchema: jsonObjectSchema.parse(advertised.outputSchema), + outputSchema: jsonObjectSchema.parse( + canonicalOutputSchema(advertised.name), + ), }; } finally { await Promise.all([client.close(), server.close()]); diff --git a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts index 43fa6d342c..dcea34b76f 100644 --- a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile, unlink } from "node:fs/promises"; import { join } from "node:path"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -42,9 +43,10 @@ it("returns a first-call overview and pages the unchanged source after its file const { client, session } = await createHarness(); const catalog = (await client.listTools()).tools; const definition = catalog.find((x) => x.name === "inspect_managed_artifact"); - if (definition?.outputSchema === undefined) - throw new Error("Managed schemas are absent"); - const outputSchema: Record = definition.outputSchema; + if (definition === undefined) throw new Error("Managed schemas are absent"); + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(definition.inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); diff --git a/tests/boundary/mcp/managedFieldConstantsMcp.test.ts b/tests/boundary/mcp/managedFieldConstantsMcp.test.ts index 2827e9cba4..aa8cfc5cab 100644 --- a/tests/boundary/mcp/managedFieldConstantsMcp.test.ts +++ b/tests/boundary/mcp/managedFieldConstantsMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile, unlink } from "node:fs/promises"; import { join } from "node:path"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -36,11 +37,10 @@ it("retains lossless field constants in MCP Evidence and pages them after the so const definition = tools.find( (tool) => tool.name === "inspect_managed_members", ); - if (definition?.outputSchema === undefined) - throw new Error("Member output schema missing"); + if (definition === undefined) throw new Error("Member output schema missing"); const reply = await client.callTool( { name: definition.name, arguments: { path, detail: "complete" } }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(reply.isError).not.toBe(true); const parent = parseEvidence(reply.structuredContent); @@ -60,8 +60,7 @@ it("retains lossless field constants in MCP Evidence and pages them after the so ); await unlink(path); const viewTool = tools.find((tool) => tool.name === "inspect_analysis_view"); - if (viewTool?.outputSchema === undefined) - throw new Error("View output schema missing"); + if (viewTool === undefined) throw new Error("View output schema missing"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const request = { source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, @@ -79,7 +78,7 @@ it("retains lossless field constants in MCP Evidence and pages them after the so ).toBe(true); const page = await client.callTool( { name: viewTool.name, arguments: request }, - { toolDefinition: viewTool }, + { toolDefinition: withCanonicalOutputSchema(viewTool) }, ); expect(page.isError).not.toBe(true); const view = parseEvidence(page.structuredContent).normalized_result; diff --git a/tests/boundary/mcp/managedMcp.test.ts b/tests/boundary/mcp/managedMcp.test.ts index fe66104342..e0f5552c75 100644 --- a/tests/boundary/mcp/managedMcp.test.ts +++ b/tests/boundary/mcp/managedMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createHash } from "node:crypto"; @@ -649,11 +650,9 @@ const createOutputAssertion = async (client: Client) => { ): void => { const contract = toolContract(name); const wire = tools.find((tool) => tool.name === name); - if (wire?.outputSchema === undefined) + if (wire === undefined) throw new Error(`Missing advertised output schema for ${name}`); - const valid = ajv.compile( - z.record(z.string(), z.unknown()).parse(wire.outputSchema), - ); + const valid = ajv.compile(canonicalOutputSchema(wire.name)); expect(valid(output), `${name} advertised output`).toBe(true); expect(contract.outputSchema.safeParse(output).success).toBe(true); }, diff --git a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts index c1a0d96c05..f4617d7fdd 100644 --- a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile, unlink } from "node:fs/promises"; import { join } from "node:path"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -66,10 +67,11 @@ const verifyProjection = async ({ const viewDefinition = advertised.find( (tool) => tool.name === "inspect_analysis_view", ); - if (viewDefinition?.outputSchema === undefined) - throw new Error("View schema is absent"); + if (viewDefinition === undefined) throw new Error("View schema is absent"); const viewInputSchema: Record = viewDefinition.inputSchema; - const viewOutputSchema: Record = viewDefinition.outputSchema; + const viewOutputSchema: Record = canonicalOutputSchema( + viewDefinition.name, + ); for (const collection of COLLECTIONS) { const projectedReply = await client.callTool( { @@ -197,9 +199,11 @@ it("summarizes selected members and pages every original collection after the fi const definition = advertised.find( (x) => x.name === "inspect_managed_members", ); - if (definition?.outputSchema === undefined) + if (definition === undefined) throw new Error("Managed member schemas are absent"); - const outputSchema: Record = definition.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + definition.name, + ); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect(ajv.validateSchema(definition.inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); diff --git a/tests/boundary/mcp/mcpDoctor.test.ts b/tests/boundary/mcp/mcpDoctor.test.ts index d98e31ed6c..0cdc3e4304 100644 --- a/tests/boundary/mcp/mcpDoctor.test.ts +++ b/tests/boundary/mcp/mcpDoctor.test.ts @@ -23,11 +23,10 @@ import { } from "../../../src/mcpDoctor.js"; import { DEFAULT_CATALOG_IDENTITY, - FULL_CATALOG_IDENTITY, catalogIdentityForToolFamilies, createCatalogIdentity, } from "../../../src/catalogIdentity.js"; -import { compactPresentationForMcpInputSchemaProfile } from "../../../src/config/mcpInputSchemaProfile.js"; +import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../../../src/config/mcpInputSchemaProfile.js"; import { parseConfig } from "../../../src/config/parseConfig.js"; import { runSessionStatus } from "../../../src/application/DirectAnalysisStatus.js"; import { ok } from "../../../src/domain/result.js"; @@ -145,69 +144,23 @@ const assertStatusCatalogIdentity = async ( const catalogProfiles = [ { - name: "default compact input", - cliParity: false, - environment: { - REA_MCP_INPUT_SCHEMA_PROFILE: undefined, - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", - }, - identity: createCatalogIdentity({ - compactPresentation: - compactPresentationForMcpInputSchemaProfile(undefined), - }), - }, - { - name: "full", + name: "default", cliParity: true, environment: { - REA_MCP_INPUT_SCHEMA_PROFILE: "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", + REA_MCP_INPUT_SCHEMA_PROFILE: undefined, + REA_MCP_OUTPUT_SCHEMA_PROFILE: undefined, }, - identity: FULL_CATALOG_IDENTITY, + identity: DEFAULT_CATALOG_IDENTITY, }, { - name: "compact", - cliParity: false, + name: "spelled compact input and compact output schemas", + cliParity: true, environment: { REA_MCP_INPUT_SCHEMA_PROFILE: "compact", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", - }, - identity: createCatalogIdentity({ - compactPresentation: - compactPresentationForMcpInputSchemaProfile("compact"), - }), - }, - { - name: "output schemas off", - cliParity: false, - environment: { - REA_MCP_INPUT_SCHEMA_PROFILE: "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "off", - }, - identity: createCatalogIdentity({ outputSchemaProfile: "off" }), - }, - { - name: "output schemas compact", - cliParity: false, - environment: { - REA_MCP_INPUT_SCHEMA_PROFILE: "full", REA_MCP_OUTPUT_SCHEMA_PROFILE: "compact", }, identity: createCatalogIdentity({ outputSchemaProfile: "compact" }), }, - { - name: "compact input and output schemas off", - cliParity: true, - environment: { - REA_MCP_INPUT_SCHEMA_PROFILE: "compact", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "off", - }, - identity: createCatalogIdentity({ - compactPresentation: - compactPresentationForMcpInputSchemaProfile("compact"), - outputSchemaProfile: "off", - }), - }, ]; const withoutOutputProfile = ( @@ -262,20 +215,18 @@ describe("production MCP doctor", () => { import { RECORDED_CATALOG_DIGEST_PROFILES as profiles } from "./dist/recordedCatalogDigests.js"; import { COMPACT_INPUT_SCHEMA_BUDGET_BYTES as budgetBytes } from "./dist/config/mcpInputSchemaProfile.js"; assert.ok(Object.isFrozen(profiles)); - for (const input of ["full", "compact"]) { - assert.ok(Object.isFrozen(profiles[input])); - for (const output of ["full", "compact", "off"]) { - const identity = createCatalogIdentity({compactPresentation: input === "compact" ? { budgetBytes } : undefined, outputSchemaProfile: output}); - assert.equal(identity.digests, profiles[input][output]); - assert.ok(Object.isFrozen(identity.digests)); - } + assert.deepEqual(Object.keys(profiles).sort(), ["compact", "off"]); + for (const output of ["compact", "off"]) { + const identity = createCatalogIdentity({ outputSchemaProfile: output }); + assert.equal(identity.digests, profiles[output]); + assert.ok(Object.isFrozen(identity.digests)); } const selection = { budgetBytes: 1 }; const custom = createCatalogIdentity({compactPresentation: selection, outputSchemaProfile: "off"}); selection.budgetBytes = budgetBytes; const expected = createCatalogDigests(undefined, undefined, { budgetBytes: 1 }, "off"); assert.deepEqual(custom.digests, expected); - assert.notEqual(custom.digests.tools_sha256, profiles.compact.off.tools_sha256); + assert.notEqual(custom.digests.tools_sha256, profiles.off.tools_sha256); assert.equal(custom.digests, custom.digests); `, ], @@ -336,7 +287,7 @@ describe("production MCP doctor", () => { result.inventory?.tools.observed, ); expect(result.inventory?.tools.expected).toBe( - FULL_CATALOG_IDENTITY.counts.mcp_tools, + DEFAULT_CATALOG_IDENTITY.counts.mcp_tools, ); expect(result.identity?.expected_catalog_digest).toBe( identity.digests.combined_sha256, @@ -354,7 +305,7 @@ describe("production MCP doctor", () => { describe("production MCP doctor family scopes", () => { it.each([ - { families: "session", output: "full", cliParity: false }, + { families: "session", output: "off", cliParity: false }, { families: "session,application", output: "compact", cliParity: true }, ] as const)( "reports a selected family scope healthy with its own inventory and digest ($output)", @@ -362,7 +313,6 @@ describe("production MCP doctor family scopes", () => { const environment = { ...process.env, REA_MCP_TOOL_FAMILIES: families, - REA_MCP_INPUT_SCHEMA_PROFILE: "compact", REA_MCP_OUTPUT_SCHEMA_PROFILE: output, }; const result = await runProductionMcpDoctor({ @@ -387,7 +337,7 @@ describe("production MCP doctor family scopes", () => { request_flow: { tool: "binary_session", ok: true }, }); const identity = catalogIdentityForToolFamilies( - compactPresentationForMcpInputSchemaProfile("compact"), + COMPACT_INPUT_SCHEMA_PRESENTATION, output, new Set(families.split(",")), ); @@ -609,7 +559,7 @@ describe("production MCP doctor output", () => { describe("production MCP doctor probe validation", () => { // The default lists no output schemas and `compact` lists widened ones; the // doctor validates the reply against the canonical contract under each. - it.each([undefined, "compact", "full"] as const)( + it.each([undefined, "compact"] as const)( "reports schema-invalid probes and mismatched inventory from the child (output profile %s)", async (outputProfile) => { const root = await createTestTempDirectory("rea-mcp-doctor-reply-"); @@ -681,11 +631,11 @@ describe("production MCP doctor probe validation", () => { }); expect(result.healthy).toBe(mode === "valid"); expect(result.inventory?.tools.observed).toBe( - FULL_CATALOG_IDENTITY.counts.mcp_tools + + DEFAULT_CATALOG_IDENTITY.counts.mcp_tools + (mode === "inventory" ? 1 : 0), ); expect(result.inventory?.prompts.observed).toBe( - FULL_CATALOG_IDENTITY.counts.mcp_prompts, + DEFAULT_CATALOG_IDENTITY.counts.mcp_prompts, ); expect(result.checks).toContainEqual({ name: "initialize", @@ -710,8 +660,8 @@ describe("production MCP doctor probe validation", () => { }); if (mode === "inventory") { expect(result.inventory?.tools).toMatchObject({ - expected: FULL_CATALOG_IDENTITY.counts.mcp_tools, - observed: FULL_CATALOG_IDENTITY.counts.mcp_tools + 1, + expected: DEFAULT_CATALOG_IDENTITY.counts.mcp_tools, + observed: DEFAULT_CATALOG_IDENTITY.counts.mcp_tools + 1, missing: [removedToolName], unexpected: ["doctor_unexpected_fixture"], duplicates: ["doctor_unexpected_fixture"], diff --git a/tests/boundary/mcp/nativeAddressContextMcp.test.ts b/tests/boundary/mcp/nativeAddressContextMcp.test.ts index 2fe4390840..8fa5f79ef6 100644 --- a/tests/boundary/mcp/nativeAddressContextMcp.test.ts +++ b/tests/boundary/mcp/nativeAddressContextMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { afterEach, expect, it } from "vitest"; import { Ajv2020 } from "ajv/dist/2020.js"; import type { AnySchemaObject } from "ajv"; @@ -55,10 +56,7 @@ const overviewClient = (segments: SegmentSummary[]) => it("preserves observed coordinates and main's optional-string output contract after SDK conversion", async () => { const client = await overviewClient([segment]); - const schema = (await client.listTools()).tools.find( - ({ name }) => name === "binary_overview", - )?.outputSchema; - if (schema === undefined) throw new Error("Missing overview output schema"); + const schema = canonicalOutputSchema("binary_overview"); const validate = new Ajv2020({ strict: false }).compile( schema as AnySchemaObject, ); diff --git a/tests/boundary/mcp/nativeFeatureTraceViews.test.ts b/tests/boundary/mcp/nativeFeatureTraceViews.test.ts index 42df465c91..339dafee30 100644 --- a/tests/boundary/mcp/nativeFeatureTraceViews.test.ts +++ b/tests/boundary/mcp/nativeFeatureTraceViews.test.ts @@ -24,7 +24,7 @@ const traceFixture = () => ({ residual_unknowns: ["0x1010 owner unknown", "0x1020 owner unknown"], }); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "pages retained native trace rows and preserves source coverage across %s offline handoff", async (profile) => { const result = traceFixture(); diff --git a/tests/boundary/mcp/nativeInstructionReferencesView.test.ts b/tests/boundary/mcp/nativeInstructionReferencesView.test.ts index 45b3456725..4538496624 100644 --- a/tests/boundary/mcp/nativeInstructionReferencesView.test.ts +++ b/tests/boundary/mcp/nativeInstructionReferencesView.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; @@ -134,7 +135,7 @@ const checkContinuation = async ( expect(original).toEqual(referenceRows); }; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "retains and pages instruction reference states through %s MCP", async (profile) => { let incoming: "rows" | "empty" | "unavailable" | "not-recorded" = "rows"; @@ -168,10 +169,12 @@ it.each(["full", "compact"] as const)( const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_analysis_view", ); - if (!advertised?.outputSchema) + if (advertised === undefined) throw new Error("Missing advertised view schema"); expect( - validation.assertions.validateSchema(advertised.outputSchema), + validation.assertions.validateSchema( + canonicalOutputSchema(advertised.name), + ), ).toBe(true); expect( ( diff --git a/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts b/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts index e01f23dd6b..42ba1d91fb 100644 --- a/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Ajv2020 } from "ajv/dist/2020.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it, onTestFinished } from "vitest"; @@ -98,19 +99,21 @@ it("shares complete/summary source identity and advertised contracts across all const tools = (await harness.mcp.listTools()).tools; const ajv = new Ajv2020({ strict: false, validateFormats: false }); const viewTool = tools.find((tool) => tool.name === "inspect_analysis_view"); - if (viewTool?.outputSchema === undefined) - throw new Error("Missing view schema"); + if (viewTool === undefined) throw new Error("Missing view schema"); const viewInputSchema: Record = viewTool.inputSchema; - const viewOutputSchema: Record = viewTool.outputSchema; + const viewOutputSchema: Record = canonicalOutputSchema( + viewTool.name, + ); expect(ajv.validateSchema(viewInputSchema)).toBe(true); expect(ajv.validateSchema(viewOutputSchema)).toBe(true); for (const operation of NATIVE_INVENTORY_OPERATIONS) { const contract = toolContract(operation); const advertised = tools.find((tool) => tool.name === operation); - if (advertised?.outputSchema === undefined) - throw new Error("Missing operation schema"); + if (advertised === undefined) throw new Error("Missing operation schema"); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const parameters = operation.startsWith("search_") diff --git a/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts b/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts index cb50b6bae4..9f76a0c2cd 100644 --- a/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts +++ b/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { copyFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -168,7 +169,7 @@ mcpTest( createServer( { kind: "session", session }, { - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, ), undefined, @@ -180,7 +181,7 @@ mcpTest( schemaValidation.assertions, ); expect( - tool.ajv.validate(tool.outputSchema, source), + tool.ajv.validate(canonicalOutputSchema(tool.name), source), JSON.stringify(tool.ajv.errors), ).toBe(true); const view = await advertisedMcpToolSchemas( @@ -247,7 +248,7 @@ mcpTest( }, ); expect( - tool.ajv.validate(tool.outputSchema, historical), + tool.ajv.validate(canonicalOutputSchema(tool.name), historical), JSON.stringify(tool.ajv.errors), ).toBe(true); const oldView = await client.callTool( @@ -330,7 +331,7 @@ mcpTest( createServer( { kind: "session", session }, { - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, ), undefined, @@ -350,7 +351,7 @@ mcpTest( view: { kind: "macho", facet }, }; expect( - schemas.ajv.validate(schemas.inputSchema, request), + schemas.ajv.validate(schemas.canonicalInputSchema, request), JSON.stringify(schemas.ajv.errors), ).toBe(true); const reply = await client.callTool( diff --git a/tests/boundary/mcp/nativeObservationViewsMcp.test.ts b/tests/boundary/mcp/nativeObservationViewsMcp.test.ts index d7ce1ca7c7..59cb0a3427 100644 --- a/tests/boundary/mcp/nativeObservationViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeObservationViewsMcp.test.ts @@ -26,7 +26,7 @@ it("delivers native primitive and graph summaries with exact retained parents an ); const call = async (name: string, input: Record) => { const tool = definitions.get(name); - if (!tool?.outputSchema) throw new Error("Missing named contract"); + if (tool === undefined) throw new Error("Missing named contract"); const result = await harness.mcp.callTool( { name, arguments: input }, { toolDefinition: tool }, diff --git a/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts b/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts index c8a55f72d1..f5c0753f90 100644 --- a/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts +++ b/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts @@ -33,7 +33,7 @@ const fixture = { unresolved_calls: [{ address: "0x1003", reason: "indirect destination" }], }; -const setup = async (profile: "full" | "compact") => { +const setup = async (profile: "compact") => { const session = createTestBinarySession(() => { throw new Error("View opened provider"); }); @@ -53,7 +53,7 @@ const setup = async (profile: "full" | "compact") => { }; }; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "pages procedure reference Evidence across a %s offline handoff", async (profile) => { const parent = createEvidence( diff --git a/tests/boundary/mcp/nativeValueFlowAvailability.test.ts b/tests/boundary/mcp/nativeValueFlowAvailability.test.ts index 03f96047f1..7ba546cfc4 100644 --- a/tests/boundary/mcp/nativeValueFlowAvailability.test.ts +++ b/tests/boundary/mcp/nativeValueFlowAvailability.test.ts @@ -21,11 +21,8 @@ afterAll(() => schemaValidation.clear()); it.each([ { profile: "compact", state: "supported" }, - { profile: "full", state: "supported" }, { profile: "compact", state: "unknown" }, - { profile: "full", state: "unknown" }, { profile: "compact", state: "unavailable" }, - { profile: "full", state: "unavailable" }, ] as const)( "reports $state value flow without a resolver in $profile MCP", async ({ profile, state }) => { diff --git a/tests/boundary/mcp/nativeValueTraceCompleteness.test.ts b/tests/boundary/mcp/nativeValueTraceCompleteness.test.ts index 8f869ad1f9..65ea6ed5e1 100644 --- a/tests/boundary/mcp/nativeValueTraceCompleteness.test.ts +++ b/tests/boundary/mcp/nativeValueTraceCompleteness.test.ts @@ -26,7 +26,7 @@ import { afterEach(closeEnhancedToolResources); const connectTraceSession = async ( - profile: "full" | "compact", + profile: "compact", analysis: AnalysisOperationPort, ) => { const [target] = await createBinarySessionTargets(); @@ -52,7 +52,7 @@ const connectTraceSession = async ( return client; }; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "separates page omission from named traversal budgets through the advertised %s MCP contract", async (profile) => { const client = await connectTraceSession( @@ -63,14 +63,13 @@ it.each(["full", "compact"] as const)( client, "trace_native_values", ); - if (profile === "full") - for (const field of [ - "page_truncated", - "traversal", - "max_nodes", - "max_edges", - ]) - expect(JSON.stringify(outputSchema)).toContain(`"${field}"`); + for (const field of [ + "page_truncated", + "traversal", + "max_nodes", + "max_edges", + ]) + expect(JSON.stringify(outputSchema)).toContain(`"${field}"`); const trace = async (arguments_: Record) => { const reply = await client.callTool( { name: "trace_native_values", arguments: arguments_ }, @@ -159,7 +158,7 @@ it.each(["full", "compact"] as const)( }, ); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "retains located call-input gaps and exact source Evidence through %s MCP", async (profile) => { const client = await connectTraceSession( diff --git a/tests/boundary/mcp/nativeViewPredicateMcp.test.ts b/tests/boundary/mcp/nativeViewPredicateMcp.test.ts index a2ced02fa8..f0dd204a1d 100644 --- a/tests/boundary/mcp/nativeViewPredicateMcp.test.ts +++ b/tests/boundary/mcp/nativeViewPredicateMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { writeFile } from "node:fs/promises"; @@ -48,7 +49,7 @@ const wrongMatchingPredicate = (matching: Evidence) => evidenceLinks: matching.evidence_links, }); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "validates native parent predicates across public sources (%s schema)", async (profile) => { const session = createTestBinarySession(() => { @@ -71,11 +72,13 @@ it.each(["full", "compact"] as const)( (await client.listTools()).tools.map((tool) => [tool.name, tool]), ); const viewTool = definitions.get("inspect_analysis_view"); - if (viewTool?.outputSchema === undefined) + if (viewTool === undefined) throw new Error("Missing advertised view schema"); - expect(validation.assertions.validateSchema(viewTool.outputSchema)).toBe( - true, - ); + expect( + validation.assertions.validateSchema( + canonicalOutputSchema(viewTool.name), + ), + ).toBe(true); const call = (name: string, args: Record) => { const toolDefinition = definitions.get(name); if (toolDefinition === undefined) throw new Error(`Missing ${name}`); diff --git a/tests/boundary/mcp/nodeSeaMcp.test.ts b/tests/boundary/mcp/nodeSeaMcp.test.ts index 6db7aef411..09ad4dde13 100644 --- a/tests/boundary/mcp/nodeSeaMcp.test.ts +++ b/tests/boundary/mcp/nodeSeaMcp.test.ts @@ -35,7 +35,7 @@ const expectBlobAdmissionFailure = ( // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "exposes SEA facts and extraction through MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-sea-mcp-"); diff --git a/tests/boundary/mcp/outputSchemaProfile.test.ts b/tests/boundary/mcp/outputSchemaProfile.test.ts index 14868fd552..8522ce4a55 100644 --- a/tests/boundary/mcp/outputSchemaProfile.test.ts +++ b/tests/boundary/mcp/outputSchemaProfile.test.ts @@ -14,36 +14,28 @@ import { import { createServer as createProductionServer } from "../../../src/server/createServer.js"; import { silentLogger } from "../../../src/logger.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { COMPACT_INPUT_SCHEMA_BUDGET_BYTES } from "../../../src/config/mcpInputSchemaProfile.js"; -import type { CompactInputSchemaPresentation } from "../../../src/contracts/compactInputSchema.js"; import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; import { createToolResultDelivery } from "../../../src/server/toolResult.js"; import type { ToolContract } from "../../../src/contracts/toolContractTypes.js"; -import { FULL_CATALOG_IDENTITY } from "../../../src/catalogIdentity.js"; +import { DEFAULT_CATALOG_IDENTITY } from "../../../src/catalogIdentity.js"; import type { McpOutputSchemaProfile } from "../../../src/config/mcpOutputSchemaProfile.js"; import { advertisedSchemaErrors } from "../../fixtures/mcpAdvertisedSchemaValidity.js"; -import { advertisedInputJsonSchema } from "../../../src/contracts/toolSchemaMetadata.js"; -const createServer = ( - outputSchemaProfile: McpOutputSchemaProfile, - compactInputPresentation: - | CompactInputSchemaPresentation - | undefined = undefined, -) => +const createServer = (outputSchemaProfile: McpOutputSchemaProfile) => new EvidenceMcpServer( { name: "output-schema-fixture", version: "1" }, { capabilities: {} }, undefined, createToolResultDelivery(10 * 1024 * 1024), - compactInputPresentation, + undefined, outputSchemaProfile, ); it("omits only output schema advertisements and keeps canonical MCP output validation", async () => { const server = createServer("off"); - expect(server.catalogIdentity.digests.combined_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.combined_sha256, + expect(server.catalogIdentity.digests.combined_sha256).toBe( + DEFAULT_CATALOG_IDENTITY.digests.combined_sha256, ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -107,7 +99,7 @@ it("omits only output schema advertisements and keeps canonical MCP output valid it("bounds output schema advertisements and keeps canonical MCP output validation", async () => { const server = createServer("compact"); expect(server.catalogIdentity.digests.combined_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.combined_sha256, + DEFAULT_CATALOG_IDENTITY.digests.combined_sha256, ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); @@ -186,21 +178,10 @@ it("bounds output schema advertisements and keeps canonical MCP output validatio } }); -it("keeps the default compact-input catalog below a measured one-megabyte frame", async () => { - const compactInput: CompactInputSchemaPresentation = { - budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, - }; - const canonicalInputs = new Map>( - TOOL_CONTRACTS.map((contract) => [ - contract.name, - advertisedInputJsonSchema(contract), - ]), - ); - const measure = async ( - profile: McpOutputSchemaProfile, - inputPresentation: CompactInputSchemaPresentation | undefined, - ): Promise => { - const server = createServer(profile, inputPresentation); +it("keeps both output profiles below a measured one-megabyte frame", async () => { + const names = new Set(TOOL_CONTRACTS.map(({ name }) => name)); + const measure = async (profile: McpOutputSchemaProfile): Promise => { + const server = createServer(profile); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "catalog-size-test", version: "1" }); @@ -216,15 +197,8 @@ it("keeps the default compact-input catalog below a measured one-megabyte frame" await client.connect(clientTransport); try { const listed = await client.listTools(); - expect(listed.tools).toHaveLength(canonicalInputs.size); - expect(new Set(listed.tools.map((tool) => tool.name))).toEqual( - new Set(canonicalInputs.keys()), - ); - if (inputPresentation === undefined) - for (const tool of listed.tools) - expect(tool.inputSchema, tool.name).toEqual( - canonicalInputs.get(tool.name), - ); + expect(listed.tools).toHaveLength(names.size); + expect(new Set(listed.tools.map((tool) => tool.name))).toEqual(names); if (profile === "off") expect( listed.tools.every((tool) => tool.outputSchema === undefined), @@ -256,18 +230,10 @@ it("keeps the default compact-input catalog below a measured one-megabyte frame" } }; - const fullBytes = await measure("full", undefined); - const compactBytes = await measure("compact", compactInput); - const offBytes = await measure("off", compactInput); - const fullInputOffBytes = await measure("off", undefined); - expect(fullBytes).toBeGreaterThan(1024 * 1024); + const compactBytes = await measure("compact"); + const offBytes = await measure("off"); expect(compactBytes).toBeLessThan(1024 * 1024); expect(offBytes).toBeLessThan(1024 * 1024); - // Full input is an explicit guidance choice, not the bounded compatibility - // profile. Omitting output schemas must preserve those complete inputs. - expect(fullInputOffBytes).toBeGreaterThan(offBytes); - expect(fullInputOffBytes).toBeLessThan(fullBytes); - expect(compactBytes).toBeLessThan(fullBytes); expect(compactBytes).toBeGreaterThan(offBytes); }); diff --git a/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts b/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts index bccb6e7446..d57477c8b1 100644 --- a/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts +++ b/tests/boundary/mcp/outputSchemaSemanticConstraints.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { createHash } from "node:crypto"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -73,9 +74,8 @@ it("advertises semantic output invariants and keeps producers aligned with Zod", ); const outputResultSchema = (name: (typeof contracts)[number]["name"]) => { const tool = advertised.get(name); - if (tool?.outputSchema === undefined) - throw new Error(`${name} output schema missing`); - const outputSchema = record(tool.outputSchema); + if (tool === undefined) throw new Error(`${name} output schema missing`); + const outputSchema = record(canonicalOutputSchema(tool.name)); const result = record(record(outputSchema.properties).normalized_result); const standaloneResultSchema = z.record(z.string(), z.unknown()).parse({ ...result, @@ -235,12 +235,10 @@ mcpTest( const advertised = (await client.listTools()).tools.find( ({ name }) => name === contract.name, ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Load-image output schema missing"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const outputSchema = z - .record(z.string(), z.unknown()) - .parse(advertised.outputSchema); + const outputSchema = canonicalOutputSchema(advertised.name); expect(ajv.validateSchema(outputSchema)).toBe(true); const validate = ajv.compile(outputSchema); expect(nativeLoadImageSchema.parse(image)).toEqual(image); diff --git a/tests/boundary/mcp/packageCatalogVerification.test.ts b/tests/boundary/mcp/packageCatalogVerification.test.ts index ec87d6f484..d5ac3ef210 100644 --- a/tests/boundary/mcp/packageCatalogVerification.test.ts +++ b/tests/boundary/mcp/packageCatalogVerification.test.ts @@ -1,23 +1,18 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { expect, it } from "vitest"; import { z } from "zod"; import { verifyCompleteToolCatalog } from "../../../scripts/lib/verify-package-core.mjs"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; -it("verifies the profiles selected for the packaged server", async () => { - const environment = { - REA_MCP_INPUT_SCHEMA_PROFILE: "full", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "compact", - }; +it("verifies the output profile selected for the packaged server", async () => { const harness = await createApplicationMcpHarness({ - serverOptions: { environment }, + serverOptions: { + environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "compact" }, + }, }); try { - const names = await verifyCompleteToolCatalog( - harness.client, - undefined, - environment, - ); + const names = await verifyCompleteToolCatalog(harness.client); expect(names).toContain("binary_session"); } finally { await harness.close(); @@ -46,7 +41,7 @@ it("verifies canonical names, converted schemas and session metadata together", name: "binary_session", arguments: {}, }, - { toolDefinition: sessionDefinition }, + { toolDefinition: withCanonicalOutputSchema(sessionDefinition) }, ); const forwarded: unknown[][] = []; const client = { diff --git a/tests/boundary/mcp/packetCaptureMcp.test.ts b/tests/boundary/mcp/packetCaptureMcp.test.ts index 7aae4197cc..cb3d5381b5 100644 --- a/tests/boundary/mcp/packetCaptureMcp.test.ts +++ b/tests/boundary/mcp/packetCaptureMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; @@ -13,17 +14,18 @@ it.each(["inspect_packet_capture", "inspect_packet_fields"])( "advertises valid target-free schemas and missing-tool diagnostics: %s", async (name) => { const harness = await createApplicationMcpHarness({ - serverOptions: { environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" } }, + serverOptions: { environment: {} }, }); onTestFinished(harness.close); const tool = (await harness.client.listTools()).tools.find( (tool) => tool.name === name, ); - if (tool?.outputSchema === undefined) - throw new Error("Capture tool missing"); + if (tool === undefined) throw new Error("Capture tool missing"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = tool.inputSchema; - const outputSchema: Record = tool.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + tool.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const input = diff --git a/tests/boundary/mcp/peCertificatesMcp.test.ts b/tests/boundary/mcp/peCertificatesMcp.test.ts index 20e886c89a..4b81a62d72 100644 --- a/tests/boundary/mcp/peCertificatesMcp.test.ts +++ b/tests/boundary/mcp/peCertificatesMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterAll, expect, onTestFinished } from "vitest"; @@ -40,9 +41,11 @@ mcpTest( const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_pe_certificates", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("PE certificates output schema is missing"); - const schema: Record = advertised.outputSchema; + const schema: Record = canonicalOutputSchema( + advertised.name, + ); const ajv = schemaValidation.assertions; expect(ajv.validateSchema(schema)).toBe(true); const root = await createTestTempDirectory("rea-pe-cert-schema-"); diff --git a/tests/boundary/mcp/peLoadConfigMcp.test.ts b/tests/boundary/mcp/peLoadConfigMcp.test.ts index c6508bc430..94f17aab04 100644 --- a/tests/boundary/mcp/peLoadConfigMcp.test.ts +++ b/tests/boundary/mcp/peLoadConfigMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; @@ -39,7 +40,7 @@ mcpTest( name: "inspect_pe_load_config", arguments: { path }, }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(response.isError, JSON.stringify(response.content)).not.toBe(true); const evidence = toolContract( @@ -69,7 +70,7 @@ mcpTest( await writeFile(path, padded); const reply = await client.callTool( { name: "inspect_pe_load_config", arguments: { path } }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true); const retained = toolContract( @@ -90,7 +91,7 @@ mcpTest( name: "inspect_pe_load_config", arguments: { path: "relative.exe" }, }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(bad.isError).toBe(true); } finally { diff --git a/tests/boundary/mcp/processCaptureCleanupSchema.test.ts b/tests/boundary/mcp/processCaptureCleanupSchema.test.ts index 6e7038c81e..2bc58ae484 100644 --- a/tests/boundary/mcp/processCaptureCleanupSchema.test.ts +++ b/tests/boundary/mcp/processCaptureCleanupSchema.test.ts @@ -12,7 +12,7 @@ import { processScenarioSchema } from "../../../src/domain/process/processScenar const validation = createMcpSchemaValidation(); afterAll(() => validation.clear()); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "advertises quiesced cleanup intervention without rejecting saved captures (%s)", async (profile) => { const session = createTestBinarySession(() => { @@ -24,7 +24,6 @@ it.each(["full", "compact"] as const)( environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: profile, - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, }, ); diff --git a/tests/boundary/mcp/promptMcp.test.ts b/tests/boundary/mcp/promptMcp.test.ts index 2d6bf66abf..069b2dfe9a 100644 --- a/tests/boundary/mcp/promptMcp.test.ts +++ b/tests/boundary/mcp/promptMcp.test.ts @@ -546,7 +546,7 @@ describe("guided prompt completion registry", () => { undefined, delivery, undefined, - "full", + undefined, undefined, families, ); diff --git a/tests/boundary/mcp/providerSelectionMcp.test.ts b/tests/boundary/mcp/providerSelectionMcp.test.ts index 028db7dc37..fe3748f8e9 100644 --- a/tests/boundary/mcp/providerSelectionMcp.test.ts +++ b/tests/boundary/mcp/providerSelectionMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; @@ -598,13 +599,8 @@ describe("binary_session detail over MCP", () => { const summary = (await sessionStatusOf(mcp, {})).raw.result; const complete = (await sessionStatusOf(mcp, { detail: "complete" })).raw .result; - const advertised = (await mcp.listTools()).tools.find( - ({ name }) => name === "binary_session", - )?.outputSchema; - if (advertised === undefined) - throw new Error("binary_session omitted its output schema"); const validate = new AjvJsonSchemaValidator().getValidator( - z.record(z.string(), z.unknown()).parse(advertised), + canonicalOutputSchema("binary_session"), ); const canonical = toolContract("binary_session").outputSchema; const { omitted_fields: _omitted, ...summaryWithoutOmissions } = summary; @@ -1124,7 +1120,7 @@ const structured = (result: CallToolResult): Record => ? parseMcpToolError(result) : z.record(z.string(), z.unknown()).parse(result.structuredContent); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "keeps provider-owned release verification bound to the selected session (%s)", async (profile) => { const directory = await createTestTempDirectory( diff --git a/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts b/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts index 8e5e40d738..ec33775403 100644 --- a/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts +++ b/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { access, readFile, rm, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { join } from "node:path"; @@ -33,7 +34,7 @@ const assertMetadataQueries = async ( const definition = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_analysis_view", ); - if (definition?.outputSchema === undefined) + if (definition === undefined) throw new Error("Analysis view output schema is missing"); const ajv = schemaValidation.assertions; const inputSchema: Record = definition.inputSchema; @@ -56,7 +57,7 @@ const assertMetadataQueries = async ( ); const reply = await client.callTool( { name: "inspect_analysis_view", arguments: args }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(reply.isError).not.toBe(true); const evidence = parseEvidence(reply.structuredContent); diff --git a/tests/boundary/mcp/pythonPycMcp.test.ts b/tests/boundary/mcp/pythonPycMcp.test.ts index cade91afbd..526cdd0a11 100644 --- a/tests/boundary/mcp/pythonPycMcp.test.ts +++ b/tests/boundary/mcp/pythonPycMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js"; @@ -146,9 +147,11 @@ it("advertises inspect_python_pyc and reads nested code objects", async () => { readOnlyHint: false, idempotentHint: true, }); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing inspect_python_pyc output schema"); - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); const ajv = schemaValidation.assertions; expect(ajv.validateSchema(outputSchema)).toBe(true); const root = await createTestTempDirectory("rea-python-pyc-mcp-"); @@ -212,7 +215,7 @@ it("advertised pyc output schema preserves historical signed fields", async () = const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_python_pyc", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing inspect_python_pyc output schema"); const root = await createTestTempDirectory("rea-python-pyc-mcp-flags-"); for (const testCase of [ diff --git a/tests/boundary/mcp/rawBinaryMcp.test.ts b/tests/boundary/mcp/rawBinaryMcp.test.ts index 497f542898..43fd458eb5 100644 --- a/tests/boundary/mcp/rawBinaryMcp.test.ts +++ b/tests/boundary/mcp/rawBinaryMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { mkdir, symlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -242,9 +243,8 @@ it("reuses a canonical raw entry and replaces the session when the entry changes response: CallToolResult, ) => { const tool = tools.find((candidate) => candidate.name === name); - if (tool?.outputSchema === undefined) - throw new Error(`missing ${name} output schema`); - const schema = z.record(z.string(), z.unknown()).parse(tool.outputSchema); + if (tool === undefined) throw new Error(`missing ${name} output schema`); + const schema = canonicalOutputSchema(tool.name); const validate = new Ajv2020({ strict: false, validateFormats: false, diff --git a/tests/boundary/mcp/recordedCrashMcp.test.ts b/tests/boundary/mcp/recordedCrashMcp.test.ts index 10fc985834..f898e049b4 100644 --- a/tests/boundary/mcp/recordedCrashMcp.test.ts +++ b/tests/boundary/mcp/recordedCrashMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it, onTestFinished } from "vitest"; @@ -51,11 +52,13 @@ it("records inline recorded-core evidence with compact schemas and canonical pat const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_recorded_crash", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("recorded crash must publish both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); expect(ajv.validate(inputSchema, { path: value.artifact.path })).toBe(true); diff --git a/tests/boundary/mcp/releaseExposureMcp.test.ts b/tests/boundary/mcp/releaseExposureMcp.test.ts index fbfbb327cd..0addf602ee 100644 --- a/tests/boundary/mcp/releaseExposureMcp.test.ts +++ b/tests/boundary/mcp/releaseExposureMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; @@ -49,7 +50,7 @@ mcpTest( ); const response = await client.callTool( { name: "audit_release_exposure", arguments: { path: root } }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(response.isError, JSON.stringify(response.content)).not.toBe(true); const evidence = outputSchema.parse(response.structuredContent); @@ -64,7 +65,7 @@ mcpTest( name: "audit_release_exposure", arguments: { path: "relative-dir" }, }, - { toolDefinition: definition }, + { toolDefinition: withCanonicalOutputSchema(definition) }, ); expect(bad.isError).toBe(true); } finally { diff --git a/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts b/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts index 8d901b2754..5986ff53ef 100644 --- a/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts +++ b/tests/boundary/mcp/retainedApplicationEvidenceMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { afterAll, expect, it } from "vitest"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -23,7 +24,7 @@ const application = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application; const pair = JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE; const exportPair = JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE; -async function connect(profile?: "full" | "compact") { +async function connect(profile?: "compact") { const session = createTestBinarySession(() => { throw new Error("references must not launch a provider"); }); @@ -42,7 +43,7 @@ it("advertises semantic discovery paging and validates the returned Evidence", a const { client } = await connect(); const { tools } = await client.listTools(); const tool = tools.find(({ name }) => name === "trace_application_feature"); - if (tool?.outputSchema === undefined) + if (tool === undefined) throw new Error("Missing feature trace output schema"); const input = { application, @@ -50,7 +51,7 @@ it("advertises semantic discovery paging and validates the returned Evidence", a semantic_page: { offset: 0, limit: 1 }, }; const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const outputSchema = tool.outputSchema as AnySchema; + const outputSchema = canonicalOutputSchema(tool.name) as AnySchema; expect(ajv.validateSchema(tool.inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); expect(ajv.compile(tool.inputSchema)(input)).toBe(true); @@ -62,7 +63,7 @@ it("advertises semantic discovery paging and validates the returned Evidence", a }); }); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "advertises the primitive literal seeds accepted by semantic tracing (%s)", async (profile) => { const { client } = await connect(profile); @@ -92,7 +93,7 @@ it.each(["full", "compact"] as const)( direction: "backward-provenance", }, }; - if (expectedPrimitive || profile === "full") + if (expectedPrimitive) expect(validate(arguments_), JSON.stringify(value)).toBe( expectedPrimitive, ); diff --git a/tests/boundary/mcp/sameFunctionObservation.test.ts b/tests/boundary/mcp/sameFunctionObservation.test.ts index 948d2913a1..3c1f5bf836 100644 --- a/tests/boundary/mcp/sameFunctionObservation.test.ts +++ b/tests/boundary/mcp/sameFunctionObservation.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { afterAll, expect, it } from "vitest"; import type { Client } from "@modelcontextprotocol/client"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; @@ -85,7 +86,7 @@ const expectRejectedDimensionCitation = async ( ); }; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "compares the same authenticated observation while preserving incomplete facets (%s)", async (profile) => { const session = createTestBinarySession(() => { @@ -103,8 +104,7 @@ it.each(["full", "compact"] as const)( const toolDefinition = (await client.listTools()).tools.find( ({ name }) => name === "compare_functions", ); - if (toolDefinition?.outputSchema === undefined) - throw new Error("Missing output schema"); + if (toolDefinition === undefined) throw new Error("Missing output schema"); const reply = await client.callTool( { name: "compare_functions", @@ -113,7 +113,7 @@ it.each(["full", "compact"] as const)( right: ref(main.evidence_id), }, }, - { toolDefinition }, + { toolDefinition: withCanonicalOutputSchema(toolDefinition) }, ); expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true); const result = parseEvidence(reply.structuredContent).normalized_result; diff --git a/tests/boundary/mcp/serverIdentity.test.ts b/tests/boundary/mcp/serverIdentity.test.ts index 34739411ba..31c98516fa 100644 --- a/tests/boundary/mcp/serverIdentity.test.ts +++ b/tests/boundary/mcp/serverIdentity.test.ts @@ -11,7 +11,6 @@ import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { machoImage } from "../../../src/artifacts/apple/MachoImage.fixture.js"; import { - FULL_CATALOG_IDENTITY, DEFAULT_CATALOG_IDENTITY, createCatalogDigestProfiles, createCatalogDigests, @@ -117,18 +116,18 @@ describe("server and catalog identity", () => { packageJson.dependencies["@modelcontextprotocol/server"], ); expect(new Set(CLI_COMMAND_NAMES).size).toBe(CLI_COMMAND_NAMES.length); - expect(FULL_CATALOG_IDENTITY.counts).toEqual({ + expect(DEFAULT_CATALOG_IDENTITY.counts).toEqual({ cli_commands: CLI_COMMAND_NAMES.length, mcp_tools: TOOL_CONTRACTS.length, mcp_prompts: PROMPT_CONTRACTS.length, }); - expect(FULL_CATALOG_IDENTITY.digests.combined_sha256).toMatch( + expect(DEFAULT_CATALOG_IDENTITY.digests.combined_sha256).toMatch( /^[a-f0-9]{64}$/u, ); }); it("covers each tool's target-content policy in the catalog digest", () => { - expect(createCatalogDigests()).toEqual(FULL_CATALOG_IDENTITY.digests); + expect(createCatalogDigests()).toEqual(DEFAULT_CATALOG_IDENTITY.digests); const [first] = TOOL_CONTRACTS; if (first === undefined) throw new Error("expected a tool contract"); const toggled = TOOL_CONTRACTS.map((contract) => @@ -138,20 +137,19 @@ describe("server and catalog identity", () => { ); const digests = createCatalogDigests(toggled); expect(digests.combined_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.combined_sha256, + DEFAULT_CATALOG_IDENTITY.digests.combined_sha256, ); expect(digests.tools_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.tools_sha256, + DEFAULT_CATALOG_IDENTITY.digests.tools_sha256, ); expect(digests.prompts_sha256).toBe( - FULL_CATALOG_IDENTITY.digests.prompts_sha256, + DEFAULT_CATALOG_IDENTITY.digests.prompts_sha256, ); }); it("binds output schema profile and canonical output contracts into identity", () => { const profiles = createCatalogDigestProfiles(); - expect(FULL_CATALOG_IDENTITY.digests).toEqual(profiles.full.full); - expect(DEFAULT_CATALOG_IDENTITY.digests).toEqual(profiles.compact.off); + expect(DEFAULT_CATALOG_IDENTITY.digests).toEqual(profiles.off); const changedOutput = TOOL_CONTRACTS.map((contract, index) => index === 0 ? { @@ -161,39 +159,23 @@ describe("server and catalog identity", () => { : contract, ); const changed = createCatalogDigestProfiles(changedOutput); - for (const input of ["full", "compact"] as const) { - const { full, off } = profiles[input]; - expect(off.tools_sha256).not.toBe(full.tools_sha256); - expect(off.combined_sha256).not.toBe(full.combined_sha256); - expect(off.prompts_sha256).toBe(full.prompts_sha256); - for (const output of ["full", "off"] as const) { - expect(changed[input][output].tools_sha256).not.toBe( - profiles[input][output].tools_sha256, - ); - expect(changed[input][output].combined_sha256).not.toBe( - profiles[input][output].combined_sha256, - ); - expect(changed[input][output].prompts_sha256).toBe( - profiles[input][output].prompts_sha256, - ); - } + const { compact, off } = profiles; + expect(off.tools_sha256).not.toBe(compact.tools_sha256); + expect(off.combined_sha256).not.toBe(compact.combined_sha256); + expect(off.prompts_sha256).toBe(compact.prompts_sha256); + for (const output of ["compact", "off"] as const) { + expect(changed[output].tools_sha256).not.toBe( + profiles[output].tools_sha256, + ); + expect(changed[output].combined_sha256).not.toBe( + profiles[output].combined_sha256, + ); + expect(changed[output].prompts_sha256).toBe( + profiles[output].prompts_sha256, + ); } }); - it("changes catalog identity when advertised input examples change", () => { - const contract = TOOL_CONTRACTS.find((item) => item.examples.length > 0); - if (contract === undefined) throw new Error("expected an input example"); - const original = createCatalogDigests(); - const withoutExamples = createCatalogDigests( - TOOL_CONTRACTS.map((item) => - item === contract ? { ...item, examples: [] } : item, - ), - ); - expect(withoutExamples.tools_sha256).not.toBe(original.tools_sha256); - expect(withoutExamples.combined_sha256).not.toBe(original.combined_sha256); - expect(withoutExamples.prompts_sha256).toBe(original.prompts_sha256); - }); - it("reports unknown without a live comparison and distinguishes aligned from stale", () => { const unknown = createServerIdentity({ catalog: DEFAULT_CATALOG_IDENTITY, @@ -378,13 +360,13 @@ describe("guided prompt catalog identity", () => { ); const digests = createCatalogDigests(TOOL_CONTRACTS, changed); expect(digests.prompts_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.prompts_sha256, + DEFAULT_CATALOG_IDENTITY.digests.prompts_sha256, ); expect(digests.combined_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.combined_sha256, + DEFAULT_CATALOG_IDENTITY.digests.combined_sha256, ); expect(digests.tools_sha256).toBe( - FULL_CATALOG_IDENTITY.digests.tools_sha256, + DEFAULT_CATALOG_IDENTITY.digests.tools_sha256, ); }); }); @@ -468,9 +450,7 @@ describe("live server identity over MCP", () => { state: "aligned", reasons: [], }); - expect(server.catalogIdentity.digests.tools_sha256).not.toBe( - FULL_CATALOG_IDENTITY.digests.tools_sha256, - ); + expect(server.catalogIdentity).toBe(DEFAULT_CATALOG_IDENTITY); } finally { await Promise.allSettled([ client.close(), diff --git a/tests/boundary/mcp/serverWorkflowMcp.test.ts b/tests/boundary/mcp/serverWorkflowMcp.test.ts index 39637e0f94..95858eb6cc 100644 --- a/tests/boundary/mcp/serverWorkflowMcp.test.ts +++ b/tests/boundary/mcp/serverWorkflowMcp.test.ts @@ -1,3 +1,7 @@ +import { + canonicalOutputSchema, + withCanonicalOutputSchema, +} from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { ok as resultOk } from "../../../src/domain/result.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; @@ -133,12 +137,12 @@ it("advertises the complete currently available inventory with a session", async ); if (sessionTool === undefined) throw new Error("Missing advertised binary_session tool"); - // Validate this reply with its current advertised schema. The SDK otherwise + // Validate this reply with its canonical output schema. The SDK otherwise // builds output validators for the entire catalog on the first cached call. const status = structured( await client.callTool( { name: "binary_session", arguments: {} }, - { toolDefinition: sessionTool }, + { toolDefinition: withCanonicalOutputSchema(sessionTool) }, ), ); const inventory = expandToolAvailability( @@ -182,6 +186,6 @@ it("advertises the complete currently available inventory with a session", async ); expect(tool.annotations, tool.name).toEqual(contract?.annotations); expect(tool.inputSchema, tool.name).toBeDefined(); - expect(tool.outputSchema, tool.name).toBeDefined(); + expect(canonicalOutputSchema(tool.name), tool.name).toBeDefined(); } }); diff --git a/tests/boundary/mcp/signalRecordingMcp.test.ts b/tests/boundary/mcp/signalRecordingMcp.test.ts index a21ad6e001..a7a52a724b 100644 --- a/tests/boundary/mcp/signalRecordingMcp.test.ts +++ b/tests/boundary/mcp/signalRecordingMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; @@ -13,17 +14,18 @@ it.each(["decode_signal_recording", "inspect_signal_decoders"])( "advertises valid target-free schemas and missing-tool diagnostics: %s", async (name) => { const harness = await createApplicationMcpHarness({ - serverOptions: { environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" } }, + serverOptions: { environment: {} }, }); onTestFinished(harness.close); const tool = (await harness.client.listTools()).tools.find( (tool) => tool.name === name, ); - if (tool?.outputSchema === undefined) - throw new Error("Signal tool missing"); + if (tool === undefined) throw new Error("Signal tool missing"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = tool.inputSchema; - const outputSchema: Record = tool.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + tool.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const input = diff --git a/tests/boundary/mcp/sourcePublicationContext.test.ts b/tests/boundary/mcp/sourcePublicationContext.test.ts index ac20643d8d..8b758b5c0b 100644 --- a/tests/boundary/mcp/sourcePublicationContext.test.ts +++ b/tests/boundary/mcp/sourcePublicationContext.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server"; import { afterAll, expect, it } from "vitest"; @@ -20,7 +21,7 @@ const validation = createMcpSchemaValidation(); afterAll(() => validation.clear()); it.each( - ["full", "compact"].flatMap((profile) => + ["compact"].flatMap((profile) => ["large-source", "large-primary", "invalid-source"].map((mode) => ({ profile, mode, @@ -113,11 +114,10 @@ it.each( const tool = (await client.listTools()).tools.find( ({ name }) => name === "inspect_address_context", ); - if (tool?.outputSchema === undefined) - throw new Error("Missing context output schema"); - expect(validation.assertions.validateSchema(tool.outputSchema)).toBe( - true, - ); + if (tool === undefined) throw new Error("Missing context output schema"); + expect( + validation.assertions.validateSchema(canonicalOutputSchema(tool.name)), + ).toBe(true); const result = await client.callTool( { name: "inspect_address_context", diff --git a/tests/boundary/mcp/sourcePublicationRecovery.test.ts b/tests/boundary/mcp/sourcePublicationRecovery.test.ts index 718a6b8dde..3ba705a09a 100644 --- a/tests/boundary/mcp/sourcePublicationRecovery.test.ts +++ b/tests/boundary/mcp/sourcePublicationRecovery.test.ts @@ -458,7 +458,7 @@ it("accounts for a wide source ID list before constructing recovery Evidence", a } }); -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "reports omitted primary when a retained provider source precedes an authentication refusal (%s)", async (profile) => { const identity = { id: "fixture", name: "Fixture", version: "1" }; diff --git a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts index 78ef80c96f..c58e188d03 100644 --- a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { afterAll, expect, it } from "vitest"; @@ -14,7 +15,7 @@ import { analyzeJavaScriptApplication } from "../../support/javascriptApplicatio const schemaValidation = createMcpSchemaValidation(); afterAll(() => schemaValidation.clear()); -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "selects real source-map correspondence with stable identities and full parent retention (%s)", async (profile) => { const { root, path, reference, session, client, inputs, call } = @@ -65,7 +66,9 @@ it.each(["full", "compact"] as const)( )!; for (const tool of [comparisonTool, viewTool]) { expect(ajv.validateSchema(tool.inputSchema)).toBe(true); - const outputSchema: Record = tool.outputSchema!; + const outputSchema: Record = canonicalOutputSchema( + tool.name, + ); expect(ajv.validateSchema(outputSchema)).toBe(true); } const request = { @@ -83,7 +86,9 @@ it.each(["full", "compact"] as const)( reference: { kind: "file", path }, view: request, }); - const outputSchema: Record = comparisonTool.outputSchema!; + const outputSchema: Record = canonicalOutputSchema( + comparisonTool.name, + ); expect(ajv.validate(outputSchema, selected)).toBe(true); expect(selected.confidence).toBe("inferred"); const page = analysisViewResultSchema.parse(selected.normalized_result); @@ -148,7 +153,7 @@ it.each(["full", "compact"] as const)( }, ); -const comparisonFixture = async (profile: "full" | "compact") => { +const comparisonFixture = async (profile: "compact") => { const root = await createTestTempDirectory("rea-source-view-"); const previous = join(root, "previous"), current = join(root, "current"); diff --git a/tests/boundary/mcp/stdioFrameDiagnostics.test.ts b/tests/boundary/mcp/stdioFrameDiagnostics.test.ts index aee9ea79fd..c0433912a6 100644 --- a/tests/boundary/mcp/stdioFrameDiagnostics.test.ts +++ b/tests/boundary/mcp/stdioFrameDiagnostics.test.ts @@ -16,8 +16,8 @@ const schemaValidation = createMcpSchemaValidation(); afterAll(() => schemaValidation.clear()); workspaceCliTest.for([ - { mode: "deferred", profile: "full" }, - { mode: "eager", profile: "off" }, + { mode: "deferred", profile: "off" }, + { mode: "eager", profile: "compact" }, ])( "reports frame validation while the $mode stdio connection remains usable", async ({ profile }, { workspace }) => { diff --git a/tests/boundary/mcp/streamableHttpMcp.test.ts b/tests/boundary/mcp/streamableHttpMcp.test.ts index 4a878171a2..a0c25e20d4 100644 --- a/tests/boundary/mcp/streamableHttpMcp.test.ts +++ b/tests/boundary/mcp/streamableHttpMcp.test.ts @@ -54,7 +54,6 @@ describe("rea mcp --http", () => { // Sessions validate advertised output schemas, which default to off. { REA_MCP_TOOL_CATALOG_PROFILE: "hot-cold", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, ); const first = await openSession(started.url, "hot-cold-first"); @@ -105,7 +104,7 @@ describe("rea mcp --http", () => { const started = await startListeningProcess( processes, ["mcp", "--http", "--port", "0"], - { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }, + {}, ); expect(started.url.hostname).toBe("127.0.0.1"); expect(started.url.pathname).toBe("/mcp"); diff --git a/tests/boundary/mcp/toolErrorSchemaDelivery.test.ts b/tests/boundary/mcp/toolErrorSchemaDelivery.test.ts index b15b70a0e0..edd220e9ea 100644 --- a/tests/boundary/mcp/toolErrorSchemaDelivery.test.ts +++ b/tests/boundary/mcp/toolErrorSchemaDelivery.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -27,7 +28,7 @@ it("delivers typed tool failures without violating the advertised success schema ]; for (const request of requests) { const tool = tools.find(({ name }) => name === request.name); - if (tool?.outputSchema === undefined) + if (tool === undefined) throw new Error(`${request.name} omitted its output schema`); const reply = await client.callTool(request, { toolDefinition: tool }); const diagnostic = parseMcpToolError(reply); @@ -40,7 +41,7 @@ it("delivers typed tool failures without violating the advertised success schema const validate = new Ajv2020({ strict: false, validateFormats: false, - }).compile(z.record(z.string(), z.unknown()).parse(tool.outputSchema)); + }).compile(canonicalOutputSchema(tool.name)); if (!validate(reply.structuredContent)) failures.push(request.name); } } diff --git a/tests/boundary/mcp/toolFamiliesMcp.test.ts b/tests/boundary/mcp/toolFamiliesMcp.test.ts index 8e15671d88..2389c6b164 100644 --- a/tests/boundary/mcp/toolFamiliesMcp.test.ts +++ b/tests/boundary/mcp/toolFamiliesMcp.test.ts @@ -29,7 +29,7 @@ const scopedServer = ( undefined, new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE), undefined, - "full", + undefined, undefined, advertisedToolFamilies, ); diff --git a/tests/boundary/mcp/toolInputCapacityHttp.test.ts b/tests/boundary/mcp/toolInputCapacityHttp.test.ts index c5ef69368c..3fd14b5598 100644 --- a/tests/boundary/mcp/toolInputCapacityHttp.test.ts +++ b/tests/boundary/mcp/toolInputCapacityHttp.test.ts @@ -16,7 +16,7 @@ import { const openHttp = async ( processes: TestProcesses, maximum: string | undefined, - outputProfile: "full" | "off" = "full", + outputProfile: "compact" | "off" = "off", ) => { const root = await createTestTempDirectory("rea-input-capacity-http-"); const { url, stderr, child } = await startListeningProcess( diff --git a/tests/boundary/mcp/traceFeatureSeedGuidanceMcp.test.ts b/tests/boundary/mcp/traceFeatureSeedGuidanceMcp.test.ts index a10c899bbc..de43136a2b 100644 --- a/tests/boundary/mcp/traceFeatureSeedGuidanceMcp.test.ts +++ b/tests/boundary/mcp/traceFeatureSeedGuidanceMcp.test.ts @@ -15,7 +15,7 @@ const SEED_KINDS = [ ]; const advertisedSeed = async ( - profile: "full" | "compact", + profile: "compact", parentEnvironment: NodeJS.ProcessEnv = process.env, ) => { const environment: NodeJS.ProcessEnv = { @@ -47,9 +47,7 @@ const advertisedSeed = async ( // the seed union and its kind enums. The field description must still name // every accepted kind so an agent can form a valid seed without the source. it.each([ - ["full", false], ["compact", false], - ["full", true], ["compact", true], ] as const)( "names every trace seed kind (%s schema, restricted parent: %s)", diff --git a/tests/boundary/mcp/uImageMcp.test.ts b/tests/boundary/mcp/uImageMcp.test.ts index d079bef6c0..ec93d9ef05 100644 --- a/tests/boundary/mcp/uImageMcp.test.ts +++ b/tests/boundary/mcp/uImageMcp.test.ts @@ -9,7 +9,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { buildUImage, buildMultiPayload } from "../../fixtures/uimage.js"; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "retains uImage physical facts and unknown text through compiled MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-uimage-mcp-"); diff --git a/tests/boundary/mcp/unityFsMcp.test.ts b/tests/boundary/mcp/unityFsMcp.test.ts index aa43c1d3fc..98810c73e3 100644 --- a/tests/boundary/mcp/unityFsMcp.test.ts +++ b/tests/boundary/mcp/unityFsMcp.test.ts @@ -9,7 +9,7 @@ import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { buildUnityFs } from "../../fixtures/unityfs.js"; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "retains UnityFS facts and unsupported content through compiled MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-unity-mcp-"); diff --git a/tests/boundary/mcp/universalSliceMcp.test.ts b/tests/boundary/mcp/universalSliceMcp.test.ts index a54d9881af..5e5465445e 100644 --- a/tests/boundary/mcp/universalSliceMcp.test.ts +++ b/tests/boundary/mcp/universalSliceMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { createHash } from "node:crypto"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -94,15 +95,13 @@ sliceTest( const advertised = (await client.listTools()).tools.find( ({ name }) => name === "inspect_go_binary", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing advertised Go output schema"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); expect( ajv.compile(advertised.inputSchema)({ path, architecture: "arm64" }), ).toBe(true); - const validateOutput = ajv.compile( - z.record(z.string(), z.unknown()).parse(advertised.outputSchema), - ); + const validateOutput = ajv.compile(canonicalOutputSchema(advertised.name)); const response = await client.callTool({ name: "inspect_go_binary", diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index 78179274fd..8296752709 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -106,7 +106,7 @@ it.each(["bin", "hop"])( const harness = await createApplicationMcpHarness({ schemaValidator: schemaValidation, session, - serverOptions: { environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" } }, + serverOptions: { environment: {} }, }); onTestFinished(harness.close); const { client } = harness; @@ -553,8 +553,7 @@ it("reads an oversized completed WASM result through its reported retained refer const advertised = catalog.tools.find( (tool) => tool.name === "inspect_analysis_view", ); - if (advertised?.outputSchema === undefined) - throw new Error("Missing WASM view schemas"); + if (advertised === undefined) throw new Error("Missing WASM view schemas"); const ajv = schemaValidation.assertions; const input: Record = advertised.inputSchema; for (const [view, expected] of [ @@ -615,7 +614,7 @@ it("rejects inconsistent decoded-WAT digests in portable Evidence without a prov const value = wasmArtifactFixture(); const harness = await createApplicationMcpHarness({ schemaValidator: schemaValidation, - serverOptions: { environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" } }, + serverOptions: { environment: {} }, }); onTestFinished(harness.close); const inconsistent = createEvidence( @@ -658,7 +657,7 @@ it("checks WASM semantic identity while preserving rebound display paths", async const value = wasmArtifactFixture(); const harness = await createApplicationMcpHarness({ schemaValidator: schemaValidation, - serverOptions: { environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" } }, + serverOptions: { environment: {} }, }); onTestFinished(harness.close); const views = [ diff --git a/tests/boundary/mcp/webBundleViewsMcp.test.ts b/tests/boundary/mcp/webBundleViewsMcp.test.ts index 79554ca932..72ccc3314c 100644 --- a/tests/boundary/mcp/webBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/webBundleViewsMcp.test.ts @@ -34,7 +34,7 @@ const connect = async () => { const { client } = await createApplicationMcpHarness({ session, serverOptions: { - environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + environment: { ...process.env }, }, clientIdentity: { name: "web-bundle-view-mcp", version: "1" }, schemaValidator: schemaValidation, diff --git a/tests/boundary/mcp/webModuleTraceMcp.test.ts b/tests/boundary/mcp/webModuleTraceMcp.test.ts index 11d5f50e3f..da15071868 100644 --- a/tests/boundary/mcp/webModuleTraceMcp.test.ts +++ b/tests/boundary/mcp/webModuleTraceMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { AnalysisError } from "../../../src/domain/analysisErrorBase.js"; import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js"; import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; @@ -49,7 +50,7 @@ it("publishes valid SDK schemas and records the named module trace without a bin const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "trace_web_module_imports", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("module trace must advertise both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; @@ -57,7 +58,7 @@ it("publishes valid SDK schemas and records the named module trace without a bin expect(ajv.validate(inputSchema, args)).toBe(true); const response = await client.callTool( { name: "trace_web_module_imports", arguments: args }, - { toolDefinition: advertised }, + { toolDefinition: withCanonicalOutputSchema(advertised) }, ); expect(response.isError).not.toBe(true); const parsed = toolContract("trace_web_module_imports").outputSchema.parse( diff --git a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts index 514c2d372c..8124f1b2f5 100644 --- a/tests/boundary/mcp/webNetworkCaptureMcp.test.ts +++ b/tests/boundary/mcp/webNetworkCaptureMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -60,11 +61,13 @@ it("publishes valid SDK schemas and retains historical inline Evidence with the const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "inspect_web_network_capture", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Capture inspection must advertise both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const args = { capture_path: "/capture.har", format: "har" }; diff --git a/tests/boundary/mcp/webRuntimeMcp.test.ts b/tests/boundary/mcp/webRuntimeMcp.test.ts index af55101182..6950edae10 100644 --- a/tests/boundary/mcp/webRuntimeMcp.test.ts +++ b/tests/boundary/mcp/webRuntimeMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { afterAll, expect, it, onTestFinished } from "vitest"; import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js"; @@ -56,10 +57,12 @@ it("advertises actual SDK schemas, instrumentation effects and session-owned inl "inspect_web_event_listeners", ] as const) { const advertised = tools.find((tool) => tool.name === name); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error(`Missing advertised runtime schemas: ${name}`); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const outputSchema: Record = canonicalOutputSchema( + advertised.name, + ); expect(ajv.validateSchema(inputSchema)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); const args = @@ -218,7 +221,7 @@ cliTest( const advertised = (await client.listTools()).tools.find( ({ name }) => name === "inspect_web_event_listeners", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("Missing advertised listener output schema"); const response = await client.callTool( { diff --git a/tests/boundary/mcp/webSourceLocationMcp.test.ts b/tests/boundary/mcp/webSourceLocationMcp.test.ts index 39d0c6cf5a..ec490a5c64 100644 --- a/tests/boundary/mcp/webSourceLocationMcp.test.ts +++ b/tests/boundary/mcp/webSourceLocationMcp.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { traceSourceMap } from "../../../src/javascript/sourceMaps/TraceSourceMap.js"; import { webSourceLocationArgs as args, @@ -50,14 +51,14 @@ it("publishes valid SDK schemas and records the named module trace without a bin const advertised = (await client.listTools()).tools.find( (tool) => tool.name === "trace_web_source_location", ); - if (advertised?.outputSchema === undefined) + if (advertised === undefined) throw new Error("module trace must advertise both schemas"); const ajv = new Ajv2020({ strict: false, validateFormats: false }); const inputSchema: Record = advertised.inputSchema; expect(ajv.validate(inputSchema, args)).toBe(true); const response = await client.callTool( { name: "trace_web_source_location", arguments: args }, - { toolDefinition: advertised }, + { toolDefinition: withCanonicalOutputSchema(advertised) }, ); expect(response.isError).not.toBe(true); const parsed = toolContract("trace_web_source_location").outputSchema.parse( diff --git a/tests/boundary/mcp/workflowUnknownPublication.test.ts b/tests/boundary/mcp/workflowUnknownPublication.test.ts index 03721ff389..287818b049 100644 --- a/tests/boundary/mcp/workflowUnknownPublication.test.ts +++ b/tests/boundary/mcp/workflowUnknownPublication.test.ts @@ -16,7 +16,7 @@ import { createTestProviderRouter } from "../../fixtures/binarySession.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "reports the committed Unknown prefix and retained trace through MCP (%s)", async (profile) => { const directory = await createTestTempDirectory("rea-unknown-publication-"); diff --git a/tests/boundary/mcp/wxapkgMcp.test.ts b/tests/boundary/mcp/wxapkgMcp.test.ts index 3e655e2870..3573cf8e31 100644 --- a/tests/boundary/mcp/wxapkgMcp.test.ts +++ b/tests/boundary/mcp/wxapkgMcp.test.ts @@ -8,7 +8,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { buildWxapkg, wxapkgEncryptedBytes } from "../../fixtures/wxapkg.js"; -it.each(["full", "compact"] as const)( +it.each(["compact"] as const)( "retains wxapkg facts and encrypted coverage through compiled MCP (%s)", async (profile) => { const root = await createTestTempDirectory("rea-wxapkg-mcp-"); diff --git a/tests/boundary/process/androidAnalysisMcp.test.ts b/tests/boundary/process/androidAnalysisMcp.test.ts index 5a99ddc62a..73bb2a0d9f 100644 --- a/tests/boundary/process/androidAnalysisMcp.test.ts +++ b/tests/boundary/process/androidAnalysisMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js"; import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server"; import { Ajv2020 } from "ajv/dist/2020.js"; @@ -133,12 +134,12 @@ it("publishes and executes all APK contracts with inline Evidence and no active const wireTools = new Map(advertised.tools.map((tool) => [tool.name, tool])); for (const contract of ANDROID_TOOL_CONTRACTS) { const wire = wireTools.get(contract.name); - if (wire === undefined || wire.outputSchema === undefined) + if (wire === undefined || canonicalOutputSchema(wire.name) === undefined) throw new Error(`Missing advertised output schema for ${contract.name}`); const validateOutput = new Ajv2020({ strict: false, validateFormats: false, - }).compile(z.record(z.string(), z.unknown()).parse(wire.outputSchema)); + }).compile(canonicalOutputSchema(wire.name)); expect(wire?.annotations).toMatchObject(contract.annotations); expect(contract.effects).toMatchObject({ mutatesTarget: false, diff --git a/tests/boundary/process/androidMappingMcp.test.ts b/tests/boundary/process/androidMappingMcp.test.ts index 67b6af712d..3ea4a5a59c 100644 --- a/tests/boundary/process/androidMappingMcp.test.ts +++ b/tests/boundary/process/androidMappingMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { createHash } from "node:crypto"; @@ -33,7 +34,6 @@ const connect = async () => { androidAnalysis: fixture.provider, environment: { REA_MCP_TOOL_FAMILIES: "android,application,session", - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, }, ); @@ -84,14 +84,11 @@ it("keeps exact class and selected-method facts in summaries with readable compl "inspect_android_method", ] as const) { const definition = catalog.find((tool) => tool.name === name); - if (definition?.outputSchema === undefined) - throw new Error(`Missing ${name} schema`); + if (definition === undefined) throw new Error(`Missing ${name} schema`); const validate = new Ajv2020({ strict: false, validateFormats: false, - }).compile( - z.record(z.string(), z.unknown()).parse(definition.outputSchema), - ); + }).compile(canonicalOutputSchema(definition.name)); const args = { path: apk, class_name: "fixture.Target", @@ -152,12 +149,12 @@ it("annotates class inspection with a caller-selected mapping and reuses one eng const contract = ANDROID_TOOL_CONTRACTS.find( (candidate) => candidate.name === "inspect_android_class", ); - if (wire?.outputSchema === undefined || contract === undefined) + if (wire === undefined || contract === undefined) throw new Error("Missing inspect_android_class contract"); const validateOutput = new Ajv2020({ strict: false, validateFormats: false, - }).compile(z.record(z.string(), z.unknown()).parse(wire.outputSchema)); + }).compile(canonicalOutputSchema(wire.name)); const apkSha256 = createHash("sha256") .update(await readFile(apk)) .digest("hex"); diff --git a/tests/boundary/process/artifactMcpVerifierOutput.test.ts b/tests/boundary/process/artifactMcpVerifierOutput.test.ts index 7fd21ab3da..cb628ab802 100644 --- a/tests/boundary/process/artifactMcpVerifierOutput.test.ts +++ b/tests/boundary/process/artifactMcpVerifierOutput.test.ts @@ -47,7 +47,7 @@ async function probe(mode: string, name = "Fixture.app") { return observationSchema.parse(JSON.parse(stdout)); } -it("validates the real resource admission reply against its advertised schema", async () => { +it("validates the real resource admission reply against its canonical output schema", async () => { expect(await probe("healthy", "Fixture.app")).toEqual({ accepted: true, catalogRequests: 1, @@ -83,22 +83,11 @@ it.each(["reuse", "concurrent"])( }, ); -it("binds the new advertised schema after reconnecting the same client", async () => { +it("rebinds the advertised catalog after reconnecting the same client", async () => { const result = await probe("reconnect"); - expect(result.accepted).toBe(false); + expect(result.accepted).toBe(true); expect(result.catalogRequests).toBe(2); expect(result.toolCalls).toBe(2); - expect(result.error).toMatchObject({ - code: -32602, - message: expect.stringContaining("__rea_second_connection"), - }); -}); - -it("rejects an omitted output schema before calling the advertised tool", async () => { - const result = await probe("missing-schema"); - expect(result.accepted).toBe(false); - expect(result.toolCalls).toBe(0); - expect(result.error?.message).toMatch(/omitted its output schema/u); }); it("rejects a supplied schema override before dispatching that call", async () => { diff --git a/tests/boundary/process/processSessionMcp.test.ts b/tests/boundary/process/processSessionMcp.test.ts index 8205469017..f828477c79 100644 --- a/tests/boundary/process/processSessionMcp.test.ts +++ b/tests/boundary/process/processSessionMcp.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { fileURLToPath } from "node:url"; import { access, chmod, mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -260,11 +261,11 @@ itWithCaptureCapability( const wire = (await mcp.listTools()).tools.find( ({ name }) => name === contract.name, ); - if (wire?.outputSchema === undefined) + if (wire === undefined) throw new Error("Missing process capture output schema"); expect( new Ajv2020({ strict: false, validateFormats: false }).validate( - z.record(z.string(), z.unknown()).parse(wire.outputSchema), + canonicalOutputSchema(wire.name), captured.structuredContent, ), ).toBe(true); diff --git a/tests/boundary/process/releaseArtifactCanary.test.ts b/tests/boundary/process/releaseArtifactCanary.test.ts index 2257ce8508..252803b3b3 100644 --- a/tests/boundary/process/releaseArtifactCanary.test.ts +++ b/tests/boundary/process/releaseArtifactCanary.test.ts @@ -1,3 +1,4 @@ +import { withCanonicalOutputSchema } from "../../fixtures/canonicalOutputSchema.js"; import { copyFile, cp, @@ -73,7 +74,7 @@ processTest.skipIf(process.platform === "win32")( tools, reply: await harness.client.callTool( { name: "binary_session", arguments: {} }, - { toolDefinition: sessionDefinition }, + { toolDefinition: withCanonicalOutputSchema(sessionDefinition) }, ), }; } finally { diff --git a/tests/boundary/providers/kuna/kunaDirectoryIndependentRetryMcp.test.ts b/tests/boundary/providers/kuna/kunaDirectoryIndependentRetryMcp.test.ts index 3598c1da49..6bcd27812c 100644 --- a/tests/boundary/providers/kuna/kunaDirectoryIndependentRetryMcp.test.ts +++ b/tests/boundary/providers/kuna/kunaDirectoryIndependentRetryMcp.test.ts @@ -45,7 +45,7 @@ const expectPriorFailureInJournal = async (output: string) => { expect(latest?.limitations).not.toContain(failed?.pseudocode_absence?.reason); }; -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "guides resume and retries only reported project failures (%s)", async (profile) => { const root = await createTestTempDirectory("rea-directory-independent-"); diff --git a/tests/boundary/providers/kuna/kunaIndependentDecompileMcp.test.ts b/tests/boundary/providers/kuna/kunaIndependentDecompileMcp.test.ts index f0e68bb690..3369bea2b8 100644 --- a/tests/boundary/providers/kuna/kunaIndependentDecompileMcp.test.ts +++ b/tests/boundary/providers/kuna/kunaIndependentDecompileMcp.test.ts @@ -119,7 +119,7 @@ it("runs a cold independent request without starting a project export", async () } }); -it.each(["compact", "full"] as const)( +it.each(["compact"] as const)( "keeps the failed project observation when an explicit fresh retry succeeds (%s)", async (profile) => { const root = await createTestTempDirectory("rea-kuna-independent-"); diff --git a/tests/conformance/contracts/mcpInputSchemaProfile.test.ts b/tests/conformance/contracts/mcpInputSchemaProfile.test.ts index 48b3769d27..7fafcacb64 100644 --- a/tests/conformance/contracts/mcpInputSchemaProfile.test.ts +++ b/tests/conformance/contracts/mcpInputSchemaProfile.test.ts @@ -1,43 +1,40 @@ import { describe, expect, it } from "vitest"; -import { - compactPresentationForMcpInputSchemaProfile, - COMPACT_INPUT_SCHEMA_BUDGET_BYTES, - parseMcpInputSchemaProfile, -} from "../../../src/config/mcpInputSchemaProfile.js"; +import { parseMcpInputSchemaProfile } from "../../../src/config/mcpInputSchemaProfile.js"; +import { parseConfig } from "../../../src/config/parseConfig.js"; describe("MCP input schema profile configuration", () => { - it("accepts the full and compact presentations", () => { + it("accepts only the compact presentation", () => { expect(parseMcpInputSchemaProfile(undefined)).toEqual({ ok: true, value: undefined, }); - expect(parseMcpInputSchemaProfile("full")).toEqual({ - ok: true, - value: "full", - }); expect(parseMcpInputSchemaProfile("compact")).toEqual({ ok: true, value: "compact", }); }); - it.each(["", "FULL", "compact ", "minimal", "0"])( - "rejects unknown profiles: %j", + // `full` was removed; a registration that still selects it fails closed. + it.each(["full", "", "FULL", "compact ", "minimal", "0"])( + "rejects unsupported profiles: %j", (value) => { const parsed = parseMcpInputSchemaProfile(value); if (parsed.ok) throw new Error("Expected invalid profile"); - expect(parsed.error.message).toContain("REA_MCP_INPUT_SCHEMA_PROFILE"); + expect(parsed.error.settings).toEqual([ + { + setting: "REA_MCP_INPUT_SCHEMA_PROFILE", + constraint: expect.any(String), + }, + ]); }, ); - it("defaults to compact while preserving an explicit full choice", () => { - expect(compactPresentationForMcpInputSchemaProfile(undefined)).toEqual({ - budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, - }); - expect(compactPresentationForMcpInputSchemaProfile("full")).toBe(undefined); - expect(compactPresentationForMcpInputSchemaProfile("compact")).toEqual({ - budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, - }); + it("refuses the removed full profile at server configuration", () => { + const parsed = parseConfig({ REA_MCP_INPUT_SCHEMA_PROFILE: "full" }); + if (parsed.ok) throw new Error("Expected the removed profile to fail"); + expect(parsed.error.settings).toEqual([ + expect.objectContaining({ setting: "REA_MCP_INPUT_SCHEMA_PROFILE" }), + ]); }); }); diff --git a/tests/conformance/contracts/mcpOutputSchemaProfile.test.ts b/tests/conformance/contracts/mcpOutputSchemaProfile.test.ts index 7b659d4dd7..dcf3a7f5df 100644 --- a/tests/conformance/contracts/mcpOutputSchemaProfile.test.ts +++ b/tests/conformance/contracts/mcpOutputSchemaProfile.test.ts @@ -1,43 +1,41 @@ import { describe, expect, it } from "vitest"; -import { - COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES, - compactPresentationForMcpOutputSchemaProfile, - parseMcpOutputSchemaProfile, -} from "../../../src/config/mcpOutputSchemaProfile.js"; +import { parseMcpOutputSchemaProfile } from "../../../src/config/mcpOutputSchemaProfile.js"; +import { parseConfig } from "../../../src/config/parseConfig.js"; describe("MCP output schema profile configuration", () => { - it("accepts the full, compact, and off presentations", () => { + it("accepts the off and compact presentations", () => { expect(parseMcpOutputSchemaProfile(undefined)).toEqual({ ok: true, value: undefined, }); - for (const value of ["full", "compact", "off"] as const) + for (const value of ["compact", "off"] as const) expect(parseMcpOutputSchemaProfile(value)).toEqual({ ok: true, value, }); }); - it.each(["", "FULL", "compact ", "off ", "minimal", "0"])( - "rejects unknown profiles: %j", + // `full` was removed; a registration that still selects it fails closed. + it.each(["full", "", "FULL", "compact ", "off ", "minimal", "0"])( + "rejects unsupported profiles: %j", (value) => { const parsed = parseMcpOutputSchemaProfile(value); if (parsed.ok) throw new Error("Expected invalid profile"); - expect(parsed.error.message).toContain("REA_MCP_OUTPUT_SCHEMA_PROFILE"); + expect(parsed.error.settings).toEqual([ + { + setting: "REA_MCP_OUTPUT_SCHEMA_PROFILE", + constraint: expect.any(String), + }, + ]); }, ); - it("selects a bounded presentation only for compact", () => { - expect(compactPresentationForMcpOutputSchemaProfile("compact")).toEqual({ - budgetBytes: COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES, - }); - expect(compactPresentationForMcpOutputSchemaProfile(undefined)).toBe( - undefined, - ); - expect(compactPresentationForMcpOutputSchemaProfile("full")).toBe( - undefined, - ); - expect(compactPresentationForMcpOutputSchemaProfile("off")).toBe(undefined); + it("refuses the removed full profile at server configuration", () => { + const parsed = parseConfig({ REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }); + if (parsed.ok) throw new Error("Expected the removed profile to fail"); + expect(parsed.error.settings).toEqual([ + expect.objectContaining({ setting: "REA_MCP_OUTPUT_SCHEMA_PROFILE" }), + ]); }); }); diff --git a/tests/fixtures/advertisedMcpTool.ts b/tests/fixtures/advertisedMcpTool.ts index dc6da77f83..f87ef5dcb5 100644 --- a/tests/fixtures/advertisedMcpTool.ts +++ b/tests/fixtures/advertisedMcpTool.ts @@ -1,3 +1,8 @@ +import { + canonicalInputSchema, + canonicalOutputSchema, + withCanonicalOutputSchema, +} from "./canonicalOutputSchema.js"; import type { Client } from "@modelcontextprotocol/client"; import { Ajv2020 } from "ajv/dist/2020.js"; import { expect } from "vitest"; @@ -14,7 +19,11 @@ interface AdvertisedSchemaAssertions { ): boolean | Promise; } -/** Verify actual advertised schemas; callers may own file-local preparation. */ +/** + * The listed input schema, the canonical input schema it presents, and the + * canonical output schema a validating client checks replies against; + * callers may own file-local preparation. + */ export const advertisedMcpToolSchemas = async ( client: Client, name: string, @@ -31,17 +40,19 @@ export const advertisedMcpToolSchemas = async ( if (matches.length > 1) throw new Error(`${name} has multiple advertised definitions`); const advertised = matches[0]; - if (advertised?.outputSchema === undefined) - throw new Error(`${name} must publish both schemas`); + if (advertised === undefined) throw new Error(`${name} is not advertised`); const inputSchema: Record = advertised.inputSchema; - const outputSchema: Record = advertised.outputSchema; + const canonicalInput = canonicalInputSchema(name); + const outputSchema = canonicalOutputSchema(advertised.name); expect(ajv.validateSchema(inputSchema)).toBe(true); + expect(ajv.validateSchema(canonicalInput)).toBe(true); expect(ajv.validateSchema(outputSchema)).toBe(true); return { name: advertised.name, - toolDefinition: advertised, + toolDefinition: withCanonicalOutputSchema(advertised), ajv, inputSchema, + canonicalInputSchema: canonicalInput, outputSchema, }; }; diff --git a/tests/fixtures/artifactMcpVerifierProbe.mjs b/tests/fixtures/artifactMcpVerifierProbe.mjs index ca2c176a66..af84fcdc72 100644 --- a/tests/fixtures/artifactMcpVerifierProbe.mjs +++ b/tests/fixtures/artifactMcpVerifierProbe.mjs @@ -4,7 +4,6 @@ import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import * as artifact from "../../scripts/lib/artifact-e2e.mjs"; -import { schemaVerifierEnvironment } from "../../scripts/lib/mcp-verification-environment.mjs"; import { withVerifierCleanup } from "../../scripts/lib/verifier-cleanup.mjs"; const [mode, target] = process.argv.slice(2); @@ -15,7 +14,6 @@ assert.ok( "reuse", "concurrent", "reconnect", - "missing-schema", "duplicate-first", "duplicate-last", "override", @@ -60,15 +58,6 @@ Client.prototype.request = async function (input, options) { return { ...result, structuredContent: { result: 777 } }; } if (input.method === "tools/list") { - if (mode === "missing-schema") - return { - ...result, - tools: result.tools.map((tool) => { - if (tool.name !== "open_bundle_resources") return tool; - const { outputSchema: _schema, ...withoutSchema } = tool; - return withoutSchema; - }), - }; if (mode === "duplicate-first" || mode === "duplicate-last") { const listed = result.tools.find( (tool) => tool.name === "open_bundle_resources", @@ -86,21 +75,6 @@ Client.prototype.request = async function (input, options) { : [...result.tools, duplicate], }; } - if (mode === "reconnect" && connection === 2) - return { - ...result, - tools: result.tools.map((tool) => - tool.name === "binary_session" - ? { - ...tool, - outputSchema: { - type: "object", - required: ["__rea_second_connection"], - }, - } - : tool, - ), - }; } if ( mode !== "corrupt" || @@ -145,10 +119,10 @@ async function standaloneConnections() { fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url)), "mcp", ], - env: schemaVerifierEnvironment({ + env: { PATH: process.env.PATH ?? "/usr/bin:/bin", REA_LOG_LEVEL: "silent", - }), + }, stderr: "pipe", }); await withVerifierCleanup(async () => { diff --git a/tests/fixtures/buildComputerMcp.ts b/tests/fixtures/buildComputerMcp.ts index 4c2a1cb641..2cdcfeb2aa 100644 --- a/tests/fixtures/buildComputerMcp.ts +++ b/tests/fixtures/buildComputerMcp.ts @@ -57,7 +57,6 @@ export const connectBuildServer = async ( { environment: { ...hostEnvironment, - REA_MCP_INPUT_SCHEMA_PROFILE: "full", ...environment, }, desktopBuildSettle: settle, diff --git a/tests/fixtures/canonicalOutputSchema.ts b/tests/fixtures/canonicalOutputSchema.ts new file mode 100644 index 0000000000..eb8b90bd63 --- /dev/null +++ b/tests/fixtures/canonicalOutputSchema.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; + +import { DISCOVER_TOOLS } from "../../src/catalogDiscovery.js"; +import { TOOL_CONTRACTS } from "../../src/contracts/toolContracts.js"; +import { + advertisedInputJsonSchema, + advertisedOutputJsonSchema, +} from "../../src/contracts/toolSchemaMetadata.js"; + +const contractNamed = (name: string) => { + const contract = TOOL_CONTRACTS.find((candidate) => candidate.name === name); + if (contract === undefined) throw new Error(`No tool contract named ${name}`); + return contract; +}; + +/** + * The canonical output JSON Schema a validating client checks a reply + * against. `tools/list` omits output schemas by default, so tests validate + * structured results against the contract rather than a listed definition. + * Transport discovery is registered from its own schema, not a tool contract. + */ +export const canonicalOutputSchema = (name: string): Record => + name === DISCOVER_TOOLS.name + ? z.toJSONSchema(DISCOVER_TOOLS.outputSchema, { io: "output" }) + : advertisedOutputJsonSchema(contractNamed(name)); + +/** The canonical input JSON Schema that the compact advertisement presents. */ +export const canonicalInputSchema = (name: string): Record => + advertisedInputJsonSchema(contractNamed(name)); + +/** + * A listed tool definition carrying its canonical output schema, so the SDK + * client (and any caller-supplied validator) checks replies against the + * contract even though `tools/list` omits output schemas by default. + */ +export const withCanonicalOutputSchema = < + Tool extends { readonly name: string }, +>( + tool: Tool, +): Tool & { outputSchema: Record } => ({ + ...tool, + outputSchema: canonicalOutputSchema(tool.name), +}); diff --git a/tests/fixtures/localToolsMcp.ts b/tests/fixtures/localToolsMcp.ts index 9d1f0664fd..a8910d5cb1 100644 --- a/tests/fixtures/localToolsMcp.ts +++ b/tests/fixtures/localToolsMcp.ts @@ -1,3 +1,7 @@ +import { + canonicalOutputSchema, + withCanonicalOutputSchema, +} from "./canonicalOutputSchema.js"; import { Client, type CallToolRequestOptions, @@ -20,8 +24,6 @@ export const localToolsMcpEnvironment = ( REA_LOG_LEVEL: "silent", HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper", GHIDRA_INSTALL_DIR: process.execPath, - // Validate each result against its complete advertised output contract. - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", ...environment, }); @@ -57,8 +59,12 @@ export async function connectLocalToolsMcp( await client.connect(transport); const tools = (await client.listTools()).tools; // Catalog dialect/reference validity belongs to toolSchemaValidity and the - // compact-profile catalog tests. SDK callTool validates each successful reply. - const definitions = new Map(tools.map((tool) => [tool.name, tool])); + // compact-profile catalog tests. + // tools/list omits output schemas by default; bind each definition to its + // canonical output contract so the SDK client validates successful replies. + const definitions = new Map( + tools.map((tool) => [tool.name, withCanonicalOutputSchema(tool)]), + ); const call = async ( name: string, arguments_: Record, @@ -73,11 +79,11 @@ export async function connectLocalToolsMcp( }, ); // The SDK skips error payloads. A caller-supplied validator also retains - // the fixture's independent advertised-schema assertion. + // the fixture's independent canonical-schema assertion. if ( (response.isError === true || jsonSchemaValidator !== undefined) && response.structuredContent !== undefined && - definition?.outputSchema !== undefined + definition !== undefined ) { expect( validation.assertions.validate( diff --git a/tests/process-global/processCaptureFilenameBytes.test.ts b/tests/process-global/processCaptureFilenameBytes.test.ts index 7b268fab83..425289c9cc 100644 --- a/tests/process-global/processCaptureFilenameBytes.test.ts +++ b/tests/process-global/processCaptureFilenameBytes.test.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { createHash } from "node:crypto"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import type { Dirent } from "node:fs"; @@ -149,8 +150,12 @@ it("advertises byte-preserving omissions in valid SDK-converted MCP schemas", as expect(tool).toBeDefined(); if (tool === undefined) throw new Error("Missing process capture contract"); expect(advertisedSchemaErrors([tool])).toEqual([]); - expect(JSON.stringify(tool.outputSchema)).toContain("unrepresentable_name"); - expect(JSON.stringify(tool.outputSchema)).toContain("raw_name_hex"); + expect(JSON.stringify(canonicalOutputSchema(tool.name))).toContain( + "unrepresentable_name", + ); + expect(JSON.stringify(canonicalOutputSchema(tool.name))).toContain( + "raw_name_hex", + ); } finally { await client.close(); await server.close(); diff --git a/tests/support/analysis-efficiency/android-jni.mjs b/tests/support/analysis-efficiency/android-jni.mjs index 28d4bbafe7..3571bb2720 100644 --- a/tests/support/analysis-efficiency/android-jni.mjs +++ b/tests/support/analysis-efficiency/android-jni.mjs @@ -1,4 +1,5 @@ /** Public SDK/stdio JNI investigation, checked against independent NDK/DEX facts. */ +import { selectedOutputContracts } from "./selected-output-contracts.mjs"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { @@ -721,6 +722,8 @@ const { StdioClientTransport } = await import( const { Ajv2020 } = await import( pathToFileURL(require.resolve("ajv/dist/2020.js")) ); +const outputContracts = await selectedOutputContracts(repo); +const { withCanonicalOutputSchema } = outputContracts; const environment = { PATH: process.env.PATH, JAVA_HOME: resolve(values["java-home"]), @@ -737,8 +740,8 @@ const environment = { ? "android,session" : "android", REA_MCP_INPUT_SCHEMA_PROFILE: "compact", - // The run validates each result against its advertised output schema. - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", + // The run validates each result against the selected revision's output contract. + ...outputContracts.environment, }; const transport = new StdioClientTransport({ command: process.execPath, @@ -809,11 +812,11 @@ try { ]) { const selected = catalog.tools.filter((tool) => tool.name === name); assert.equal(selected.length, 1); - assert.ok(selected[0].outputSchema); + const definition = withCanonicalOutputSchema(selected[0]); contracts.set(name, { - definition: selected[0], - input: ajv.compile(selected[0].inputSchema), - output: ajv.compile(selected[0].outputSchema), + definition, + input: ajv.compile(definition.inputSchema), + output: ajv.compile(definition.outputSchema), }); } validateJniOutput = contracts.get("inspect_android_jni_bindings").output; diff --git a/tests/support/analysis-efficiency/android-mapping.mjs b/tests/support/analysis-efficiency/android-mapping.mjs index 117045f087..c094e59d1f 100644 --- a/tests/support/analysis-efficiency/android-mapping.mjs +++ b/tests/support/analysis-efficiency/android-mapping.mjs @@ -1,4 +1,5 @@ /** Public MCP mapping investigation with a source inventory and raw DEX oracle. */ +import { selectedOutputContracts } from "./selected-output-contracts.mjs"; import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { readFile, writeFile, mkdir } from "node:fs/promises"; @@ -56,6 +57,8 @@ const { StdioClientTransport } = await import( const { Ajv2020 } = await import( pathToFileURL(require.resolve("ajv/dist/2020.js")) ); +const outputContracts = await selectedOutputContracts(repo); +const { withCanonicalOutputSchema } = outputContracts; const transport = new StdioClientTransport({ command: process.execPath, args: [join(repo, "scripts/rea.mjs"), "mcp"], @@ -71,8 +74,8 @@ const transport = new StdioClientTransport({ REA_LOG_LEVEL: "silent", REA_MCP_TOOL_FAMILIES: values.proof ? "android,session" : "android", REA_MCP_INPUT_SCHEMA_PROFILE: "compact", - // The run validates each result against its advertised output schema. - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", + // The run validates each result against the selected revision's output contract. + ...outputContracts.environment, }, stderr: "pipe", maxBufferSize: 16 * 1024 * 1024, @@ -99,10 +102,10 @@ try { ]) { const definitions = catalog.tools.filter((tool) => tool.name === name); assert.equal(definitions.length, 1); - assert.ok(definitions[0].outputSchema); + const definition = withCanonicalOutputSchema(definitions[0]); contracts.set(name, { - definition: definitions[0], - validate: ajv.compile(definitions[0].outputSchema), + definition, + validate: ajv.compile(definition.outputSchema), }); } const parents = []; diff --git a/tests/support/analysis-efficiency/catalog-eval.py b/tests/support/analysis-efficiency/catalog-eval.py index 6081992a76..e8776b2da7 100644 --- a/tests/support/analysis-efficiency/catalog-eval.py +++ b/tests/support/analysis-efficiency/catalog-eval.py @@ -175,8 +175,8 @@ def comparison(rows, expected_runtime_digest): return {'pairs': pairs, 'groups': groups} -def catalog_receipt(root, profile, selected): - source = root / 'dist' / ('mcp-catalog-full.json' if profile == 'full' else 'mcp-catalog.json') +def catalog_receipt(root, selected): + source = root / 'dist' / 'mcp-catalog.json' data = source.read_bytes() catalog = json.loads(data) names = {tool['name'] for tool in catalog['tools']} @@ -200,7 +200,6 @@ def main(): parser.add_argument('--reasoning', default='low') parser.add_argument('--modes', nargs='+', choices=['direct', 'client-code'], default=['client-code'], help='Default: supported host mode. direct disables the host as a diagnostic control.') - parser.add_argument('--input-profile', choices=['compact', 'full'], default='compact') parser.add_argument('--selected-tools', nargs='+', default=['binary_session']) parser.add_argument('--repetitions', type=int, default=1) parser.add_argument('--timeout', type=int, default=180) @@ -212,7 +211,7 @@ def main(): root = args.runtime_root.resolve() output = args.output.resolve() runtime_files, pinned_digest = (None, None) if args.plan else runtime_fingerprint(root) - receipt = catalog_receipt(root, args.input_profile, args.selected_tools) + receipt = catalog_receipt(root, args.selected_tools) if not args.plan: catalog_path = Path(receipt['path']).relative_to(root).as_posix() pinned_catalog = next((row for row in runtime_files if row['path'] == catalog_path), None) @@ -227,7 +226,7 @@ def main(): for arm in ['none', 'all', 'selected'][::1 if repetition % 2 == 0 else -1]: matrix.append({'repetition': repetition + 1, 'mode': mode, 'arm': arm}) experiment = {'version': 1, 'runtime_revision': revision, 'model': args.model, 'reasoning': args.reasoning, - 'input_profile': args.input_profile, 'catalog': receipt, 'matrix': matrix, + 'catalog': receipt, 'matrix': matrix, 'sources_sha256': {name: hashlib.sha256((HERE / name).read_bytes()).hexdigest() for name in ['catalog-eval.py', 'agent-trial.py', 'codex_context.py', 'runtime_identity.py', 'resource-run.py', 'resource_payload.py', 'provider-environment.json']}, @@ -242,7 +241,7 @@ def main(): write_json(output / 'runtime-files.json', runtime_files) (output / 'prompt.txt').write_text(PROMPT) write_json(output / 'selected-tools.json', args.selected_tools) - write_json(output / 'providers.json', {'REA_MCP_INPUT_SCHEMA_PROFILE': args.input_profile}) + write_json(output / 'providers.json', {}) rows = [] for index, cell in enumerate(matrix): reason = 'Runtime changed between trials; no further model calls were made' diff --git a/tests/support/analysis-efficiency/selected-output-contracts.mjs b/tests/support/analysis-efficiency/selected-output-contracts.mjs new file mode 100644 index 0000000000..ad4a62e5fe --- /dev/null +++ b/tests/support/analysis-efficiency/selected-output-contracts.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; + +/** + * Output contracts of the revision under test, not the harness checkout's. + * Revisions with the canonical helper validate against their compiled + * contracts; earlier revisions list complete output schemas only under the + * full output profile, so the runner requests it for them. + */ +export const selectedOutputContracts = async (repo) => { + const helper = join(repo, "scripts/lib/canonical-output-schema.mjs"); + if (existsSync(helper)) { + const { withCanonicalOutputSchema } = await import( + pathToFileURL(helper).href + ); + return { environment: {}, withCanonicalOutputSchema }; + } + return { + environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: "full" }, + withCanonicalOutputSchema: (tool) => { + assert.ok(tool.outputSchema, `${tool.name} lists no output schema`); + return tool; + }, + }; +}; diff --git a/tests/support/reverseDebuggerMcpTestHelpers.ts b/tests/support/reverseDebuggerMcpTestHelpers.ts index 5ba79c869f..dea42e0981 100644 --- a/tests/support/reverseDebuggerMcpTestHelpers.ts +++ b/tests/support/reverseDebuggerMcpTestHelpers.ts @@ -1,3 +1,4 @@ +import { canonicalOutputSchema } from "../fixtures/canonicalOutputSchema.js"; import { readFile, rm } from "node:fs/promises"; import type { Client } from "@modelcontextprotocol/client"; @@ -27,7 +28,7 @@ export const validateDebuggerStatusResponse = async ( const listed = (await client.listTools()).tools.find( (tool) => tool.name === name, ); - if (listed?.outputSchema === undefined) + if (listed === undefined) throw new Error(`Missing advertised ${name} output schema`); const response = await client.callTool({ name, @@ -39,7 +40,7 @@ export const validateDebuggerStatusResponse = async ( ); expect( new Ajv2020({ strict: false, validateFormats: false }).validate( - z.record(z.string(), z.unknown()).parse(listed.outputSchema), + canonicalOutputSchema(listed.name), parsed, ), ).toBe(true); diff --git a/vitest.config.ts b/vitest.config.ts index e0ffaf84ad..c27ffe1756 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -177,10 +177,6 @@ export default defineConfig({ test: { env: { TMPDIR: CANONICAL_TEMPORARY_DIRECTORY, - // In-process servers advertise complete output schemas so SDK clients - // validate every structured result. Tests of the default (`off`) set - // the profile, or pass an environment without it, explicitly. - REA_MCP_OUTPUT_SCHEMA_PROFILE: "full", }, maxWorkers: MAX_TEST_WORKERS, projects,