mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Storage plugins already own project storage and task checkout preparation. > - Core only selected these plugins through an explicit project policy. > - Installation could therefore leave new and existing projects on incompatible Core preparation. > - This pull request adds one generic manifest declaration and one shared effective-policy resolver. > - Project configuration shows inheritance while durable workspace bindings retain their owners. ## Linked Issues or Issue Description **Subsystem affected** Plugin contracts, project configuration, and execution workspace preparation. **Problem or motivation** An installed storage provider cannot become the company default. Projects without an explicit provider keep Core preparation, even when their execution environment cannot use a Core Git checkout. **Proposed solution** A provider declares supported `workspaceProvider.defaultForProjects` mode and strategy. Company-enabled installations supply inherited defaults. Explicit plugin selections and explicit Core/local (`storageProviderPluginId: null`) win. Conflicting defaults require an operator choice. The declaration and implementation contain no provider-specific identifiers. **Alternatives considered** Rewriting every project would lose the distinction between inheritance and overrides. Selecting by worker health would silently change storage on an outage. **Roadmap alignment** Extends the existing workspace and sandbox provider contracts. The roadmap has no duplicate default-provider implementation. Related public work includes #17243, which covers prepared execution directories rather than provider selection. ## What Changed - Add a validated generic default-provider declaration and document it in the SDK. - Resolve provider selection and Git defaults consistently in project responses, source selection, heartbeat source resolution, reservation, and preparation. - Keep stored policy separate from effective configuration. Add the normal project configuration selector for inheritance, explicit plugins, and Core/local. - Preserve existing local and plugin workspace ownership. Pin selection through preparation and keep provider failures visible. - Add database, API, manifest, heartbeat, and UI regressions. Update the verification maps. ## Verification - Targeted Vitest runs cover manifest validation, project defaults and API access, explicit overrides, existing ownership, new/imported task inheritance, heartbeat routing, source-free tasks, selection pinning during setup, and UI configuration. - Combined policy, provider integration, and heartbeat run: 58 tests pass; 60 unrelated cases excluded. After the final shared-provider fallback fix, all 47 policy tests pass, including shared and operator-mode regressions. - Server and UI TypeScript checks, SDK build, verification-map validation, credential-pattern scan, added-comment scan, and `git diff --check` pass. - A downstream integration uses this Core resolver and preparation service with a real storage hook and mocked remote infrastructure. A new Git project reaches the hook without a stored provider ID. - Standard Greptile and Apex both scored 5/5 on5d42998466. All review threads are resolved. - Final GitHub CI is fully green on5d42998466. Browser shard 3 initially hit a PostgreSQL deadlock during cleanup cancellation, which left an experiment flag enabled for a later test. A failed-job retry passed all 26 browser tests. The workflow completed successfully on attempt 2. Full final verification was delegated to CI at the user's request. - No live provider or hosted tenant run was performed. ## Risks - Installing one default-claiming provider changes effective selection for unconfigured projects. Existing explicit settings and workspace ownership remain intact; incompatible explicit settings fail clearly. - Each configuration/admission resolution reads installed plugin metadata and the selected company's enablement. Project list reads share one catalog query. Selection does not call workers or external providers. - Deploy the Core contract before any downstream manifest uses the declaration. No migration or backfill is required. A downstream immutable image pin must include this change before packaging its declaration. - No merge or deployment is authorized by this PR. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, and code execution. The exact served model ID and context-window size are not exposed 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 searched GitHub for duplicate or related PRs and linked them above - [x] I have described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal ticket id - [x] I have run targeted tests locally and they pass; full gates are delegated to CI at the user's request - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green on 5d42998466ad42400564ba34f48124f40c53fbc5; the branch has no merge conflicts - [x] Standard Greptile is 5/5 on5d4299846after all fixes, with no unresolved threads - [x] Apex is 5/5 on5d42998466, after standard Greptile 5/5 on that same head - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
73 lines
4.0 KiB
TypeScript
73 lines
4.0 KiB
TypeScript
import { and, eq, notInArray } from "drizzle-orm";
|
|
import { pluginCompanySettings, plugins, type Db } from "@paperclipai/db";
|
|
import type { PaperclipPluginManifestV1, ProjectWorkspaceProviderConfiguration } from "@paperclipai/shared";
|
|
import { parseProjectExecutionWorkspacePolicy } from "./execution-workspace-policy.js";
|
|
import { conflict } from "../errors.js";
|
|
|
|
export function isWorkspaceProvider(manifest: PaperclipPluginManifestV1) {
|
|
return Boolean(manifest.workspaceProvider && manifest.capabilities.includes("workspaces.manage") &&
|
|
manifest.resourceLifecycle?.includes("project"));
|
|
}
|
|
|
|
type ProviderDb = Pick<Db, "select">;
|
|
|
|
export async function listProjectWorkspaceProviders(db: ProviderDb, companyId: string) {
|
|
const rows = await db.select({ plugin: plugins, enabled: pluginCompanySettings.enabled })
|
|
.from(plugins)
|
|
.leftJoin(pluginCompanySettings, and(eq(pluginCompanySettings.pluginId, plugins.id),
|
|
eq(pluginCompanySettings.companyId, companyId)))
|
|
.where(notInArray(plugins.status, ["disabled", "uninstalled"]));
|
|
return rows.filter(row => row.enabled !== false && isWorkspaceProvider(row.plugin.manifestJson))
|
|
.map(row => row.plugin);
|
|
}
|
|
|
|
type Provider = Awaited<ReturnType<typeof listProjectWorkspaceProviders>>[number];
|
|
|
|
export function resolveProjectWorkspaceProvider(
|
|
rawPolicy: unknown,
|
|
providers: Provider[],
|
|
hasGitSource: boolean,
|
|
): ProjectWorkspaceProviderConfiguration {
|
|
const policy = parseProjectExecutionWorkspacePolicy(rawPolicy);
|
|
const explicit = policy?.storageProviderPluginId !== undefined;
|
|
const defaults = providers.filter(plugin => plugin.manifestJson.workspaceProvider?.defaultForProjects);
|
|
const selected = explicit ? providers.find(plugin => plugin.id === policy?.storageProviderPluginId)
|
|
: defaults.length === 1 ? defaults[0] : undefined;
|
|
const pluginId = explicit ? policy?.storageProviderPluginId ?? null : selected?.id ?? null;
|
|
let error = !explicit && defaults.length > 1
|
|
? `Multiple workspace storage plugins claim the project default: ${defaults.map(plugin => plugin.manifestJson.displayName).sort().join(", ")}. Select a storage provider in project configuration or disable a conflicting default.`
|
|
: null;
|
|
const declaration = selected?.manifestJson.workspaceProvider;
|
|
const supplied = hasGitSource ? declaration?.defaultForProjects : undefined;
|
|
const effectivePolicy = pluginId ? parseProjectExecutionWorkspacePolicy({
|
|
enabled: true,
|
|
...(supplied ? { defaultMode: supplied.mode } : {}),
|
|
...policy,
|
|
storageProviderPluginId: pluginId,
|
|
...(supplied ? { workspaceStrategy: { type: supplied.strategy, ...policy?.workspaceStrategy } } : {}),
|
|
}) : policy;
|
|
if (pluginId && !selected) error = "The workspace storage plugin is unavailable";
|
|
if (declaration && effectivePolicy && hasGitSource && (
|
|
effectivePolicy.enabled === false ||
|
|
(effectivePolicy.defaultMode && !declaration.modes.includes(effectivePolicy.defaultMode as "isolated_workspace")) ||
|
|
(effectivePolicy.workspaceStrategy?.type && !declaration.strategies.includes(effectivePolicy.workspaceStrategy.type as "git_worktree"))
|
|
)) {
|
|
error = "The storage plugin does not support the explicit project workspace settings. Change the workspace mode or strategy, or select another storage provider in project configuration.";
|
|
}
|
|
return {
|
|
source: explicit ? "project" : selected || error ? "default" : "core",
|
|
pluginId,
|
|
displayName: selected?.manifestJson.displayName ?? (pluginId ? "Unavailable provider" : "Core / local"),
|
|
providers: providers.map(plugin => ({ id: plugin.id, displayName: plugin.manifestJson.displayName,
|
|
isDefault: Boolean(plugin.manifestJson.workspaceProvider?.defaultForProjects) })),
|
|
effectivePolicy,
|
|
error,
|
|
};
|
|
}
|
|
|
|
export async function effectiveProjectWorkspacePolicy(db: ProviderDb, companyId: string, rawPolicy: unknown, hasGitSource: boolean) {
|
|
const configuration = resolveProjectWorkspaceProvider(rawPolicy, await listProjectWorkspaceProviders(db, companyId), hasGitSource);
|
|
if (configuration.error) throw conflict(configuration.error);
|
|
return configuration.effectivePolicy;
|
|
}
|