Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/appkit/src/plugins/agents/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,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";
Expand Down Expand Up @@ -128,7 +128,7 @@ interface RunState {
}

export class AgentsPlugin extends Plugin implements ToolProvider {
static manifest = manifest as PluginManifest;
static manifest = defineManifest(manifest);
static phase: PluginPhase = "deferred";

protected declare config: AgentsPluginConfig;
Expand Down
4 changes: 2 additions & 2 deletions packages/appkit/src/plugins/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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 { queryDefaults } from "./defaults";
import manifest from "./manifest.json";
import { QueryProcessor } from "./query";
Expand Down Expand Up @@ -97,7 +97,7 @@ async function* streamCallbacks<T>(

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";
protected declare config: IAnalyticsConfig;
Expand Down
6 changes: 3 additions & 3 deletions packages/appkit/src/plugins/files/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,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 {
FILES_DOWNLOAD_DEFAULTS,
FILES_MAX_UPLOAD_SIZE,
Expand Down Expand Up @@ -67,7 +67,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(manifest);
protected static description = "Files plugin for Databricks file operations";
protected declare config: IFilesConfig;

Expand Down
4 changes: 2 additions & 2 deletions packages/appkit/src/plugins/genie/genie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,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 {
Expand All @@ -31,7 +31,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";
Expand Down
6 changes: 3 additions & 3 deletions packages/appkit/src/plugins/jobs/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
JOBS_READ_DEFAULTS,
JOBS_STREAM_DEFAULTS,
Expand Down Expand Up @@ -85,7 +85,7 @@ function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
}

class JobsPlugin extends Plugin {
static manifest = manifest as PluginManifest;
static manifest = defineManifest(manifest);

protected declare config: IJobsConfig;
private connector: JobsConnector;
Expand Down
4 changes: 2 additions & 2 deletions packages/appkit/src/plugins/lakebase/lakebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 manifest from "./manifest.json";
import type { ILakebaseConfig } from "./types";

Expand Down Expand Up @@ -65,7 +65,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);

protected declare config: ILakebaseConfig;
private pool: RoutingPool | null = null;
Expand Down
4 changes: 2 additions & 2 deletions packages/appkit/src/plugins/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { 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";
Expand Down Expand Up @@ -55,7 +55,7 @@ export class ServerPlugin extends Plugin {
};

/** 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;
Expand Down
6 changes: 3 additions & 3 deletions packages/appkit/src/plugins/serving/serving.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,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";
Expand Down Expand Up @@ -40,7 +40,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";
Expand Down
4 changes: 2 additions & 2 deletions packages/appkit/src/plugins/vector-search/vector-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { VsRawResponse } from "../../connectors/vector-search/types";
import { getWorkspaceClient } from "../../context";
import { createLogger } from "../../logging/logger";
import { Plugin, toPlugin } from "../../plugin";
import type { PluginManifest } from "../../registry";
import { defineManifest } from "../../registry";
import { vectorSearchDefaults } from "./defaults";
import manifest from "./manifest.json";
import type {
Expand All @@ -22,7 +22,7 @@ const querySettings: PluginExecutionSettings = {
};

export class VectorSearchPlugin extends Plugin<IVectorSearchConfig> {
static manifest = manifest as PluginManifest<"vector-search">;
static manifest = defineManifest<"vector-search">(manifest);

protected static description =
"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination";
Expand Down
6 changes: 5 additions & 1 deletion packages/appkit/src/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
28 changes: 27 additions & 1 deletion packages/appkit/src/registry/manifest-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PluginConstructor } from "shared";
import { type PluginConstructor, pluginManifestSchema } from "shared";
import { ConfigurationError } from "../errors";
import { createLogger } from "../logging/logger";
import type {
Expand All @@ -10,6 +10,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<TName extends string = string>(
manifest: unknown,
): PluginManifest<TName> {
return pluginManifestSchema.parse(
manifest,
) as unknown as PluginManifest<TName>;
}

/** Loose resource from shared/manifest (string type and permission). */
interface LooseResource {
type: string;
Expand Down
59 changes: 59 additions & 0 deletions packages/appkit/src/registry/tests/manifest-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { PluginConstructor } from "shared";
import { describe, expect, it } from "vitest";
import { ConfigurationError } from "../../errors";
import {
defineManifest,
getPluginManifest,
getResourceRequirements,
isValidManifest,
Expand Down Expand Up @@ -554,4 +555,62 @@ describe("Manifest Loader", () => {
expect(isValidManifest(valid)).toBe(true);
});
});

describe("defineManifest", () => {
const validManifest = {
name: "test-plugin",
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<"test-plugin">(validManifest);
expect(result.name).toBe("test-plugin");
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();
});
});
});
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from "./cache";
export * from "./execute";
export * from "./genie";
export * from "./plugin";
export { pluginManifestSchema } from "./schemas/manifest";
export * from "./sql";
export * from "./sse/analytics";
export * from "./tunnel";
Loading