CS 690 Master's Project · one-on-one with Mario Lim · November 7, 2026

AutoSpark: what I built this semester

Jon Rahoi. AutoSpark finds slow PySpark jobs, suggests configuration fixes, proves them in a sandbox, and records which named person approved each change. The sponsor is Openprise. The team is Max Lubarsky, Lisa Jurca and me.

Summary

28pull requests, all merged
52issues opened, 20 closed
17,349lines added, 689 removed
314file changes across my PRs
37%of what I added is tests

Of the 66 pull requests merged into the repo, 28 are mine. The repo has 270 commits that are not merges, and 101 of them are mine. My first merged PR was October 14; the first commit in the repo is September 22.

Pull requests merged per day

How I worked. Every feature started as an issue, then a written spec or plan, then tests before code, then a pull request, and for the larger changes a review by an independent reviewer who had not seen my reasoning. I built with Claude Code as a pair programmer: 103 of the 105 commits inside my merged PRs list Claude as a co-author. The decisions, the reviews I acted on, and the production changes (database migrations, Terraform, the sign-in cutover) were mine to approve and run.

Modules I worked on

Where my work sits in the repo, and what I did there. Teammates built the other detectors and several tabs; this table covers only my part.

ModuleWhat it isWhat I didPRs
diagnostics/Detectors that turn History Server data into structured findings.Created the package, its Diagnosis type, the first three detectors (skew, shuffle spill, underutilization) and the repo's first pytest setup, from a written spec and plan. Teammates added the later detectors; it is 2,600 lines today.#23 #24 #25
db/ and store/Postgres schema, audit trail, and the data layer the app uses.Wrote the initial schema, the rule that approvals are audited by a trigger rather than by app code, and an app login that cannot delete rows or change the schema. Added migrations 010 (people by email) and 012 (API tokens). Ran migrations 010 to 012 on the production database.#32 #34 #126 #176
dashboard/ and service/The web app (FastAPI + HTMX) and the plain functions behind it.Built the Findings tab with its database sync and the Jobs tab (list, run, watch, cancel). Moved the Findings, Jobs, suggestions and Trends logic into service/ so a second front end can reuse it.#34 #99 #118 #179
mcp_server/An MCP endpoint so Claude Code and similar tools can use Autospark.Built it end to end: nine tools (six read, run and cancel, upload), personal access tokens with scopes, rate limiting, and the allowlist check. Tools that act sit behind opt-in scopes.#119 #176 #178 #180
infra/, proxy/ and .github/Terraform for AWS, the Caddy and nginx front door, and the deploy workflow.Wrote the EMR experiment plan, the deploy queueing, the nginx proxy, Caddy HTTPS, Google sign-in through oauth2-proxy, named IAM users with MFA, and the team S3 policy. Removed the app host's access to the master database secret.#29 #33 #35 #36 #90 #126 #217
present/A demo deck and Spark visualization, served at /present/ (unlinked).Built it for the sponsor demo.#87
docs/ and READMEsSpecs, plans, runbooks and guides.Wrote the diagnostics spec and plan, the handoff runbook, the MCP reference and the team quickstart, and kept the architecture notes current.#23 #24 #30 #31 #62 #187

Source code overview

The system as it runs today. People reach the app host two ways: a browser signs in with Google, and an MCP client sends a personal access token. Both land on the same dashboard process, which reads metrics from Spark History Servers and uses AWS for jobs, storage and the database.

Built by meShared or built by teammatesSolid arrows: requests. Dashed: admin access.
People App host: EC2 t4g.small, Docker Compose AWS, built with Terraform Dashboard: FastAPI + HTMX Browser team and sponsors Google sign-in, external MCP client Claude Code, with a token Admin laptop SSH, Terraform, migrations Caddy HTTPS, routes /mcp oauth2-proxy Google sign-in, verified email nginx :8081 trusts the email :8080 trusts nothing Findings, Jobs, Trends tabs HTML pages MCP gateway at /mcp tokens, scopes, rate limit service/ plain functions both front ends use store/ Postgres, S3 and EMR access diagnostics/ detectors: skew, spill, plans Spark History Servers prod and sandbox, one container each event logs come from S3 EMR Serverless prod (32 vCPU cap), sandbox (16) S3 job code, event logs (prod, sandbox) RDS Postgres findings, approvals, audit log IAM and Secrets Manager named users with MFA Terraform infra/: every AWS piece HTTPS who is this? sign in signed in token, no Google redirect REST run, cancel upload, read SQL via IAM SSH to the host, then to the database event logs
  1. Browser. Caddy terminates HTTPS and asks oauth2-proxy who the person is. Google confirms; the verified email goes to nginx's trusted listener and becomes the person's identity everywhere (approvals, audit rows, run tags).
  2. MCP client. Caddy sends only the exact path /mcp past the Google redirect, to a listener that trusts no identity header. The gateway checks the token on every call: shape, lookup, owner still on the sign-in list, rate limit, then the tool's scope.
  3. Run and prove. A job file goes to S3 and runs on EMR Serverless. Event logs return to S3, History Servers serve them over REST, detectors turn them into findings, and findings, suggestions, sandbox results and approvals live in Postgres.
  4. Safety rails. The app's database login cannot delete rows or change the schema. Approvals are audited by a trigger, not by app code. The sandbox cluster cannot read the production buckets.

Repo at a glance

9,600 lines of production Python, 16,300 lines of tests in 64 test files, 12 database migrations, 790 lines of Terraform, and 20 Markdown files.

Directories: diagnostics/ detectors, service/ plain functions, store/ data access, dashboard/ the web app, mcp_server/ the MCP endpoint, db/ migrations, infra/ Terraform and Caddy, proxy/ nginx, pathology_jobs/ test jobs, present/ the demo deck.

The older deck

The team's AutoSpark slide deck has component and data-flow slides from October 24. They predate Google sign-in, MCP and named IAM users, and still show a shared password. This diagram replaces them.

Issues I opened

52 issues, 20 closed. They fall into six groups, from the first scoping questions in week one to the MCP epic. Two MCP issues (#112 and #113) are implemented and merged but still open on GitHub.

Early questions and scoping 10 issues, 2 closed

  • #9 Can the History Server use a DB instead of flat event-log files?open
  • #10 What should we store in the AutoSpark DB (TimescaleDB)?open
  • #11 Should sandbox runs use a separate History Server from the curated demo jobs?closed
  • #12 How is input data for Spark jobs stored today, and how should it be?open
  • #13 Can we use a client's real data for sandbox experiments, or do we need a toy dataset?open
  • #14 Is the sandbox a separate Spark cluster, or does it share compute with the control plane?closed
  • #15 Running sandbox experiments: single run or multiple runs per candidate config?open
  • #16 How do we get Spark event logs into the DB: direct write, or a sync/cron job?open
  • #17 Add AWS EMR Serverless support for job executionopen
  • #18 Introduce a more realistic workload into the Spark jobsopen

Detectors and analysis 11 issues, 7 closed

  • #19 Build a diagnostics module to identify job pathologiesclosed
  • #41 Compute vCPU-hours per run (the billed cost)closed
  • #42 Executor analysis should use /allexecutorsclosed
  • #44 Detector: driver-side gaps (planning and optimizer time)open
  • #45 Detector: lost shuffle data and stage resubmitsopen
  • #46 Detector: too few tasks for the clusterclosed
  • #47 Detector: single-partition stageclosed
  • #48 Detector: over-provisioning and executor churnopen
  • #49 Plan-based detectors from /sql planDescriptionclosed
  • #50 Extend the skew detector: pair-count skew that AQE cannot seeclosed
  • #58 Detector: per-rule wrapper overheadopen

Sponsor pipeline and scale 6 issues, 0 closed

  • #37 Epic: what the Openprise example pipeline means for Autosparkopen
  • #38 Identify a pipeline by its EMR job-run name, not the Spark app nameopen
  • #39 Attribute time to rules: group Spark jobs by job groupopen
  • #40 Make the sync and the History Servers cope with multi-GB event logsopen
  • #43 Questions for the sponsors, from the example pipelineopen
  • #57 Validate detectors and sizing against a real sponsor event logopen

Suggestions and the sandbox loop 15 issues, 7 closed

  • #51 Suggestions need two kinds: config and adviceopen
  • #52 Suggestion engine and approval UI (step 2)open
  • #53 Sandbox run on sampled data with a cost guard (step 3)open
  • #54 Compare baseline and candidate per rule, with a verdict (step 3)open
  • #55 Record the applied fix and close the loop (step 4)open
  • #56 Pathology job suite on EMR: reproduce each pattern at small scaleopen
  • #63 Sandbox config: emit the EXP_* variables from print_post_apply.shopen
  • #64 Interim approver identity: restrict approvals to known usersclosed
  • #65 Sandbox in-flight check blocks re-testing forever and is racyclosed
  • #66 Sandbox follow-ups from the PR #60 reviewclosed
  • #74 Re-home suggestions in the per-application Findings drill-downclosed
  • #75 Findings tab follow-ups from the #73 reviewclosed
  • #76 Harden sync_exp_eventlog: deadline, error handling, restart recoveryopen
  • #79 Findings tab: each application starts collapsedclosed
  • #80 Tooltip from the default tab stays stuck over the Findings tabclosed

Sign-in and security 2 issues, 0 closed

  • #61 Add Google OAuth sign-in (replace the shared basic-auth password)open
  • #175 Dashboard has no Host-header check (DNS rebinding on the SSH-tunnel path)open

MCP server 8 issues, 4 closed

  • #109 Epic: an MCP server so people and agents can use Autospark from their own toolsopen
  • #110 Pull the dashboard's logic out of the HTML routesclosed
  • #111 Server skeleton and read-only toolsclosed
  • #112 Authentication and permissions (shipped in PR #176; issue still open)open
  • #113 Run and cancel prod jobs (shipped in PR #178; issue still open)open
  • #114 Upload a jobclosed
  • #115 Sandbox-test, approve and reject suggestions, with a human in the loop (on hold)open
  • #117 Trends data as plain functionsclosed

Pull requests I opened

All 28 are merged. Sizes are additions and deletions in the PR. Dates are merge dates, so the order is by PR number.

PRMergedAreaTitleLinesFiles
#7Oct 14SandboxAdd S3-based ad hoc sandbox job submission+83 -03
#20Oct 16DocsFix dead PRD/docs links in README+12 -51
#21Oct 16SandboxFix CPU oversubscription in dummy_jobs (local[4] to local[2])+4 -44
#22Oct 16SandboxDistinguish and filter sandbox runs in the dashboard+37 -23
#23Oct 16DiagnosticsAdd diagnostics module design spec+124 -01
#24Oct 16DiagnosticsAdd diagnostics module implementation plan+982 -01
#25Oct 16DiagnosticsAdd diagnostics module (skew, shuffle spill, resource underutilization)+1,521 -6620
#29Oct 18Infra & CIDocs/emr prod experiment plan+1,234 -3228
#30Oct 18DocsHandoff note for host setup Terraform doesn't manage (.env, tea...)+43 -21
#31Oct 18DocsDocs/handoff host setup+51 -77
#32Oct 18DatabaseInitial database schema for the Autospark workflow, with smoke tests+345 -03
#33Oct 18Infra & CICI: queue deploys (no overlap), raise SSH command timeout+8 -11
#34Oct 22DatabaseFindings tab, database sync, and IAM-auth app role (step 1)+1,152 -1228
#35Oct 22Security & IAMSecurity: remove master DB secret access from the app host role+26 -184
#36Oct 22Infra & CIUnified nav proxy+445 -1414
#62Oct 23DocsDocument the Caddy HTTPS front and refresh the architecture+51 -72
#87Oct 25DemoDemo deck and Spark visualization at /present/ (unlinked)+1,215 -025
#90Oct 30Security & IAMNamed IAM users for the team and sponsor contributors+243 -149
#99Oct 29DashboardJobs tab: list, run, watch and cancel prod jobs+1,551 -1516
#118Oct 29MCPMCP groundwork: dashboard logic moves into service/ (part of #110)+1,141 -15514
#119Oct 29MCPMCP server: read-only tools at POST /mcp (closes #111)+1,848 -320
#126Nov 5Sign-inGoogle sign-in: oauth2-proxy + Caddy, people by verified email (part of #61)+1,469 -7531
#176Nov 5MCPMCP authentication: personal access tokens (#112)+1,855 -9832
#178Nov 5MCPMCP: run and cancel prod jobs, behind the run scope (#113)+378 -2714
#179Nov 6MCPTrends data as plain functions in service/history.py (#117)+536 -918
#180Nov 7MCPMCP: upload a job behind the upload scope (#114)+867 -3718
#187Nov 7DocsDocs: MCP quickstart for the team+77 -33
#217Nov 7Security & IAMInfra: team-wide S3 access including delete (auto-spark-team-s3)+51 -13

Code merged by me

17,349 lines added and 689 removed across 314 file changes in 28 merged PRs. I merged them with merge commits, the convention in this repo. A deploy runs on every merge to main.

What the lines are

Tests6,38637%
Documentation4,41125%
Application code and scripts4,16824%
Infrastructure and CI1,2667%
Demo deck (present/)1,1186%

By directory

DirectoryLines addedFiles
tests/6,38664
docs/3,72324
infra/1,40447
present/1,15021
mcp_server/94914
dashboard/81442
store/75411
db/57712
service/5076
scripts/39314
proxy/1438
diagnostics/1485

All file types. diagnostics/ is small here because I wrote the first three detectors; teammates wrote the rest.

The tests are the largest category on purpose: the database tests run against a real Postgres, the MCP tests run over real HTTP, and for the security-sensitive code I broke it on purpose to check the tests fail (removing the commits, the scope check, the allowlist check, the token shape check).

Code walkthrough

Four pieces I would show, all from the MCP and sign-in work. Together they answer one question: how does a request prove who it is and what it may do?

1. A token is random, shown once, and never stored

store/tokens.py:56-98

def mint(conn, owner_id, name, scopes=("read",), ttl_days=None):
    ...
    token = PREFIX + secrets.token_urlsafe(_TOKEN_BYTES)        # 32 random bytes
    row = conn.execute(
        "INSERT INTO api_token (owner_id, name, token_hash, prefix, scopes, expires_at) "
        "VALUES (%s, %s, %s, %s, %s, now() + %s) RETURNING id",
        (owner_id, name, _hash(token), token[:8], list(scopes), timedelta(days=ttl_days))).fetchone()
    conn.commit()
    return token, row["id"]        # the token goes to the person once; only its SHA-256 is kept

def verify(conn, token):
    if not looks_like_token(token):                              # shape first: no database work for junk
        return None
    row = conn.execute("SELECT ... FROM api_token t JOIN app_user u ON u.id = t.owner_id "
                       "WHERE t.token_hash = %s AND t.revoked_at IS NULL AND t.expires_at > now()",
                       (_hash(token),)).fetchone()
  • Only a hash is stored, so a database leak does not leak working tokens.
  • Every write commits explicitly: the app's connection is not autocommit, and tests fail without the commits.
  • Scopes are checked by a database constraint as well as by the code.

2. The order of the checks on every call

mcp_server/auth.py:122-173

def authenticate(headers):
    token = bearer_token(headers)
    if token is None:                       raise AuthError(401, ...)     # no header
    if not tokens.looks_like_token(token):  raise AuthError(401, ...)     # junk: refused before the database
    principal = tokens.verify(conn, token)                                # unknown, expired or revoked: 401
    if principal.email.lower() not in allowed:  raise AuthError(401, ...) # owner removed from the sign-in list
    wait = _limiter.hit(principal.token_id)
    if wait:                                raise AuthError(429, ...)     # 120 calls a minute per token
    return principal

def require_scope(tool, principal):
    need = TOOL_SCOPES.get(tool)
    if need is None:  raise ScopeError(...)       # a tool with no declared scope is refused
  • Cheapest checks first, so anyone on the internet can only cost us a regular expression.
  • Removing someone from allowed_emails.txt ends their tokens too.
  • Default-deny: a new tool is unusable until someone assigns it a scope.

3. The gateway keeps the dashboard alive under load

mcp_server/app.py:47-70

class McpGateway:
    async def __call__(self, scope, receive, send):
        ...
        principal = await anyio.to_thread.run_sync(
            auth.authenticate, scope["headers"], limiter=self._lookups)   # off the event loop, on its own pool of 4
        ...
        reset = auth.current_principal.set(principal)    # tools read the caller from here, never from arguments
        try:
            await app(scope, receive, send)
        finally:
            auth.current_principal.reset(reset)
  • The token lookup blocks, so it runs in a pool of four threads that is separate from the dashboard's own.
  • No tool takes the caller as an argument, so a client cannot claim to be someone else.

4. The front door: Google sign-in for people, tokens for MCP

infra/caddy/Caddyfile:24-58

@mcp path_regexp ^/mcp$                 # exactly /mcp (Caddy's plain path match ignores case)
handle @mcp {
    request_header -X-Auth-Request-Email  # a client must never choose who it is
    request_header -X-Autospark-Email
    rewrite * /mcp                         # whatever matched, upstream sees /mcp
    reverse_proxy 127.0.0.1:8080           # the listener that trusts no identity header
}
handle {
    route {
        request_header -X-Auth-Request-Email
        request_header -X-Autospark-Email
        forward_auth 127.0.0.1:4180 { uri /oauth2/auth  copy_headers X-Auth-Request-Email ... }
        reverse_proxy 127.0.0.1:8081       # the listener that trusts the verified email
    }
}
  • Browsers get a Google redirect; command-line MCP clients send a token instead.
  • An independent review found that Caddy matched the cleaned path but forwarded the raw one. The rewrite closes that.

Documentation

I wrote 4,411 lines of documentation. The runbook and the quickstart are written to be followed by someone who was not in the room.

README.mdWhat AutoSpark is, what is built, what is planned, how to run it.
docs/architecture.mdDiagrams of the components, the data flow and the sandbox loop.
docs/mcp.mdThe MCP reference: tools, scopes, limits, errors, how to turn it off.
docs/mcp-quickstart.mdThe five-minute guide for teammates: make a token, connect Claude Code.
infra/HANDOFF.mdThe operator runbook: host setup, sign-in cutover, token roll-out, rollback steps.
db/README.mdThe data model, the rules the database enforces, how to run a migration safely.
dashboard/DASHBOARD.mdHow the web app is organised.
docs/superpowers/Ten written specs and plans, such as the diagnostics module and the Google sign-in cutover.

My 10-minute plan

A run-of-show so the ten minutes follow Mario's outline.

TimeStepWhat I do
0:00What it isOne sentence, then the system diagram (section 3). Point at the two ways in: browser with Google sign-in, MCP client with a token.
1:30Live: the dashboardFindings tab, then the Jobs tab (my PR #99). Show a finding and its suggestion.
3:00Live: the MCPIn Claude Code: list_applications, then get_problem. Say what the scopes do and why run and upload are opt-in.
5:00Code walkthroughPick two of the four in section 7. I would use the order of checks and the front door.
7:30Numbers28 PRs, 52 issues, tests are 37% of what I added. Show the timeline.
8:30Process and docsSpec, plan, test first, independent review. Open the quickstart and the handoff runbook.
9:30NextApprovals through MCP (on hold for a team decision), apply-fix recording, sponsor data.