Preserve shared provider strategy fallback

Ignore agent strategy defaults for non-isolated provider modes when the project and task have no explicit strategy. Move explanations from added code comments into the SDK documentation to follow contribution rules.

Verified all 47 workspace policy tests, including primary-source fallback regressions for shared and operator modes. Credential scan and diff checks pass.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-10-22 16:52:52 -07:00
1 parent 63b0752a9a
commit 5d42998466
9 files changed
+14 -10

No files matched your search

+5
View File
@@ -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
-1
View File
@@ -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";
@@ -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;
@@ -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(),
@@ -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({
@@ -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;
@@ -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();
-2
View File
@@ -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 :
@@ -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<Db, "select">;
export async function listProjectWorkspaceProviders(db: ProviderDb, companyId: string) {
@@ -24,7 +23,6 @@ export async function listProjectWorkspaceProviders(db: ProviderDb, companyId: s
type Provider = Awaited<ReturnType<typeof listProjectWorkspaceProviders>>[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[],