From c15360898a1da381047397ea2b294e7fa300acf3 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Mon, 20 Jul 2026 18:58:07 +0200 Subject: [PATCH] refactor(appkit): add defineManifest to remove manifest casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every plugin declared `static manifest = manifest as PluginManifest` (agents needed `as unknown as`). The cast is unavoidable with a raw JSON import: TS widens JSON fields to `string`, but PluginManifest.resources[].type is the nominal ResourceType enum, so the structural shape never assigns — and multi-resource plugins infer a heterogeneous union that a plain `as` also rejects. Add defineManifest() in the registry: it parses the JSON through the canonical pluginManifestSchema (real runtime validation, which the loader did not do before) and returns the strict type via one audited internal assertion. All 9 plugins now use `static manifest = defineManifest<"name">(manifest)` with no local casts. Export pluginManifestSchema from shared for the parse. Adds tests covering valid pass-through and rejection of unknown resource type, invalid permission, and missing required fields. Signed-off-by: MarioCadenas --- .../api/appkit/Function.defineManifest.md | 40 +++++++++++++ docs/docs/api/appkit/Variable.agents.md | 2 +- docs/docs/api/appkit/index.md | 1 + docs/docs/api/appkit/typedoc-sidebar.ts | 5 ++ packages/appkit/src/index.ts | 1 + packages/appkit/src/plugins/agents/agents.ts | 8 +-- .../appkit/src/plugins/ai-search/ai-search.ts | 4 +- .../appkit/src/plugins/analytics/analytics.ts | 4 +- packages/appkit/src/plugins/files/plugin.ts | 6 +- packages/appkit/src/plugins/genie/genie.ts | 4 +- packages/appkit/src/plugins/jobs/plugin.ts | 6 +- .../appkit/src/plugins/lakebase/lakebase.ts | 4 +- packages/appkit/src/plugins/server/index.ts | 4 +- .../appkit/src/plugins/serving/serving.ts | 6 +- .../appkit/src/plugins/ui-variants/index.ts | 4 +- packages/appkit/src/registry/index.ts | 6 +- .../appkit/src/registry/manifest-loader.ts | 28 ++++++++- .../registry/tests/manifest-loader.test.ts | 59 +++++++++++++++++++ .../commands/plugin/create/scaffold.test.ts | 2 +- .../cli/commands/plugin/create/scaffold.ts | 4 +- packages/shared/src/index.ts | 1 + 21 files changed, 166 insertions(+), 33 deletions(-) create mode 100644 docs/docs/api/appkit/Function.defineManifest.md diff --git a/docs/docs/api/appkit/Function.defineManifest.md b/docs/docs/api/appkit/Function.defineManifest.md new file mode 100644 index 000000000..4d1a16db8 --- /dev/null +++ b/docs/docs/api/appkit/Function.defineManifest.md @@ -0,0 +1,40 @@ +# Function: defineManifest() + +```ts +function defineManifest(manifest: unknown): PluginManifest; +``` + +Validates a raw manifest (typically a `manifest.json` import) against the +canonical Zod schema and returns it as a strict [PluginManifest](Interface.PluginManifest.md). + +Plugins declare `static manifest = defineManifest<"my-plugin">(manifestJson)` +instead of casting. A plain `as PluginManifest` can't work: a JSON import +widens every field to `string`, and `PluginManifest.resources[].type` is the +nominal `ResourceType` enum, so the structural JSON shape never assigns. The +single internal assertion here bridges that gap in one audited place — after +`parse()` has confirmed the values are real `ResourceType`/permission strings +— rather than every plugin repeating `as unknown as PluginManifest`. + +Pass the plugin name as `TName` so the literal is preserved: `toPlugin` +derives the typed plugin key from `manifest.name`, and a widened `string` +there would collapse the typed plugin registry. + +## Type Parameters + +| Type Parameter | Default type | +| ------ | ------ | +| `TName` *extends* `string` | `string` | + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `manifest` | `unknown` | + +## Returns + +[`PluginManifest`](Interface.PluginManifest.md)\<`TName`\> + +## Throws + +If the manifest doesn't match the schema. diff --git a/docs/docs/api/appkit/Variable.agents.md b/docs/docs/api/appkit/Variable.agents.md index 227a5bf3b..148adb6da 100644 --- a/docs/docs/api/appkit/Variable.agents.md +++ b/docs/docs/api/appkit/Variable.agents.md @@ -1,7 +1,7 @@ # Variable: agents ```ts -const agents: ToPlugin; +const agents: ToPlugin; ``` Plugin factory for the agents plugin. Reads `config/agents/*.md` by default, diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index f39a52db2..520908353 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -147,6 +147,7 @@ surface with `@databricks/appkit/beta`. Not meant for application imports. | [createLakebasePool](Function.createLakebasePool.md) | Create a Lakebase pool with appkit's logger integration. Telemetry automatically uses appkit's OpenTelemetry configuration via global registry. | | [createLakebasePoolManager](Function.createLakebasePoolManager.md) | Create a pool manager that maintains per-key Lakebase connection pools. | | [createWorkspaceClient](Function.createWorkspaceClient.md) | Construct an AppKit workspace client. | +| [defineManifest](Function.defineManifest.md) | Validates a raw manifest (typically a `manifest.json` import) against the canonical Zod schema and returns it as a strict [PluginManifest](Interface.PluginManifest.md). | | [defineTool](Function.defineTool.md) | Defines a single tool entry for a plugin's internal registry. | | [executeFromRegistry](Function.executeFromRegistry.md) | Validates tool-call arguments against the entry's schema and invokes its handler. On validation failure, returns an LLM-friendly error string (matching the behavior of `tool()`) rather than throwing, so the model can self-correct on its next turn. | | [extractServingEndpoints](Function.extractServingEndpoints.md) | Extract serving endpoint config from a server file by AST-parsing it. Looks for `serving({ endpoints: { alias: { env: "..." }, ... } })` calls and extracts the endpoint alias names and their environment variable mappings. | diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index 18a5333b1..a3d0aff4b 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -610,6 +610,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Function.createWorkspaceClient", label: "createWorkspaceClient" }, + { + type: "doc", + id: "api/appkit/Function.defineManifest", + label: "defineManifest" + }, { type: "doc", id: "api/appkit/Function.defineTool", diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index eac0b27b9..f223061a7 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -93,6 +93,7 @@ export type { ValidationResult, } from "./registry"; export { + defineManifest, getPluginManifest, getResourceRequirements, ResourceRegistry, diff --git a/packages/appkit/src/plugins/agents/agents.ts b/packages/appkit/src/plugins/agents/agents.ts index 6f755462d..936f66690 100644 --- a/packages/appkit/src/plugins/agents/agents.ts +++ b/packages/appkit/src/plugins/agents/agents.ts @@ -52,7 +52,7 @@ import type { import { isToolkitEntry } from "../../core/agent/types"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import { agentStreamDefaults } from "./defaults"; import { EventChannel } from "./event-channel"; import { AgentEventTranslator } from "./event-translator"; @@ -142,11 +142,7 @@ interface RunState { } export class AgentsPlugin extends Plugin implements ToolProvider { - // Routed through `unknown`: the optional resources have differing `fields` - // keys (serving `name`, experiment `experimentId`), which TS widens to an - // incompatible union on the JSON import. The shape is validated at runtime - // against the plugin-manifest schema. - static manifest = manifest as unknown as PluginManifest; + static manifest = defineManifest<"agents">(manifest); static phase: PluginPhase = "deferred"; declare protected config: AgentsPluginConfig; diff --git a/packages/appkit/src/plugins/ai-search/ai-search.ts b/packages/appkit/src/plugins/ai-search/ai-search.ts index 303f8ebb3..4c62cf7a8 100644 --- a/packages/appkit/src/plugins/ai-search/ai-search.ts +++ b/packages/appkit/src/plugins/ai-search/ai-search.ts @@ -11,7 +11,7 @@ import type { import { getCurrentUserId, getWorkspaceClient } from "../../context"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import { formatWarningBanner } from "../../utils/banner"; import { aiSearchDefaults } from "./defaults"; import manifest from "./manifest.json"; @@ -32,7 +32,7 @@ const querySettings: PluginExecutionSettings = { }; export class AiSearchPlugin extends Plugin { - static manifest = manifest as PluginManifest<"aiSearch">; + static manifest = defineManifest<"aiSearch">(manifest); protected static description = "Query Databricks Vector Search indexes with hybrid search, reranking, and pagination"; diff --git a/packages/appkit/src/plugins/analytics/analytics.ts b/packages/appkit/src/plugins/analytics/analytics.ts index 782ac33c4..dc3543be4 100644 --- a/packages/appkit/src/plugins/analytics/analytics.ts +++ b/packages/appkit/src/plugins/analytics/analytics.ts @@ -27,7 +27,7 @@ import { assertReadOnlySql } from "../../core/agent/tools/sql-policy"; import { AppKitError, ExecutionError } from "../../errors"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import type { WorkspaceClient } from "../../workspace-client"; import { queryDefaults } from "./defaults"; import manifest from "./manifest.json"; @@ -110,7 +110,7 @@ async function* streamCallbacks( export class AnalyticsPlugin extends Plugin implements ToolProvider { /** Plugin manifest declaring metadata and resource requirements */ - static manifest = manifest as PluginManifest<"analytics">; + static manifest = defineManifest<"analytics">(manifest); protected static description = "Analytics plugin for data analysis"; declare protected config: IAnalyticsConfig; diff --git a/packages/appkit/src/plugins/files/plugin.ts b/packages/appkit/src/plugins/files/plugin.ts index b770616c4..fa175870c 100644 --- a/packages/appkit/src/plugins/files/plugin.ts +++ b/packages/appkit/src/plugins/files/plugin.ts @@ -37,8 +37,8 @@ import { import { AuthenticationError } from "../../errors"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest, ResourceRequirement } from "../../registry"; -import { ResourceType } from "../../registry"; +import type { ResourceRequirement } from "../../registry"; +import { defineManifest, ResourceType } from "../../registry"; import { ApiError } from "../../workspace-client"; import { FILES_DOWNLOAD_DEFAULTS, @@ -69,7 +69,7 @@ export class FilesPlugin extends Plugin implements ToolProvider { name = "files"; /** Plugin manifest declaring metadata and resource requirements. */ - static manifest = manifest as PluginManifest; + static manifest = defineManifest<"files">(manifest); protected static description = "Files plugin for Databricks file operations"; declare protected config: IFilesConfig; diff --git a/packages/appkit/src/plugins/genie/genie.ts b/packages/appkit/src/plugins/genie/genie.ts index 845d715ee..351d3988e 100644 --- a/packages/appkit/src/plugins/genie/genie.ts +++ b/packages/appkit/src/plugins/genie/genie.ts @@ -20,7 +20,7 @@ import { } from "../../core/agent/tools/define-tool"; import { createLogger } from "../../logging"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import { genieStreamDefaults } from "./defaults"; import manifest from "./manifest.json"; import type { @@ -33,7 +33,7 @@ import type { const logger = createLogger("genie"); export class GeniePlugin extends Plugin implements ToolProvider { - static manifest = manifest as PluginManifest<"genie">; + static manifest = defineManifest<"genie">(manifest); protected static description = "AI/BI Genie space integration for natural language data queries"; diff --git a/packages/appkit/src/plugins/jobs/plugin.ts b/packages/appkit/src/plugins/jobs/plugin.ts index 4945b5741..fe2af9599 100644 --- a/packages/appkit/src/plugins/jobs/plugin.ts +++ b/packages/appkit/src/plugins/jobs/plugin.ts @@ -14,8 +14,8 @@ import { ExecutionError, ValidationError } from "../../errors"; import { createLogger } from "../../logging/logger"; import type { ExecutionResult } from "../../plugin"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest, ResourceRequirement } from "../../registry"; -import { ResourceType } from "../../registry"; +import type { ResourceRequirement } from "../../registry"; +import { defineManifest, ResourceType } from "../../registry"; import type { jobs as jobsTypes } from "../../workspace-client"; import { JOBS_READ_DEFAULTS, @@ -85,7 +85,7 @@ function abortableSleep(ms: number, signal?: AbortSignal): Promise { } class JobsPlugin extends Plugin { - static manifest = manifest as PluginManifest; + static manifest = defineManifest<"jobs">(manifest); declare protected config: IJobsConfig; private connector: JobsConnector; diff --git a/packages/appkit/src/plugins/lakebase/lakebase.ts b/packages/appkit/src/plugins/lakebase/lakebase.ts index 42b383df2..8518b3ab2 100644 --- a/packages/appkit/src/plugins/lakebase/lakebase.ts +++ b/packages/appkit/src/plugins/lakebase/lakebase.ts @@ -23,7 +23,7 @@ import { import { assertReadOnlySql } from "../../core/agent/tools/sql-policy"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import { createWorkspaceClient } from "../../workspace-client"; import manifest from "./manifest.json"; import type { ILakebaseConfig } from "./types"; @@ -66,7 +66,7 @@ const OBO_POOL_DEFAULTS = { */ export class LakebasePlugin extends Plugin implements ToolProvider { /** Plugin manifest declaring metadata and resource requirements */ - static manifest = manifest as PluginManifest<"lakebase">; + static manifest = defineManifest<"lakebase">(manifest); declare protected config: ILakebaseConfig; private pool: RoutingPool | null = null; diff --git a/packages/appkit/src/plugins/server/index.ts b/packages/appkit/src/plugins/server/index.ts index b882f6294..bcdea787a 100644 --- a/packages/appkit/src/plugins/server/index.ts +++ b/packages/appkit/src/plugins/server/index.ts @@ -12,7 +12,7 @@ import { AppKitError, ServerError } from "../../errors"; import { TelemetryReporter } from "../../internal-telemetry"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import { instrumentations } from "../../telemetry"; import { sanitizeClientConfig } from "./client-config-sanitizer"; import manifest from "./manifest.json"; @@ -66,7 +66,7 @@ export class ServerPlugin extends Plugin { private static readonly SERVER_CLOSE_TIMEOUT_MS = 2_000; /** Plugin manifest declaring metadata and resource requirements */ - static manifest = manifest as PluginManifest<"server">; + static manifest = defineManifest<"server">(manifest); private serverApplication: express.Application; private server: HTTPServer | null; private viteDevServer?: ViteDevServer; diff --git a/packages/appkit/src/plugins/serving/serving.ts b/packages/appkit/src/plugins/serving/serving.ts index 8acfd4393..24097dc0a 100644 --- a/packages/appkit/src/plugins/serving/serving.ts +++ b/packages/appkit/src/plugins/serving/serving.ts @@ -9,8 +9,8 @@ import * as servingConnector from "../../connectors/serving/client"; import { getWorkspaceClient } from "../../context"; import { createLogger } from "../../logging"; import { type ExecutionResult, Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest, ResourceRequirement } from "../../registry"; -import { ResourceType } from "../../registry"; +import type { ResourceRequirement } from "../../registry"; +import { defineManifest, ResourceType } from "../../registry"; import { servingInvokeDefaults } from "./defaults"; import manifest from "./manifest.json"; import { filterRequestBody, loadEndpointSchemas } from "./schema-filter"; @@ -42,7 +42,7 @@ interface ResolvedEndpoint { } export class ServingPlugin extends Plugin { - static manifest = manifest as PluginManifest<"serving">; + static manifest = defineManifest<"serving">(manifest); protected static description = "Authenticated proxy to Databricks Model Serving endpoints"; diff --git a/packages/appkit/src/plugins/ui-variants/index.ts b/packages/appkit/src/plugins/ui-variants/index.ts index 9d0ccbfde..bea2f65f3 100644 --- a/packages/appkit/src/plugins/ui-variants/index.ts +++ b/packages/appkit/src/plugins/ui-variants/index.ts @@ -2,7 +2,7 @@ import type { BasePluginConfig, IAppRouter } from "shared"; import { createLogger } from "../../logging/logger"; import { Plugin, toPlugin } from "../../plugin"; -import type { PluginManifest } from "../../registry"; +import { defineManifest } from "../../registry"; import { FileChoiceStore, type UiChoiceRecord } from "./choice-sink"; import manifest from "./manifest.json"; @@ -26,7 +26,7 @@ interface ConfirmRequestBody { * per `` id — and the plugin only records; it never edits source. */ class UiVariantsPlugin extends Plugin { - static manifest = manifest as PluginManifest<"uiVariants">; + static manifest = defineManifest<"uiVariants">(manifest); protected static description = "Dev-only recorder for the UI picker"; diff --git a/packages/appkit/src/registry/index.ts b/packages/appkit/src/registry/index.ts index 08b1cc5fc..ceff9adfd 100644 --- a/packages/appkit/src/registry/index.ts +++ b/packages/appkit/src/registry/index.ts @@ -12,7 +12,11 @@ * - (Future) Config generators for app.yaml, databricks.yml, .env.example */ -export { getPluginManifest, getResourceRequirements } from "./manifest-loader"; +export { + defineManifest, + getPluginManifest, + getResourceRequirements, +} from "./manifest-loader"; export { ResourceRegistry } from "./resource-registry"; export * from "./types"; diff --git a/packages/appkit/src/registry/manifest-loader.ts b/packages/appkit/src/registry/manifest-loader.ts index 255dddd70..a3523d8ac 100644 --- a/packages/appkit/src/registry/manifest-loader.ts +++ b/packages/appkit/src/registry/manifest-loader.ts @@ -1,4 +1,4 @@ -import type { PluginConstructor } from "shared"; +import { type PluginConstructor, pluginManifestSchema } from "shared"; import { ConfigurationError } from "../errors"; import { createLogger } from "../logging/logger"; @@ -11,6 +11,32 @@ import { PERMISSIONS_BY_TYPE, ResourceType } from "./types"; const logger = createLogger("manifest-loader"); +/** + * Validates a raw manifest (typically a `manifest.json` import) against the + * canonical Zod schema and returns it as a strict {@link PluginManifest}. + * + * Plugins declare `static manifest = defineManifest<"my-plugin">(manifestJson)` + * instead of casting. A plain `as PluginManifest` can't work: a JSON import + * widens every field to `string`, and `PluginManifest.resources[].type` is the + * nominal `ResourceType` enum, so the structural JSON shape never assigns. The + * single internal assertion here bridges that gap in one audited place — after + * `parse()` has confirmed the values are real `ResourceType`/permission strings + * — rather than every plugin repeating `as unknown as PluginManifest`. + * + * Pass the plugin name as `TName` so the literal is preserved: `toPlugin` + * derives the typed plugin key from `manifest.name`, and a widened `string` + * there would collapse the typed plugin registry. + * + * @throws {ZodError} If the manifest doesn't match the schema. + */ +export function defineManifest( + manifest: unknown, +): PluginManifest { + return pluginManifestSchema.parse( + manifest, + ) as unknown as PluginManifest; +} + /** Loose resource from shared/manifest (string type and permission). */ interface LooseResource { type: string; diff --git a/packages/appkit/src/registry/tests/manifest-loader.test.ts b/packages/appkit/src/registry/tests/manifest-loader.test.ts index 145ecd36d..9514116ac 100644 --- a/packages/appkit/src/registry/tests/manifest-loader.test.ts +++ b/packages/appkit/src/registry/tests/manifest-loader.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { ConfigurationError } from "../../errors"; import { + defineManifest, getPluginManifest, getResourceRequirements, isValidManifest, @@ -555,4 +556,62 @@ describe("Manifest Loader", () => { expect(isValidManifest(valid)).toBe(true); }); }); + + describe("defineManifest", () => { + const validManifest = { + name: "testPlugin", + displayName: "Test Plugin", + description: "A test plugin", + resources: { + required: [ + { + type: "sql_warehouse", + alias: "warehouse", + resourceKey: "sql-warehouse", + description: "Test warehouse", + permission: "CAN_USE", + fields: { id: { env: "TEST_WAREHOUSE_ID" } }, + }, + ], + optional: [], + }, + }; + + it("returns a schema-valid manifest unchanged", () => { + const result = defineManifest<"testPlugin">(validManifest); + expect(result.name).toBe("testPlugin"); + expect(result.resources.required[0].type).toBe("sql_warehouse"); + }); + + it("throws when a resource has an unknown type", () => { + const bad = { + ...validManifest, + resources: { + required: [ + { ...validManifest.resources.required[0], type: "not_a_type" }, + ], + optional: [], + }, + }; + expect(() => defineManifest(bad)).toThrow(); + }); + + it("throws when a permission is invalid for the resource type", () => { + const bad = { + ...validManifest, + resources: { + required: [ + { ...validManifest.resources.required[0], permission: "CAN_QUERY" }, + ], + optional: [], + }, + }; + expect(() => defineManifest(bad)).toThrow(); + }); + + it("throws when a required top-level field is missing", () => { + const { name: _omit, ...noName } = validManifest; + expect(() => defineManifest(noName)).toThrow(); + }); + }); }); diff --git a/packages/shared/src/cli/commands/plugin/create/scaffold.test.ts b/packages/shared/src/cli/commands/plugin/create/scaffold.test.ts index 56dde0fb4..074ba8a07 100644 --- a/packages/shared/src/cli/commands/plugin/create/scaffold.test.ts +++ b/packages/shared/src/cli/commands/plugin/create/scaffold.test.ts @@ -104,7 +104,7 @@ describe("scaffold", () => { expect(pluginTs).toContain("class MyPlugin"); expect(pluginTs).toContain("export const myPlugin = toPlugin(MyPlugin)"); expect(pluginTs).toContain('import manifest from "./manifest.json"'); - expect(pluginTs).toContain("manifest as PluginManifest"); + expect(pluginTs).toContain('defineManifest<"my-plugin">(manifest)'); }); it("generates index.ts with correct exports", () => { diff --git a/packages/shared/src/cli/commands/plugin/create/scaffold.ts b/packages/shared/src/cli/commands/plugin/create/scaffold.ts index 91c0c0b74..da7bd04c2 100644 --- a/packages/shared/src/cli/commands/plugin/create/scaffold.ts +++ b/packages/shared/src/cli/commands/plugin/create/scaffold.ts @@ -118,15 +118,15 @@ export function scaffoldPlugin( ); const pluginTs = `import { + defineManifest, Plugin, toPlugin, type IAppRouter, - type PluginManifest, } from "@databricks/appkit"; import manifest from "./manifest.json"; export class ${className} extends Plugin { - static manifest = manifest as PluginManifest<"${answers.name}">; + static manifest = defineManifest<"${answers.name}">(manifest); injectRoutes(router: IAppRouter): void { // Add your routes here, e.g.: diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index df3cbdd81..c8e7e8fa5 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -5,6 +5,7 @@ export * from "./genie"; export * from "./metric-filter"; export * from "./metric-metadata"; export * from "./plugin"; +export { pluginManifestSchema } from "./schemas/manifest"; export * from "./sql"; export * from "./sse/analytics"; export * from "./tunnel";