fix: enforce low-trust runtime issue context

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-05 18:06:39 +00:00
1 parent 89945fd23d
commit a5f2d64e27
2 files changed
+76 -8

No files matched your search

@@ -198,6 +198,47 @@ describeEmbeddedPostgres("workspace runtime service authz helper", () => {
});
});
it("rejects CEO runtime service mutations for low-trust workspace issues without runtime.manage", async () => {
const companyId = await seedCompany();
const { projectId, projectWorkspaceId } = await seedProjectWorkspace(companyId);
const ceoAgentId = await seedAgent(companyId, { role: "ceo", name: "CEO" });
await db.insert(issues).values({
id: randomUUID(),
companyId,
projectId,
projectWorkspaceId,
title: "Issue-scoped low-trust workspace",
status: "todo",
priority: "medium",
assigneeAgentId: ceoAgentId,
executionPolicy: {
authorizationPolicy: {
trustBoundary: {
mode: LOW_TRUST_REVIEW_PRESET,
companyId,
projectIds: [projectId],
},
},
},
});
await expect(assertCanManageProjectWorkspaceRuntimeServices(db, {
actor: {
type: "agent",
agentId: ceoAgentId,
companyId,
source: "agent_key",
},
} as any, {
companyId,
projectWorkspaceId,
})).rejects.toMatchObject({
status: 403,
message: "Low-trust runs cannot manage workspace runtime services unless the boundary grants runtime.manage",
});
});
it("allows agents with a non-terminal assigned issue in the target project workspace", async () => {
const companyId = await seedCompany();
const { projectId, projectWorkspaceId } = await seedProjectWorkspace(companyId);
@@ -117,11 +117,6 @@ async function assertAgentCanManageRuntimeServicesForWorkspace(
});
}
if (actorAgent.role === "ceo") {
return;
}
const eligibleAgentIds = await listReportingSubtreeAgentIds(db, input.companyId, actorAgent.id);
const workspaceScopeConditions = [
input.projectWorkspaceId ? eq(issues.projectWorkspaceId, input.projectWorkspaceId) : null,
input.executionWorkspaceId ? eq(issues.executionWorkspaceId, input.executionWorkspaceId) : null,
@@ -132,6 +127,40 @@ async function assertAgentCanManageRuntimeServicesForWorkspace(
throw forbidden("Missing permission to manage workspace runtime services");
}
const workspaceScopeCondition = workspaceScopeConditions.length === 1
? workspaceScopeConditions[0]!
: or(...workspaceScopeConditions);
const linkedScopeIssues = await db
.select({
id: issues.id,
companyId: issues.companyId,
projectId: issues.projectId,
executionPolicy: issues.executionPolicy,
projectExecutionWorkspacePolicy: projects.executionWorkspacePolicy,
})
.from(issues)
.leftJoin(projects, and(eq(projects.id, issues.projectId), eq(projects.companyId, issues.companyId)))
.where(and(
eq(issues.companyId, input.companyId),
isNull(issues.hiddenAt),
inArray(issues.status, WORKSPACE_RUNTIME_ELIGIBLE_ISSUE_STATUSES),
workspaceScopeCondition,
));
for (const linkedScopeIssue of linkedScopeIssues) {
assertLowTrustCanManageRuntimeForIssue({
actorAgent,
issue: linkedScopeIssue,
projectExecutionWorkspacePolicy: linkedScopeIssue.projectExecutionWorkspacePolicy,
});
}
if (actorAgent.role === "ceo") {
return;
}
const eligibleAgentIds = await listReportingSubtreeAgentIds(db, input.companyId, actorAgent.id);
const linkedIssue = await db
.select({
id: issues.id,
@@ -147,9 +176,7 @@ async function assertAgentCanManageRuntimeServicesForWorkspace(
isNull(issues.hiddenAt),
inArray(issues.status, WORKSPACE_RUNTIME_ELIGIBLE_ISSUE_STATUSES),
inArray(issues.assigneeAgentId, eligibleAgentIds),
workspaceScopeConditions.length === 1
? workspaceScopeConditions[0]!
: or(...workspaceScopeConditions),
workspaceScopeCondition,
))
.then((rows) => rows[0] ?? null);