Make local plugins manageable and debuggable (#3446)
* feat(plugins): expose Paseo API to local plugins Plugin surfaces borrow the selected host connection while subprocess handlers receive an exclusively owned IPC session, so plugins can use normal workspace and agent operations without inventing another transport. * feat(plugins): reload global enablement safely Treat the global plugin switch as a live daemon setting and teach plugin automation to inspect it before acting. Enabling remains permission-gated because plugin backend code can access the daemon machine and client contributions execute inside the app. * feat(plugins): expose plugin logs Capture plugin stdout and stderr separately from IPC so authors can debug initialization, RPC, cleanup, and crash behavior from the app or CLI. Keep a bounded tail across lifecycle recovery while preserving structured output in the daemon log. * fix(build): order plugin dependency builds * test(plugins): align startup and CLI expectations
This commit is contained in:
@@ -48,6 +48,73 @@ describe("plugin protocol compatibility", () => {
|
||||
).toBe("plugin.directory.install.response");
|
||||
});
|
||||
|
||||
it("uses a namespaced snapshot RPC for structured plugin logs", () => {
|
||||
expect(
|
||||
SessionInboundMessageSchema.parse({
|
||||
type: "plugin.logs.get.request",
|
||||
requestId: "request-logs",
|
||||
pluginId: "example",
|
||||
}),
|
||||
).toEqual({
|
||||
type: "plugin.logs.get.request",
|
||||
requestId: "request-logs",
|
||||
pluginId: "example",
|
||||
});
|
||||
expect(
|
||||
SessionOutboundMessageSchema.parse({
|
||||
type: "plugin.logs.get.response",
|
||||
payload: {
|
||||
requestId: "request-logs",
|
||||
pluginId: "example",
|
||||
entries: [
|
||||
{
|
||||
sequence: 7,
|
||||
timestamp: "2026-08-16T12:00:00.000Z",
|
||||
stream: "stderr",
|
||||
message: "failed to connect",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toEqual({
|
||||
type: "plugin.logs.get.response",
|
||||
payload: {
|
||||
requestId: "request-logs",
|
||||
pluginId: "example",
|
||||
entries: [
|
||||
{
|
||||
sequence: 7,
|
||||
timestamp: "2026-08-16T12:00:00.000Z",
|
||||
stream: "stderr",
|
||||
message: "failed to connect",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the plugin logs capability optional for older server info", () => {
|
||||
const older = StatusMessageSchema.parse({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "server_info",
|
||||
serverId: "older-host",
|
||||
features: { pluginManagement: true },
|
||||
},
|
||||
});
|
||||
const current = StatusMessageSchema.parse({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "server_info",
|
||||
serverId: "current-host",
|
||||
features: { pluginManagement: true, pluginLogs: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(older.payload.features?.pluginLogs).toBeUndefined();
|
||||
expect(current.payload.features?.pluginLogs).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the catalog change notification safe for older clients", () => {
|
||||
expect(
|
||||
StatusMessageSchema.parse({
|
||||
|
||||
@@ -1353,6 +1353,12 @@ export const PluginListRequestSchema = z.object({
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
export const PluginLogsGetRequestSchema = z.object({
|
||||
type: z.literal("plugin.logs.get.request"),
|
||||
requestId: z.string(),
|
||||
pluginId: PluginIdSchema,
|
||||
});
|
||||
|
||||
export const PluginDirectoryInstallRequestSchema = z.object({
|
||||
type: z.literal("plugin.directory.install.request"),
|
||||
requestId: z.string(),
|
||||
@@ -2855,6 +2861,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
||||
DiagnosticsRequestSchema,
|
||||
PluginCatalogGetRequestSchema,
|
||||
PluginListRequestSchema,
|
||||
PluginLogsGetRequestSchema,
|
||||
PluginDirectoryInstallRequestSchema,
|
||||
PluginDirectoryInspectRequestSchema,
|
||||
PluginReloadRequestSchema,
|
||||
@@ -3190,6 +3197,8 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
plugins: z.boolean().optional(),
|
||||
// COMPAT(pluginManagement): added in v0.4.0, remove gate after 2027-08-14.
|
||||
pluginManagement: z.boolean().optional(),
|
||||
// COMPAT(pluginLogs): added in v0.4.0, remove gate after 2027-08-16.
|
||||
pluginLogs: z.boolean().optional(),
|
||||
// COMPAT(terminalRestoreModes): added in v0.1.81, remove gate after 2026-11-23.
|
||||
"terminal-restore-modes": z.boolean().optional(),
|
||||
// COMPAT(terminalInputModeReplay): added in v0.2.6, remove gate after 2027-02-02.
|
||||
@@ -5858,11 +5867,28 @@ export const PluginListItemSchema = z.object({
|
||||
});
|
||||
export type PluginListItem = z.infer<typeof PluginListItemSchema>;
|
||||
|
||||
export const PluginLogEntrySchema = z.object({
|
||||
sequence: z.number().int().nonnegative(),
|
||||
timestamp: z.string().datetime(),
|
||||
stream: z.enum(["stdout", "stderr"]),
|
||||
message: z.string(),
|
||||
});
|
||||
export type PluginLogEntry = z.infer<typeof PluginLogEntrySchema>;
|
||||
|
||||
export const PluginListResponseSchema = z.object({
|
||||
type: z.literal("plugin.list.response"),
|
||||
payload: z.object({ requestId: z.string(), plugins: z.array(PluginListItemSchema) }),
|
||||
});
|
||||
|
||||
export const PluginLogsGetResponseSchema = z.object({
|
||||
type: z.literal("plugin.logs.get.response"),
|
||||
payload: z.object({
|
||||
requestId: z.string(),
|
||||
pluginId: PluginIdSchema,
|
||||
entries: z.array(PluginLogEntrySchema),
|
||||
}),
|
||||
});
|
||||
|
||||
export const PluginDirectoryInstallResponseSchema = z.object({
|
||||
type: z.literal("plugin.directory.install.response"),
|
||||
payload: z.object({ requestId: z.string(), plugin: PluginListItemSchema }),
|
||||
@@ -5905,6 +5931,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
||||
BrowserAutomationExecuteRequestSchema,
|
||||
PluginCatalogGetResponseSchema,
|
||||
PluginListResponseSchema,
|
||||
PluginLogsGetResponseSchema,
|
||||
PluginDirectoryInstallResponseSchema,
|
||||
PluginDirectoryInspectResponseSchema,
|
||||
PluginReloadResponseSchema,
|
||||
|
||||
Reference in New Issue
Block a user