sudoX · reference

Inside sudoX

A level below the overview: what each part actually does, and the judgement calls that decide how it behaves. For someone who will operate or extend it.

The system

One Go binary. Two ingestion paths converge on one emission pipeline; every alert is persisted before anybody is notified; the estate is reached only through tool providers, never directly.

SIGNALS IN Webhook alerts POST /api/incidents AWS SNS / SQS Signal sources Elasticsearch, Loki, CloudWatch, Graylog, Splunk, files ONE GO BINARY + EMBEDDED CONSOLE AI SRE agent patterns + baselines Detect agent LLM triage Emission pipeline persist first, then fan out — a failed channel never short-circuits the rest Analyze agent read-only ReAct investigation Act agent remediation planner Action Engine approval-gated executor Episodic memory Runbook corpus Draft agent Storage — file / memory / Postgres PEOPLE OUT Email Slack / Teams / Telegram / Viber / Lark PagerDuty / ServiceNow / AWS IM / incident.io TOOL PROVIDERS MCP servers Postgres, MongoDB, MySQL, MSSQL, Oracle, Redis, AWS, Azure, GCP, Kubernetes Native on-prem toolset SSH + Ansible, allowlisted per host, no arbitrary-shell tool exists read + write catalogs
Two ingestion paths converge on one emission pipeline: free-form webhook alerts, and the AI SRE agent that tails logs, clusters them into patterns and raises incidents only for novel or spiking behaviour. Everything inside the accent border is one process, and the tool providers along the bottom are the only way the estate is ever touched. Which of those tools an agent may reach, and under what gate, is the tool-integration figure later on.

Signals in

Three ways something reaches sudoX. Two of them are learned; the third is pushed at it.

Logs

Seven backends — Elasticsearch, Loki, CloudWatch Logs, Google Cloud Logging, Graylog, Splunk and plain files. Lines are clustered into templates by Drain-style mining, and each template gets an EWMA baseline of its arrival rate. Something is raised only for a pattern that is genuinely new, or one arriving far faster than usual.

Cloud Logging is shaped like Elasticsearch rather than CloudWatch, and the reason is in what each API orders by. CloudWatch returns events in ingestion order, so a strict "newer than the cursor" never skips anything. Cloud Logging orders by the entry's own timestamp — the producer's clock — while visibility depends on when Google indexed it. Each tick therefore re-scans an overlapping window and deduplicates on insertId, because losing log lines silently is the worst failure an incident platform can have: nothing in the output distinguishes "nothing happened" from "we did not look".

Metrics

Golden signals — traffic, errors, latency, saturation — learned per service from PromQL, CloudWatch Metrics Insights, or Google Cloud Monitoring, all behind one learner with one set of statistics. Cloud Monitoring needed no client of its own: Google serves a Prometheus-compatible query API, so a GCP project is a Prometheus endpoint with a different address and credential. Two gates decide whether a sample is worth raising, and both must hold.

Statistical: at least N standard deviations from the learned mean. Without it, ordinary movement on a noisy metric alerts.

Practical: the sample also moved by a minimum fraction of normal. Without it, a very stable metric alerts on trivia — 100 ± 1 reading 103 is three sigma out and three percent different.

Direction is judged by kind: errors, latency and saturation only matter when they rise, because a service getting faster is not an incident. Traffic is the exception — demand collapsing usually means something upstream broke. Samples far enough out are held out of the mean, so a sustained incident cannot become the new normal and silence the detector while it is still happening.

Webhooks

Anything that can POST JSON. These arrive already classified, so they skip learning and enter the incident lifecycle directly.

Try it

The reorder window, entry by entry.

Six entries arrive per burst. One is out of order, one is a duplicate insertId, one is very late. The window holds what arrived until it is old enough to be placed in order; the cursor moves only past what was emitted. Change the window and send the next burst.

gcplogging source · acme-prod

arrived this tick

held in the window

emitted, in order

in the console Watching (each source's cursor and last error) · pkg/signalsources/gcplogging.go

What to notice. With a 0s window the out-of-order entry arrives after the cursor has passed it and is counted as too late — never emitted, and never silently lost either: the count is on the source's row. With 5s it is caught and emitted in its proper place. The very late entry, twenty seconds old, is caught only by the 30s window, which is the trade the operator makes: a wider window is more complete and further behind. The duplicate is dropped under every setting, because the same insertId is the same entry however many times Google shows it.

From alert to record

An alert is persisted before notification fans out. An operator opens an investigation, and a read-only ReAct loop writes back an analysis record with its tool-call trail — every call it made, in order, so the conclusion can be checked rather than trusted.

OPERATOR PIPELINE STORAGE CHANNELS ANALYZE + TOOLS alert arrives persist the incident — before any notify fan out to every enabled channel notification with an HMAC-signed ack link open an investigation AnalysisRecord: finding + full tool-call trail ReAct loop, read-only: logs, metrics, schema, EXPLAIN, recall — every call audited
The order is the guarantee. Persisting before notifying means a channel outage costs a notification, not the incident; and because the fan-out never short-circuits on a failed channel, one broken webhook does not silence the three that work.

The agents

Eight single-purpose model calls. None of them touches infrastructure — every one produces a record a human reads.

AgentRuns whenProduces
detecta pattern is new or spikingan incident classification
analyzean operator asks about an incidentroot-cause hypotheses with evidence and a tool trace
askan operator asks the estate a questionan answer whose every citation was checked against what the tools returned
actan operator asks for a fixan ActionPlan awaiting approval
inspectan estate assessment, or a verificationfindings, and measured before/after outcomes
extractan operator enriches the graphentity and relationship proposals
drafta plan has executeda runbook draft
postmorteman incident closesa postmortem from the records the platform already holds

Only analyze and ask call tools, and only read-only ones — enforced twice, by a compile-time import guard for native tools and a runtime mode gate for MCP ones. Each task can run on its own model, because detect runs on every tick and is judged on cost while act is judged on being right.

Investigation and the knowledge graph

Seven deterministic record sources — services, incidents, episodes, patterns, runbooks, hosts, providers, and now environments — build a typed knowledge graph read by the analyze agent, the planner and the console. Operations questions are relational: "what does this depend on", "what fixed this before". The graph makes those hops first-class, so one call returns an evidence chain instead of loosely related snippets.

DETERMINISTIC SOURCES — NO LLM tools.yaml dependencies incidents episodes + executed plans pattern catalog runbook corpus on-prem inventory MCP server scopes Knowledge graph NODES service, incident, episode, pattern, runbook, host, provider EDGES remediates, depends_on, affects, documents, observed_in, hosts, provides derived data — no new database Analyze agent Planner + assessment Knowledge graph page graph_context tool knowledge_graph prompt section nodes + edges
Retrieval is the GraphRAG pattern: find an entry point, then walk. A query scores nodes by relevance, and the walk expands two hops in both directions into a bounded evidence chain — so one call returns “this episode remediated that incident on this service, which depends on postgres, which this runbook documents, and checkout depends on it” instead of a pile of separately-retrieved snippets.

Try it

What depends on this?

A small estate as the graph holds it: an environment, two providers, the services they host, the databases the services read, and a finding about one of them. Hover or click a node. The answer to "what depends on it" is every node with a path into it — and the environment at the top says which estate the answer is about.

knowledge graph · 10 nodes · 14 edges

in the console Knowledge graph · Topology · Analyses (the evidence chain an analysis reads) · pkg/knowledge

What to notice. Click the RDS database: the two services that read it, the load balancer that fronts one of them, and the finding about it all light up, and the answer names prod. That is the hop an analysis makes in one call when an incident mentions the database — and the reason the environment is a node rather than a label: a label is a string on a record, a node is something a path can go through.

Enrichment: proposals, not writes

The extract agent turns completed analyses into pending proposals. An operator either accepts one into the graph, flagged as proposed so its provenance stays visible, or rejects it into a history that is never retrieved. Nothing a model invented enters retrieval on its own.

Completed analyses and runbooks Extract agent single call, JSON out Proposals pending review Accepted → the graph flagged “proposed”, never silent Rejected → history kept, but never retrieved a named actor decides, and the decision is final
Every proposal carries its rationale grounded in the source text, the source reference and the model that produced it. Validation runs before review, not instead of it: types must be in the vocabulary, entity names must be names rather than sentences, and each relationship must respect its domain and range — remediates joins an episode to an incident, observed_in joins a pattern to a service and nothing else. This is the Action Engine's approval gate, one layer down at the data.

The Action Engine

The one path to changing anything. A plan is an artifact, not a conversation: what the planner wrote is what an operator approves and what the executor runs, with no gap between them.

draft pending_approval approved executing rejected completed failed rolled_back has steps operator approves — the actor is recorded execute, or dry-run operator rejects all steps succeeded nothing to roll back the rollback chain ran
Dashed states are terminal, and a failed step reaches one of two of them depending on whether a rollback chain existed. The executor re-validates everything the API already checked — tool existence, dependency order, the approval gate — resolves {{step.field}} output references, persists every transition, and on failure runs the rollback chain in reverse order.
Risk classMeaningGate
readobserves, changes nothingruns autonomously
reversible_writechanges something with a defined undoneeds approval
destructivecannot be undoneneeds two different people
  • Risk floors are clamped at parse time. A tool that can write cannot be smuggled into a plan labelled as a read.
  • Dry run is real. Read steps execute for actual; write steps resolve their arguments and report what they would have invoked. Nothing is persisted, and failures still surface.
  • Rollback runs on failure, each step's own undo, in reverse.
  • Two-person control counts credentials, not names. One person holding two tokens is one approver. It used to count display names, and one human with ayse-cli and ayse-web could pass the gate alone.
  • Where an environment is armed, cleared plans run unattended — three a pass, oldest first, with the policy re-evaluated at execution rather than trusted from creation.

Where tools come from

External MCP servers reach the platform through a client hub whose mode gate decides which catalog a tool may enter: a read-only server's tools join investigations, a write-capable server's tools are reachable only through an approved plan. The native cloud passthroughs (run_aws, run_gcloud, run_az) and the on-prem toolset feed both catalogs directly. A verb allowlist compiled into the binary decides what a passthrough may run — never the model.

EXTERNAL MCP SERVERS pgEdge Postgres MongoDB official awslabs AWS API Azure azmcp Kubernetes MySQL, MSSQL, Oracle, Redis, GCP each declares a scope: provider / account / region MCP CLIENT HUB discovery + namespacing mcp_<server>_<tool> mode gate readonly → analyze catalog write → Action Engine only Analyze catalog read-only, no approval needed Action Engine catalog every write passes the gate Native on-prem toolset — no MCP Curated SSH diagnostics and remediations under strict per-host allowlists. There is deliberately no arbitrary-shell tool; a rejected call never reaches an SSH dial. stdio / HTTP
Each server's declared scope is stamped onto every plan step that uses its tools, so a multi-cloud plan tells the approving operator, per step, where it acts. Connection failures are per-server and never block boot; the Integrations page shows live state with one-click reconnect.

Claim and result

A finding is a problem the platform found in the estate, tracked from discovery to a measured outcome. The lifecycle exists to keep one distinction honest — the gap between "a plan completed" and "the problem is gone".

Scope inventory Inspect agent Finding: open ActionPlan Approval Execution Finding: resolveda claim Inspect agent: verifyre-reads the scope Finding: verifieda result, with an outcome draft a remediation completed re-read the scope evidence confirms it problem still present
Only verified outcomes count as demonstrated benefit. A finding an assessment sees again after it was resolved is flagged needs_reverify — a question, not a verdict. A broad sweep does not compare against the evidence recorded when the finding was created, so it is not entitled to overturn a verified result; the proven outcome stands until a verification answers the question, and only a verification may declare a regression.
Only a verification may change an outcome. An assessment is a broad sweep that does not compare against the evidence recorded at discovery, so when it sees a fixed problem again it may only raise a question — never overturn the result. Letting an unverified judgement beat a verified one means a single model slip erases a demonstrated win.

Re-running an assessment updates the finding it already has, matched on scope and title, rather than creating a second one. That property is what makes the sweep safe to put on a timer — and verification runs on a schedule too, so "resolved" becomes "verified" without anybody remembering to check.

The loop closes

Executed plans become episodes that feed recall back into analysis and planning, and can be drafted into runbooks that join the corpus the analyze agent searches. Successes teach what to repeat; rollbacks teach what to avoid.

Incident Analysis Plan Approval Execution Episode store Runbook draft Runbook corpus terminal state recall_similar_incidents + past_episodes in the prompt completed or rolled back joins find_runbook RAG
Recall is lexical by design — service match dominates, token overlap contributes, recency breaks ties — so it works with zero configuration and its ranking is explainable. Successes teach the planner what to repeat; rollbacks teach it what to avoid.

The estate

A cloud account's problems are relational, and relational problems are invisible in a per-resource listing. sudoX draws the estate as a typed graph, keeps every snapshot, and compares consecutive ones to answer what changed.

Fixed probe list compiled in, never chosen by a model Deterministic parsers AWS / Azure / GCP drivers, each modelled as it is Typed graph provider-agnostic Nested-box architecture diagram one claim: what the internet can reach Inspect agent reads the finished graph Observations written nowhere until an operator accepts
Accepting an observation turns it into an ordinary finding, which then flows through the same plan → approve → execute → verify path as everything else. The review is proposal-first at every layer of this platform, and this is the third place it appears.
  • The probe list is compiled in. A fixed set of describe/list calls per provider, never a command a model chose — otherwise "read-only" is a hope about the model rather than a property of the code.
  • A failed probe is shown, not hidden. An unreadable subsystem must not render identically to an absent one.
  • Drift is deterministic. If it says a security group opened at 14:20, that is a fact rather than an inference — and exposure changes are ordered above every other kind of change.
  • The first look runs once per scope. Connecting a cloud draws it, derives what to watch, assesses it and reads its bill — and "once" means "there is no snapshot for it yet", so a reconcile that fires on every edit cannot double-spend.

Knowledge & memory

StoreHoldsRead by
episodesevery executed plan and how it endedthe planner, so it repeats what worked
runbookswritten procedures, searchable by meaninginvestigations, grounded in your own docs
graphservices, incidents, hosts, environments and how they relateboth, for connected evidence chains

The graph is built deterministically from records the platform already trusts. An extraction agent can propose additions, but those are proposals until someone accepts them.

Configuration

Five files, each owning one concern. Everything that is a decision people revisit — the mode, the sweep schedule, what is watched, which environments exist — lives in the console instead, so changing it does not need a deploy.

FileDeclares
config.yamlthe agent, storage, notification channels, on-call, a model per task
agent_sources.yamlwhich log backends to read, and which environment each watches
metric_signals.yamlwhich PromQL to learn baselines for, and the CloudWatch / Cloud Monitoring backends beside it
mcp_servers.yamlwhich providers and databases to connect, and their trust mode
onprem.yamlthe SSH inventory and per-host allowlists
← How it works Who can do what →