diff --git a/.env.development b/.env.development index 53e26dd724..16f7a6fe6a 100644 --- a/.env.development +++ b/.env.development @@ -24,3 +24,5 @@ FLY_ACCESS_TOKEN="encrypted:BDm2qpf85nXBtq7KENGMiwcsFg3Ln/gRcZoUm5busQ8tRgEDXhlX JOB_AUTH_PRIVATE_KEY="encrypted:BM7xBs73k3nh0isBhVELr4LFt2iMiP7XD7knKtEnm7uudQ9DclnL+Hulun8SFFsGzkIp+FyYK47JAMHqyK3NqGHE/93efpACNRNH/YVv0OfUvVw58trPdvdY60hnUrW17z/ae16LlvkBd6roCZxn0GvhgMKPRxlL1B4ZxxtfHfz+yazl0tSsr2t/iMzIhRCtQZ1638PrZJ7ugmsK1lK5g+cQDV91Onth+eVNy6cDX9NAC77iotnzXyOWPyzcxECCSje2jYysAugTTRV2pR/Lnzi506yrWumSv5SpNcDqZjek89CK29nfUJv+UIxxW2dIMIB19ZqjmSU84nIYymVzkwl7kOaDqTIEIMRSzYs/npTWDOGbtB2x49vaD5YeysFiamLlaSzvS46N+uiSrH+8VCujs7HLirOJhtkindVRiV4qfl/eycf9OG2A4jNWhS/uJYcF2QC8KZL1SwKcn7u+FDov1NAYUw25BmeYVvb+KPujsaH1DniqucaxtcGqior1ZjKU5RutbzV5QWknpNAtBD2aVv0HOB7/5bHJpwFl2vaR" JOB_AUTH_PUBLIC_KEY="encrypted:BOU3vH2HFJjRuORyr3psrDpqaXS1hXo2SxA3N2q4M2ihHgHNnbjbKAV3xYHTo9WQA6XGvRWXpo3C/soV6ukMQGkqmvjyCNPVLN3ZlJJlmbBC1qRTn83dejp4YCqV0eb3JOyOhYVpuCCIcYU3s67MRqywTxST1h1PRo3xykpgm3nY0kfZMmHvL7rTh4ik6uVebDhIY7jDrXRWlj/dia/cyEDzB88cBpmCpJx8uqFmK9889+Le4PTb7hLBIv7KvkzMb4TaB4yqpUFd/bawA0fOX5WC85t2DZ+7+Ddqp7s4x6nsNWjh51t0uYGICdF1ReWI1jZ4nKGjxpXcjfmgcXPnOT6PdjOWfMWfMGq+sE7o1GUdu5GKVwATYe4+NXrcMVsTQKdbtRrl4k1lz46jgAegUemGuBxR4Ro6qxK5xhsKeyaMnbrH8f/X0BZHAGOx" + +ROOMOTE_FALLBACK_USER_ID="user_2wbhXyFa2AKzjh5GEqyPz3wGlN3" diff --git a/.env.preview b/.env.preview index 7844a02768..1a64229ea5 100644 --- a/.env.preview +++ b/.env.preview @@ -24,3 +24,5 @@ FLY_ACCESS_TOKEN="encrypted:BCoSnp5DqB0Td27HYtDwIS/dZfZkyvjsgUz+0XcKdovFMr3/ZRZk JOB_AUTH_PRIVATE_KEY="encrypted:BFm76/By4KHU3bILl4tYJOwwHN8xR49C/wcSrh8/0+QAhYgetocntzJ90fYS3+ER2e/Mdh1awOynhbsZ/gpLJC6PmqX+MKzkpv/xJC5AJZTAeyWAKwaL1NhUijAU8mS88/87EvKZCH9Rw/tBGy/7CFrOwipjKQxvuRZjgxmypkV8t6eIEmfGgIbzrjvSyZ6O31FpYliAABeefRpNQo3zd7tV3dX9xtVDt1Hc68vzak0Aw+EJwi+Q7YWvghpcLJxXXo+PYs15Cj9LS8Ww2Ijz/EXO98UIDMN1e947O7hrmTWPC/JVZlGJOctMyNyaX/+rSJP+XepJxkVYeG9jglOLOsIFS+KKiYfnlT3TC5pt9USaAQAsdWvcfT09TN4qYR/eeSfj7mZYNCU4LtIqrFVT65ehntTOokqN7uNSLmSQnzPIO9lhIVcarFz1C9Bx/xE4FaBVKm9agszVcUumJcfktuV/WTxRxgTmhJYbJ08shhrTubWDCxOGdr5NFR+NFN5oMe9KXCOcUjtuHQ8UUSjYwXvZzJOvRAMP0QAv5xzGTlQI" JOB_AUTH_PUBLIC_KEY="encrypted:BPcQ3GgxlCm77QmmsLm5jzFLzCVRuDJzPC7t3jfMbPYNGMBveYIJvrmnpHCNSJIGCrHi0MtFUypvyjjHgQADNN8GaWpyvifk8jTBtp1ptQq736ilxq/Z76KY96uHf59TQCvPR2pjsu7GK0AqL3wg1MV+30+wZzGNo2JYZYczMdYgrMj72d2qsOPydUgPAkXcwRMigNyPhF8JdOT9G5l1w+qRKjRSFynFIXcP61J9kykeYz3BmCIeKw2QopSYz3ie98o+PTmnyQ3KGgEdJ7IVTmV3AglWMJx6Gj6qo8hMW+pUJU790nj9FMkMHHnAIRvhEX9MmInNu+ysVuowKWfxB3IC8A9ktgk3TOZ4/Ub1EZ3rWGVyR9xVFLSYPUlW2s/PfPA8b+KdSd/QDRpGhJ0rd6pl8r33/HOErwu6Mta/4NHIBR9SRj8SrjCNTPG0" + +ROOMOTE_FALLBACK_USER_ID="user_2wbhXyFa2AKzjh5GEqyPz3wGlN3" diff --git a/.env.production b/.env.production index ef3e58191a..fe88c193a9 100644 --- a/.env.production +++ b/.env.production @@ -24,3 +24,5 @@ FLY_ACCESS_TOKEN="encrypted:BFcS7pt6Evj8OG1khLFJkqzg2MI/pP4g4tP1l85vbsiTL1x1Ovjs JOB_AUTH_PRIVATE_KEY="encrypted:BNv2gXyx2FICPfSCJxDJX0ELwt35/Ie/WR0xUai/k9OqEM8p2rhLkRkde45dioGlEPbETZgWa6PDD9mXCR8qBWh4iZziDEwYd8x1uFTx26BBs1MyWk8WhRhjhQMXi0h8V7inVELiPtMcq40aSJ/2S2yTR/qyNlW0YrhQJO1FeU13n5Eelp5PSvmCVDMu+f5m4m5haiJIzvnXbnETOGqatWWNn3qXY/neyfTLlv21E95ulu2bUP1/4iRw4tf0e5LIf4TLeo3M2tSuUc/s7sELb6hNkBtBZCPkQG1+dHci/6MLrer0LiMF3Pv3fUP9ZRfRu8AnOjHuk6xBq3EshbTE+NQoWSf/47F23lHnY+pU+1cT2W+3L21RvZiVoENWC00/udbyNx5GglZmRY8+pZHUwS0l7FFVFtjXJxQigLtMsCijmmZhfz+JAlOouOUoh26HzvB7dcyHnwsXhGwEiG8wmUi8lFCmFosdMp210SRpfrJ/NywObBHYSUXDyELEpS6+GTeZQDmUs44vQ7v58waDHRQvJXGxIw4zK+s+WGlbHTnF" JOB_AUTH_PUBLIC_KEY="encrypted:BAWhQhxInT33caEIc2QQ16wSjJ7SqJ2nYc0FEwQboBfajqh9mp2SVwH/A9CbbM+JrVbqYjNamVCf+BdIJVKn4rTrrV64HVMI5T++EfxPmN59sQWgqb4Ig3DXnoo9UanXGccn23dB/k4ZtweoHSPeVXn8XbZ867TPO3QLI1Xr6fLfgxHZXcyvH6lEZlClJjM+SLRLtixkvKb5c2y1JG6cVQ+dHrvxuh10FdCFzfhkEvPHdv09dWpw6OrPfxBEk+dHpjXWO54p/4WThjJ+41Rpqne0pupLwkILaa/xMj364Q/duTKORUDdnDc+Mk9TMgHuj2M5kQGSSXXuNO4shGxupoA2y5HRI9IhjNKQIq2qikUjzzxjvfHs5ESj6DfJsDq5AwiqU6vD7XcQFbJLaUZc8FioN+gEiHnNvCoi6ZSKjvrCggt5afN+lGVNyROe" + +ROOMOTE_FALLBACK_USER_ID="user_2xjmYpL6p3dfIFLaxJl2WNOSS7X" diff --git a/.env.test b/.env.test index 864fa1930b..cbd30e93cc 100644 --- a/.env.test +++ b/.env.test @@ -23,3 +23,5 @@ FLY_ACCESS_TOKEN="encrypted:BIobJarcf4MpmWDkSjVGjXUH87dNj5rbMqow0/7QNpa1s3eMZeRN JOB_AUTH_PRIVATE_KEY="encrypted:BB5ohcsYq19krr7Rw4CVtS6GcxEbpWPeobkByt0ppr6oU92AgbbdgMdEEyA8IRFpK6H87ud6DdfV4iRnkwART3HhdJJZe06R8MfV6SCxUioz6uwRmagmeJ5/cmnViUihhFiq/NC7qSD/zfRREuhyKXA/Tr1KTllaoQn89PzpfVM42duSRZcSRQ3/vHV62v+MIRFvm2gWqL2mwgr7RtRDpijyzFvSzgerrorHMOPQTYL5uYAcS20lnFEnWzJfDU/hZmqFvyCnxG3u80IlPu+RWN7xLs/WtXZr+5UFP8sgVFRi18+xrNtam3auA163HbC8u0mk3MeYHfKihN35j+2U24oEg+sflFPSm1EfXS+da1wd7GsVfOv/9jRhSrXju1Vg1DI3PSBygiQXpTD23KtOFy5v96RtAGGUeq94Fxti1jTzngaUNxBSCO69L7PL2fo2g2ZML2GlSfSq7R8Q/T7zsjpirbecOLermJnMP0hgrkCTDaqD008iXYte01wVOqgNRuXIXtIqGrsUSf+eGLLP669TqIbNPOSq1tccOtNnPVAH" JOB_AUTH_PUBLIC_KEY="encrypted:BI7/E85qpiZMVZ79gLNRMETFCBoja4wq1Gw03BAWTct3GoCypOsgm2tYvbPOVpe/XKqFot7WVzGhaWrvx9blzrp1+LPAHcChNoa61CeUMtrNAL5VQETZ/9SJU1zLFWpHlCJuviu8G+DlC0UwfAQBdutcwhlcwP8fQSGGXgSaCcnshlhsX/tuqSnyCOrQ2kERXEFK4QT0ngETFdOzVqO0/QHbk6tL/86iv4XQ7Iz2g9XLCdAWXxcLzRn7RE9BXna4eCPahJI8NZx50E3gtD6IsQj7msL1p20PWauOJsm4futPdPNZe2k8plR48PAxTYRHgRHwHJukEqQcNjL5LdXBW0fMNQRBJLYjgxJfAs0cm6ioYXqj0cliDharBGQ71YMjsihsCPZAsozibddiKT02lEtLQOiHOp27wfDNkSOXvspkaaxjHoB4j6cTSQWb" + +ROOMOTE_FALLBACK_USER_ID="test-user-123" diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/__tests__/utils.test.ts b/apps/roomote/src/app/api/webhooks/github/handlers/__tests__/utils.test.ts index 778e6e43ed..e94acb5f71 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/__tests__/utils.test.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/__tests__/utils.test.ts @@ -5,8 +5,10 @@ import { createHmac } from 'crypto'; vi.mock('@roo-code-cloud/db/server', () => ({ db: { insert: vi.fn(), + select: vi.fn(), }, cloudJobs: {}, + orgs: {}, })); vi.mock('@/lib', () => ({ @@ -17,17 +19,35 @@ describe('GitHub Webhook Utils', () => { let verifySignature: typeof import('../utils').verifySignature; let createAndEnqueueJob: typeof import('../utils').createAndEnqueueJob; let isRoomoteMention: typeof import('../utils').isRoomoteMention; - let mockDb: { insert: ReturnType }; + let mockDb: { + insert: ReturnType; + select: ReturnType; + }; let mockEnqueue: ReturnType; beforeEach(async () => { vi.clearAllMocks(); + // Set default environment variable for tests + process.env.ROOMOTE_FALLBACK_USER_ID = 'test-user-123'; + const dbModule = await import('@roo-code-cloud/db/server'); const libModule = await import('@/lib'); - mockDb = dbModule.db as unknown as { insert: ReturnType }; + mockDb = dbModule.db as unknown as { + insert: ReturnType; + select: ReturnType; + }; mockEnqueue = libModule.enqueue as unknown as ReturnType; + // Mock the select chain for organization lookup + mockDb.select.mockReturnValue({ + from: vi.fn().mockReturnValue({ + where: vi.fn().mockReturnValue({ + limit: vi.fn().mockResolvedValue([{ id: 'default-org-id' }]), + }), + }), + }); + const utilsModule = await import('../utils'); verifySignature = utilsModule.verifySignature; createAndEnqueueJob = utilsModule.createAndEnqueueJob; @@ -205,7 +225,7 @@ describe('GitHub Webhook Utils', () => { }); }); - it('should log the enqueued job', async () => { + it('should log the enqueued job with user ID', async () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); const type = 'github.issue.fix'; @@ -259,6 +279,57 @@ describe('GitHub Webhook Utils', () => { createAndEnqueueJob(type, payload, testOrgId), ).rejects.toThrow('Queue service unavailable'); }); + + it('should always include fallback user ID in job creation', async () => { + const type = 'github.issue.fix'; + const payload = { + repo: 'test/repo', + issue: 123, + title: 'Test issue', + body: 'Test body', + }; + + const mockValuesCall = vi.fn().mockReturnValue({ + returning: vi.fn().mockResolvedValue([mockJob]), + }); + mockDb.insert.mockReturnValue({ + values: mockValuesCall, + }); + + await createAndEnqueueJob(type, payload, testOrgId); + + expect(mockValuesCall).toHaveBeenCalledWith({ + type, + payload, + status: 'pending', + orgId: testOrgId, + userId: 'test-user-123', + }); + }); + + it('should throw error when ROOMOTE_FALLBACK_USER_ID is not set', async () => { + const originalEnv = process.env.ROOMOTE_FALLBACK_USER_ID; + delete process.env.ROOMOTE_FALLBACK_USER_ID; + + const type = 'github.issue.fix'; + const payload = { + repo: 'test/repo', + issue: 123, + title: 'Test issue', + body: 'Test body', + }; + + await expect( + createAndEnqueueJob(type, payload, testOrgId), + ).rejects.toThrow( + 'ROOMOTE_FALLBACK_USER_ID environment variable is required but not set', + ); + + // Restore original environment + if (originalEnv !== undefined) { + process.env.ROOMOTE_FALLBACK_USER_ID = originalEnv; + } + }); }); describe('isRoomoteMention', () => { diff --git a/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts b/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts index 7e2810670f..91070a0f37 100644 --- a/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts +++ b/apps/roomote/src/app/api/webhooks/github/handlers/utils.ts @@ -44,9 +44,24 @@ export async function createAndEnqueueJob( throw new Error('Organization ID is required for job creation.'); } + // Require fallback user ID from environment variable + const fallbackUserId = process.env.ROOMOTE_FALLBACK_USER_ID; + + if (!fallbackUserId) { + throw new Error( + 'ROOMOTE_FALLBACK_USER_ID environment variable is required but not set', + ); + } + const [job] = await db .insert(cloudJobs) - .values({ type, payload, status: 'pending', orgId: organizationId }) + .values({ + type, + payload, + status: 'pending', + orgId: organizationId, + userId: fallbackUserId, + }) .returning(); if (!job) { diff --git a/apps/roomote/src/lib/job.ts b/apps/roomote/src/lib/job.ts index ade512fb13..d3dbeb05f7 100644 --- a/apps/roomote/src/lib/job.ts +++ b/apps/roomote/src/lib/job.ts @@ -44,6 +44,7 @@ export async function processJob({ case 'github.issue.fix': result = await fixGitHubIssue( payload as JobPayload<'github.issue.fix'>, + jobId, { onTaskStarted }, mode, ); @@ -52,6 +53,7 @@ export async function processJob({ case 'github.issue.comment.respond': result = await processIssueComment( payload as JobPayload<'github.issue.comment.respond'>, + jobId, { onTaskStarted }, mode, ); @@ -60,6 +62,7 @@ export async function processJob({ case 'github.pr.comment.respond': result = await processPullRequestComment( payload as JobPayload<'github.pr.comment.respond'>, + jobId, { onTaskStarted }, mode, ); @@ -69,7 +72,7 @@ export async function processJob({ const jobPayload = payload as JobPayload<'slack.app.mention'>; const { channel, thread_ts } = jobPayload; - result = await processSlackMention(jobPayload, { + result = await processSlackMention(jobPayload, jobId, { onTaskStarted, onTaskMessage: async (message: ClineMessage) => { console.log(`onTaskMessage (${channel}, ${thread_ts}) ->`, message); diff --git a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts index 2795d9e53a..147950386b 100644 --- a/apps/roomote/src/lib/jobs/fixGitHubIssue.ts +++ b/apps/roomote/src/lib/jobs/fixGitHubIssue.ts @@ -8,6 +8,7 @@ import { export async function fixGitHubIssue( jobPayload: JobPayload<'github.issue.fix'>, + jobId?: number, callbacks?: RunTaskCallbacks, mode?: string, ) { @@ -27,6 +28,7 @@ ${MAIN_BRANCH_PROTECTION} const result = await runTask({ jobType: 'github.issue.fix', jobPayload, + jobId, prompt, callbacks, mode, diff --git a/apps/roomote/src/lib/jobs/processIssueComment.ts b/apps/roomote/src/lib/jobs/processIssueComment.ts index 3d124d9db1..dc1fa8e6f0 100644 --- a/apps/roomote/src/lib/jobs/processIssueComment.ts +++ b/apps/roomote/src/lib/jobs/processIssueComment.ts @@ -8,6 +8,7 @@ import { export async function processIssueComment( jobPayload: JobPayload<'github.issue.comment.respond'>, + jobId?: number, callbacks?: RunTaskCallbacks, mode?: string, ) { @@ -53,6 +54,7 @@ gh api repos/${jobPayload.repo}/issues/${jobPayload.issueNumber}/comments --meth const result = await runTask({ jobType: 'github.issue.comment.respond', jobPayload, + jobId, prompt, callbacks, mode, diff --git a/apps/roomote/src/lib/jobs/processPullRequestComment.ts b/apps/roomote/src/lib/jobs/processPullRequestComment.ts index c5edf1130e..4483b90929 100644 --- a/apps/roomote/src/lib/jobs/processPullRequestComment.ts +++ b/apps/roomote/src/lib/jobs/processPullRequestComment.ts @@ -8,6 +8,7 @@ import { export async function processPullRequestComment( jobPayload: JobPayload<'github.pr.comment.respond'>, + jobId?: number, callbacks?: RunTaskCallbacks, mode?: string, ) { @@ -66,6 +67,7 @@ Do not create a new pull request - work directly on the existing PR branch. const result = await runTask({ jobType: 'github.pr.comment.respond', jobPayload, + jobId, prompt, callbacks, mode, diff --git a/apps/roomote/src/lib/jobs/processSlackMention.ts b/apps/roomote/src/lib/jobs/processSlackMention.ts index 4f5a38c599..2526b0e21d 100644 --- a/apps/roomote/src/lib/jobs/processSlackMention.ts +++ b/apps/roomote/src/lib/jobs/processSlackMention.ts @@ -9,6 +9,7 @@ import { export async function processSlackMention( jobPayload: JobPayload<'slack.app.mention'>, + jobId?: number, callbacks?: RunTaskCallbacks, mode?: string, ) { @@ -48,6 +49,7 @@ ${GIT_WORKFLOW_INSTRUCTIONS} const result = await runTask({ jobType: 'slack.app.mention', jobPayload, + jobId, prompt, callbacks, notify: false, diff --git a/apps/roomote/src/lib/runTask.ts b/apps/roomote/src/lib/runTask.ts index 9f8aa601d6..ce3ac4a5a3 100644 --- a/apps/roomote/src/lib/runTask.ts +++ b/apps/roomote/src/lib/runTask.ts @@ -17,7 +17,7 @@ import { } from '@roo-code/types'; import { IpcClient } from '@roo-code-cloud/ipc'; import { createJobToken } from '@roo-code-cloud/job-auth'; -import { db, users } from '@roo-code-cloud/db/server'; +import { db, cloudJobs } from '@roo-code-cloud/db/server'; import { eq } from 'drizzle-orm'; import type { JobPayload, JobType } from '@roo-code-cloud/db'; @@ -58,7 +58,6 @@ type RunTaskOptions = { jobType: T; jobPayload: JobPayload; jobId?: number; - userId?: string; prompt: string; logger?: Logger; callbacks?: RunTaskCallbacks; @@ -72,7 +71,6 @@ export const runTask = async ({ jobType, jobPayload, jobId, - userId, prompt, logger, callbacks, @@ -92,26 +90,31 @@ export const runTask = async ({ let envVars = `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath}`; - // Create JWT token if we have jobId and userId. - if (jobId && userId) { + if (jobId) { try { - const user = await db - .select() - .from(users) - .where(eq(users.id, userId)) - .limit(1); + const job = await db.query.cloudJobs.findFirst({ + where: eq(cloudJobs.id, jobId), + }); + if (!job) { + throw new Error(`job ${jobId} not found`); + } - const orgId = user[0]?.orgId || null; + const userId = job.userId; - const token = await createJobToken( - jobId.toString(), - userId, - orgId, - TIMEOUT, - ); + if (userId) { + const token = await createJobToken( + jobId.toString(), + userId, + job.orgId, + TIMEOUT, + ); - envVars += ` ROO_CODE_CLOUD_TOKEN=${token}`; - envVars += ` ROO_CODE_CLOUD_ORG_SETTINGS=${Buffer.from(JSON.stringify(ORGANIZATION_DEFAULT)).toString('base64')}`; + envVars += ` ROO_CODE_CLOUD_TOKEN=${token}`; + + envVars += ` ROO_CODE_CLOUD_ORG_SETTINGS=${Buffer.from(JSON.stringify(ORGANIZATION_DEFAULT)).toString('base64')}`; + } else { + logger?.warn(`No userId found for jobId ${jobId}`); + } } catch (error) { logger?.error('Failed to create job token:', error); // Continue without token - job will fall back to no auth.