ATC is an MCP gateway that sits between AI agents and the tools they call, risk-assesses every tool call, holds the dangerous ones for a human to decide, and records the whole intent → decision → action trail as OpenTelemetry traces in SigNoz.
Autonomous agents now run real tools against real systems: databases, filesystems, git remotes. When one of them does the wrong thing (a coding agent that drops a production table, an assistant that resurrects a stale task after a bad memory compaction), the damage is usually done in seconds, and the first anyone hears about it is the postmortem. ATC's bet is that governance has to sit before execution, not in a dashboard you check afterward.
Built for the SigNoz Hackathon (WeMakeDevs × SigNoz, Jul 20–26 2026).
Repo: github.com/furyfist/agent-atcc
Three things are true at once in 2026:
- AI agents now execute real tools. Not "suggest code": they call MCP tools that write to databases, delete files, and push to git, with no human in the loop by default.
- Existing observability is retrospective. APM tools tell you what a request did after it happened. That's fine for latency and error rates. It does nothing to stop a bad tool call before it runs.
- Governance has to be a pre-execution gate, not a post-hoc log. By the time an alert fires, the
DROP TABLEalready ran.
ATC's framing: governance signals are observability signals. We didn't want to build a separate logging system just to answer "why did the agent do that": the trace is the audit log, and the same OpenTelemetry pipeline that would tell you a request was slow is the one that tells you a tool call was denied.
The air-traffic-control analogy, literally: ATC doesn't stop planes from flying. It holds the ones that look dangerous on the runway until a controller clears them, and every clearance, hold, and denial is logged against the flight. That's the whole product: agents are the aircraft, the risk engine is the tower's radar, the approval queue is the hold, and SigNoz is the flight recorder.
[Architecture Blog]: a real, evidence-backed write-up already exists in-repo, docs/BLOG_DRAFT.md, with the full evidence log it was written from in docs/evidence/.
flowchart TD
A["Agent"] -->|"MCP tool call"| B["ATC Gateway"]
B --> C["Risk Engine"]
C -->|"LOW / MEDIUM"| E["Tool Server"]
C -->|"HIGH: held"| D["Approval Engine"]
D -->|"approved"| E
D -->|"denied"| A
E --> F[("Victim Systems: Postgres, FS, Git")]
B -.->|"spans + metrics"| G["OTel Collector"]
E -.->|"spans"| G
G --> H[("SigNoz")]
- Agent: one of three Groq-driven personas (
coder-01,assist-01,comply-01), each with a declared tool scope, calling tools over MCP. - ATC Gateway (
atc-core): an MCP server to agents and an MCP client to the tool servers. Aggregates and namespaces the tool catalog (db__execute,fs__write, etc.), enforces scope, and orchestrates the decision pipeline. - Risk Engine: deterministic, YAML-driven rule matching (
policies/risk_rules.yaml). No LLM ever runs in this path. First matching rule wins; unmatched or unparseable input fails closed. - Approval Engine: holds HIGH-risk calls for up to 120s, exposes them over REST/WebSocket to the operator console, and resolves them to approve/deny/expire.
- Tool Server:
tools-db,tools-fs,tools-git: sandboxed MCP servers that actually touch the victim systems, only reachable through the gateway. - OpenTelemetry / SigNoz: every hop emits spans and metrics through one OTel Collector into SigNoz. Export is fire-and-forget: a dead collector never blocks a governance decision.
One tool call, start to finish (this is the real Act 2 scenario from docs/evidence/exp01-flagship-near-miss.md):
coder-01 calls db__execute("DROP TABLE customers")
↓
ATC Gateway intercepts, checks scope (db, fs, git: in scope, OK)
↓
Risk Engine evaluates the SQL: touches a prod-tagged table → HIGH
↓
Held for approval: WS event fires, operator console shows a countdown card
↓
Human denies it → agent gets back [ATC-DENIED] reason=... policy_rule=SQL-PROD-TABLE-HIGH
↓
Agent reasons on its own, proposes ALTER TABLE customers RENAME TO archived_customers
↓
Still HIGH (same prod table) → held again → human approves → executes
↓
Every step is a span (atc.gate → atc.risk_assessment → atc.interception →
atc.approval_wait → atc.execution) exported to SigNoz, linked by one trace id
| Feature | Description | Status |
|---|---|---|
| MCP gateway interception | Scope-checked, risk-assessed proxy in front of every agent tool call | Implemented |
| Deterministic risk engine | YAML policy, first-match-wins, fail-closed on unparseable/unmatched input | Implemented |
| Approval workflow | REST + WebSocket hold queue, live countdown, agent quarantine/kill-switch | Implemented |
| React operator console | Full SPA (Overview, Approvals, Fleet, Activity, Incidents, Policy Lab, Notifications, Settings) | Implemented |
| Blast-radius estimation | Pre-approval ~N rows affected estimate for mutating SQL, via a live COUNT |
Implemented |
| Reversibility classification | REVERSIBLE / COMPENSABLE / IRREVERSIBLE, computed separately from risk | Implemented |
| Pre-image journal + undo | Captures prior state before COMPENSABLE mutations; POST /api/actions/{id}/undo replays the compensation as its own audited action |
Implemented, tested; not yet exercised against a real journaled action live |
| Token-budget circuit breaker | Denies further calls with [ATC-BUDGET] once an agent's heartbeat-reported spend crosses a threshold |
Implemented |
| Loop-suspicion detection | Flags repeated near-identical calls in a short window (non-gating) | Implemented |
| Permission-creep detection | Flags an in-scope resource touched for the first time by an agent | Implemented |
| Narrator | LLM (Groq) explains a trace in plain English, cached by trace id | Implemented; primary SigNoz Trace API path blocked on an API-key permissions issue, SQLite fallback works |
| Policy replay / shadow / diff engine | Replays real trajectories offline against a policy, runs N policies side by side, diffs two runs call-by-call | Implemented |
| Telegram notifications | Approval requests routed to Telegram with inline approve/deny | Implemented, mock-tested; no live bot verified |
| SigNoz dashboards & alerts | 4 dashboards, 3 alert rules, authored against verified-live metrics | Authored; not yet import-verified against a live SigNoz session (see dashboards/README.md) |
| Postgres-backed tools-db | victim-postgres is seeded and running |
Not built: tools-db still runs on its SQLite fallback |
Every service that emits telemetry (atc-core, agent-runner, history-seeder) uses the OpenTelemetry Python SDK via the shared packages/atc_telemetry package, exporting OTLP to a single collector (otel-collector/config.yaml) that forwards everything to SigNoz. Switching from self-hosted SigNoz (via Foundry) to SigNoz Cloud is one environment variable; no service knows which SigNoz it's talking to.
Traces. One trace per agent mission:
agent.mission (root)
├── agent.turn
│ ├── gen_ai.chat (OTel GenAI semconv: model, token usage)
│ └── mcp.tool.call {tool}
│ └── atc.gate {tool}
│ ├── atc.risk_assessment
│ ├── atc.interception (HELD only, instant, links to atc.execution)
│ ├── atc.approval_wait (= human decision latency)
│ └── atc.execution
│ └── tool.{name}
Services. atc-core (the gateway), agent-runner (the agents), history-seeder (backfilled baseline history), and atc-replay (offline policy replay runs, tagged separately so they never pollute live dashboards).
Custom metrics (all confirmed landing in ClickHouse): atc_actions_total, atc_interceptions_total, atc_approval_latency_seconds, atc_agent_risk_score (EWMA gauge), atc_agent_heartbeat, agent_tokens_total, atc_novel_resource_total, atc_loops_suspected_total, atc_incidents_total, atc_notifications_sent_total, plus the atc_replay_* family (FP/FN rate, approval burden, decision latency).
Why this matters for an operator: the Fleet Tower and Governance dashboards (dashboards/) are built entirely on this data (agent risk scores, approval latency, denial rate by policy rule), so an operator never needs a second observability stack to answer "is this agent behaving." Permission-creep detection itself queries SigNoz history (novel resource = zero prior spans for this agent+resource), so SigNoz is a real dependency of the product, not just a dashboard bolted on afterward.
What's honestly incomplete: tools-db, tools-fs, and tools-git don't yet emit their own spans; only the gateway side of a tool call is traced. Resource attributes on every provider currently set only service.name, not deployment.environment or host.name. The logs pipeline is wired end-to-end but no service constructs a LoggerProvider yet, so it's an empty pipe. Dashboards and alerts are authored against real, verified metric names but not yet round-tripped through a live SigNoz import (blocked on minting a role-scoped API key through the SigNoz UI; see dashboards/README.md and alerts/README.md).
Nothing here is hardcoded to our demo stack. The SigNoz base URL, operator identity, UI density, and live-update behavior are all configurable from this one screen, so any team can point their own console at their own SigNoz instance and make it theirs, no rebuild, no env var spelunking.
Real captured evidence (traces, terminal output, dashboard panels) from live runs lives in docs/evidence/screenshots/.
services/
atc-core/ # gateway, risk engine, approval manager, REST/WS API, replay/shadow/diff, narrator
agent-runner/ # 3 Groq-driven agent personas, scenario runner
tools-db/ # MCP server: db__query, db__execute (SQLite; Postgres seeded but unused)
tools-fs/ # MCP server: fs__read/write/delete, sandboxed volume
tools-git/ # MCP server: mocked in-memory git__push/force_push
history-seeder/ # one-shot: backfills baseline action history
packages/
atc_telemetry/ # shared OTel span/metric instrumentation, one schema for every service
web/ # React + TypeScript operator console (Vite, builds into services/atc-core/static)
policies/ # risk_rules.yaml (risk engine), agents.yaml (agent registry/scope)
dashboards/ # SigNoz dashboard JSON (Fleet Tower, Governance, Replay Analytics)
alerts/ # SigNoz alert rule JSON
signoz/ # Foundry casting.yaml, deploys local SigNoz
otel-collector/ # single OTLP collector config
scripts/ # demo/red-team trigger scripts used to produce docs/evidence/
docs/ # BLOG_DRAFT.md, PRODUCT_STRATEGY.md, FRONTEND_RFC.md, evidence/, contracts/
Prerequisites: Docker, uv, Node.js (for the web console), a free Groq API key.
1. Bring up SigNoz first (via Foundry; this repo's compose stack attaches to the network it creates):
cd signoz && foundryctl castComplete the one-time signup wizard at http://localhost:8080 before anything else: OTLP ingestion is silently dropped until an org/admin account exists (see signoz/README.md).
2. Environment variables:
cp .env.example .envFill in GROQ_API_KEY and generate the three ATC_TOKEN_* agent bearer tokens:
python -c "import secrets; print(secrets.token_urlsafe(32))"3. Build and run:
docker compose restart otel-collector # reconnect after SigNoz signup
docker compose up -d --build
ATC_HISTORY_FORCE=true docker compose --profile seed run --rm history-seederThe operator console is at http://localhost:8001 (host port 8000 is reserved for the SigNoz MCP server Foundry starts alongside SigNoz).
4. Frontend dev loop (optional; the console is already built into the image above):
make web-dev # vite dev server, proxies /api and /ws to atc-core on :8001
make web-build # writes straight into services/atc-core/static/5. Backend dependencies and tests:
uv sync --all-packages
uv run --package atc-core pytest services/atc-core/tests/
uv run --package tools-fs pytest services/tools-fs/tests/Run tests per-service with an explicit path: several services share test filenames (test_server.py), which breaks a combined pytest services/ packages/ invocation.
6. Demo reset (restores Postgres/fs/SQLite state and force-reseeds baseline history between takes):
make reset-demoThe centerpiece scenario is the near-miss: coder-01 is asked to clean up an old staging table, and reasonably-sounding cleanup targets a table policy has tagged production.
Overview → fleet green, nothing pending
↓
python scripts/flagship_near_miss.py runs inside the atc-core/agent-runner
containers: a real Groq-driven mission that issues DROP TABLE customers
↓
Approvals → held HIGH-risk card appears live, countdown running,
reason "Statement touches a table tagged as production"
↓
Deny it live → agent gets [ATC-DENIED], reasons on its own, proposes
ALTER TABLE customers RENAME TO archived_customers
↓
Approve the recovery → mission completes, table preserved not dropped
↓
Incidents → open the incident, click [Explain this trace] to get
the Narrator's plain-English summary
↓
SigNoz → [Open in SigNoz] on the incident for the full trace
waterfall: risk_assessment → interception → approval_wait
→ execution, all real spans, one trace id
Other single-purpose trigger scripts under scripts/ reproduce specific findings without an LLM in the loop: trigger_scope_violation.py, trigger_permission_creep.py, trigger_blast_radius.py, trigger_budget_breaker.py, trigger_loop_suspicion.py, redteam_policy_gaps.py, prompt_injection_probe.py. Each corresponds to one write-up in docs/evidence/.
The demo's live-vs-replay gate: a scenario must pass ≥8/10 scripted runs to be recorded live; otherwise that portion is deterministic replay, disclosed without shame.
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, Tailwind CSS v4, shadcn/radix-ui, TanStack Query/Table/Virtual |
| Backend | Python 3.12, FastAPI, official MCP Python SDK (Streamable HTTP transport), uv workspace |
| Database | SQLite (ATC state: agents, actions, incidents, journal), PostgreSQL 16 (victim-postgres, seeded fake prod data) |
| Observability | OpenTelemetry Python SDK, OpenTelemetry Collector, SigNoz (self-hosted via Foundry, or Cloud) |
| AI | Groq (llama-3.3-70b-versatile) for agent reasoning and the Narrator |
| Infrastructure | Docker Compose, Foundry (foundryctl) for SigNoz deployment |
- Wire
tools-db,tools-fs,tools-gitintoatc_telemetryso tool-server spans exist, not just gateway-side ones. - Fix the SigNoz Trace API permissions issue blocking the Narrator's primary (non-fallback) fetch path.
- Import-verify all four dashboards and three alert rules against a live SigNoz session once a role-scoped API key is minted.
- Build the Postgres backend for
tools-db:victim-postgresis already provisioned and seeded, just unused. - Behavioral risk scoring that earns an agent back autonomy over time instead of holding every call at a fixed rate forever.
- Tamper-evident, exportable decision records shaped for EU AI Act Article 12: the policy-version content hash already stamped on every decision is the seed of this.
OpenTelemetry, SigNoz and Foundry, the Model Context Protocol and its official Python SDK, Groq for free-tier LLM inference, sqlglot for SQL risk parsing, and the FastAPI / React / Vite / TanStack / shadcn ecosystem this is built on.




