mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-11 19:05:16 +00:00
Enforce shared, client, and server plugin SDK boundaries (#4347)
* fix(plugins): keep React out of the server plugin host * fix(plugins): align SDK guidance and tests with runtime boundaries * fix(plugin): loosen the react peer to ~19.1.0 The plugin SDK pins its react peer to exactly 19.1.0, and that pin is what a downstream `npm i -g @getpaseo/cli` resolves against. So every user ends up with react 19.1.0 in their global node_modules, where corporate dependency scanners flag it: the CVE-2025-55182 advisory range covers 19.1.0 even though the vulnerability itself lives in react-server-dom-webpack/-parcel/-turbopack, none of which the CLI installs. The user is then left hand-patching node_modules or uninstalling Paseo. Widening the peer to ~19.1.0 lets a downstream install pick the latest 19.1.x patch (19.1.9 today) while this repo stays on 19.1.0, which the root override still pins. Keeping the repo itself on 19.1.0 is deliberate. react-native 0.81.5 embeds a 19.1.0 Paper renderer that hard-throws "Incompatible React versions" on any exact mismatch, and it is reachable from Fabric builds too: RendererImplementation.js requires the Paper shim unconditionally in findNodeHandle, unstable_batchedUpdates, sendAccessibilityEvent and three other entry points, and react-native-gesture-handler re-exports findNodeHandle. Bumping the app's react is therefore not safe until react-native moves. The published CLI tree contains no react-native, so downstream installs are not subject to that constraint. Supersedes the earlier 19.1.5/19.1.9 version-bump approach on this branch, which would have broken the mobile app. * Enforce example import ownership including client types * Update Nix dependency hash for plugin peer metadata * Group plugin APIs under client and server entry trees * Resolve plugin type dependencies through declarations * Validate declarations for implicit plugin type imports * Preserve optional imports and validate declaration references * Refresh Nix hash for plugin declaration resolver * Canonicalize plugin paths consistently on Windows * Normalize plugin ownership paths across resolver boundaries --------- Co-authored-by: liujin0506 <liujin0506@qq.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
co-authored by
liujin0506
Mohamed Boudra
parent
d779514f10
commit
1f5b6143d6
@@ -1,4 +1,11 @@
|
||||
import { z } from "zod";
|
||||
import type { PluginAttachmentSourceContribution } from "./contracts.js";
|
||||
|
||||
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
|
||||
definition: Definition,
|
||||
): Definition {
|
||||
return definition;
|
||||
}
|
||||
|
||||
export const PluginAttachmentItemSchema = z.object({
|
||||
id: z.string(),
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const sourceDirectory = path.dirname(fileURLToPath(import.meta.url));
|
||||
const entries = {
|
||||
".": "shared",
|
||||
"./server": "server",
|
||||
"./server/provider": "server",
|
||||
"./server/acp": "server",
|
||||
"./client": "client",
|
||||
"./client/host": "client",
|
||||
"./client/react-native": "client",
|
||||
"./client/ui": "client",
|
||||
} as const;
|
||||
const uiDependency = /^(react(?:-dom|-native)?|use-sync-external-store)(\/|$)/;
|
||||
const serverModule = /^server\//;
|
||||
const clientModule = /^client\//;
|
||||
|
||||
function resolveLocal(importer: string, specifier: string): string {
|
||||
const base = path.resolve(path.dirname(importer), specifier.replace(/\.js$/, ""));
|
||||
const result = [base + ".ts", base + ".tsx"].find(existsSync);
|
||||
if (!result) throw new Error(`Cannot resolve ${specifier} from ${importer}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function boundaryViolations(entry: string, runtime: "shared" | "server" | "client"): string[] {
|
||||
const pending = [entry];
|
||||
const visited = new Set<string>();
|
||||
const violations: string[] = [];
|
||||
while (pending.length) {
|
||||
const file = pending.pop()!;
|
||||
if (visited.has(file)) continue;
|
||||
visited.add(file);
|
||||
// Includes type imports, re-exports, import types, and literal dynamic imports.
|
||||
const imports = ts.preProcessFile(readFileSync(file, "utf8"), true, true).importedFiles;
|
||||
for (const { fileName: specifier } of imports) {
|
||||
const label = `${path.relative(sourceDirectory, file)} -> ${specifier}`;
|
||||
if (specifier.startsWith(".")) {
|
||||
const local = resolveLocal(file, specifier);
|
||||
const module = path
|
||||
.relative(sourceDirectory, local)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(/\.tsx?$/, ".js");
|
||||
if (runtime !== "client" && clientModule.test(module)) violations.push(label);
|
||||
if (runtime !== "server" && serverModule.test(module)) violations.push(label);
|
||||
pending.push(local);
|
||||
} else if (runtime === "shared") {
|
||||
if (specifier !== "zod" && specifier !== "@getpaseo/protocol/agent-types")
|
||||
violations.push(label);
|
||||
} else if (runtime === "server") {
|
||||
if (uiDependency.test(specifier) || /^@getpaseo\/plugin\/client(\/|$)/.test(specifier))
|
||||
violations.push(label);
|
||||
} else if (isBuiltin(specifier) || /^@getpaseo\/plugin\/server(\/|$)/.test(specifier)) {
|
||||
violations.push(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
describe("plugin SDK import boundaries", () => {
|
||||
it("classifies every published entry", () => {
|
||||
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
||||
expect(Object.keys(manifest.exports).sort()).toEqual(Object.keys(entries).sort());
|
||||
});
|
||||
|
||||
it.each(Object.entries(entries))(
|
||||
"%s respects its %s boundary, including type dependencies",
|
||||
(specifier, runtime) => {
|
||||
const name =
|
||||
specifier === "."
|
||||
? "index"
|
||||
: specifier.slice(2) +
|
||||
(specifier === "./client" || specifier === "./server" ? "/index" : "");
|
||||
expect(boundaryViolations(path.join(sourceDirectory, `${name}.ts`), runtime)).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("plugin example import boundaries", () => {
|
||||
const examples = path.resolve(sourceDirectory, "../../../plugin-examples");
|
||||
const files = readdirSync(examples, { recursive: true }).filter((file) =>
|
||||
/(?<!\.test)\.tsx?$/.test(file),
|
||||
);
|
||||
const owner = (file: string) => {
|
||||
const [, directory] = path.relative(examples, file).split(path.sep);
|
||||
if (directory?.startsWith("index.client.")) return "client";
|
||||
if (directory?.startsWith("index.server.")) return "server";
|
||||
return directory;
|
||||
};
|
||||
|
||||
it.each(files)("%s respects runtime ownership, including type imports", (relative) => {
|
||||
const file = path.join(examples, relative);
|
||||
const runtime = owner(file);
|
||||
expect(["shared", "client", "server"]).toContain(runtime);
|
||||
const violations: string[] = [];
|
||||
for (const { fileName: specifier } of ts.preProcessFile(readFileSync(file, "utf8"), true, true)
|
||||
.importedFiles) {
|
||||
if (specifier.startsWith(".")) {
|
||||
const imported = owner(resolveLocal(file, specifier));
|
||||
if (imported !== "shared" && imported !== runtime) violations.push(specifier);
|
||||
} else if (specifier.startsWith("@getpaseo/plugin")) {
|
||||
const entry = specifier.replace("@getpaseo/plugin", ".") as keyof typeof entries;
|
||||
if (
|
||||
entry === "./client/host" ||
|
||||
!(entry in entries) ||
|
||||
(entries[entry] !== "shared" && entries[entry] !== runtime)
|
||||
)
|
||||
violations.push(specifier);
|
||||
} else if (
|
||||
(runtime !== "client" && uiDependency.test(specifier)) ||
|
||||
(runtime !== "server" && isBuiltin(specifier))
|
||||
) {
|
||||
violations.push(specifier);
|
||||
}
|
||||
}
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useContext, type ReactNode } from "react";
|
||||
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
|
||||
import type { PluginAgentSnapshot, PluginWorkspaceSnapshot } from "./contracts.js";
|
||||
import type { PluginAgentSnapshot, PluginWorkspaceSnapshot } from "../contracts.js";
|
||||
import { shallowEqual } from "./shallow.js";
|
||||
|
||||
export interface PluginClientStateSource {
|
||||
@@ -0,0 +1,246 @@
|
||||
import type { ComponentType } from "react";
|
||||
import type { PaseoApi } from "@getpaseo/client";
|
||||
import type { AgentTimelineItem } from "@getpaseo/protocol/agent-types";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import type { PluginRpcContract } from "../rpc.js";
|
||||
import type {
|
||||
PluginTheme,
|
||||
PluginWorkspaceSnapshot,
|
||||
PluginAgentSnapshot,
|
||||
PluginThemeContribution,
|
||||
PluginAttachmentSourceContribution,
|
||||
PluginTimelineTransformResult,
|
||||
PluginCleanup,
|
||||
} from "../contracts.js";
|
||||
|
||||
export interface PluginHostProps {
|
||||
theme: PluginTheme;
|
||||
host: {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
layout: {
|
||||
compact: boolean;
|
||||
platform: "ios" | "android" | "web";
|
||||
};
|
||||
}
|
||||
|
||||
interface PluginNavigableHostProps extends PluginHostProps {
|
||||
/** Client-owned navigation. Undefined on older hosts; hide dependent affordances when absent. */
|
||||
readonly navigation?: {
|
||||
readonly openAgent: (input: { readonly agentId: string }) => void;
|
||||
readonly openWorkspace: (input: { readonly workspaceId: string }) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginSurfaceProps extends PluginNavigableHostProps {}
|
||||
|
||||
export interface PluginIconProps {
|
||||
name: string;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export type PluginPanelLocation = "workspace" | "explorer";
|
||||
|
||||
export interface PluginOpenPanelOptions {
|
||||
location?: PluginPanelLocation;
|
||||
}
|
||||
|
||||
interface PluginWorkspacePanelBase {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
locations?: readonly PluginPanelLocation[];
|
||||
}
|
||||
|
||||
export interface PluginWorkspacePanelProps extends PluginNavigableHostProps {
|
||||
context: "workspace";
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface PluginAgentPanelProps extends PluginNavigableHostProps {
|
||||
context: "agent";
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export interface PluginComposerPillProps extends PluginHostProps {
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export interface PluginComposerPillContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
Component: ComponentType<PluginComposerPillProps>;
|
||||
onPress(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginClientOpenPanelOptions extends PluginOpenPanelOptions {
|
||||
workspaceId: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export interface PluginClientContext extends PluginCommandCapabilities {
|
||||
addSettingsScreen(contribution: PluginSettingsScreenContribution): PluginCleanup;
|
||||
addSurface(id: string, Component: ComponentType<PluginSurfaceProps>): PluginCleanup;
|
||||
addSidebarItem(contribution: PluginSidebarContribution): PluginCleanup;
|
||||
addWorkspacePanel(contribution: PluginWorkspacePanelContribution): PluginCleanup;
|
||||
addCommandCenterItem(contribution: PluginCommandCenterItemContribution): PluginCleanup;
|
||||
addSlashCommand(contribution: PluginClientSlashCommandContribution): PluginCleanup;
|
||||
addComposerPill(contribution: PluginComposerPillContribution): PluginCleanup;
|
||||
addAttachmentSource(contribution: PluginAttachmentSourceContribution): PluginCleanup;
|
||||
addTheme(contribution: PluginThemeContribution): PluginCleanup;
|
||||
addTimelineTransformer<ItemType extends AgentTimelineItem["type"]>(
|
||||
contribution: PluginTimelineTransformerContribution<ItemType>,
|
||||
): PluginCleanup;
|
||||
addTimelineRenderer<Schema extends ZodType>(
|
||||
contribution: PluginTimelineRendererContribution<Schema>,
|
||||
): PluginCleanup;
|
||||
openPanel(id: string, options: PluginClientOpenPanelOptions): void;
|
||||
}
|
||||
|
||||
export type PluginClientContribution = (client: PluginClientContext) => PluginCleanup;
|
||||
|
||||
export type PluginWorkspacePanelContribution =
|
||||
| (PluginWorkspacePanelBase & {
|
||||
context: "workspace";
|
||||
Component: ComponentType<PluginWorkspacePanelProps>;
|
||||
})
|
||||
| (PluginWorkspacePanelBase & {
|
||||
context: "agent";
|
||||
Component: ComponentType<PluginAgentPanelProps>;
|
||||
});
|
||||
|
||||
export interface PluginSettingsScreenContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
Component: ComponentType<PluginSurfaceProps>;
|
||||
}
|
||||
|
||||
export interface PluginSurfaceContribution {
|
||||
id: string;
|
||||
Component: ComponentType<PluginSurfaceProps>;
|
||||
}
|
||||
|
||||
export interface PluginSidebarContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
surface: string;
|
||||
}
|
||||
|
||||
export type PluginTimelineTransformerContribution<
|
||||
ItemType extends AgentTimelineItem["type"] = AgentTimelineItem["type"],
|
||||
> = ItemType extends AgentTimelineItem["type"]
|
||||
? {
|
||||
id: string;
|
||||
query: {
|
||||
itemType: ItemType;
|
||||
};
|
||||
transform(input: {
|
||||
item: Extract<AgentTimelineItem, { type: ItemType }>;
|
||||
phase: "streaming" | "complete";
|
||||
}): PluginTimelineTransformResult | undefined;
|
||||
}
|
||||
: never;
|
||||
|
||||
export interface PluginTimelineItemProps<Data = unknown> extends PluginHostProps {
|
||||
agentId: string;
|
||||
item: {
|
||||
type: "plugin";
|
||||
kind: string;
|
||||
version: number;
|
||||
data: Data;
|
||||
};
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface PluginTimelineRendererContribution<Schema extends ZodType = ZodType> {
|
||||
kind: string;
|
||||
version: number;
|
||||
schema: Schema;
|
||||
Component: ComponentType<PluginTimelineItemProps<ZodOutput<Schema>>>;
|
||||
}
|
||||
|
||||
export interface PluginCommandCapabilities {
|
||||
paseo: PaseoApi;
|
||||
rpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
input: ZodInput<InputSchema>,
|
||||
): Promise<ZodOutput<OutputSchema>>;
|
||||
openSurface(id: string): void;
|
||||
openSettings(id: string): void;
|
||||
}
|
||||
|
||||
export interface PluginGlobalCommandContext extends PluginCommandCapabilities {
|
||||
context: "global";
|
||||
}
|
||||
|
||||
export interface PluginWorkspaceCommandContext extends PluginCommandCapabilities {
|
||||
context: "workspace";
|
||||
workspace: PluginWorkspaceSnapshot;
|
||||
openPanel(id: string, options?: PluginOpenPanelOptions): void;
|
||||
}
|
||||
|
||||
export interface PluginAgentCommandContext extends PluginCommandCapabilities {
|
||||
context: "agent";
|
||||
workspace: PluginWorkspaceSnapshot;
|
||||
agent: PluginAgentSnapshot;
|
||||
openPanel(id: string, options?: PluginOpenPanelOptions): void;
|
||||
}
|
||||
|
||||
interface PluginCommandCenterItemBase {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
keywords?: readonly string[];
|
||||
}
|
||||
|
||||
export type PluginCommandCenterItemContribution =
|
||||
| (PluginCommandCenterItemBase & {
|
||||
context: "global";
|
||||
onSelect(context: PluginGlobalCommandContext): void | Promise<void>;
|
||||
})
|
||||
| (PluginCommandCenterItemBase & {
|
||||
context: "workspace";
|
||||
onSelect(context: PluginWorkspaceCommandContext): void | Promise<void>;
|
||||
})
|
||||
| (PluginCommandCenterItemBase & {
|
||||
context: "agent";
|
||||
onSelect(context: PluginAgentCommandContext): void | Promise<void>;
|
||||
});
|
||||
|
||||
interface PluginClientSlashCommandBase {
|
||||
name: string;
|
||||
description: string;
|
||||
argumentHint: string;
|
||||
}
|
||||
|
||||
export type PluginClientSlashCommandContribution =
|
||||
| (PluginClientSlashCommandBase & {
|
||||
context: "workspace";
|
||||
onSubmit(context: PluginWorkspaceCommandContext & { args: string }): void | Promise<void>;
|
||||
})
|
||||
| (PluginClientSlashCommandBase & {
|
||||
context: "agent";
|
||||
onSubmit(context: PluginAgentCommandContext & { args: string }): void | Promise<void>;
|
||||
});
|
||||
|
||||
export type SettingsState<Schema extends ZodType> = (
|
||||
| { status: "loading" }
|
||||
| { status: "error"; error: string }
|
||||
| { status: "invalid"; error: string; revision: string }
|
||||
| { status: "ready"; values: ZodOutput<Schema>; revision: string }
|
||||
) & {
|
||||
saving: boolean;
|
||||
saveError: string | null;
|
||||
/** Save an entire document against the revision currently displayed. Never throws. */
|
||||
save(values: ZodOutput<Schema>, revision: string): Promise<boolean>;
|
||||
reset(): Promise<boolean>;
|
||||
reload(): Promise<void>;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { defineAttachmentSource, defineRpc, type RpcInput, type RpcOutput } from "./index.js";
|
||||
import { defineAttachmentSource, defineRpc, type RpcInput, type RpcOutput } from "../index.js";
|
||||
import { callPluginRpc, searchPluginAttachments } from "./host.js";
|
||||
|
||||
const contract = defineRpc({
|
||||
@@ -1,14 +1,13 @@
|
||||
export { settingsRpc } from "./settings.js";
|
||||
import { PluginAttachmentSearchPayloadSchema } from "./attachments.js";
|
||||
import { PluginAttachmentSearchPayloadSchema } from "../attachments.js";
|
||||
export { PluginClientStateProvider, type PluginClientStateSource } from "./client-state.js";
|
||||
export {
|
||||
usePluginRuntimeContextBridge,
|
||||
type PluginRuntimeContextBridge,
|
||||
} from "./runtime-context-bridge.js";
|
||||
import type { PluginAttachmentSourceContribution } from "./contracts.js";
|
||||
import type { PluginAttachmentSourceContribution } from "../contracts.js";
|
||||
import { PluginRpcProvider } from "./rpc-context.js";
|
||||
import { PaseoApiProvider } from "./paseo-context.js";
|
||||
import { callPluginRpc } from "./rpc.js";
|
||||
import { callPluginRpc } from "../rpc.js";
|
||||
|
||||
export async function searchPluginAttachments(
|
||||
source: PluginAttachmentSourceContribution,
|
||||
@@ -0,0 +1,37 @@
|
||||
export type {
|
||||
PluginHostProps,
|
||||
PluginSurfaceProps,
|
||||
PluginIconProps,
|
||||
PluginPanelLocation,
|
||||
PluginOpenPanelOptions,
|
||||
PluginWorkspacePanelProps,
|
||||
PluginAgentPanelProps,
|
||||
PluginComposerPillProps,
|
||||
PluginComposerPillContribution,
|
||||
PluginClientOpenPanelOptions,
|
||||
PluginClientContext,
|
||||
PluginClientContribution,
|
||||
PluginWorkspacePanelContribution,
|
||||
PluginSettingsScreenContribution,
|
||||
PluginSurfaceContribution,
|
||||
PluginSidebarContribution,
|
||||
PluginTimelineTransformerContribution,
|
||||
PluginTimelineItemProps,
|
||||
PluginTimelineRendererContribution,
|
||||
PluginCommandCapabilities,
|
||||
PluginGlobalCommandContext,
|
||||
PluginWorkspaceCommandContext,
|
||||
PluginAgentCommandContext,
|
||||
PluginCommandCenterItemContribution,
|
||||
PluginClientSlashCommandContribution,
|
||||
SettingsState,
|
||||
} from "./contracts.js";
|
||||
export { usePaseo } from "./paseo-context.js";
|
||||
export { useAgent, useWorkspace } from "./client-state.js";
|
||||
export { useRpc } from "./rpc-context.js";
|
||||
import type { SettingsDefinition } from "../settings.js";
|
||||
import type { SettingsState } from "./contracts.js";
|
||||
import type { ZodType } from "zod";
|
||||
export declare function useSettings<Schema extends ZodType>(
|
||||
definition: SettingsDefinition<Schema>,
|
||||
): SettingsState<Schema>;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createContext, useCallback, useContext, useMemo, type ReactNode } from "react";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import { callPluginRpc, type PluginRpcContract } from "./rpc.js";
|
||||
import { callPluginRpc, type PluginRpcContract } from "../rpc.js";
|
||||
|
||||
export interface PluginRpcContextValue {
|
||||
invoke(method: string, input: unknown): Promise<unknown>;
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { PluginLifecycleRegistration } from "./lifecycle.js";
|
||||
import type { ComponentType } from "react";
|
||||
import type { PaseoApi } from "@getpaseo/client";
|
||||
import type { AgentTimelineItem, JsonValue } from "@getpaseo/protocol/agent-types";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import type { JsonValue } from "@getpaseo/protocol/agent-types";
|
||||
import type { PluginRpcContract } from "./rpc.js";
|
||||
import type { ProviderRegistration } from "./provider.js";
|
||||
|
||||
export interface PluginTheme {
|
||||
readonly colors: {
|
||||
@@ -22,34 +17,6 @@ export interface PluginTheme {
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginHostProps {
|
||||
theme: PluginTheme;
|
||||
host: {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
layout: {
|
||||
compact: boolean;
|
||||
platform: "ios" | "android" | "web";
|
||||
};
|
||||
}
|
||||
|
||||
interface PluginNavigableHostProps extends PluginHostProps {
|
||||
/** Client-owned navigation. Undefined on older hosts; hide dependent affordances when absent. */
|
||||
readonly navigation?: {
|
||||
readonly openAgent: (input: { readonly agentId: string }) => void;
|
||||
readonly openWorkspace: (input: { readonly workspaceId: string }) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PluginSurfaceProps extends PluginNavigableHostProps {}
|
||||
|
||||
export interface PluginIconProps {
|
||||
name: string;
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface PluginWorkspaceSnapshot {
|
||||
readonly id: string;
|
||||
readonly projectId: string;
|
||||
@@ -85,99 +52,6 @@ export interface PluginAgentSnapshot {
|
||||
readonly labels: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export type PluginPanelLocation = "workspace" | "explorer";
|
||||
|
||||
export interface PluginOpenPanelOptions {
|
||||
location?: PluginPanelLocation;
|
||||
}
|
||||
|
||||
interface PluginWorkspacePanelBase {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
locations?: readonly PluginPanelLocation[];
|
||||
}
|
||||
|
||||
export interface PluginWorkspacePanelProps extends PluginNavigableHostProps {
|
||||
context: "workspace";
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
export interface PluginAgentPanelProps extends PluginNavigableHostProps {
|
||||
context: "agent";
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export interface PluginComposerPillProps extends PluginHostProps {
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export interface PluginComposerPillContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
Component: ComponentType<PluginComposerPillProps>;
|
||||
onPress(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface PluginClientOpenPanelOptions extends PluginOpenPanelOptions {
|
||||
workspaceId: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export interface PluginClientContext extends PluginCommandCapabilities {
|
||||
addSettingsScreen(contribution: PluginSettingsScreenContribution): PluginCleanup;
|
||||
addSurface(id: string, Component: ComponentType<PluginSurfaceProps>): PluginCleanup;
|
||||
addSidebarItem(contribution: PluginSidebarContribution): PluginCleanup;
|
||||
addWorkspacePanel(contribution: PluginWorkspacePanelContribution): PluginCleanup;
|
||||
addCommandCenterItem(contribution: PluginCommandCenterItemContribution): PluginCleanup;
|
||||
addSlashCommand(contribution: PluginClientSlashCommandContribution): PluginCleanup;
|
||||
addComposerPill(contribution: PluginComposerPillContribution): PluginCleanup;
|
||||
addAttachmentSource(contribution: PluginAttachmentSourceContribution): PluginCleanup;
|
||||
addTheme(contribution: PluginThemeContribution): PluginCleanup;
|
||||
addTimelineTransformer<ItemType extends AgentTimelineItem["type"]>(
|
||||
contribution: PluginTimelineTransformerContribution<ItemType>,
|
||||
): PluginCleanup;
|
||||
addTimelineRenderer<Schema extends ZodType>(
|
||||
contribution: PluginTimelineRendererContribution<Schema>,
|
||||
): PluginCleanup;
|
||||
openPanel(id: string, options: PluginClientOpenPanelOptions): void;
|
||||
}
|
||||
|
||||
export type PluginClientContribution = (client: PluginClientContext) => PluginCleanup;
|
||||
|
||||
export type PluginWorkspacePanelContribution =
|
||||
| (PluginWorkspacePanelBase & {
|
||||
context: "workspace";
|
||||
Component: ComponentType<PluginWorkspacePanelProps>;
|
||||
})
|
||||
| (PluginWorkspacePanelBase & {
|
||||
context: "agent";
|
||||
Component: ComponentType<PluginAgentPanelProps>;
|
||||
});
|
||||
|
||||
export interface PluginSettingsScreenContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
Component: ComponentType<PluginSurfaceProps>;
|
||||
}
|
||||
|
||||
export interface PluginSurfaceContribution {
|
||||
id: string;
|
||||
Component: ComponentType<PluginSurfaceProps>;
|
||||
}
|
||||
|
||||
export interface PluginSidebarContribution {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
surface: string;
|
||||
}
|
||||
|
||||
export interface PluginThemeColors {
|
||||
background: string;
|
||||
foreground: string;
|
||||
@@ -219,121 +93,4 @@ export interface PluginTimelineTransformResult {
|
||||
items: PluginTimelineItem[];
|
||||
}
|
||||
|
||||
export type PluginTimelineTransformerContribution<
|
||||
ItemType extends AgentTimelineItem["type"] = AgentTimelineItem["type"],
|
||||
> = ItemType extends AgentTimelineItem["type"]
|
||||
? {
|
||||
id: string;
|
||||
query: {
|
||||
itemType: ItemType;
|
||||
};
|
||||
transform(input: {
|
||||
item: Extract<AgentTimelineItem, { type: ItemType }>;
|
||||
phase: "streaming" | "complete";
|
||||
}): PluginTimelineTransformResult | undefined;
|
||||
}
|
||||
: never;
|
||||
|
||||
export interface PluginTimelineItemProps<Data = unknown> extends PluginHostProps {
|
||||
agentId: string;
|
||||
item: {
|
||||
type: "plugin";
|
||||
kind: string;
|
||||
version: number;
|
||||
data: Data;
|
||||
};
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface PluginTimelineRendererContribution<Schema extends ZodType = ZodType> {
|
||||
kind: string;
|
||||
version: number;
|
||||
schema: Schema;
|
||||
Component: ComponentType<PluginTimelineItemProps<ZodOutput<Schema>>>;
|
||||
}
|
||||
|
||||
export interface PluginCommandCapabilities {
|
||||
paseo: PaseoApi;
|
||||
rpc<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
input: ZodInput<InputSchema>,
|
||||
): Promise<ZodOutput<OutputSchema>>;
|
||||
openSurface(id: string): void;
|
||||
openSettings(id: string): void;
|
||||
}
|
||||
|
||||
export interface PluginGlobalCommandContext extends PluginCommandCapabilities {
|
||||
context: "global";
|
||||
}
|
||||
|
||||
export interface PluginWorkspaceCommandContext extends PluginCommandCapabilities {
|
||||
context: "workspace";
|
||||
workspace: PluginWorkspaceSnapshot;
|
||||
openPanel(id: string, options?: PluginOpenPanelOptions): void;
|
||||
}
|
||||
|
||||
export interface PluginAgentCommandContext extends PluginCommandCapabilities {
|
||||
context: "agent";
|
||||
workspace: PluginWorkspaceSnapshot;
|
||||
agent: PluginAgentSnapshot;
|
||||
openPanel(id: string, options?: PluginOpenPanelOptions): void;
|
||||
}
|
||||
|
||||
interface PluginCommandCenterItemBase {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
keywords?: readonly string[];
|
||||
}
|
||||
|
||||
export type PluginCommandCenterItemContribution =
|
||||
| (PluginCommandCenterItemBase & {
|
||||
context: "global";
|
||||
onSelect(context: PluginGlobalCommandContext): void | Promise<void>;
|
||||
})
|
||||
| (PluginCommandCenterItemBase & {
|
||||
context: "workspace";
|
||||
onSelect(context: PluginWorkspaceCommandContext): void | Promise<void>;
|
||||
})
|
||||
| (PluginCommandCenterItemBase & {
|
||||
context: "agent";
|
||||
onSelect(context: PluginAgentCommandContext): void | Promise<void>;
|
||||
});
|
||||
|
||||
interface PluginClientSlashCommandBase {
|
||||
name: string;
|
||||
description: string;
|
||||
argumentHint: string;
|
||||
}
|
||||
|
||||
export type PluginClientSlashCommandContribution =
|
||||
| (PluginClientSlashCommandBase & {
|
||||
context: "workspace";
|
||||
onSubmit(context: PluginWorkspaceCommandContext & { args: string }): void | Promise<void>;
|
||||
})
|
||||
| (PluginClientSlashCommandBase & {
|
||||
context: "agent";
|
||||
onSubmit(context: PluginAgentCommandContext & { args: string }): void | Promise<void>;
|
||||
});
|
||||
|
||||
export interface PluginHandlerContext {
|
||||
paseo: PaseoApi;
|
||||
}
|
||||
|
||||
export interface PluginServerContext extends PluginLifecycleRegistration {
|
||||
registerSettings<Schema extends ZodType>(
|
||||
definition: import("./settings.js").SettingsDefinition<Schema>,
|
||||
): void;
|
||||
handle<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
handler: (
|
||||
input: ZodOutput<InputSchema>,
|
||||
context: PluginHandlerContext,
|
||||
) => ZodInput<OutputSchema> | Promise<ZodInput<OutputSchema>>,
|
||||
): void;
|
||||
registerProvider(provider: ProviderRegistration): void;
|
||||
}
|
||||
|
||||
export type PluginCleanup = () => void | Promise<void>;
|
||||
|
||||
export type PluginServerContribution = (server: PluginServerContext) => PluginCleanup;
|
||||
|
||||
@@ -1,79 +1,22 @@
|
||||
import type { SettingsDefinition, SettingsState } from "./settings.js";
|
||||
import type { ZodType } from "zod";
|
||||
export { defineSettings, type SettingsDefinition, type SettingsState } from "./settings.js";
|
||||
export declare function useSettings<Schema extends ZodType>(
|
||||
definition: SettingsDefinition<Schema>,
|
||||
): SettingsState<Schema>;
|
||||
|
||||
import type { ComponentType } from "react";
|
||||
import type { PluginAttachmentSourceContribution, PluginIconProps } from "./contracts.js";
|
||||
|
||||
// Shared SDK entry. Keep runtime-specific imports and re-exports on /client or /server.
|
||||
export type {
|
||||
PluginTheme,
|
||||
PluginWorkspaceSnapshot,
|
||||
PluginAgentSnapshot,
|
||||
PluginThemeColors,
|
||||
PluginThemeContribution,
|
||||
PluginAttachmentSourceContribution,
|
||||
PluginTimelineData,
|
||||
PluginTimelineItem,
|
||||
PluginTimelineTransformResult,
|
||||
PluginCleanup,
|
||||
} from "./contracts.js";
|
||||
export { defineSettings, settingsRpc, type SettingsDefinition } from "./settings.js";
|
||||
export {
|
||||
defineAttachmentSource,
|
||||
PluginAttachmentItemSchema,
|
||||
PluginAttachmentSearchPayloadSchema,
|
||||
type PluginAttachmentItem,
|
||||
type PluginAttachmentSearchPayload,
|
||||
} from "./attachments.js";
|
||||
export { defineRpc, type PluginRpcContract, type RpcInput, type RpcOutput } from "./rpc.js";
|
||||
|
||||
export function defineAttachmentSource<Definition extends PluginAttachmentSourceContribution>(
|
||||
definition: Definition,
|
||||
): Definition {
|
||||
return definition;
|
||||
}
|
||||
|
||||
export declare const Icon: ComponentType<PluginIconProps>;
|
||||
export type {
|
||||
PluginAttachmentSourceContribution,
|
||||
PluginAgentCommandContext,
|
||||
PluginAgentPanelProps,
|
||||
PluginAgentSnapshot,
|
||||
PluginCleanup,
|
||||
PluginCommandCapabilities,
|
||||
PluginCommandCenterItemContribution,
|
||||
PluginClientContext,
|
||||
PluginClientContribution,
|
||||
PluginClientSlashCommandContribution,
|
||||
PluginClientOpenPanelOptions,
|
||||
PluginComposerPillContribution,
|
||||
PluginComposerPillProps,
|
||||
PluginServerContribution,
|
||||
PluginServerContext,
|
||||
PluginGlobalCommandContext,
|
||||
PluginHandlerContext,
|
||||
PluginHostProps,
|
||||
PluginOpenPanelOptions,
|
||||
PluginIconProps,
|
||||
PluginPanelLocation,
|
||||
PluginTheme,
|
||||
PluginSidebarContribution,
|
||||
PluginSurfaceContribution,
|
||||
PluginSettingsScreenContribution,
|
||||
PluginSurfaceProps,
|
||||
PluginThemeColors,
|
||||
PluginThemeContribution,
|
||||
PluginTimelineData,
|
||||
PluginTimelineItem,
|
||||
PluginTimelineItemProps,
|
||||
PluginTimelineRendererContribution,
|
||||
PluginTimelineTransformerContribution,
|
||||
PluginTimelineTransformResult,
|
||||
PluginWorkspaceCommandContext,
|
||||
PluginWorkspacePanelContribution,
|
||||
PluginWorkspacePanelProps,
|
||||
PluginWorkspaceSnapshot,
|
||||
} from "./contracts.js";
|
||||
export { usePaseo } from "./paseo-context.js";
|
||||
export { useAgent, useWorkspace } from "./client-state.js";
|
||||
export { useRpc } from "./rpc-context.js";
|
||||
|
||||
export type {
|
||||
PluginBeforeRequests,
|
||||
PluginHookAgent,
|
||||
PluginHookContext,
|
||||
PluginHookWorkspace,
|
||||
PluginLifecycleEvents,
|
||||
PluginLifecycleRegistration,
|
||||
PluginSessionOpenRequest,
|
||||
PluginTurnOutcome,
|
||||
} from "./lifecycle.js";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("plugin runtime-entry migration guide", () => {
|
||||
it("maps every client registration method", async () => {
|
||||
const contracts = await readFile(new URL("./contracts.ts", import.meta.url), "utf8");
|
||||
const contracts = await readFile(new URL("./client/contracts.ts", import.meta.url), "utf8");
|
||||
const context = /export interface PluginClientContext[^{]*{([\s\S]*?)\n}/.exec(contracts)?.[1];
|
||||
if (!context) throw new Error("PluginClientContext was not found");
|
||||
const methods = [...context.matchAll(/^\s+(add[A-Z]\w*)\(/gm)].map((match) => match[1]);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const srcDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const forbiddenSpecifiers = new Set([
|
||||
"react",
|
||||
"react/jsx-runtime",
|
||||
"react-native",
|
||||
"use-sync-external-store",
|
||||
"use-sync-external-store/shim/with-selector",
|
||||
]);
|
||||
|
||||
function localFile(fromFile: string, specifier: string): string {
|
||||
const withoutJs = specifier.replace(/\.js$/, "");
|
||||
for (const candidate of [`${withoutJs}.ts`, `${withoutJs}.tsx`, withoutJs]) {
|
||||
const resolved = path.resolve(path.dirname(fromFile), candidate);
|
||||
if (existsSync(resolved)) return resolved;
|
||||
}
|
||||
throw new Error(`Could not resolve ${specifier} from ${fromFile}`);
|
||||
}
|
||||
|
||||
function valueImportSpecifiers(source: string): string[] {
|
||||
const specifiers: string[] = [];
|
||||
const pattern = /(?:^|\n)(?:export|import)\s+(?!type\b)(?:[\s\S]*?)from\s+["']([^"']+)["']/g;
|
||||
for (const match of source.matchAll(pattern)) {
|
||||
if (match[1]) specifiers.push(match[1]);
|
||||
}
|
||||
return specifiers;
|
||||
}
|
||||
|
||||
function collectBareSpecifiers(entry: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
const bare: string[] = [];
|
||||
const queue = [entry];
|
||||
while (queue.length > 0) {
|
||||
const file = queue.pop();
|
||||
if (!file || seen.has(file)) continue;
|
||||
seen.add(file);
|
||||
for (const specifier of valueImportSpecifiers(readFileSync(file, "utf8"))) {
|
||||
if (specifier.startsWith(".")) {
|
||||
queue.push(localFile(file, specifier));
|
||||
continue;
|
||||
}
|
||||
bare.push(specifier);
|
||||
}
|
||||
}
|
||||
return bare;
|
||||
}
|
||||
|
||||
describe("@getpaseo/plugin/server", () => {
|
||||
it("does not load react or the client hook graph", () => {
|
||||
const specifiers = collectBareSpecifiers(path.join(srcDir, "server.ts"));
|
||||
expect(specifiers.filter((specifier) => forbiddenSpecifiers.has(specifier))).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { PaseoApi } from "@getpaseo/client";
|
||||
import type { ZodType, input as ZodInput, output as ZodOutput } from "zod";
|
||||
import type { PluginRpcContract } from "../rpc.js";
|
||||
import type { PluginCleanup } from "../contracts.js";
|
||||
import type { ProviderRegistration } from "./provider.js";
|
||||
import type { PluginLifecycleRegistration } from "./lifecycle.js";
|
||||
|
||||
export interface PluginHandlerContext {
|
||||
paseo: PaseoApi;
|
||||
}
|
||||
|
||||
export interface PluginServerContext extends PluginLifecycleRegistration {
|
||||
registerSettings<Schema extends ZodType>(
|
||||
definition: import("../settings.js").SettingsDefinition<Schema>,
|
||||
): void;
|
||||
handle<InputSchema extends ZodType, OutputSchema extends ZodType>(
|
||||
contract: PluginRpcContract<InputSchema, OutputSchema>,
|
||||
handler: (
|
||||
input: ZodOutput<InputSchema>,
|
||||
context: PluginHandlerContext,
|
||||
) => ZodInput<OutputSchema> | Promise<ZodInput<OutputSchema>>,
|
||||
): void;
|
||||
registerProvider(provider: ProviderRegistration): void;
|
||||
}
|
||||
|
||||
export type PluginServerContribution = (server: PluginServerContext) => PluginCleanup;
|
||||
@@ -1,12 +1,15 @@
|
||||
export type { PluginHandlerContext } from "./contracts.js";
|
||||
|
||||
export type {
|
||||
PluginBeforeRequests,
|
||||
PluginHookAgent,
|
||||
PluginHandlerContext,
|
||||
PluginServerContext,
|
||||
PluginServerContribution,
|
||||
} from "./contracts.js";
|
||||
export type {
|
||||
PluginHookContext,
|
||||
PluginHookWorkspace,
|
||||
PluginLifecycleEvents,
|
||||
PluginLifecycleRegistration,
|
||||
PluginHookAgent,
|
||||
PluginSessionOpenRequest,
|
||||
PluginTurnOutcome,
|
||||
PluginLifecycleEvents,
|
||||
PluginBeforeRequests,
|
||||
PluginLifecycleRegistration,
|
||||
} from "./lifecycle.js";
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import type { output, ZodType } from "zod";
|
||||
import type { ZodType } from "zod";
|
||||
import { defineRpc } from "./rpc.js";
|
||||
|
||||
export interface SettingsDefinition<Schema extends ZodType = ZodType> {
|
||||
@@ -49,17 +49,3 @@ export function settingsRpc(id: string) {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export type SettingsState<Schema extends ZodType> = (
|
||||
| { status: "loading" }
|
||||
| { status: "error"; error: string }
|
||||
| { status: "invalid"; error: string; revision: string }
|
||||
| { status: "ready"; values: output<Schema>; revision: string }
|
||||
) & {
|
||||
saving: boolean;
|
||||
saveError: string | null;
|
||||
/** Save an entire document against the revision currently displayed. Never throws. */
|
||||
save(values: output<Schema>, revision: string): Promise<boolean>;
|
||||
reset(): Promise<boolean>;
|
||||
reload(): Promise<void>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user