fix(typebox): migrate to v1 with extension compat (#3474)
* fix(typebox): migrate to v1 with extension compat Replace AJV-based validation with TypeBox-native validation, keep legacy extension imports working (including @sinclair/typebox/compiler), and restore coercion for serialized/plain JSON schemas. This change closes #3112. * fix(typebox): use canonical imports and harden coercion Switch first-party code to canonical typebox imports while retaining legacy extension aliases in the loader. Remove obsolete runtime codegen guards, expand serialized JSON-schema coercion coverage, and update related tests and fixtures. Fixes #3112. --------- Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
@@ -18,7 +18,9 @@ import * as _bundledPiTui from "@mariozechner/pi-tui";
|
||||
// Static imports of packages that extensions may use.
|
||||
// These MUST be static so Bun bundles them into the compiled binary.
|
||||
// The virtualModules option then makes them available to extensions.
|
||||
import * as _bundledTypebox from "@sinclair/typebox";
|
||||
import * as _bundledTypebox from "typebox";
|
||||
import * as _bundledTypeboxCompile from "typebox/compile";
|
||||
import * as _bundledTypeboxValue from "typebox/value";
|
||||
import { getAgentDir, isBunBinary } from "../../config.js";
|
||||
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
|
||||
// avoiding a circular dependency. Extensions can import from @mariozechner/pi-coding-agent.
|
||||
@@ -27,6 +29,7 @@ import { createEventBus, type EventBus } from "../event-bus.js";
|
||||
import type { ExecOptions } from "../exec.js";
|
||||
import { execCommand } from "../exec.js";
|
||||
import { createSyntheticSourceInfo } from "../source-info.js";
|
||||
import * as _bundledTypeboxCompilerCompat from "./typebox-compiler-compat.js";
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionAPI,
|
||||
@@ -41,7 +44,14 @@ import type {
|
||||
|
||||
/** Modules available to extensions via virtualModules (for compiled Bun binary) */
|
||||
const VIRTUAL_MODULES: Record<string, unknown> = {
|
||||
typebox: _bundledTypebox,
|
||||
"typebox/compile": _bundledTypeboxCompile,
|
||||
"typebox/value": _bundledTypeboxValue,
|
||||
"typebox/compiler": _bundledTypeboxCompilerCompat,
|
||||
"@sinclair/typebox": _bundledTypebox,
|
||||
"@sinclair/typebox/compile": _bundledTypeboxCompile,
|
||||
"@sinclair/typebox/value": _bundledTypeboxValue,
|
||||
"@sinclair/typebox/compiler": _bundledTypeboxCompilerCompat,
|
||||
"@mariozechner/pi-agent-core": _bundledPiAgentCore,
|
||||
"@mariozechner/pi-tui": _bundledPiTui,
|
||||
"@mariozechner/pi-ai": _bundledPiAi,
|
||||
@@ -56,14 +66,25 @@ const require = createRequire(import.meta.url);
|
||||
* In Bun binary mode, virtualModules is used instead.
|
||||
*/
|
||||
let _aliases: Record<string, string> | null = null;
|
||||
|
||||
function resolveTypeboxCompilerCompatPath(baseDir: string): string {
|
||||
const jsPath = path.resolve(baseDir, "typebox-compiler-compat.js");
|
||||
if (fs.existsSync(jsPath)) {
|
||||
return jsPath;
|
||||
}
|
||||
return path.resolve(baseDir, "typebox-compiler-compat.ts");
|
||||
}
|
||||
|
||||
function getAliases(): Record<string, string> {
|
||||
if (_aliases) return _aliases;
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageIndex = path.resolve(__dirname, "../..", "index.js");
|
||||
|
||||
const typeboxEntry = require.resolve("@sinclair/typebox");
|
||||
const typeboxRoot = typeboxEntry.replace(/[\\/]build[\\/]cjs[\\/]index\.js$/, "");
|
||||
const typeboxEntry = require.resolve("typebox");
|
||||
const typeboxCompileEntry = require.resolve("typebox/compile");
|
||||
const typeboxValueEntry = require.resolve("typebox/value");
|
||||
const typeboxCompilerCompat = resolveTypeboxCompilerCompatPath(__dirname);
|
||||
|
||||
const packagesRoot = path.resolve(__dirname, "../../../../");
|
||||
const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {
|
||||
@@ -80,7 +101,14 @@ function getAliases(): Record<string, string> {
|
||||
"@mariozechner/pi-tui": resolveWorkspaceOrImport("tui/dist/index.js", "@mariozechner/pi-tui"),
|
||||
"@mariozechner/pi-ai": resolveWorkspaceOrImport("ai/dist/index.js", "@mariozechner/pi-ai"),
|
||||
"@mariozechner/pi-ai/oauth": resolveWorkspaceOrImport("ai/dist/oauth.js", "@mariozechner/pi-ai/oauth"),
|
||||
"@sinclair/typebox": typeboxRoot,
|
||||
typebox: typeboxEntry,
|
||||
"typebox/compile": typeboxCompileEntry,
|
||||
"typebox/value": typeboxValueEntry,
|
||||
"typebox/compiler": typeboxCompilerCompat,
|
||||
"@sinclair/typebox": typeboxEntry,
|
||||
"@sinclair/typebox/compile": typeboxCompileEntry,
|
||||
"@sinclair/typebox/value": typeboxValueEntry,
|
||||
"@sinclair/typebox/compiler": typeboxCompilerCompat,
|
||||
};
|
||||
|
||||
return _aliases;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Code, Compile, Validator } from "typebox/compile";
|
||||
|
||||
export { Code, Compile, Validator };
|
||||
|
||||
// Legacy @sinclair/typebox/compiler compatibility for extensions.
|
||||
export const TypeCompiler = {
|
||||
Compile,
|
||||
};
|
||||
|
||||
// In TypeBox 0.x this was a named export. Map it to the v1 Validator class.
|
||||
export const TypeCheck = Validator;
|
||||
|
||||
export default {
|
||||
Code,
|
||||
Compile,
|
||||
Validator,
|
||||
TypeCompiler,
|
||||
TypeCheck,
|
||||
};
|
||||
@@ -39,7 +39,7 @@ import type {
|
||||
OverlayOptions,
|
||||
TUI,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import type { Static, TSchema } from "@sinclair/typebox";
|
||||
import type { Static, TSchema } from "typebox";
|
||||
import type { Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import type { BashResult } from "../bash-executor.js";
|
||||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
|
||||
@@ -479,7 +479,7 @@ type AnyToolDefinition = ToolDefinition<any, any, any>;
|
||||
export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
|
||||
tool: ToolDefinition<TParams, TDetails, TState>,
|
||||
): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition {
|
||||
return tool;
|
||||
return tool as ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -18,10 +18,11 @@ import {
|
||||
type SimpleStreamOptions,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import { registerOAuthProvider, resetOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import AjvModule from "ajv";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { Compile } from "typebox/compile";
|
||||
import type { TLocalizedValidationError } from "typebox/error";
|
||||
import { getAgentDir } from "../config.js";
|
||||
import type { AuthStorage } from "./auth-storage.js";
|
||||
import {
|
||||
@@ -31,9 +32,6 @@ import {
|
||||
resolveHeadersOrThrow,
|
||||
} from "./resolve-config-value.js";
|
||||
|
||||
const Ajv = (AjvModule as any).default || AjvModule;
|
||||
const ajv = new Ajv();
|
||||
|
||||
// Schema for OpenRouter routing preferences
|
||||
const PercentileCutoffsSchema = Type.Object({
|
||||
p50: Type.Optional(Type.Number()),
|
||||
@@ -179,10 +177,23 @@ const ModelsConfigSchema = Type.Object({
|
||||
providers: Type.Record(Type.String(), ProviderConfigSchema),
|
||||
});
|
||||
|
||||
ajv.addSchema(ModelsConfigSchema, "ModelsConfig");
|
||||
const validateModelsConfig = Compile(ModelsConfigSchema);
|
||||
|
||||
type ModelsConfig = Static<typeof ModelsConfigSchema>;
|
||||
|
||||
function formatValidationPath(error: TLocalizedValidationError): string {
|
||||
if (error.keyword === "required") {
|
||||
const requiredProperties = (error.params as { requiredProperties?: string[] }).requiredProperties;
|
||||
const requiredProperty = requiredProperties?.[0];
|
||||
if (requiredProperty) {
|
||||
const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
||||
return basePath ? `${basePath}.${requiredProperty}` : requiredProperty;
|
||||
}
|
||||
}
|
||||
const path = error.instancePath.replace(/^\//, "").replace(/\//g, ".");
|
||||
return path || "root";
|
||||
}
|
||||
|
||||
/** Provider override config (baseUrl, compat) without request auth/headers */
|
||||
interface ProviderOverride {
|
||||
baseUrl?: string;
|
||||
@@ -417,17 +428,19 @@ export class ModelRegistry {
|
||||
|
||||
try {
|
||||
const content = readFileSync(modelsJsonPath, "utf-8");
|
||||
const config: ModelsConfig = JSON.parse(content);
|
||||
const parsed = JSON.parse(content) as unknown;
|
||||
|
||||
// Validate schema
|
||||
const validate = ajv.getSchema("ModelsConfig")!;
|
||||
if (!validate(config)) {
|
||||
if (!validateModelsConfig.Check(parsed)) {
|
||||
const errors =
|
||||
validate.errors?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
|
||||
"Unknown schema error";
|
||||
validateModelsConfig
|
||||
.Errors(parsed)
|
||||
.map((error) => ` - ${formatValidationPath(error)}: ${error.message}`)
|
||||
.join("\n") || "Unknown schema error";
|
||||
return emptyCustomModelsResult(`Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`);
|
||||
}
|
||||
|
||||
const config = parsed as ModelsConfig;
|
||||
|
||||
// Additional validation
|
||||
this.validateConfig(config);
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { spawn } from "child_process";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
||||
import { theme } from "../../modes/interactive/theme/theme.js";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Box, Container, Spacer, Text } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { renderDiff } from "../../modes/interactive/components/diff.js";
|
||||
import type { ToolDefinition } from "../extensions/types.js";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { spawn } from "child_process";
|
||||
import { existsSync } from "fs";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { createInterface } from "node:readline";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { spawn } from "child_process";
|
||||
import { readFileSync, statSync } from "fs";
|
||||
import path from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { ensureTool } from "../../utils/tools-manager.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { existsSync, readdirSync, statSync } from "fs";
|
||||
import nodePath from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { resolveToCwd } from "./path-utils.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
|
||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Container, Text } from "@mariozechner/pi-tui";
|
||||
import { type Static, Type } from "@sinclair/typebox";
|
||||
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
|
||||
import { dirname } from "path";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
|
||||
Reference in New Issue
Block a user