A reverse proxy for gandalf.lakera.ai that logs every student prompt and response. Students interact with Gandalf normally, the proxy sits invisibly in between, captures all API traffic, and stores it in a local SQLite database.
It works by injecting a small script into every Gandalf page that reroutes the game's API calls through the proxy, where each request is recorded before being forwarded to Lakera unchanged.
Configuration is through environment variables, all optional.
PORT- port to listen on. Defaults to 3000.PUBLIC_URL- external base URL the browser uses. Defaults tohttp://localhost:PORT. Set this if the proxy runs behind a domain or tunnel.SESSION_SECRET- key for signing session cookies. If unset, a random secret is generated and persisted todata/.session-secretso sessions survive restarts.
Users are defined in data/roster.json, keyed by ID, each with a password and a role of student or admin.
{
"admin": { "password": "admin123", "role": "admin" },
"STU001": { "password": "STU001", "role": "student" }
}If the file is absent, a small built-in roster is used. Passwords are currently stored in plain text.
Requires Node.js. Install and start:
npm install
npm start
The proxy is then available at PUBLIC_URL. Students sign in with their roster ID and password and are proxied to Gandalf. The admin account is redirected to /admin.
For development with auto-reload:
npm run dev
The database is created automatically at data/prompts.db on first run. Clear it from the admin panel before a real session.
npm start stops when the terminal closes. To keep the proxy running in the background and restart it automatically, use a process manager such as PM2.
npm install -g pm2
pm2 start server.js --name gandalf-proxy
Useful commands:
pm2 logs gandalf-proxy view output
pm2 restart gandalf-proxy restart
pm2 stop gandalf-proxy stop
To relaunch on machine reboot, run pm2 startup once and follow the printed instruction, then pm2 save. The server closes its database cleanly on stop, so PM2 restarts do not corrupt or leave stray database files.
- Node.js with Express
- express-session for authentication
- http-proxy-middleware for both proxies
- better-sqlite3 for storage, in WAL mode
- Vanilla HTML, CSS, and JavaScript for the admin panel, served statically. No build step.
server.js Entry point: config, database, routes, both proxies
package.json
data/
roster.json User accounts (IDs, passwords, roles)
prompts.db SQLite database (generated)
.session-secret Session key (generated, git-ignored)
public/
admin/
admin.css Admin panel styles
admin.js Admin panel client logic
server.js renders the login and admin HTML and holds all server logic. The admin panel's CSS and JavaScript are served as static files from public/.
The database has two tables. entries holds the structured columns queried by the panel (student, timestamp, request type, level name and number, prompt, guess, answer, LLM, pass state). entries_raw holds the full raw request and response payloads, keyed by entry ID and read only by the raw export. Splitting the payloads out keeps the queried table small. Startup migrations bring older databases up to the current schema.
[Browser]
|
|-- Loads a Gandalf page through the proxy
| Proxy injects the intercept script before serving the page
|
|-- Student sends a prompt or password guess
| Script reroutes the API call to /gandalf-api on the proxy
|
[Proxy]
|-- Buffers the request body
|-- Forwards it to gandalf-api.lakera.ai unchanged
|-- Reads the response
| send_message and guess_password: save an entry (structured + raw)
| get_defender and trial_assignment: cache level number and LLM
|-- Returns the response to the browser unchanged
|
[Browser] receives the normal game response
Level names map to numbers reported by Gandalf. Main game levels are 1 and up. Side challenges (Adventures) report 0 and share the same endpoints, so they are logged but excluded from the leaderboard and grades.
The admin panel at /admin has two tabs. The submission log is a filterable, paginated feed of all entries with live refresh, plus total, pass, fail, and student counts. The leaderboard ranks students by highest main game level reached, with prompts sent and last active time. Exports:
- JSON - structured entries, respects filters
- CSV - same as JSON in tabular form
- Raw - structured entries plus full raw payloads
- Grades - one row per student with their highest main game level, for grading
- Passwords in
roster.jsonare plain text and should be hashed. - The session cookie uses
secure: falsefor local HTTP. Set it totruewhen serving over HTTPS. - All student traffic exits through the proxy's single IP, which may hit Lakera's rate limits at scale.
- Agent Breaker mode is not captured. Its attack submissions never reach the proxy, so nothing is logged. Supporting it would require intercepting its separate endpoints.