mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
Fix LLM Wiki package and migration validation (#6010)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies. > - Plugins extend the control plane with optional capabilities such as LLM Wiki. > - LLM Wiki needs its package assets and plugin-owned database migrations to work when installed from the packaged plugin. > - The bundled spaces migration used validation-hostile dynamic SQL, and the packaged plugin could omit non-dist runtime assets. > - This pull request makes the LLM Wiki package include its required assets and cuts the spaces migration over to explicit, idempotent SQL that passes the production plugin database validator. > - The benefit is a simpler plugin install path that validates and applies the bundled LLM Wiki migrations without adding plugin-specific legacy handling to Paperclip core. ## What Changed - Added the LLM Wiki package asset allowlist so agents, migrations, skills, templates, dist output, and README are included when packaged. - Renamed the bootstrap `.gitignore` template to `gitignore.template` and updated the runtime lookup so package tooling does not drop the hidden template file. - Relaxed plugin migration validation to allow namespace-scoped `INSERT`/`UPDATE` backfills and `CREATE INDEX` statements while continuing to reject destructive or cross-namespace SQL. - Replaced the LLM Wiki spaces migration's dynamic constraint-drop DO block with explicit `DROP CONSTRAINT IF EXISTS` statements. - Replaced fragile regex-source dispatch in SQL reference extraction with explicit capture-group descriptors. - Added regression coverage that applies the bundled LLM Wiki migrations through the production validator and checks the expected constraints. ## Verification - `pnpm exec vitest run --project @paperclipai/server server/src/__tests__/plugin-database.test.ts --pool=forks --poolOptions.forks.isolate=true` - `pnpm --filter @paperclipai/plugin-llm-wiki build` - `git diff --check` - Confirmed `pnpm-lock.yaml` is not included in the branch diff. ## Risks - Low migration risk for current users: LLM Wiki spaces are new, so this intentionally cuts over the plugin migration instead of adding legacy handling in core. - Validator behavior is broader than before, but still requires fully qualified plugin namespace targets, blocks deletes/destructive DDL, and keeps public table access read-only and allowlisted. > Checked [`ROADMAP.md`](ROADMAP.md); this is a targeted plugin packaging/migration fix and does not duplicate planned core feature work. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, GPT-5 based coding agent, tool-enabled local repo access, reasoning mode managed by the Paperclip/Codex runtime. Exact context window was not surfaced in this session. ## 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
dfcebf082b
commit
eb38b226c2
6 changed files with 203 additions and 41 deletions
|
|
@ -19,6 +19,9 @@ const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|||
const MAX_POSTGRES_IDENTIFIER_LENGTH = 63;
|
||||
|
||||
type SqlRef = { schema: string; table: string; keyword: string };
|
||||
type QualifiedRefPattern =
|
||||
| { pattern: RegExp; groups: "keyword-schema-table" }
|
||||
| { pattern: RegExp; groups: "schema-table"; keyword: string };
|
||||
|
||||
export type PluginDatabaseRuntimeResult<T = Record<string, unknown>> = {
|
||||
rows?: T[];
|
||||
|
|
@ -123,14 +126,29 @@ function normaliseSql(input: string): string {
|
|||
|
||||
function extractQualifiedRefs(statement: string): SqlRef[] {
|
||||
const refs: SqlRef[] = [];
|
||||
const patterns = [
|
||||
/\b(from|join|references|into|update)\s+"?([A-Za-z_][A-Za-z0-9_]*)"?\."?([A-Za-z_][A-Za-z0-9_]*)"?/gi,
|
||||
/\b(alter\s+table|create\s+table|create\s+view|drop\s+table|truncate\s+table)\s+(?:if\s+(?:not\s+)?exists\s+)?"?([A-Za-z_][A-Za-z0-9_]*)"?\."?([A-Za-z_][A-Za-z0-9_]*)"?/gi,
|
||||
const patterns: QualifiedRefPattern[] = [
|
||||
{
|
||||
pattern: /\b(from|join|references|into|update)\s+"?([A-Za-z_][A-Za-z0-9_]*)"?\."?([A-Za-z_][A-Za-z0-9_]*)"?/gi,
|
||||
groups: "keyword-schema-table",
|
||||
},
|
||||
{
|
||||
pattern: /\b(alter\s+table|create\s+table|create\s+view|drop\s+table|truncate\s+table)\s+(?:if\s+(?:not\s+)?exists\s+)?"?([A-Za-z_][A-Za-z0-9_]*)"?\."?([A-Za-z_][A-Za-z0-9_]*)"?/gi,
|
||||
groups: "keyword-schema-table",
|
||||
},
|
||||
{
|
||||
pattern: /\bcreate\s+(?:unique\s+)?index(?:\s+concurrently)?\s+(?:if\s+not\s+exists\s+)?"?[A-Za-z_][A-Za-z0-9_]*"?\s+on\s+"?([A-Za-z_][A-Za-z0-9_]*)"?\."?([A-Za-z_][A-Za-z0-9_]*)"?/gi,
|
||||
groups: "schema-table",
|
||||
keyword: "create index",
|
||||
},
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const { pattern, ...mapping } of patterns) {
|
||||
for (const match of statement.matchAll(pattern)) {
|
||||
refs.push({ keyword: match[1]!.toLowerCase(), schema: match[2]!, table: match[3]! });
|
||||
if (mapping.groups === "keyword-schema-table") {
|
||||
refs.push({ keyword: match[1]!.toLowerCase(), schema: match[2]!, table: match[3]! });
|
||||
} else {
|
||||
refs.push({ keyword: mapping.keyword, schema: match[1]!, table: match[2]! });
|
||||
}
|
||||
}
|
||||
}
|
||||
return refs;
|
||||
|
|
@ -182,9 +200,16 @@ export function validatePluginMigrationStatement(
|
|||
throw new Error("Destructive plugin migrations are not allowed in Phase 1");
|
||||
}
|
||||
|
||||
const ddlAllowed = /^(create|alter|comment)\b/.test(normalized);
|
||||
if (!ddlAllowed) {
|
||||
throw new Error("Plugin migrations may contain DDL statements only");
|
||||
if (/\bdelete\s+from\b/.test(normalized)) {
|
||||
throw new Error("Plugin migrations cannot delete data");
|
||||
}
|
||||
|
||||
const ddlOrBackfillAllowed =
|
||||
/^(create|alter|comment)\b/.test(normalized) ||
|
||||
/^(insert\s+into|update)\b/.test(normalized) ||
|
||||
(normalized.startsWith("with ") && /\b(insert\s+into|update)\b/.test(normalized));
|
||||
if (!ddlOrBackfillAllowed) {
|
||||
throw new Error("Plugin migrations may contain DDL or namespace-scoped backfill statements only");
|
||||
}
|
||||
|
||||
const refs = extractQualifiedRefs(statement);
|
||||
|
|
@ -192,6 +217,21 @@ export function validatePluginMigrationStatement(
|
|||
throw new Error("Plugin migration objects must use fully qualified schema names");
|
||||
}
|
||||
|
||||
const objectRefKeywords = new Set([
|
||||
"alter table",
|
||||
"create index",
|
||||
"create table",
|
||||
"create view",
|
||||
"drop table",
|
||||
"into",
|
||||
"truncate table",
|
||||
"update",
|
||||
]);
|
||||
const hasQualifiedObjectRef = refs.some((ref) => objectRefKeywords.has(ref.keyword));
|
||||
if (!hasQualifiedObjectRef && !normalized.startsWith("comment ")) {
|
||||
throw new Error("Plugin migration objects must use fully qualified schema names");
|
||||
}
|
||||
|
||||
const allowedCoreReadTables = new Set(coreReadTables);
|
||||
for (const ref of refs) {
|
||||
if (ref.schema === namespace) continue;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue