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.
Read the write-up in paper/paper.pdf, and the slides in
deck/deck.pdf.
No cloud project. No credentials. No network.
cd app
python3 -m venv .venv && .venv/bin/pip install -e '.[dev]'
make demoui/index.html is the same run as a self-contained page: open it as a file, and
the loop plays on load.
| # | 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.
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
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.
| 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.
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
credentialbut notcredentials,attackbut notattacks, and the tokenexfiltratcould 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_modelbuilt a BigQuery client and submitted aCREATE MODELjob regardless ofATLAS_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():1e9raised at import and madeatlas --checkunstartable, while0and-1parsed fine and disabled both the per-query rail and the per-cycle budget. - The dollar figure never reached a human.
compute_impactwas renamed to emitlost_so_far; the PR body and the Slack alert kept readingdollars_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
-5and a confidence of7.5both reachedML.FORECAST; a negative baseline window becameINTERVAL -30 DAY, a window in the future. atlas demo --jsonwas 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.
make test # 303 tests, under 1s on the reference machineHermetic 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.
| 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.
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:8080The 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.
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-configas 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.
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
@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.

