mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
Merge remote-tracking branch 'origin/master' into codex/pap-9980-plugin-runtime-hardening
# Conflicts: # packages/plugins/sdk/src/host-client-factory.ts # packages/plugins/sdk/src/protocol.ts # packages/plugins/sdk/src/worker-rpc-host.ts # packages/plugins/sdk/tests/host-client-factory.test.ts # packages/plugins/sdk/tests/worker-rpc-host.test.ts # server/src/__tests__/fixtures/plugin-worker-invocation-scope.cjs # server/src/__tests__/plugin-worker-manager.test.ts # server/src/routes/plugins.ts # server/src/services/plugin-worker-manager.ts
This commit is contained in:
commit
4161c63bc0
101 files changed
+6609
-605
No files matched your search
@@ -739,6 +739,11 @@ export const PLUGIN_CAPABILITIES = [
|
||||
"activity.read",
|
||||
"costs.read",
|
||||
"issues.orchestration.read",
|
||||
"access.members.read",
|
||||
"access.invites.read",
|
||||
"authorization.grants.read",
|
||||
"authorization.policies.read",
|
||||
"authorization.audit.read",
|
||||
"database.namespace.read",
|
||||
// Data Write
|
||||
"issues.create",
|
||||
@@ -756,6 +761,10 @@ export const PLUGIN_CAPABILITIES = [
|
||||
"agents.resume",
|
||||
"agents.invoke",
|
||||
"agents.managed",
|
||||
"access.members.write",
|
||||
"access.invites.write",
|
||||
"authorization.grants.write",
|
||||
"authorization.policies.write",
|
||||
"agent.sessions.create",
|
||||
"agent.sessions.list",
|
||||
"agent.sessions.send",
|
||||
@@ -857,6 +866,7 @@ export const PLUGIN_UI_SLOT_TYPES = [
|
||||
"commentAnnotation",
|
||||
"commentContextMenuItem",
|
||||
"settingsPage",
|
||||
"companySettingsPage",
|
||||
] as const;
|
||||
export type PluginUiSlotType = (typeof PLUGIN_UI_SLOT_TYPES)[number];
|
||||
|
||||
@@ -887,6 +897,21 @@ export const PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS = [
|
||||
export type PluginReservedCompanyRouteSegment =
|
||||
(typeof PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS)[number];
|
||||
|
||||
/**
|
||||
* Reserved route segments under `/:companyPrefix/company/settings/...` that
|
||||
* plugin company settings pages may not claim.
|
||||
*/
|
||||
export const PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS = [
|
||||
"general",
|
||||
"environments",
|
||||
"access",
|
||||
"members",
|
||||
"invites",
|
||||
"secrets",
|
||||
] as const;
|
||||
export type PluginReservedCompanySettingsRouteSegment =
|
||||
(typeof PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS)[number];
|
||||
|
||||
/**
|
||||
* Launcher placement zones describe where a plugin-owned launcher can appear
|
||||
* in the host UI. These are intentionally aligned with current slot surfaces
|
||||
|
||||
@@ -111,6 +111,7 @@ export {
|
||||
PLUGIN_CAPABILITIES,
|
||||
PLUGIN_UI_SLOT_TYPES,
|
||||
PLUGIN_UI_SLOT_ENTITY_TYPES,
|
||||
PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS,
|
||||
PLUGIN_LAUNCHER_PLACEMENT_ZONES,
|
||||
PLUGIN_LAUNCHER_ACTIONS,
|
||||
PLUGIN_LAUNCHER_BOUNDS,
|
||||
@@ -226,6 +227,7 @@ export {
|
||||
type PluginCapability,
|
||||
type PluginUiSlotType,
|
||||
type PluginUiSlotEntityType,
|
||||
type PluginReservedCompanySettingsRouteSegment,
|
||||
type PluginLauncherPlacementZone,
|
||||
type PluginLauncherAction,
|
||||
type PluginLauncherBounds,
|
||||
|
||||
@@ -58,7 +58,7 @@ export interface AgentInstructionsBundle {
|
||||
|
||||
export interface AgentAccessState {
|
||||
canAssignTasks: boolean;
|
||||
taskAssignSource: "explicit_grant" | "agent_creator" | "ceo_role" | "none";
|
||||
taskAssignSource: "simple_default" | "explicit_grant" | "agent_creator" | "ceo_role" | "none";
|
||||
membership: CompanyMembership | null;
|
||||
grants: PrincipalPermissionGrant[];
|
||||
}
|
||||
|
||||
@@ -346,8 +346,11 @@ export interface PluginUiSlotDeclaration {
|
||||
*/
|
||||
entityTypes?: PluginUiSlotEntityType[];
|
||||
/**
|
||||
* Optional company-scoped route segment for page and routeSidebar slots.
|
||||
* Optional company-scoped route segment for page, routeSidebar, and
|
||||
* companySettingsPage slots.
|
||||
* Example: `kitchensink` becomes `/:companyPrefix/kitchensink`.
|
||||
* For companySettingsPage, `permissions` becomes
|
||||
* `/:companyPrefix/company/settings/permissions`.
|
||||
*/
|
||||
routePath?: string;
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,37 @@ describe("plugin capability constants", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin manifest validators", () => {
|
||||
it("accepts existing-style plugins that do not request access or authorization capabilities", () => {
|
||||
const parsed = pluginManifestV1Schema.parse({
|
||||
id: "paperclip.compat-dashboard",
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Compat Dashboard",
|
||||
description: "Dashboard-only plugin without access or authorization host APIs.",
|
||||
author: "Paperclip",
|
||||
categories: ["ui"],
|
||||
capabilities: ["ui.dashboardWidget.register"],
|
||||
entrypoints: {
|
||||
worker: "./dist/worker.js",
|
||||
ui: "./dist/ui.js",
|
||||
},
|
||||
ui: {
|
||||
slots: [
|
||||
{
|
||||
type: "dashboardWidget",
|
||||
id: "compat-dashboard",
|
||||
displayName: "Compat Dashboard",
|
||||
exportName: "CompatDashboard",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.capabilities).toEqual(["ui.dashboardWidget.register"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin managed routine validators", () => {
|
||||
it("accepts core issue surface visibility values in routine templates", () => {
|
||||
const parsed = pluginManagedRoutineDeclarationSchema.parse({
|
||||
@@ -128,4 +159,30 @@ describe("plugin UI slot validators", () => {
|
||||
|
||||
expect(parsed.entityTypes).toEqual(["execution_workspace"]);
|
||||
});
|
||||
|
||||
it("accepts company settings page slots with a non-core settings route", () => {
|
||||
const parsed = pluginUiSlotDeclarationSchema.parse({
|
||||
type: "companySettingsPage",
|
||||
id: "permissions-settings",
|
||||
displayName: "Permissions",
|
||||
exportName: "PermissionsSettingsPage",
|
||||
routePath: "permissions",
|
||||
});
|
||||
|
||||
expect(parsed.routePath).toBe("permissions");
|
||||
});
|
||||
|
||||
it("prevents company settings page slots from shadowing core settings routes", () => {
|
||||
const parsed = pluginUiSlotDeclarationSchema.safeParse({
|
||||
type: "companySettingsPage",
|
||||
id: "access-settings",
|
||||
displayName: "Access",
|
||||
exportName: "AccessSettingsPage",
|
||||
routePath: "access",
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) return;
|
||||
expect(parsed.error.issues.some((issue) => issue.message.includes("reserved by the host"))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PLUGIN_UI_SLOT_TYPES,
|
||||
PLUGIN_UI_SLOT_ENTITY_TYPES,
|
||||
PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS,
|
||||
PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS,
|
||||
PLUGIN_LAUNCHER_PLACEMENT_ZONES,
|
||||
PLUGIN_LAUNCHER_ACTIONS,
|
||||
PLUGIN_LAUNCHER_BOUNDS,
|
||||
@@ -322,10 +323,10 @@ export const pluginUiSlotDeclarationSchema = z.object({
|
||||
path: ["entityTypes"],
|
||||
});
|
||||
}
|
||||
if (value.routePath && value.type !== "page" && value.type !== "routeSidebar") {
|
||||
if (value.routePath && value.type !== "page" && value.type !== "routeSidebar" && value.type !== "companySettingsPage") {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "routePath is only supported for page and routeSidebar slots",
|
||||
message: "routePath is only supported for page, routeSidebar, and companySettingsPage slots",
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
@@ -336,6 +337,13 @@ export const pluginUiSlotDeclarationSchema = z.object({
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
if (value.type === "companySettingsPage" && !value.routePath) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "companySettingsPage slots require routePath",
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
if (value.routePath && PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS.includes(value.routePath as (typeof PLUGIN_RESERVED_COMPANY_ROUTE_SEGMENTS)[number])) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
@@ -343,6 +351,17 @@ export const pluginUiSlotDeclarationSchema = z.object({
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
if (
|
||||
value.type === "companySettingsPage"
|
||||
&& value.routePath
|
||||
&& PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS.includes(value.routePath as (typeof PLUGIN_RESERVED_COMPANY_SETTINGS_ROUTE_SEGMENTS)[number])
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `company settings routePath "${value.routePath}" is reserved by the host`,
|
||||
path: ["routePath"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type PluginUiSlotDeclarationInput = z.infer<typeof pluginUiSlotDeclarationSchema>;
|
||||
|
||||
Reference in new issue
Block a user