├── .gitignore ├── packages ├── slack-bolt-app │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── util │ │ │ ├── error.ts │ │ │ ├── cost.ts │ │ │ ├── idempotency.ts │ │ │ ├── session.ts │ │ │ ├── session-map.ts │ │ │ ├── auth.ts │ │ │ └── history.ts │ │ ├── lambda.ts │ │ ├── local.ts │ │ ├── handlers │ │ │ └── approve-user.ts │ │ └── async-handler.ts │ └── package.json ├── webapp │ ├── src │ │ ├── jobs │ │ │ ├── async-jobs │ │ │ │ └── index.ts │ │ │ └── async-job-runner.ts │ │ ├── actions │ │ │ ├── api-key │ │ │ │ ├── index.ts │ │ │ │ ├── schemas.ts │ │ │ │ └── actions.ts │ │ │ ├── image │ │ │ │ └── action.ts │ │ │ └── upload │ │ │ │ └── action.ts │ │ ├── app │ │ │ ├── api │ │ │ │ ├── health │ │ │ │ │ ├── route.ts │ │ │ │ │ └── warm │ │ │ │ │ │ └── route.ts │ │ │ │ ├── auth │ │ │ │ │ ├── [slug] │ │ │ │ │ │ └── route.ts │ │ │ │ │ └── api-key.ts │ │ │ │ └── cognito-token │ │ │ │ │ └── route.ts │ │ │ ├── favicon.ico │ │ │ ├── preferences │ │ │ │ ├── schemas.ts │ │ │ │ ├── components │ │ │ │ │ ├── PreferenceSection.tsx │ │ │ │ │ └── PromptForm.tsx │ │ │ │ ├── actions.ts │ │ │ │ └── page.tsx │ │ │ ├── auth-callback │ │ │ │ └── page.tsx │ │ │ ├── sessions │ │ │ │ ├── (root) │ │ │ │ │ ├── actions.ts │ │ │ │ │ └── page.tsx │ │ │ │ ├── new │ │ │ │ │ ├── schemas.ts │ │ │ │ │ └── template-actions.ts │ │ │ │ └── [workerId] │ │ │ │ │ ├── schemas.ts │ │ │ │ │ ├── component │ │ │ │ │ └── UrlRenderer.tsx │ │ │ │ │ └── actions.ts │ │ │ ├── custom-agent │ │ │ │ ├── schemas.ts │ │ │ │ ├── actions.ts │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── sign-in │ │ │ │ └── page.tsx │ │ ├── i18n │ │ │ ├── config.ts │ │ │ ├── request.ts │ │ │ └── db.ts │ │ ├── lib │ │ │ ├── utils.ts │ │ │ ├── jobs.ts │ │ │ ├── origin.ts │ │ │ ├── auth.ts │ │ │ ├── amplifyServerUtils.ts │ │ │ ├── safe-action.ts │ │ │ ├── events.ts │ │ │ └── message-formatter.ts │ │ ├── components │ │ │ ├── RefreshOnFocus.tsx │ │ │ ├── ui │ │ │ │ ├── sonner.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ └── button.tsx │ │ │ └── ThemeToggle.tsx │ │ ├── utils │ │ │ └── session-status.ts │ │ ├── middleware.ts │ │ └── hooks │ │ │ ├── use-event-bus.ts │ │ │ └── use-scroll-position.ts │ ├── postcss.config.mjs │ ├── run.sh │ ├── .env.local.example │ ├── components.json │ ├── eslint.config.mjs │ ├── tsconfig.json │ ├── .gitignore │ ├── next.config.ts │ └── package.json ├── agent-core │ ├── .gitignore │ ├── src │ │ ├── tools │ │ │ ├── todo │ │ │ │ ├── index.ts │ │ │ │ └── todo-update.ts │ │ │ ├── index.ts │ │ │ ├── command-execution │ │ │ │ ├── suggestion.ts │ │ │ │ └── github.ts │ │ │ ├── report-progress │ │ │ │ └── index.ts │ │ │ ├── think │ │ │ │ └── index.ts │ │ │ ├── read-image │ │ │ │ └── index.ts │ │ │ └── send-image │ │ │ │ └── index.ts │ │ ├── lib │ │ │ ├── aws │ │ │ │ ├── ec2.ts │ │ │ │ ├── index.ts │ │ │ │ ├── s3.ts │ │ │ │ ├── ddb.ts │ │ │ │ └── ssm.ts │ │ │ ├── images.ts │ │ │ ├── index.ts │ │ │ ├── worker-id.ts │ │ │ ├── webapp-origin.ts │ │ │ ├── metadata.ts │ │ │ ├── preferences.ts │ │ │ ├── cost.test.ts │ │ │ ├── prompt.ts │ │ │ ├── api-key.ts │ │ │ ├── cost.ts │ │ │ └── events.ts │ │ ├── schema │ │ │ ├── README.md │ │ │ ├── api-key.ts │ │ │ ├── todo.ts │ │ │ ├── index.ts │ │ │ ├── message.ts │ │ │ ├── preferences.ts │ │ │ ├── mcp.ts │ │ │ ├── agent.ts │ │ │ ├── session.ts │ │ │ └── events.ts │ │ └── private │ │ │ └── common │ │ │ └── lib.ts │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── worker │ ├── .gitignore │ ├── vitest.config.ts │ ├── src │ │ ├── main.ts │ │ ├── common │ │ │ ├── status.ts │ │ │ ├── signal-handler.ts │ │ │ ├── cancellation-token.ts │ │ │ ├── refresh-session.ts │ │ │ ├── ec2.ts │ │ │ └── kill-timer.ts │ │ ├── agent-core.ts │ │ ├── local.ts │ │ └── agent │ │ │ └── mcp │ │ │ └── index.ts │ ├── .env.local │ ├── mcp.json │ ├── README.md │ ├── compose.yml │ ├── run.sh │ ├── package.json │ └── scripts │ │ └── setup-dynamodb-local.ts └── github-actions │ ├── src │ ├── lib │ │ ├── context.ts │ │ ├── trigger.ts │ │ └── permission.ts │ ├── handlers │ │ ├── issue-assignment.ts │ │ └── pr-assignment.ts │ └── index.ts │ ├── tsconfig.json │ └── package.json ├── .prettierignore ├── cdk ├── lib │ └── constructs │ │ ├── auth │ │ ├── .gitignore │ │ └── prefix-generator.js │ │ ├── worker │ │ ├── resources │ │ │ ├── .gitignore │ │ │ ├── bus-event-handler.mjs │ │ │ ├── versioning-handler.js │ │ │ └── image-component-template.yml │ │ └── bus.ts │ │ ├── lambda-warmer │ │ ├── lambda │ │ │ ├── type.ts │ │ │ └── handler.ts │ │ └── index.ts │ │ ├── cf-lambda-furl-service │ │ ├── lambda │ │ │ └── sign-payload.ts │ │ └── edge-function.ts │ │ ├── ec2-gc │ │ ├── index.ts │ │ └── sfn │ │ │ └── index.ts │ │ ├── storage.ts │ │ └── async-job.ts ├── .npmignore ├── .gitignore ├── jest.config.js ├── .dockerignore ├── test │ ├── snapshot-plugin.ts │ └── cdk.test.ts ├── README.md ├── tsconfig.json ├── .env.local.example └── package.json ├── docs └── imgs │ ├── concept.png │ ├── ss-chat.png │ ├── ss-cost.png │ ├── ss-list.png │ ├── ss-new.png │ ├── example1.png │ ├── example2.png │ ├── example3.png │ ├── example4.png │ └── architecture.png ├── docker ├── worker.Dockerfile ├── agent.Dockerfile.dockerignore ├── worker.Dockerfile.dockerignore ├── slack-bolt-app.Dockerfile.dockerignore ├── job.Dockerfile.dockerignore ├── webapp.Dockerfile.dockerignore ├── slack-bolt-app.Dockerfile ├── job.Dockerfile ├── webapp.Dockerfile └── agent.Dockerfile ├── .prettierrc ├── .github ├── ISSUE_TEMPLATE │ └── request-for-agents.md ├── workflows │ ├── remote-swe.yml │ └── deploy-prod.yml └── oidc │ └── template.yml ├── CODE_OF_CONDUCT.md ├── package.json ├── LICENSE ├── resources ├── slack-app-manifest.json ├── slack-app-manifest-relaxed.json └── assume-role.sh └── action.yml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env.local 3 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/webapp/src/jobs/async-jobs/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | cdk/cdk.out 2 | .next 3 | dist 4 | -------------------------------------------------------------------------------- /packages/agent-core/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /cdk/lib/constructs/auth/.gitignore: -------------------------------------------------------------------------------- 1 | !prefix-generator.js 2 | -------------------------------------------------------------------------------- /packages/worker/.gitignore: -------------------------------------------------------------------------------- 1 | !.env.local 2 | dist 3 | docker 4 | -------------------------------------------------------------------------------- /packages/webapp/src/actions/api-key/index.ts: -------------------------------------------------------------------------------- 1 | export * from './actions'; 2 | -------------------------------------------------------------------------------- /cdk/lib/constructs/worker/resources/.gitignore: -------------------------------------------------------------------------------- 1 | *image-component.yml 2 | !versioning-handler.js 3 | -------------------------------------------------------------------------------- /cdk/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /docs/imgs/concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/concept.png -------------------------------------------------------------------------------- /docs/imgs/ss-chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/ss-chat.png -------------------------------------------------------------------------------- /docs/imgs/ss-cost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/ss-cost.png -------------------------------------------------------------------------------- /docs/imgs/ss-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/ss-list.png -------------------------------------------------------------------------------- /docs/imgs/ss-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/ss-new.png -------------------------------------------------------------------------------- /docs/imgs/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/example1.png -------------------------------------------------------------------------------- /docs/imgs/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/example2.png -------------------------------------------------------------------------------- /docs/imgs/example3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/example3.png -------------------------------------------------------------------------------- /docs/imgs/example4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/example4.png -------------------------------------------------------------------------------- /packages/agent-core/src/tools/todo/index.ts: -------------------------------------------------------------------------------- 1 | export * from './todo-init'; 2 | export * from './todo-update'; 3 | -------------------------------------------------------------------------------- /docker/worker.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/alpine AS builder 2 | WORKDIR /build 3 | COPY ./ ./ 4 | -------------------------------------------------------------------------------- /docs/imgs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/docs/imgs/architecture.png -------------------------------------------------------------------------------- /packages/agent-core/src/lib/aws/ec2.ts: -------------------------------------------------------------------------------- 1 | import { EC2Client } from '@aws-sdk/client-ec2'; 2 | export const ec2 = new EC2Client(); 3 | -------------------------------------------------------------------------------- /packages/webapp/src/app/api/health/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET() { 2 | return new Response('ok', { status: 200 }); 3 | } 4 | -------------------------------------------------------------------------------- /packages/webapp/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /packages/webapp/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/remote-swe-agents/HEAD/packages/webapp/src/app/favicon.ico -------------------------------------------------------------------------------- /cdk/lib/constructs/lambda-warmer/lambda/type.ts: -------------------------------------------------------------------------------- 1 | export type LambdaWarmerPayload = { 2 | url: string; 3 | concurrency: number; 4 | }; 5 | -------------------------------------------------------------------------------- /docker/agent.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !packages/agent-core 3 | !packages/worker 4 | !package*.json 5 | **/node_modules 6 | **/dist 7 | -------------------------------------------------------------------------------- /docker/worker.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !packages/agent-core 3 | !packages/worker 4 | !package*.json 5 | **/node_modules 6 | **/dist 7 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/README.md: -------------------------------------------------------------------------------- 1 | The content of this directory must be able to imported from both Node.js and browser environment. 2 | -------------------------------------------------------------------------------- /packages/webapp/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | [ ! -d '/tmp/cache' ] && mkdir -p /tmp/cache 4 | 5 | exec node packages/webapp/server.js 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 120 7 | } 8 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/aws/index.ts: -------------------------------------------------------------------------------- 1 | export * from './s3'; 2 | export * from './ddb'; 3 | export * from './ec2'; 4 | export * from './ssm'; 5 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/README.md: -------------------------------------------------------------------------------- 1 | # Slack Bolt interface 2 | 3 | This is a Slack bolt app that works as an interface between you and agents. 4 | -------------------------------------------------------------------------------- /packages/worker/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: {}, 5 | }); 6 | -------------------------------------------------------------------------------- /docker/slack-bolt-app.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !packages/agent-core 3 | !packages/slack-bolt-app 4 | !package*.json 5 | **/node_modules 6 | **/dist 7 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/util/error.ts: -------------------------------------------------------------------------------- 1 | export class NonRetryableError extends Error {} 2 | 3 | export class ValidationError extends NonRetryableError {} 4 | -------------------------------------------------------------------------------- /docker/job.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !packages/agent-core 3 | !packages/webapp 4 | !package*.json 5 | **/node_modules 6 | **/dist 7 | **/.next 8 | **/.env.local 9 | -------------------------------------------------------------------------------- /packages/webapp/src/i18n/config.ts: -------------------------------------------------------------------------------- 1 | export const locales = ['en', 'ja'] as const; 2 | export type Locale = (typeof locales)[number]; 3 | export const defaultLocale: Locale = 'en'; 4 | -------------------------------------------------------------------------------- /packages/agent-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['src/**/*.test.ts'], 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/github-actions/src/lib/context.ts: -------------------------------------------------------------------------------- 1 | export interface ActionContext { 2 | triggerPhrase: string; 3 | assigneeTrigger?: string; 4 | apiBaseUrl: string; 5 | apiKey: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/worker/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Entrypoint for EC2. This file is named `main.ts` for backward compatibility. 3 | */ 4 | import { main } from './entry'; 5 | 6 | main(process.env.WORKER_ID!); 7 | -------------------------------------------------------------------------------- /packages/webapp/src/app/api/health/warm/route.ts: -------------------------------------------------------------------------------- 1 | import { setTimeout } from 'timers/promises'; 2 | 3 | export async function GET() { 4 | await setTimeout(200); 5 | return new Response('ok', { status: 200 }); 6 | } 7 | -------------------------------------------------------------------------------- /packages/webapp/src/app/preferences/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // Schema for prompt saving 4 | export const savePromptSchema = z.object({ 5 | additionalSystemPrompt: z.string().optional(), 6 | }); 7 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /docker/webapp.Dockerfile.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !packages/agent-core 3 | !packages/webapp 4 | !package*.json 5 | !patches 6 | **/node_modules 7 | **/dist 8 | **/.next 9 | **/.env.local 10 | **/next-env.d.ts 11 | **/tsconfig.tsbuildinfo 12 | -------------------------------------------------------------------------------- /cdk/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | .env.local 11 | 12 | # ignore cdk.context.json because it is an OSS project. 13 | cdk.context.json 14 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/images.ts: -------------------------------------------------------------------------------- 1 | import { extname } from 'path'; 2 | 3 | export const getAttachedImageKey = (workerId: string, toolUseId: string, filePath: string) => { 4 | const ext = extname(filePath); 5 | return `${workerId}/${toolUseId}${ext}`; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/util/cost.ts: -------------------------------------------------------------------------------- 1 | // Import from agent-core to ensure we use the same cost calculation logic 2 | import { calculateCost } from '@remote-swe-agents/agent-core/lib'; 3 | 4 | // Re-export for backwards compatibility 5 | export { calculateCost }; 6 | -------------------------------------------------------------------------------- /packages/webapp/.env.local.example: -------------------------------------------------------------------------------- 1 | COGNITO_DOMAIN=auth.example.com 2 | APP_ORIGIN=http://localhost:3011 3 | USER_POOL_CLIENT_ID=dummy 4 | USER_POOL_ID=us-west-2_dummy 5 | NEXT_PUBLIC_EVENT_HTTP_ENDPOINT="" 6 | NEXT_PUBLIC_AWS_REGION="us-west-2" 7 | ASYNC_JOB_HANDLER_ARN="" 8 | -------------------------------------------------------------------------------- /packages/webapp/src/actions/api-key/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const createApiKeySchema = z.object({ 4 | description: z.string().optional(), 5 | }); 6 | 7 | export const deleteApiKeySchema = z.object({ 8 | apiKey: z.string(), 9 | }); 10 | -------------------------------------------------------------------------------- /packages/webapp/src/app/api/auth/[slug]/route.ts: -------------------------------------------------------------------------------- 1 | import { createAuthRouteHandlers } from '@/lib/amplifyServerUtils'; 2 | 3 | export const GET = createAuthRouteHandlers({ 4 | redirectOnSignInComplete: '/auth-callback', 5 | redirectOnSignOutComplete: '/sign-in', 6 | }); 7 | -------------------------------------------------------------------------------- /cdk/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest', 7 | }, 8 | snapshotSerializers: ['/test/snapshot-plugin.ts'], 9 | }; 10 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/api-key.ts: -------------------------------------------------------------------------------- 1 | export interface ApiKeyItem { 2 | PK: 'api-key'; 3 | SK: string; // API key string (32 bytes hex encoded) 4 | LSI1: string; // createdAt timestamp formatted for sorting 5 | createdAt: number; 6 | description?: string; 7 | ownerId?: string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { receiver } from './app'; 2 | 3 | export const handler = async (event: any, context: any, callback: any) => { 4 | const handler = await receiver.start(); 5 | console.log(JSON.stringify(event)); 6 | return handler(event, context, callback); 7 | }; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request-for-agents.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request for Agents 3 | about: Use this template when you request AI agents 4 | title: "(directory): " 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Related Files 11 | 12 | ## Spec 13 | 1. a 14 | * 1 15 | 2. b 16 | * 2 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/todo.ts: -------------------------------------------------------------------------------- 1 | export interface TodoItem { 2 | id: string; 3 | description: string; 4 | status: 'pending' | 'in_progress' | 'completed' | 'cancelled'; 5 | createdAt: number; 6 | updatedAt: number; 7 | } 8 | 9 | export interface TodoList { 10 | items: TodoItem[]; 11 | lastUpdated: number; 12 | } 13 | -------------------------------------------------------------------------------- /packages/worker/.env.local: -------------------------------------------------------------------------------- 1 | AWS_REGION=ap-northeast-1 2 | EVENT_HTTP_ENDPOINT= 3 | TABLE_NAME=RemoteSweStack-Sandbox-StorageHistory 4 | BUCKET_NAME=remoteswestack-prod-storageimagebucket99ba9550-xxxxxx 5 | BEDROCK_AWS_ACCOUNTS= 6 | BEDROCK_AWS_ROLE_NAME=bedrock-remote-swe-role 7 | GITHUB_PERSONAL_ACCESS_TOKEN='dummy' 8 | DYNAMODB_ENDPOINT=http://localhost:8000 9 | -------------------------------------------------------------------------------- /cdk/.dockerignore: -------------------------------------------------------------------------------- 1 | # common .dockerignore for cdk asset exclude 2 | # We use this file because * ignore pattern in each docker/*.dockerignore file does not work with cdk assets. 3 | .git 4 | .github 5 | cdk 6 | docs 7 | resources 8 | **/node_modules 9 | **/dist 10 | **/.next 11 | **/*.md 12 | **/.env.local 13 | **/next-env.d.ts 14 | **/tsconfig.tsbuildinfo 15 | -------------------------------------------------------------------------------- /cdk/test/snapshot-plugin.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test: (val: any) => typeof val === 'string', 3 | serialize: (val: any) => { 4 | return `"${val // 5 | .replace(/([A-Fa-f0-9]{64}.zip)/, 'REDACTED') 6 | .replace(/:([A-Fa-f0-9]{64})/, ':REDACTED') 7 | .replace(/.*cdk-hnb659fds-container-assets-.*/, 'REDACTED')}"`; 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | // This file is meant to be imported from both sever and client side. 2 | 3 | export * from './events'; 4 | export * from './todo'; 5 | export * from './agent'; 6 | export * from './session'; 7 | export * from './message'; 8 | export * from './api-key'; 9 | export * from './mcp'; 10 | export * from './model'; 11 | export * from './preferences'; 12 | -------------------------------------------------------------------------------- /packages/agent-core/src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ci'; 2 | export * from './command-execution'; 3 | export * from './create-pr'; 4 | export * from './editor'; 5 | export * from './github-comments'; 6 | export * from './repo'; 7 | export * from './report-progress'; 8 | export * from './send-image'; 9 | export * from './think'; 10 | export * from './read-image'; 11 | export * from './todo'; 12 | -------------------------------------------------------------------------------- /packages/worker/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "fetch": { 4 | "command": "uvx", 5 | "args": ["mcp-server-fetch"] 6 | }, 7 | "playwright": { 8 | "command": "npx", 9 | "args": ["@playwright/mcp@latest", "--headless", "--browser=chromium"] 10 | }, 11 | "DeepWiki": { 12 | "url": "https://mcp.deepwiki.com/sse", 13 | "enabled": false 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/local.ts: -------------------------------------------------------------------------------- 1 | import { exit } from 'process'; 2 | import app from './app'; 3 | 4 | const isTest = process.env.TESTING_BOOTSTRAP; 5 | 6 | (async () => { 7 | // Initialize and start your app 8 | await app.start(process.env.PORT || 3000); 9 | 10 | console.log('⚡️ Bolt app is running!'); 11 | 12 | if (isTest == 'true') { 13 | console.log('Successfully booted the api.'); 14 | exit(0); 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /packages/webapp/src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { getUserLocale } from './db'; 2 | import { getRequestConfig } from 'next-intl/server'; 3 | 4 | export const locales = ['en', 'ja'] as const; 5 | export type Locale = (typeof locales)[number]; 6 | export default getRequestConfig(async () => { 7 | const locale = await getUserLocale(); 8 | 9 | return { 10 | locale, 11 | messages: (await import(`../messages/${locale}.json`)).default, 12 | }; 13 | }); 14 | -------------------------------------------------------------------------------- /cdk/lib/constructs/lambda-warmer/lambda/handler.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from 'aws-lambda'; 2 | import { LambdaWarmerPayload } from './type'; 3 | 4 | export const handler: Handler = async (event, context) => { 5 | const { url, concurrency } = event; 6 | 7 | console.log(`warming ${url} with concurrency ${concurrency}...`); 8 | 9 | await Promise.all( 10 | new Array(concurrency).fill(0).map(async () => { 11 | await fetch(url); 12 | }) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/worker/src/common/status.ts: -------------------------------------------------------------------------------- 1 | import { updateSessionAgentStatus, sendWebappEvent } from '@remote-swe-agents/agent-core/lib'; 2 | 3 | /** 4 | * Updates the agent status and sends a corresponding webapp event 5 | */ 6 | export async function updateAgentStatusWithEvent(workerId: string, status: 'working' | 'pending'): Promise { 7 | await updateSessionAgentStatus(workerId, status); 8 | await sendWebappEvent(workerId, { 9 | type: 'agentStatusUpdate', 10 | status, 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /packages/worker/README.md: -------------------------------------------------------------------------------- 1 | # Worker 2 | 3 | This is the agent implementation that works in its own EC2 environment. 4 | 5 | ## Run locally 6 | 7 | You can run the agent locally using the below command. Note that you must provide `BUCKET_NAME` and `TABLE_NAME` using the actual ARN. 8 | 9 | ```sh 10 | cd packages/common 11 | npm run watch 12 | ``` 13 | 14 | ```sh 15 | cd packages/worker 16 | npm run setup:local 17 | npm run start:local 18 | 19 | # access http://localhost:8001 for DynamoDB Admin 20 | ``` 21 | -------------------------------------------------------------------------------- /packages/agent-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2023", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "declarationMap": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "types": ["vitest/globals", "node"] 14 | }, 15 | "include": ["src/**/*.ts"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/github-actions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "declaration": true, 13 | "rootDir": "./src" 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules", "dist"] 17 | } 18 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/jobs.ts: -------------------------------------------------------------------------------- 1 | import { JobPayloadProps } from '@/jobs/async-job-runner'; 2 | import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda'; 3 | 4 | const lambda = new LambdaClient(); 5 | 6 | const handlerArn = process.env.ASYNC_JOB_HANDLER_ARN!; 7 | 8 | export async function runJob(props: JobPayloadProps) { 9 | await lambda.send( 10 | new InvokeCommand({ 11 | FunctionName: handlerArn, 12 | InvocationType: 'Event', 13 | Payload: JSON.stringify(props), 14 | }) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/worker/src/common/signal-handler.ts: -------------------------------------------------------------------------------- 1 | import { closeMcpServers } from '../agent/mcp'; 2 | 3 | const exit = async (signal: string) => { 4 | console.log(`${signal} received. Now shutting down ... please wait`); 5 | setTimeout(() => { 6 | process.exit(0); 7 | }, 3000); 8 | await closeMcpServers(); 9 | }; 10 | 11 | process.on('SIGHUP', () => { 12 | exit('SIGHUP'); 13 | }); 14 | 15 | process.on('SIGINT', () => { 16 | exit('SIGINT'); 17 | }); 18 | 19 | process.on('SIGTERM', () => { 20 | exit('SIGTERM'); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/webapp/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /packages/webapp/src/components/RefreshOnFocus.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { useEffect } from 'react'; 5 | 6 | export function RefreshOnFocus() { 7 | const { refresh } = useRouter(); 8 | 9 | useEffect(() => { 10 | const onFocus = () => { 11 | refresh(); 12 | }; 13 | 14 | window.addEventListener('focus', onFocus); 15 | 16 | return () => { 17 | window.removeEventListener('focus', onFocus); 18 | }; 19 | }, [refresh]); 20 | 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prompt'; 2 | export * from './slack'; 3 | export * from './messages'; 4 | export * from './metadata'; 5 | export * from './converse'; 6 | export * from './sessions'; 7 | export * from './worker-id'; 8 | export * from './worker-manager'; 9 | export * from './events'; 10 | export * from './cost'; 11 | export * from './todo'; 12 | export * from './api-key'; 13 | export * from './images'; 14 | export * from './webapp-origin'; 15 | export * from './preferences'; 16 | export * from './custom-agent'; 17 | -------------------------------------------------------------------------------- /packages/webapp/src/app/auth-callback/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { getSession, UserNotCreatedError } from '@/lib/auth'; 3 | 4 | export const dynamic = 'force-dynamic'; 5 | 6 | export default async function AuthCallbackPage() { 7 | try { 8 | await getSession(); 9 | } catch (e) { 10 | console.log(e); 11 | if (e instanceof UserNotCreatedError) { 12 | const userId = e.userId; 13 | console.log(userId); 14 | } else { 15 | throw e; 16 | } 17 | } 18 | redirect('/'); 19 | } 20 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/origin.ts: -------------------------------------------------------------------------------- 1 | import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; 2 | 3 | if (process.env.APP_ORIGIN_SOURCE_PARAMETER && !process.env.APP_ORIGIN) { 4 | const ssm = new SSMClient({ region: process.env.AWS_REGION }); 5 | try { 6 | const res = await ssm.send(new GetParameterCommand({ Name: process.env.APP_ORIGIN_SOURCE_PARAMETER })); 7 | process.env.APP_ORIGIN = res.Parameter?.Value; 8 | } catch (e) { 9 | console.log(e); 10 | } 11 | } 12 | 13 | export const AppOrigin = process.env.APP_ORIGIN; 14 | -------------------------------------------------------------------------------- /packages/webapp/src/i18n/db.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import { cookies } from 'next/headers'; 3 | import { hasLocale } from 'next-intl'; 4 | import { defaultLocale, Locale, locales } from './config'; 5 | const COOKIE_NAME = 'NEXT_LOCALE'; 6 | 7 | export async function getUserLocale(): Promise { 8 | const candidate = (await cookies()).get(COOKIE_NAME)?.value; 9 | return hasLocale(locales, candidate) ? candidate : defaultLocale; 10 | } 11 | 12 | export async function setUserLocale(locale: string) { 13 | (await cookies()).set(COOKIE_NAME, locale); 14 | } 15 | -------------------------------------------------------------------------------- /packages/agent-core/src/tools/command-execution/suggestion.ts: -------------------------------------------------------------------------------- 1 | import { ciTool } from '../ci'; 2 | 3 | export const generateSuggestion = (command: string, success: boolean): string | undefined => { 4 | const suggestion: string[] = []; 5 | if (command.toLowerCase().includes('git push')) { 6 | if (success) { 7 | suggestion.push( 8 | 'Remember, when you push git commits to a pull request, make sure you check the CI status and fix the code until it passes.' 9 | ); 10 | } 11 | } 12 | return suggestion.join('\n') || undefined; 13 | }; 14 | -------------------------------------------------------------------------------- /cdk/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `npx cdk deploy` deploy this stack to your default AWS account/region 13 | * `npx cdk diff` compare deployed stack with current state 14 | * `npx cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /packages/webapp/src/app/sessions/(root)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { updateSessionVisibility } from '@remote-swe-agents/agent-core/lib'; 4 | import { authActionClient } from '@/lib/safe-action'; 5 | import { z } from 'zod'; 6 | 7 | const hideSessionSchema = z.object({ 8 | workerId: z.string(), 9 | }); 10 | 11 | export const hideSessionAction = authActionClient 12 | .inputSchema(hideSessionSchema) 13 | .action(async ({ parsedInput, ctx }) => { 14 | const { workerId } = parsedInput; 15 | await updateSessionVisibility(workerId, true); 16 | return { success: true }; 17 | }); 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remote-swe-agents", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "build": "npm run build --workspaces", 9 | "test": "npm run test --workspaces", 10 | "format": "prettier --write \"**/*.{ts,tsx,js,jsx,mjs,mts}\"", 11 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mjs,mts}\"", 12 | "postinstall": "patch-package" 13 | }, 14 | "devDependencies": { 15 | "prettier": "^3.5.1", 16 | "typescript": "^5.7.3" 17 | }, 18 | "dependencies": { 19 | "patch-package": "^8.0.1", 20 | "zod": "^4.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/message.ts: -------------------------------------------------------------------------------- 1 | import { ModelType } from './model'; 2 | 3 | export type MessageItem = { 4 | /** 5 | * message-${workerId}` 6 | */ 7 | PK: `message-${string}`; 8 | /** 9 | * chronologically-sortable key (usually stringified timestamp) 10 | */ 11 | SK: string; 12 | /** 13 | * messsage.content in json string 14 | */ 15 | content: string; 16 | role: string; 17 | tokenCount: number; 18 | messageType: string; 19 | slackUserId?: string; 20 | /** 21 | * Thinking budget in tokens when ultrathink is enabled 22 | */ 23 | thinkingBudget?: number; 24 | modelOverride?: ModelType; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/webapp/src/app/sessions/new/schemas.ts: -------------------------------------------------------------------------------- 1 | import { modelTypeSchema } from '@remote-swe-agents/agent-core/schema'; 2 | import { z } from 'zod'; 3 | 4 | export const createNewWorkerSchema = z.object({ 5 | message: z.string().min(1), 6 | imageKeys: z.array(z.string()).optional(), 7 | modelOverride: modelTypeSchema.optional(), 8 | customAgentId: z.string().optional(), 9 | }); 10 | 11 | export const promptTemplateSchema = z.object({ 12 | PK: z.literal('prompt-template'), 13 | SK: z.string(), 14 | content: z.string(), 15 | createdAt: z.number(), 16 | }); 17 | 18 | export type PromptTemplate = z.infer; 19 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/preferences.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { modelTypeSchema } from './model'; 3 | 4 | export const globalPreferencesSchema = z.object({ 5 | PK: z.literal('global-config'), 6 | SK: z.literal('general'), 7 | modelOverride: modelTypeSchema.default('sonnet3.7'), 8 | enableLinkInPr: z.boolean().default(false), 9 | updatedAt: z.number().default(0), 10 | }); 11 | 12 | export const updateGlobalPreferenceSchema = z.object({ 13 | modelOverride: modelTypeSchema.optional(), 14 | enableLinkInPr: z.boolean().optional(), 15 | }); 16 | 17 | export type GlobalPreferences = z.infer; 18 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/mcp.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | 3 | export const mcpConfigSchema = z.object({ 4 | mcpServers: z.record( 5 | z.string(), 6 | z.union([ 7 | z.object({ 8 | command: z.string(), 9 | args: z.array(z.string()), 10 | env: z.record(z.string(), z.string()).optional(), 11 | enabled: z.boolean().optional(), 12 | }), 13 | z.object({ 14 | url: z.string(), 15 | enabled: z.boolean().optional(), 16 | }), 17 | ]) 18 | ), 19 | }); 20 | 21 | export type McpConfig = z.infer; 22 | export const EmptyMcpConfig: McpConfig = { mcpServers: {} }; 23 | -------------------------------------------------------------------------------- /packages/webapp/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { FlatCompat } from '@eslint/eslintrc'; 4 | import { defineConfig } from 'eslint/config'; 5 | 6 | const __filename = fileURLToPath(import.meta.url); 7 | const __dirname = dirname(__filename); 8 | 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | }); 12 | 13 | export default defineConfig([ 14 | ...compat.extends('next/core-web-vitals', 'next/typescript'), 15 | { 16 | rules: { 17 | '@typescript-eslint/no-unused-vars': 'off', 18 | '@typescript-eslint/no-explicit-any': 'off', 19 | }, 20 | }, 21 | ]); 22 | -------------------------------------------------------------------------------- /packages/github-actions/src/lib/trigger.ts: -------------------------------------------------------------------------------- 1 | import { ActionContext } from './context'; 2 | 3 | export function shouldTriggerAction(context: ActionContext, comment: string): boolean { 4 | return comment.includes(context.triggerPhrase); 5 | } 6 | 7 | export function shouldTriggerForAssignee(context: ActionContext, assignees: string[]): boolean { 8 | // If no assignee trigger is specified, do nothing 9 | if (!context.assigneeTrigger) { 10 | return false; 11 | } 12 | 13 | // If assignee trigger is specified, only allow the specified assignee 14 | const targetAssignee = context.assigneeTrigger.replace('@', ''); 15 | return assignees.some((assignee) => assignee === targetAssignee); 16 | } 17 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/aws/s3.ts: -------------------------------------------------------------------------------- 1 | import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'; 2 | 3 | export const s3 = new S3Client(); 4 | export const BucketName = process.env.BUCKET_NAME!; 5 | 6 | export const getBytesFromKey = async (key: string) => { 7 | const { Body } = await s3.send( 8 | new GetObjectCommand({ 9 | Bucket: BucketName, 10 | Key: key, 11 | }) 12 | ); 13 | return Body!.transformToByteArray(); 14 | }; 15 | 16 | export const writeBytesToKey = async (key: string, bytes: Uint8Array) => { 17 | await s3.send( 18 | new PutObjectCommand({ 19 | Bucket: BucketName, 20 | Key: key, 21 | Body: bytes, 22 | }) 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner, ToasterProps } from 'sonner'; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = 'system' } = useTheme(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /packages/github-actions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-actions", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w", 9 | "start": "npx tsx src/index.ts", 10 | "format": "prettier --write \"src/**/*.ts\"", 11 | "format:check": "prettier --check \"src/**/*.ts\"" 12 | }, 13 | "dependencies": { 14 | "@actions/core": "^1.11.1", 15 | "@actions/github": "^6.0.0", 16 | "@octokit/rest": "^22.0.0", 17 | "@remote-swe-agents/agent-core": "file:../agent-core", 18 | "tsx": "^4.19.2" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^22.13.1", 22 | "typescript": "^5.7.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/webapp/src/jobs/async-job-runner.ts: -------------------------------------------------------------------------------- 1 | import { Handler } from 'aws-lambda'; 2 | import { z } from 'zod'; 3 | 4 | const jobPayloadPropsSchema = z.discriminatedUnion('type', [ 5 | z.object({ 6 | type: z.literal('example'), 7 | }), 8 | ]); 9 | 10 | export type JobPayloadProps = z.infer; 11 | 12 | export const handler: Handler = async (event, context) => { 13 | const { data: payload, error } = jobPayloadPropsSchema.safeParse(event); 14 | if (error) { 15 | console.log(error); 16 | throw new Error(error.toString()); 17 | } 18 | 19 | switch (payload.type) { 20 | case 'example': 21 | console.log('example job processed'); 22 | break; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/github-actions/src/lib/permission.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as github from '@actions/github'; 3 | 4 | export async function isCollaborator(user: string, repository: string): Promise { 5 | const [owner, repo] = repository.split('/'); 6 | try { 7 | const octokit = github.getOctokit(process.env.GITHUB_TOKEN!); 8 | const res = await octokit.rest.repos.getCollaboratorPermissionLevel({ 9 | owner, 10 | repo, 11 | username: user, 12 | }); 13 | return ['admin', 'write'].includes(res.data.permission); 14 | } catch (e) { 15 | core.info(`got error on isCollaborator ${e}. owner: ${owner} repo: ${repo} user: ${user}`); 16 | return false; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/aws/ddb.ts: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 2 | import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; 3 | 4 | export const TableName = process.env.TABLE_NAME!; 5 | 6 | // Allow configuration of DynamoDB endpoint via environment variable 7 | // This enables using DynamoDB Local for development 8 | const clientConfig: { 9 | endpoint?: string; 10 | } = {}; 11 | 12 | if (process.env.DYNAMODB_ENDPOINT) { 13 | clientConfig.endpoint = process.env.DYNAMODB_ENDPOINT; 14 | console.log(`Using custom DynamoDB endpoint: ${process.env.DYNAMODB_ENDPOINT}`); 15 | } 16 | 17 | const client = new DynamoDBClient(clientConfig); 18 | export const ddb = DynamoDBDocumentClient.from(client); 19 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Label({ className, ...props }: React.ComponentProps) { 9 | return ( 10 | 18 | ); 19 | } 20 | 21 | export { Label }; 22 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/aws/ssm.ts: -------------------------------------------------------------------------------- 1 | import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm'; 2 | 3 | export const ssm = new SSMClient({}); 4 | 5 | /** 6 | * Get a parameter from SSM Parameter Store 7 | * @param parameterName The name of the parameter 8 | * @returns The parameter value 9 | */ 10 | export const getParameter = async (parameterName: string): Promise => { 11 | try { 12 | const response = await ssm.send( 13 | new GetParameterCommand({ 14 | Name: parameterName, 15 | }) 16 | ); 17 | return response.Parameter?.Value; 18 | } catch (error) { 19 | console.error(`Error getting parameter ${parameterName}:`, error); 20 | return undefined; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /docker/slack-bolt-app.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 AS builder 2 | WORKDIR /build 3 | COPY package*.json ./ 4 | COPY packages/agent-core/package*.json ./packages/agent-core/ 5 | COPY packages/slack-bolt-app/package*.json ./packages/slack-bolt-app/ 6 | RUN npm ci 7 | COPY ./ ./ 8 | RUN cd packages/agent-core && npm run build 9 | RUN cd packages/slack-bolt-app && npm run bundle 10 | 11 | FROM public.ecr.aws/lambda/nodejs:22 AS runner 12 | 13 | COPY package*.json ./ 14 | COPY packages/agent-core/package*.json ./packages/agent-core/ 15 | COPY packages/slack-bolt-app/package*.json ./packages/slack-bolt-app/ 16 | RUN npm ci --omit=dev 17 | COPY --from=builder /build/packages/slack-bolt-app/dist/. ./ 18 | 19 | CMD ["lambda.handler"] 20 | -------------------------------------------------------------------------------- /packages/worker/compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | dynamodb-local: 3 | image: amazon/dynamodb-local:latest 4 | container_name: dynamodb-local 5 | ports: 6 | - "8000:8000" 7 | command: "-jar DynamoDBLocal.jar -sharedDb -inMemory" 8 | healthcheck: 9 | test: ["CMD-SHELL", "curl -f http://localhost:8000/shell/ || exit 1"] 10 | interval: 10s 11 | timeout: 5s 12 | retries: 3 13 | volumes: 14 | - "./docker/dynamodb:/home/dynamodblocal/data" 15 | working_dir: /home/dynamodblocal 16 | 17 | dynamodb-admin: 18 | image: aaronshaf/dynamodb-admin:latest 19 | container_name: dynamodb-admin 20 | ports: 21 | - "8001:8001" 22 | environment: 23 | - DYNAMO_ENDPOINT=http://dynamodb-local:8000 24 | -------------------------------------------------------------------------------- /.github/workflows/remote-swe.yml: -------------------------------------------------------------------------------- 1 | name: Remote SWE Trigger 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [assigned] 10 | pull_request: 11 | types: [assigned] 12 | 13 | jobs: 14 | trigger-remote-swe: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Trigger Remote SWE 19 | uses: aws-samples/remote-swe-agents@main 20 | with: 21 | trigger_phrase: '@remote-swe-user' 22 | assignee_trigger: 'remote-swe-user' 23 | api_base_url: ${{ secrets.REMOTE_SWE_API_BASE_URL }} 24 | api_key: ${{ secrets.REMOTE_SWE_API_KEY }} 25 | 26 | permissions: 27 | pull-requests: write 28 | issues: write 29 | -------------------------------------------------------------------------------- /packages/webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | dist 44 | 45 | !prisma/.env 46 | !.env.local.example 47 | src/lib/generated 48 | -------------------------------------------------------------------------------- /packages/webapp/src/app/sessions/[workerId]/schemas.ts: -------------------------------------------------------------------------------- 1 | import { agentStatusSchema, modelTypeSchema } from '@remote-swe-agents/agent-core/schema'; 2 | import { z } from 'zod'; 3 | 4 | export const sendMessageToAgentSchema = z.object({ 5 | workerId: z.string(), 6 | message: z.string().min(1), 7 | imageKeys: z.array(z.string()).optional(), 8 | modelOverride: modelTypeSchema.optional(), 9 | }); 10 | 11 | export const fetchTodoListSchema = z.object({ 12 | workerId: z.string(), 13 | }); 14 | 15 | export const updateAgentStatusSchema = z.object({ 16 | workerId: z.string(), 17 | status: agentStatusSchema, 18 | }); 19 | 20 | export const sendEventSchema = z.object({ 21 | workerId: z.string(), 22 | event: z.object({ 23 | type: z.literal('forceStop'), 24 | }), 25 | }); 26 | -------------------------------------------------------------------------------- /cdk/lib/constructs/worker/resources/bus-event-handler.mjs: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | /** 4 | * Allow subscription only for the channels that 5 | * 1. begin with /public 6 | * 2. begin with /user/ 7 | * https://docs.aws.amazon.com/appsync/latest/eventapi/channel-namespace-handlers.html 8 | */ 9 | export function onSubscribe(ctx) { 10 | if (ctx.info.channel.path.startsWith(`/event-bus/public`)) { 11 | return; 12 | } 13 | if (ctx.info.channel.path.startsWith(`/event-bus/user`)) { 14 | if (ctx.info.channel.path.startsWith(`/event-bus/user/${ctx.identity.username}`)) { 15 | return; 16 | } 17 | console.log(`user ${ctx.identity.username} tried connecting to wrong channel: ${ctx.channel}`); 18 | util.unauthorized(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/worker/src/common/cancellation-token.ts: -------------------------------------------------------------------------------- 1 | export class CancellationToken { 2 | private _isCancelled: boolean = false; 3 | private _callback: (() => Promise) | undefined = undefined; 4 | 5 | public get isCancelled(): boolean { 6 | return this._isCancelled; 7 | } 8 | 9 | /** 10 | * The function that cancelled task must call after it completed stopping its task. 11 | */ 12 | public async completeCancel() { 13 | if (this._callback) { 14 | await this._callback(); 15 | } 16 | } 17 | 18 | /** 19 | * @param callback The callback function that is executed when each session is cancelled. 20 | */ 21 | public cancel(callback?: () => Promise): void { 22 | this._isCancelled = true; 23 | this._callback = callback; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/webapp/src/app/preferences/components/PreferenceSection.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface PreferenceSectionProps { 4 | title: string; 5 | description: string; 6 | children: ReactNode; 7 | } 8 | 9 | export default function PreferenceSection({ title, description, children }: PreferenceSectionProps) { 10 | return ( 11 |
12 |
13 |

{title}

14 |

{description}

15 |
16 | 17 |
{children}
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /cdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/webapp/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | import createNextIntlPlugin from 'next-intl/plugin'; 3 | import path from 'path'; 4 | 5 | const nextConfig: NextConfig = { 6 | output: 'standalone', 7 | outputFileTracingRoot: path.join(__dirname, '../../'), 8 | eslint: { 9 | ignoreDuringBuilds: true, 10 | }, 11 | experimental: { 12 | webpackBuildWorker: true, 13 | parallelServerBuildTraces: true, 14 | parallelServerCompiles: true, 15 | serverActions: { 16 | allowedOrigins: ['localhost:3011', process.env.ALLOWED_ORIGIN_HOST!], 17 | }, 18 | }, 19 | typescript: { 20 | ignoreBuildErrors: process.env.SKIP_TS_BUILD == 'true', 21 | }, 22 | }; 23 | 24 | const withNextIntl = createNextIntlPlugin(); 25 | export default withNextIntl(nextConfig); 26 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/worker-id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extracts worker ID from a text that contains the WORKER_ID metadata 3 | * @param text - The text to search 4 | * @returns The worker ID if found, null otherwise 5 | */ 6 | export const extractWorkerIdFromText = (text: string): string | null => { 7 | const match = text.match(//); 8 | return match ? match[1] : null; 9 | }; 10 | 11 | /** 12 | * Appends worker ID metadata to content (comments, PR descriptions, etc.) 13 | * @param content - The original content 14 | * @returns The content with worker ID metadata appended 15 | */ 16 | export const appendWorkerIdMetadata = (content: string, workerId: string): string => { 17 | return `${content}\n\n\n`; 18 | }; 19 | -------------------------------------------------------------------------------- /docker/job.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 AS builder 2 | WORKDIR /build 3 | COPY package*.json ./ 4 | COPY packages/agent-core/package*.json ./packages/agent-core/ 5 | COPY packages/webapp/package*.json ./packages/webapp/ 6 | RUN --mount=type=cache,target=/root/.npm npm ci 7 | COPY ./ ./ 8 | RUN cd packages/agent-core && npm run build 9 | RUN cd packages/webapp && npx esbuild src/jobs/*.ts --bundle --outdir=dist --platform=node --charset=utf8 10 | 11 | FROM public.ecr.aws/lambda/nodejs:22 AS runner 12 | 13 | COPY package*.json ./ 14 | COPY packages/agent-core/package*.json ./packages/agent-core/ 15 | COPY packages/webapp/package*.json ./packages/webapp/ 16 | RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev 17 | COPY --from=builder /build/packages/webapp/dist/. ./ 18 | 19 | CMD ["async-job-runner.handler"] 20 | -------------------------------------------------------------------------------- /packages/worker/src/common/refresh-session.ts: -------------------------------------------------------------------------------- 1 | import { getSession, setSlackDestination } from '@remote-swe-agents/agent-core/lib'; 2 | 3 | /** 4 | * Set required global variables for the session (this is dirty but at least is working...) 5 | */ 6 | export const refreshSession = async (workerId: string) => { 7 | const session = await getSession(workerId); 8 | if (!session) return; 9 | 10 | if (session.slackChannelId && session.slackThreadTs) { 11 | setSlackDestination(session.slackChannelId, session.slackThreadTs); 12 | } 13 | { 14 | // For backward compatibility. Will remove this block half year later. 15 | const SlackChannelId = process.env.SLACK_CHANNEL_ID!; 16 | const SlackThreadTs = process.env.SLACK_THREAD_TS!; 17 | if (SlackChannelId && SlackThreadTs) { 18 | setSlackDestination(SlackChannelId, SlackThreadTs); 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /cdk/.env.local.example: -------------------------------------------------------------------------------- 1 | # GitHub configuration when using GitHub Apps 2 | GITHUB_APP_ID="" 3 | GITHUB_INSTALLATION_ID="" 4 | 5 | # Restrict access to the Slack app for certain users (comma-separated) 6 | # Example: "U0123456789,U0QWERTYUIO," 7 | SLACK_ADMIN_USER_ID_LIST="" 8 | 9 | # Invitation email is sent on deploy to this email. 10 | # Example: "foo@example.com" 11 | INITIAL_WEBAPP_USER_EMAIL="" 12 | 13 | # AWS WAF config. 14 | ALLOWED_IPV4_CIDRS="" 15 | ALLOWED_IPV6_CIDRS="" 16 | ALLOWED_COUNTRY_CODES="" 17 | 18 | # Additional IAM managed policies to allow access from agents. 19 | # Example: "AmazonS3ReadOnlyAccess,AmazonDynamoDBReadOnlyAccess" 20 | WORKER_ADDITIONAL_POLICIES="" 21 | 22 | # The prefix of the Bedrock cross-region inference profile to override 23 | # Choose from: global, us, eu, apac, jp, au 24 | # @default 'us' (use US CRI profile) 25 | BEDROCK_CRI_REGION_OVERRIDE="" 26 | -------------------------------------------------------------------------------- /packages/worker/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # runner script for AgentCore Runtime 3 | # dependencies: 4 | # - aws cli 5 | 6 | if [ -n "$GITHUB_APP_PRIVATE_KEY_PARAMETER_NAME" ]; then 7 | aws ssm get-parameter \ 8 | --name $GITHUB_APP_PRIVATE_KEY_PARAMETER_NAME \ 9 | --query "Parameter.Value" \ 10 | --output text > /opt/private-key.pem 11 | export GITHUB_APP_PRIVATE_KEY_PATH="/opt/private-key.pem" 12 | fi 13 | 14 | if [ -n "$GITHUB_PERSONAL_ACCESS_TOKEN_PARAMETER_NAME" ]; then 15 | export GITHUB_PERSONAL_ACCESS_TOKEN=$(aws ssm get-parameter --name $GITHUB_PERSONAL_ACCESS_TOKEN_PARAMETER_NAME --query "Parameter.Value" --output text) 16 | fi 17 | 18 | if [ -n "$SLACK_BOT_TOKEN_PARAMETER_NAME" ]; then 19 | export SLACK_BOT_TOKEN=$(aws ssm get-parameter --name $SLACK_BOT_TOKEN_PARAMETER_NAME --query "Parameter.Value" --output text) 20 | fi 21 | 22 | exec npx tsx src/agent-core.ts 23 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/agent.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { modelTypeSchema } from './model'; 3 | 4 | export const agentStatusSchema = z.union([z.literal('working'), z.literal('pending'), z.literal('completed')]); 5 | export const runtimeTypeSchema = z.union([z.literal('ec2'), z.literal('agent-core')]); 6 | 7 | export type AgentStatus = z.infer; 8 | export type RuntimeType = z.infer; 9 | 10 | export const customAgentSchema = z.object({ 11 | PK: z.literal('custom-agent'), 12 | SK: z.string(), 13 | name: z.string(), 14 | description: z.string(), 15 | defaultModel: modelTypeSchema, 16 | systemPrompt: z.string(), 17 | tools: z.array(z.string()), 18 | mcpConfig: z.string(), 19 | runtimeType: runtimeTypeSchema, 20 | createdAt: z.number(), 21 | updatedAt: z.number(), 22 | }); 23 | 24 | export type CustomAgent = z.infer; 25 | -------------------------------------------------------------------------------- /packages/webapp/src/app/api/auth/api-key.ts: -------------------------------------------------------------------------------- 1 | import { validateApiKey } from '@remote-swe-agents/agent-core/lib'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | 4 | /** 5 | * Middleware to validate API key in headers 6 | * @param request The Next.js request object 7 | * @returns Response or undefined if validation passes 8 | */ 9 | export async function validateApiKeyMiddleware(request: NextRequest): Promise { 10 | // Extract API key from x-api-key header 11 | const apiKey = request.headers.get('x-api-key'); 12 | 13 | if (!apiKey) { 14 | return NextResponse.json({ error: 'Missing API key' }, { status: 401 }); 15 | } 16 | 17 | const isValid = await validateApiKey(apiKey); 18 | 19 | if (!isValid) { 20 | return NextResponse.json({ error: 'Invalid API key' }, { status: 401 }); 21 | } 22 | 23 | // If we reach here, the API key is valid 24 | return undefined; 25 | } 26 | -------------------------------------------------------------------------------- /packages/webapp/src/app/api/cognito-token/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { cookies } from 'next/headers'; 3 | import { fetchAuthSession } from 'aws-amplify/auth/server'; 4 | import { runWithAmplifyServerContext } from '@/lib/amplifyServerUtils'; 5 | 6 | export async function GET() { 7 | try { 8 | const session = await runWithAmplifyServerContext({ 9 | nextServerContext: { cookies }, 10 | operation: (contextSpec) => fetchAuthSession(contextSpec), 11 | }); 12 | 13 | if (session.tokens?.accessToken == null) { 14 | return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); 15 | } 16 | 17 | return NextResponse.json({ 18 | accessToken: session.tokens.accessToken.toString(), 19 | }); 20 | } catch (error) { 21 | console.error('Error fetching Cognito token:', error); 22 | return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/webapp/src/app/sessions/(root)/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/Header'; 2 | import { getSessions } from '@remote-swe-agents/agent-core/lib'; 3 | import { RefreshOnFocus } from '@/components/RefreshOnFocus'; 4 | import SessionsList from './components/SessionsList'; 5 | import { getSession } from '@/lib/auth'; 6 | 7 | export const dynamic = 'force-dynamic'; 8 | export const revalidate = 0; 9 | 10 | export default async function SessionsPage() { 11 | const sessions = await getSessions(100); 12 | const { userId } = await getSession(); 13 | 14 | return ( 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface InputProps extends React.InputHTMLAttributes {} 6 | 7 | const Input = React.forwardRef(({ className, type, ...props }, ref) => { 8 | return ( 9 | 18 | ); 19 | }); 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | import { fetchAuthSession } from 'aws-amplify/auth/server'; 3 | import { runWithAmplifyServerContext } from '@/lib/amplifyServerUtils'; 4 | 5 | export class UserNotCreatedError { 6 | constructor(public readonly userId: string) {} 7 | } 8 | 9 | export async function getSession() { 10 | const session = await runWithAmplifyServerContext({ 11 | nextServerContext: { cookies }, 12 | operation: (contextSpec) => fetchAuthSession(contextSpec), 13 | }); 14 | if (session.userSub == null || session.tokens?.idToken == null || session.tokens?.accessToken == null) { 15 | throw new Error('session not found'); 16 | } 17 | const userId = session.userSub; 18 | const email = session.tokens.idToken.payload.email; 19 | if (typeof email != 'string') { 20 | throw new Error(`invalid email ${userId}.`); 21 | } 22 | return { 23 | userId, 24 | email, 25 | accessToken: session.tokens.accessToken.toString(), 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /cdk/lib/constructs/cf-lambda-furl-service/lambda/sign-payload.ts: -------------------------------------------------------------------------------- 1 | import type { CloudFrontRequestEvent, CloudFrontRequestHandler } from 'aws-lambda'; 2 | import { createHash } from 'crypto'; 3 | 4 | const hashPayload = (payload: Buffer) => { 5 | return createHash('sha256').update(payload).digest('hex'); 6 | }; 7 | 8 | export const handler: CloudFrontRequestHandler = async (event: CloudFrontRequestEvent) => { 9 | const request = event.Records[0].cf.request; 10 | const body = request.body?.data ?? ''; 11 | 12 | const hashedBody = hashPayload(Buffer.from(body, 'base64')); 13 | request.headers['x-amz-content-sha256'] = [{ key: 'x-amz-content-sha256', value: hashedBody }]; 14 | 15 | // LWA replaces authorization2 to authorization again 16 | // if (request.headers['authorization'] != null) { 17 | // request.headers['authorization2'] = [{ key: 'authorization2', value: request.headers['authorization'][0].value }]; 18 | // delete request.headers['authorization']; 19 | // } 20 | 21 | return request; 22 | }; 23 | -------------------------------------------------------------------------------- /cdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk", 3 | "version": "0.1.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "test": "jest", 11 | "format": "prettier --write './**/*.ts'", 12 | "format:check": "prettier --check './**/*.ts'", 13 | "cdk": "cdk" 14 | }, 15 | "devDependencies": { 16 | "@types/aws-lambda": "^8.10.149", 17 | "@types/jest": "^29.5.14", 18 | "@types/node": "22.7.9", 19 | "aws-cdk": "^2.178.1", 20 | "aws-lambda": "^1.0.7", 21 | "esbuild": "^0.25.0", 22 | "jest": "^29.7.0", 23 | "prettier": "^3.5.1", 24 | "ts-jest": "^29.2.5", 25 | "typescript": "~5.6.3" 26 | }, 27 | "dependencies": { 28 | "@aws-sdk/client-ec2": "^3.749.0", 29 | "aws-cdk-lib": "^2.187.0", 30 | "cdk-image-pipeline": "^0.5.107", 31 | "cdk-nag": "^2.35.33", 32 | "constructs": "^10.0.0", 33 | "deploy-time-build": "^0.3.32", 34 | "tsx": "^4.20.3", 35 | "yaml": "^2.7.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/webapp/src/actions/api-key/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { createApiKey, deleteApiKey, getApiKeys } from '@remote-swe-agents/agent-core/lib'; 4 | import { ApiKeyItem } from '@remote-swe-agents/agent-core/schema'; 5 | import { authActionClient } from '@/lib/safe-action'; 6 | import { createApiKeySchema, deleteApiKeySchema } from './schemas'; 7 | 8 | export const listApiKeysAction = authActionClient.action(async ({ ctx }) => { 9 | const apiKeys = await getApiKeys(); 10 | return { apiKeys }; 11 | }); 12 | 13 | export const createApiKeyAction = authActionClient 14 | .inputSchema(createApiKeySchema) 15 | .action(async ({ parsedInput, ctx }) => { 16 | const { description } = parsedInput; 17 | const apiKey = await createApiKey(description, ctx.userId); 18 | return { apiKey }; 19 | }); 20 | 21 | export const deleteApiKeyAction = authActionClient.inputSchema(deleteApiKeySchema).action(async ({ parsedInput }) => { 22 | const { apiKey } = parsedInput; 23 | await deleteApiKey(apiKey); 24 | return { success: true }; 25 | }); 26 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/amplifyServerUtils.ts: -------------------------------------------------------------------------------- 1 | import { AppOrigin } from '@/lib/origin'; 2 | import { createServerRunner } from '@aws-amplify/adapter-nextjs'; 3 | 4 | process.env.AMPLIFY_APP_ORIGIN = AppOrigin; 5 | 6 | export const { runWithAmplifyServerContext, createAuthRouteHandlers } = createServerRunner({ 7 | config: { 8 | Auth: { 9 | Cognito: { 10 | userPoolId: process.env.USER_POOL_ID!, 11 | userPoolClientId: process.env.USER_POOL_CLIENT_ID!, 12 | loginWith: { 13 | oauth: { 14 | redirectSignIn: [`${AppOrigin}/api/auth/sign-in-callback`], 15 | redirectSignOut: [`${AppOrigin}/api/auth/sign-out-callback`], 16 | responseType: 'code', 17 | domain: process.env.COGNITO_DOMAIN!, 18 | scopes: ['profile', 'openid', 'aws.cognito.signin.user.admin'], 19 | }, 20 | }, 21 | }, 22 | }, 23 | }, 24 | runtimeOptions: { 25 | cookies: { 26 | sameSite: 'lax', 27 | maxAge: 60 * 60 * 24 * 30, // 30 days 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/worker/src/common/ec2.ts: -------------------------------------------------------------------------------- 1 | import { ec2 } from '@remote-swe-agents/agent-core/aws'; 2 | import { StopInstancesCommand } from '@aws-sdk/client-ec2'; 3 | 4 | const workerRuntime = process.env.WORKER_RUNTIME ?? 'ec2'; 5 | 6 | export const stopMyself = async () => { 7 | if (workerRuntime !== 'ec2') return; 8 | const instanceId = await getInstanceId(); 9 | await ec2.send( 10 | new StopInstancesCommand({ 11 | InstanceIds: [instanceId], 12 | }) 13 | ); 14 | }; 15 | 16 | const getInstanceId = async () => { 17 | const token = await getImdsV2Token(); 18 | const res = await fetch('http://169.254.169.254/latest/meta-data/instance-id', { 19 | headers: { 20 | 'X-aws-ec2-metadata-token': token, 21 | }, 22 | }); 23 | return await res.text(); 24 | }; 25 | 26 | const getImdsV2Token = async () => { 27 | const res = await fetch('http://169.254.169.254/latest/api/token', { 28 | method: 'PUT', 29 | headers: { 30 | 'X-aws-ec2-metadata-token-ttl-seconds': '900', 31 | }, 32 | }); 33 | return await res.text(); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/util/idempotency.ts: -------------------------------------------------------------------------------- 1 | import { IdempotencyConfig } from '@aws-lambda-powertools/idempotency'; 2 | import * as idempotency from '@aws-lambda-powertools/idempotency'; 3 | import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb'; 4 | 5 | // https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/idempotency/#installation 6 | const persistenceStore = new DynamoDBPersistenceLayer({ 7 | tableName: process.env.TABLE_NAME!, 8 | keyAttr: 'PK', 9 | sortKeyAttr: 'SK', 10 | expiryAttr: 'TTL', 11 | }); 12 | const config = new IdempotencyConfig({ expiresAfterSeconds: 600 }); 13 | 14 | /** 15 | * make `func` called exactly once. 16 | * @param func a function that takes idempotency key as the first argument. 17 | * @returns 18 | */ 19 | export const makeIdempotent = ( 20 | func: (key: string) => Promise, 21 | option?: { config?: IdempotencyConfig } 22 | ): ((key: string) => Promise) => { 23 | return idempotency.makeIdempotent(func, { 24 | persistenceStore, 25 | config: option?.config ?? config, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/util/session.ts: -------------------------------------------------------------------------------- 1 | import { PutCommand } from '@aws-sdk/lib-dynamodb'; 2 | import { ddb, TableName } from '@remote-swe-agents/agent-core/aws'; 3 | import { SessionItem } from '@remote-swe-agents/agent-core/schema'; 4 | 5 | export const saveSessionInfo = async ( 6 | workerId: string, 7 | initialMessage: string, 8 | initiatorSlackUserId: string, 9 | slackChannelId: string, 10 | slackThreadTs: string 11 | ) => { 12 | const now = Date.now(); 13 | const timestamp = String(now).padStart(15, '0'); 14 | 15 | await ddb.send( 16 | new PutCommand({ 17 | TableName, 18 | Item: { 19 | PK: 'sessions', 20 | SK: workerId, 21 | workerId, 22 | createdAt: now, 23 | updatedAt: now, 24 | LSI1: timestamp, 25 | initialMessage, 26 | instanceStatus: 'terminated', 27 | sessionCost: 0, 28 | agentStatus: 'pending', 29 | initiator: `slack#${initiatorSlackUserId}`, 30 | slackChannelId, 31 | slackThreadTs, 32 | } satisfies SessionItem, 33 | }) 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/webapp/src/app/custom-agent/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { mcpConfigSchema, modelTypeSchema } from '@remote-swe-agents/agent-core/schema'; 3 | 4 | export const upsertCustomAgentSchema = z.object({ 5 | id: z.string().optional(), 6 | name: z.string().min(1, 'Agent name is required').max(100, 'Agent name must be less than 100 characters'), 7 | description: z.string().default(''), 8 | defaultModel: modelTypeSchema, 9 | systemPrompt: z.string().default(''), 10 | tools: z.array(z.string()), 11 | mcpConfig: z 12 | .string() 13 | .optional() 14 | .default('') 15 | .refine((val) => { 16 | if (val === '') return true; 17 | try { 18 | const json = JSON.parse(val); 19 | mcpConfigSchema.parse(json); 20 | return true; 21 | } catch (e) { 22 | return false; 23 | } 24 | }, 'Invalid mcpConfig schema.'), 25 | runtimeType: z.union([z.literal('ec2'), z.literal('agent-core')]), 26 | }); 27 | 28 | export const deleteCustomAgentSchema = z.object({ 29 | id: z.string().min(1, 'Agent ID is required'), 30 | }); 31 | -------------------------------------------------------------------------------- /packages/webapp/src/app/preferences/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { authActionClient } from '@/lib/safe-action'; 4 | import { savePromptSchema } from './schemas'; 5 | import { writeCommonPrompt, updatePreferences } from '@remote-swe-agents/agent-core/lib'; 6 | import { updateGlobalPreferenceSchema } from '@remote-swe-agents/agent-core/schema'; 7 | 8 | export const updateAdditionalSystemPrompt = authActionClient 9 | .inputSchema(savePromptSchema) 10 | .action(async ({ parsedInput }) => { 11 | const { additionalSystemPrompt } = parsedInput; 12 | try { 13 | await writeCommonPrompt({ 14 | additionalSystemPrompt: additionalSystemPrompt || '', 15 | }); 16 | 17 | return { success: true }; 18 | } catch (error) { 19 | console.error('Error saving prompt:', error); 20 | throw new Error('Failed to save prompt configuration'); 21 | } 22 | }); 23 | 24 | export const updateGlobalPreferences = authActionClient 25 | .inputSchema(updateGlobalPreferenceSchema) 26 | .action(async ({ parsedInput }) => { 27 | return await updatePreferences(parsedInput); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/session.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { agentStatusSchema, runtimeTypeSchema } from './agent'; 3 | 4 | export const instanceStatusSchema = z.union([ 5 | z.literal('starting'), 6 | z.literal('running'), 7 | z.literal('stopped'), 8 | z.literal('terminated'), 9 | ]); 10 | 11 | export type InstanceStatus = z.infer; 12 | 13 | export const sessionItemSchema = z.object({ 14 | PK: z.literal('sessions'), 15 | SK: z.string(), 16 | workerId: z.string(), 17 | createdAt: z.number(), 18 | updatedAt: z.number(), 19 | LSI1: z.string(), 20 | initialMessage: z.string(), 21 | instanceStatus: instanceStatusSchema, 22 | sessionCost: z.number(), 23 | agentStatus: agentStatusSchema, 24 | initiator: z.string().optional(), 25 | isHidden: z.boolean().optional(), 26 | slackChannelId: z.string().optional(), 27 | slackThreadTs: z.string().optional(), 28 | title: z.string().optional(), 29 | customAgentId: z.string().optional(), 30 | runtimeType: runtimeTypeSchema.optional(), 31 | }); 32 | 33 | export type SessionItem = z.infer; 34 | -------------------------------------------------------------------------------- /cdk/lib/constructs/ec2-gc/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Duration } from 'aws-cdk-lib'; 3 | import * as events from 'aws-cdk-lib/aws-events'; 4 | import * as targets from 'aws-cdk-lib/aws-events-targets'; 5 | import { EC2GarbageCollectorStepFunctions } from './sfn'; 6 | 7 | export interface EC2GarbageCollectorProps { 8 | imageRecipeName: string; 9 | expirationInDays: number; 10 | } 11 | 12 | export class EC2GarbageCollector extends Construct { 13 | constructor(scope: Construct, id: string, props: EC2GarbageCollectorProps) { 14 | super(scope, id); 15 | 16 | // EC2 garbage collection implementation using Step Functions and JSONata 17 | const eC2GarbageCollectorStepFunctions = new EC2GarbageCollectorStepFunctions(this, 'StateMachine', { 18 | imageRecipeName: props.imageRecipeName, 19 | expirationInDays: props.expirationInDays, 20 | }); 21 | 22 | const schedule = new events.Rule(this, 'Schedule', { 23 | schedule: events.Schedule.rate(Duration.hours(2)), 24 | }); 25 | schedule.addTarget(new targets.SfnStateMachine(eC2GarbageCollectorStepFunctions.stateMachine)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/worker/src/agent-core.ts: -------------------------------------------------------------------------------- 1 | // API implementation for agent core runtime 2 | // https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html 3 | 4 | import express from 'express'; 5 | import { main } from './entry'; 6 | 7 | let getCurrentStatus: () => 'busy' | 'idle' | undefined; 8 | const app = express(); 9 | 10 | app.use(express.json()); 11 | 12 | app.post('/invocations', async (req, res) => { 13 | const body = req.body; 14 | console.log(body); 15 | const sessionId = body.sessionId; 16 | const tracker = await main(sessionId); 17 | if (tracker) { 18 | getCurrentStatus = () => (tracker.isBusy() ? 'busy' : 'idle'); 19 | } 20 | 21 | res.json({ 22 | response: 'ok', 23 | status: 'success', 24 | }); 25 | }); 26 | 27 | app.get('/ping', (_req, res) => { 28 | const status = getCurrentStatus?.() ?? 'idle'; 29 | res.json({ 30 | status: status == 'idle' ? 'Healthy' : 'HealthyBusy', 31 | time_of_last_update: Math.floor(Date.now() / 1000), 32 | }); 33 | }); 34 | 35 | const port = 8080; 36 | app.listen(port, () => { 37 | console.log(`Agent server listening on 0.0.0.0:${port}`); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/agent-core/src/tools/report-progress/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ToolDefinition, zodToJsonSchemaBody } from '../../private/common/lib'; 3 | import { sendMessageToSlack } from '../../lib/slack'; 4 | 5 | const inputSchema = z.object({ 6 | message: z.string().describe('The message you want to send to the user.'), 7 | }); 8 | 9 | const name = 'sendMessageToUser'; 10 | 11 | export const reportProgressTool: ToolDefinition> = { 12 | name, 13 | handler: async (input: z.infer) => { 14 | await sendMessageToSlack(input.message); 15 | return 'Successfully sent a message.'; 16 | }, 17 | schema: inputSchema, 18 | toolSpec: async () => ({ 19 | name, 20 | description: ` 21 | Send any message to the user. This is especially valuable if the message contains any information the user want to know, such as how you are solving the problem now. Without this tool, a user cannot know your progress because message is only sent when you finished using tools and end your turn. 22 | `.trim(), 23 | inputSchema: { 24 | json: zodToJsonSchemaBody(inputSchema), 25 | }, 26 | }), 27 | }; 28 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/webapp-origin.ts: -------------------------------------------------------------------------------- 1 | import { getParameter } from './aws/ssm'; 2 | 3 | /** 4 | * Get the webapp origin URL from SSM parameter 5 | * First tries to use the WEBAPP_ORIGIN_NAME_PARAMETER environment variable 6 | * If that's not available, searches for parameters with 'OriginSourceParameter' in the name 7 | * @returns The webapp origin URL or undefined if not available 8 | */ 9 | export const getWebappOrigin = async (): Promise => { 10 | // First try to use the environment variable if it's set 11 | const parameterName = process.env.WEBAPP_ORIGIN_NAME_PARAMETER; 12 | 13 | if (parameterName) { 14 | const origin = await getParameter(parameterName); 15 | return origin; 16 | } 17 | }; 18 | 19 | /** 20 | * Build webapp session URL for a worker 21 | * @param workerId The worker ID 22 | * @returns The session URL or undefined if webapp origin is not available 23 | */ 24 | export const getWebappSessionUrl = async (workerId: string): Promise => { 25 | const origin = await getWebappOrigin(); 26 | 27 | if (!origin) { 28 | return undefined; 29 | } 30 | 31 | return `${origin}/sessions/${workerId}`; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/worker/src/local.ts: -------------------------------------------------------------------------------- 1 | import { createInterface } from 'readline'; 2 | import { onMessageReceived } from './agent'; 3 | import { renderUserMessage, saveConversationHistory } from '@remote-swe-agents/agent-core/lib'; 4 | import { CancellationToken } from './common/cancellation-token'; 5 | import './common/signal-handler'; 6 | import { randomBytes } from 'crypto'; 7 | 8 | const rl = createInterface({ 9 | input: process.stdin, 10 | output: process.stdout, 11 | }); 12 | const workerId = process.env.WORKER_ID ?? randomBytes(10).toString('hex'); 13 | 14 | async function processInput(input: string) { 15 | try { 16 | if (input) { 17 | await saveConversationHistory( 18 | workerId, 19 | { 20 | role: 'user', 21 | content: [{ text: renderUserMessage({ message: input }) }], 22 | }, 23 | 0, 24 | 'userMessage' 25 | ); 26 | } 27 | await onMessageReceived(workerId, new CancellationToken()); 28 | } catch (error) { 29 | console.error('An error occurred:', error); 30 | } 31 | rl.question('Enter your message: ', processInput); 32 | } 33 | 34 | console.log(`Local worker started. workerId: ${workerId}`); 35 | rl.question('Enter your message: ', processInput); 36 | -------------------------------------------------------------------------------- /packages/agent-core/src/tools/think/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ToolDefinition, zodToJsonSchemaBody } from '../../private/common/lib'; 3 | 4 | const inputSchema = z.object({ 5 | thought: z.string().describe('Your thoughts.'), 6 | }); 7 | 8 | const name = 'think'; 9 | 10 | // https://www.anthropic.com/engineering/claude-think-tool 11 | export const thinkTool: ToolDefinition> = { 12 | name, 13 | handler: async (input: z.infer) => { 14 | return 'Nice thought.'; 15 | }, 16 | schema: inputSchema, 17 | toolSpec: async () => ({ 18 | name, 19 | description: `Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed. For example, if you explore the repo and discover the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective. Alternatively, if you receive some test results, call this tool to brainstorm ways to fix the failing tests. 20 | `, 21 | inputSchema: { 22 | json: zodToJsonSchemaBody(inputSchema), 23 | }, 24 | }), 25 | }; 26 | -------------------------------------------------------------------------------- /packages/agent-core/src/lib/metadata.ts: -------------------------------------------------------------------------------- 1 | import { PutCommand, GetCommand } from '@aws-sdk/lib-dynamodb'; 2 | import { ddb, TableName } from './aws/ddb'; 3 | 4 | /** 5 | * Write metadata to DynamoDB. 6 | * @param tag The tag to use as the SK in DynamoDB 7 | * @param data The object data to store 8 | * @param workerId The worker ID to use as part of the PK 9 | */ 10 | export const writeMetadata = async (tag: string, data: object, workerId: string = process.env.WORKER_ID!) => { 11 | await ddb.send( 12 | new PutCommand({ 13 | TableName, 14 | Item: { 15 | PK: `metadata-${workerId}`, 16 | SK: tag, 17 | ...data, 18 | }, 19 | }) 20 | ); 21 | }; 22 | 23 | /** 24 | * Read metadata from DynamoDB. 25 | * @param tag The tag to use as the SK in DynamoDB 26 | * @param workerId The worker ID to use as part of the PK 27 | * @returns The metadata object or null if not found 28 | */ 29 | export const readMetadata = async (tag: string, workerId: string = process.env.WORKER_ID!) => { 30 | const result = await ddb.send( 31 | new GetCommand({ 32 | TableName, 33 | Key: { 34 | PK: `metadata-${workerId}`, 35 | SK: tag, 36 | }, 37 | }) 38 | ); 39 | 40 | return result.Item; 41 | }; 42 | -------------------------------------------------------------------------------- /cdk/lib/constructs/worker/resources/versioning-handler.js: -------------------------------------------------------------------------------- 1 | const response = require('cfn-response'); 2 | 3 | exports.handler = async function (event, context) { 4 | try { 5 | console.log(event); 6 | if (event.RequestType == 'Delete') { 7 | return await response.send(event, context, response.SUCCESS); 8 | } 9 | const initialVersion = event.ResourceProperties.initialVersion; 10 | if (event.RequestType == 'Create') { 11 | return await response.send(event, context, response.SUCCESS, { version: initialVersion }, initialVersion); 12 | } 13 | if (event.RequestType == 'Update') { 14 | const currentVersion = event.PhysicalResourceId; // e.g. 1.0.0 15 | // increment patch version 16 | const [major, minor, patch] = currentVersion.split('.').map(Number); 17 | const [oMajor, oMinor, oPatch] = initialVersion.split('.').map(Number); 18 | let newVersion = [major, minor, patch + 1].join('.'); 19 | if (oMajor > major || (oMajor == major && oMinor > minor)) { 20 | newVersion = initialVersion; 21 | } 22 | await response.send(event, context, response.SUCCESS, { version: newVersion }, newVersion); 23 | } 24 | } catch (e) { 25 | console.log(e); 26 | await response.send(event, context, response.FAILED); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /packages/agent-core/src/private/common/lib.ts: -------------------------------------------------------------------------------- 1 | import { Tool, ToolResultContentBlock } from '@aws-sdk/client-bedrock-runtime'; 2 | import z, { ZodType } from 'zod'; 3 | import { GlobalPreferences } from '../../schema'; 4 | 5 | export type ToolDefinition = { 6 | /** 7 | * Name of the tool. This is the identifier of the tool for the agent. 8 | */ 9 | readonly name: string; 10 | readonly handler: ( 11 | input: Input, 12 | context: { workerId: string; toolUseId: string; globalPreferences: GlobalPreferences } 13 | ) => Promise; 14 | readonly schema: ZodType; 15 | readonly toolSpec: () => Promise>; 16 | }; 17 | 18 | export const zodToJsonSchemaBody = (schema: ZodType) => { 19 | return z.toJSONSchema(schema) as any; 20 | }; 21 | 22 | export const truncate = (str: string, maxLength: number = 10 * 1e3, headRatio = 0.2) => { 23 | if (str.length < maxLength) return str; 24 | if (headRatio < 0 || headRatio > 1) throw new Error('headRatio must be between 0 and 1'); 25 | 26 | const first = str.slice(0, maxLength * headRatio); 27 | const last = str.slice(-maxLength * (1 - headRatio)); 28 | return first + '\n..(truncated)..\n' + last + `\n// Output was truncated. Original length: ${str.length} characters.`; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/webapp/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | import { NextIntlClientProvider } from 'next-intl'; 3 | import { getLocale, getMessages } from 'next-intl/server'; 4 | import './globals.css'; 5 | import { ThemeProvider } from 'next-themes'; 6 | import { Toaster } from '@/components/ui/sonner'; 7 | 8 | export default async function RootLayout({ children }: { children: ReactNode }) { 9 | // Get the locale from the request 10 | const locale = await getLocale(); 11 | // Get the messages for the current locale 12 | const messages = await getMessages(); 13 | const isLocal = process.env.IS_LOCAL == 'true'; 14 | const title = isLocal ? 'Remote SWE Agents (local)' : 'Remote SWE Agents'; 15 | 16 | return ( 17 | 18 | 19 | {title} 20 | 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/webapp/src/utils/session-status.ts: -------------------------------------------------------------------------------- 1 | import { AgentStatus, InstanceStatus } from '@remote-swe-agents/agent-core/schema'; 2 | 3 | export type StatusI18nKey = 4 | | 'agentStatus.completed' 5 | | 'agentStatus.pending' 6 | | 'agentStatus.working' 7 | | 'agentStatus.unknown' 8 | | 'sessionStatus.stopped' 9 | | 'sessionStatus.starting'; 10 | 11 | export interface UnifiedStatusResult { 12 | i18nKey: StatusI18nKey; 13 | color: string; 14 | } 15 | 16 | export function getUnifiedStatus( 17 | agentStatus: AgentStatus | undefined, 18 | instanceStatus: InstanceStatus | undefined 19 | ): UnifiedStatusResult { 20 | if (instanceStatus === 'starting') { 21 | return { i18nKey: 'sessionStatus.starting', color: 'bg-blue-500' }; 22 | } 23 | if (agentStatus === 'completed') { 24 | return { i18nKey: 'agentStatus.completed', color: 'bg-gray-500' }; 25 | } 26 | if (instanceStatus === 'stopped' || instanceStatus === 'terminated') { 27 | return { i18nKey: 'sessionStatus.stopped', color: 'bg-gray-500' }; 28 | } 29 | if (agentStatus === 'pending') { 30 | return { i18nKey: 'agentStatus.pending', color: 'bg-yellow-500' }; 31 | } 32 | if (agentStatus === 'working') { 33 | return { i18nKey: 'agentStatus.working', color: 'bg-green-500' }; 34 | } 35 | return { i18nKey: 'agentStatus.unknown', color: 'bg-gray-400' }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { CheckIcon } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/utils'; 8 | 9 | function Checkbox({ className, ...props }: React.ComponentProps) { 10 | return ( 11 | 19 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | export { Checkbox }; 30 | -------------------------------------------------------------------------------- /cdk/lib/constructs/auth/prefix-generator.js: -------------------------------------------------------------------------------- 1 | const response = require('cfn-response'); 2 | const crypto = require('crypto'); 3 | 4 | exports.handler = async function (event, context) { 5 | try { 6 | console.log(event); 7 | if (event.RequestType == 'Delete') { 8 | return await response.send(event, context, response.SUCCESS); 9 | } 10 | 11 | const prefix = event.ResourceProperties.prefix ?? ''; 12 | const length = event.ResourceProperties.length ?? '8'; 13 | const generate = () => { 14 | const random = crypto.randomBytes(parseInt(length)).toString('hex'); 15 | return `${prefix}${random.slice(0, length)}`; 16 | }; 17 | 18 | if (event.RequestType == 'Create') { 19 | const generated = generate(); 20 | return await response.send(event, context, response.SUCCESS, { generated }, generated); 21 | } 22 | if (event.RequestType == 'Update') { 23 | const current = event.PhysicalResourceId; 24 | if (current.startsWith(prefix)) { 25 | return await response.send(event, context, response.SUCCESS, { generated: current }, current); 26 | } 27 | const generated = generate(); 28 | return await response.send(event, context, response.SUCCESS, { generated }, generated); 29 | } 30 | } catch (e) { 31 | console.log(e); 32 | await response.send(event, context, response.FAILED); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /packages/agent-core/src/tools/read-image/index.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { ToolDefinition, zodToJsonSchemaBody } from '../../private/common/lib'; 3 | import { promises as fs } from 'fs'; 4 | import sharp from 'sharp'; 5 | 6 | const inputSchema = z.object({ 7 | imagePath: z.string().describe('The local file system path (absolute) to the image.'), 8 | }); 9 | 10 | const name = 'readLocalImage'; 11 | 12 | export const readImageTool: ToolDefinition> = { 13 | name, 14 | handler: async (input: z.infer) => { 15 | // Check if file exists 16 | await fs.access(input.imagePath); 17 | 18 | // Convert image to webp format using sharp 19 | const webpBuffer = await sharp(input.imagePath).webp().toBuffer(); 20 | 21 | // Return JSON stringified result with image data 22 | return [ 23 | { 24 | image: { 25 | format: 'webp', 26 | source: { bytes: webpBuffer }, 27 | }, 28 | }, 29 | ]; 30 | }, 31 | schema: inputSchema, 32 | toolSpec: async () => ({ 33 | name, 34 | description: `Read an image in the local file system. Use this tool to get the visual details of an image. You cannot pass an Internet URL here; you must download the image locally first.`, 35 | inputSchema: { 36 | json: zodToJsonSchemaBody(inputSchema), 37 | }, 38 | }), 39 | }; 40 | -------------------------------------------------------------------------------- /resources/slack-app-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_information": { 3 | "name": "remoteSWE", 4 | "description": "They work hard, you chill.", 5 | "background_color": "#070708" 6 | }, 7 | "features": { 8 | "bot_user": { 9 | "display_name": "remote-swe", 10 | "always_online": true 11 | } 12 | }, 13 | "oauth_config": { 14 | "redirect_urls": [ 15 | "https://redacted.execute-api.us-east-1.amazonaws.com" 16 | ], 17 | "scopes": { 18 | "bot": [ 19 | "app_mentions:read", 20 | "chat:write", 21 | "files:read", 22 | "files:write", 23 | "reactions:read", 24 | "reactions:write" 25 | ] 26 | } 27 | }, 28 | "settings": { 29 | "event_subscriptions": { 30 | "request_url": "https://redacted.execute-api.us-east-1.amazonaws.com", 31 | "bot_events": [ 32 | "app_mention" 33 | ] 34 | }, 35 | "interactivity": { 36 | "is_enabled": true, 37 | "request_url": "https://redacted.execute-api.us-east-1.amazonaws.com" 38 | }, 39 | "org_deploy_enabled": false, 40 | "socket_mode_enabled": false, 41 | "token_rotation_enabled": false 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slack-bolt-app", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "tsx watch src/local.ts", 7 | "build": "tsc", 8 | "bundle": "esbuild src/lambda.ts src/async-handler.ts src/local.ts --bundle --outdir=dist --platform=node --charset=utf8", 9 | "format": "prettier --write './**/*.ts'", 10 | "format:check": "prettier --check './**/*.ts'", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "dependencies": { 14 | "@aws-crypto/sha256-js": "^5.2.0", 15 | "@aws-lambda-powertools/idempotency": "^2.14.0", 16 | "@aws-sdk/client-bedrock-runtime": "^3.751.0", 17 | "@aws-sdk/client-dynamodb": "^3.744.0", 18 | "@aws-sdk/client-ec2": "^3.743.0", 19 | "@aws-sdk/client-lambda": "^3.744.0", 20 | "@aws-sdk/client-s3": "^3.758.0", 21 | "@aws-sdk/client-ssm": "^3.787.0", 22 | "@aws-sdk/credential-provider-node": "^3.750.0", 23 | "@aws-sdk/lib-dynamodb": "^3.751.0", 24 | "@remote-swe-agents/agent-core": "file:../agent-core", 25 | "@slack/bolt": "^4.0.0", 26 | "@slack/web-api": "^7.8.0", 27 | "zod": "^4.0.0" 28 | }, 29 | "devDependencies": { 30 | "@types/aws-lambda": "^8.10.147", 31 | "@types/node": "^22.8.6", 32 | "esbuild": "^0.25.0", 33 | "prettier": "^3.3.3", 34 | "tsx": "^4.19.2", 35 | "typescript": "^5.6.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/agent-core/src/tools/command-execution/github.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process'; 2 | import { promisify } from 'util'; 3 | const execAsync = promisify(exec); 4 | 5 | const cache = { 6 | updatedAt: 0, 7 | token: '', 8 | }; 9 | 10 | export const authorizeGitHubCli = async () => { 11 | if (cache.updatedAt > Date.now() - 50 * 60 * 1000) { 12 | return cache.token; 13 | } 14 | if (process.env.GITHUB_PERSONAL_ACCESS_TOKEN) { 15 | cache.token = process.env.GITHUB_PERSONAL_ACCESS_TOKEN; 16 | } else if ( 17 | // 18 | process.env.GITHUB_APP_PRIVATE_KEY_PATH && 19 | process.env.GITHUB_APP_ID && 20 | process.env.GITHUB_APP_INSTALLATION_ID 21 | ) { 22 | console.log(`refreshing token...`); 23 | const { stdout } = await execAsync( 24 | `gh-token generate --key ${process.env.GITHUB_APP_PRIVATE_KEY_PATH} --app-id ${process.env.GITHUB_APP_ID} --installation-id ${process.env.GITHUB_APP_INSTALLATION_ID}` 25 | ); 26 | const token = JSON.parse(stdout).token; 27 | if (!token) { 28 | throw new Error('Failed to get GitHub token'); 29 | } 30 | cache.token = token; 31 | } else { 32 | throw new Error('No GitHub credentials provided'); 33 | } 34 | 35 | await execAsync(`gh auth setup-git`, { 36 | env: { 37 | ...process.env, 38 | GITHUB_TOKEN: cache.token, 39 | }, 40 | }); 41 | cache.updatedAt = Date.now(); 42 | return cache.token; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/safe-action.ts: -------------------------------------------------------------------------------- 1 | import { runWithAmplifyServerContext } from '@/lib/amplifyServerUtils'; 2 | import { getCurrentUser } from 'aws-amplify/auth/server'; 3 | import { createSafeActionClient, DEFAULT_SERVER_ERROR_MESSAGE } from 'next-safe-action'; 4 | import { cookies } from 'next/headers'; 5 | 6 | export class MyCustomError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = 'MyCustomError'; 10 | } 11 | } 12 | 13 | const actionClient = createSafeActionClient({ 14 | handleServerError(e) { 15 | // Log to console. 16 | console.error('Action error:', e.message); 17 | 18 | // In this case, we can use the 'MyCustomError` class to unmask errors 19 | // and return them with their actual messages to the client. 20 | if (e instanceof MyCustomError) { 21 | return e.message; 22 | } 23 | 24 | // Every other error that occurs will be masked with the default message. 25 | return DEFAULT_SERVER_ERROR_MESSAGE; 26 | }, 27 | }); 28 | 29 | export const authActionClient = actionClient.use(async ({ next }) => { 30 | const currentUser = await runWithAmplifyServerContext({ 31 | nextServerContext: { cookies }, 32 | operation: (contextSpec) => getCurrentUser(contextSpec), 33 | }); 34 | 35 | if (!currentUser) { 36 | throw new Error('Session is not valid!'); 37 | } 38 | 39 | return next({ ctx: { userId: currentUser.userId } }); 40 | }); 41 | -------------------------------------------------------------------------------- /docker/webapp.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/nodejs:22 AS builder 2 | WORKDIR /build 3 | COPY package*.json ./ 4 | COPY ./patches ./patches 5 | COPY packages/agent-core/package*.json ./packages/agent-core/ 6 | COPY packages/webapp/package*.json ./packages/webapp/ 7 | RUN --mount=type=cache,target=/root/.npm npm ci 8 | COPY ./ ./ 9 | RUN cd packages/agent-core && npm run build 10 | 11 | ARG SKIP_TS_BUILD="" 12 | ARG ALLOWED_ORIGIN_HOST="" 13 | ARG NEXT_PUBLIC_EVENT_HTTP_ENDPOINT="" 14 | ARG NEXT_PUBLIC_AWS_REGION="" 15 | ARG NEXT_PUBLIC_BEDROCK_CRI_REGION_OVERRIDE="" 16 | ENV USER_POOL_CLIENT_ID="dummy" 17 | ENV USER_POOL_ID="dummy" 18 | ENV APP_ORIGIN="https://dummy.example.com" 19 | ENV COGNITO_DOMAIN="dummy.example.com" 20 | RUN --mount=type=cache,target=/build/packages/webapp/.next/cache cd packages/webapp && npm run build 21 | 22 | FROM public.ecr.aws/lambda/nodejs:22 AS runner 23 | COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 /lambda-adapter /opt/extensions/lambda-adapter 24 | ENV AWS_LWA_PORT=3000 25 | ENV AWS_LWA_READINESS_CHECK_PATH="/api/health" 26 | ENV AWS_LWA_INVOKE_MODE="response_stream" 27 | 28 | COPY --from=builder /build/packages/webapp/.next/standalone ./ 29 | COPY --from=builder /build/packages/webapp/.next/static ./packages/webapp/.next/static 30 | COPY --from=builder /build/packages/webapp/run.sh ./run.sh 31 | 32 | RUN ln -s /tmp/cache ./packages/webapp/.next/cache 33 | 34 | ENTRYPOINT ["sh"] 35 | CMD ["run.sh"] 36 | -------------------------------------------------------------------------------- /packages/worker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worker", 3 | "scripts": { 4 | "build": "tsc", 5 | "bundle": "esbuild src/*.ts --bundle --outdir=dist --platform=node --charset=utf8", 6 | "format": "prettier --write './**/*.ts'", 7 | "format:check": "prettier --check './**/*.ts'", 8 | "test": "npm run testv -- run --silent", 9 | "testv": "vitest --passWithNoTests", 10 | "setup:local": "docker compose up -d && npx tsx --env-file=.env.local scripts/setup-dynamodb-local.ts", 11 | "start:local": "tsx --env-file=.env.local src/local.ts" 12 | }, 13 | "devDependencies": { 14 | "@types/node": "^22.13.1", 15 | "esbuild": "^0.25.1", 16 | "prettier": "^3.5.1", 17 | "tsx": "^4.19.2", 18 | "typescript": "^5.7.3", 19 | "vitest": "^3.1.1" 20 | }, 21 | "dependencies": { 22 | "@aws-sdk/client-bedrock-runtime": "^3.744.0", 23 | "@aws-sdk/client-dynamodb": "^3.744.0", 24 | "@aws-sdk/client-ec2": "^3.868.0", 25 | "@aws-sdk/client-s3": "^3.864.0", 26 | "@aws-sdk/client-sts": "^3.758.0", 27 | "@aws-sdk/credential-providers": "^3.750.0", 28 | "@aws-sdk/lib-dynamodb": "^3.744.0", 29 | "@modelcontextprotocol/sdk": "^1.24.0", 30 | "@remote-swe-agents/agent-core": "file:../agent-core", 31 | "@slack/bolt": "^4.2.0", 32 | "aws-amplify": "^6.12.3", 33 | "express": "^5.2.0", 34 | "p-retry": "^6.2.1", 35 | "playwright": "^1.55.1", 36 | "sharp": "^0.33.5", 37 | "ws": "^8.18.0", 38 | "zod": "^4.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/handlers/approve-user.ts: -------------------------------------------------------------------------------- 1 | import { WebClient } from '@slack/web-api'; 2 | import { ApproveUsers } from '../util/auth'; 3 | import { ValidationError } from '../util/error'; 4 | 5 | export async function handleApproveUser( 6 | event: { 7 | text: string; 8 | user?: string; 9 | channel: string; 10 | ts: string; 11 | thread_ts?: string; 12 | blocks?: any[]; 13 | }, 14 | client: WebClient 15 | ): Promise { 16 | const userId = event.user ?? ''; 17 | const channel = event.channel; 18 | 19 | const block = event.blocks?.[0]; 20 | if (block != null && 'elements' in block) { 21 | const element = block.elements[0]; 22 | if (element.type == 'rich_text_section') { 23 | const users = element.elements 24 | .slice(1) 25 | .filter((elem: any) => elem.type == 'user') 26 | .map((elem: any) => elem.user_id); 27 | if (users.length >= 25) { 28 | throw new ValidationError('too many users.'); 29 | } 30 | if (users.length == 0) { 31 | throw new ValidationError('no user is specified.'); 32 | } 33 | await ApproveUsers(users, channel); 34 | await client.chat.postMessage({ 35 | channel, 36 | thread_ts: event.thread_ts ?? event.ts, 37 | text: `<@${userId}> Successfully approved ${users.length} user(s) in this channel!`, 38 | }); 39 | return; 40 | } 41 | } 42 | throw new ValidationError('Usage: @remote-swe approve_user @user1 @user2'); 43 | } 44 | -------------------------------------------------------------------------------- /packages/webapp/src/app/sessions/[workerId]/component/UrlRenderer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | type UrlRendererProps = { 4 | content: string; 5 | }; 6 | 7 | export const UrlRenderer = ({ content }: UrlRendererProps) => { 8 | // Regular expression to detect URLs 9 | const urlRegex = /(https?:\/\/[^\s]+)/g; 10 | 11 | // Split text into URL and non-URL parts 12 | const parts = content.split(urlRegex); 13 | const matches = content.match(urlRegex) || []; 14 | 15 | // Arrange matched and unmatched parts alternately 16 | const elements: React.ReactNode[] = []; 17 | 18 | parts.forEach((part, i) => { 19 | if (part.match(urlRegex)) { 20 | // Display as a link if it's a URL 21 | elements.push( 22 | 29 | {part} 30 | 31 | ); 32 | } else { 33 | // For regular text content 34 | const lines = part.split('\n'); 35 | elements.push( 36 | 37 | {lines.map((line, j) => ( 38 | 39 | {j > 0 &&
} 40 | {line} 41 |
42 | ))} 43 |
44 | ); 45 | } 46 | }); 47 | 48 | return
{elements}
; 49 | }; 50 | -------------------------------------------------------------------------------- /cdk/lib/constructs/lambda-warmer/index.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { LambdaInvoke } from 'aws-cdk-lib/aws-scheduler-targets'; 3 | import { Schedule, ScheduleExpression, ScheduleTargetInput } from 'aws-cdk-lib/aws-scheduler'; 4 | import { Duration } from 'aws-cdk-lib'; 5 | import { Function, Runtime } from 'aws-cdk-lib/aws-lambda'; 6 | import { LambdaWarmerPayload } from './lambda/type'; 7 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 8 | import { join } from 'path'; 9 | 10 | export interface LambdaWarmerProps {} 11 | 12 | export class LambdaWarmer extends Construct { 13 | private handler: Function; 14 | 15 | constructor(scope: Construct, id: string, props: LambdaWarmerProps) { 16 | super(scope, id); 17 | 18 | const handler = new NodejsFunction(this, 'Handler', { 19 | entry: join(__dirname, 'lambda', 'handler.ts'), 20 | runtime: Runtime.NODEJS_22_X, 21 | timeout: Duration.seconds(10), 22 | }); 23 | 24 | this.handler = handler; 25 | } 26 | 27 | addTarget(id: string, url: string, concurrency: number) { 28 | const target = new LambdaInvoke(this.handler, { 29 | maxEventAge: Duration.minutes(1), 30 | retryAttempts: 0, 31 | input: ScheduleTargetInput.fromObject({ 32 | url, 33 | concurrency, 34 | } satisfies LambdaWarmerPayload), 35 | }); 36 | 37 | new Schedule(this, `${id}Schedule`, { 38 | schedule: ScheduleExpression.rate(Duration.minutes(4)), 39 | target, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/github-actions/src/handlers/issue-assignment.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { startRemoteSweSession, RemoteSweApiConfig } from '../lib/remote-swe-api'; 3 | import { submitIssueComment } from '../lib/comments'; 4 | import { shouldTriggerForAssignee } from '../lib/trigger'; 5 | import { ActionContext } from '../lib/context'; 6 | import { WebhookPayload } from '@actions/github/lib/interfaces'; 7 | 8 | export async function handleIssueAssignmentEvent(context: ActionContext, payload: WebhookPayload): Promise { 9 | const assignee = payload.assignee?.login; 10 | 11 | // Check assignee trigger if specified 12 | if (!shouldTriggerForAssignee(context, [assignee])) { 13 | core.info(`Assignee trigger not matched for user: ${assignee}, exiting`); 14 | return; 15 | } 16 | 17 | if (!payload.issue) { 18 | core.info(`payload.issue is empty.`); 19 | return; 20 | } 21 | 22 | const message = `Please resolve this issue and create a pull request. Use GitHub CLI to check the issue detail. 23 | Issue URL: ${payload.issue.html_url}`; 24 | 25 | const sessionContext = {}; 26 | 27 | // Start remote-swe session 28 | core.info('Trigger conditions met, starting remote-swe session'); 29 | const session = await startRemoteSweSession(message, sessionContext, context); 30 | 31 | // Post comment with session URL to the original PR/Issue 32 | await submitIssueComment(session.sessionId, session.sessionUrl, payload.issue.number); 33 | 34 | core.info('Remote-swe session started successfully'); 35 | } 36 | -------------------------------------------------------------------------------- /packages/webapp/src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import type { NextRequest } from 'next/server'; 3 | import { fetchAuthSession } from 'aws-amplify/auth/server'; 4 | import { runWithAmplifyServerContext } from '@/lib/amplifyServerUtils'; 5 | 6 | // This file is critical for webapp authentication mechanism. 7 | // DO NOT remove any existing logic if you are not sure! 8 | 9 | export async function middleware(request: NextRequest) { 10 | const response = NextResponse.next(); 11 | 12 | const authenticated = await runWithAmplifyServerContext({ 13 | nextServerContext: { request, response }, 14 | operation: async (contextSpec) => { 15 | try { 16 | const session = await fetchAuthSession(contextSpec); 17 | return session.tokens?.accessToken !== undefined && session.tokens?.idToken !== undefined; 18 | } catch (error) { 19 | console.log(error); 20 | return false; 21 | } 22 | }, 23 | }); 24 | 25 | if (authenticated) { 26 | return response; 27 | } 28 | 29 | return NextResponse.redirect(new URL('/sign-in', request.url)); 30 | } 31 | 32 | export const config = { 33 | runtime: 'nodejs', 34 | matcher: [ 35 | /* 36 | * Match all request paths except for the ones starting with: 37 | * - api (API routes) 38 | * - _next/static (static files) 39 | * - _next/image (image optimization files) 40 | * - favicon.ico (favicon file) 41 | */ 42 | '/((?!api|_next/static|_next/image|favicon.ico|sign-in).*)', 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /packages/webapp/src/lib/events.ts: -------------------------------------------------------------------------------- 1 | import { SignatureV4 } from '@smithy/signature-v4'; 2 | import { defaultProvider } from '@aws-sdk/credential-provider-node'; 3 | import { HttpRequest } from '@smithy/protocol-http'; 4 | import { Sha256 } from '@aws-crypto/sha256-js'; 5 | 6 | const httpEndpoint = process.env.EVENT_HTTP_ENDPOINT!; 7 | const region = process.env.AWS_REGION!; 8 | 9 | export async function sendEvent(channelName: string, payload: unknown) { 10 | if (httpEndpoint == null) { 11 | console.log(`event api is not configured!`); 12 | return; 13 | } 14 | 15 | const endpoint = `${httpEndpoint}/event`; 16 | const url = new URL(endpoint); 17 | 18 | // generate request 19 | const requestToBeSigned = new HttpRequest({ 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | host: url.host, 24 | }, 25 | hostname: url.host, 26 | body: JSON.stringify({ 27 | channel: `event-bus/${channelName}`, 28 | events: [JSON.stringify({ payload })], 29 | }), 30 | path: url.pathname, 31 | }); 32 | 33 | // initialize signer 34 | const signer = new SignatureV4({ 35 | credentials: defaultProvider(), 36 | region, 37 | service: 'appsync', 38 | sha256: Sha256, 39 | }); 40 | 41 | // sign request 42 | const signed = await signer.sign(requestToBeSigned); 43 | const request = new Request(endpoint, signed); 44 | 45 | // publish event via fetch 46 | const res = await fetch(request); 47 | 48 | const t = await res.text(); 49 | console.log(t); 50 | } 51 | -------------------------------------------------------------------------------- /packages/webapp/src/actions/image/action.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { authActionClient } from '@/lib/safe-action'; 4 | import { GetObjectCommand, HeadObjectCommand, NoSuchKey } from '@aws-sdk/client-s3'; 5 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 6 | import { BucketName, s3 } from '@remote-swe-agents/agent-core/aws'; 7 | import { z } from 'zod'; 8 | 9 | const getImageUrlsSchema = z.object({ 10 | keys: z.array(z.string()), 11 | }); 12 | 13 | export const getImageUrls = authActionClient.inputSchema(getImageUrlsSchema).action(async ({ parsedInput }) => { 14 | const { keys } = parsedInput; 15 | if (!BucketName) { 16 | throw new Error('S3 bucket name is not configured'); 17 | } 18 | 19 | const results = ( 20 | await Promise.all( 21 | keys.map(async (key) => { 22 | try { 23 | await s3.send( 24 | new HeadObjectCommand({ 25 | Bucket: BucketName, 26 | Key: key, 27 | }) 28 | ); 29 | } catch (e) { 30 | if (e instanceof NoSuchKey) { 31 | return; 32 | } 33 | throw e; 34 | } 35 | 36 | const command = new GetObjectCommand({ 37 | Bucket: BucketName, 38 | Key: key, 39 | }); 40 | 41 | const signedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 }); 42 | 43 | return { 44 | url: signedUrl, 45 | key, 46 | }; 47 | }) 48 | ) 49 | ).filter((r) => r != null); 50 | 51 | return results; 52 | }); 53 | -------------------------------------------------------------------------------- /packages/webapp/src/actions/upload/action.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { authActionClient } from '@/lib/safe-action'; 4 | import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; 5 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; 6 | import { randomBytes } from 'crypto'; 7 | import { z } from 'zod'; 8 | 9 | const s3 = new S3Client({}); 10 | 11 | const bucketName = process.env.BUCKET_NAME; 12 | 13 | const getUploadUrlSchema = z.object({ 14 | workerId: z.string().optional(), 15 | contentType: z.string(), 16 | }); 17 | 18 | export const getUploadUrl = authActionClient.inputSchema(getUploadUrlSchema).action(async ({ parsedInput }) => { 19 | const { workerId, contentType } = parsedInput; 20 | if (!bucketName) { 21 | throw new Error('S3 bucket name is not configured'); 22 | } 23 | if (!['image/png', 'image/webp', 'image/jpeg'].includes(contentType)) { 24 | throw new Error('Invalid content type'); 25 | } 26 | 27 | const extension = contentType.split('/')[1]; 28 | const randomId = randomBytes(8).toString('hex'); 29 | 30 | // If workerId is provided, use it in the path, otherwise use webapp_init prefix 31 | const key = workerId ? `${workerId}/${randomId}.${extension}` : `webapp_init/${randomId}.${extension}`; 32 | 33 | const command = new PutObjectCommand({ 34 | Bucket: bucketName, 35 | Key: key, 36 | ContentType: contentType, 37 | }); 38 | 39 | const signedUrl = await getSignedUrl(s3, command, { expiresIn: 60 }); 40 | 41 | return { 42 | url: signedUrl, 43 | key, 44 | }; 45 | }); 46 | -------------------------------------------------------------------------------- /packages/agent-core/README.md: -------------------------------------------------------------------------------- 1 | # @remote-swe-agents/agent-core 2 | 3 | This package contains common code shared between the slack-bolt-app and worker packages in the remote-swe-agents monorepo. 4 | 5 | ## Installation 6 | 7 | Since this is a workspace package, you don't need to install it separately. It's automatically available to other packages in the monorepo. 8 | 9 | ## Usage 10 | 11 | Import the shared utilities in your code: 12 | 13 | ```typescript 14 | // Import specific utilities 15 | import { s3, BucketName, getBytesFromKey } from '@remote-swe-agents/agent-core'; 16 | import { ddb, TableName } from '@remote-swe-agents/agent-core'; 17 | import { createSlackApp, sendMessage, sendImageWithMessage } from '@remote-swe-agents/agent-core'; 18 | 19 | // Or import everything 20 | import * as common from '@remote-swe-agents/agent-core'; 21 | ``` 22 | 23 | ## Available Utilities 24 | 25 | ### AWS S3 26 | 27 | - `s3`: S3Client instance 28 | - `BucketName`: Environment variable for the S3 bucket name 29 | - `getBytesFromKey(key: string)`: Function to get bytes from an S3 object 30 | 31 | ### AWS DynamoDB 32 | 33 | - `ddb`: DynamoDBDocumentClient instance 34 | - `TableName`: Environment variable for the DynamoDB table name 35 | 36 | ### Slack 37 | 38 | - `createSlackApp(botToken: string, signingSecret?: string)`: Function to create a Slack app instance 39 | - `sendMessage(client, channelID, threadTs, message, progress?)`: Function to send a message to Slack 40 | - `sendImageWithMessage(client, channelID, threadTs, imagePath, message, progress?)`: Function to send an image with a message to Slack 41 | -------------------------------------------------------------------------------- /packages/agent-core/src/schema/events.ts: -------------------------------------------------------------------------------- 1 | import z from 'zod'; 2 | import { agentStatusSchema } from './agent'; 3 | import { instanceStatusSchema } from './session'; 4 | 5 | export const webappEventSchema = z.discriminatedUnion('type', [ 6 | z.object({ 7 | type: z.literal('message'), 8 | role: z.union([z.literal('user'), z.literal('assistant')]), 9 | workerId: z.string(), 10 | message: z.string(), 11 | timestamp: z.number(), 12 | thinkingBudget: z.number().optional(), 13 | reasoningText: z.string().optional(), 14 | }), 15 | z.object({ 16 | type: z.literal('toolUse'), 17 | toolName: z.string(), 18 | workerId: z.string(), 19 | toolUseId: z.string(), 20 | input: z.string(), 21 | timestamp: z.number(), 22 | thinkingBudget: z.number().optional(), 23 | reasoningText: z.string().optional(), 24 | }), 25 | z.object({ 26 | type: z.literal('toolResult'), 27 | toolName: z.string(), 28 | workerId: z.string(), 29 | toolUseId: z.string(), 30 | output: z.string(), 31 | timestamp: z.number(), 32 | }), 33 | z.object({ 34 | type: z.literal('instanceStatusChanged'), 35 | status: instanceStatusSchema, 36 | workerId: z.string(), 37 | timestamp: z.number(), 38 | }), 39 | z.object({ 40 | type: z.literal('agentStatusUpdate'), 41 | status: agentStatusSchema, 42 | timestamp: z.number(), 43 | workerId: z.string(), 44 | }), 45 | z.object({ 46 | type: z.literal('sessionTitleUpdate'), 47 | newTitle: z.string(), 48 | timestamp: z.number(), 49 | workerId: z.string(), 50 | }), 51 | ]); 52 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Sun, Moon, Monitor } from 'lucide-react'; 5 | import { Button } from '@/components/ui/button'; 6 | import { useTranslations } from 'next-intl'; 7 | 8 | export default function ThemeToggle() { 9 | const { theme, setTheme } = useTheme(); 10 | const t = useTranslations('theme'); 11 | 12 | const cycleTheme = () => { 13 | if (theme === 'light') { 14 | setTheme('dark'); 15 | } else if (theme === 'dark') { 16 | setTheme('system'); 17 | } else { 18 | setTheme('light'); 19 | } 20 | }; 21 | 22 | const getIcon = () => { 23 | switch (theme) { 24 | case 'light': 25 | return ; 26 | case 'dark': 27 | return ; 28 | case 'system': 29 | return ; 30 | default: 31 | return ; 32 | } 33 | }; 34 | 35 | const getTooltip = () => { 36 | switch (theme) { 37 | case 'light': 38 | return t('light'); 39 | case 'dark': 40 | return t('dark'); 41 | case 'system': 42 | return t('system'); 43 | default: 44 | return t('light'); 45 | } 46 | }; 47 | 48 | return ( 49 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/util/session-map.ts: -------------------------------------------------------------------------------- 1 | import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb'; 2 | import { ddb, TableName } from '@remote-swe-agents/agent-core/aws'; 3 | import { getSession } from '@remote-swe-agents/agent-core/lib'; 4 | import { z } from 'zod'; 5 | 6 | /** 7 | * When taking over a session, this item is created and associate a slack thread (threadTs) 8 | * with a sessionId. 9 | */ 10 | const sessionMapSchema = z.object({ 11 | PK: z.literal('session-map'), 12 | /** 13 | * key of the session map 14 | */ 15 | SK: z.string(), 16 | sessionId: z.string(), 17 | }); 18 | 19 | export type SessionMap = z.infer; 20 | 21 | const getSessionMap = async (channelId: string, threadTs: string) => { 22 | const result = await ddb.send( 23 | new GetCommand({ 24 | TableName, 25 | Key: { 26 | PK: 'session-map', 27 | SK: `slack-${channelId}-${threadTs}`, 28 | }, 29 | }) 30 | ); 31 | 32 | if (!result.Item) { 33 | return; 34 | } 35 | 36 | return result.Item as SessionMap; 37 | }; 38 | 39 | export const getSessionIdFromSlack = async (channelId: string, threadTs: string, isThreadRoot: boolean) => { 40 | const workerId = threadTs.replace('.', ''); 41 | if (isThreadRoot) return workerId; 42 | 43 | const session = await getSession(workerId); 44 | if (session) { 45 | return workerId; 46 | } 47 | 48 | const sessionMap = await getSessionMap(channelId, threadTs); 49 | if (!sessionMap) { 50 | throw new Error('No session was found for the thread!'); 51 | } 52 | 53 | return sessionMap.sessionId; 54 | }; 55 | -------------------------------------------------------------------------------- /resources/slack-app-manifest-relaxed.json: -------------------------------------------------------------------------------- 1 | { 2 | "display_information": { 3 | "name": "remoteSWE", 4 | "description": "They work hard, you chill.", 5 | "background_color": "#070708" 6 | }, 7 | "features": { 8 | "bot_user": { 9 | "display_name": "remote-swe", 10 | "always_online": true 11 | } 12 | }, 13 | "oauth_config": { 14 | "redirect_urls": [ 15 | "https://redacted.execute-api.ap-northeast-1.amazonaws.com" 16 | ], 17 | "scopes": { 18 | "bot": [ 19 | "app_mentions:read", 20 | "chat:write", 21 | "files:read", 22 | "files:write", 23 | "reactions:read", 24 | "reactions:write", 25 | "channels:history", 26 | "groups:history", 27 | "im:history" 28 | ] 29 | } 30 | }, 31 | "settings": { 32 | "event_subscriptions": { 33 | "request_url": "https://redacted.execute-api.ap-northeast-1.amazonaws.com", 34 | "bot_events": [ 35 | "app_mention", 36 | "message.channels", 37 | "message.groups", 38 | "message.im" 39 | ] 40 | }, 41 | "interactivity": { 42 | "is_enabled": true, 43 | "request_url": "https://redacted.execute-api.ap-northeast-1.amazonaws.com" 44 | }, 45 | "org_deploy_enabled": false, 46 | "socket_mode_enabled": false, 47 | "token_rotation_enabled": false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/slack-bolt-app/src/util/auth.ts: -------------------------------------------------------------------------------- 1 | import { BatchWriteCommand, QueryCommand } from '@aws-sdk/lib-dynamodb'; 2 | import { ddb, TableName } from '@remote-swe-agents/agent-core/aws'; 3 | 4 | // We cannot use email to refer to a user because user:read scope is forbidden. 5 | // Instead, we use userId directly. 6 | const getAdminUserIds = (): string[] => { 7 | const adminUserIdList = process.env.ADMIN_USER_ID_LIST; 8 | if (!adminUserIdList) return []; 9 | return adminUserIdList.split(','); 10 | }; 11 | 12 | export const isAuthorized = async (userId: string, channelId: string) => { 13 | // If ADMIN_USER_ID_LIST is not set, authorize all users 14 | if (!process.env.ADMIN_USER_ID_LIST) { 15 | return true; 16 | } 17 | 18 | const adminUserIds = getAdminUserIds(); 19 | if (adminUserIds.includes(userId)) return true; 20 | 21 | const res = await ddb.send( 22 | new QueryCommand({ 23 | TableName, 24 | KeyConditionExpression: 'PK = :pk', 25 | ExpressionAttributeValues: { 26 | ':pk': `approved-${channelId}`, 27 | }, 28 | }) 29 | ); 30 | const approved = (res.Items ?? []).map((item) => item.SK as string); 31 | 32 | return approved.includes(userId); 33 | }; 34 | 35 | export const ApproveUsers = async (userIdList: string[], channelId: string) => { 36 | await ddb.send( 37 | new BatchWriteCommand({ 38 | RequestItems: { 39 | [TableName]: userIdList.map((userId) => ({ 40 | PutRequest: { 41 | Item: { 42 | PK: `approved-${channelId}`, 43 | SK: userId, 44 | }, 45 | }, 46 | })), 47 | }, 48 | }) 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /resources/assume-role.sh: -------------------------------------------------------------------------------- 1 | # Variable definitions 2 | SOURCE_ACCOUNT="" # AWS account ID that will assume the role 3 | ROLE_NAME="bedrock-remote-swe-role" # Name of the role to be created 4 | 5 | # Create trust policy (temporary file) 6 | cat > trust-policy.json << EOF 7 | { 8 | "Version": "2012-10-17", 9 | "Statement": [ 10 | { 11 | "Effect": "Allow", 12 | "Principal": { 13 | "AWS": "arn:aws:iam::${SOURCE_ACCOUNT}:root" 14 | }, 15 | "Action": "sts:AssumeRole", 16 | "Condition": {} 17 | } 18 | ] 19 | } 20 | EOF 21 | 22 | # Create permission policy (temporary file) - Example of Bedrock invoke permissions 23 | cat > permission-policy.json << EOF 24 | { 25 | "Version": "2012-10-17", 26 | "Statement": [ 27 | { 28 | "Effect": "Allow", 29 | "Action": [ 30 | "bedrock:InvokeModel" 31 | ], 32 | "Resource": [ 33 | "*" 34 | ] 35 | } 36 | ] 37 | } 38 | EOF 39 | 40 | # Create the role 41 | aws iam create-role \ 42 | --role-name ${ROLE_NAME} \ 43 | --assume-role-policy-document file://trust-policy.json \ 44 | --no-cli-pager 45 | 46 | # Add inline policy 47 | aws iam put-role-policy \ 48 | --role-name ${ROLE_NAME} \ 49 | --policy-name "BedrockRuntimePolicy" \ 50 | --policy-document file://permission-policy.json \ 51 | --no-cli-pager 52 | 53 | # Delete temporary files 54 | rm trust-policy.json permission-policy.json 55 | 56 | # Display the ARN of the created role 57 | aws iam get-role --role-name ${ROLE_NAME} --query 'Role.Arn' --output text --no-cli-pager 58 | -------------------------------------------------------------------------------- /packages/webapp/src/app/custom-agent/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { authActionClient } from '@/lib/safe-action'; 4 | import { upsertCustomAgentSchema, deleteCustomAgentSchema } from './schemas'; 5 | import { createCustomAgent, updateCustomAgent, deleteCustomAgent } from '@remote-swe-agents/agent-core/lib'; 6 | import { revalidatePath } from 'next/cache'; 7 | 8 | export const upsertCustomAgentAction = authActionClient 9 | .inputSchema(upsertCustomAgentSchema) 10 | .action(async ({ parsedInput }) => { 11 | try { 12 | const { id, ...agentData } = parsedInput; 13 | agentData.mcpConfig = JSON.stringify(JSON.parse(agentData.mcpConfig)); // minify 14 | 15 | let agent; 16 | if (id) { 17 | // Update existing agent 18 | agent = await updateCustomAgent(id, agentData); 19 | } else { 20 | // Create new agent 21 | agent = await createCustomAgent(agentData); 22 | } 23 | 24 | revalidatePath('/custom-agent'); 25 | return { success: true, agent }; 26 | } catch (error) { 27 | console.error('Error upserting custom agent:', error); 28 | throw new Error('Failed to save custom agent'); 29 | } 30 | }); 31 | 32 | export const deleteCustomAgentAction = authActionClient 33 | .inputSchema(deleteCustomAgentSchema) 34 | .action(async ({ parsedInput }) => { 35 | try { 36 | const { id } = parsedInput; 37 | await deleteCustomAgent(id); 38 | 39 | revalidatePath('/custom-agent'); 40 | return { success: true }; 41 | } catch (error) { 42 | console.error('Error deleting custom agent:', error); 43 | throw new Error('Failed to delete custom agent'); 44 | } 45 | }); 46 | -------------------------------------------------------------------------------- /.github/workflows/deploy-prod.yml: -------------------------------------------------------------------------------- 1 | name: Deploy-Prod 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | concurrency: ${{ github.workflow }} 8 | env: 9 | ROLE_ARN: ${{ secrets.IAM_ROLE_ARN }} 10 | TARGET_ENV: 'Prod' 11 | TARGET_AWS_REGION: us-east-1 12 | AWS_ACCOUNT_ID_LIST_FOR_LB: ${{ secrets.PROD_AWS_ACCOUNT_ID_LIST_FOR_LB }} 13 | SLACK_ADMIN_USER_ID_LIST: ${{ vars.PROD_ADMIN_USER_ID_LIST }} 14 | ROLE_NAME_FOR_LB: ${{ vars.PROD_ROLE_NAME_FOR_LB }} 15 | WORKER_ADDITIONAL_POLICIES: ${{ secrets.PROD_WORKER_ADDITIONAL_POLICIES }} 16 | ENABLE_LAMBDA_WARMER: true 17 | BEDROCK_CRI_REGION_OVERRIDE: global 18 | jobs: 19 | Deploy-cdk: 20 | runs-on: ubuntu-24.04-arm 21 | steps: 22 | - name: add-mask 23 | run: | 24 | echo "::add-mask::${{ secrets.PRODACCOUNTID }}" 25 | - uses: actions/checkout@v4 26 | - name: Use Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: '22.x' 30 | - name: configure aws credentials 31 | uses: aws-actions/configure-aws-credentials@v4 32 | with: 33 | role-to-assume: ${{ env.ROLE_ARN }} 34 | role-session-name: gha-session 35 | aws-region: ${{ env.TARGET_AWS_REGION }} 36 | - run: | 37 | npm ci 38 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws 39 | npx cdk deploy --all --require-approval never 40 | working-directory: ./cdk 41 | name: build and deploy CDK 42 | permissions: 43 | id-token: write # This is required for requesting the JWT 44 | contents: read # This is required for actions/checkout 45 | -------------------------------------------------------------------------------- /packages/webapp/src/hooks/use-event-bus.ts: -------------------------------------------------------------------------------- 1 | import { decodeJWT } from 'aws-amplify/auth'; 2 | import { Amplify } from 'aws-amplify'; 3 | import { events } from 'aws-amplify/data'; 4 | import { useEffect } from 'react'; 5 | 6 | Amplify.configure( 7 | { 8 | API: { 9 | Events: { 10 | endpoint: `${process.env.NEXT_PUBLIC_EVENT_HTTP_ENDPOINT}/event`, 11 | region: process.env.NEXT_PUBLIC_AWS_REGION, 12 | defaultAuthMode: 'userPool', 13 | }, 14 | }, 15 | }, 16 | { 17 | Auth: { 18 | tokenProvider: { 19 | getTokens: async () => { 20 | const res = await fetch('/api/cognito-token'); 21 | const { accessToken } = await res.json(); 22 | return { 23 | accessToken: decodeJWT(accessToken), 24 | }; 25 | }, 26 | }, 27 | }, 28 | } 29 | ); 30 | 31 | type UseEventBusProps = { 32 | channelName: string; 33 | onReceived: (payload: unknown) => void; 34 | }; 35 | 36 | export const useEventBus = ({ channelName, onReceived }: UseEventBusProps) => { 37 | useEffect(() => { 38 | const connectAndSubscribe = async () => { 39 | const channel = await events.connect(`event-bus/${channelName}`); 40 | console.log(`subscribing channel ${channelName}`); 41 | 42 | channel.subscribe({ 43 | next: (data) => { 44 | onReceived(data.event); 45 | }, 46 | error: (err) => console.error('error', err), 47 | }); 48 | return channel; 49 | }; 50 | 51 | const pr = connectAndSubscribe(); 52 | 53 | return () => { 54 | pr.then((channel) => { 55 | channel.close(); 56 | }); 57 | }; 58 | }, [channelName, onReceived]); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/agent-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@remote-swe-agents/agent-core", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "exports": { 6 | "./lib": { 7 | "types": "./dist/lib/index.d.ts", 8 | "default": "./dist/lib/index.js" 9 | }, 10 | "./aws": { 11 | "types": "./dist/lib/aws/index.d.ts", 12 | "default": "./dist/lib/aws/index.js" 13 | }, 14 | "./schema": { 15 | "types": "./dist/schema/index.d.ts", 16 | "default": "./dist/schema/index.js" 17 | }, 18 | "./tools": { 19 | "types": "./dist/tools/index.d.ts", 20 | "default": "./dist/tools/index.js" 21 | } 22 | }, 23 | "scripts": { 24 | "build": "tsc", 25 | "watch": "tsc -w", 26 | "test": "vitest run", 27 | "format": "prettier --write \"src/**/*.ts\"", 28 | "format:check": "prettier --check \"src/**/*.ts\"" 29 | }, 30 | "dependencies": { 31 | "@aws-sdk/client-bedrock-agentcore": "^3.864.0", 32 | "@aws-sdk/client-bedrock-runtime": "^3.744.0", 33 | "@aws-sdk/client-dynamodb": "^3.744.0", 34 | "@aws-sdk/client-ec2": "^3.746.0", 35 | "@aws-sdk/client-s3": "^3.758.0", 36 | "@aws-sdk/client-ssm": "^3.817.0", 37 | "@aws-sdk/client-sts": "^3.758.0", 38 | "@aws-sdk/credential-providers": "^3.750.0", 39 | "@aws-sdk/lib-dynamodb": "^3.744.0", 40 | "@modelcontextprotocol/sdk": "^1.24.0", 41 | "@octokit/rest": "^22.0.0", 42 | "@slack/bolt": "^4.2.0", 43 | "p-retry": "^6.2.1", 44 | "sharp": "^0.33.5" 45 | }, 46 | "devDependencies": { 47 | "@types/node": "^22.13.1", 48 | "typescript": "^5.7.3", 49 | "vitest": "^3.1.1", 50 | "zod": "^4.0.0" 51 | }, 52 | "peerDependencies": { 53 | "zod": "^4.0.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/github-actions/src/handlers/pr-assignment.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import { addIssueCommentTool } from '@remote-swe-agents/agent-core/tools'; 3 | import { startRemoteSweSession, RemoteSweApiConfig } from '../lib/remote-swe-api'; 4 | import { submitIssueComment } from '../lib/comments'; 5 | import { shouldTriggerForAssignee } from '../lib/trigger'; 6 | import { ActionContext } from '../lib/context'; 7 | import { WebhookPayload } from '@actions/github/lib/interfaces'; 8 | 9 | export async function handlePrAssignmentEvent(context: ActionContext, payload: WebhookPayload): Promise { 10 | const assignee = payload.assignee?.login; 11 | 12 | // Check assignee trigger if specified 13 | if (!shouldTriggerForAssignee(context, [assignee])) { 14 | core.info(`Assignee trigger not matched for user: ${assignee}, exiting`); 15 | return; 16 | } 17 | 18 | if (!payload.pull_request) { 19 | core.info(`payload.pull_request is empty.`); 20 | return; 21 | } 22 | 23 | const message = `Please review this pull request and provide feedback or comments. 24 | 25 | Use GitHub CLI to check the pull request detail. When providing feedback, use ${addIssueCommentTool.name} tool to directly submit comments to the PR. 26 | 27 | PR URL: ${payload.pull_request.html_url}`.trim(); 28 | 29 | const sessionContext = {}; 30 | 31 | // Start remote-swe session 32 | core.info('Trigger conditions met, starting remote-swe session'); 33 | const session = await startRemoteSweSession(message, sessionContext, context); 34 | 35 | // Post comment with session URL to the original PR/Issue 36 | await submitIssueComment(session.sessionId, session.sessionUrl, payload.pull_request.number); 37 | 38 | core.info('Remote-swe session started successfully'); 39 | } 40 | -------------------------------------------------------------------------------- /cdk/lib/constructs/worker/resources/image-component-template.yml: -------------------------------------------------------------------------------- 1 | name: MyComponentDocument 2 | description: This is an example component document 3 | schemaVersion: 1.0 4 | 5 | phases: 6 | - name: build 7 | steps: 8 | - name: InstallUpdates 9 | action: UpdateOS 10 | maxAttempts: 3 11 | - name: InstallDependencies 12 | action: ExecuteBash 13 | inputs: 14 | # commands will be replaced from CDK 15 | commands: 16 | - dummy 17 | - name: validate 18 | steps: 19 | - name: ValidateStep 20 | action: ExecuteBash 21 | inputs: 22 | commands: 23 | - aws --version || exit 1 24 | - docker --version || exit 1 25 | - python --version || exit 1 26 | - gh --version || exit 1 27 | - sudo -u ubuntu bash -i -c "uv --version" || exit 1 28 | - sudo -u ubuntu bash -i -c "uvx --version" || exit 1 29 | - sudo -u ubuntu bash -i -c "node --version" || exit 1 30 | - sudo -u ubuntu bash -i -c "npm --version" || exit 1 31 | - sudo -u ubuntu bash -i -c "npx --version" || exit 1 32 | - name: test 33 | steps: 34 | - name: TestStep 35 | action: ExecuteBash 36 | inputs: 37 | commands: 38 | - aws --version || exit 1 39 | - docker --version || exit 1 40 | - python --version || exit 1 41 | - gh --version || exit 1 42 | - sudo -u ubuntu bash -i -c "uv --version" || exit 1 43 | - sudo -u ubuntu bash -i -c "uvx --version" || exit 1 44 | - sudo -u ubuntu bash -i -c "node --version" || exit 1 45 | - sudo -u ubuntu bash -i -c "npm --version" || exit 1 46 | - sudo -u ubuntu bash -i -c "npx --version" || exit 1 47 | -------------------------------------------------------------------------------- /packages/webapp/src/app/preferences/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/Header'; 2 | import { readCommonPrompt, getPreferences } from '@remote-swe-agents/agent-core/lib'; 3 | import PromptForm from './components/PromptForm'; 4 | import PreferenceSection from './components/PreferenceSection'; 5 | import GlobalPreferencesForm from './components/GlobalPreferencesForm'; 6 | import { getTranslations } from 'next-intl/server'; 7 | 8 | export const dynamic = 'force-dynamic'; 9 | 10 | export default async function PreferencesPage() { 11 | // Get the current prompt and preferences directly in server component 12 | const promptData = await readCommonPrompt(); 13 | const additionalSystemPrompt = promptData?.additionalSystemPrompt || ''; 14 | const globalPreferences = await getPreferences(); 15 | 16 | const t = await getTranslations('preferences'); 17 | const promptT = await getTranslations('preferences.prompt'); 18 | const globalT = await getTranslations('preferences.global'); 19 | 20 | return ( 21 |
22 |
23 | 24 |
25 |
26 |

{t('title')}

27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Remote SWE Action' 2 | description: 'GitHub Action to trigger remote-swe sessions based on comments in issues and PRs' 3 | branding: 4 | icon: 'code' 5 | color: 'blue' 6 | 7 | inputs: 8 | trigger_phrase: 9 | description: 'The phrase that triggers the action (e.g., @remote-swe)' 10 | required: true 11 | default: '@remote-swe' 12 | assignee_trigger: 13 | description: 'The assignee username that triggers the action (e.g., @remote-swe-user)' 14 | required: false 15 | api_base_url: 16 | description: 'Base URL for the remote-swe API (e.g., https://remote-swe.example.com)' 17 | required: true 18 | api_key: 19 | description: 'API key for authentication with the remote-swe service' 20 | required: true 21 | 22 | runs: 23 | using: 'composite' 24 | steps: 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '22' 29 | - name: Install dependencies 30 | shell: bash 31 | run: | 32 | rm -rf packages/slack-bolt-app 33 | rm -rf packages/webapp 34 | rm -rf packages/worker 35 | rm -rf patches 36 | npm ci 37 | working-directory: ${{ github.action_path }} 38 | - name: Build common module 39 | shell: bash 40 | run: | 41 | npm run build -w @remote-swe-agents/agent-core 42 | working-directory: ${{ github.action_path }} 43 | - name: Run action 44 | shell: bash 45 | run: | 46 | npm run start -w github-actions 47 | working-directory: ${{ github.action_path }} 48 | env: 49 | INPUT_TRIGGER_PHRASE: ${{ inputs.trigger_phrase }} 50 | INPUT_ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }} 51 | INPUT_API_BASE_URL: ${{ inputs.api_base_url }} 52 | INPUT_API_KEY: ${{ inputs.api_key }} 53 | GITHUB_TOKEN: ${{ github.token }} 54 | -------------------------------------------------------------------------------- /packages/webapp/src/app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { getLocale, getTranslations } from 'next-intl/server'; 2 | import Link from 'next/link'; 3 | 4 | export default async function SignInPage() { 5 | const t = await getTranslations('auth'); 6 | const headerT = await getTranslations('header'); 7 | const locale = await getLocale(); 8 | 9 | return ( 10 |
11 |
12 |

{headerT('title')}

13 |

14 | Streamline software development by interacting with AI agents 15 |

16 |
17 | 18 |
19 |
20 |
21 |

22 | Please sign in with your Cognito account to continue 23 |

24 | 25 | 30 | {t('signInWith', { provider: 'Cognito' })} 31 | 32 |
33 |
34 |
35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/webapp/src/hooks/use-scroll-position.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from 'react'; 2 | 3 | interface ScrollPositionOptions { 4 | threshold?: number; 5 | bottomThreshold?: number; 6 | } 7 | 8 | export function useScrollPosition(options: ScrollPositionOptions = {}) { 9 | const { threshold = 80, bottomThreshold = 0.95 } = options; 10 | const [isBottom, setIsBottom] = useState(false); 11 | const [isHeaderVisible, setIsHeaderVisible] = useState(true); 12 | const [lastScrollY, setLastScrollY] = useState(0); 13 | const rafRef = useRef(0); 14 | 15 | const handleScroll = useCallback(() => { 16 | if (rafRef.current) { 17 | cancelAnimationFrame(rafRef.current); 18 | } 19 | 20 | rafRef.current = requestAnimationFrame(() => { 21 | const currentScrollY = window.scrollY; 22 | 23 | // Header visibility logic 24 | if (currentScrollY > lastScrollY && currentScrollY > threshold) { 25 | setIsHeaderVisible(false); 26 | } else { 27 | setIsHeaderVisible(true); 28 | } 29 | setLastScrollY(currentScrollY); 30 | 31 | // Bottom detection logic 32 | const height = document.documentElement.scrollHeight - document.documentElement.clientHeight; 33 | const windowScroll = document.documentElement.scrollTop; 34 | const scrolled = height > 0 ? windowScroll / height : 0; 35 | 36 | const newIsBottom = scrolled > bottomThreshold; 37 | setIsBottom((prev) => (prev !== newIsBottom ? newIsBottom : prev)); 38 | }); 39 | }, [lastScrollY, threshold, bottomThreshold]); 40 | 41 | useEffect(() => { 42 | window.addEventListener('scroll', handleScroll, { passive: true }); 43 | return () => { 44 | window.removeEventListener('scroll', handleScroll); 45 | if (rafRef.current) { 46 | cancelAnimationFrame(rafRef.current); 47 | } 48 | }; 49 | }, [handleScroll]); 50 | 51 | return { isBottom, isHeaderVisible }; 52 | } 53 | -------------------------------------------------------------------------------- /packages/webapp/src/app/preferences/components/PromptForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from 'react'; 4 | import { Button } from '@/components/ui/button'; 5 | import { useAction } from 'next-safe-action/hooks'; 6 | import { updateAdditionalSystemPrompt } from '../actions'; 7 | import { Save } from 'lucide-react'; 8 | import { toast } from 'sonner'; 9 | import { useTranslations } from 'next-intl'; 10 | 11 | interface PromptFormProps { 12 | initialPrompt: string; 13 | } 14 | 15 | export default function PromptForm({ initialPrompt }: PromptFormProps) { 16 | const [prompt, setPrompt] = useState(initialPrompt); 17 | const t = useTranslations('preferences.prompt'); 18 | 19 | const { execute: savePrompt, status: saveStatus } = useAction(updateAdditionalSystemPrompt, { 20 | onSuccess: () => { 21 | toast.success(t('saveSuccess')); 22 | }, 23 | onError: (error) => { 24 | const errorMessage = error.error.serverError || t('saveError'); 25 | toast.error(errorMessage); 26 | }, 27 | }); 28 | 29 | const handleSave = () => { 30 | savePrompt({ additionalSystemPrompt: prompt }); 31 | }; 32 | 33 | return ( 34 |
35 |
36 |