mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* 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. * 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): keep an allOf member's tuple tail with its own positions * fix(mcp): relax references into negations and conditionals presentation may remove * 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): 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): expect the advertised base64 schema to omit the unbounded pad-bit probe * fix(mcp): protect references into unevaluated keywords lost evaluation removes * 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.
371 lines
15 KiB
TypeScript
371 lines
15 KiB
TypeScript
import {
|
|
JsonSchemaReferences,
|
|
type JsonSchemaScope,
|
|
} from "./jsonSchemaReferences.js";
|
|
import { z } from "zod";
|
|
|
|
import type { ToolContract } from "./toolContractTypes.js";
|
|
import { advertisedInputExamples } from "./advertisedInputExamples.js";
|
|
import {
|
|
presentInputJsonSchema,
|
|
presentOutputJsonSchema,
|
|
} from "./inputSchemaPresentation.js";
|
|
import {
|
|
schemaArrays,
|
|
schemaChildren,
|
|
schemaMaps,
|
|
schemaLiteralValues,
|
|
} from "./jsonSchemaKeywords.js";
|
|
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
|
|
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
|
|
import { isRecord } from "../domain/isRecord.js";
|
|
|
|
const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
|
addresses: "Ordered provider-normalized procedure addresses to analyze.",
|
|
after: "The later or right-hand observation to compare.",
|
|
before: "The earlier or left-hand observation to compare.",
|
|
boundary_id: "Exact reconstruction boundary identifier to evaluate.",
|
|
case_sensitive:
|
|
"Whether text matching distinguishes uppercase and lowercase.",
|
|
cdp_endpoint: "Literal loopback Chrome DevTools Protocol endpoint.",
|
|
comment: "Exact analyst comment text to write.",
|
|
comparisons: "Validated comparison Evidence records to aggregate.",
|
|
coverage: "Exact reconstruction-coverage commitment to verify.",
|
|
direction: "Direction in which to traverse or compare relationships.",
|
|
document: "Exact provider document or program identity.",
|
|
error: "Structured, caller-actionable error when the operation fails.",
|
|
evidence: "Evidence record produced by this operation.",
|
|
evidence_id: "Stable identifier of the recorded Evidence observation.",
|
|
executable: "Command name or executable path to run.",
|
|
format: "Declared input artifact format.",
|
|
name: "Exact name used by this operation.",
|
|
pattern:
|
|
"Literal text or regular expression used to filter matching results.",
|
|
phase: "Current plan or execution phase of the operation.",
|
|
plan: "Content-bound execution plan and its digest commitment.",
|
|
query: "Non-empty feature or text query to investigate.",
|
|
question: "Concrete unresolved question to retain for later investigation.",
|
|
result: "Primary structured result returned by this operation.",
|
|
left: "Left-hand input used for comparison or differential execution.",
|
|
limits:
|
|
"Resource budgets that bound execution and retained output for this operation.",
|
|
mode: "Operation mode that selects the requested behavior.",
|
|
overwrite: "Whether an existing destination may be replaced.",
|
|
path: "Local filesystem path used by this operation.",
|
|
provider_id:
|
|
"Exact deep-analysis provider ID, or automatic selection when omitted.",
|
|
right: "Right-hand input used for comparison or differential execution.",
|
|
source_evidence:
|
|
"Evidence records supporting the prepared source transformation.",
|
|
status: "Current lifecycle or verification status.",
|
|
summary: "Concise evidence-backed summary of the result.",
|
|
symbols: "Ordered Swift symbols to demangle.",
|
|
target_id:
|
|
"Exact CDP target identifier selected from the endpoint's target listing.",
|
|
unknown_id: "Exact residual-unknown identifier.",
|
|
};
|
|
|
|
type JsonSchemaProjection = z.ZodType["~standard"]["jsonSchema"]["input"];
|
|
|
|
// Contracts are immutable module values. Build each advertised wrapper once so
|
|
// its memoized projection serves every server in the process, not only the
|
|
// server that registered it.
|
|
const advertisedInputSchemas = new WeakMap<ToolContract, z.ZodType>();
|
|
const advertisedOutputSchemas = new WeakMap<ToolContract, z.ZodType>();
|
|
|
|
const oncePerContract = <
|
|
Contract extends ToolContract,
|
|
Schema extends z.ZodType,
|
|
>(
|
|
cache: WeakMap<ToolContract, z.ZodType>,
|
|
contract: Contract,
|
|
build: () => Schema,
|
|
): Schema => {
|
|
const cached = cache.get(contract);
|
|
if (cached !== undefined) return cached as Schema;
|
|
const built = build();
|
|
cache.set(contract, built);
|
|
return built;
|
|
};
|
|
|
|
/** Attach caller guidance to a canonical schema for the SDK wire projection. */
|
|
export const toolInputSchemaWithMetadata = <Contract extends ToolContract>(
|
|
contract: Contract,
|
|
): Contract["inputSchema"] =>
|
|
oncePerContract(advertisedInputSchemas, contract, () =>
|
|
withAdvertisedJsonSchema(
|
|
contract.inputSchema,
|
|
"input",
|
|
(project) => (options) => {
|
|
const inline = project({
|
|
...options,
|
|
libraryOptions: { ...options.libraryOptions, reused: "inline" },
|
|
});
|
|
const examples = advertisedInputExamples(contract);
|
|
const presented = presentInputJsonSchema(
|
|
inline,
|
|
fallbackPropertyDescription,
|
|
);
|
|
const sourceGuidance = getEvidenceInputContract(contract.inputSchema)
|
|
.hasEvidence
|
|
? 'Declared Evidence fields accept complete records or {"kind":"retained-evidence","evidence_id":"ev_..."}. For a complete source, use the summary\'s parent_evidence_id.'
|
|
: undefined;
|
|
return {
|
|
// Keep advertised parameters self-contained for clients that do not
|
|
// resolve local references. The registered canonical schema remains
|
|
// responsible for runtime validation.
|
|
...presented,
|
|
...(sourceGuidance === undefined
|
|
? {}
|
|
: {
|
|
description: [presented.description, sourceGuidance]
|
|
.filter((text) => typeof text === "string")
|
|
.join(" "),
|
|
}),
|
|
...(examples.length === 0 ? {} : { examples }),
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
/** Return the full-profile advertised input JSON Schema registered on MCP. */
|
|
export const advertisedInputJsonSchema = <Contract extends ToolContract>(
|
|
contract: Contract,
|
|
): Record<string, unknown> =>
|
|
toolInputSchemaWithMetadata(contract)["~standard"].jsonSchema.input({
|
|
target: "draft-2020-12",
|
|
}) as Record<string, unknown>;
|
|
|
|
/**
|
|
* Return the canonical output JSON Schema: the complete projection of the
|
|
* contract's output, which the full output profile lists. A client that
|
|
* validates a reply uses it whatever the selected profile advertises.
|
|
*/
|
|
export const advertisedOutputJsonSchema = <Contract extends ToolContract>(
|
|
contract: Contract,
|
|
): Record<string, unknown> =>
|
|
toolOutputSchemaWithMetadata(contract)["~standard"].jsonSchema.output({
|
|
target: "draft-2020-12",
|
|
}) as Record<string, unknown>;
|
|
|
|
/** Share output definitions and content-bound validator identities on the wire. */
|
|
export const toolOutputSchemaWithMetadata = <Contract extends ToolContract>(
|
|
contract: Contract,
|
|
): Contract["outputSchema"] =>
|
|
oncePerContract(advertisedOutputSchemas, contract, () =>
|
|
withAdvertisedJsonSchema(
|
|
contract.outputSchema,
|
|
"output",
|
|
(project) => (options) => {
|
|
const shared = presentOutputJsonSchema(
|
|
project({
|
|
...options,
|
|
libraryOptions: { reused: "ref", ...options.libraryOptions },
|
|
}),
|
|
);
|
|
// An explicit ID owns the reference base. Relative external references
|
|
// also depend on that base, so leave those projections unchanged.
|
|
if ("$id" in shared || hasRelativeSchemaReference(shared))
|
|
return shared;
|
|
return {
|
|
...shared,
|
|
$id: `urn:rea:tool-output:sha256:${digestCanonicalValue(
|
|
{ target: options.target, schema: shared },
|
|
"Tool output schema",
|
|
)}`,
|
|
};
|
|
},
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Apply a wire-presentation transform to one schema's advertised input
|
|
* projection. The wrapper preserves the schema's own parser and standard
|
|
* metadata, so both Zod registrations and plain standard schemas keep their
|
|
* canonical validation while only the advertised JSON changes.
|
|
*/
|
|
export const transformAdvertisedInputJsonSchema = <Schema>(
|
|
schema: Schema,
|
|
transform: (projected: Record<string, unknown>) => Record<string, unknown>,
|
|
): Schema => {
|
|
const standard = Reflect.get(schema as object, "~standard");
|
|
if (!isRecord(standard)) return schema;
|
|
const jsonSchema = Reflect.get(standard, "jsonSchema");
|
|
if (!isRecord(jsonSchema)) return schema;
|
|
const project = Reflect.get(jsonSchema, "input");
|
|
if (typeof project !== "function") return schema;
|
|
const byTarget = new Map<string, Record<string, unknown>>();
|
|
const transformedInput = (options: unknown): unknown => {
|
|
const target = Reflect.get(options as object, "target");
|
|
const cacheable =
|
|
typeof target === "string" &&
|
|
Reflect.get(options as object, "libraryOptions") === undefined;
|
|
if (cacheable) {
|
|
const cached = byTarget.get(target);
|
|
if (cached !== undefined) return cached;
|
|
}
|
|
const projected = (project as (options: unknown) => unknown)(options);
|
|
const transformed = isRecord(projected) ? transform(projected) : projected;
|
|
if (cacheable && isRecord(transformed)) byTarget.set(target, transformed);
|
|
return transformed;
|
|
};
|
|
return {
|
|
...(schema as object),
|
|
"~standard": {
|
|
...standard,
|
|
jsonSchema: { ...jsonSchema, input: transformedInput },
|
|
},
|
|
} as Schema;
|
|
};
|
|
|
|
type SchemaPosition = "schema" | "map" | "unknown";
|
|
|
|
const hasRelativeSchemaReference = (
|
|
schema: Record<string, unknown>,
|
|
): boolean => {
|
|
// Local pointers can make even annotation values executable schemas. Keep
|
|
// their resource root and traversal role separate from ordinary instance data.
|
|
const references = new JsonSchemaReferences(schema);
|
|
const pending: {
|
|
value: unknown;
|
|
scope: JsonSchemaScope | undefined;
|
|
position: SchemaPosition;
|
|
}[] = [{ value: schema, scope: references.rootScope, position: "schema" }];
|
|
const visited = new WeakMap<
|
|
JsonSchemaScope,
|
|
Record<SchemaPosition, WeakSet<object>>
|
|
>();
|
|
while (pending.length > 0) {
|
|
const entry = pending.pop()!;
|
|
const current = entry.value;
|
|
if (current === null || typeof current !== "object") continue;
|
|
const { position } = entry;
|
|
const scope =
|
|
position !== "map" && isRecord(current)
|
|
? references.enter(current, entry.scope)
|
|
: entry.scope;
|
|
if (scope === undefined) return true;
|
|
let positions = visited.get(scope);
|
|
if (positions === undefined) {
|
|
positions = {
|
|
schema: new WeakSet(),
|
|
map: new WeakSet(),
|
|
unknown: new WeakSet(),
|
|
};
|
|
visited.set(scope, positions);
|
|
}
|
|
if (positions[position].has(current)) continue;
|
|
positions[position].add(current);
|
|
if (Array.isArray(current)) {
|
|
for (const value of current) pending.push({ value, scope, position });
|
|
continue;
|
|
}
|
|
for (const [key, value] of Object.entries(current)) {
|
|
if (
|
|
position !== "map" &&
|
|
["$id", "$ref", "$dynamicRef", "$recursiveRef"].includes(key) &&
|
|
typeof value === "string"
|
|
) {
|
|
if (key === "$dynamicRef" || key === "$recursiveRef") return true;
|
|
if (!value.startsWith("#") && !URL.canParse(value)) return true;
|
|
if (key !== "$id" && value.startsWith("#")) {
|
|
const target = references.resolve(scope, value);
|
|
// Unknown anchors are already examined at their schema positions.
|
|
// Unresolvable pointers retain the existing reference base.
|
|
if (target.kind === "unresolved") return true;
|
|
if (target.kind === "target")
|
|
pending.push({ value: target.value, scope, position: "schema" });
|
|
}
|
|
}
|
|
if (position === "schema" && schemaLiteralValues.has(key)) continue;
|
|
const childPosition =
|
|
position === "map"
|
|
? "schema"
|
|
: position === "schema" && schemaMaps.has(key)
|
|
? "map"
|
|
: position === "schema" &&
|
|
(schemaArrays.has(key) || schemaChildren.has(key))
|
|
? "schema"
|
|
: "unknown";
|
|
if (value !== null && typeof value === "object")
|
|
pending.push({ value, scope, position: childPosition });
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
const withAdvertisedJsonSchema = <Schema extends z.ZodType>(
|
|
canonical: Schema,
|
|
io: "input" | "output",
|
|
advertise: (project: JsonSchemaProjection) => JsonSchemaProjection,
|
|
): Schema => {
|
|
// Zod's input projection drops root metadata when a descendant transforms.
|
|
// Preserve the parser and let the SDK own conversion of everything else.
|
|
const schema = canonical.meta(z.globalRegistry.get(canonical) ?? {});
|
|
const standard = schema["~standard"];
|
|
const project = standard.jsonSchema?.[io];
|
|
if (project === undefined)
|
|
throw new TypeError(
|
|
`Tool ${io} schema does not expose Standard JSON Schema`,
|
|
);
|
|
Object.defineProperty(schema, "~standard", {
|
|
value: {
|
|
...standard,
|
|
jsonSchema: {
|
|
...standard.jsonSchema,
|
|
[io]: memoizeByTarget(advertise(project)),
|
|
},
|
|
},
|
|
});
|
|
return schema;
|
|
};
|
|
|
|
// The SDK reconverts every registered schema on each `tools/list`, although a
|
|
// registered contract cannot change. Like the SDK's own memoized output
|
|
// projection, the advertised value is shared and treated as read-only.
|
|
const memoizeByTarget = (
|
|
project: JsonSchemaProjection,
|
|
): JsonSchemaProjection => {
|
|
const byTarget = new Map<string, Record<string, unknown>>();
|
|
return (options) => {
|
|
if (options.libraryOptions !== undefined) return project(options);
|
|
const cached = byTarget.get(options.target);
|
|
if (cached !== undefined) return cached;
|
|
const projected = project(options);
|
|
byTarget.set(options.target, projected);
|
|
return projected;
|
|
};
|
|
};
|
|
|
|
const fallbackPropertyDescription = (property: string): string | undefined => {
|
|
const explicit = Object.hasOwn(PROPERTY_DESCRIPTIONS, property)
|
|
? PROPERTY_DESCRIPTIONS[property]
|
|
: undefined;
|
|
if (explicit !== undefined) return explicit;
|
|
const words = property.replaceAll("_", " ");
|
|
if (property.startsWith("max_"))
|
|
return `Maximum permitted ${words.slice(4)} for this operation.`;
|
|
if (property.startsWith("include_"))
|
|
return `Whether to include ${words.slice(8)} in the result.`;
|
|
if (property.startsWith("expected_"))
|
|
return `Expected ${words.slice(9)} used to reject stale or mismatched input.`;
|
|
if (property.endsWith("_sha256"))
|
|
return `Exact SHA-256 digest of ${words.slice(0, -7)}.`;
|
|
if (property.endsWith("_evidence_id"))
|
|
return `Exact Evidence identifier for the ${words.slice(0, -12)} observation.`;
|
|
if (property.endsWith("_evidence_ids"))
|
|
return `Ordered Evidence identifiers for the ${words.slice(0, -13)} observations.`;
|
|
if (property.endsWith("_path"))
|
|
return `Local filesystem path for ${words.slice(0, -5)}.`;
|
|
if (property.endsWith("_uri"))
|
|
return `Canonical URI for ${words.slice(0, -4)}.`;
|
|
if (property.endsWith("_bytes"))
|
|
return `Byte count for ${words.slice(0, -6)}.`;
|
|
if (property.endsWith("_root") || property.endsWith("_roots"))
|
|
return `Local filesystem ${words} selected for this operation.`;
|
|
if (property.startsWith("is_") || property.startsWith("has_"))
|
|
return `Whether ${words}.`;
|
|
// A generic sentence would only restate the property name in every schema.
|
|
return undefined;
|
|
};
|