Merge remote-tracking branch 'origin/farm/39009a49/fix-extension-validation-shims'

This commit is contained in:
can1357
2026-06-18 04:52:33 +02:00
7 changed files with 296 additions and 23 deletions
@@ -11,14 +11,54 @@
* bare package root. Subpath imports (`@oh-my-pi/pi-ai/oauth`, etc.)
* continue to resolve directly against the bundled pi-ai package.
*
* The `Type` runtime is borrowed from the Zod-backed TypeBox shim that
* already serves bare `@sinclair/typebox` imports for the same extension
* class, keeping the legacy-compat surface internally consistent.
* The `Type` runtime and legacy `StringEnum()` helper are borrowed from the
* Zod-backed TypeBox shim that already serves TypeBox imports for the same
* extension class, keeping the legacy-compat surface internally consistent.
*
* Type-level `Static` and `TSchema` continue to come from pi-ai's own
* `types.ts` via the `export *` below — pi-ai still exports both as types,
* only the runtime `Type` builder was removed.
* only the runtime `Type` builder and `StringEnum()` helper were removed.
*/
import { z } from "zod/v4";
import { type TSchema, Type } from "./typebox";
export interface StringEnumOptions<T extends string> {
description?: string;
default?: T;
examples?: T[];
[key: string]: unknown;
}
function stringEnumWireSchema<T extends string>(values: readonly T[], options: StringEnumOptions<T> | undefined) {
const schema: Record<string, unknown> = {
type: "string",
enum: [...values],
};
if (!options) return schema;
for (const key in options) {
if (options[key] !== undefined) {
schema[key] = options[key];
}
}
return schema;
}
export function StringEnum<T extends string>(values: readonly T[], options?: StringEnumOptions<T>): TSchema {
let schema: TSchema =
values.length === 0
? z.never()
: z.enum(values).describe(options?.description ?? "Legacy string enum compatibility schema");
if (options && "default" in options) {
schema = schema.default(options.default);
}
Object.defineProperty(schema, "toJSON", {
value: () => stringEnumWireSchema(values, options),
enumerable: false,
writable: true,
configurable: true,
});
return schema;
}
export * from "@oh-my-pi/pi-ai";
export { Type } from "./typebox";
export { Type };
@@ -12,4 +12,117 @@
* the same module identity as a direct `@oh-my-pi/pi-coding-agent` import.
*/
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
import type { TSchema } from "@oh-my-pi/pi-ai";
import { parseFrontmatter as parseOmpFrontmatter } from "@oh-my-pi/pi-utils";
import { Settings } from "../config/settings";
import { BUILTIN_TOOLS, type Tool, type ToolSession } from "../tools";
import type { ToolDefinition } from "./extensions/types";
import { Type } from "./typebox";
const TOOL_DEFINITION_MARKER = "__isToolDefinition";
const LEGACY_BUILTIN_TOOL_MARKER = "__ompLegacyBuiltinTool";
const LEGACY_CODING_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
type LegacyCodingToolName = (typeof LEGACY_CODING_TOOL_NAMES)[number];
type LegacyBuiltinToolDefinition = ToolDefinition & { [LEGACY_BUILTIN_TOOL_MARKER]: true };
function markToolDefinition<TParams extends TSchema, TDetails>(
tool: ToolDefinition<TParams, TDetails>,
): ToolDefinition<TParams, TDetails> {
Object.defineProperty(tool, TOOL_DEFINITION_MARKER, {
value: true,
enumerable: false,
writable: false,
configurable: true,
});
return tool;
}
function legacyToolSession(cwd: string): ToolSession {
return {
cwd,
hasUI: false,
getSessionFile: () => null,
getSessionSpawns: () => null,
settings: Settings.isolated(),
};
}
function createBuiltinTool(cwd: string, name: LegacyCodingToolName): Tool {
const tool = BUILTIN_TOOLS[name](legacyToolSession(cwd));
if (tool instanceof Promise) {
throw new Error(`Built-in ${name} tool factory unexpectedly returned a promise.`);
}
if (!tool) {
throw new Error(`Built-in ${name} tool is unavailable.`);
}
return tool;
}
async function executeBuiltinTool(
cwd: string,
name: LegacyCodingToolName,
toolCallId: string,
params: unknown,
signal: AbortSignal | undefined,
onUpdate: AgentToolUpdateCallback | undefined,
) {
const tool = createBuiltinTool(cwd, name);
return tool.execute(toolCallId, params, signal, onUpdate);
}
function legacyBuiltinTool(cwd: string, name: LegacyCodingToolName): ToolDefinition {
const tool = createBuiltinTool(cwd, name);
const definition: LegacyBuiltinToolDefinition = {
name: tool.name,
label: tool.label,
description: tool.description,
parameters: tool.parameters,
hidden: tool.hidden,
deferrable: tool.deferrable,
approval: tool.approval,
execute: (toolCallId, params, signal, onUpdate) =>
executeBuiltinTool(cwd, name, toolCallId, params, signal, onUpdate),
[LEGACY_BUILTIN_TOOL_MARKER]: true,
};
return markToolDefinition(definition);
}
export interface ParsedFrontmatter<T extends Record<string, unknown> = Record<string, unknown>> {
frontmatter: T;
body: string;
}
export function parseFrontmatter<T extends Record<string, unknown> = Record<string, unknown>>(
content: string,
): ParsedFrontmatter<T> {
const { frontmatter, body } = parseOmpFrontmatter(content, { level: "fatal" });
return { frontmatter: frontmatter as T, body };
}
export function stripFrontmatter(content: string): string {
return parseFrontmatter(content).body;
}
export function defineTool<TParams extends TSchema = TSchema, TDetails = unknown>(
tool: ToolDefinition<TParams, TDetails>,
): ToolDefinition<TParams, TDetails> {
return markToolDefinition(tool);
}
export function createCodingTools(cwd: string): ToolDefinition[] {
return LEGACY_CODING_TOOL_NAMES.map(name => legacyBuiltinTool(cwd, name));
}
export const SettingsManager = {
create(cwd: string, agentDir?: string): Promise<Settings> {
return Settings.init({ cwd, agentDir });
},
inMemory(): Settings {
return Settings.isolated();
},
} as const;
export * from "../index";
export { Type };
@@ -64,14 +64,14 @@ const packageRootCache = new Map<string, string | null>();
const packageImportsCache = new Map<string, Record<string, unknown> | null>();
const PACKAGE_IMPORT_EXCLUDED = Symbol("packageImportExcluded");
// Extensions that imported `@sinclair/typebox` directly used to resolve against a
// real `@sinclair/typebox` install. The runtime dep was replaced with the Zod-backed
// shim under `extensibility/typebox.ts`; plugins still importing the public name
// are redirected to that shim so existing extensions keep working without code
// changes. Submodules like `@sinclair/typebox/compiler` are intentionally not
// remapped — those expose TypeBox-only APIs the shim does not provide and plugins
// relying on them must vendor `@sinclair/typebox` directly.
const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/;
// Extensions that imported TypeBox directly used to resolve against a real
// `@sinclair/typebox` or `typebox` install. The runtime dep was replaced with
// the Zod-backed shim under `extensibility/typebox.ts`; plugins still importing
// either public name are redirected to that shim so existing extensions keep
// working without code changes. Submodules like `@sinclair/typebox/compiler`
// are intentionally not remapped — those expose TypeBox-only APIs the shim does
// not provide and plugins relying on them must vendor TypeBox directly.
const TYPEBOX_SPECIFIER_FILTER = /^(?:@sinclair\/typebox|typebox)$/;
// Compat shim and bundled-package paths used in compiled-binary mode. The shim
// paths must point at files that ship inside the bunfs root; in dev /
@@ -295,14 +295,14 @@ function rewriteLegacyPiImports(source: string): string {
);
}
// Match the bare `@sinclair/typebox` import specifier (static + dynamic).
// Subpath imports like `@sinclair/typebox/compiler` are intentionally excluded —
// they expose TypeBox-only APIs the Zod-backed shim does not provide.
const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(@sinclair\/typebox)(["'])/g;
// Match the bare TypeBox import specifiers (static + dynamic). Subpath imports
// like `@sinclair/typebox/compiler` are intentionally excluded — they expose
// TypeBox-only APIs the Zod-backed shim does not provide.
const TYPEBOX_IMPORT_SPECIFIER_REGEX = /((?:from\s+|import\s+|import\s*\(\s*)["'])(@sinclair\/typebox|typebox)(["'])/g;
/**
* Rewrite the extension-owned specifiers OMP must host-resolve — legacy
* `@(scope)/pi-*`, bare `@sinclair/typebox`, and package `imports` aliases like
* `@(scope)/pi-*`, bare TypeBox packages, and package `imports` aliases like
* `#src/*` — to absolute `file://` URLs. Every other specifier (relative
* siblings and third-party dependencies) is left untouched so Bun resolves it
* natively from the extension's real on-disk location.
+16 -4
View File
@@ -528,6 +528,11 @@ export interface CreateAgentSessionOptions {
/** Settings instance. Default: Settings.init({ cwd, agentDir }) */
settings?: Settings;
/**
* Legacy alias for `settings`. Older Pi extensions pass SettingsManager.create(...)
* through this field; accept it so their SDK calls keep the configured settings.
*/
settingsManager?: Settings | Promise<Settings>;
/** Whether UI is available (enables interactive tools like ask). Default: false */
hasUI?: boolean;
@@ -874,6 +879,10 @@ function isCustomTool(tool: CustomTool | ToolDefinition): tool is CustomTool {
return !(tool as any).__isToolDefinition;
}
function isLegacyBuiltinToolDefinition(tool: CustomTool | ToolDefinition): boolean {
return !isCustomTool(tool) && "__ompLegacyBuiltinTool" in tool && tool.__ompLegacyBuiltinTool === true;
}
const TOOL_DEFINITION_MARKER = Symbol("__isToolDefinition");
/** Matches the truncation applied to per-server instructions inside `rebuildSystemPrompt`. */
@@ -1138,7 +1147,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
startupCredentialDisabledEvents.push(event);
}
});
const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
const settings = await (options.settings ??
options.settingsManager ??
logger.time("settings", Settings.init, { cwd, agentDir }));
logger.time("initializeWithSettings", initializeWithSettings, settings);
if (!options.modelRegistry) {
modelRegistry.refreshInBackground();
@@ -2008,12 +2019,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
const toolContextStore = new ToolContextStore(getSessionContext);
const registeredTools = extensionRunner.getAllRegisteredTools();
const sdkCustomTools = options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? [];
const allCustomTools = [
...registeredTools,
...(options.customTools?.map(tool => {
...sdkCustomTools.map(tool => {
const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
return { definition, extensionPath: "<sdk>" };
}) ?? []),
}),
];
// `wrapToolWithMetaNotice` runs the centralized large-output → artifact spill.
// Built-in tools get it in `createTools`; extension, SDK-custom, image-gen,
@@ -2291,7 +2303,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
// Custom tools and extension-registered tools are always included regardless of toolNames filter
const alwaysInclude: string[] = [
...(options.customTools?.map(t => (isCustomTool(t) ? t.name : t.name)) ?? []),
...sdkCustomTools.map(t => (isCustomTool(t) ? t.name : t.name)),
...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
];
for (const name of alwaysInclude) {