Add support for cloud job auth tokens (#182)

This commit is contained in:
John Richmond 2025-07-03 10:00:37 -07:00 • committed by GitHub
parent 0cbd4caec2
commit e20feaa1ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1662 additions and 81 deletions

View file

@ -38,6 +38,7 @@ COPY packages/config-typescript/package.json ./packages/config-typescript/
COPY packages/env/package.json ./packages/env/
COPY packages/db/package.json ./packages/db/
COPY packages/ipc/package.json ./packages/ipc/
COPY packages/job-auth/package.json ./packages/job-auth/
COPY apps/roomote/package.json ./apps/roomote/
RUN pnpm install
@ -47,6 +48,7 @@ COPY packages/config-typescript ./packages/config-typescript/
COPY packages/env ./packages/env/
COPY packages/db ./packages/db/
COPY packages/ipc ./packages/ipc/
COPY packages/job-auth ./packages/job-auth/
COPY apps/roomote ./apps/roomote/
WORKDIR /roo/apps/roomote

View file

@ -43,6 +43,7 @@ COPY packages/config-typescript/package.json ./packages/config-typescript/
COPY packages/env/package.json ./packages/env/
COPY packages/db/package.json ./packages/db/
COPY packages/ipc/package.json ./packages/ipc/
COPY packages/job-auth/package.json ./packages/job-auth/
COPY apps/roomote/package.json ./apps/roomote/
RUN pnpm install
@ -52,6 +53,7 @@ COPY packages/config-typescript ./packages/config-typescript/
COPY packages/env ./packages/env/
COPY packages/db ./packages/db/
COPY packages/ipc ./packages/ipc/
COPY packages/job-auth ./packages/job-auth/
COPY apps/roomote ./apps/roomote/
WORKDIR /roo/apps/roomote

View file

@ -92,6 +92,7 @@ COPY packages/config-typescript/package.json ./packages/config-typescript/
COPY packages/env/package.json ./packages/env/
COPY packages/db/package.json ./packages/db/
COPY packages/ipc/package.json ./packages/ipc/
COPY packages/job-auth/package.json ./packages/job-auth/
COPY apps/roomote/package.json ./apps/roomote/
RUN pnpm install
@ -101,6 +102,7 @@ COPY packages/config-typescript ./packages/config-typescript/
COPY packages/env ./packages/env/
COPY packages/db ./packages/db/
COPY packages/ipc ./packages/ipc/
COPY packages/job-auth ./packages/job-auth/
COPY apps/roomote ./apps/roomote/
WORKDIR /roo/apps/roomote

View file

@ -20,6 +20,7 @@ OPENROUTER_API_KEY=encrypted:BA3bYeSEXFA0jnw3lmzLKz53ekkGr0K6sDLN6+nFGsa0TdJrYoK
SLACK_API_TOKEN=encrypted:BJyTFw3gugKZr6uaNZaqCH0UpW/mP4S8P1oL5NzeCvoZzSMJEcDrKsW+b+3Ojp0EuqBFI7T8SPUfRMr6ZTSI880+pKYWkmfAAl2jYYE5aw48LGkg+WJTjIW8g2jTEc3PXPYeMzszrJ2Igf8NuyDMLmhI9S9ew5OfhxD6bKwr73WdUKVDJ+MMVBQGIkbrt1PBX30kRuKZq5oqiA==
FLY_ACCESS_TOKEN="encrypted:BDm2qpf85nXBtq7KENGMiwcsFg3Ln/gRcZoUm5busQ8tRgEDXhlXUB4gSUT+Z/XRvwurBcBmYZhkjkDycNQLME53htE8lMLfBqmPTIWFgHhKVZWtlk3EhC7GZcycuLYKZVFuEsvKxu4kH5JibqUtJ5fRjKEbQG1HXuTvwuwGjSahgZC3I+Q1xb/83+nFmHKAr0ibaLHHO/mbZZ4YLSS91dw/BiVPmhaY2eDpdDvrLkYZVJZ28f+JkYocYyNUdaCnUqlzfpQAa6xBTelk9OaNwm3XnCYlm+VssMQpN6z5iOyBDG4Jr2Wx8eNyg9PC5lOptweYRYh2ho3RYW0gDwYujfFfRpMKGaEnCB2l/++iDWB3LY9p3xh8ysJ59OTMb8+luOHJbZdiwbnOgY4WcaF0nJyPNX8aAq22Z/imvFqb5hFnytg5kD2mn4h6KG36NmldAw/zJeyat8j4KQEZSW9pBv/vXN2UtTWnA+vsgnua5SHaHoa2GHN/8HyODbdNPWv1tWnjOhZGbxa1+MzkX/4w6BS9jNG+6Pe4m0YVZMjX/Bv6B8qIrcKqVouBrsKIjN9BSFdQzqlDLk6GsP25Nu/eTQS8+/fuSbI+mn7eo84xHWtawlgej4ydkNDOBVmUV9xgoST8LGq6H6Gq5zvuNyY8IStglpybN1yQzEP2RT+JNM112qQK5EWuWTEGuMq4C8ZIIO8ETgDgDjVBGuEeOVFIHCpXzrwB9HTWf3W4DO7JKeNRBmbYkNBTbr3AqzqRKa/+aTw4G8YTyn0T++AaYKUv9KZAajtZ6TQKL1bkGOvsqBBzRW1/NCyuRxEt7+r/ffx0zo7qLW6ir2gTZuDoPi8kQIu8RxkGJypg8QYZHGtr3yGXxm8IEjPlCYINx2RWZjFr0RDO6FRqlexCnnnj10n+TZJgeEQb0esGXJd20klhYwngi9BnvIWoOPLPPeC+JHeILVAUgAxKjsspF5ck3I/Td2u/"
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"

View file

@ -21,3 +21,6 @@ OPENROUTER_API_KEY=encrypted:BDoXCMz3fV5DXa89cwd77/pZhw6tVIw96LvtyZEFwFQtQV1Qk+Z
SLACK_API_TOKEN=encrypted:BEmAqaFftmVNlZdx19KGXBDtJ1E1SFBJrAH52pwhFR00Eq6DJAOJSBLY+9lV4wsDRnhTaR8QBQ5fx4kX6751mqJ/Ldu9n7zYV+NTV5cWowvTh4jD+w7yufbUvNgnqs1CpvGXysaJoyQCQqQlUJ+wxlf+rb3yEN5CMR9y1Es/d+cXF2d73H2VfBDtVcKf1pCPn+kVeQW9XHquiw==
FLY_ACCESS_TOKEN="encrypted:BCoSnp5DqB0Td27HYtDwIS/dZfZkyvjsgUz+0XcKdovFMr3/ZRZkjLW8GgBpNIdvxYz+/iS8wGPtnFHmD73Kf1+CU5eKNWUTwRaxoD7GyCTXvWwYnNt55unoNpr51rCWTgQ66M8aQGm85gC+948woiL64i5ozwWezkxbPtk4vOM5dbbeIObzfPoPKOqFDOIdFjfbaEFpd6Yuw2BrxCT9HExiK+IOp2xUNEvB4/w4xbzenDDkkyw+kNx0hcFvkefX2EkuSHRS3nPVKMFVH2Pcfc7HuccAbmjIvlvuKMulyiAyJi7XrkyW46ZN7BCRokYR0j7zIqAhQmNgYgs37tbiq+CvqDHbkOEqGIZZIxV+pzONbJ07moAbDBpBeMJ6WeH6gAURJ3mqa3tCyCEFk8TxHMe5lbfjJGuYe4SakrOTMPOMH451gtw5KdUN6m7XFKfL1+MbklpIy/lVgOWAKiyMlxxUmeDShn4ElMIKEGYxY8gOcI5Akyn/3B3tk1RgutTQ7p3gjya5GCf2hKNnOAAjlRzOfDbnObslQZv/ZG1QQ81VLqpvj0S2zMDITwjuGCey6MT9UXrVs1JYaLPsJHiEKlylWuXRenWBaVz2iRdFugRhx1PbisOPkgrqhU5K/B5d8vpsuKOCX65ty8CkJq3KoW6TzzkO2nAm8LJ10Ihv3y+fGRCgUUmjLmSOzg7yG6a89wklCUW3BS3bDdvgEfBMGcdPH/9BTVOQ5wpXZWB9CX4tvPLlXnLyWqH6VWShsfhRRL0MoQ9PXMD9NK61jHIw8RXB1xAASL8B+NeVthj490ymhUHuaL0nBetS38W+tD6jE5IVAyvaR4tvz/4eJz/uxbvgqCJyd4GFW8kyp6blTziksryr4TlLzS7d1F84WKIjUcNyaG5te/L4GixvnPQ5nxFo2Ib464knuljvjdDRxLiOk4H8k7S45OLx2seSsV+KTGlKeN3S013rpXFnG5APUEvz"
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"

View file

@ -21,3 +21,6 @@ OPENROUTER_API_KEY=encrypted:BLBANC6AI/i5gHIeiBaY5lOogEEahxSLAvsxUpk6Z1FXtfrIEj/
SLACK_API_TOKEN=encrypted:BHQC5Lv0k3VnZ0Ez4TcVvL0meJs8R5ysPFa47U3kkjOJstrtDa26F8Fjvd8/Qr7ztiKdJzGXpr9k4/Tm8mL9jZvG7HNH9D+fVHkyGZ2Mf7B97FdXjiDxA3f+Pifj0aTfM7U58tDrKTLeQ/6UzE3Emcxyg4CxKjAjEaRFOAiQxs1lVAtqzbeF7OCV6GHScpSe4zoAaFXvtQ07FA==
FLY_ACCESS_TOKEN="encrypted:BFcS7pt6Evj8OG1khLFJkqzg2MI/pP4g4tP1l85vbsiTL1x1OvjsIqhZvGUm/PkamInYMTVOhWOWHx/s0Iv3F++FfXeWCzRTaaGvCupMgGB5BEEckODT+4GLrsc14NUCT6p+DE/iREzkJFlHjPwj8NergqnUwWmxhNyFDxFK0AZKpZAKSf69/WrAHDN5I907mOm4wUkPag03L9SuhaotCedsv/zqvIfvbr8gxyk7N1vphXaiihVtE+GIaBnHkHjz3eiS/kDtKFPPeZT1lS9yE9T00Bah2hZkSIxpIu4Cd3zAAWYIplqlhDrO32tm89gPSeQfn7ORaHPogK5I8n9ctlKJV8PFO9YoYqaq8TUnkDzvLj7BNk7GpPibodl2mSX8eVspJXCSewKhnbVFA1RNWJ6B3Bqp5n3PmmBaF4n0VCYMzxUHT4UvOfoQGH1GhzVQiQkV0+s8jfD3eRFiovpvOc89WXmuhtGGMwZ0pYrFWg1qTgmUndcfKzob7yzvrlLpJ+kqp6X0c+cO26yOfN+7LfnZt9BeuzzHSEN9+aEWfHv8IK/xqDjT/V8ZoctOGJG/nFFqF7OeYk2Er0AvD+53kwXBCKiBRjx/b2w6s+Z4f71+62alH/Pm62Q3vWGpXIuj8GF7CaHIQD9FH6cegOa7klabrdXjyIETQLPtpKniwVJbaRhEj9p+WOB0dhibZVFX/LtQ8hIt1HuV0TcoB1jLToN835gPuPV5db+Zs0qf3v0lgM4dRM2N3R7d+XdTVZVvfpdJTTNpN9P6AMrHjTHGp2sCDkInymjrhZlpwD3h3XcsnbcGkjTraJsoNsfGGoH1JUyX0AVyF4ec61eFiaFec4RGH2Oxn/rdCq/X4xbqM7ZQhxhhBhkyIMlztRONXYfz8Jqn+f9KK4lxuhg5Ao/xWWyqv5UCeGh2KB32CO1ez3ei9QqD72wyw/NJLLh8XPW1f9TPuFQsfQTG1ftljC2w/XGu"
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"

View file

@ -20,3 +20,6 @@ OPENROUTER_API_KEY=encrypted:BJHyYNaUsHiU6By54yUA2lG48W6NFuafGOr6Sc4XDopmTB9WulF
SLACK_API_TOKEN=encrypted:BDkskSYEb9SYQLpS0LyyOMgS7pVOyha9agMrS15iQUBoeULTzXbfGxEc+w3oEUa7mFyuRW+gHpo1jqEYicc2Nm9xXawDFsfhoTgXkhUdDlAVsJY/jctWtw97Hb9qLvigRrJWyx7ha5wUMg==
FLY_ACCESS_TOKEN="encrypted:BIobJarcf4MpmWDkSjVGjXUH87dNj5rbMqow0/7QNpa1s3eMZeRNoi8oMGn3lBSgPv/hZjCUJJ9t8+5mZNgZk0CrrLUz9W1VxD1hIJwzYlKA6vhR8Ef+A8z6W4H+XBl3zFXPxfvfdy+v"
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"

View file

@ -21,6 +21,7 @@
"@roo-code-cloud/db": "workspace:^",
"@roo-code-cloud/env": "workspace:^",
"@roo-code-cloud/ipc": "workspace:^",
"@roo-code-cloud/job-auth": "workspace:^",
"@roo-code/types": "^1.30.0",
"bullmq": "^5.37.0",
"cmd-ts": "^0.13.0",

View file

@ -15,6 +15,9 @@ import {
EVALS_SETTINGS,
} 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 { eq } from 'drizzle-orm';
import type { JobPayload, JobType } from '@roo-code-cloud/db';
@ -53,6 +56,8 @@ export type RunTaskCallbacks = {
type RunTaskOptions<T extends JobType> = {
jobType: T;
jobPayload: JobPayload<T>;
jobId?: number;
userId?: string;
prompt: string;
logger?: Logger;
callbacks?: RunTaskCallbacks;
@ -64,6 +69,8 @@ type RunTaskOptions<T extends JobType> = {
export const runTask = async <T extends JobType>({
jobType,
jobPayload,
jobId,
userId,
prompt,
logger,
callbacks,
@ -80,9 +87,36 @@ export const runTask = async <T extends JobType>({
const cancelSignal = controller.signal;
const containerized = isFlyMachine() || isDockerContainer();
let envVars = `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath}`;
// Create JWT token if we have jobId and userId
if (jobId && userId) {
try {
// Get user's org info
const user = await db
.select()
.from(users)
.where(eq(users.id, userId))
.limit(1);
const orgId = user[0]?.orgId || null;
const token = await createJobToken(
jobId.toString(),
userId,
orgId,
TIMEOUT,
);
envVars += ` ROO_CODE_CLOUD_TOKEN=${token}`;
} catch (error) {
logger?.error('Failed to create job token:', error);
// Continue without token - job will fall back to no auth
}
}
const codeCommand = containerized
? `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}`
: `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} code --disable-workspace-trust -n ${workspacePath}`;
? `${envVars} xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}`
: `${envVars} code --disable-workspace-trust -n ${workspacePath}`;
if (!logger) {
logger = new Logger({

View file

@ -32,6 +32,7 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@roo-code-cloud/db": "workspace:^",
"@roo-code-cloud/env": "workspace:^",
"@roo-code-cloud/job-auth": "workspace:^",
"@roo-code/types": "^1.30.0",
"@sentry/nextjs": "^9.23.0",
"@tailwindcss/postcss": "^4.1.8",

View file

@ -7,6 +7,7 @@ import { Env } from '@roo-code-cloud/env';
import { type AuthResult, type ApiAuthResult, isOrgRole } from '@/types';
import { logger } from '@/lib/server';
import { validateJobToken } from '@roo-code-cloud/job-auth';
// import {
// type AgentTokenPayload,
// validateAgentToken,
@ -44,64 +45,39 @@ export async function authorize(): Promise<AuthResult> {
* Validates authentication and authorization for API endpoints.
*/
export async function authorizeApi(
_request: NextRequest,
request: NextRequest,
): Promise<ApiAuthResult> {
return authorize();
const authHeader = request.headers.get('authorization');
// const isAgent = request.headers.get('authorization')?.startsWith('Bearer ');
if (!authHeader?.startsWith('Bearer ')) {
return authorize(); // Fall back to Clerk auth
}
// if (!isAgent) {
// return authorize();
// }
const token = authHeader.slice(7);
if (!token) {
return {
success: false,
error: 'Unauthorized: Malformed authorization header',
};
}
// const startTime = Date.now();
// Try job token first
try {
const jobContext = await validateJobToken(token);
return {
success: true,
userType: 'job',
userId: jobContext.userId,
orgId: jobContext.orgId || null,
jobId: jobContext.jobId,
};
} catch {
// If job token validation fails, try agent token
// (existing agent token code would go here)
// try {
// const authHeader = request.headers.get('authorization');
// if (!authHeader?.startsWith('Bearer ')) {
// return {
// success: false,
// error: 'Unauthorized: Missing authorization header',
// };
// }
// const token = authHeader.slice(7);
// if (!token) {
// return {
// success: false,
// error: 'Unauthorized: Malformed authorization header',
// };
// }
// let payload: AgentTokenPayload;
// try {
// payload = await validateAgentToken(token);
// } catch {
// return { success: false, error: 'Unauthorized: Invalid token' };
// }
// const { agent_id: userId, org_id: orgId } = payload;
// updateAgentUsage(
// userId,
// new URL(request.url).pathname,
// request.method,
// 200,
// Date.now() - startTime,
// request.headers.get('user-agent') || undefined,
// );
// return { success: true, userType: 'agent', userId, orgId };
// } catch (error) {
// console.error(
// `authorizeApi: ${error instanceof Error ? error.message : 'Unknown error'}`,
// );
// return { success: false, error: 'Unauthorized: Unexpected error' };
// }
// If both fail, fall back to Clerk
return authorize();
}
}
/**

View file

@ -9,13 +9,13 @@ import {
organizationDefaultSettingsSchema,
organizationCloudSettingsSchema,
ORGANIZATION_ALLOW_ALL,
ORGANIZATION_DEFAULT,
} from '@roo-code/types';
import { AuditLogTargetType, db, orgSettings } from '@roo-code-cloud/db/server';
import { authorize } from './auth';
import { insertAuditLog } from './auditLogs';
import { getOrganizationSettingsByOrgId } from '@/lib/server/organizationSettings';
export async function getOrganizationSettings(): Promise<OrganizationSettings> {
const authResult = await authorize();
@ -24,18 +24,7 @@ export async function getOrganizationSettings(): Promise<OrganizationSettings> {
throw new Error('Unauthorized');
}
// Organization settings are only available for organizations, not personal accounts
if (!authResult.orgId) {
return ORGANIZATION_DEFAULT;
}
const settings = await db
.select()
.from(orgSettings)
.where(eq(orgSettings.orgId, authResult.orgId))
.limit(1);
return settings[0] || ORGANIZATION_DEFAULT;
return getOrganizationSettingsByOrgId(authResult.orgId);
}
/**

View file

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { authorizeApi } from '@/actions/auth';
import { getOrganizationSettings } from '@/actions/organizationSettings';
import { getOrganizationSettingsByOrgId } from '@/lib/server/organizationSettings';
export async function GET(request: NextRequest) {
try {
@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
);
}
const settings = await getOrganizationSettings();
const settings = await getOrganizationSettingsByOrgId(authResult.orgId);
return NextResponse.json(settings);
} catch (error) {

View file

@ -0,0 +1,29 @@
import { eq } from 'drizzle-orm';
import {
type OrganizationSettings,
ORGANIZATION_DEFAULT,
} from '@roo-code/types';
import { db, orgSettings } from '@roo-code-cloud/db/server';
/**
* Fetches organization settings by organization ID.
* Returns default settings for personal accounts (null orgId) or when no settings exist.
*/
export async function getOrganizationSettingsByOrgId(
orgId: string | null,
): Promise<OrganizationSettings> {
// Organization settings are only available for organizations, not personal accounts
if (!orgId) {
return ORGANIZATION_DEFAULT;
}
const settings = await db
.select()
.from(orgSettings)
.where(eq(orgSettings.orgId, orgId))
.limit(1);
return settings[0] || ORGANIZATION_DEFAULT;
}

View file

@ -1,5 +1,6 @@
// import type { NextRequest } from 'next/server';
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
import type { NextRequest } from 'next/server';
const isUnprotectedRoute = createRouteMatcher([
'/sign-in(.*)',
@ -10,18 +11,18 @@ const isUnprotectedRoute = createRouteMatcher([
'/',
]);
// const isApiRoute = createRouteMatcher(['/api(.*)']);
const isApiRoute = createRouteMatcher(['/api(.*)']);
// const isAgentRequest = (req: NextRequest) => {
// return req.headers.get('authorization')?.startsWith('Bearer ');
// };
const isAgentRequest = (req: NextRequest) => {
return req.headers.get('authorization')?.startsWith('Bearer ');
};
export default clerkMiddleware(
async (auth, req) => {
// const isUnprotected =
// isUnprotectedRoute(req) || (isApiRoute(req) && isAgentRequest(req));
const isUnprotected =
isUnprotectedRoute(req) || (isApiRoute(req) && isAgentRequest(req));
if (!isUnprotectedRoute(req)) {
if (!isUnprotected) {
await auth.protect();
}
},

View file

@ -2,7 +2,7 @@
* UserType
*/
const userTypes = ['user', 'agent'] as const;
const userTypes = ['user', 'agent', 'job'] as const;
export type UserType = (typeof userTypes)[number];
@ -48,6 +48,18 @@ export type AgentAuthSuccess = {
orgId: string;
};
export type JobAuthSuccess = {
success: true;
userType: 'job';
userId: string;
orgId: string | null;
jobId: string;
};
export type AuthResult = UserAuthSuccess | AuthError;
export type ApiAuthResult = UserAuthSuccess | AgentAuthSuccess | AuthError;
export type ApiAuthResult =
| UserAuthSuccess
| AgentAuthSuccess
| JobAuthSuccess
| AuthError;

View file

@ -0,0 +1,2 @@
ALTER TABLE "cloud_jobs" ADD COLUMN "user_id" text;--> statement-breakpoint
ALTER TABLE "cloud_jobs" ADD CONSTRAINT "cloud_jobs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,13 @@
"when": 1750703234599,
"tag": "0001_noisy_captain_stacy",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1751397021767,
"tag": "0002_slippery_alex_power",
"breakpoints": true
}
]
}

View file

@ -301,6 +301,7 @@ export const cloudJobs = pgTable('cloud_jobs', {
result: jsonb('result'),
error: text('error'),
slackThreadTs: text('slack_thread_ts'),
userId: text('user_id').references(() => users.id),
startedAt: timestamp('started_at'),
completedAt: timestamp('completed_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),

View file

@ -11,6 +11,8 @@ export const Env = createEnv({
CLICKHOUSE_URL: z.string().min(1),
CLICKHOUSE_PASSWORD: z.string().min(1),
CLERK_SECRET_KEY: z.string().min(1),
JOB_AUTH_PRIVATE_KEY: z.string().min(1),
JOB_AUTH_PUBLIC_KEY: z.string().min(1),
},
client: {},
// You need to destructure all the keys manually.
@ -21,5 +23,7 @@ export const Env = createEnv({
CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
JOB_AUTH_PRIVATE_KEY: process.env.JOB_AUTH_PRIVATE_KEY,
JOB_AUTH_PUBLIC_KEY: process.env.JOB_AUTH_PUBLIC_KEY,
},
});

View file

@ -0,0 +1,4 @@
import { config } from "@roo-code-cloud/config-eslint/base"
/** @type {import("eslint").Linter.Config} */
export default [...config]

View file

@ -0,0 +1,24 @@
{
"name": "@roo-code-cloud/job-auth",
"version": "0.1.0",
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "dotenvx run -f ../../.env.test -- vitest",
"clean": "rimraf .turbo"
},
"dependencies": {
"jsonwebtoken": "^9.0.0",
"zod": "^3.22.0",
"@roo-code-cloud/env": "workspace:*"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@roo-code-cloud/config-eslint": "workspace:^",
"@roo-code-cloud/config-typescript": "workspace:*",
"@types/node": "^24.0.3",
"vitest": "^3.2.4"
}
}

View file

@ -0,0 +1,253 @@
// pnpm test --filter @roo-code-cloud/job-auth
import jwt from 'jsonwebtoken';
import {
createJobToken,
validateJobToken,
type JobTokenPayload,
} from '../index';
describe('job-auth', () => {
const testPrivateKey = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVTWTozeXjKjcNk+g
zTWjJHlt9W3NOY7Wr2egTq3W/2ehRANCAAQdNAio5NZLvXJ2mcNBkhzcAA63g17y
/1uIE2wJYtYN002cxgqqzyWIpZOB0BV9Bm6TIMBbeigHo83EPP4SAuIQ
-----END PRIVATE KEY-----`;
beforeEach(() => {
// Mock Date.now() to return a consistent timestamp
vi.useFakeTimers();
vi.setSystemTime(new Date('2023-01-01T00:00:00Z'));
});
afterEach(() => {
vi.useRealTimers();
});
describe('createJobToken', () => {
it('should create a valid job token', async () => {
const jobId = 'job-123';
const userId = 'user-456';
const orgId = 'org-789';
const timeoutMs = 30 * 60 * 1000; // 30 minutes
const token = await createJobToken(jobId, userId, orgId, timeoutMs);
expect(typeof token).toBe('string');
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
// Verify the token can be decoded and has correct structure
const decoded = jwt.decode(token) as JobTokenPayload;
expect(decoded).toEqual({
iss: 'rcc',
sub: jobId,
exp: 1672531200 + 30 * 60 + 5 * 60, // timeout + grace period
iat: 1672531200, // 2023-01-01T00:00:00Z in seconds
nbf: 1672531200 - 30, // iat - clock skew grace
v: 1,
r: {
u: userId,
o: orgId,
t: 'cj',
},
});
});
it('should create token without orgId when null', async () => {
const jobId = 'job-123';
const userId = 'user-456';
const orgId = null;
const timeoutMs = 30 * 60 * 1000;
const token = await createJobToken(jobId, userId, orgId, timeoutMs);
const decoded = jwt.decode(token) as JobTokenPayload;
expect(decoded.r.o).toBeUndefined();
expect(decoded.r.u).toBe(userId);
});
it('should use correct issuer', async () => {
const token = await createJobToken('job-1', 'user-1', null, 1000);
const decoded = jwt.decode(token) as JobTokenPayload;
expect(decoded.iss).toBe('rcc');
});
it('should set correct expiration time with grace period', async () => {
const timeoutMs = 15 * 60 * 1000; // 15 minutes
const token = await createJobToken('job-1', 'user-1', null, timeoutMs);
const decoded = jwt.decode(token) as JobTokenPayload;
const expectedExp = decoded.iat + Math.floor(timeoutMs / 1000) + 5 * 60; // timeout + 5 min grace
expect(decoded.exp).toBe(expectedExp);
});
});
describe('validateJobToken', () => {
const validPayload: JobTokenPayload = {
iss: 'rcc',
sub: 'job-123',
exp: Math.floor(Date.now() / 1000) + 15 * 60,
iat: Math.floor(Date.now() / 1000),
nbf: Math.floor(Date.now() / 1000) - 30,
v: 1,
r: {
u: 'user-456',
o: 'org-789',
t: 'cj',
},
};
it('should validate and return context for valid token', async () => {
const token = jwt.sign(validPayload, testPrivateKey, {
algorithm: 'ES256',
});
const result = await validateJobToken(token);
expect(result).toEqual({
jobId: 'job-123',
userId: 'user-456',
orgId: 'org-789',
tokenType: 'cj',
version: 1,
});
});
it('should validate token without orgId', async () => {
const payloadWithoutOrg = {
...validPayload,
r: {
u: 'user-456',
t: 'cj' as const,
},
};
const token = jwt.sign(payloadWithoutOrg, testPrivateKey, {
algorithm: 'ES256',
});
const result = await validateJobToken(token);
expect(result.orgId).toBeUndefined();
expect(result.userId).toBe('user-456');
});
it('should throw error for invalid JWT signature', async () => {
// Generate a different private key for testing wrong signature
const wrongPrivateKey = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDCtVZ3X8QqSHLcrp
NYfy87xEuVYS3cU3xztnRADyBJKhRANCAASXyME7rnCKJvIN+W9aYEkOjfCUk4Q6
G4d1mCKQT05YKo5JdhxbGD3gm3lP/U3NhY+GxA6Dy1BTAYy+dR7prjly
-----END PRIVATE KEY-----`;
const tokenWithWrongSecret = jwt.sign(validPayload, wrongPrivateKey, {
algorithm: 'ES256',
});
await expect(validateJobToken(tokenWithWrongSecret)).rejects.toThrow(
'invalid signature',
);
});
it('should throw error for expired JWT', async () => {
const expiredPayload = {
...validPayload,
exp: Math.floor(Date.now() / 1000) - 3600, // expired 1 hour ago
};
const expiredToken = jwt.sign(expiredPayload, testPrivateKey, {
algorithm: 'ES256',
});
await expect(validateJobToken(expiredToken)).rejects.toThrow(
'jwt expired',
);
});
it('should throw error for wrong issuer', async () => {
const wrongIssuerPayload = {
...validPayload,
iss: 'wrong-issuer',
};
const token = jwt.sign(wrongIssuerPayload, testPrivateKey, {
algorithm: 'ES256',
});
await expect(validateJobToken(token)).rejects.toThrow(
'jwt issuer invalid. expected: rcc',
);
});
it('should throw specific error for invalid token structure', async () => {
const invalidPayload = {
...validPayload,
r: {
u: '', // Invalid: empty string
o: 'org-789',
t: 'cj',
},
};
const token = jwt.sign(invalidPayload, testPrivateKey, {
algorithm: 'ES256',
});
await expect(validateJobToken(token)).rejects.toThrow(
'Invalid job token structure: r.u: User ID is required',
);
});
it('should throw error for wrong token type', async () => {
const wrongTypePayload = {
...validPayload,
r: {
u: 'user-456',
o: 'org-789',
t: 'agent', // Should be 'cj'
},
};
const token = jwt.sign(wrongTypePayload, testPrivateKey, {
algorithm: 'ES256',
});
await expect(validateJobToken(token)).rejects.toThrow(
'Invalid job token structure',
);
});
it('should throw error for wrong version', async () => {
const wrongVersionPayload = {
...validPayload,
v: 2, // Should be 1
};
const token = jwt.sign(wrongVersionPayload, testPrivateKey, {
algorithm: 'ES256',
});
await expect(validateJobToken(token)).rejects.toThrow(
'Invalid job token structure: v: Version must be 1',
);
});
it('should validate token created by createJobToken function', async () => {
const jobId = 'test-job-456';
const userId = 'test-user-789';
const orgId = 'test-org-123';
const timeoutMs = 30 * 60 * 1000;
const token = await createJobToken(jobId, userId, orgId, timeoutMs);
const validatedContext = await validateJobToken(token);
expect(validatedContext.jobId).toBe(jobId);
expect(validatedContext.userId).toBe(userId);
expect(validatedContext.orgId).toBe(orgId);
expect(validatedContext.tokenType).toBe('cj');
expect(validatedContext.version).toBe(1);
});
it('should handle malformed JWT', async () => {
const malformedToken = 'not.a.valid.jwt.token';
await expect(validateJobToken(malformedToken)).rejects.toThrow(
'jwt malformed',
);
});
});
});

View file

@ -0,0 +1,74 @@
import jwt from 'jsonwebtoken';
import { Env } from '@roo-code-cloud/env';
import {
jobTokenPayloadSchema,
type JobTokenPayload,
type JobTokenContext,
} from './types';
export * from './types';
const ISSUER = 'rcc';
export async function createJobToken(
jobId: string,
userId: string,
orgId: string | null,
timeoutMs: number,
): Promise<string> {
const now = Math.floor(Date.now() / 1000);
const gracePeriod = 5 * 60; // 5 minutes
const clockSkewGrace = 30; // 30 seconds
const payload: JobTokenPayload = {
iss: ISSUER,
sub: jobId,
exp: now + Math.floor(timeoutMs / 1000) + gracePeriod,
iat: now,
nbf: now - clockSkewGrace,
v: 1,
r: {
u: userId,
o: orgId || undefined,
t: 'cj',
},
};
const privateKey = Buffer.from(Env.JOB_AUTH_PRIVATE_KEY, 'base64').toString(
'utf-8',
);
return jwt.sign(payload, privateKey, { algorithm: 'ES256' });
}
export async function validateJobToken(
token: string,
): Promise<JobTokenContext> {
const publicKey = Buffer.from(Env.JOB_AUTH_PUBLIC_KEY, 'base64').toString(
'utf-8',
);
const rawPayload = jwt.verify(token, publicKey, {
algorithms: ['ES256'],
clockTolerance: 60, // 60 seconds clock skew tolerance for tests
ignoreNotBefore: Env.NODE_ENV === 'test', // Ignore nbf in tests due to fake timers
issuer: ISSUER,
});
const parseResult = jobTokenPayloadSchema.safeParse(rawPayload);
if (!parseResult.success) {
const validationErrors = parseResult.error.errors
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
throw new Error(`Invalid job token structure: ${validationErrors}`);
}
const payload = parseResult.data;
return {
jobId: payload.sub,
userId: payload.r.u,
orgId: payload.r.o,
tokenType: payload.r.t,
version: payload.v,
};
}

View file

@ -0,0 +1,27 @@
import { z } from 'zod';
export const jobTokenPayloadSchema = z.object({
iss: z.string().min(1, 'Issuer (iss) is required'),
sub: z.string().min(1, 'Subject (sub) is required'), // CloudJob ID
exp: z.number().int().positive('Expiration (exp) must be a positive integer'),
iat: z.number().int().positive('Issued at (iat) must be a positive integer'),
nbf: z.number().int().positive('Not before (nbf) must be a positive integer'),
v: z.literal(1, { errorMap: () => ({ message: 'Version must be 1' }) }),
r: z.object({
u: z.string().min(1, 'User ID is required'),
o: z.string().optional(), // Organization ID (optional)
t: z.literal('cj', {
errorMap: () => ({ message: 'Token type must be "cj"' }),
}),
}),
});
export type JobTokenPayload = z.infer<typeof jobTokenPayloadSchema>;
export interface JobTokenContext {
jobId: string;
userId: string;
orgId?: string;
tokenType: 'cj';
version: number;
}

View file

@ -0,0 +1,8 @@
{
"extends": "@roo-code-cloud/config-typescript/base.json",
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
watch: false,
environment: 'node',
},
});

34
pnpm-lock.yaml generated
View file

@ -48,6 +48,9 @@ importers:
'@roo-code-cloud/ipc':
specifier: workspace:^
version: link:../../packages/ipc
'@roo-code-cloud/job-auth':
specifier: workspace:^
version: link:../../packages/job-auth
'@roo-code/types':
specifier: ^1.30.0
version: 1.30.0
@ -209,6 +212,9 @@ importers:
'@roo-code-cloud/env':
specifier: workspace:^
version: link:../../packages/env
'@roo-code-cloud/job-auth':
specifier: workspace:^
version: link:../../packages/job-auth
'@roo-code/types':
specifier: ^1.30.0
version: 1.30.0
@ -531,6 +537,34 @@ importers:
specifier: ^3.2.3
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
packages/job-auth:
dependencies:
'@roo-code-cloud/env':
specifier: workspace:*
version: link:../env
jsonwebtoken:
specifier: ^9.0.0
version: 9.0.2
zod:
specifier: ^3.22.0
version: 3.25.41
devDependencies:
'@roo-code-cloud/config-eslint':
specifier: workspace:^
version: link:../config-eslint
'@roo-code-cloud/config-typescript':
specifier: workspace:*
version: link:../config-typescript
'@types/jsonwebtoken':
specifier: ^9.0.0
version: 9.0.10
'@types/node':
specifier: ^24.0.3
version: 24.0.3
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
packages:
'@adobe/css-tools@4.4.3':