mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-14 17:40:37 +02:00
fix: align workspace source authorization with admission strategy
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
0049e01678
commit
b014a5ad1e
7 files changed
+142
-55
No files matched your search
@@ -195,6 +195,7 @@ function issueUpdateWithReceipt(issue: ReturnType<typeof makeIssue>, patch: Reco
|
||||
actorRunId: _actorRunId,
|
||||
actorRunStopId: _actorRunStopId,
|
||||
actorUserId: _actorUserId,
|
||||
workspaceSelectionActor: _workspaceSelectionActor,
|
||||
blockedByIssueIds: _blockedByIssueIds,
|
||||
...issuePatch
|
||||
} = patch;
|
||||
|
||||
@@ -62,7 +62,10 @@ it("dispatch retains raw trust before workspace and broker setup", () => {
|
||||
const dispatch = heartbeat.slice(start, end);
|
||||
expect(start).toBeGreaterThan(0);
|
||||
expect(dispatch).toContain(
|
||||
"executionWorkspacePolicy: projectContext.executionWorkspacePolicy",
|
||||
"executionWorkspacePolicy: taskProjectContext.executionWorkspacePolicy",
|
||||
);
|
||||
expect(dispatch).toContain(
|
||||
"executionWorkspacePolicy: workspaceProjectContext.executionWorkspacePolicy",
|
||||
);
|
||||
expect(dispatch).toContain(
|
||||
"context.executionPolicy = retainedTrust.executionPolicy",
|
||||
|
||||
@@ -334,6 +334,56 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
} finally { await settings.updateExperimental({ enableIsolatedWorkspaces: previous.enableIsolatedWorkspaces }); }
|
||||
});
|
||||
|
||||
it.each(["create", "patch", "selection"] as const)("uses the assigned agent's effective source strategy for %s authorization", async operation => {
|
||||
const f = await fixture(), userId = randomUUID(), settings = instanceSettingsService(db);
|
||||
const previous = await settings.getExperimental();
|
||||
await settings.updateExperimental({ enableIsolatedWorkspaces: true, enableIsolatedWorkspacesByDefault: false });
|
||||
try {
|
||||
await db.insert(authUsers).values({ id: userId, name: "Source reader", email: `${userId}@example.test`, createdAt: new Date(), updatedAt: new Date() });
|
||||
await db.insert(companyMemberships).values({ companyId: f.companyId, principalType: "user", principalId: userId, status: "active", membershipRole: "member" });
|
||||
const [isolatedAgent, sharedAgent] = await db.insert(agents).values(["git_worktree", "project_primary"].map(type => ({
|
||||
companyId: f.companyId, name: type, status: "idle", adapterConfig: { workspaceStrategy: { type } },
|
||||
runtimeConfig: { heartbeat: { enabled: false, wakeOnDemand: false } },
|
||||
}))).returning();
|
||||
const [project] = await db.insert(projects).values({ companyId: f.companyId, name: "Read-only source",
|
||||
executionWorkspacePolicy: { authorizationPolicy: { assignmentPolicy: { mode: "protected" } } } }).returning();
|
||||
const [source] = await db.insert(projectWorkspaces).values({ companyId: f.companyId, projectId: project.id, name: "Source", cwd: `/tmp/source-${f.issueId}` }).returning();
|
||||
const actor = { type: "board" as const, source: "session" as const, userId, companyIds: [f.companyId], isInstanceAdmin: false };
|
||||
const sharedOverrides = { adapterConfig: { workspaceStrategy: { type: "project_primary" } } };
|
||||
const tasks = issueService(db), workspaces = executionWorkspaceService(db);
|
||||
if (operation === "create") {
|
||||
const input = { title: "Read source in separate checkout", assigneeAgentId: isolatedAgent.id,
|
||||
projectWorkspaceId: source.id, workspaceSelectionActor: actor, createdByUserId: userId };
|
||||
await expect(tasks.create(f.companyId, { ...input, assigneeAdapterOverrides: sharedOverrides })).rejects.toThrow(/protected/);
|
||||
await expect(tasks.create(f.companyId, input)).resolves.toMatchObject({ assigneeAgentId: isolatedAgent.id, projectWorkspaceId: source.id });
|
||||
await expect(tasks.create(f.companyId, { ...input, workspaceSelection: { kind: "configured_source", projectWorkspaceId: source.id, mode: "managed_isolated" },
|
||||
assigneeAdapterOverrides: sharedOverrides })).rejects.toThrow(/protected/);
|
||||
} else if (operation === "patch") {
|
||||
await db.update(issues).set({ assigneeAgentId: sharedAgent.id }).where(eq(issues.id, f.issueId));
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => { req.actor = actor; next(); });
|
||||
app.use("/api", issueRoutes(db, {} as never)); app.use(errorHandler);
|
||||
const patch = { projectWorkspaceId: source.id, assigneeAgentId: isolatedAgent.id };
|
||||
const denied = await request(app).patch(`/api/issues/${f.issueId}`).send({ ...patch, assigneeAdapterOverrides: sharedOverrides });
|
||||
expect(denied.status, JSON.stringify(denied.body)).toBe(403);
|
||||
expect((await db.select().from(issues).where(eq(issues.id, f.issueId)))[0]).toMatchObject({ assigneeAgentId: sharedAgent.id, projectWorkspaceId: null });
|
||||
const allowed = await request(app).patch(`/api/issues/${f.issueId}`).send(patch);
|
||||
expect(allowed.status, JSON.stringify(allowed.body)).toBe(200);
|
||||
expect((await db.select().from(issues).where(eq(issues.id, f.issueId)))[0]).toMatchObject({ assigneeAgentId: isolatedAgent.id, projectWorkspaceId: source.id });
|
||||
} else {
|
||||
await db.update(issues).set({ assigneeAgentId: isolatedAgent.id, assigneeAdapterOverrides: sharedOverrides }).where(eq(issues.id, f.issueId));
|
||||
const input = { companyId: f.companyId, issueId: f.issueId, actor,
|
||||
selection: { kind: "configured_source" as const, projectWorkspaceId: source.id, mode: "managed_isolated" as const } };
|
||||
await expect(workspaces.validateSelection(input)).rejects.toThrow(/protected/);
|
||||
await db.update(issues).set({ assigneeAdapterOverrides: null }).where(eq(issues.id, f.issueId));
|
||||
await expect(workspaces.validateSelection(input)).resolves.toMatchObject({ projectWorkspaceId: source.id });
|
||||
await db.update(issues).set({ assigneeAgentId: sharedAgent.id }).where(eq(issues.id, f.issueId));
|
||||
await expect(workspaces.validateSelection(input)).rejects.toThrow(/protected/);
|
||||
}
|
||||
} finally { await settings.updateExperimental({ enableIsolatedWorkspaces: previous.enableIsolatedWorkspaces, enableIsolatedWorkspacesByDefault: previous.enableIsolatedWorkspacesByDefault }); }
|
||||
});
|
||||
|
||||
it("requires source project assignment authority for shared files while preserving isolated reads", async () => {
|
||||
const f = await fixture(), svc = executionWorkspaceService(db), tasks = issueService(db);
|
||||
const settings = instanceSettingsService(db), previous = await settings.getExperimental();
|
||||
|
||||
@@ -393,6 +393,29 @@ export function resolveExecutionWorkspaceMode(input: {
|
||||
return "shared_workspace";
|
||||
}
|
||||
|
||||
interface ParsedIssueAssigneeAdapterOverrides {
|
||||
adapterConfig: Record<string, unknown> | null;
|
||||
useProjectWorkspace: boolean | null;
|
||||
}
|
||||
|
||||
export function parseIssueAssigneeAdapterOverrides(
|
||||
raw: unknown,
|
||||
): ParsedIssueAssigneeAdapterOverrides | null {
|
||||
const parsed = parseObject(raw);
|
||||
const parsedAdapterConfig = parseObject(parsed.adapterConfig);
|
||||
const adapterConfig =
|
||||
Object.keys(parsedAdapterConfig).length > 0 ? parsedAdapterConfig : null;
|
||||
const useProjectWorkspace =
|
||||
typeof parsed.useProjectWorkspace === "boolean"
|
||||
? parsed.useProjectWorkspace
|
||||
: null;
|
||||
if (!adapterConfig && useProjectWorkspace === null) return null;
|
||||
return {
|
||||
adapterConfig,
|
||||
useProjectWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSharedWorkspaceConcurrency(raw: unknown): SharedWorkspaceConcurrency | undefined {
|
||||
return raw === "auto" || raw === "serialize" || raw === "allow" ? raw : undefined;
|
||||
}
|
||||
@@ -408,6 +431,7 @@ export function resolveSharedWorkspaceConcurrency(input: {
|
||||
|
||||
export function buildExecutionWorkspaceAdapterConfig(input: {
|
||||
agentConfig: Record<string, unknown>;
|
||||
adapterConfigOverrides?: Record<string, unknown> | null;
|
||||
projectPolicy: ProjectExecutionWorkspacePolicy | null;
|
||||
issueSettings: IssueExecutionWorkspaceSettings | null;
|
||||
mode: ParsedExecutionWorkspaceMode;
|
||||
@@ -450,7 +474,7 @@ export function buildExecutionWorkspaceAdapterConfig(input: {
|
||||
}
|
||||
}
|
||||
|
||||
return nextConfig;
|
||||
return { ...nextConfig, ...input.adapterConfigOverrides };
|
||||
}
|
||||
|
||||
/** Persisted filesystem authority survives UI feature-flag changes. */
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { assertTaskWorkspaceAccess, assertTaskWorkspaceSourceProjectAccess } from "./task-workspace-source-access.js";
|
||||
import { parseObject } from "../adapters/utils.js";
|
||||
import { resolveCoreTrustPreset } from "./trust-preset-resolver.js";
|
||||
import { instanceSettingsService } from "./instance-settings.js";
|
||||
import { taskWorkspaceSelectableCondition } from "./task-workspace-selection.js";
|
||||
import { executionWorkspaceRepositoryService } from "./execution-workspace-repositories.js";
|
||||
import { forbidden } from "../errors.js";
|
||||
@@ -15,6 +18,7 @@ import { and, asc, desc, eq, gt, inArray, isNull, lte, ne, or, sql } from "drizz
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
executionWorkspaces,
|
||||
agents,
|
||||
heartbeatRuns,
|
||||
nativeRunFinalizations,
|
||||
issueComments,
|
||||
@@ -50,7 +54,7 @@ import {
|
||||
normalizeIssueExecutionPolicy,
|
||||
parseIssueExecutionState,
|
||||
} from "./issue-execution-policy.js";
|
||||
import { buildExecutionWorkspaceAdapterConfig, resolveEffectiveWorkspaceStrategyType, parseProjectExecutionWorkspacePolicy, gateProjectExecutionWorkspacePolicy, parseIssueExecutionWorkspaceSettings, resolveExecutionWorkspaceMode } from "./execution-workspace-policy.js";
|
||||
import { buildExecutionWorkspaceAdapterConfig, resolveEffectiveWorkspaceStrategyType, parseProjectExecutionWorkspacePolicy, gateProjectExecutionWorkspacePolicy, parseIssueExecutionWorkspaceSettings, resolveExecutionWorkspaceMode, parseIssueAssigneeAdapterOverrides, applyDefaultIsolatedExecutionWorkspacePolicy } from "./execution-workspace-policy.js";
|
||||
import { issueRecoveryActionService } from "./issue-recovery-actions.js";
|
||||
import { logActivity } from "./activity-log.js";
|
||||
import {
|
||||
@@ -1287,8 +1291,43 @@ type WorkspaceOverviewIssueRow = WorkspaceOverviewLinkedIssue & {
|
||||
const inspectGitForDisplay = createWorkspaceGitInspectionCache(inspectGitCloseReadiness);
|
||||
|
||||
export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServiceOptions = {}) {
|
||||
/** Resolve prospective source use from the same deterministic inputs as admission. */
|
||||
async function resolveSourceWorkspaceStrategy(input: {
|
||||
companyId: string; issueId?: string | null; assigneeAgentId?: string | null;
|
||||
assigneeAdapterOverrides?: unknown; executionWorkspaceSettings?: unknown;
|
||||
executionWorkspacePolicy: unknown; typedSelection?: boolean; projectId?: string | null; executionPolicy?: unknown;
|
||||
}, reader: Db | DbTransaction = db) {
|
||||
const [task] = input.issueId ? await reader.select().from(issues).where(and(
|
||||
eq(issues.id, input.issueId), eq(issues.companyId, input.companyId),
|
||||
)) : [];
|
||||
const assigneeAgentId = task ? task.assigneeAgentId : input.assigneeAgentId;
|
||||
const [agent] = assigneeAgentId ? await reader.select({ adapterConfig: agents.adapterConfig, permissions: agents.permissions }).from(agents)
|
||||
.where(and(eq(agents.id, assigneeAgentId), eq(agents.companyId, input.companyId))) : [];
|
||||
const overrides = assigneeAgentId ? parseIssueAssigneeAdapterOverrides(task ? task.assigneeAdapterOverrides : input.assigneeAdapterOverrides) : null;
|
||||
const experimental = await instanceSettingsService(db).getExperimental();
|
||||
const projectPolicy = applyDefaultIsolatedExecutionWorkspacePolicy({
|
||||
projectPolicy: gateProjectExecutionWorkspacePolicy(parseProjectExecutionWorkspacePolicy(input.executionWorkspacePolicy), experimental.enableIsolatedWorkspaces),
|
||||
defaultIsolatedWorkspacesEnabled: experimental.enableIsolatedWorkspaces && experimental.enableIsolatedWorkspacesByDefault,
|
||||
hasProjectWorkspace: true,
|
||||
});
|
||||
const settingsEnabled = experimental.enableIsolatedWorkspaces || input.typedSelection || task?.workspaceSelection || task?.executionWorkspaceId;
|
||||
const issueSettings = settingsEnabled ? parseIssueExecutionWorkspaceSettings(input.executionWorkspaceSettings) : null;
|
||||
const projectId = task ? task.projectId : input.projectId;
|
||||
const [organization] = projectId ? await reader.select({ companyId: projects.companyId, executionWorkspacePolicy: projects.executionWorkspacePolicy })
|
||||
.from(projects).where(and(eq(projects.id, projectId), eq(projects.companyId, input.companyId))) : [];
|
||||
const trust = resolveCoreTrustPreset({ companyId: input.companyId, agent,
|
||||
project: organization, workspaceSourceProject: { companyId: input.companyId, executionWorkspacePolicy: input.executionWorkspacePolicy },
|
||||
issue: { companyId: input.companyId, executionPolicy: task ? task.executionPolicy : input.executionPolicy } });
|
||||
if (trust.kind === "denied") throw forbidden(trust.detail);
|
||||
const resolvedMode = resolveExecutionWorkspaceMode({ projectPolicy, issueSettings, legacyUseProjectWorkspace: overrides?.useProjectWorkspace ?? null });
|
||||
const mode = trust.kind === "low_trust_review" && resolvedMode === "shared_workspace" ? "isolated_workspace" : resolvedMode;
|
||||
const config = buildExecutionWorkspaceAdapterConfig({ agentConfig: parseObject(agent?.adapterConfig), projectPolicy, issueSettings, mode,
|
||||
legacyUseProjectWorkspace: overrides?.useProjectWorkspace ?? null, adapterConfigOverrides: overrides?.adapterConfig });
|
||||
return resolveEffectiveWorkspaceStrategyType(mode, config);
|
||||
}
|
||||
|
||||
/** Validate intent without allocating files or changing a task binding. */
|
||||
async function validateSelection(input: { companyId: string; actor: AuthorizationActor; selection: TaskWorkspaceSelection; issueId?: string | null; parentIssueId?: string | null; assigneeAgentId?: string | null; assigneeUserId?: string | null }, reader: Db | DbTransaction = db) {
|
||||
async function validateSelection(input: { companyId: string; actor: AuthorizationActor; selection: TaskWorkspaceSelection; issueId?: string | null; parentIssueId?: string | null; assigneeAgentId?: string | null; assigneeUserId?: string | null; assigneeAdapterOverrides?: unknown; projectId?: string | null; executionPolicy?: unknown }, reader: Db | DbTransaction = db) {
|
||||
const selection = taskWorkspaceSelectionSchema.parse(input.selection);
|
||||
if (selection.kind === "existing") {
|
||||
const [workspace] = await reader.select().from(executionWorkspaces).where(and(
|
||||
@@ -1309,11 +1348,10 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
|
||||
));
|
||||
if (!source) throw notFound("Workspace source is unavailable or inaccessible");
|
||||
const mode = selection.mode === "shared" ? "shared_workspace" : "isolated_workspace";
|
||||
const sourceConfig = buildExecutionWorkspaceAdapterConfig({ agentConfig: {},
|
||||
projectPolicy: parseProjectExecutionWorkspacePolicy(source.executionWorkspacePolicy),
|
||||
issueSettings: { mode }, mode, legacyUseProjectWorkspace: null });
|
||||
const strategy = await resolveSourceWorkspaceStrategy({ ...input, typedSelection: true,
|
||||
executionWorkspaceSettings: { mode }, executionWorkspacePolicy: source.executionWorkspacePolicy }, reader);
|
||||
await assertTaskWorkspaceSourceProjectAccess(reader, input.actor, input.companyId, source.projectId, {
|
||||
write: resolveEffectiveWorkspaceStrategyType(mode, sourceConfig) !== "git_worktree",
|
||||
write: strategy !== "git_worktree",
|
||||
issueId: input.issueId, parentIssueId: input.parentIssueId,
|
||||
assigneeAgentId: input.assigneeAgentId, assigneeUserId: input.assigneeUserId,
|
||||
});
|
||||
@@ -1420,7 +1458,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
|
||||
|
||||
/** Ordinary issue edits participate in the same binding revision transaction. */
|
||||
async function assertTaskWorkspaceUpdateAccess(input: {
|
||||
task: typeof issues.$inferSelect; actor: AuthorizationActor; isolatedWorkspacesEnabled: boolean;
|
||||
task: typeof issues.$inferSelect; actor: AuthorizationActor;
|
||||
}, tx: DbTransaction) {
|
||||
const { task, actor } = input;
|
||||
if (task.executionWorkspaceId) {
|
||||
@@ -1433,12 +1471,10 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
|
||||
.where(and(eq(projectWorkspaces.id, task.projectWorkspaceId), eq(projectWorkspaces.companyId, task.companyId),
|
||||
await projectReadSqlCondition(tx, actor)));
|
||||
if (!source) throw notFound("Workspace source is unavailable or inaccessible");
|
||||
const projectPolicy = gateProjectExecutionWorkspacePolicy(parseProjectExecutionWorkspacePolicy(source.executionWorkspacePolicy), input.isolatedWorkspacesEnabled);
|
||||
const issueSettings = parseIssueExecutionWorkspaceSettings(task.executionWorkspaceSettings);
|
||||
const mode = resolveExecutionWorkspaceMode({ projectPolicy, issueSettings, legacyUseProjectWorkspace: null });
|
||||
const config = buildExecutionWorkspaceAdapterConfig({ agentConfig: {}, projectPolicy, issueSettings, mode, legacyUseProjectWorkspace: null });
|
||||
const strategy = await resolveSourceWorkspaceStrategy({ companyId: task.companyId, issueId: task.id,
|
||||
executionWorkspaceSettings: task.executionWorkspaceSettings, executionWorkspacePolicy: source.executionWorkspacePolicy }, tx);
|
||||
await assertTaskWorkspaceSourceProjectAccess(tx, actor, task.companyId, source.projectId, {
|
||||
issueId: task.id, write: resolveEffectiveWorkspaceStrategyType(mode, config) !== "git_worktree",
|
||||
issueId: task.id, write: strategy !== "git_worktree",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2075,7 +2111,7 @@ export function executionWorkspaceService(db: Db, opts: ExecutionWorkspaceServic
|
||||
requestTaskRepository: executionWorkspaceRepositoryService(db).request,
|
||||
listTaskRepositories: executionWorkspaceRepositoryService(db).list,
|
||||
prepareTaskRepositoriesForAdmission: executionWorkspaceRepositoryService(db).prepareForAdmission,
|
||||
validateSelection, inspectTaskWorkspace, selectTaskWorkspace, applyPendingTaskWorkspaceSelection, bindTaskWorkspace, prepareTaskWorkspaceUpdate, assertTaskWorkspaceUpdateAccess,
|
||||
resolveSourceWorkspaceStrategy, validateSelection, inspectTaskWorkspace, selectTaskWorkspace, applyPendingTaskWorkspaceSelection, bindTaskWorkspace, prepareTaskWorkspaceUpdate, assertTaskWorkspaceUpdateAccess,
|
||||
listOverview: async (
|
||||
companyId: string,
|
||||
filters: WorkspaceOverviewQuery,
|
||||
|
||||
@@ -538,6 +538,7 @@ import {
|
||||
import {
|
||||
applyDefaultIsolatedExecutionWorkspacePolicy,
|
||||
buildExecutionWorkspaceAdapterConfig,
|
||||
parseIssueAssigneeAdapterOverrides,
|
||||
gateProjectExecutionWorkspacePolicy,
|
||||
issueExecutionWorkspaceModeForPersistedWorkspace,
|
||||
parseIssueExecutionWorkspaceSettings,
|
||||
@@ -755,33 +756,10 @@ export { resolveReusableSandboxLifecycle } from "./heartbeat/runtime-selection.j
|
||||
|
||||
export { resolveNativeSandboxLifecycle } from "./heartbeat/runtime-selection.js";
|
||||
|
||||
interface ParsedIssueAssigneeAdapterOverrides {
|
||||
adapterConfig: Record<string, unknown> | null;
|
||||
useProjectWorkspace: boolean | null;
|
||||
}
|
||||
|
||||
function readNonEmptyString(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
||||
}
|
||||
|
||||
function parseIssueAssigneeAdapterOverrides(
|
||||
raw: unknown,
|
||||
): ParsedIssueAssigneeAdapterOverrides | null {
|
||||
const parsed = parseObject(raw);
|
||||
const parsedAdapterConfig = parseObject(parsed.adapterConfig);
|
||||
const adapterConfig =
|
||||
Object.keys(parsedAdapterConfig).length > 0 ? parsedAdapterConfig : null;
|
||||
const useProjectWorkspace =
|
||||
typeof parsed.useProjectWorkspace === "boolean"
|
||||
? parsed.useProjectWorkspace
|
||||
: null;
|
||||
if (!adapterConfig && useProjectWorkspace === null) return null;
|
||||
return {
|
||||
adapterConfig,
|
||||
useProjectWorkspace,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatRuntimeWorkspaceWarningLog(warning: string) {
|
||||
return {
|
||||
stream: "stdout" as const,
|
||||
@@ -3142,19 +3120,18 @@ export function heartbeatService(
|
||||
workspaceReuseRequest.existingExecutionWorkspaceAvailable
|
||||
? existingExecutionWorkspace
|
||||
: null;
|
||||
const workspaceManagedConfig = buildExecutionWorkspaceAdapterConfig({
|
||||
const sourceWorkspaceConfig = buildExecutionWorkspaceAdapterConfig({
|
||||
agentConfig: config,
|
||||
projectPolicy: projectExecutionWorkspacePolicy,
|
||||
issueSettings: issueExecutionWorkspaceSettings,
|
||||
mode: requestedExecutionWorkspaceMode,
|
||||
legacyUseProjectWorkspace:
|
||||
issueAssigneeOverrides?.useProjectWorkspace ?? null,
|
||||
adapterConfigOverrides: {
|
||||
...Object.fromEntries(Object.entries(issueAssigneeOverrides?.adapterConfig ?? {}).filter(([key]) => requestedAiBinding?.mode !== "router" || !["provider", "acpxAgent", "model", "modelReasoningEffort", "reasoningEffort", "effort", "variant"].includes(key))),
|
||||
...(requestedAiBinding?.mode === "router" ? parseObject(parseObject(context.aiRouterSelection).runtimeConfig) : {}),
|
||||
},
|
||||
});
|
||||
const sourceWorkspaceConfig = {
|
||||
...workspaceManagedConfig,
|
||||
...Object.fromEntries(Object.entries(issueAssigneeOverrides?.adapterConfig ?? {}).filter(([key]) => requestedAiBinding?.mode !== "router" || !["provider", "acpxAgent", "model", "modelReasoningEffort", "reasoningEffort", "effort", "variant"].includes(key))),
|
||||
...(requestedAiBinding?.mode === "router" ? parseObject(parseObject(context.aiRouterSelection).runtimeConfig) : {}),
|
||||
};
|
||||
const workspaceAuthorizationActor = { type: "agent" as const, agentId: agent.id, companyId: agent.companyId,
|
||||
source: "agent_jwt" as const, runId: run.id, onBehalfOfUserId: responsibleUserId === "local-board" ? null : responsibleUserId };
|
||||
if (!persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId) {
|
||||
|
||||
@@ -123,8 +123,6 @@ import {
|
||||
SUCCESSFUL_RUN_HANDOFF_LIVE_WAKE_STATUSES,
|
||||
} from "./successful-run-handoff-state.js";
|
||||
import {
|
||||
buildExecutionWorkspaceAdapterConfig,
|
||||
resolveEffectiveWorkspaceStrategyType,
|
||||
defaultIssueExecutionWorkspaceSettingsForProject,
|
||||
gateProjectExecutionWorkspacePolicy,
|
||||
issueExecutionWorkspaceModeForPersistedWorkspace,
|
||||
@@ -132,7 +130,6 @@ import {
|
||||
parseIssueExecutionWorkspaceSettings,
|
||||
parseProjectExecutionWorkspacePolicy,
|
||||
resolvePinnedIssueWorkspaceStrategyType,
|
||||
resolveExecutionWorkspaceMode,
|
||||
WORKSPACE_WORKTREE_REQUIRES_PROJECT_CODE,
|
||||
WORKSPACE_WORKTREE_REQUIRES_PROJECT_MESSAGE,
|
||||
WORKSPACE_WORKTREE_REQUIRES_PROJECT_REMEDIATION,
|
||||
@@ -10170,7 +10167,7 @@ export function issueService(db: Db) {
|
||||
const projection = await executionWorkspaceService(db).validateSelection({ companyId,
|
||||
actor: workspaceActor,
|
||||
selection: workspaceSelection, parentIssueId: issueData.parentId,
|
||||
assigneeAgentId: issueData.assigneeAgentId, assigneeUserId: issueData.assigneeUserId }, tx);
|
||||
assigneeAgentId: issueData.assigneeAgentId, assigneeUserId: issueData.assigneeUserId, assigneeAdapterOverrides: issueData.assigneeAdapterOverrides, projectId: issueData.projectId, executionPolicy: issueData.executionPolicy }, tx);
|
||||
projectWorkspaceId = projection.projectWorkspaceId;
|
||||
executionWorkspaceId = projection.executionWorkspaceId;
|
||||
executionWorkspacePreference = projection.executionWorkspacePreference;
|
||||
@@ -10269,14 +10266,13 @@ export function issueService(db: Db) {
|
||||
.innerJoin(projects, eq(projects.id, projectWorkspaces.projectId))
|
||||
.where(and(eq(projectWorkspaces.id, projectWorkspaceId), eq(projectWorkspaces.companyId, companyId), await projectReadSqlCondition(tx, workspaceActor)));
|
||||
if (!readableSource) throw notFound("Workspace source is unavailable or inaccessible");
|
||||
const sourcePolicy = gateProjectExecutionWorkspacePolicy(parseProjectExecutionWorkspacePolicy(readableSource.executionWorkspacePolicy), isolatedWorkspacesEnabled);
|
||||
const sourceSettings = parseIssueExecutionWorkspaceSettings(executionWorkspaceSettings);
|
||||
const sourceMode = resolveExecutionWorkspaceMode({ projectPolicy: sourcePolicy, issueSettings: sourceSettings, legacyUseProjectWorkspace: null });
|
||||
const sourceConfig = buildExecutionWorkspaceAdapterConfig({ agentConfig: {}, projectPolicy: sourcePolicy,
|
||||
issueSettings: sourceSettings, mode: sourceMode, legacyUseProjectWorkspace: null });
|
||||
const sourceStrategy = await executionWorkspaceService(db).resolveSourceWorkspaceStrategy({ companyId,
|
||||
assigneeAgentId: issueData.assigneeAgentId, assigneeAdapterOverrides: issueData.assigneeAdapterOverrides,
|
||||
executionWorkspaceSettings, executionWorkspacePolicy: readableSource.executionWorkspacePolicy,
|
||||
typedSelection: Boolean(workspaceIntent), projectId: issueData.projectId, executionPolicy: issueData.executionPolicy }, tx);
|
||||
await assertTaskWorkspaceSourceProjectAccess(tx, workspaceActor, companyId, readableSource.projectId, {
|
||||
parentIssueId: issueData.parentId, assigneeAgentId: issueData.assigneeAgentId, assigneeUserId: issueData.assigneeUserId,
|
||||
write: !executionWorkspaceId && resolveEffectiveWorkspaceStrategyType(sourceMode, sourceConfig) !== "git_worktree",
|
||||
write: !executionWorkspaceId && sourceStrategy !== "git_worktree",
|
||||
});
|
||||
}
|
||||
if (executionWorkspaceId && workspaceSelectionActor) {
|
||||
@@ -11288,7 +11284,7 @@ export function issueService(db: Db) {
|
||||
// assignment/workspace edit checks the actual new assignee. A denial
|
||||
// rolls back the binding, revision and retained-privacy trigger too.
|
||||
await executionWorkspaceService(db).assertTaskWorkspaceUpdateAccess({
|
||||
task: updated, actor: workspaceSelectionActor, isolatedWorkspacesEnabled,
|
||||
task: updated, actor: workspaceSelectionActor,
|
||||
}, tx);
|
||||
}
|
||||
if (issueData.description !== undefined) await attachOwnedDraftImages(tx, updated, updated.description,
|
||||
|
||||
Reference in new issue
Block a user