fix: align low-trust workspace boundary ancestry

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-05 18:06:39 +00:00
1 parent af9e2503f6
commit 3d2cf8fa71
4 files changed
+79 -3

No files matched your search

@@ -111,6 +111,20 @@ function standardTrustResolution(): TrustPresetResolution {
};
}
function buildIssueAncestryDb(rows: Array<{ id: string; companyId: string; parentId: string | null }>) {
const queue = [...rows];
return {
select: () => ({
from: () => ({
where: () => {
const row = queue.shift();
return Promise.resolve(row ? [row] : []);
},
}),
}),
};
}
describe("stripHostWorkspaceProvisionForLowTrustSandbox", () => {
it("removes only the host-side provision command for sandbox-backed low-trust runs", () => {
const config = {
@@ -212,6 +226,24 @@ describe("preflightLowTrustWorkspaceIsolation", () => {
resolveSelectedEnvironmentDriver: async () => "sandbox",
})).resolves.toBe("sandbox");
});
it("allows child issues inside a rootIssueId low-trust boundary during workspace preflight", async () => {
await expect(preflightLowTrustWorkspaceIsolation({
db: buildIssueAncestryDb([
{ id: "issue-child", companyId: "company-1", parentId: "issue-1" },
{ id: "issue-1", companyId: "company-1", parentId: null },
]) as any,
trustPreset: lowTrustResolution(),
isolatedWorkspacesEnabled: true,
effectiveExecutionWorkspaceMode: "isolated_workspace",
issue: {
companyId: "company-1",
id: "issue-child",
projectId: null,
},
resolveSelectedEnvironmentDriver: async () => "sandbox",
})).resolves.toBe("sandbox");
});
});
describe("resolveWorkspaceAfterLowTrustPreflight", () => {
+1
View File
@@ -1039,6 +1039,7 @@ export function issueRoutes(
issue: { id: string; companyId: string; projectId?: string | null; executionPolicy?: unknown },
actor: ReturnType<typeof getActorInfo>,
) {
// Board users are trusted reviewers and intentionally receive raw quarantined output for promotion decisions.
if (actor.actorType !== "agent") return false;
const sourceTrust = await sourceTrustForActorWrite(issue, actor);
return !sourceTrust;
+6 -1
View File
@@ -775,6 +775,7 @@ export function stripHostWorkspaceProvisionForLowTrustSandbox(input: {
}
export async function preflightLowTrustWorkspaceIsolation(input: {
db?: Db;
trustPreset: TrustPresetResolution;
isolatedWorkspacesEnabled: boolean;
effectiveExecutionWorkspaceMode: string | null | undefined;
@@ -790,7 +791,8 @@ export async function preflightLowTrustWorkspaceIsolation(input: {
? await input.resolveSelectedEnvironmentDriver()
: null;
assertLowTrustWorkspaceIsolation({
await assertLowTrustWorkspaceIsolation({
db: input.db,
resolution: input.trustPreset,
isolatedWorkspacesEnabled: input.isolatedWorkspacesEnabled,
effectiveExecutionWorkspaceMode: input.effectiveExecutionWorkspaceMode,
@@ -802,6 +804,7 @@ export async function preflightLowTrustWorkspaceIsolation(input: {
}
export async function resolveWorkspaceAfterLowTrustPreflight<TWorkspace>(input: {
db?: Db;
trustPreset: TrustPresetResolution;
isolatedWorkspacesEnabled: boolean;
effectiveExecutionWorkspaceMode: string | null | undefined;
@@ -810,6 +813,7 @@ export async function resolveWorkspaceAfterLowTrustPreflight<TWorkspace>(input:
resolveWorkspace: () => Promise<TWorkspace>;
}): Promise<{ selectedEnvironmentDriver: string | null; workspace: TWorkspace }> {
const selectedEnvironmentDriver = await preflightLowTrustWorkspaceIsolation({
db: input.db,
trustPreset: input.trustPreset,
isolatedWorkspacesEnabled: input.isolatedWorkspacesEnabled,
effectiveExecutionWorkspaceMode: input.effectiveExecutionWorkspaceMode,
@@ -7666,6 +7670,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver,
workspace: resolvedWorkspace,
} = await resolveWorkspaceAfterLowTrustPreflight({
db,
trustPreset,
isolatedWorkspacesEnabled,
effectiveExecutionWorkspaceMode,
@@ -1,15 +1,46 @@
import { eq } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import { issues } from "@paperclipai/db";
import { unprocessable } from "../errors.js";
import type { TrustPresetResolution } from "./trust-preset-resolver.js";
import { isIssueWithinLowTrustBoundary } from "./trust-preset-resolver.js";
export const LOW_TRUST_RUNTIME_MANAGEMENT_TOOL_CLASS = "runtime.manage";
const LOW_TRUST_ISSUE_ANCESTRY_MAX_DEPTH = 12;
export function isLowTrustRuntimeManagementAllowed(resolution: TrustPresetResolution) {
return resolution.kind === "low_trust_review" &&
(resolution.boundary.allowedToolClasses ?? []).includes(LOW_TRUST_RUNTIME_MANAGEMENT_TOOL_CLASS);
}
export function assertLowTrustWorkspaceIsolation(input: {
async function issueIdIsDescendantOf(db: Db, issueId: string, rootIssueId: string, companyId: string) {
let cursor: string | null = issueId;
// Keep the runtime preflight aligned with authorization while bounding DB work.
for (let depth = 0; cursor && depth < LOW_TRUST_ISSUE_ANCESTRY_MAX_DEPTH; depth += 1) {
if (cursor === rootIssueId) return true;
const row: { id: string; companyId: string; parentId: string | null } | null = await db
.select({ id: issues.id, companyId: issues.companyId, parentId: issues.parentId })
.from(issues)
.where(eq(issues.id, cursor))
.then((rows) => rows[0] ?? null);
if (!row || row.companyId !== companyId) return false;
cursor = row.parentId;
}
return false;
}
async function workspaceIssueWithinLowTrustBoundary(input: {
db?: Db;
boundary: Extract<TrustPresetResolution, { kind: "low_trust_review" }>["boundary"];
issue: { companyId: string; id?: string | null; projectId?: string | null };
}) {
if (isIssueWithinLowTrustBoundary(input.boundary, input.issue)) return true;
if (!input.db || !input.issue.id || !input.boundary.rootIssueId) return false;
return issueIdIsDescendantOf(input.db, input.issue.id, input.boundary.rootIssueId, input.boundary.companyId);
}
export async function assertLowTrustWorkspaceIsolation(input: {
db?: Db;
resolution: TrustPresetResolution;
isolatedWorkspacesEnabled: boolean;
effectiveExecutionWorkspaceMode: string | null | undefined;
@@ -34,7 +65,14 @@ export function assertLowTrustWorkspaceIsolation(input: {
code: "low_trust_requires_isolated_workspace",
});
}
if (!input.issue || !isIssueWithinLowTrustBoundary(input.resolution.boundary, input.issue)) {
if (
!input.issue ||
!(await workspaceIssueWithinLowTrustBoundary({
db: input.db,
boundary: input.resolution.boundary,
issue: input.issue,
}))
) {
throw unprocessable("Low-trust execution issue is outside the active trust boundary.", {
code: "low_trust_boundary_mismatch",
});