Fence shared workspace readers and preserve projectless chat admission

Retain root source and reader provenance, recheck publication access, keep plain agent transfers independent of repository discovery, and resolve organizational projects without filesystem sources.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 08:12:05 -05:00
1 parent c205ab39f0
commit e1863d4fe7
24 files changed
+1088 -58320

No files matched your search

@@ -91,7 +91,7 @@ describe("stable live agent directory observation", () => {
const baseline = await captureDirectorySnapshot(root);
runtime = await prepareAdapterExecutionTargetRuntime({ target, runId: "probe-transfer", adapterKey: "agent-files",
workspaceLocalDir: root, workspaceRemoteDir: remote, syncWorkspace: true, workspaceBaseline: baseline,
workspaceGitSnapshot: null, workspaceFileMode: "all", workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
workspaceGitSnapshot: null, workspaceRepositories: [], workspaceFileMode: "all", workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
expect(fs.readdirSync(`${remote}/.paperclip-runtime`)).toEqual(["agent-files"]);
expect(fs.readdirSync(`${remote}/.paperclip-runtime/agent-files`)).toEqual([]);
execFileSync(process.execPath, ["-e", agentDirectoryTransferCleanupProgram, remote], { env: {}, timeout: 10_000 });
@@ -99,6 +99,49 @@ describe("stable live agent directory observation", () => {
expect(probeAgentDirectory(remote).digest).toBe(agentDirectoryBaselineDigest(baseline));
} finally { await runtime?.cleanupWorkspaceSnapshot?.(); fs.rmSync(remote, { recursive: true, force: true }); }
});
it.each([false, true])("round-trips the raw agent-directory repository namespace without discovery (recovering=%s)", async recovering => {
const remoteCwd = `${root}-remote`;
const agentHome = join(remoteCwd, ".paperclip-runtime", "agent-files", "agent", "original-run");
const scratch = join(remoteCwd, ".paperclip-runtime", "paperclip-runner", "agent-file-transfers", "agent", "original-run");
const rawFiles = {
".paperclip-repositories/notes.txt": "Ordinary agent memory\n",
".paperclip-repositories/archive/.git/config": "Raw nested configuration\n",
".paperclip-repositories/archive/.git/hooks/post-commit": "Raw nested hook\n",
".git/HEAD": "Raw root Git metadata\n",
"node_modules/memory.bin": "Ordinary cache directory bytes\n",
};
for (const [relative, contents] of Object.entries(rawFiles)) {
fs.mkdirSync(join(root, relative, ".."), { recursive: true });
fs.writeFileSync(join(root, relative), contents);
}
fs.mkdirSync(agentHome, { recursive: true });
if (recovering) fs.cpSync(root, agentHome, { recursive: true });
const target = { kind: "remote" as const, transport: "sandbox" as const, remoteCwd,
runner: { execute: async (input: Parameters<import("@paperclipai/adapter-utils/command-managed-runtime").CommandManagedRuntimeRunner["execute"]>[0]) => ({
stdout: execFileSync(input.command, input.args ?? [], { cwd: input.cwd, input: input.stdin, encoding: "utf8",
env: { PATH: process.env.PATH, ...input.env }, timeout: input.timeoutMs, maxBuffer: 32 * 1024 * 1024 }),
stderr: "", exitCode: 0, signal: null, timedOut: false, pid: null, startedAt: new Date().toISOString(),
}) } };
let runtime: Awaited<ReturnType<typeof prepareAdapterExecutionTargetRuntime>> | undefined;
try {
runtime = await prepareAdapterExecutionTargetRuntime({ target, runId: "plain-agent-directory", adapterKey: "agent-files",
workspaceLocalDir: root, workspaceRemoteDir: agentHome, runtimeRootDir: scratch, syncWorkspace: true,
workspaceInboundMode: recovering ? "adopt_remote" : undefined, workspaceBaseline: await captureDirectorySnapshot(root),
workspaceGitSnapshot: null, workspaceRepositories: [], workspaceFileMode: "all",
workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
for (const [relative, contents] of Object.entries(rawFiles)) expect(fs.readFileSync(join(agentHome, relative), "utf8")).toBe(contents);
expect(probeAgentDirectory(agentHome).digest).toBe(probeAgentDirectory(root).digest);
fs.writeFileSync(join(agentHome, ".paperclip-repositories/notes.txt"), "Updated memory\n");
fs.writeFileSync(join(agentHome, ".paperclip-repositories/archive/.git/hooks/post-commit"), "Updated raw hook\n");
fs.unlinkSync(join(agentHome, "notes/memory.bin"));
await runtime.restoreWorkspace();
expect(fs.readFileSync(join(root, ".paperclip-repositories/notes.txt"), "utf8")).toBe("Updated memory\n");
expect(fs.readFileSync(join(root, ".paperclip-repositories/archive/.git/hooks/post-commit"), "utf8")).toBe("Updated raw hook\n");
expect(fs.existsSync(join(root, "notes/memory.bin"))).toBe(false);
expect(fs.readFileSync(join(root, ".git/HEAD"), "utf8")).toBe(rawFiles[".git/HEAD"]);
expect(probeAgentDirectory(agentHome).digest).toBe(probeAgentDirectory(root).digest);
} finally { await runtime?.cleanupWorkspaceSnapshot?.(); fs.rmSync(remoteCwd, { recursive: true, force: true }); }
});
it("keeps restart fallback transfer scratch outside the native memory directory", async () => {
const remoteCwd = `${root}-remote`;
const agentHome = join(remoteCwd, ".paperclip-runtime", "agent-files", "agent", "original-run");
@@ -119,7 +162,7 @@ describe("stable live agent directory observation", () => {
runtime = await prepareAdapterExecutionTargetRuntime({ target, runId: "restart-transfer", adapterKey: "agent-files",
workspaceLocalDir: root, workspaceRemoteDir: agentHome, runtimeRootDir: scratch,
syncWorkspace: true, workspaceInboundMode: "adopt_remote", workspaceBaseline: baseline,
workspaceGitSnapshot: null, workspaceFileMode: "all", workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
workspaceGitSnapshot: null, workspaceRepositories: [], workspaceFileMode: "all", workspaceExclude: [".paperclip-runtime", ".paperclip-runtime/**"] });
const memory = "0123456789abcdef0123456789abcdef\n";
fs.writeFileSync(join(agentHome, "notes", "memory.txt"), memory);
await runtime.restoreWorkspace();
@@ -3338,11 +3338,18 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
});
});
it("dispatches local native external chat inside the server-selected task root", async () => {
it.each(["projectless", "organization_only", "empty_source"] as const)("dispatches local native external chat inside the server-selected task root: %s", async placement => {
await withTempPaperclipHome(async () => {
const { companyId, agentId, issueId, runId } =
await seedQueuedIssueRunFixture();
await fs.mkdir(resolvePaperclipInstanceRoot(), { recursive: true });
const projectId = placement === "projectless" ? null : randomUUID();
if (projectId) {
await db.insert(projects).values({ id: projectId, companyId, name: "Chat channel project default" });
if (placement === "empty_source") {
await db.insert(projectWorkspaces).values({ companyId, projectId, name: "Unconfigured source", cwd: " ", repoUrl: null });
}
}
await db
.update(agents)
.set({
@@ -3352,7 +3359,7 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
.where(eq(agents.id, agentId));
await db
.update(issues)
.set({ originKind: "chat_channel" })
.set({ originKind: "chat_channel", projectId })
.where(eq(issues.id, issueId));
await db
.update(heartbeatRuns)
@@ -3390,6 +3397,10 @@ describeEmbeddedPostgres("heartbeat orphaned process recovery", () => {
expect(input.workspace.cwd).not.toBe(
resolveDefaultAgentWorkspaceDir(agentId),
);
const [task] = await db.select().from(issues).where(eq(issues.id, issueId));
const [workspace] = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, task.executionWorkspaceId!));
expect(task.projectId).toBe(projectId);
expect(workspace).toMatchObject({ sourceIssueId: issueId, projectId: null, projectWorkspaceId: null, cwd: input.workspace.cwd });
expect(mockAdapterExecute).not.toHaveBeenCalled();
});
});
@@ -64,14 +64,18 @@ suite("task project repository provisioning", () => {
});
});
it("isolates repository-free low-trust tasks while preserving reassignment and authorized referenced projects", async () => {
it.each(["agent", "organization"] as const)("isolates repository-free low-trust tasks while preserving reassignment and authorized referenced projects (%s policy)", async policyOwner => {
const companyId = randomUUID(), projectId = randomUUID(), agentId = randomUUID(), environmentId = randomUUID();
const referencedProjectId = randomUUID(), deniedProjectId = randomUUID();
vi.stubEnv("PAPERCLIP_MULTI_PROJECT_WORKSPACE_SYNC", "true");
await instanceSettingsService(db).updateExperimental({ enableIsolatedWorkspaces: true });
await db.insert(companies).values({ id: companyId, name: "Email company", issuePrefix: `E${companyId.slice(0, 6)}`, defaultResponsibleUserId: "responsible-user" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "owner" });
await db.insert(projects).values({ id: projectId, companyId, name: "Onboarding" });
const authorizationPolicy = {
trustPreset: "low_trust_review", trustBoundary: { mode: "low_trust_review", companyId, projectIds: [projectId, referencedProjectId] },
};
await db.insert(projects).values({ id: projectId, companyId, name: "Onboarding",
executionWorkspacePolicy: policyOwner === "organization" ? { authorizationPolicy } : null });
for (const id of [referencedProjectId, deniedProjectId]) {
const source = path.join(root, companyId, id);
await mkdir(source, { recursive: true });
@@ -79,13 +83,11 @@ suite("task project repository provisioning", () => {
await db.insert(projects).values({ id, companyId, name: id });
await db.insert(projectWorkspaces).values({ id: randomUUID(), companyId, projectId: id, name: "Reference", sourceType: "local_path", cwd: source, isPrimary: true });
}
await db.insert(environments).values({ id: environmentId, name: "Email sandbox", driver: "sandbox", status: "active", config: { provider: "fake", image: "fake:test" } });
await db.insert(environments).values({ id: environmentId, name: `Email sandbox ${policyOwner}`, driver: "sandbox", status: "active", config: { provider: "fake", image: "fake:test" } });
await db.insert(agents).values({
id: agentId, companyId, name: "Email agent", role: "engineer", status: "idle", adapterType: "codex_local",
defaultEnvironmentId: environmentId, adapterConfig: {}, runtimeConfig: {},
permissions: { trustPreset: "low_trust_review", authorizationPolicy: {
trustPreset: "low_trust_review", trustBoundary: { mode: "low_trust_review", companyId, projectIds: [projectId, referencedProjectId] },
} },
permissions: policyOwner === "agent" ? { trustPreset: "low_trust_review", authorizationPolicy } : {},
});
// Exercise the real lease and workspace lifecycle with the built-in fake
// provider, executing its setup commands in a disposable filesystem root.
@@ -120,6 +122,11 @@ suite("task project repository provisioning", () => {
expect(execute.mock.calls.filter(([input]) => input.runId === run!.id)).toHaveLength(1);
const call = execute.mock.calls.find(([input]) => input.runId === run!.id)![0];
expect(call.context.paperclipEnvironment.driver).toBe("sandbox");
expect((await sandboxHeartbeat.getRun(run!.id))?.contextSnapshot).toMatchObject({
executionPolicy: { trustPreset: "low_trust_review", authorizationPolicy: {
trustBoundary: { projectIds: [projectId, referencedProjectId].sort() },
} },
});
expect(call.context.paperclipWorkspaces.map((workspace: { projectId: string }) => workspace.projectId)).toEqual([referencedProjectId]);
directories.push(workspace.cwd!);
await writeFile(path.join(workspace.cwd!, "email.txt"), `private email ${index}`);
@@ -248,14 +255,14 @@ suite("task project repository provisioning", () => {
await expect(readFile(path.join(agentDefaultCwd, "output.txt"), "utf8")).rejects.toThrow();
}, 40_000);
it("keeps a task directory source projectless across chat admissions with an organizational project", async () => {
it.each([false, true])("keeps a task directory source projectless across chat admissions with an organizational project (explicit=%s)", async explicit => {
const companyId = randomUUID(), projectId = randomUUID(), agentId = randomUUID(), issueId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Chat files", issuePrefix: `C${companyId.slice(0, 6)}`, defaultResponsibleUserId: "responsible-user" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "owner" });
await db.insert(projects).values({ id: projectId, companyId, name: "Organization only" });
await db.insert(agents).values({ id: agentId, companyId, name: "Chat agent", status: "idle", adapterType: "codex_local", adapterConfig: {}, permissions: {} });
await db.insert(issues).values({ id: issueId, companyId, projectId, title: "Keep chat files", originKind: "chat_channel", status: "todo", assigneeAgentId: agentId,
workspaceSelection: { version: 1, source: "explicit", selection: { kind: "task_directory" } } });
workspaceSelection: explicit ? { version: 1, source: "explicit", selection: { kind: "task_directory" } } : null });
let bindingId: string | undefined;
for (let admission = 0; admission < 2; admission++) {
await db.update(issues).set({ status: "todo" }).where(eq(issues.id, issueId));
@@ -274,7 +281,7 @@ suite("task project repository provisioning", () => {
}
}, 40_000);
it.each(["task_directory", "configured_source"] as const)("keeps organizational environment separate from %s files", async (kind) => {
it.each(["task_directory", "configured_source", "default"] as const)("keeps organizational environment separate from %s files", async (kind) => {
const companyId = randomUUID(), projectId = randomUUID(), sourceProjectId = randomUUID(), sourceId = randomUUID(), agentId = randomUUID(), issueId = randomUUID();
const cwd = path.join(root, companyId, "source");
await mkdir(cwd, { recursive: true });
@@ -288,7 +295,7 @@ suite("task project repository provisioning", () => {
await db.insert(agents).values({ id: agentId, companyId, name: "Worker", status: "idle", adapterType: "codex_local", adapterConfig: {}, permissions: {} });
await db.insert(issues).values({ id: issueId, companyId, projectId, title: "Use task environment", status: "todo", assigneeAgentId: agentId, executionWorkspaceSettings: { mode: "shared_workspace" },
projectWorkspaceId: kind === "configured_source" ? sourceId : null,
workspaceSelection: { version: 1, source: "explicit", selection: kind === "configured_source"
workspaceSelection: kind === "default" ? null : { version: 1, source: "explicit", selection: kind === "configured_source"
? { kind, projectWorkspaceId: sourceId, mode: "shared" } : { kind } },
});
const run = await heartbeat.wakeup(agentId, { source: "on_demand", triggerDetail: "manual", contextSnapshot: { issueId } });
@@ -303,6 +310,7 @@ suite("task project repository provisioning", () => {
it.each([
{ policy: "organization", selection: "task_directory" },
{ policy: "organization", selection: "default" },
{ policy: "organization", selection: "configured_source" },
{ policy: "source", selection: "configured_source" },
] as const)("retains $policy low-trust restrictions with $selection", async ({ policy, selection }) => {
@@ -320,7 +328,7 @@ suite("task project repository provisioning", () => {
await db.insert(agents).values({ id: agentId, companyId, name: "Worker", status: "idle", adapterType: "codex_local", adapterConfig: {}, permissions: {} });
await db.insert(issues).values({ id: issueId, companyId, projectId, title: "Do not relax policy", status: "todo", assigneeAgentId: agentId, executionWorkspaceSettings: { mode: "shared_workspace" },
projectWorkspaceId: selection === "configured_source" ? sourceId : null,
workspaceSelection: { version: 1, source: "explicit", selection: selection === "configured_source"
workspaceSelection: selection === "default" ? null : { version: 1, source: "explicit", selection: selection === "configured_source"
? { kind: selection, projectWorkspaceId: sourceId, mode: "shared" } : { kind: selection } },
});
const run = await heartbeat.wakeup(agentId, { source: "on_demand", triggerDetail: "manual", contextSnapshot: { issueId } });
@@ -245,7 +245,7 @@ const support = await getEmbeddedPostgresTestSupport();
const [retainedWorkspace] = await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.id, workspaceId));
expect(retainedWorkspace.metadata?._issuePrivacySources).toMatchObject({ [privateSource.id]: true });
// Reauthorization also applies after the second inventory has a durable pin.
await expect(prepareForAdmission(admission)).rejects.toThrow(/outside this actor's access/);
await expect(prepareForAdmission(admission)).rejects.toThrow(/Task workspace access is no longer available/);
expect(await readFile(path.join(prepared[0].cwd, "source.txt"), "utf8")).toBe("uncommitted work\n");
});
it("stages a root change without replacing active files or adding a wake", async () => {
@@ -7,8 +7,8 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import { agents, authUsers, companies, companyMemberships, createDb, executionWorkspaceRepositoryRoots, executionWorkspaceRepositories, executionWorkspaces,
heartbeatRuns, issues, projects, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { agents, authUsers, companies, companyMemberships, createDb, executionWorkspaceRepositoryRoots, executionWorkspaceRepositoryAccess, executionWorkspaceRepositories, executionWorkspaces,
heartbeatRuns, issueAccessGrants, issues, projects, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { executionWorkspaceRepositoryService } from "../services/execution-workspace-repositories.js";
import { buildNativeExecutionInput } from "../services/native-runtime/native-execution-input.js";
import { nativeRuntimeContextFixture } from "../services/native-runtime/runtime-context.test-fixture.js";
@@ -32,12 +32,13 @@ const support = await getEmbeddedPostgresTestSupport();
let db: ReturnType<typeof createDb>;
let root: string;
beforeAll(async () => {
vi.stubEnv("PAPERCLIP_ISSUE_PRIVACY_MODE", "enforce");
database = await startEmbeddedPostgresTestDatabase("paperclip-repository-trust-");
db = createDb(database.connectionString);
root = await mkdtemp(path.join(tmpdir(), "paperclip-repository-trust-"));
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); });
afterAll(async () => { await database?.cleanup(); if (root) await rm(root, { recursive: true, force: true }); });
afterAll(async () => { vi.unstubAllEnvs(); await database?.cleanup(); if (root) await rm(root, { recursive: true, force: true }); });
async function fixture() {
const companyId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), runId = randomUUID();
@@ -84,6 +85,161 @@ const support = await getEmbeddedPostgresTestSupport();
return { workspace, admission: { ...f.admission, cwd, issueId, runId, workspaceId: workspace.id }, actor: { ...f.actor, runId } };
}
async function standardPeer(f: Awaited<ReturnType<typeof fixture>>) {
const userId = randomUUID(), agentId = randomUUID(), issueId = randomUUID(), runId = randomUUID();
await db.insert(authUsers).values({ id: userId, name: "Other sponsor", email: `${userId}@example.test`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() });
await db.insert(agents).values({ id: agentId, companyId: f.companyId, name: "Other worker", adapterType: "process" });
await db.insert(companyMemberships).values([
{ companyId: f.companyId, principalType: "user", principalId: userId, membershipRole: "member", status: "active" },
{ companyId: f.companyId, principalType: "agent", principalId: agentId, membershipRole: "member", status: "active" },
]);
const [workspace] = await db.insert(executionWorkspaces).values({ companyId: f.companyId, name: "Other task files", cwd: f.admission.cwd,
mode: "shared_workspace", strategyType: "operator_directory" }).returning();
await db.insert(issues).values({ id: issueId, companyId: f.companyId, title: "Other task", assigneeAgentId: agentId, executionWorkspaceId: workspace.id });
await db.insert(heartbeatRuns).values({ id: runId, companyId: f.companyId, agentId, responsibleUserId: userId, status: "running", contextSnapshot: { issueId } });
return { userId, agentId, issueId, runId, workspace,
actor: { ...f.actor, agentId, runId, onBehalfOfUserId: userId },
admission: { ...f.admission, agentId, runId, issueId, workspaceId: workspace.id, responsibleUserId: userId } };
}
async function privateTask(f: Awaited<ReturnType<typeof fixture>>) {
await db.update(issues).set({ visibility: "private", privacyRootIssueId: f.issueId, responsibleUserId: f.userId }).where(eq(issues.id, f.issueId));
}
async function grantPeer(f: Awaited<ReturnType<typeof fixture>>, peer: Awaited<ReturnType<typeof standardPeer>>) {
return db.insert(issueAccessGrants).values([
{ issueId: f.issueId, subjectType: "agent", subjectId: peer.agentId, source: "explicit", grantedByUserId: f.userId },
{ issueId: f.issueId, subjectType: "user", subjectId: peer.userId, source: "explicit", grantedByUserId: f.userId },
]).returning();
}
async function localPrivateClone(f: Awaited<ReturnType<typeof fixture>>, beforePublish?: () => Promise<void>) {
const source = path.join(root, `${f.issueId}-privacy-source`);
await mkdir(source);
const exec = promisify(execFileCallback);
await exec("git", ["init", "--initial-branch=main", source]);
await writeFile(path.join(source, "private.txt"), "Private task edits\n");
await exec("git", ["-C", source, "add", "."]);
await exec("git", ["-C", source, "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "source"]);
const actual = await vi.importActual<typeof import("../services/managed-repository-checkout.js")>("../services/managed-repository-checkout.js");
acquisition.materialize.mockImplementationOnce((cwd, input) => actual.materializeManagedProjectWorkspace(cwd, { ...input, localSource: source,
beforePublish: async cloneCwd => { await beforePublish?.(); await input.beforePublish(cloneCwd); } }));
}
it.each(["empty", "different_identity", "unreadable_alias", "anonymous_private_edits", "authorized_peer"] as const)(
"authorizes every physical repository source for a standard reader with %s", async scenario => {
const f = await fixture(), peer = await standardPeer(f);
if (scenario === "different_identity") await f.service.request({ companyId: f.companyId, issueId: peer.issueId, actor: peer.actor,
request: { repository: { kind: "url", url: "https://github.com/public/other" }, requestKey: randomUUID() } });
await privateTask(f);
const intent = await f.request(scenario === "anonymous_private_edits" ? { kind: "url", url: "https://github.com/public/source" } : { kind: "catalog", id: "private-repo" });
await localPrivateClone(f);
const [prepared] = await f.service.prepareForAdmission(f.admission);
expect(await readFile(path.join(prepared.cwd, "private.txt"), "utf8")).toBe("Private task edits\n");
if (scenario === "unreadable_alias") {
const alias = path.join(root, `${f.issueId}-private-alias`);
await symlink(f.admission.cwd, alias);
await db.update(executionWorkspaces).set({ cwd: alias }).where(eq(executionWorkspaces.id, f.workspace.id));
// Published path evidence must also bootstrap provenance for old rows.
await db.delete(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, f.companyId));
const actual = fs.realpath.bind(fs);
vi.spyOn(fs, "realpath").mockImplementation(async pathname => {
if (String(pathname) === alias) throw Object.assign(new Error("Private alias unavailable"), { code: "EACCES" });
return actual(pathname);
});
}
if (scenario === "authorized_peer") {
await grantPeer(f, peer);
await expect(f.service.prepareForAdmission(peer.admission)).resolves.toEqual([]);
} else await expect(f.service.prepareForAdmission(peer.admission)).rejects.toThrow(/workspace access|authorization boundary/);
expect((await f.service.list(f.companyId, f.workspace.id))[0].id).toBe(intent.operationId);
expect(acquisition.materialize).toHaveBeenCalledOnce();
},
);
it("rechecks retained own-workspace privacy without adding an empty-admission membership gate", async () => {
const f = await fixture();
await db.delete(companyMemberships).where(eq(companyMemberships.companyId, f.companyId));
await expect(f.service.prepareForAdmission(f.admission)).resolves.toEqual([]);
await expect(executionWorkspaceRepositoryService(db).prepareForAdmission(f.admission)).resolves.toEqual([]);
const [privateSource] = await db.insert(issues).values({ companyId: f.companyId, title: "Other private writer", visibility: "private",
executionWorkspaceId: f.workspace.id }).returning();
await db.update(issues).set({ executionWorkspaceId: null }).where(eq(issues.id, privateSource.id));
await expect(f.service.prepareForAdmission(f.admission)).rejects.toThrow(/workspace access/);
});
it("does not publish managed bytes to a retained low-trust reader even with private source grants", async () => {
const f = await fixture(), peer = await standardPeer(f);
await db.update(issues).set({ visibility: "private", privacyRootIssueId: peer.issueId, responsibleUserId: peer.userId,
parentId: f.issueId }).where(eq(issues.id, peer.issueId));
await db.insert(issueAccessGrants).values([
{ issueId: peer.issueId, subjectType: "agent", subjectId: f.agentId, source: "explicit", grantedByUserId: peer.userId },
{ issueId: peer.issueId, subjectType: "user", subjectId: f.userId, source: "explicit", grantedByUserId: peer.userId },
]);
await expect(f.service.prepareForAdmission(f.admission)).resolves.toEqual([]);
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId: f.issueId, executionPolicy: f.policy } }).where(eq(heartbeatRuns.id, f.runId));
await expect(f.service.request({ companyId: f.companyId, issueId: peer.issueId, actor: peer.actor,
request: { repository: { kind: "catalog", id: "private-repo" }, requestKey: randomUUID() } })).rejects.toThrow(/anonymous public URLs only/);
expect(await f.service.list(f.companyId, peer.workspace.id)).toEqual([]);
expect(acquisition.materialize).not.toHaveBeenCalled();
});
it("retains private direct files from an admitted workspace with no repository inventory", async () => {
const f = await fixture(), peer = await standardPeer(f);
await privateTask(f);
await expect(f.service.prepareForAdmission(f.admission)).resolves.toEqual([]);
await writeFile(path.join(f.admission.cwd, "private-direct.txt"), "Private direct task output");
await db.update(executionWorkspaces).set({ cwd: path.join(root, "moved-direct-source"), metadata: {} }).where(eq(executionWorkspaces.id, f.workspace.id));
await expect(f.service.prepareForAdmission(peer.admission)).rejects.toThrow(/workspace access|authorization boundary/);
await grantPeer(f, peer);
await expect(f.service.prepareForAdmission(peer.admission)).resolves.toEqual([]);
expect(await readFile(path.join(f.admission.cwd, "private-direct.txt"), "utf8")).toBe("Private direct task output");
expect(acquisition.materialize).not.toHaveBeenCalled();
});
it("blocks a future private source after an empty standard reader was admitted and moved", async () => {
const f = await fixture(), peer = await standardPeer(f);
await expect(f.service.prepareForAdmission(peer.admission)).resolves.toEqual([]);
await db.update(executionWorkspaces).set({ cwd: path.join(root, "moved-standard-reader"), metadata: {}, status: "archived" }).where(eq(executionWorkspaces.id, peer.workspace.id));
await privateTask(f);
await expect(executionWorkspaceRepositoryService(db).request({ companyId: f.companyId, issueId: f.issueId, actor: f.actor,
request: { repository: { kind: "catalog", id: "private-repo" }, requestKey: randomUUID() } })).rejects.toThrow(/workspace access|authorization boundary/);
expect(await f.service.list(f.companyId, f.workspace.id)).toEqual([]);
expect(acquisition.materialize).not.toHaveBeenCalled();
});
it("rechecks configured-source publication against retained readers after grant revocation", async () => {
const f = await fixture(), peer = await standardPeer(f);
await privateTask(f);
const grants = await grantPeer(f, peer);
await expect(f.service.prepareForAdmission(f.admission)).resolves.toEqual([]);
await expect(f.service.prepareForAdmission(peer.admission)).resolves.toEqual([]);
await expect(f.service.guardForPublication(f.admission)).resolves.toBeUndefined();
await db.update(issueAccessGrants).set({ revokedAt: new Date() }).where(eq(issueAccessGrants.id, grants[0].id));
await expect(f.service.guardForPublication(f.admission)).rejects.toThrow(/workspace access|authorization boundary/);
await expect(f.service.guardForPublication({ ...f.admission, workspaceId: peer.workspace.id })).rejects.toThrow(/publication binding/);
expect(acquisition.materialize).not.toHaveBeenCalled();
expect(acquisition.catalog).not.toHaveBeenCalled();
});
it("fences pending private clones and rechecks granted readers before actual publication", async () => {
const f = await fixture(), peer = await standardPeer(f);
await privateTask(f);
const intent = await f.request({ kind: "catalog", id: "private-repo" });
let release!: () => void, started!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
const staged = new Promise<void>(resolve => { started = resolve; });
await localPrivateClone(f, async () => { started(); await gate; });
const preparation = f.service.prepareForAdmission(f.admission);
const outcome = preparation.then(value => ({ value, error: null }), error => ({ value: null, error }));
try {
await Promise.race([staged, outcome.then(() => { throw new Error("Clone ended before publication barrier"); })]);
await expect(f.service.prepareForAdmission(peer.admission)).rejects.toThrow(/workspace access|authorization boundary/);
await grantPeer(f, peer);
await expect(f.service.prepareForAdmission(peer.admission)).resolves.toEqual([]);
await db.update(issueAccessGrants).set({ revokedAt: new Date() }).where(eq(issueAccessGrants.issueId, f.issueId));
} finally { release(); }
expect((await outcome).error).toBeTruthy();
expect(await fs.lstat(path.join(f.admission.cwd, intent.repository.relativePath)).catch(() => null)).toBeNull();
});
it.each(["agent", "issue", "project", "workspace_source_project", "run"] as const)(
"rejects catalog acquisition from raw %s low-trust policy", async source => {
const f = await fixture();
@@ -314,6 +470,7 @@ const support = await getEmbeddedPostgresTestSupport();
it("reserves a legacy low-trust recovery folder after its task binding moves", async () => {
const f = await fixture();
const peer = await lowTrustPeer(f);
await db.update(issues).set({ parentId: peer.admission.issueId }).where(eq(issues.id, f.issueId));
await db.update(issues).set({ executionWorkspaceId: null }).where(eq(issues.id, peer.admission.issueId));
await db.update(executionWorkspaces).set({ cwd: path.join(root, "moved-recovery"), metadata: {}, status: "archived" })
.where(eq(executionWorkspaces.id, peer.workspace.id));
@@ -343,6 +500,7 @@ const support = await getEmbeddedPostgresTestSupport();
const alias = path.join(root, `${f.issueId}-alias`);
await symlink(f.admission.cwd, alias);
const peer = await lowTrustPeer(f, alias);
await db.update(issues).set({ parentId: peer.admission.issueId }).where(eq(issues.id, f.issueId));
const outcomes = await Promise.allSettled([
f.service.prepareForAdmission(peer.admission),
f.request({ kind: "catalog", id: "private-repo" }),
@@ -364,6 +522,8 @@ const support = await getEmbeddedPostgresTestSupport();
// A standard task's initially anonymous intent may later appear in its catalog.
const pending = await f.request({ kind: "url", url: "https://github.com/public/later-catalog" });
const peer = await lowTrustPeer(f);
// This source task belongs to the reader's trusted task subtree.
await db.update(issues).set({ parentId: peer.admission.issueId }).where(eq(issues.id, f.issueId));
await f.service.prepareForAdmission(peer.admission);
await db.update(executionWorkspaces).set({ cwd: path.join(root, "moved-anonymous-workspace"), metadata: {}, status: "archived" })
.where(eq(executionWorkspaces.id, peer.workspace.id));