Data processing: what happens to cluster data before and after the model¶
The agent does not pass raw cluster data to the model. Most data is read, filtered, grouped, summarised or cut in Python first, and the model's answers are checked and normalised before anything is stored or posted. This page lists every such step, and also what is not done, so that a security or privacy review can rely on it.
The flow:
cluster / Prometheus / Alertmanager
│ read (read-only; Secrets never requested)
▼
① reduce in Python summaries, filters, grouping, formatting (tools)
▼
② cap the size every tool result ≤ TOOL_OUTPUT_MAX_CHARS (agent loop)
▼
③ the request system prompt + conversation + tool results ──▶ the model (any provider)
▼ │
④ check the answer findings JSON parsed, validated, normalised ◀─────┘
▼
⑤ compute facts new / still open / resolved, the night's story (memory, report)
▼
⑥ shape the output Markdown report, Teams card shrunk to fit (teams)
Plus one path where no data reaches the model at all: the "anything changed?" check of scheduled runs.
① Reduction in Python, per tool¶
| Tool | Raw data read | What Python does before the model sees it | Typical result |
|---|---|---|---|
cluster_problems |
Full JSON of all nodes, pods, Deployments, StatefulSets, DaemonSets, PDBs, ResourceQuotas, PVCs, Jobs (a large test cluster: ~1,250 pods, 615 Deployments, ~400 PDBs, ~570 quotas) | Evaluates health rules per object; keeps only unhealthy ones; groups pods by owning workload (one line per Deployment, not per pod); parses quantities (4Gi, 500m) to compute quota usage; keeps failed Jobs of the last 24 h only; at most 50 lines per section; adds counts of what was checked |
787 characters |
warning_events |
Warning events | The API server drops the reasons in EVENTS_EXCLUDE_REASONS (default PolicyViolation) before they are sent; Python keeps only events within the time window, groups them by (namespace, object, reason), sums the counts, keeps the latest time and message, collapses whitespace, cuts each message to 200 characters, sorts by frequency, keeps at most 60 groups |
test cluster: 3,710 events → 207 in 18 groups, 3.3 KB |
prometheus_query |
Prometheus JSON | One line per series: name{labels} value, numbers in short form |
a few lines |
prometheus_query_range |
All samples of a range | Each series reduced to 6 numbers: points, first, last, min, max, and the time range. The raw samples never reach the model | one line per series |
alertmanager_alerts |
Alert JSON | Only active, not silenced, not inhibited alerts; one line each: name, severity, namespace/pod/node/job, start time, the summary (or description) annotation; most severe first. Other labels and annotations are dropped |
one line per alert |
| MCP tools | Text from kubernetes-mcp-server | Text blocks passed through; non-text content replaced by [… content omitted]; errors prefixed with ERROR: and advice |
as returned |
kubectl |
kubectl's output | Passed through; stderr appended under [stderr]; failures and timeouts become ERROR: text |
as returned |
load_skill |
A skill's Markdown | Passed through | 2–5 KB |
For the morning report's history, two more reductions happen without the model:
- Alerts that fired overnight come from one Prometheus query that already
aggregates (max by (alertname, severity, namespace, pod, node, job)); Python then
drops always-firing alerts (ALERTS_IGNORE_NAMES), info-level alerts and those still
firing.
- Findings history is computed from memory (below); only the result is sent.
② Size cap on every tool result¶
Whatever a tool returns, the agent loop cuts it to TOOL_OUTPUT_MAX_CHARS (default
8,000) before it is added to the conversation: two thirds from the head, one third
from the tail, and a marker in between telling the model to narrow the query. The head
of a listing has the columns and first rows; the tail of a log has the latest lines.
This protects the context window and the cost: every later model call resends the
whole conversation.
③ What the request to the model contains¶
| Part | Content | Processing |
|---|---|---|
| System prompt | Rules, environment description, cluster notes, learned notes, skill catalog | Assembled per run; learned notes cut to the newest NOTES_MAX_CHARS (8,000) at a line boundary (how-the-agent-learns.md) |
| The task | A question, or the health-check request with the previously open findings (fingerprint, severity, title, since when) | Built by healthcheck.build_check_prompt() |
| Tool definitions | Names, descriptions, argument schemas | — |
| Conversation | The model's earlier messages and the tool results (after ① and ②) | — |
The morning report's overview is a separate request without tools: the model gets only the facts computed in ⑤ (as Markdown) and writes 3–6 sentences. It cannot look anything up, so it can only summarise what Python gave it.
④ Checking the model's answer¶
The model's output is never trusted as-is:
- Findings JSON. The answer of a health check must end with a JSON block. It is
parsed and validated: it must be an object with a
findingslist, and every finding must have a fingerprint. An unknown severity becomeswarning(logged); a missing title falls back to the fingerprint. Invalid → one follow-up asking only for the JSON; still invalid → the run is recorded as failed and memory is not changed. - Fingerprints are normalised (lower case, spaces around
/removed, words joined with-), because the model does not always spell them the same way. - Duplicate fingerprints in one answer are merged, keeping the most severe one.
- Learned notes (
save_note): whitespace and newlines collapsed to one line; rejected if empty or longer than 500 characters; logged at WARNING. - Tool calls: unknown tools, invalid JSON arguments and invalid values
(namespace, event reason, time window, kubectl flags) are refused with
ERROR:text before anything runs.
⑤ Facts computed in Python, not by the model¶
| Fact | How |
|---|---|
| New / still open / resolved | Memory compares the current fingerprints with the open ones (Memory.apply_check()) |
| The morning report's window | Since the previous morning report, max 72 h (report_window_start()) |
| New overnight, resolved overnight, came and went | Queries on first_seen / resolved_at in the window |
| Checks run / skipped / failed | Counted from the run history |
| "Anything changed?" for scheduled checks | A SHA-256 hash of the problem lines of cluster_problems and the alerts' identity (name, severity, location; not their start time or text). Compared locally; nothing is sent to the model |
The model never decides what is "new" or "resolved"; it only reports what it sees now.
⑥ Shaping the output¶
- Markdown reports (stdout, history): rendered from the parsed findings and the facts, not copied from the model's free text (except the summary/overview).
- Teams cards: built from the same data; if larger than 27,000 bytes, details are shortened step by step and lists cut with "… and N more".
- Logs: tool calls are logged with their arguments and output size; full outputs only at DEBUG. The webhook URL is never logged.
What is NOT done¶
Being explicit matters here:
- No redaction or masking. Text that is passed through (logs from
pods_logorkubectl logs, event messages, ConfigMap contents, annotations, describe output) reaches the model as it is. If an application writes personal data or a credential into its logs, the model can see it, and it can end up in an answer, the history and Teams. Secrets themselves are never read (RBAC, MCP denied resources, the kubectl guard). - No anonymisation. Object names, namespaces, node names and IP addresses in messages are sent unchanged; the model needs them to investigate.
- No local filtering of what the model asks for. Within the read-only tools, the model chooses what to read (for example which pod's logs). Limits apply (tail, size cap), content filtering does not.
Mitigations: logs are read only when investigating a specific problem, with a tail;
you choose the provider, so read its data and retention terms
(list of links) or run the model inside
your network (Ollama or another OpenAI-compatible server); the Responses API (Foundry,
OpenAI) is called with store=false, so no conversation is stored by the provider;
with Azure AI Foundry the model runs in a dedicated resource in the EU data zone and
does not train on prompts; Secrets are out of reach; keep credentials out of logs and ConfigMaps
(good practice anyway). If redaction is needed later, the natural place is the agent
loop's step ②, where every tool result passes. See security.md.
Where this lives in the code¶
| Step | Code |
|---|---|
| ① | tools/cluster_tools.py, tools/prometheus_tool.py, tools/mcp_tools.py (_result_to_text), tools/kubectl_tool.py (run_kubectl) |
| ② | tools/registry.py (truncate_output), called in agent.py |
| ③ | agent.py (build_system_prompt), healthcheck.py (build_check_prompt), report.py (OVERVIEW_PROMPT) |
| ④ | findings.py (parse_findings, normalize_fingerprint), healthcheck.py (run_check), memory.py (add_note) |
| ⑤ | memory.py (apply_check, findings_changed_since), report.py, healthcheck.py (sweep_signature) |
| ⑥ | healthcheck.py (render_check_report), report.py, teams.py |