mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-15 02:20:38 +09:00
[codex] Add LLM Wiki plugin host support (#5597)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - The plugin system needs host contracts and runtime support before large plugins can integrate cleanly. > - The source branch mixed the LLM Wiki package with supporting host/runtime work, managed plugin skills, root-level storage spaces, and a bookmarks reference plugin. > - [PAP-9173](/PAP/issues/PAP-9173) asked for the current branch to be split by file boundary: plugin package separately from everything else. > - [PAP-9188](/PAP/issues/PAP-9188) clarified that LLM Wiki may have plugin-local spaces, but Paperclip core should not reorganize top-level local storage into spaces. > - Follow-up review clarified that the bookmarks example should not ship in this PR either. > - This pull request contains the non-`packages/plugins/plugin-llm-wiki/` host/runtime work, keeps runtime state under the selected Paperclip instance root, and no longer includes the bookmarks example. ## What Changed - Added/updated plugin host contracts, SDK types, worker RPC plumbing, managed plugin skill support, and related server tests. - Removed the bookmarks example plugin package and its bundled-example/workspace references. - Removed the root-level local spaces CLI/migration surface and restored instance-root runtime defaults for config, db, logs, storage, secrets, workspaces, projects, and adapter homes. - Replaced shared root `space-paths` helpers with `home-paths` helpers for core runtime storage. - Tightened stranded recovery unique-conflict detection so concurrent recovery scans reuse the raced recovery issue when Postgres errors are wrapped. - Kept `packages/plugins/plugin-llm-wiki/` out of this PR diff; plugin-local spaces remain in the stacked plugin-only PR. ## Verification - `pnpm exec vitest run cli/src/__tests__/data-dir.test.ts cli/src/__tests__/home-paths.test.ts cli/src/__tests__/onboard.test.ts packages/shared/src/home-paths.test.ts packages/db/src/runtime-config.test.ts server/src/__tests__/agent-instructions-service.test.ts server/src/__tests__/claude-local-execute.test.ts server/src/__tests__/codex-local-execute.test.ts` - `pnpm exec vitest run packages/db/src/runtime-config.test.ts` - `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts` - `pnpm --filter @paperclipai/server typecheck` - `pnpm exec vitest run server/src/__tests__/heartbeat-process-recovery.test.ts -t "reuses the raced stranded recovery issue"` skipped locally because embedded Postgres did not initialize on this macOS temp host; the code path was typechecked and is covered by Linux CI. - Boundary check: no core references remain for `PAPERCLIP_SPACE_ID`, `spaces migrate-default`, `@paperclipai/shared/space-paths`, `registerSpacesCommands`, or the removed bookmarks example. - Previous PR head `4f23e034` had green GitHub checks: `verify`, all four serialized server shards, `e2e`, `Canary Dry Run`, `policy`, Snyk, and `Greptile Review`. Current head `582f466d` is re-running checks after the bookmarks deletion. ## Risks - Plugin host changes touch shared runtime paths, so regressions would most likely appear in adapter startup, plugin loading, or local dev path defaults. - Removing the bookmarks example also removes one demonstration of plugin database namespaces plus local-folder persistence; remaining plugin examples still cover bundled example discovery and plugin host flows. - The plugin package itself is intentionally deferred to the stacked plugin-only PR, where LLM Wiki plugin-local spaces live. - Existing installs that tested the transient root-level spaces CLI should stop using it; this PR intentionally removes that unsupported migration surface before merge. > 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 GPT-5 Codex via Codex CLI, tool use and local code execution enabled; context window not exposed. ## 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, except where noted above for host-specific embedded Postgres initialization - [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 Stacked follow-up: PR #5592 contains only `packages/plugins/plugin-llm-wiki/` and targets this branch. --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
eb12c42009
commit
0096b56a1c
40 changed files with 1892 additions and 224 deletions
|
|
@ -394,6 +394,8 @@ export {
|
|||
pluginLauncherRenderDeclarationSchema,
|
||||
pluginLauncherDeclarationSchema,
|
||||
pluginDatabaseDeclarationSchema,
|
||||
pluginManagedSkillFileDeclarationSchema,
|
||||
pluginManagedSkillDeclarationSchema,
|
||||
pluginApiRouteDeclarationSchema,
|
||||
pluginManifestV1Schema,
|
||||
installPluginSchema,
|
||||
|
|
@ -413,6 +415,8 @@ export {
|
|||
type PluginLauncherRenderDeclarationInput,
|
||||
type PluginLauncherDeclarationInput,
|
||||
type PluginDatabaseDeclarationInput,
|
||||
type PluginManagedSkillFileDeclarationInput,
|
||||
type PluginManagedSkillDeclarationInput,
|
||||
type PluginApiRouteDeclarationInput,
|
||||
type PluginManifestV1Input,
|
||||
type InstallPlugin,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { PLUGIN_CAPABILITIES } from "../constants.js";
|
||||
import { pluginManagedRoutineDeclarationSchema, pluginUiSlotDeclarationSchema } from "./plugin.js";
|
||||
import { pluginManagedRoutineDeclarationSchema, pluginManifestV1Schema, pluginUiSlotDeclarationSchema } from "./plugin.js";
|
||||
|
||||
describe("plugin capability constants", () => {
|
||||
it("exposes each capability once", () => {
|
||||
|
|
@ -30,6 +30,41 @@ describe("plugin managed routine validators", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("plugin managed skill validators", () => {
|
||||
const baseManifest = {
|
||||
id: "paperclip.test-managed-skills",
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Managed Skills",
|
||||
description: "Managed skills test plugin.",
|
||||
author: "Paperclip",
|
||||
categories: ["automation"],
|
||||
entrypoints: { worker: "./dist/worker.js" },
|
||||
} as const;
|
||||
|
||||
it("requires skills.managed when managed skills are declared", () => {
|
||||
const parsed = pluginManifestV1Schema.safeParse({
|
||||
...baseManifest,
|
||||
capabilities: [],
|
||||
skills: [{ skillKey: "wiki-maintainer", displayName: "Wiki Maintainer" }],
|
||||
});
|
||||
|
||||
expect(parsed.success).toBe(false);
|
||||
if (parsed.success) return;
|
||||
expect(parsed.error.issues.some((issue) => issue.message.includes("skills.managed"))).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts managed skills with the skills.managed capability", () => {
|
||||
const parsed = pluginManifestV1Schema.parse({
|
||||
...baseManifest,
|
||||
capabilities: ["skills.managed"],
|
||||
skills: [{ skillKey: "wiki-maintainer", displayName: "Wiki Maintainer" }],
|
||||
});
|
||||
|
||||
expect(parsed.skills?.[0]?.skillKey).toBe("wiki-maintainer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("plugin UI slot validators", () => {
|
||||
it("accepts route-scoped sidebar slots with a routePath", () => {
|
||||
const parsed = pluginUiSlotDeclarationSchema.parse({
|
||||
|
|
|
|||
|
|
@ -151,6 +151,7 @@ export const pluginManagedAgentDeclarationSchema = z.object({
|
|||
instructions: z.object({
|
||||
entryFile: z.string().min(1).max(200).optional(),
|
||||
content: z.string().max(200_000).optional(),
|
||||
files: z.record(z.string().max(200_000)).optional(),
|
||||
assetPath: z.string().min(1).max(500).optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
|
@ -172,7 +173,7 @@ export type PluginManagedProjectDeclarationInput = z.infer<typeof pluginManagedP
|
|||
|
||||
const pluginManagedResourceRefSchema = z.object({
|
||||
pluginKey: z.string().min(1).max(100).optional(),
|
||||
resourceKind: z.enum(["agent", "project", "routine"]),
|
||||
resourceKind: z.enum(["agent", "project", "routine", "skill"]),
|
||||
resourceKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "resourceKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
|
|
@ -232,6 +233,41 @@ export const pluginLocalFolderDeclarationSchema = z.object({
|
|||
|
||||
export type PluginLocalFolderDeclarationInput = z.infer<typeof pluginLocalFolderDeclarationSchema>;
|
||||
|
||||
export const pluginManagedSkillFileDeclarationSchema = z.object({
|
||||
path: pluginLocalFolderRelativePathSchema.refine(
|
||||
(value) => value.toLowerCase() !== "skill.md",
|
||||
{ message: "managed skill files cannot replace SKILL.md; use markdown for the main skill file" },
|
||||
),
|
||||
content: z.string().max(200_000),
|
||||
});
|
||||
|
||||
export type PluginManagedSkillFileDeclarationInput = z.infer<typeof pluginManagedSkillFileDeclarationSchema>;
|
||||
|
||||
export const pluginManagedSkillDeclarationSchema = z.object({
|
||||
skillKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "skillKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}),
|
||||
displayName: z.string().min(1).max(100),
|
||||
slug: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, {
|
||||
message: "slug must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens",
|
||||
}).optional(),
|
||||
description: z.string().max(2000).nullable().optional(),
|
||||
markdown: z.string().max(200_000).optional(),
|
||||
files: z.array(pluginManagedSkillFileDeclarationSchema).max(50).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
const paths = (value.files ?? []).map((file) => file.path);
|
||||
const duplicates = paths.filter((path, index) => paths.indexOf(path) !== index);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed skill file paths: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["files"],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type PluginManagedSkillDeclarationInput = z.infer<typeof pluginManagedSkillDeclarationSchema>;
|
||||
|
||||
/**
|
||||
* Validates a {@link PluginUiSlotDeclaration} — a UI extension slot the plugin
|
||||
* fills with a React component. Includes `superRefine` checks for slot-specific
|
||||
|
|
@ -589,6 +625,7 @@ export const pluginManifestV1Schema = z.object({
|
|||
agents: z.array(pluginManagedAgentDeclarationSchema).optional(),
|
||||
projects: z.array(pluginManagedProjectDeclarationSchema).optional(),
|
||||
routines: z.array(pluginManagedRoutineDeclarationSchema).optional(),
|
||||
skills: z.array(pluginManagedSkillDeclarationSchema).optional(),
|
||||
localFolders: z.array(pluginLocalFolderDeclarationSchema).optional(),
|
||||
launchers: z.array(pluginLauncherDeclarationSchema).optional(),
|
||||
ui: z.object({
|
||||
|
|
@ -678,6 +715,16 @@ export const pluginManifestV1Schema = z.object({
|
|||
}
|
||||
}
|
||||
|
||||
if (manifest.skills && manifest.skills.length > 0) {
|
||||
if (!manifest.capabilities.includes("skills.managed")) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Capability 'skills.managed' is required when managed skills are declared",
|
||||
path: ["capabilities"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.localFolders && manifest.localFolders.length > 0) {
|
||||
if (!manifest.capabilities.includes("local.folders")) {
|
||||
ctx.addIssue({
|
||||
|
|
@ -871,6 +918,18 @@ export const pluginManifestV1Schema = z.object({
|
|||
}
|
||||
}
|
||||
|
||||
if (manifest.skills) {
|
||||
const skillKeys = manifest.skills.map((skill) => skill.skillKey);
|
||||
const duplicates = skillKeys.filter((key, i) => skillKeys.indexOf(key) !== i);
|
||||
if (duplicates.length > 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `Duplicate managed skill keys: ${[...new Set(duplicates)].join(", ")}`,
|
||||
path: ["skills"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// UI slot ids must be unique within the plugin (namespaced at runtime)
|
||||
if (manifest.ui) {
|
||||
if (manifest.ui.slots) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue