-
Notifications
You must be signed in to change notification settings - Fork 334
Add a demo interface for Hackbot #6115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
suhaibmujahid
wants to merge
1
commit into
mozilla:master
Choose a base branch
from
suhaibmujahid:worktree-hackbot-ui
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,420
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| node_modules | ||
| .next | ||
| .git | ||
| .env | ||
| .env.local | ||
| *.db | ||
| *.db-shm | ||
| *.db-wal | ||
| npm-debug.log | ||
| README.md |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # Base URL of the hackbot-api service (no trailing slash). | ||
| HACKBOT_API_URL=http://localhost:8080 | ||
|
|
||
| # API key sent as the X-API-Key header. Must match hackbot-api's | ||
| # EXTERNAL_API_KEY. Stays server-side — it is never shipped to the browser. | ||
| HACKBOT_API_KEY= | ||
|
|
||
| # Public base URL of THIS web app, used by better-auth for callbacks. | ||
| BETTER_AUTH_URL=http://localhost:3000 | ||
|
|
||
| # Random 32+ char secret used to sign sessions. Generate with `openssl rand -base64 32`. | ||
| BETTER_AUTH_SECRET= | ||
|
|
||
| # Google OAuth client (console.cloud.google.com → APIs & Services → Credentials). | ||
| # Authorized redirect URI must be: <BETTER_AUTH_URL>/api/auth/callback/google | ||
| GOOGLE_CLIENT_ID= | ||
| GOOGLE_CLIENT_SECRET= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # The repo-root .gitignore ignores `lib/` (Python build artifacts). Re-include | ||
| # this Next.js app's lib/ source directory. | ||
| !/lib/ | ||
| /node_modules | ||
| /.next | ||
| /out | ||
| /build | ||
| .env | ||
| .env.local | ||
| *.tsbuildinfo | ||
| next-env.d.ts.bak | ||
| .DS_Store |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # syntax=docker/dockerfile:1 | ||
|
|
||
| FROM node:22-slim AS deps | ||
| WORKDIR /app | ||
| COPY package.json package-lock.json ./ | ||
| RUN npm ci --no-audit --no-fund | ||
|
|
||
| FROM node:22-slim AS builder | ||
| WORKDIR /app | ||
| COPY --from=deps /app/node_modules ./node_modules | ||
| COPY . . | ||
| ENV NEXT_TELEMETRY_DISABLED=1 | ||
| RUN npm run build | ||
|
|
||
| FROM node:22-slim AS runner | ||
| WORKDIR /app | ||
| ENV NODE_ENV=production | ||
| ENV NEXT_TELEMETRY_DISABLED=1 | ||
| ENV PORT=3000 | ||
| # Cloud Run injects its own PORT (8080) which the standalone server honors. | ||
| # Bind to all interfaces — the standalone server defaults HOSTNAME to | ||
| # "localhost", which would fail Cloud Run's health check. | ||
| ENV HOSTNAME=0.0.0.0 | ||
|
|
||
| RUN useradd --create-home --shell /bin/bash app | ||
|
|
||
| # Next.js standalone output bundles only the files needed to run the server. | ||
| COPY --from=builder /app/public ./public | ||
| COPY --from=builder /app/.next/standalone ./ | ||
| COPY --from=builder /app/.next/static ./.next/static | ||
|
|
||
| USER app | ||
| EXPOSE 3000 | ||
| CMD ["node", "server.js"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # Hackbot Launchpad | ||
|
|
||
| A small Next.js web UI for **demonstrating** the [`hackbot-api`](../hackbot-api). | ||
| It lets you: | ||
|
|
||
| - **Trigger** the `bug-fix` agent by entering a Bugzilla bug ID (plus optional | ||
| model / max-turns / effort). | ||
| - **Observe state** — the dashboard and the run detail page poll the API and | ||
| show each run's status live (`pending → running → succeeded / failed / timed_out`). | ||
| - **Read the result** on completion — the agent's summary findings (rendered as | ||
| an "Agent log" pane when a free-text log/output field is present) and the | ||
| artifacts written to the results bucket, each a **download link** (the browser | ||
| is redirected to a short-lived signed GCS URL). | ||
|
|
||
| > This is a demo surface, not a system of record. It has no list-runs endpoint | ||
| > upstream, so the browser remembers the runs you triggered in `localStorage`. | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| Browser ──> Next.js route handlers (/api/*) ──> hackbot-api (X-API-Key) | ||
| └─ better-auth (Google, @mozilla.com only) | ||
| ``` | ||
|
|
||
| The `hackbot-api` key never reaches the browser: every call goes through a | ||
| server-side route handler in `app/api/*` that injects the `X-API-Key` header | ||
| (see `lib/hackbot.ts`). Those handlers also re-validate the session. | ||
|
|
||
| ### Authentication | ||
|
|
||
| Sign-in is **Google OAuth via [better-auth](https://better-auth.com)**, limited | ||
| to `@mozilla.com` accounts: | ||
|
|
||
| - **No database — fully stateless.** `lib/auth.ts` configures better-auth with | ||
| no `database`, so the session lives entirely in a signed + encrypted (JWE) | ||
| cookie and the server never queries any store to validate it. The only shared | ||
| state is `BETTER_AUTH_SECRET`. See better-auth's | ||
| [stateless session docs](https://better-auth.com/docs/concepts/session-management#basic-stateless-setup). | ||
| - The `@mozilla.com` restriction is enforced in two mode-independent layers: | ||
| the Google provider's `mapProfileToUser` rejects non-Mozilla identities during | ||
| the OAuth callback (before a session is issued), and `getAuthedEmail()` | ||
| (`lib/session.ts`) re-checks the domain on every proxy request. `hd: "mozilla.com"` | ||
| is also passed to Google as a UI hint. | ||
| - `middleware.ts` redirects unauthenticated visitors to `/login` (and returns | ||
| `401` JSON for `/api/*`). | ||
|
|
||
| ## Endpoints used (hackbot-api) | ||
|
|
||
| | UI action | hackbot-api call | | ||
| | ----------------- | --------------------------------------- | | ||
| | Trigger a run | `POST /agents/bug-fix/runs` | | ||
| | Poll run state | `GET /runs/{run_id}` | | ||
| | Download artifact | `GET /runs/{run_id}/artifacts/{path}` † | | ||
| | (available) | `GET /agents` | | ||
|
|
||
| ## Local development | ||
|
|
||
| 1. Install dependencies: | ||
|
|
||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| 2. Configure the environment: | ||
|
|
||
| ```bash | ||
| cp .env.example .env.local | ||
| # fill in HACKBOT_API_URL, HACKBOT_API_KEY, | ||
| # BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | ||
| ``` | ||
|
|
||
| For the Google OAuth client, add this authorized redirect URI: | ||
| `http://localhost:3000/api/auth/callback/google` | ||
|
|
||
| (No database setup or migration step — auth is stateless.) | ||
|
|
||
| 3. Run it: | ||
|
|
||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| Open http://localhost:3000 — you'll be redirected to `/login`. | ||
|
|
||
| ## Production build / container | ||
|
|
||
| ```bash | ||
| docker build -t hackbot-ui -f services/hackbot-ui/Dockerfile services/hackbot-ui | ||
| docker run -p 3000:3000 --env-file services/hackbot-ui/.env.local hackbot-ui | ||
| ``` | ||
|
|
||
| The image uses Next.js `output: "standalone"`. | ||
|
|
||
| ### Cloud Run | ||
|
|
||
| Stateless auth makes this Cloud Run-friendly out of the box: sessions are | ||
| self-contained cookies, so they survive scale-to-zero and work across any number | ||
| of instances — **as long as every instance shares the same `BETTER_AUTH_SECRET`** | ||
| (set it as a secret/env var on the service). No database to provision. | ||
|
|
||
| Use `./deploy.sh` (build → push to Artifact Registry → `gcloud run deploy`). It | ||
| keeps secrets in Secret Manager and reads everything else from env vars. The | ||
| service runs as a **dedicated least-privilege service account** | ||
| (`hackbot-ui-run@<project>`) that the script creates and grants | ||
| `secretmanager.secretAccessor` on just the three secrets it reads — no other GCP | ||
| permissions (it reaches hackbot-api over HTTPS with the API key, not via IAM). | ||
|
|
||
| The hackbot-api key reuses the existing shared **`external-api-key`** secret | ||
| (override with `API_KEY_SECRET=...`), so only two UI-specific secrets need | ||
| creating: `hackbot-ui-auth-secret` and `hackbot-ui-google-secret`. | ||
|
|
||
| ```bash | ||
| # one-time: enable APIs + create the two UI secrets (see the header of deploy.sh). | ||
| # The deployer needs run.admin + iam.serviceAccountUser on the new SA. | ||
|
|
||
| # first deploy (no BETTER_AUTH_URL yet — Cloud Run assigns the URL): | ||
| PROJECT=my-proj REGION=us-central1 \ | ||
| HACKBOT_API_URL=https://hackbot-api-xxxx.run.app \ | ||
| GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com \ | ||
| ./deploy.sh | ||
|
|
||
| # then: add <printed-url>/api/auth/callback/google to the Google OAuth client, | ||
| # and re-run with BETTER_AUTH_URL=<printed-url> to finalize. | ||
| ``` | ||
|
|
||
| `HACKBOT_API_URL` is the hackbot-api's public Cloud Run URL; `HACKBOT_API_KEY` | ||
| must match its `X-API-Key`. | ||
|
|
||
| ## Environment variables | ||
|
|
||
| | Variable | Purpose | | ||
| | ---------------------- | -------------------------------------------------- | | ||
| | `HACKBOT_API_URL` | Base URL of hackbot-api (no trailing slash) | | ||
| | `HACKBOT_API_KEY` | Value for the `X-API-Key` header (server-side) | | ||
| | `BETTER_AUTH_URL` | Public base URL of this app | | ||
| | `BETTER_AUTH_SECRET` | Session signing secret (`openssl rand -base64 32`) | | ||
| | `GOOGLE_CLIENT_ID` | Google OAuth client ID | | ||
| | `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { HackbotError, listAgents } from "@/lib/hackbot"; | ||
| import { getAuthedEmail } from "@/lib/session"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| export async function GET() { | ||
| if (!(await getAuthedEmail())) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
| try { | ||
| const agents = await listAgents(); | ||
| return NextResponse.json(agents); | ||
| } catch (err) { | ||
| const status = err instanceof HackbotError ? err.status : 500; | ||
| return NextResponse.json({ error: (err as Error).message }, { status }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { toNextJsHandler } from "better-auth/next-js"; | ||
|
|
||
| import { auth } from "@/lib/auth"; | ||
|
|
||
| export const { GET, POST } = toNextJsHandler(auth); |
30 changes: 30 additions & 0 deletions
30
services/hackbot-ui/app/api/runs/[runId]/artifacts/[...path]/route.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { getArtifactDownloadUrl, HackbotError } from "@/lib/hackbot"; | ||
| import { getAuthedEmail } from "@/lib/session"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| // GET /api/runs/:runId/artifacts/*path | ||
| // Resolves a signed download URL from hackbot-api and redirects the browser | ||
| // straight to GCS, so artifact bytes never stream through this server and the | ||
| // X-API-Key stays server-side. | ||
| export async function GET( | ||
| _req: Request, | ||
| { params }: { params: Promise<{ runId: string; path: string[] }> } | ||
| ) { | ||
| if (!(await getAuthedEmail())) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { runId, path } = await params; | ||
| const artifactName = path.join("/"); // catch-all segments are pre-decoded | ||
|
|
||
| try { | ||
| const { url } = await getArtifactDownloadUrl(runId, artifactName); | ||
| return NextResponse.redirect(url, 302); | ||
| } catch (err) { | ||
|
suhaibmujahid marked this conversation as resolved.
|
||
| const status = err instanceof HackbotError ? err.status : 500; | ||
| return NextResponse.json({ error: (err as Error).message }, { status }); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { NextResponse } from "next/server"; | ||
|
|
||
| import { getRun, HackbotError } from "@/lib/hackbot"; | ||
| import { getAuthedEmail } from "@/lib/session"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| // GET /api/runs/:runId — proxy the full run document (state, summary, artifacts). | ||
| export async function GET( | ||
| _req: Request, | ||
| { params }: { params: Promise<{ runId: string }> } | ||
| ) { | ||
| if (!(await getAuthedEmail())) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { runId } = await params; | ||
| try { | ||
| const run = await getRun(runId); | ||
| return NextResponse.json(run); | ||
| } catch (err) { | ||
| const status = err instanceof HackbotError ? err.status : 500; | ||
| return NextResponse.json({ error: (err as Error).message }, { status }); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| import { createRun, HackbotError } from "@/lib/hackbot"; | ||
| import { getAuthedEmail } from "@/lib/session"; | ||
|
|
||
| export const dynamic = "force-dynamic"; | ||
|
|
||
| // POST /api/runs { agent: string, inputs: object } | ||
| // Triggers a new agent run via hackbot-api. | ||
| export async function POST(req: NextRequest) { | ||
| if (!(await getAuthedEmail())) { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| let body: unknown; | ||
| try { | ||
| body = await req.json(); | ||
| } catch { | ||
| return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 }); | ||
| } | ||
|
|
||
| const { agent, inputs } = (body ?? {}) as { | ||
| agent?: string; | ||
| inputs?: Record<string, unknown>; | ||
| }; | ||
|
|
||
| if (!agent || typeof agent !== "string") { | ||
| return NextResponse.json( | ||
| { error: "Missing 'agent' field" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const run = await createRun(agent, inputs ?? {}); | ||
| return NextResponse.json(run, { status: 201 }); | ||
|
suhaibmujahid marked this conversation as resolved.
|
||
| } catch (err) { | ||
| const status = err instanceof HackbotError ? err.status : 500; | ||
| return NextResponse.json({ error: (err as Error).message }, { status }); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.