mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
Expand plugin host surface (#5205)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The plugin system is the extension boundary for optional product capabilities > - Rich plugins need more than a worker entrypoint: they need scoped database storage, local project folders, managed agents/routines, host navigation, and reusable UI components > - The LLM Wiki work exposed those missing host surfaces while keeping plugin code outside the core control plane > - This pull request expands the core plugin host, SDK, server APIs, and UI bridge so plugins can declare and use those surfaces > - The benefit is that future plugins can integrate with Paperclip through documented, validated contracts instead of bespoke server or UI imports ## What Changed - Added plugin-managed database namespaces and migration tracking, including Drizzle schema/migration files and SQL validation for namespace isolation. - Added server support for plugin local folders, managed agents, managed routines, scoped plugin APIs, and plugin operation visibility. - Expanded shared plugin manifest/types/validators and SDK host/testing/UI exports for richer plugin surfaces. - Added reusable UI pieces for file trees, managed routines, resizable sidebars, route sidebars, and plugin bridge initialization. - Updated plugin docs and example plugins to use the expanded host and SDK surface. ## Verification - `pnpm install --frozen-lockfile` - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/validators/plugin.test.ts server/src/__tests__/plugin-database.test.ts server/src/__tests__/plugin-local-folders.test.ts server/src/__tests__/plugin-managed-agents.test.ts server/src/__tests__/plugin-managed-routines.test.ts server/src/__tests__/plugin-orchestration-apis.test.ts ui/src/api/plugins.test.ts ui/src/components/FileTree.test.tsx ui/src/components/ResizableSidebarPane.test.tsx ui/src/pages/PluginPage.test.tsx ui/src/plugins/bridge.test.ts` passed: 11 files, 67 tests. - Confirmed this PR changes 89 files and does not include `pnpm-lock.yaml` or `.github/workflows/*`. ## Risks - Medium: this expands plugin host contracts across db/shared/server/ui and includes a new core migration (`0076_useful_elektra.sql`). - The plugin database namespace validator is intentionally restrictive; plugin authors may need follow-up affordances for SQL patterns that remain blocked. - Merge this before the LLM Wiki plugin PR so the plugin can resolve the new SDK and host APIs. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 coding agent, tool-enabled shell/git/GitHub workflow. Context window size was not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
d6bee62f02
commit
3c73ed26b5
89 changed files with 27516 additions and 914 deletions
|
|
@ -90,6 +90,16 @@ export interface HostServices {
|
|||
get(): Promise<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
/** Provides trusted company-scoped local folder helpers. */
|
||||
localFolders: {
|
||||
declarations(params: WorkerToHostMethods["localFolders.declarations"][0]): Promise<WorkerToHostMethods["localFolders.declarations"][1]>;
|
||||
configure(params: WorkerToHostMethods["localFolders.configure"][0]): Promise<WorkerToHostMethods["localFolders.configure"][1]>;
|
||||
status(params: WorkerToHostMethods["localFolders.status"][0]): Promise<WorkerToHostMethods["localFolders.status"][1]>;
|
||||
list(params: WorkerToHostMethods["localFolders.list"][0]): Promise<WorkerToHostMethods["localFolders.list"][1]>;
|
||||
readText(params: WorkerToHostMethods["localFolders.readText"][0]): Promise<WorkerToHostMethods["localFolders.readText"][1]>;
|
||||
writeTextAtomic(params: WorkerToHostMethods["localFolders.writeTextAtomic"][0]): Promise<WorkerToHostMethods["localFolders.writeTextAtomic"][1]>;
|
||||
};
|
||||
|
||||
/** Provides `state.get`, `state.set`, `state.delete`. */
|
||||
state: {
|
||||
get(params: WorkerToHostMethods["state.get"][0]): Promise<WorkerToHostMethods["state.get"][1]>;
|
||||
|
|
@ -165,6 +175,18 @@ export interface HostServices {
|
|||
listWorkspaces(params: WorkerToHostMethods["projects.listWorkspaces"][0]): Promise<WorkerToHostMethods["projects.listWorkspaces"][1]>;
|
||||
getPrimaryWorkspace(params: WorkerToHostMethods["projects.getPrimaryWorkspace"][0]): Promise<WorkerToHostMethods["projects.getPrimaryWorkspace"][1]>;
|
||||
getWorkspaceForIssue(params: WorkerToHostMethods["projects.getWorkspaceForIssue"][0]): Promise<WorkerToHostMethods["projects.getWorkspaceForIssue"][1]>;
|
||||
getManaged(params: WorkerToHostMethods["projects.managed.get"][0]): Promise<WorkerToHostMethods["projects.managed.get"][1]>;
|
||||
reconcileManaged(params: WorkerToHostMethods["projects.managed.reconcile"][0]): Promise<WorkerToHostMethods["projects.managed.reconcile"][1]>;
|
||||
resetManaged(params: WorkerToHostMethods["projects.managed.reset"][0]): Promise<WorkerToHostMethods["projects.managed.reset"][1]>;
|
||||
};
|
||||
|
||||
/** Provides `routines.managed.*`. */
|
||||
routines: {
|
||||
managedGet(params: WorkerToHostMethods["routines.managed.get"][0]): Promise<WorkerToHostMethods["routines.managed.get"][1]>;
|
||||
managedReconcile(params: WorkerToHostMethods["routines.managed.reconcile"][0]): Promise<WorkerToHostMethods["routines.managed.reconcile"][1]>;
|
||||
managedReset(params: WorkerToHostMethods["routines.managed.reset"][0]): Promise<WorkerToHostMethods["routines.managed.reset"][1]>;
|
||||
managedUpdate(params: WorkerToHostMethods["routines.managed.update"][0]): Promise<WorkerToHostMethods["routines.managed.update"][1]>;
|
||||
managedRun(params: WorkerToHostMethods["routines.managed.run"][0]): Promise<WorkerToHostMethods["routines.managed.run"][1]>;
|
||||
};
|
||||
|
||||
/** Provides issue read/write, relation, checkout, wakeup, summary, comment methods. */
|
||||
|
|
@ -202,6 +224,9 @@ export interface HostServices {
|
|||
pause(params: WorkerToHostMethods["agents.pause"][0]): Promise<WorkerToHostMethods["agents.pause"][1]>;
|
||||
resume(params: WorkerToHostMethods["agents.resume"][0]): Promise<WorkerToHostMethods["agents.resume"][1]>;
|
||||
invoke(params: WorkerToHostMethods["agents.invoke"][0]): Promise<WorkerToHostMethods["agents.invoke"][1]>;
|
||||
managedGet(params: WorkerToHostMethods["agents.managed.get"][0]): Promise<WorkerToHostMethods["agents.managed.get"][1]>;
|
||||
managedReconcile(params: WorkerToHostMethods["agents.managed.reconcile"][0]): Promise<WorkerToHostMethods["agents.managed.reconcile"][1]>;
|
||||
managedReset(params: WorkerToHostMethods["agents.managed.reset"][0]): Promise<WorkerToHostMethods["agents.managed.reset"][1]>;
|
||||
};
|
||||
|
||||
/** Provides `agents.sessions.create`, `agents.sessions.list`, `agents.sessions.sendMessage`, `agents.sessions.close`. */
|
||||
|
|
@ -281,6 +306,14 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
|
|||
// Config — always allowed
|
||||
"config.get": null,
|
||||
|
||||
// Trusted local folders
|
||||
"localFolders.declarations": null,
|
||||
"localFolders.configure": "local.folders",
|
||||
"localFolders.status": "local.folders",
|
||||
"localFolders.list": "local.folders",
|
||||
"localFolders.readText": "local.folders",
|
||||
"localFolders.writeTextAtomic": "local.folders",
|
||||
|
||||
// State
|
||||
"state.get": "plugin.state.read",
|
||||
"state.set": "plugin.state.write",
|
||||
|
|
@ -326,6 +359,14 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
|
|||
"projects.listWorkspaces": "project.workspaces.read",
|
||||
"projects.getPrimaryWorkspace": "project.workspaces.read",
|
||||
"projects.getWorkspaceForIssue": "project.workspaces.read",
|
||||
"projects.managed.get": "projects.managed",
|
||||
"projects.managed.reconcile": "projects.managed",
|
||||
"projects.managed.reset": "projects.managed",
|
||||
"routines.managed.get": "routines.managed",
|
||||
"routines.managed.reconcile": "routines.managed",
|
||||
"routines.managed.reset": "routines.managed",
|
||||
"routines.managed.update": "routines.managed",
|
||||
"routines.managed.run": "routines.managed",
|
||||
|
||||
// Issues
|
||||
"issues.list": "issues.read",
|
||||
|
|
@ -357,6 +398,9 @@ const METHOD_CAPABILITY_MAP: Record<WorkerToHostMethodName, PluginCapability | n
|
|||
"agents.pause": "agents.pause",
|
||||
"agents.resume": "agents.resume",
|
||||
"agents.invoke": "agents.invoke",
|
||||
"agents.managed.get": "agents.managed",
|
||||
"agents.managed.reconcile": "agents.managed",
|
||||
"agents.managed.reset": "agents.managed",
|
||||
|
||||
// Agent Sessions
|
||||
"agents.sessions.create": "agent.sessions.create",
|
||||
|
|
@ -439,6 +483,25 @@ export function createHostClientHandlers(
|
|||
return services.config.get();
|
||||
}),
|
||||
|
||||
"localFolders.declarations": gated("localFolders.declarations", async (params) => {
|
||||
return services.localFolders.declarations(params);
|
||||
}),
|
||||
"localFolders.configure": gated("localFolders.configure", async (params) => {
|
||||
return services.localFolders.configure(params);
|
||||
}),
|
||||
"localFolders.status": gated("localFolders.status", async (params) => {
|
||||
return services.localFolders.status(params);
|
||||
}),
|
||||
"localFolders.list": gated("localFolders.list", async (params) => {
|
||||
return services.localFolders.list(params);
|
||||
}),
|
||||
"localFolders.readText": gated("localFolders.readText", async (params) => {
|
||||
return services.localFolders.readText(params);
|
||||
}),
|
||||
"localFolders.writeTextAtomic": gated("localFolders.writeTextAtomic", async (params) => {
|
||||
return services.localFolders.writeTextAtomic(params);
|
||||
}),
|
||||
|
||||
// State
|
||||
"state.get": gated("state.get", async (params) => {
|
||||
return services.state.get(params);
|
||||
|
|
@ -530,6 +593,32 @@ export function createHostClientHandlers(
|
|||
"projects.getWorkspaceForIssue": gated("projects.getWorkspaceForIssue", async (params) => {
|
||||
return services.projects.getWorkspaceForIssue(params);
|
||||
}),
|
||||
"projects.managed.get": gated("projects.managed.get", async (params) => {
|
||||
return services.projects.getManaged(params);
|
||||
}),
|
||||
"projects.managed.reconcile": gated("projects.managed.reconcile", async (params) => {
|
||||
return services.projects.reconcileManaged(params);
|
||||
}),
|
||||
"projects.managed.reset": gated("projects.managed.reset", async (params) => {
|
||||
return services.projects.resetManaged(params);
|
||||
}),
|
||||
|
||||
// Routines
|
||||
"routines.managed.get": gated("routines.managed.get", async (params) => {
|
||||
return services.routines.managedGet(params);
|
||||
}),
|
||||
"routines.managed.reconcile": gated("routines.managed.reconcile", async (params) => {
|
||||
return services.routines.managedReconcile(params);
|
||||
}),
|
||||
"routines.managed.reset": gated("routines.managed.reset", async (params) => {
|
||||
return services.routines.managedReset(params);
|
||||
}),
|
||||
"routines.managed.update": gated("routines.managed.update", async (params) => {
|
||||
return services.routines.managedUpdate(params);
|
||||
}),
|
||||
"routines.managed.run": gated("routines.managed.run", async (params) => {
|
||||
return services.routines.managedRun(params);
|
||||
}),
|
||||
|
||||
// Issues
|
||||
"issues.list": gated("issues.list", async (params) => {
|
||||
|
|
@ -611,6 +700,15 @@ export function createHostClientHandlers(
|
|||
"agents.invoke": gated("agents.invoke", async (params) => {
|
||||
return services.agents.invoke(params);
|
||||
}),
|
||||
"agents.managed.get": gated("agents.managed.get", async (params) => {
|
||||
return services.agents.managedGet(params);
|
||||
}),
|
||||
"agents.managed.reconcile": gated("agents.managed.reconcile", async (params) => {
|
||||
return services.agents.managedReconcile(params);
|
||||
}),
|
||||
"agents.managed.reset": gated("agents.managed.reset", async (params) => {
|
||||
return services.agents.managedReset(params);
|
||||
}),
|
||||
|
||||
// Agent Sessions
|
||||
"agents.sessions.create": gated("agents.sessions.create", async (params) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue