import type { PropsSpec, StreamSpec, ActionSpec, ContextSpec, JsonSchema, JsonObject, DataContract } from '../types/data-contract.js'; import { DomainError } from '../errors/domain-error.js'; import type { ActionEnvelope } from '../types/events.js'; import type { CompiledContractValidators } from '../integrations/mcp-apps.js'; import type { ValidateFunction } from './ajv-runtime.js'; export type { ValidateFunction } from './ajv-runtime.js'; import { type ReservedChannelValidator } from './reserved-channels.js'; export interface ContractViolation extends JsonObject { field: string; message: string; expected?: string; received?: string; /** * The JSON Schema keyword whose assertion failed (`'enum'`, * `'required'`, `'type'`, `'additionalProperties'`, …) — retained * verbatim from the underlying validator error. Absent on synthetic * violations that no schema keyword produced (e.g. the * props-without-propsSpec rejection). Consumers segment failure * classes on it (telemetry `violationKeywords`); it is additive * metadata, never part of the violation's identity. */ keyword?: string; } export interface ValidationResult { valid: boolean; violations: ContractViolation[]; } /** * Synthesize a {@link PropsSpec} into the single object-node JSON * Schema the runtime validates `props` against — `{type:'object', * properties:{…entry.schema…}, required:[…entry.required…]}`. * Closed-shape (`additionalProperties:false` at every depth) is NOT * injected here — the Ajv compile step ({@link compileForValidation} / * {@link compileValidatorModule}) does that, so this returns the raw * pre-injection wrapper. * * Shared by {@link validatePropsData} (server-side runtime check) and * {@link compileContractValidators} (render-time standalone emission) so * the precompiled in-iframe validator enforces byte-identical * semantics to the runtime validator — one synthesis, no drift. */ export declare function buildPropsWrapperSchema(spec: PropsSpec): JsonSchema; /** * Validate runtime props against an EXPLICIT enforced schema — the * persisted-schema render path of the schema-precise render contract * (docs/plans/2026-08-19-schema-precise-render.md P1). The paired * `ggui_handshake` persists the exact `buildEnforcedPropsSchema` * artifact it returned on the wire; `ggui_render` validates against * that PERSISTED schema rather than recomputing from the propsSpec, * so the returned and enforced schemas cannot diverge under * rolling-deploy version skew (the AUTHORITY obligation is structural, * not best-effort). Closed-shape injection at compile is idempotent — * a pre-injected enforced schema round-trips unchanged. */ export declare function validatePropsDataWithSchema(props: Record, schema: JsonSchema): ValidationResult; /** * Validate runtime props data against a PropsSpec contract. * * Synthesizes the propsSpec into a single JSON Schema object node * — `{type:'object', properties: {…spec.properties[name].schema…}, * required: [...names where entry.required], additionalProperties: * false}` — and validates `props` against it via the shared Ajv * runtime. The closed-shape injector recurses into every nested * object so the bidirectional contract (every declared key * validated, every data key declared) holds at any depth. * * Load-bearing for `ggui_update kind:'merge'` (RFC 7396): a patch * adding a key absent from `propsSpec.properties` would silently * land on the render without this gate. Same rule applies to * the `done`-vs-declared-`completed` class of bug inside array * items — Ajv rejects with the exact path (`todos[0].done`). */ export declare function validatePropsData(props: Record, spec: PropsSpec, precompiled?: ValidateFunction): ValidationResult; /** * Validate a stream delivery's payload against the channel's declared * schema on a {@link StreamSpec}. * * Signature takes the channel name + payload explicitly — matching * the {@link StreamEnvelope} wire shape (where channel is a first- * class envelope field, not a field nested inside the payload). * * Checks: * - `channelName` is declared in `spec` (a flat `Record` post-2026-04-22 flatten) — undeclared * channels reject with `'Unknown stream channel'` in the * violation message. * - `payload` conforms to `spec[channelName].schema` when * that schema declares a `type`. * * Reserved-channel handling (injection pattern): * * Known reserved channels (see {@link isKnownReservedChannel}) are * server-owned and bypass the streamSpec path entirely — agents * never declare them. Their payloads are validated through the * TWO-TIER validator lookup: * * 1. `extraReservedValidators` — optional, caller-provided. Primary * consumer: a hosting implementation composing the A2UI * validator for `_ggui:preview`. Consulted FIRST so callers can * override or extend built-ins. * 2. `BUILTIN_RESERVED_VALIDATORS` — protocol-owned, always active. * Ships the {@link validateGguiLifecyclePayload} for * `_ggui:lifecycle`. * 3. Fall-through: if no validator is registered for the known * reserved channel, return `{valid: true}`. Preserves backward * compatibility for any future reserved channel the runtime * adds before its validator is authored. * * Without this structure, a `_ggui:preview` emission into a render * whose active render carries ANY user streamSpec would * synthesize a false "Unknown channel" violation, blocking the * provisional preview runtime. Symmetric with the client-side * handling in `GguiRender`. * * Crucially narrow by design — the known-reserved path is a CLOSED * SET, not a prefix check. A typo inside the reserved namespace * (e.g. `_ggui:preveiw`) is NOT recognized, falls through to the * normal unknown-channel rejection, and surfaces the bug at its * emission site instead of turning into a silent no-op delivery. * * Does NOT validate channel semantics (mode / replay / complete) — * those are declarations, not shape constraints. See * `resolveStreamChannel` for semantics lookup. */ export declare function validateStreamData(channelName: string, payload: unknown, spec: StreamSpec, extraReservedValidators?: ReadonlyMap, precompiledChannels?: ReadonlyMap): ValidationResult; /** * Validate a contextSpec slot value against the spec's declared * schema. Symmetric with {@link validateStreamData} / * {@link validateActionData}: the iframe-runtime observer uses this * to gate Provider values BEFORE posting `ui/update-model-context` * envelopes (per the contextSpec design-lock — Q4 schema check). * * Checks: * - `slotName` is declared in `spec` — undeclared slots reject with * `'Unknown context slot'`. * - `value` conforms to `spec[slotName].schema` when that schema * declares a `type`. * * Mirrors `validateActionData`'s posture: the runtime that calls this * decides whether to surface the failure (dev-only `console.warn`, * drop silently in production) — the validator is a pure shape gate. */ export declare function validateContextData(slotName: string, value: unknown, spec: ContextSpec, precompiledSlots?: ReadonlyMap): ValidationResult; /** * Validate an inbound user-action payload against the render's ActionSpec. * * Symmetric with {@link validatePropsData} / {@link validateStreamData}, but for * live-channel INBOUND user → core traffic. Enforces the action contract at the * wire boundary BEFORE the event is buffered or forwarded to an agent. * * Input shape mirrors `ActionEventValue` from `events.ts`: * `{ action: string, data?: JsonValue, tool?: string }` * * Checks: * - `action` is a non-empty string * - `action` is declared in `spec` (a flat `Record` post-2026-04-22 flatten) * - If the declared action has a `schema`, `data` matches it * * Actions without a declared schema are void-payload (fire-and-forget) — a * present-but-unexpected `data` is tolerated to stay forward-compatible with * clients that attach UI metadata the contract doesn't model. Contracts that * want strict emptiness should declare `schema: { type: 'null' }`. */ export declare function validateActionData(value: unknown, spec: ActionSpec, precompiledActions?: ReadonlyMap): ValidationResult; /** * Validate an inbound {@link ActionEnvelope} against the target * render's {@link ActionSpec}. Payload-contract layer of live-channel * inbound enforcement — the ONLY gate on inbound actions today (the * pre-Phase-B `subscription.events` allowlist gate was deleted with * the session-stack collapse). * * Semantics: * - `envelope.type !== 'data:submit'` → `{valid: true, violations: []}`. * {@link EventType} has exactly one member, so a TYPED caller never * hits this branch — it is a wire-trust guard: a rogue client's * envelope claiming an unknown type string is ledger-only upstream * and gets no payload enforcement on this layer. * - `spec === undefined` → `{valid: true, violations: []}`. Renders * without an actionSpec have no contract; legacy renders keep * flowing. * - Otherwise `envelope.payload` is validated against `spec` via * {@link validateActionData}. Same rules, same output shape. * * This helper does NOT enforce allowlist, render binding, or render * routing — those are ingress-plumbing concerns. Pure payload-shape * check; returns `ValidationResult` rather than throwing so callers * can decide whether to surface as a wire error, log, etc. */ export declare function validateActionEnvelope(envelope: ActionEnvelope, spec: ActionSpec | undefined, precompiledActions?: ReadonlyMap): ValidationResult; /** * Compile a contract's runtime-validated sub-schemas into standalone, * eval-free ESM validator modules — the producer half of the * precompiled-validator channel * ({@link CompiledContractValidators} on `McpAppAiGguiMeta`). * * The renderer iframe runs under a strict CSP with no `'unsafe-eval'`, * so it cannot call `ajv.compile()` (which builds validators via * `new Function`). Compilation therefore happens server-side at render * time — where the contract schema is fixed and codegen is legal — and * the iframe loads each emitted module via a `blob:` dynamic import. * * One module per runtime-validated surface, matching the four runtime * validators in this file exactly (no second contract model): * * - `props` — the synthesized object wrapper from * {@link buildPropsWrapperSchema}, as {@link validatePropsData} * validates `props`. * - `actions` — per-action `entry.schema`, as {@link validateActionData} * validates `data`. Void actions (no `schema`) contribute no entry. * - `streams` — per-channel `entry.schema`, as {@link validateStreamData} * validates `payload`. * - `context` — per-slot `entry.schema`, as {@link validateContextData} * validates `value`. * * Returns `undefined` when the contract declares no runtime-validated * schema at all — the slice-meta projection then omits the field. */ export declare function compileContractValidators(specs: { readonly propsSpec?: PropsSpec; readonly actionSpec?: ActionSpec; readonly streamSpec?: StreamSpec; readonly contextSpec?: ContextSpec; }): CompiledContractValidators | undefined; /** * A contract's runtime-validator compilation in **expression form** — * same grouping as {@link CompiledContractValidators}, but each string * is a JS EXPRESSION evaluating to the validate function (see * `compileValidatorFunctionExpr`), not an ESM module source. The * expression form exists so the executable bundle below can be one * plain module; the ESM-module form remains the inline * `_meta.compiledValidators` channel's shape. * * @public */ export interface ContractValidatorExprs { readonly props?: string; readonly actions?: Readonly>; readonly streams?: Readonly>; readonly context?: Readonly>; } /** * Compile a contract's runtime-validated sub-schemas into * expression-form validators ({@link ContractValidatorExprs}) — the * same four surfaces, guards, and closed-shape semantics as * {@link compileContractValidators}, differing only in emission form. * Returns `undefined` when the contract declares no runtime-validated * schema at all. */ export declare function compileContractValidatorExprs(specs: { readonly propsSpec?: PropsSpec; readonly actionSpec?: ActionSpec; readonly streamSpec?: StreamSpec; readonly contextSpec?: ContextSpec; }): ContractValidatorExprs | undefined; /** * Wrap {@link ContractValidatorExprs} as the source text of ONE plain * ES module whose `default` export carries the validate FUNCTIONS * themselves — the v2 wire format of the content-addressable contract * route (`GET /contract/.js`), ggui#522 slice 2. * * The v1 format exported the validator-module SOURCES as strings, so * the iframe still needed one `blob:` dynamic import per validator — * exactly the scheme-source grant a strict host CSP refuses, which * made client-side validation silently fail open on such hosts. Here * the expressions concatenate at BUILD time into ordinary code: the * frame does one `import(validatorsUrl)` (a plain https module load, * governed by `script-src` origins alone) and receives functions. No * `blob:`, no `data:`, no eval anywhere. * * Key emission order follows the producer's iteration order, which is * deterministic in V8/Node for string keys — {@link computeContractBundle} * hashes the INPUT specs anyway, so byte-level determinism of this * source is a courtesy, not a correctness requirement. * * @public */ export declare function bundleValidatorExprsAsExecutableModule(exprs: ContractValidatorExprs): string; /** * Convenience over {@link compileContractValidatorExprs} + * {@link bundleValidatorExprsAsExecutableModule} + sha256 — produces * the `{contractHash, bundleSource, validators}` triple the emitter * (render.ts / the resource read / the /state route) writes to the * content-addressable store and emits as * `{contractHash, validatorsUrl}` on the render slice. * * `contractHash` is `sha256(salt + canonicalJsonStringify(specs))` * (hex). Hashing the INPUT specs — not the compiled output — * guarantees a stable hash across server processes and Ajv version * bumps: the same contract definition always lands at the same URL. * Compiled output bytes may differ across calls (Ajv's standalone * emitter uses incrementing counter names like `validate10`/ * `validate11`), but the CodeStore is idempotent (first write wins) * and the URL response carries `Cache-Control: immutable`, so browsers * + CDNs lock in the first-served bytes and never observe a * counter-name reshuffle. The salt exists precisely because of that * immutability — see {@link CONTRACT_BUNDLE_HASH_SALT}. * * Returns `undefined` when the contract declares no runtime-validated * schema at all (matches {@link compileContractValidators}'s posture). * * @public */ export declare function computeContractBundle(specs: { readonly propsSpec?: PropsSpec; readonly actionSpec?: ActionSpec; readonly streamSpec?: StreamSpec; readonly contextSpec?: ContextSpec; }): Promise<{ readonly contractHash: string; readonly bundleSource: string; readonly validators: ContractValidatorExprs; } | undefined>; /** * Format violations into a human-readable error message for the target agent. */ export declare function formatViolations(violations: ContractViolation[]): string; /** * Validate the contract structure itself — catches malformed contract * before they're persisted and used to validate runtime data. * * Checks: * - PropsSpec properties have valid schemas (type or oneOf/anyOf defined) * - Array schemas have items defined (otherwise element validation is impossible) * - Object schemas with required fields reference existing properties * - StreamSpec channels have schemas defined (reserved-prefix names rejected) * - ActionSpec actions have schemas defined * - ContextSpec slots: identifier-shape keys, no reserved keys * (`__proto__`/`constructor`/`prototype`), schema present, default * satisfies schema, debounceMs is a non-negative integer, no key * collision with propsSpec, and `deriveContextDefault` yields a * non-undefined initial value (see `validateContextStructure`). * - Cross-reference invariants (`actionSpec.nextStep`, * `streamSpec.source.tool` resolve to `agentCapabilities.tools[*]`). * - Name invariants (no collision across actionSpec / streamSpec / * contextSpec; no `_ggui:` reserved-prefix keys). * - Schema-compat invariants (`actionSpec[*].schema` ⊆ * `tool.inputSchema`; `streamSpec[*].schema` ⊇ `tool.outputSchema`). */ export declare function validateContractStructure(contract: DataContract): ValidationResult; export declare class ContractViolationError extends DomainError<'contract_violation'> { readonly violations: ContractViolation[]; readonly tool: 'ggui_render' | 'ggui_update' | 'ggui_amend' | 'ggui_emit' | 'ggui_event'; readonly hint: string; /** * sha256 (lowercase hex) of the RFC 8785 canonical bytes of the * ENFORCED props schema this violation was validated against — * present when the enforcing site holds one (the handshake-persisted * schema on the render path). The breach classifier of the * schema-precise render contract: a caller holding the handshake's * `propsSchemaHash` compares it to this value — mismatch means the * server enforced a different schema than it returned (server * breach); match means the props themselves were at fault. Absent on * violations produced without an enforced-schema identity (synthetic * violations, stream/context/action validation). */ readonly propsSchemaHash?: string; constructor(opts: { tool: 'ggui_render' | 'ggui_update' | 'ggui_amend' | 'ggui_emit' | 'ggui_event'; violations: ContractViolation[]; hint?: string; propsSchemaHash?: string; }); /** * Structured recovery payload carrying the same slug (`error: * 'contract_violation'`). It rides the live channel's error frame; on * `tools/call` it does not travel — the slug leads the result text there * (SPEC §7.9 Plane 2, ggui#880) and the detail carries the same facts. */ toErrorData(): { error: 'contract_violation'; tool: string; violations: ContractViolation[]; hint: string; propsSchemaHash?: string; }; } //# sourceMappingURL=contract-validator.d.ts.map