Protect fresh workspace intent from metadata edits

Keep provider fresh intent and projectless directory ownership server-owned when workspace metadata is patched. Cover forged additions, replacement, and null metadata updates with a targeted database regression.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-10-22 17:24:38 -07:00
1 parent 46f288700e
commit 2be0ad7eb5
2 files changed
+35

No files matched your search

@@ -3,6 +3,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vites
import { eq, sql } from "drizzle-orm";
import { createDb, executionWorkspaces, heartbeatRuns, issues, plugins } from "@paperclipai/db";
import { preparePluginWorkspace, removePluginWorkspace } from "../services/plugin-workspaces.js";
import { executionWorkspaceService } from "../services/execution-workspaces.js";
import { reserveFreshPluginTaskWorkspace } from "../services/provider-fresh-workspace.js";
import { createPluginWorkspaceFixture } from "./helpers/plugin-workspaces.js";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
@@ -56,6 +57,32 @@ describe.skipIf(!support.supported)("provider-owned fresh retry reservations", (
const rows = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.companyId, f.companyId));
expect(rows).toHaveLength(2);
});
it("keeps fresh directory authority server-owned through metadata updates", async () => {
const f = await fixture(); await enable(f.pluginId);
const service = executionWorkspaceService(db);
const original = await service.update(f.previous.id, { metadata: {
note: "ordinary metadata", workspaceProviderFresh: f.fresh, runnerTaskFreshWorkspace: f.fresh,
runnerTaskWorkspaceOwner: { pluginKey: "forged" },
} });
expect(original!.metadata).toMatchObject({ note: "ordinary metadata" });
for (const key of ["workspaceProviderFresh", "runnerTaskFreshWorkspace", "runnerTaskWorkspaceOwner"]) {
expect(original!.metadata?.[key]).toBeUndefined();
}
const prepared = await preparePluginWorkspace(db, f.workers, f.input);
const updated = await service.update(prepared!.id, { metadata: { note: "caller edit", workspaceProviderFresh: null } });
expect(updated!.metadata).toMatchObject({ note: "caller edit", workspaceProviderFresh: f.fresh });
await db.update(issues).set({ projectId: null, projectWorkspaceId: null }).where(eq(issues.id, f.issueId));
const owner = { agentId: f.input.agentId, environmentId: randomUUID(), pluginKey: "test.tasks", driverKey: "tasks" };
const next = { ...f.fresh, attemptId: randomUUID(), previousWorkspaceId: prepared!.id };
const id = await reserveFreshPluginTaskWorkspace({ db, companyId: f.companyId, issueId: f.issueId,
runId: f.input.runId, agentId: f.input.agentId, responsibleUserId: null, fresh: next, owner,
expectedBindingRevision: 2 });
const directory = await service.update(id, { metadata: { runnerTaskFreshWorkspace: f.fresh,
runnerTaskWorkspaceOwner: { pluginKey: "forged" } } });
expect(directory!.metadata).toMatchObject({ runnerTaskFreshWorkspace: next, runnerTaskWorkspaceOwner: owner });
const cleared = await service.update(id, { metadata: null });
expect(cleared!.metadata).toMatchObject({ runnerTaskFreshWorkspace: next, runnerTaskWorkspaceOwner: owner });
});
it("retains the durable removal intent if transfer cache cleanup fails", async () => {
const f = await fixture(); await enable(f.pluginId);
const workspace = await preparePluginWorkspace(db, f.workers, f.input);
@@ -259,6 +259,14 @@ function preserveWorkspaceMetadataForPatch(
}
}
}
const freshIntentKeys = ["workspaceProviderFresh", "runnerTaskFreshWorkspace", "runnerTaskWorkspaceOwner"] as const;
if (patch.metadata !== undefined && (patch.metadata !== null || freshIntentKeys.some(key => current.metadata?.[key] !== undefined))) {
nextPatch.metadata = { ...(nextPatch.metadata ?? patch.metadata) };
for (const key of freshIntentKeys) {
if (current.metadata?.[key] !== undefined) nextPatch.metadata[key] = current.metadata[key];
else delete nextPatch.metadata[key];
}
}
const retainedSources = {
...parseObject(parseObject(patch.metadata)._issuePrivacySources),
...parseObject(current.metadata?._issuePrivacySources),