Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 14 additions & 23 deletions .agents/skills/deploy-open-harness/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,8 @@ Use this checklist when guiding the user.

### Required for GitHub-enabled repo flows

- `NEXT_PUBLIC_GITHUB_CLIENT_ID`
- `GITHUB_CLIENT_SECRET`
- `GITHUB_APP_ID`
- `GITHUB_APP_PRIVATE_KEY`
- `GITHUB_CONNECTOR`
- `NEXT_PUBLIC_GITHUB_APP_SLUG`
- `GITHUB_WEBHOOK_SECRET`

### Optional

Expand Down Expand Up @@ -127,31 +123,26 @@ Store the credentials as:
### GitHub App
Tell the user they do not need a separate GitHub OAuth app. Open Harness uses the GitHub App's user authorization flow.

Tell the user to create a GitHub App and set:
Tell the user to create a Vercel Connect GitHub connector and attach it to the project (run from `apps/web`, Vercel CLI >= 56):

- Homepage URL: `https://YOUR_DOMAIN`
- Callback URL: `https://YOUR_DOMAIN/api/github/app/callback`
- Setup URL: `https://YOUR_DOMAIN/api/github/app/callback`
- For local dev: homepage `http://localhost:3000`, callback/setup `http://localhost:3000/api/github/app/callback`
```bash
vercel connect create github --name <name> --triggers
vercel connect attach github/<name> -e production -e preview \
-e development --triggers --trigger-path /api/github/webhook
```

Also tell them to:
Also tell them to, in the connector's Advanced settings:

- enable "Request user authorization (OAuth) during installation"
- use the GitHub App Client ID and Client Secret for `NEXT_PUBLIC_GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`
- make the app public if they want org installs to work cleanly
- generate a webhook secret
- download/generate the private key
- grant permissions: Contents (read/write), Pull requests (read/write), Metadata (read-only), Checks (read-only)
- enable the `pull_request` trigger event type (plus `installation` / `installation_repositories` if offered)
- set the Setup URL to `https://YOUR_DOMAIN/api/github/app/callback` if editable

Store the values as:

- `NEXT_PUBLIC_GITHUB_CLIENT_ID`
- `GITHUB_CLIENT_SECRET`
- `GITHUB_APP_ID`
- `GITHUB_APP_PRIVATE_KEY`
- `NEXT_PUBLIC_GITHUB_APP_SLUG`
- `GITHUB_WEBHOOK_SECRET`
- `GITHUB_CONNECTOR` — the connector UID (e.g. `github/<name>`)
- `NEXT_PUBLIC_GITHUB_APP_SLUG` — the Connect-created app's github.com slug

Mention that `GITHUB_APP_PRIVATE_KEY` can be stored either as PEM contents with escaped newlines or as a base64-encoded PEM.
Mention that Vercel creates and manages the GitHub App, so there is no client secret, private key, or webhook secret to store, and that local dev needs `vercel env pull` for the `VERCEL_OIDC_TOKEN` the Connect SDK authenticates with.

### Redis / KV
Explain that Redis is optional. It improves resumable streams, stop signaling, and caching, but it is not required for the first deploy.
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ This file provides guidance for AI coding agents working in this repository.

## Authentication

Authentication uses [Better Auth](https://www.better-auth.com/) with Vercel OAuth (sign-in) and GitHub OAuth (repo access). Config lives in `apps/web/lib/auth/config.ts`. Sessions are managed by better-auth's built-in session system — there is no manual JWE/encryption layer.
Authentication uses [Better Auth](https://www.better-auth.com/) with Vercel OAuth (sign-in) and GitHub linked through a [Vercel Connect](https://vercel.com/docs/connect) GitHub connector (repo access, via the `genericOAuth` plugin). Config lives in `apps/web/lib/auth/config.ts` (GitHub provider in `apps/web/lib/auth/github-provider.ts`). Sessions are managed by better-auth's built-in session system — there is no manual JWE/encryption layer.

Key env vars: `BETTER_AUTH_SECRET` (session signing), `NEXT_PUBLIC_VERCEL_APP_CLIENT_ID` + `VERCEL_APP_CLIENT_SECRET` (Vercel OAuth), plus GitHub App credentials for repo access. See `apps/web/.env.example` for the full list.
GitHub tokens are exchanged at runtime via `@vercel/connect` (`apps/web/lib/github/token.ts` for user tokens, `apps/web/lib/github/installation-tokens.ts` for installation tokens) — there are no GitHub App credentials in the environment. Key env vars: `BETTER_AUTH_SECRET` (session signing), `NEXT_PUBLIC_VERCEL_APP_CLIENT_ID` + `VERCEL_APP_CLIENT_SECRET` (Vercel OAuth), `GITHUB_CONNECTOR` (Connect connector UID). See `apps/web/.env.example` for the full list.

## Database & Migrations

Expand Down
49 changes: 17 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Open Agents

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?project-name=open-agents&repository-name=open-agents&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fopen-agents&demo-title=Open+Agents&demo-description=Open-source+reference+app+for+building+and+running+background+coding+agents+on+Vercel.&demo-url=https%3A%2F%2Fopen-agents.dev%2F&env=POSTGRES_URL%2CBETTER_AUTH_SECRET%2CNEXT_PUBLIC_VERCEL_APP_CLIENT_ID%2CVERCEL_APP_CLIENT_SECRET%2CNEXT_PUBLIC_GITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CGITHUB_APP_ID%2CGITHUB_APP_PRIVATE_KEY%2CNEXT_PUBLIC_GITHUB_APP_SLUG%2CGITHUB_WEBHOOK_SECRET&envDescription=Neon+can+provide+POSTGRES_URL+automatically.+Generate+BETTER_AUTH_SECRET+yourself%2C+then+add+your+Vercel+OAuth+and+GitHub+App+credentials+for+a+full+deployment.&products=%255B%257B%2522type%2522%253A%2522integration%2522%252C%2522protocol%2522%253A%2522storage%2522%252C%2522productSlug%2522%253A%2522neon%2522%252C%2522integrationSlug%2522%253A%2522neon%2522%257D%252C%257B%2522type%2522%253A%2522integration%2522%252C%2522protocol%2522%253A%2522storage%2522%252C%2522productSlug%2522%253A%2522upstash-kv%2522%252C%2522integrationSlug%2522%253A%2522upstash%2522%257D%255D&skippable-integrations=1)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?project-name=open-agents&repository-name=open-agents&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fopen-agents&demo-title=Open+Agents&demo-description=Open-source+reference+app+for+building+and+running+background+coding+agents+on+Vercel.&demo-url=https%3A%2F%2Fopen-agents.dev%2F&env=POSTGRES_URL%2CBETTER_AUTH_SECRET%2CNEXT_PUBLIC_VERCEL_APP_CLIENT_ID%2CVERCEL_APP_CLIENT_SECRET%2CGITHUB_CONNECTOR%2CNEXT_PUBLIC_GITHUB_APP_SLUG&envDescription=Neon+can+provide+POSTGRES_URL+automatically.+Generate+BETTER_AUTH_SECRET+yourself%2C+then+add+your+Vercel+OAuth+credentials+and+Vercel+Connect+GitHub+connector+for+a+full+deployment.&products=%255B%257B%2522type%2522%253A%2522integration%2522%252C%2522protocol%2522%253A%2522storage%2522%252C%2522productSlug%2522%253A%2522neon%2522%252C%2522integrationSlug%2522%253A%2522neon%2522%257D%252C%257B%2522type%2522%253A%2522integration%2522%252C%2522protocol%2522%253A%2522storage%2522%252C%2522productSlug%2522%253A%2522upstash-kv%2522%252C%2522integrationSlug%2522%253A%2522upstash%2522%257D%255D&skippable-integrations=1)

Open Agents is an open-source reference app for building and running background coding agents on Vercel. It includes the web UI, the agent runtime, sandbox orchestration, and the GitHub integration needed to go from prompt to code changes without keeping your laptop involved.

Expand Down Expand Up @@ -69,13 +69,11 @@ VERCEL_APP_CLIENT_SECRET=

### Required for GitHub repo access, pushes, and PRs

GitHub access runs through a [Vercel Connect](https://vercel.com/docs/connect) GitHub connector — Vercel creates and manages the GitHub App, so there is no client secret, private key, or webhook secret to store.

```env
NEXT_PUBLIC_GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
NEXT_PUBLIC_GITHUB_APP_SLUG=
GITHUB_WEBHOOK_SECRET=
GITHUB_CONNECTOR= # connector UID, e.g. github/open-agents
NEXT_PUBLIC_GITHUB_APP_SLUG= # the Connect-created app's github.com slug
```

### Optional
Expand Down Expand Up @@ -127,17 +125,17 @@ ELEVENLABS_API_KEY=
VERCEL_APP_CLIENT_SECRET=
```

8. If you want the full GitHub-enabled coding-agent flow, create a GitHub App using:
8. If you want the full GitHub-enabled coding-agent flow, create a Vercel Connect GitHub connector and attach it to your project (run from `apps/web`):

- Homepage URL: `https://YOUR_DOMAIN`
- Callback URL: `https://YOUR_DOMAIN/api/auth/callback/github`
- Setup URL: `https://YOUR_DOMAIN/api/github/app/callback`
```bash
vercel connect create github --name open-agents --triggers
vercel connect attach github/open-agents -e production -e preview \
-e development --triggers --trigger-path /api/github/webhook
```

In the GitHub App settings:
- use the GitHub App's Client ID and Client Secret for `NEXT_PUBLIC_GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`
- make the app public if you want org installs to work cleanly
In the connector's Advanced settings, grant Contents (read/write), Pull requests (read/write), Metadata (read-only), and Checks (read-only), enable the `pull_request` trigger event type (plus `installation` / `installation_repositories` if offered), and set the Setup URL to `https://YOUR_DOMAIN/api/github/app/callback` if editable.

9. Add the GitHub App env vars and redeploy.
9. Set `GITHUB_CONNECTOR` to the connector UID and `NEXT_PUBLIC_GITHUB_APP_SLUG` to the created app's github.com slug, then redeploy.
10. Optionally add Redis/KV, `OPEN_AGENTS_RESOURCE_PROFILE=hobby` for Hobby-compatible resource defaults, the canonical production URL vars, and your own `VERCEL_SANDBOX_BASE_SNAPSHOT_ID` if you want fresh sandboxes to start from a preconfigured image.

## Local setup
Expand Down Expand Up @@ -193,27 +191,14 @@ VERCEL_APP_CLIENT_SECRET=...

You do not need a separate GitHub OAuth app. Open Agents uses the GitHub App's OAuth credentials as a Better Auth social provider, plus the App's installation tokens for repo access.

Create a GitHub App for installation-based repo access and configure:

- Homepage URL: `https://YOUR_DOMAIN`
- Callback URL: `https://YOUR_DOMAIN/api/auth/callback/github`
- Setup URL: `https://YOUR_DOMAIN/api/github/app/callback`
- make the app public if you want org installs to work cleanly

For local development, use `http://localhost:3000` as the homepage URL, `http://localhost:3000/api/auth/callback/github` as the callback URL, and `http://localhost:3000/api/github/app/callback` as the setup URL.

Then set:
Repo access is brokered by a Vercel Connect GitHub connector (see the deploy steps above for creation and attachment). Then set:

```env
NEXT_PUBLIC_GITHUB_CLIENT_ID=... # GitHub App Client ID
GITHUB_CLIENT_SECRET=... # GitHub App Client Secret
GITHUB_APP_ID=...
GITHUB_APP_PRIVATE_KEY=...
NEXT_PUBLIC_GITHUB_APP_SLUG=...
GITHUB_WEBHOOK_SECRET=...
GITHUB_CONNECTOR=... # connector UID, e.g. github/open-agents
NEXT_PUBLIC_GITHUB_APP_SLUG=... # the Connect-created app's github.com slug
```

`GITHUB_APP_PRIVATE_KEY` can be stored as the PEM contents with escaped newlines or as a base64-encoded PEM.
For local development, run `vercel env pull` after linking the project — the SDK authenticates Connect token requests with the pulled `VERCEL_OIDC_TOKEN` (expires after ~12 hours; re-pull when it does).

## Useful commands

Expand Down
29 changes: 16 additions & 13 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ BETTER_AUTH_SECRET=
NEXT_PUBLIC_VERCEL_APP_CLIENT_ID=
VERCEL_APP_CLIENT_SECRET=

# GitHub App (required for repo access, pushes, and PRs)
# Callback URL: {YOUR_ORIGIN}/api/auth/callback/github (better-auth)
# Setup URL: {YOUR_ORIGIN}/api/github/app/callback (installation sync)
# In the GitHub App settings:
# 1. Use the GitHub App Client ID and Client Secret below
# 2. Make the app public if you want org installs to work cleanly
# 3. Store the private key either as PEM contents with escaped newlines
# or as a base64-encoded PEM
NEXT_PUBLIC_GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
# GitHub via Vercel Connect (required for repo access, pushes, and PRs)
# One-time setup (run from apps/web, needs Vercel CLI >= 56):
# 1. vercel connect create github --name <name> --triggers
# (Vercel creates and manages the GitHub App — no client secret,
# private key, or webhook secret to store)
# 2. In the connector's Advanced settings: permissions Contents R/W,
# Pull requests R/W, Metadata RO, Checks RO; trigger event types
# pull_request (+ installation, installation_repositories if offered);
# Setup URL {YOUR_ORIGIN}/api/github/app/callback if editable
# 3. vercel connect attach github/<name> -e production -e preview \
# -e development --triggers --trigger-path /api/github/webhook
# 4. Set GITHUB_CONNECTOR to the connector UID and
# NEXT_PUBLIC_GITHUB_APP_SLUG to the created app's github.com slug
# Local dev: `vercel env pull` provides the VERCEL_OIDC_TOKEN the SDK uses
# to authenticate token requests (expires ~12h; re-pull when it does).
GITHUB_CONNECTOR=
NEXT_PUBLIC_GITHUB_APP_SLUG=
GITHUB_WEBHOOK_SECRET=

# Optional integrations
ELEVENLABS_API_KEY=
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/github/app/install/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ mock.module("@/lib/github/token", () => ({
mock.module("@/lib/github/users", () => ({
hasGitHubAccount: async () => hasLinkedGitHub,
getGitHubUsername: async () => (hasLinkedGitHub ? "testuser" : null),
getGitHubAccountId: async () => (hasLinkedGitHub ? "12345" : null),
getGitHubUserProfile: async () =>
hasLinkedGitHub ? { username: "testuser", externalUserId: "12345" } : null,
}));

mock.module("@/lib/db/installations", () => ({
Expand Down
9 changes: 6 additions & 3 deletions apps/web/app/api/github/app/install/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getInstallationsByUserId } from "@/lib/db/installations";
import { syncUserInstallations } from "@/lib/github/sync";
import { getUserGitHubToken } from "@/lib/github/token";
import {
getGitHubAccountId,
getGitHubUserProfile,
getGitHubUsername,
hasGitHubAccount,
} from "@/lib/github/users";
Expand Down Expand Up @@ -82,10 +82,13 @@ export async function GET(req: NextRequest): Promise<Response> {
return NextResponse.redirect(connectUrl);
}

// reconnect mode — skip account picker, target the user's personal account
// reconnect mode — skip account picker, target the user's personal account.
// GitHub's target_id must be the numeric user id; `accounts.accountId` is
// the Connect OIDC subject, so resolve the id live from the GitHub API.
const reconnect = req.nextUrl.searchParams.get("reconnect");
if (reconnect === "1") {
const accountId = await getGitHubAccountId(session.user.id);
const accountId = (await getGitHubUserProfile(session.user.id))
?.externalUserId;
if (accountId) {
const installUrl = new URL(
`https://github.com/apps/${appSlug}/installations/new/permissions`,
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/api/github/orgs/install-status/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { getInstallationsByUserId } from "@/lib/db/installations";
import { isGitHubAppConfigured } from "@/lib/github/app";
import { isGitHubConnectorConfigured } from "@/lib/github/connector";
import { getInstallationManageUrl } from "@/lib/github/urls";
import { syncUserInstallations } from "@/lib/github/sync";
import { getUserGitHubToken } from "@/lib/github/token";
Expand Down Expand Up @@ -52,9 +52,9 @@ export async function GET() {
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
}

if (!isGitHubAppConfigured()) {
if (!isGitHubConnectorConfigured()) {
return NextResponse.json(
{ error: "GitHub App not configured" },
{ error: "GitHub connector not configured" },
{ status: 500 },
);
}
Expand Down
36 changes: 6 additions & 30 deletions apps/web/app/api/github/webhook/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { createHmac, timingSafeEqual } from "crypto";
import { and, eq, sql } from "drizzle-orm";
import { after } from "next/server";
import { z } from "zod";
Expand All @@ -11,6 +10,7 @@ import {
import { updateSession } from "@/lib/db/sessions";
import { db } from "@/lib/db/client";
import { sessions } from "@/lib/db/schema";
import { verifyConnectWebhook } from "@/lib/github/webhook-verification";
import { archiveSession } from "@/lib/sandbox/archive-session";

const installationWebhookSchema = z.object({
Expand Down Expand Up @@ -46,22 +46,6 @@ function normalizeAccountType(type: string): "User" | "Organization" {
return type === "Organization" ? "Organization" : "User";
}

function verifySignature(
payload: string,
signatureHeader: string,
secret: string,
): boolean {
const digest = createHmac("sha256", secret).update(payload).digest("hex");
const expected = Buffer.from(`sha256=${digest}`);
const provided = Buffer.from(signatureHeader);

if (expected.length !== provided.length) {
return false;
}

return timingSafeEqual(expected, provided);
}

async function handlePullRequestWebhook(
payload: z.infer<typeof pullRequestWebhookSchema>,
): Promise<Response> {
Expand Down Expand Up @@ -149,25 +133,17 @@ async function handlePullRequestWebhook(
}

export async function POST(req: Request): Promise<Response> {
const webhookSecret = process.env.GITHUB_WEBHOOK_SECRET;
if (!webhookSecret) {
return Response.json(
{ error: "GITHUB_WEBHOOK_SECRET is not configured" },
{ status: 500 },
);
}

// Events arrive via Vercel Connect trigger forwarding, authenticated with a
// Vercel OIDC token instead of GitHub's HMAC signature.
const event = req.headers.get("x-github-event");
const signature = req.headers.get("x-hub-signature-256");

if (!event || !signature) {
if (!event) {
return Response.json({ error: "Missing webhook headers" }, { status: 400 });
}

const payloadText = await req.text();
if (!verifySignature(payloadText, signature, webhookSecret)) {
if (!(await verifyConnectWebhook(req, payloadText))) {
return Response.json(
{ error: "Invalid webhook signature" },
{ error: "Invalid webhook credentials" },
{ status: 401 },
);
}
Expand Down
3 changes: 1 addition & 2 deletions apps/web/app/api/sandbox/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,14 @@ mock.module("@/lib/github/access", () => ({
getRepoAccessErrorMessage: () => "Access denied",
}));

mock.module("@/lib/github/app", () => ({
mock.module("@/lib/github/installation-tokens", () => ({
mintInstallationToken: async () => ({
token: "installation-token-mock",
expiresAt: null,
installationId: 999,
repositoryIds: [123],
permissions: { contents: "read" },
}),
revokeInstallationToken: async () => {},
}));

mock.module("@/lib/vercel/token", () => ({
Expand Down
Loading
Loading