Skip to content

Repository files navigation

Atlas

An autonomous data-ops agent whose answer is a pull request.

A payments config is pushed at 14:03. It routes EMEA card traffic to a gateway that declines 85% of authorisations. Order volume looks normal, because customers still try to buy. Completed revenue falls 35%, and nobody notices until the next morning.

Atlas notices in one cycle, and it does not stop at noticing:

  regions:
-   EMEA: { active_provider: adyen,  fallback: stripe }
+   EMEA: { active_provider: stripe, fallback: adyen  }
    NA:   { active_provider: stripe, fallback: adyen  }
    APAC: { active_provider: adyen,  fallback: paypal }

That one line, opened as a pull request against the config repository that caused the incident, with the incident report as the PR body, is the whole remediation. The merge button stays with a human.

Atlas live demo: a resolved revenue incident, the six-stage predict-to-verify loop, and the pull request it opened

Paper and deck

Read the write-up in paper/paper.pdf, and the slides in deck/deck.pdf.

See it in twelve seconds

No cloud project. No credentials. No network.

cd app
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
make demo

The full detect to verify loop running offline against the fixture warehouse

ui/index.html is the same run as a self-contained page: open it as a file, and the loop plays on load.

What actually happens in those twelve seconds

# Stage What it does
1 Detect Trailing z-score on the KPI. z = -3.70, 35.3% below a $405,552/day baseline.
2 Investigate The agent writes its own SQL and slices until one cell moves: EMEA/adyen, 1,568 of 1,842 authorisations failing.
3 Correlate Joins the anomaly to a change log and attributes it: the 14:03 config push by deploy-bot.
4 Quantify $143,152 lost so far; ~$1,002,062 over seven days if left alone.
5 Remediate Classifies the domain, routes the fix, computes the payments.yaml diff.
6 Verify Re-reads the metric next cycle and marks the incident resolved or escalated.

Every stage appends to one run document, so the chain from a number moved to this diff is a single auditable object. atlas demo --json prints it.

Architecture

flowchart TB
  TRIG["Cloud Scheduler tick<br/>or atlas watch"] --> SUP

  subgraph RT["ADK runtime: supervisor and five sub-agents"]
    SUP["Supervisor<br/>atlas_supervisor"]
    W["1 Watcher<br/>detect_kpi_anomalies"]
    I["2 Investigator<br/>run_bigquery + query_change_events"]
    C["3 Correlator<br/>Gemini, change-attributed cause"]
    A["4 Actor<br/>alert, ticket, summarise"]
    RM["5 Remediation<br/>recommend the concrete fix"]
    SUP --> W --> I --> C --> A --> RM
  end

  RM --> ENG["Remediation engine, plain Python:<br/>propose, gate, route, execute"]
  ENG --> V["Verifier: re-read the KPI next cycle<br/>resolved or escalated"]

  subgraph GUARD["Guardrails: no model call, tested separately"]
    G1["READ_ONLY_SQL<br/>one SELECT, no federated writes"]
    G2["QUERY_BUDGET<br/>bytes reserved before dispatch"]
    G3["CONTENT_SAFETY<br/>screens model output"]
    G4["ACTION_LIMITER<br/>per cycle and per hour"]
    G5["REMEDIATION<br/>approval gate"]
  end

  W -.-> G1
  I -.-> G2
  C -.-> G3
  A -.-> G4
  ENG -.-> G5

  BQ[("BigQuery<br/>orders, live KPI + audit views")]
  W --> BQ
  I --> BQ
  V --> BQ

  ENG --> R{"Domain classifier"}
  R -->|finance| SL["Slack alert"]
  R -->|finance and infra| PR["GitHub pull request<br/>payments.yaml, one line"]
  R -->|security| TK["Ticket, and nothing else"]
  R -->|data quality and unknown| TK

  FS[("Firestore<br/>run document, recurrence memory")]
  SUP --> FS
  V --> FS

  style SUP fill:#a3e635,stroke:#65a30d,color:#0a0a0a
  style PR fill:#0f1a12,stroke:#a3e635,color:#bef264
  style TK fill:#1c1917,stroke:#eab308,color:#eab308
Loading

Five guardrails, each tested without the model

The model decides sequencing and writes prose. It never computes a number that a decision reads: the z-score, the dollar impact, the byte reservation, the domain classification and the YAML edit are all plain Python. A guardrail a model can talk its way past is a suggestion.

Guardrail What it refuses
READ_ONLY_SQL anything but a single SELECT/WITH, including a write smuggled through EXTERNAL_QUERY
QUERY_BUDGET a query whose worst case exceeds the cycle's byte allowance, checked before dispatch
CONTENT_SAFETY acting on model text carrying injection or escalation markers
ACTION_LIMITER more than N outbound actions per cycle and per hour; a refusal does not spend a slot
REMEDIATION executing a fix without passing the approval gate

Each decision is recorded on the run by name, so "guardrails enforced: 17 checks" is a count of things that happened, not a claim.

The routing rule that matters most

Domain Handlers
finance / payments Slack, pull request, ticket
infra / config pull request, ticket
data quality ticket
security ticket, and nothing else
unknown ticket

A credential leak in the payments service matches the finance vocabulary, and the finance route publishes to a public repository. So the security domain wins precedence unconditionally, and the Slack and pull-request handlers refuse that domain independently of the routing table, so a misconfigured route cannot override the property.

What writing the tests found

303 tests. Nine real defects. Five of them were in the safety layer itself.

Each is now pinned by a test that fails without the fix.

  • The security classifier missed every plural. The pattern matched credential but not credentials, attack but not attacks, and the token exfiltrat could never match anything, because no English word is exactly that string. "Payment credentials were exfiltrated overnight" classified as finance and would have been published to a public pull request. Fourteen tests fail without the fix.
  • Offline mode issued a billable write. ensure_forecast_model built a BigQuery client and submitted a CREATE MODEL job regardless of ATLAS_OFFLINE=1, the one mode documented as opening no connection.
  • The spend cap could be turned off by a typo. The byte cap was parsed with a bare int(): 1e9 raised at import and made atlas --check unstartable, while 0 and -1 parsed fine and disabled both the per-query rail and the per-cycle budget.
  • The dollar figure never reached a human. compute_impact was renamed to emit lost_so_far; the PR body and the Slack alert kept reading dollars_lost, so both printed "material revenue" over a figure computed to the cent.
  • The audit trail could be edited after the fact. The in-memory store copied run documents with dict(doc), which shares every nested list.
  • Concurrent cycles cross-charged each other. The active run id was a module global, so the last cycle to bind won for the whole process. It is now a ContextVar.
  • Out-of-domain numbers were forwarded verbatim. A forecast horizon of -5 and a confidence of 7.5 both reached ML.FORECAST; a negative baseline window became INTERVAL -30 DAY, a window in the future.
  • atlas demo --json was not parseable. The stage printers wrote to stdout unconditionally, so six stages of prose preceded the JSON.
  • The PR handler spent an action slot without recording it, so the audit trail reported three ACTION_LIMITER decisions for four consumed slots.

The generalisable part is narrow: naming a guardrail and writing its docstring produced no assurance at all. What found these was writing a test that tried to violate the stated property with inputs the author had not thought of.

The tests

make test        # 303 tests, under 1s on the reference machine

Hermetic by construction. tests/conftest.py pins the environment before atlas is imported (offline, in-memory state, no credential discoverable), then replaces socket.socket, urllib.request.urlopen and subprocess.run with objects that fail the test. Nothing in the suite can reach BigQuery, Firestore, Gemini or GitHub even by accident.

Module Tests Property under test
test_guardrails.py 34 the screen, the limiter and the budget as units
test_tools.py 69 the tool surface a model actually calls
test_analysis.py 48 detection, impact and forecast, including their input domains
test_remediation_router.py 44 routing decisions and the disclosure rule
test_github_pr.py 24 the exact requests it would send, against a recording transport
test_state.py 27 run identity, lifecycle, and audit-trail isolation
test_demo.py 18 the whole cycle end to end, and what it may claim

The GitHub tests are worth a look: there is no token on the machine that produced them, so they inject a fake transport and assert the six REST calls, the headers, the branch name, the base64 file content and the commit message, plus that with no token the path sends nothing at all rather than half-trying.

Commands

Command What it does
atlas demo The whole loop, offline, against the fixture warehouse. No cloud project needed.
atlas demo --json The same cycle, printed as the run document.
atlas --check Print the config preflight and exit. Calls nothing.
atlas One autonomous cycle against live BigQuery and Gemini.
atlas serve The web dashboard plus the /trigger endpoint (binds $PORT).
atlas watch --interval 300 Run a cycle every N seconds, unattended.
atlas ask "why did revenue drop in EMEA?" One-off ad-hoc investigation through the same agents.
atlas mcp Serve Atlas's tools over the Model Context Protocol (stdio).
atlas media <run_id> Generate a shareable incident card (needs ATLAS_MEDIA=on).

make help lists the wrappers.

Running it against real Google Cloud

A Cloud Run deployment of this agent is still up at https://atlas-d6trrulpna-uc.a.run.app/ (scale-to-zero, so the first request wakes it). It is not a live agent: it last ran a cycle on 2026-08-18, and what it is showing is that day's run history. The five pull requests it opened are on doom2quake/atlas-demo-config.

To run your own:

cp .env.example .env          # set GOOGLE_CLOUD_PROJECT; keep LOCATION=global
gcloud auth application-default login
make train-model              # once: bootstrap history and train ARIMA_PLUS
make seed                     # fast DML reset of today's anomaly
make check                    # preflight, calls nothing
make run                      # one live cycle
make serve                    # dashboard on http://localhost:8080

The server also runs CREATE MODEL IF NOT EXISTS during startup when ATLAS_FORECAST=on. A cold Cloud Run process therefore verifies the persistent model before accepting traffic. POST /api/demo never trains or deletes it; the request only replants today's rows and reads the existing model with ML.FORECAST.

Deploys to Cloud Run with make deploy (scale-to-zero, unauthenticated access off), or to Vertex AI Agent Engine, since atlas.agents.root_agent is a standard ADK agent. Point a Cloud Scheduler job at POST /trigger for one cycle per tick. Grant the runtime service account roles/aiplatform.user, roles/bigquery.dataEditor + roles/bigquery.jobUser, and roles/datastore.user. Keep webhooks and tokens in Secret Manager.

Optional integrations such as Slack, media generation, Agent2Agent handoff, and consuming tools over MCP are env-gated and degrade to a labelled no-op. The live server deliberately fails startup if forecasting is disabled or the persistent BigQuery ML model cannot be verified. It never substitutes fixture data for a failed live service.

Honesty

docs/LIMITATIONS.md separates what has run from what has not, by evidence class. In short:

  • The offline loop and the 303 tests are reproducible on any machine, and every offline number is tagged source="fixture" so it cannot be mistaken for a warehouse read.
  • A Cloud Run deployment ran on live BigQuery and Gemini on 2026-08-18 and left five public pull requests on doom2quake/atlas-demo-config as artifacts. Three of its thirteen cycles errored.
  • A local live benchmark on 2026-08-31 ran the complete loop against BigQuery, Gemini, Firestore, and GitHub. The final 25.23-second run opened PR #17, remediated 78 real warehouse rows, and verified the incident as resolved. That build has not been deployed or pushed.
  • Slack delivery, Veo clip generation, the Agent2Agent handoff, and consuming tools over MCP are implemented and have never been observed working.

Repository layout

app/
├── atlas/
│   ├── agents.py             ADK supervisor and specialists
│   ├── skills.py             named capabilities: purpose, model tier, tools, instruction
│   ├── tools.py              read-only BigQuery, anomaly detector, change events, actions
│   ├── guardrails.py         the content screen, the action limiter, the spend cap
│   ├── remediation.py        propose, gate, execute, verify
│   ├── remediation_router.py the domain classifier and the pluggable handlers
│   ├── github_pr.py          the REST calls, the payments.yaml edit, token resolution
│   ├── impact.py             dollar quantification
│   ├── forecast.py           BigQuery ML ARIMA_PLUS early warning
│   ├── fixtures.py           the offline warehouse
│   ├── demo.py               the offline cycle, `atlas demo`
│   ├── state.py              run documents, recurrence memory, lifecycle stats
│   ├── server.py             dashboard, triggers, remediate/forecast/media endpoints
│   ├── mcp_server.py         serving Atlas's tools over MCP
│   └── main.py               the CLI and run_cycle
├── tests/                    303 hermetic tests
├── ui/index.html             self-contained one-page demo
├── paper/paper.tex           the write-up and its references
├── deck/deck.md              Marp slides
├── docs/LIMITATIONS.md       what has and has not run
└── DEMO.md                   the timed shot list

Cite

@software{sarkar_atlas_2026,
  author  = {Dipankar Sarkar},
  title   = {Atlas: An Autonomous {BigQuery} Data-Ops Agent},
  year    = {2026},
  version = {0.1.0},
  url     = {https://github.com/doom2quake/atlas},
  license = {MIT}
}

See CITATION.cff. Licensed MIT, see LICENSE.

doom2quake's entry for the All Things Agentic Hackathon, Taskmaster track.

About

An autonomous data-ops analyst on Google ADK that predicts a KPI miss, finds the cause, and opens the pull request that fixes it.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages