diff --git a/packages/plugins/sdk/README.md b/packages/plugins/sdk/README.md index 579bd8ed26a..5db1363d7d7 100644 --- a/packages/plugins/sdk/README.md +++ b/packages/plugins/sdk/README.md @@ -1336,6 +1336,11 @@ an explicit project choice or disabling a conflicting provider. Worker health do not change the selected default: unavailable providers fail without local fallback. No company settings row means enabled, matching the normal plugin contract. +New and imported tasks leave an unspecified project mode inherited. Provider-owned +workspaces retain explicit project and task strategies in shared and operator-branch +modes. If neither supplies a strategy, those modes use `project_primary`; an agent's +Git strategy remains a fallback only for isolated mode. + Project responses keep the stored `executionWorkspacePolicy` and expose the separate `workspaceProvider` configuration, including its source, effective policy, provider choices and configuration error. The project configuration UI supports diff --git a/packages/shared/src/types/plugin.ts b/packages/shared/src/types/plugin.ts index 030f58bf2f9..3f121a74f45 100644 --- a/packages/shared/src/types/plugin.ts +++ b/packages/shared/src/types/plugin.ts @@ -698,7 +698,6 @@ export interface PaperclipPluginManifestV1 { workspaceProvider?: { modes: Array<"shared_workspace" | "isolated_workspace" | "operator_branch">; strategies: Array<"project_primary" | "git_worktree">; - /** Inherit this provider and these Git workspace defaults for projects without an explicit selection. */ defaultForProjects?: { mode: "shared_workspace" | "isolated_workspace" | "operator_branch"; strategy: "project_primary" | "git_worktree"; diff --git a/packages/shared/src/types/workspace-runtime.ts b/packages/shared/src/types/workspace-runtime.ts index e36c463e26c..00ab237577d 100644 --- a/packages/shared/src/types/workspace-runtime.ts +++ b/packages/shared/src/types/workspace-runtime.ts @@ -171,7 +171,6 @@ export interface ExecutionWorkspaceCloseReadiness { export interface ProjectExecutionWorkspacePolicy { enabled: boolean; - /** Omitted inherits the installed default; null explicitly selects Core/local. */ storageProviderPluginId?: string | null; sharedWorkspaceConcurrency?: SharedWorkspaceConcurrency; defaultMode?: ProjectExecutionWorkspaceDefaultMode; diff --git a/packages/shared/src/validators/project.ts b/packages/shared/src/validators/project.ts index 6ea254cd4d3..61839f7a1b5 100644 --- a/packages/shared/src/validators/project.ts +++ b/packages/shared/src/validators/project.ts @@ -29,7 +29,6 @@ export const projectExecutionWorkspacePolicySchema = z allowIssueOverride: z.boolean().optional(), defaultProjectWorkspaceId: z.string().guid().optional().nullable(), environmentId: z.string().guid().optional().nullable(), - // Omitted inherits; null explicitly selects Core/local. storageProviderPluginId: z.string().uuid().optional().nullable(), workspaceStrategy: executionWorkspaceStrategySchema.optional().nullable(), workspaceRuntime: z.record(z.string(), z.unknown()).optional().nullable(), diff --git a/server/src/__tests__/execution-workspace-policy.test.ts b/server/src/__tests__/execution-workspace-policy.test.ts index c8093d7a21c..e44ea327f3a 100644 --- a/server/src/__tests__/execution-workspace-policy.test.ts +++ b/server/src/__tests__/execution-workspace-policy.test.ts @@ -52,6 +52,14 @@ describe("execution workspace policy helpers", () => { expect(resolveEffectiveWorkspaceStrategyType(mode, config)).toBe("git_worktree"); }); + it.each(["shared_workspace", "operator_branch"] as const)("keeps the primary-source fallback for %s providers without a project strategy", mode => { + const config = buildExecutionWorkspaceAdapterConfig({ agentConfig: { workspaceStrategy: { type: "git_worktree", baseRef: "agent-branch" } }, + issueSettings: null, mode, legacyUseProjectWorkspace: null, + projectPolicy: { enabled: true, storageProviderPluginId: "10000000-0000-4000-8000-000000000001" } }); + expect(config.workspaceStrategy).toEqual({ type: "project_primary" }); + expect(resolveEffectiveWorkspaceStrategyType(mode, config)).toBe("project_primary"); + }); + it("prefers explicit issue mode over project policy and legacy overrides", () => { expect( resolveExecutionWorkspaceMode({ diff --git a/server/src/services/execution-workspace-policy.ts b/server/src/services/execution-workspace-policy.ts index 39ae803c488..f25ee1451f0 100644 --- a/server/src/services/execution-workspace-policy.ts +++ b/server/src/services/execution-workspace-policy.ts @@ -345,7 +345,6 @@ export function resolveExecutionWorkspaceEnvironmentId(input: { export function defaultIssueExecutionWorkspaceSettingsForProject( projectPolicy: ProjectExecutionWorkspacePolicy | null, ): IssueExecutionWorkspaceSettings | null { - // An unspecified mode must keep inheriting the effective provider default. if (!projectPolicy?.enabled || projectPolicy.defaultMode === undefined) return null; return { mode: @@ -460,7 +459,7 @@ export function buildExecutionWorkspaceAdapterConfig(input: { const strategy = issueStrategy && projectStrategy?.type === issueStrategy.type ? { ...projectStrategy, ...issueStrategy } : issueStrategy ?? projectStrategy ?? - parseExecutionWorkspaceStrategy(nextConfig.workspaceStrategy) ?? + (input.mode === "isolated_workspace" ? parseExecutionWorkspaceStrategy(nextConfig.workspaceStrategy) : undefined) ?? ({ type: input.mode === "isolated_workspace" ? "git_worktree" : "project_primary" } satisfies ExecutionWorkspaceStrategy); if (issueStrategy?.existingBranch && issueStrategy.branchTemplate === undefined && strategy !== issueStrategy) { delete strategy.branchTemplate; diff --git a/server/src/services/heartbeat/workspace-preparation.test.ts b/server/src/services/heartbeat/workspace-preparation.test.ts index a0c613daad3..cc4d2e16dc7 100644 --- a/server/src/services/heartbeat/workspace-preparation.test.ts +++ b/server/src/services/heartbeat/workspace-preparation.test.ts @@ -164,7 +164,6 @@ describe.skipIf(!support.supported)("heartbeat workspace preparation boundary", }); it("pins a resolved Core choice through source selection when a default appears during setup", async () => { const { input, source } = await fixture(); - // The run resolved Core before installation; its policy and mode are already admitted. expect(input.policy.projectExecutionWorkspacePolicy).toBeNull(); await installDefault(); const call = vi.fn(); diff --git a/server/src/services/plugin-workspaces.ts b/server/src/services/plugin-workspaces.ts index 4d725f9b111..e519bd7123a 100644 --- a/server/src/services/plugin-workspaces.ts +++ b/server/src/services/plugin-workspaces.ts @@ -185,7 +185,6 @@ export async function preparePluginWorkspace( existing?.metadata?.workspaceProviderRemoval ) throw conflict("Workspace storage is retired or awaiting cleanup"); - // A source-free task directory has no project storage to prepare. Existing bindings still require their owner. if (!existing && (!input.projectId || !input.projectWorkspaceId)) return null; const saved = readWorkspaceProviderBinding(existing?.metadata); const mode = existing?.mode ?? input.mode, @@ -202,7 +201,6 @@ export async function preparePluginWorkspace( ), ) : []; - // A project default can only select storage for a new workspace. if (existing && existing.providerType !== "plugin" && !saved && !existing.metadata?.workspaceProviderOwner) return null; const owner = saved?.pluginId ?? existing?.metadata?.workspaceProviderOwner ?? (!existing && input.selectedProviderPluginId !== undefined ? input.selectedProviderPluginId : diff --git a/server/src/services/project-workspace-provider.ts b/server/src/services/project-workspace-provider.ts index 26df8244b6f..cc8e8272b2c 100644 --- a/server/src/services/project-workspace-provider.ts +++ b/server/src/services/project-workspace-provider.ts @@ -9,7 +9,6 @@ export function isWorkspaceProvider(manifest: PaperclipPluginManifestV1) { manifest.resourceLifecycle?.includes("project")); } -/** Installation/enablement select the provider; transient worker health must never change it. */ type ProviderDb = Pick; export async function listProjectWorkspaceProviders(db: ProviderDb, companyId: string) { @@ -24,7 +23,6 @@ export async function listProjectWorkspaceProviders(db: ProviderDb, companyId: s type Provider = Awaited>[number]; -/** Keep stored policy separate from effective defaults so reads and edits never turn inheritance into an override. */ export function resolveProjectWorkspaceProvider( rawPolicy: unknown, providers: Provider[],