diff --git a/server/src/__tests__/routines-service.test.ts b/server/src/__tests__/routines-service.test.ts index 9ffcf04f7e..b634b81330 100644 --- a/server/src/__tests__/routines-service.test.ts +++ b/server/src/__tests__/routines-service.test.ts @@ -612,6 +612,72 @@ describeEmbeddedPostgres("routine service live-execution coalescing", () => { expect(routine.status).toBe("paused"); }); + it("serializes routine detail with assignee identity but without protected agent configuration", async () => { + const { agentId, companyId, routine, svc } = await seedFixture(); + const sentinelSecret = "routine-assignee-secret-sentinel"; + await db + .update(agents) + .set({ + adapterConfig: { + env: { + ROUTINE_ASSIGNEE_SECRET: { type: "plain", value: sentinelSecret }, + }, + }, + runtimeConfig: { + modelProfiles: { + cheap: { + adapterConfig: { + env: { + ROUTINE_ASSIGNEE_RUNTIME_SECRET: { type: "plain", value: sentinelSecret }, + }, + }, + }, + }, + }, + }) + .where(eq(agents.id, agentId)); + const { trigger } = await svc.createTrigger(routine.id, { + kind: "schedule", + label: "Daily", + cronExpression: "0 10 * * *", + timezone: "UTC", + }, {}); + + const detail = await svc.getDetail(routine.id); + + expect(detail).toMatchObject({ + id: routine.id, + companyId, + title: "ascii frog", + assignee: { + id: agentId, + name: "CodexCoder", + role: "engineer", + title: null, + urlKey: "codexcoder", + }, + triggers: [{ + id: trigger.id, + kind: "schedule", + label: "Daily", + cronExpression: "0 10 * * *", + timezone: "UTC", + }], + }); + expect(detail?.assignee).toEqual({ + id: agentId, + name: "CodexCoder", + role: "engineer", + title: null, + urlKey: "codexcoder", + }); + + const serialized = JSON.stringify(detail); + expect(serialized).not.toContain(sentinelSecret); + expect(serialized).not.toContain("adapterConfig"); + expect(serialized).not.toContain("runtimeConfig"); + }); + it("creates revision 1 on routine create and appends revisions for real updates only", async () => { const { routine, svc } = await seedFixture(); diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index 279232552f..f501696b15 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -50,6 +50,7 @@ import { extractRoutineVariableNames, interpolateRoutineTemplate, isValidRoutineDateString, + normalizeAgentUrlKey, pluginOperationIssueOriginKind, routineRevisionSnapshotSchema, stringifyRoutineVariableValue, @@ -644,6 +645,25 @@ export function routineService( .then((rows) => rows[0] ?? null); } + async function getRoutineAgentSummary( + companyId: string, + agentId: string, + ): Promise { + return db + .select({ + id: agents.id, + name: agents.name, + role: agents.role, + title: agents.title, + }) + .from(agents) + .where(and(eq(agents.companyId, companyId), eq(agents.id, agentId))) + .then((rows) => { + const row = rows[0]; + return row ? { ...row, urlKey: normalizeAgentUrlKey(row.name) ?? row.id } : null; + }); + } + async function getManagedRoutineBinding(routine: typeof routines.$inferSelect) { return db .select({ @@ -1979,7 +1999,7 @@ export function routineService( ? db.select().from(projects).where(eq(projects.id, row.projectId)).then((rows) => rows[0] ?? null) : null, row.assigneeAgentId - ? db.select().from(agents).where(eq(agents.id, row.assigneeAgentId)).then((rows) => rows[0] ?? null) + ? getRoutineAgentSummary(row.companyId, row.assigneeAgentId) : null, row.parentIssueId ? issueSvc.getById(row.parentIssueId) : null, getRoutineDescriptionDocument(row.id),