/** * ChassisEnvelope — the shared envelope chassis for all Class A tools. * * WHY THIS EXISTS * * There are two tool classes: * * Class A (older, chaotic): * top_patterns, pattern_examples, pattern_mitigate, cost_options, * estimate_savings, preview_filter, pattern_detail, pattern_trend * — inconsistent arg names, no threshold disclosure, no source * disclosure, empty actions[], human_summary missing or tautological. * * Class B (newer, cross-pillar): * metrics_that_moved, investigate, rank_by_shape_similarity, * metric_overlay * — threshold_basis explicit, threshold_audit nested, candidates * split usable/evaluated/failed, human_summary honest with * next-step, investigation_id for traceability. * * The catalog has ~30 tools. Every Class A tool that adopts this chassis * gets Class B consistency without a per-tool refactor of the logic: * the builder enforces the shape at construction time, and the Zod * schema rejects drift at the boundary. * * RELATIONSHIP TO EXISTING ENVELOPE * * ChassisEnvelope does NOT replace StructuredOutput from output-types.ts. * The outer transport envelope (schema_version / schema_epoch / tool / * generated_at / view / summary / data / actions / render_hint / * truncated / next_cursor / warnings / images) stays unchanged. * * What changes is what goes INSIDE `data`. Class A tools historically * put an ad-hoc object there; a chassis tool puts a ChassisData object. * `buildChassisEnvelope()` produces a complete StructuredOutput where * `data` is a validated ChassisData. * * BACK-COMPAT * * Old call sites that read flat fields from `data` (status, query_count, * total_latency_ms, human_summary) still work because ChassisData puts * them at the top of its flat surface via `toLegacyDataShape()`. Tools * can migrate incrementally: pass `legacyCompat: true` and * `legacyExtraFields: { ...existingDataFields }` to add those fields * alongside the structured chassis fields during the transition. * * USAGE * * import { buildChassisEnvelope, newChasisTelemetry } from '../lib/chassis-envelope.js'; * * const telemetry = newChassisTelemetry(); * // ... do work, call telemetry.recordQuery() after each backend call ... * * return buildChassisEnvelope({ * tool: 'log10x_top_patterns', * view: 'summary', * headline: '12 patterns above floor, top 3 are auth-service ERROR.', * status: 'success', * decisions: { * threshold_used: floorBytesPerSec, * threshold_basis: 'default', * }, * source_disclosure: { * bytes_source: 'tsdb', * rate_source: rateSourceFromEnv, * pattern_count_source: { * kind: 'top_n_above_threshold', * count: shownPatterns.length, * denominator_meaning: 'Top N patterns above the 1 KB/s floor in window', * }, * }, * scope: { window: timeRange, window_basis: 'explicit' }, * payload: { patterns: shownPatterns, totals, incidents }, * human_summary: '...', * actions: [...], * telemetry, * }); */ import { z } from 'zod'; import { type StructuredOutput, type View, type Action, type RenderHint, type InlineImage } from './output-types.js'; import type { Mode } from './mode-detect.js'; /** * Chassis schema version. Bumped when the ChassisData shape changes * in a way that is NOT back-compatible with older readers. Distinct * from SCHEMA_VERSION (the outer transport envelope version). */ export declare const CHASSIS_VERSION: "1.0"; /** * Top-level call status. Every tool must set this so agents can branch * on it before reading anything else. * * - success — results are available and usable; read payload. * - no_signal — search ran, nothing crossed the threshold; stop, * do not auto-retry with the same params. * - partial — some sub-queries failed; results are available * but partial. Read scope.candidates_failed[] for * what was skipped. May be worth a narrower retry. * - insufficient_data — anchor resolved but window / backend coverage * too thin to produce a usable result. Widen or * re-anchor. * - error — structural failure; read data.error. * - demo_read_only — the MCP is running in the hosted demo playground * with LOG10X_MCP_READ_ONLY=true; the writer tool * refused to execute. data.error carries the * `demo_read_only` discriminant and the would_have * side-effect description. */ export declare const ChassisStatusSchema: z.ZodEnum<{ error: "error"; success: "success"; no_signal: "no_signal"; demo_read_only: "demo_read_only"; partial: "partial"; insufficient_data: "insufficient_data"; }>; export type ChassisStatus = z.infer; /** * Where the threshold number came from. Agents use this to decide * whether to act on the result: * * customer_supplied — caller passed it explicitly; treat as trusted. * snapshot — read from a stored config/calibration file. * default — a hand-picked spec constant; use with caution. * unvalidated_default — same as default but the tool explicitly flags * that no empirical calibration has been done on * this deployment's data. Agents MUST NOT * auto-mitigate when this is set. */ export declare const ThresholdBasisSchema: z.ZodEnum<{ default: "default"; customer_supplied: "customer_supplied"; snapshot: "snapshot"; unvalidated_default: "unvalidated_default"; }>; export type ThresholdBasis = z.infer; /** * Observed distribution summary for the pool of values the threshold * was compared against. The agent compares threshold_used against * this distribution to judge whether the threshold is well above noise, * at the noise floor, or below it (false positives). */ export declare const ThresholdAuditDistributionSchema: z.ZodNullable>; export declare const ThresholdAuditSchema: z.ZodNullable; basis: z.ZodString; observed_distribution: z.ZodOptional>>; n_candidate_slots: z.ZodOptional; }, z.core.$strip>>; export type ThresholdAudit = z.infer; /** * All numeric thresholds the tool applied, plus their provenance. * Mandatory on every tool; if a tool has no threshold semantics, set * threshold_used: null and threshold_basis: 'default'. */ export declare const DecisionsSchema: z.ZodObject<{ threshold_used: z.ZodNullable; threshold_basis: z.ZodEnum<{ default: "default"; customer_supplied: "customer_supplied"; snapshot: "snapshot"; unvalidated_default: "unvalidated_default"; }>; threshold_audit: z.ZodOptional; basis: z.ZodString; observed_distribution: z.ZodOptional>>; n_candidate_slots: z.ZodOptional; }, z.core.$strip>>>; }, z.core.$strip>; export type Decisions = z.infer; /** * Where bytes figures come from. Every tool that surfaces GB/s, bytes, * or cost numbers must label the origin so an agent or human reader * knows what they are comparing. */ export declare const BytesSourceSchema: z.ZodEnum<{ tsdb: "tsdb"; customer_supplied_csv: "customer_supplied_csv"; engine_aggregated_csv: "engine_aggregated_csv"; siem_direct: "siem_direct"; estimate: "estimate"; }>; export type BytesSource = z.infer; /** * Where the $/GB rate came from. Absent when the tool surfaces no * dollar values. */ export declare const RateSourceSchema: z.ZodEnum<{ none: "none"; list_price: "list_price"; customer_supplied: "customer_supplied"; snapshot: "snapshot"; }>; export type RateSource = z.infer; /** * Pattern count semantics. Populating this field prevents the most * common Class A ambiguity: "10 patterns" — 10 of how many? Above what? */ export declare const PatternCountSourceSchema: z.ZodObject<{ kind: z.ZodEnum<{ top_n_above_threshold: "top_n_above_threshold"; scoped_total_above_threshold: "scoped_total_above_threshold"; env_total: "env_total"; scoped_total: "scoped_total"; above_volume_floor: "above_volume_floor"; raw_label_universe: "raw_label_universe"; }>; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>; export type PatternCountSource = z.infer; /** * Source labels for every number class the tool surfaces. All fields * are optional because not every tool surfaces every number type. When * a tool surfaces a number and omits the source field, the Zod * validator will NOT reject the envelope — but the principle is that * any number that could be ambiguous should carry a label. */ export declare const SourceDisclosureSchema: z.ZodObject<{ bytes_source: z.ZodOptional>; rate_source: z.ZodOptional>; pattern_count_source: z.ZodOptional; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>>; siem_vendor: z.ZodOptional; source_label: z.ZodOptional; retriever_state_source: z.ZodOptional>; service_count_source: z.ZodOptional; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>>; label_source: z.ZodOptional>; siem_actual: z.ZodOptional; siem_lens: z.ZodOptional; siem_lens_basis: z.ZodOptional>; volume_actual_gb: z.ZodOptional>; volume_projected_gb: z.ZodOptional>; volume_scale_factor: z.ZodOptional; volume_projection_note: z.ZodOptional>; }, z.core.$strip>; export type SourceDisclosure = z.infer; /** * What universe the tool queried. Mirrors the Class B n_candidates_* * split to let agents reason about result completeness. */ export declare const ScopeSchema: z.ZodObject<{ window: z.ZodString; window_basis: z.ZodEnum<{ explicit: "explicit"; auto_default: "auto_default"; }>; candidates_count: z.ZodOptional; candidates_usable: z.ZodOptional; candidates_evaluated: z.ZodOptional; candidates_failed: z.ZodOptional>; }, z.core.$strip>; export type Scope = z.infer; /** * Structured question the agent MUST surface to the user before * routing to any follow-up tool. This moves compliance directives OUT * of prose markdown (where they are routinely ignored) and into a * typed field agents can read and act on mechanically. */ export declare const MustAskUserSchema: z.ZodOptional; }, z.core.$strip>>; export type MustAskUser = z.infer; /** * Per-call telemetry surfaced on every envelope. Enables agents to * pace themselves and flag slow backends. */ export declare const PerformanceSchema: z.ZodObject<{ query_count: z.ZodNumber; total_latency_ms: z.ZodNumber; backend_pressure_hint: z.ZodNullable>; }, z.core.$strip>; export type Performance = z.infer; /** * The full ChassisData shape. Every Class A tool puts one of these * inside the `data` field of its StructuredOutput envelope. * * `payload` is generic — the tool-specific result rows go there. * The chassis validates everything around it. */ export declare const ChassisDataSchema: z.ZodObject<{ status: z.ZodEnum<{ error: "error"; success: "success"; no_signal: "no_signal"; demo_read_only: "demo_read_only"; partial: "partial"; insufficient_data: "insufficient_data"; }>; decisions: z.ZodObject<{ threshold_used: z.ZodNullable; threshold_basis: z.ZodEnum<{ default: "default"; customer_supplied: "customer_supplied"; snapshot: "snapshot"; unvalidated_default: "unvalidated_default"; }>; threshold_audit: z.ZodOptional; basis: z.ZodString; observed_distribution: z.ZodOptional>>; n_candidate_slots: z.ZodOptional; }, z.core.$strip>>>; }, z.core.$strip>; source_disclosure: z.ZodObject<{ bytes_source: z.ZodOptional>; rate_source: z.ZodOptional>; pattern_count_source: z.ZodOptional; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>>; siem_vendor: z.ZodOptional; source_label: z.ZodOptional; retriever_state_source: z.ZodOptional>; service_count_source: z.ZodOptional; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>>; label_source: z.ZodOptional>; siem_actual: z.ZodOptional; siem_lens: z.ZodOptional; siem_lens_basis: z.ZodOptional>; volume_actual_gb: z.ZodOptional>; volume_projected_gb: z.ZodOptional>; volume_scale_factor: z.ZodOptional; volume_projection_note: z.ZodOptional>; }, z.core.$strip>; scope: z.ZodObject<{ window: z.ZodString; window_basis: z.ZodEnum<{ explicit: "explicit"; auto_default: "auto_default"; }>; candidates_count: z.ZodOptional; candidates_usable: z.ZodOptional; candidates_evaluated: z.ZodOptional; candidates_failed: z.ZodOptional>; }, z.core.$strip>; payload: z.ZodOptional; human_summary: z.ZodString; must_render_verbatim: z.ZodOptional; markdown: z.ZodOptional; must_ask_user: z.ZodOptional; }, z.core.$strip>>; forbidden_next_actions: z.ZodOptional>; error: z.ZodOptional; retryable: z.ZodBoolean; suggested_backoff_ms: z.ZodNullable; hint: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>; export type ChassisData = z.infer; /** * The complete chassis envelope. Outer wrapper is StructuredOutput * (transport layer, unchanged); `data` is a validated ChassisData. * `invocation_id` and `performance` live at the top level alongside * the existing envelope fields. * * This is a structural interface rather than a Zod schema because * StructuredOutput already has a Zod schema, and the chassis extension * extension fields on top. */ export interface ChassisEnvelope extends StructuredOutput { /** * UUID for chain traceability. Pass this as `prior_invocation_id` * on follow-up calls so a harness can reconstruct the call graph * without parsing prose. */ invocation_id: string; /** * Performance telemetry lifted to the top level for observability * harnesses that scan the outer envelope without deserializing `data`. */ performance: Performance; /** * Top-level call status — mirror of `data.status`. Lifted to the * envelope so agents and harnesses can branch on call outcome without * descending into `data`. `data.status` is kept in place for * back-compat with existing readers (toLegacyShape still works). */ status: ChassisStatus; /** Typed override — data is always a ChassisData on these envelopes. */ data: ChassisData; } /** * Mutable telemetry accumulator. Create one at the start of a handler, * call `recordQuery()` after each backend call, then pass it to * `buildChassisEnvelope()`. */ export interface ChassisTelemetry { readonly startedAt: number; queryCount: number; throttledHit: boolean; } export declare function newChassisTelemetry(): ChassisTelemetry; /** * Convenience mutator. Call after each backend query completes. * Returns `telemetry` so callers can chain if desired. */ export declare function recordQuery(telemetry: ChassisTelemetry, throttled?: boolean): ChassisTelemetry; /** * Compute the backend pressure hint from accumulated telemetry. * Returns null when no queries were made (paste-mode / local-only tools). */ export declare function computePressureHint(t: ChassisTelemetry, nowMs?: number): Performance['backend_pressure_hint']; /** * Input to `buildChassisEnvelope()`. All fields are required except * where marked optional. The builder fills in: * - schema_version, schema_epoch (from output-types.ts constants) * - invocation_id (crypto.randomUUID) * - generated_at (new Date().toISOString()) * - performance (derived from telemetry) * - truncated default false * - warnings default [] */ export interface ChassisEnvelopeInput { tool: string; view: View; /** `summary.headline` — 1-3 sentence line the agent quotes cold. */ headline: string; /** Optional bullets for the summary block. */ headline_bullets?: string[]; /** Optional callout for the summary block. */ headline_callout?: string; status: ChassisStatus; decisions: Decisions; source_disclosure: SourceDisclosure; scope: Scope; /** * The tool-specific result rows. Anything goes here; the chassis * does not validate the payload shape. */ payload: unknown; human_summary: string; must_render_verbatim?: string; must_ask_user?: MustAskUser; forbidden_next_actions?: string[]; /** * Structured error. Required when status === 'error'; should be * omitted otherwise. The builder enforces this at runtime and emits * a warning if the contract is violated. */ error?: ChassisData['error']; /** * Pass the tool's ChassisTelemetry accumulator. The builder derives * query_count, total_latency_ms, and backend_pressure_hint from it. * * When omitted (paste-mode / local tools), performance is set to * { query_count: 0, total_latency_ms: 0, backend_pressure_hint: null }. */ telemetry?: ChassisTelemetry; actions?: Action[]; render_hint?: RenderHint; truncated?: boolean; next_cursor?: string; warnings?: string[]; images?: InlineImage[]; /** * Current boot mode. When provided, `buildChassisEnvelope()` filters * `actions[]` to remove entries for tools not registered in this mode, * and appends a warning for each dropped entry. * * Call sites that already have the boot mode available (e.g. those * that call `getBootMode()`) should pass it here. When omitted (null / * undefined), actions[] is passed through unfiltered — a safe default * for tools that run before mode detection completes. */ mode?: Mode | null; /** * Back-compat mode. When true, `buildChassisEnvelope()` also spreads * `legacyExtraFields` into the `data` object alongside the chassis * fields. Allows a tool to return the new chassis shape while old * callers that read flat fields from `data` continue to work. * * Remove once all call sites have migrated to reading ChassisData. */ legacyCompat?: boolean; /** * Extra fields to spread into `data` when `legacyCompat: true`. * Should carry the same flat fields the old tool returned * (status, query_count, total_latency_ms, human_summary, etc.). */ legacyExtraFields?: Record; } /** * Build a complete ChassisEnvelope from a tool handler's inputs. * * The returned object is a valid StructuredOutput (passes * `isStructuredOutput()` from output-types.ts) AND carries the * chassis extension fields (`invocation_id`, `performance`). * * Validation: ChassisDataSchema.parse() runs on the assembled `data` * block. If it throws, the error message contains the Zod path and the * reason — this is intentional; it surfaces schema drift at the * boundary during development rather than silently emitting malformed * output. * * In production: wrap the call site in try/catch and emit a * buildChassisErrorEnvelope() when a schema violation occurs so the * agent still gets a structured, branchable error envelope rather than * an MCP protocol error. */ export declare function buildChassisEnvelope(input: ChassisEnvelopeInput): ChassisEnvelope; /** * Strip leading markdown H1/H2/... header lines from an error hint * before it is inserted into the structured `summary.headline` field. * * Some error paths (e.g. configure-engine.ts's renderError()) format * their output with a markdown heading as the first line, like * `# configure_engine — gitops repo not resolved`. When that string * flows verbatim into `buildChassisErrorEnvelope`, the H1 `# ` syntax * bleeds into the headline. This helper skips any leading `#+`-prefixed * lines and returns the first line of prose content. * * The original `errHint` (with the heading) is still passed to * `human_summary` and `payload.remediation` so markdown renderers see * it intact — only the `headline` field is sanitized. */ export declare function sanitizeHeadline(msg: string): string; /** * Convenience builder for structural error envelopes. Use when the * tool cannot produce a payload because a backend call failed. * * The envelope still carries full traceability (invocation_id, * performance from the accumulated telemetry up to the failure point). */ export declare function buildChassisErrorEnvelope(opts: { tool: string; /** The PrimitiveError or equivalent structured error shape. */ err: ChassisData['error']; telemetry?: ChassisTelemetry; /** Scope that was known before the failure, if any. */ scope?: Partial; /** Extra context already available (e.g. the input echo). */ contextPayload?: Record; warnings?: string[]; /** * Partial source disclosure for fields known before the failure. * Callers that resolved vendor selection or know the bytes source * (e.g. 'tsdb') should pass what they have so the error envelope * carries traceable provenance. Defaults to {} for back-compat. */ source_disclosure?: Partial; /** * Structured chain-next nudges. Error envelopes should populate this * when an obvious remediation tool exists (e.g. config_missing → * configure_env, backend_unavailable → doctor). Agents pick these up * without parsing human_summary text. */ actions?: Action[]; }): ChassisEnvelope; /** * Build a `demo_read_only` envelope for a writer tool that was blocked * by `requireWriteAccess()` while the MCP is running with * `LOG10X_MCP_READ_ONLY=true`. * * The catalog stays "visible-but-locked": tool descriptions + schemas * are still served, but calling a writer tool returns this envelope * instead of executing. The shape is: * * status: 'error' * data.status: 'demo_read_only' * data.error.error_type: 'demo_read_only' * data.error.retryable: false * data.error.suggested_backoff_ms: null * data.error.hint: full remediation string from DemoReadOnlyError * summary.headline: 'Demo read-only mode: ' * * `data.status` is one of the ChassisStatus enum values; this path uses * 'error' because the call did not produce a payload. The `demo_read_only` * discriminant lives on `data.error.error_type`, which is what agents * branch on. Headline + hint surface the per-tool would_have phrase so * a human reader knows exactly what side effect was blocked. */ export declare function buildDemoReadOnlyEnvelope(opts: { tool: string; /** The `would_have` phrase from the DemoReadOnlyError. */ would_have: string; /** The full agent-facing hint from the DemoReadOnlyError. */ hint: string; telemetry?: ChassisTelemetry; /** Optional context echo of the call args. */ contextPayload?: Record; warnings?: string[]; }): ChassisEnvelope; /** * toLegacyShape — emit the pre-chassis flat data shape from a * ChassisEnvelope. Use this during the transition period when a call * site reads from the old flat fields (status, query_count, * total_latency_ms, human_summary) and has not yet been updated to * read ChassisData. * * Returns a plain Record that matches what the old * tool would have put in StructuredOutput.data. It is NOT a validated * shape — it is a shim for old readers. */ export declare function toLegacyShape(envelope: ChassisEnvelope): Record; /** * isChassisEnvelope — type guard that distinguishes a ChassisEnvelope * from a plain StructuredOutput. Use in `wrap()` or harnesses that * handle both old and new tool shapes. */ export declare function isChassisEnvelope(x: unknown): x is ChassisEnvelope; /** * Zod schema for the full ChassisEnvelope shape, for use in test * assertions and harness-level validation. This schema validates * only the chassis-specific extension fields; outer StructuredOutput * fields are validated separately by StructuredOutputSchema. */ export declare const ChassisEnvelopeExtensionSchema: z.ZodObject<{ invocation_id: z.ZodString; performance: z.ZodObject<{ query_count: z.ZodNumber; total_latency_ms: z.ZodNumber; backend_pressure_hint: z.ZodNullable>; }, z.core.$strip>; status: z.ZodEnum<{ error: "error"; success: "success"; no_signal: "no_signal"; demo_read_only: "demo_read_only"; partial: "partial"; insufficient_data: "insufficient_data"; }>; data: z.ZodObject<{ status: z.ZodEnum<{ error: "error"; success: "success"; no_signal: "no_signal"; demo_read_only: "demo_read_only"; partial: "partial"; insufficient_data: "insufficient_data"; }>; decisions: z.ZodObject<{ threshold_used: z.ZodNullable; threshold_basis: z.ZodEnum<{ default: "default"; customer_supplied: "customer_supplied"; snapshot: "snapshot"; unvalidated_default: "unvalidated_default"; }>; threshold_audit: z.ZodOptional; basis: z.ZodString; observed_distribution: z.ZodOptional>>; n_candidate_slots: z.ZodOptional; }, z.core.$strip>>>; }, z.core.$strip>; source_disclosure: z.ZodObject<{ bytes_source: z.ZodOptional>; rate_source: z.ZodOptional>; pattern_count_source: z.ZodOptional; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>>; siem_vendor: z.ZodOptional; source_label: z.ZodOptional; retriever_state_source: z.ZodOptional>; service_count_source: z.ZodOptional; count: z.ZodNumber; denominator_meaning: z.ZodString; }, z.core.$strip>>; label_source: z.ZodOptional>; siem_actual: z.ZodOptional; siem_lens: z.ZodOptional; siem_lens_basis: z.ZodOptional>; volume_actual_gb: z.ZodOptional>; volume_projected_gb: z.ZodOptional>; volume_scale_factor: z.ZodOptional; volume_projection_note: z.ZodOptional>; }, z.core.$strip>; scope: z.ZodObject<{ window: z.ZodString; window_basis: z.ZodEnum<{ explicit: "explicit"; auto_default: "auto_default"; }>; candidates_count: z.ZodOptional; candidates_usable: z.ZodOptional; candidates_evaluated: z.ZodOptional; candidates_failed: z.ZodOptional>; }, z.core.$strip>; payload: z.ZodOptional; human_summary: z.ZodString; must_render_verbatim: z.ZodOptional; markdown: z.ZodOptional; must_ask_user: z.ZodOptional; }, z.core.$strip>>; forbidden_next_actions: z.ZodOptional>; error: z.ZodOptional; retryable: z.ZodBoolean; suggested_backoff_ms: z.ZodNullable; hint: z.ZodString; }, z.core.$strip>>; }, z.core.$strip>; }, z.core.$strip>; export type ChassisEnvelopeExtension = z.infer;