mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 15:55:46 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Projects supply repository files to agent runs through core staging or a storage plugin. > - A storage plugin's default claim silently attached new projects even when their agents used a different environment. > - An environment name or driver type does not establish shared storage access. The existing binding and task-driver contracts do. > - This pull request groups creation choices by that contract and selects storage from the creator's effective environment. > - The benefit is a visible, validated choice and a clear error before an incompatible managed workspace operation. ## Linked Issues or Issue Description Fixes #20459. Related: #20156 covers a separate provider-failure diagnostic. The PR search found no other fix for #20459. ROADMAP.md has no duplicate project-creation change. ## What Changed - Show **Available in** when configured environments require different project storage. Each option names the environments that can use the same storage owner. Hide the selector when all choices are compatible. - Derive compatibility from repository staging and managed binding ownership. Local, SSH, and sandbox environments use core staging. Managed storage requires a registered task driver from the same installed storage plugin. Separate storage plugins remain incompatible. Unknown plugin contracts and the fake sandbox are excluded. - Follow the creating agent's environment, or the person's primary agent, then the instance default and local fallback. Apply managed-only and forced Kubernetes policy. The product has no separate user or company environment-default field today. - Add a company-authorized discovery endpoint and an optional creation `environmentId`. Validate the choice again on save. Store the storage owner without overriding agent run environments. Preserve explicit API storage choices and existing project ownership. - Keep the hidden repository-free private-task project independent of environment availability. Submit the observed storage owner as `expectedStorageProviderPluginId` so a changed contract fails at save without setting a workspace policy. Instance task-isolation defaults stay inherited. - Reject an incompatible managed workspace before a provider operation. Name the storage and selected environment in the error. Add regression tests, API documentation, creation-story fixtures, and minimal corrections to obsolete map text. ## Verification - **102 tests passed across 7 files:** project environment integration, installed storage policy, repository persistence, project environment routes, project creation telemetry routes, OpenAPI, and NewProjectDialog. Run with `node node_modules/vitest/vitest.mjs run server/src/__tests__/project-environments.test.ts server/src/__tests__/project-workspace-provider.integration.test.ts server/src/__tests__/project-repositories-persistence.test.ts server/src/__tests__/project-routes-env.test.ts server/src/__tests__/project-goal-telemetry-routes.test.ts server/src/__tests__/openapi-routes.test.ts ui/src/components/NewProjectDialog.test.tsx --silent`. - The review fixes cover both archived-default private-task cases and changed-storage rejection. With both instance isolation flags enabled, a new core repository project keeps a null policy and resolves to an isolated Git worktree. The complete affected set passes all 102 tests. - The CI query-error policy failure was fixed by using the shared query view. The form and shared query-view suites pass **24 tests**, including cached choices during a transient refetch failure. `node scripts/check-query-error-rendering.mjs --enforce --summary` reports zero findings. - CI exposed stale SDK source hashes and heartbeat fixtures that paired plugin storage with Local. Refreshed exactly four shared-source hashes with the maintained generator (runtime pins unchanged). All **53 SDK privacy/source-proof tests**, including real built SDK output, pass. All **7 corrected managed-storage heartbeat scenarios** pass; the fixtures declare the owning plugin's task environment and substitute transport at the existing execution seam. - Shared and UI TypeScript checks pass. The UI production build passes. `pnpm check:token-gates`, `node scripts/check-maps.mjs`, `node scripts/check-module-boundaries.mjs`, and diff checks pass. - Tested UI states: compatible choices, incompatible groups, default within a group, manual override, create failure, loading, discovery failure and retry, and an unavailable default. Tests use the production form and existing design tokens. - The integration tests use real isolated PostgreSQL on macOS arm64 with Node 26.12.0. They cover user/agent defaults, storage persistence, policy restrictions, company and authentication denials, and rejection before a provider call. Discovery uses saved configuration and makes no worker or provider requests. - Local verification limits: `pnpm -r typecheck` and `pnpm build` stop in unchanged adapter-utils code because the installed ACPX types lack `inheritProcessEnv`. A direct server check also finds an unchanged AI SDK type-version mismatch. The existing heartbeat test `inherits the instance default environment across companies while preserving explicit agent overrides` times out at line 518 on both this change and unmodified master with the same dependencies. The full `pnpm test:run` attempt passed all six chat shards (1,948 tests), then the Git workspace expiration suite reported 14 failures. I stopped that broad run. Two sampled failures reproduce on unchanged master with the same `unknown_worktree_git_files` refusal; the other expiration failures were not independently compared. A later broad workspace-preparation run also hit workspace validation failures and was stopped; the seven corrected fixture scenarios pass independently. These limits do not establish a full local suite pass. Hosted CI remains required. - Initial missing-package and loopback-denial failures were resolved by repairing isolated dependency links, building the SDK, and running database tests with loopback access. No deployed or live-provider execution is claimed. ## Risks - Creation defaults now follow the effective environment. A plugin's `defaultForProjects` claim alone no longer selects storage. This is the intended behavior change. - Compatibility covers repository project storage. It does not guarantee arbitrary host paths, retained-workspace moves, adapter/image support, credentials, or worker readiness. Matching provider configs cannot prove a cross-plugin storage contract. - A person's primary agent supplies the personal default because there is no separate personal environment setting. An unavailable configured default requires a valid explicit choice or a configuration repair. - No migration runs. Existing projects keep their storage owner. This change does not detach existing managed data or provide a storage migration. - Full local verification has the limits listed above. CI and standard Greptile review are pending. ## Model Used OpenAI Codex, based on GPT-6, with reasoning, tool use, and code execution. The exact serving snapshot and context-window size were 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 either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [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 - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
101 lines
5.4 KiB
TypeScript
101 lines
5.4 KiB
TypeScript
import { and, eq, notInArray } from "drizzle-orm";
|
|
import { pluginCompanySettings, plugins, type Db } from "@paperclipai/db";
|
|
import type { Environment, PaperclipPluginManifestV1, ProjectExecutionWorkspacePolicy, 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 pluginId = policy?.storageProviderPluginId ?? null;
|
|
const selected = pluginId ? providers.find(plugin => plugin.id === pluginId) : undefined;
|
|
let error: string | null = 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" : "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 function sourcelessProjectWorkspacePolicy(rawPolicy: unknown): ProjectExecutionWorkspacePolicy | null {
|
|
const policy = parseProjectExecutionWorkspacePolicy(rawPolicy);
|
|
if (policy?.storageProviderPluginId === undefined) return policy;
|
|
const { storageProviderPluginId: _storage, ...settings } = policy;
|
|
return Object.keys(settings).some(key => key !== "enabled") ? settings : null;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
export function projectStorageForEnvironment(
|
|
environment: Pick<Environment, "driver" | "config">,
|
|
providers: Array<Pick<Provider, "id" | "pluginKey" | "manifestJson">>,
|
|
): { pluginId: string | null } | null {
|
|
if (["local", "ssh", "sandbox"].includes(environment.driver)) {
|
|
return environment.driver === "sandbox" && environment.config.provider === "fake" ? null : { pluginId: null };
|
|
}
|
|
if (environment.driver !== "plugin") return null;
|
|
const plugin = providers.find(value => value.pluginKey === environment.config.pluginKey);
|
|
const driver = plugin?.manifestJson.environmentDrivers?.find(value => value.driverKey === environment.config.driverKey
|
|
&& value.kind !== "sandbox_provider" && value.supportsTasks === true);
|
|
return plugin && driver && plugin.manifestJson.capabilities.includes("environment.drivers.register")
|
|
? { pluginId: plugin.id } : null;
|
|
}
|
|
|
|
export function assertProjectStorageEnvironment(
|
|
provider: Pick<Provider, "id" | "pluginKey" | "manifestJson">,
|
|
environment: Pick<Environment, "driver" | "config" | "name">,
|
|
) {
|
|
if (projectStorageForEnvironment(environment, [provider])?.pluginId !== provider.id) {
|
|
throw conflict(`This project's workspace storage (${provider.manifestJson.displayName}) does not support the environment "${environment.name}". Use an agent in a compatible environment, or create a project for this environment.`,
|
|
{ code: "project_environment_incompatible", retryable: false });
|
|
}
|
|
}
|