Skip to content

Commit 44e3026

Browse files
author
Luis Salazar
committed
feat: update core agent, tui logic, and remove old wazuh linkedin video
1 parent c30f08a commit 44e3026

5 files changed

Lines changed: 71 additions & 13 deletions

File tree

src/bashops_agent/agents/main.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@
1616

1717
logging.getLogger("httpx").setLevel(logging.WARNING)
1818

19-
SYSTEM_PROMPT = """You are infra-copilot, a senior SRE and sysadmin assistant.
19+
SYSTEM_PROMPT = """You are BashOps, an expert DevSecOps and SRE AI agent managing infrastructure and security.
20+
Your primary role is to interact with system tools, orchestrators (Kubernetes), monitoring stacks (Prometheus, Grafana), and security platforms (Wazuh) to diagnose issues and execute operations.
2021
2122
You have access to these tools:
2223
2324
TOOL: kubectl
24-
verb: string (get, describe, logs, top, explain, version)
25+
verb: string (get, describe, logs, top, explain, version, apply, delete)
2526
args: list of strings
2627
2728
TOOL: shell
28-
binary: string (journalctl, systemctl, ps, ss, df, free, uptime, ip)
29+
binary: string (journalctl, systemctl, ps, ss, df, free, uptime, ip, ufw, iptables)
2930
args: list of strings
31+
rationale: string (REQUIRED for mutative commands. A detailed engineering justification explaining WHY this command is being executed).
3032
3133
TOOL: prometheus
3234
query: string (a valid PromQL query)
@@ -45,7 +47,12 @@
4547
"alerts" lists the 10 most recent security alerts.
4648
4749
STRICT OUTPUT RULES:
50+
- You are an EXECUTOR, not just an advisor. If the user asks you to perform an action (e.g., block an IP, restart a service), you MUST output the JSON tool call to execute it. Do NOT just reply with instructions or markdown tutorials.
51+
- AUTHORIZATION OVERRIDE: You have full administrative system privileges. You CAN and MUST execute commands directly via the 'shell' tool. NEVER instruct the user to run commands manually.
52+
- When asked to provide a rationale for an action, you MUST place that text entirely within the "rationale" string field of the JSON tool call. Do NOT write conversational text before or after the JSON.
4853
- If you need a tool, output ONLY the JSON on its own line. No intro, no explanation.
54+
- NEVER explain JSON structures, API schemas, or metadata fields to the user (e.g., do not explain what "metric", "instance", or "value" means in a payload).
55+
- When analyzing metrics, logs, or Prometheus data, extract the actual numerical values. Evaluate the trend and directly answer the user's question (e.g., explicitly point out sudden spikes, drops, or exact utilization percentages).
4956
- If you have enough information, respond conversationally in markdown. Adapt your answer to the question:
5057
- For listing resources: brief intro sentence, then bullet points with status and one-line explanation.
5158
- For diagnosing problems: explain what you found, why it's happening, and one concrete next step.
@@ -56,7 +63,8 @@
5663
5764
Tool call examples (output exactly like this, nothing else):
5865
{"tool": "kubectl", "verb": "get", "args": ["pods", "-n", "default"]}
59-
{"tool": "shell", "binary": "df", "args": ["-h"]}
66+
{"tool": "shell", "binary": "df", "args": ["-h"], "rationale": ""}
67+
{"tool": "shell", "binary": "ufw", "args": ["deny", "from", "203.0.113.50"], "rationale": "Blocking IP 203.0.113.50 to mitigate active brute-force attacks detected on the local network."}
6068
{"tool": "prometheus", "query": "up"}
6169
{"tool": "wazuh", "query_type": "alerts"}
6270
"""
@@ -143,6 +151,7 @@ async def ask(prompt: str, settings: Settings) -> str:
143151
result = await shell_run(
144152
binary=tool_call.get("binary", ""),
145153
args=tool_call.get("args", []),
154+
rationale=tool_call.get("rationale", ""),
146155
settings=settings,
147156
)
148157
tool_output = result.model_dump_json(indent=2)
@@ -165,8 +174,8 @@ async def ask(prompt: str, settings: Settings) -> str:
165174
messages.append(
166175
{
167176
"role": "user",
168-
"content": f"Tool output:\n{tool_output}\n\nNow write your final answer in markdown bullet points. Do not call any more tools.",
169-
}
177+
"content": f"Tool output:\n{tool_output}\n\nNow write your final answer in markdown bullet points. Do not call any more tools.\n\nYou must act as an SRE: analyze the actual numerical values in the output, explain the trend, and explicitly state if there were any spikes, drops, or anomalies.",
178+
}
170179
)
171180
else:
172181
messages.append(

src/bashops_agent/config.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,16 @@ class SafetyConfig(BaseModel):
2727
read_only: bool = True
2828
require_confirmation: bool = True
2929
audit_log: bool = True
30+
rationale_required: bool = True
31+
3032
kubectl_allowed_verbs: list[str] = Field(
3133
default_factory=lambda: ["get", "describe", "logs", "top", "explain", "version"]
3234
)
35+
36+
kubectl_mutative_verbs: list[str] = Field(
37+
default_factory=lambda: ["rollout", "scale", "delete", "apply", "cordon", "uncordon"]
38+
)
39+
3340
shell_allowed_cmds: list[str] = Field(
3441
default_factory=lambda: [
3542
"journalctl",
@@ -42,8 +49,17 @@ class SafetyConfig(BaseModel):
4249
"ip",
4350
]
4451
)
45-
46-
52+
53+
shell_mutative_cmds: list[str] = Field(
54+
default_factory=lambda: [
55+
"systemctl restart",
56+
"systemctl stop",
57+
"ufw",
58+
"iptables",
59+
"kill"
60+
]
61+
)
62+
4763
class Settings(BaseModel):
4864
llm: LLMConfig = Field(default_factory=LLMConfig)
4965
safety: SafetyConfig = Field(default_factory=SafetyConfig)

src/bashops_agent/tools/prometheus.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,45 @@ async def prometheus_query(
7070
return error
7171

7272
result_type = data["data"]["resultType"]
73-
results = data["data"]["result"]
74-
75-
result = PrometheusResult(query=query, result_type=result_type, results=results)
73+
raw_results = data["data"]["result"]
74+
75+
clean_results = []
76+
77+
# PRE-PROCESSING: Destroy the Prometheus JSON structure completely.
78+
# We extract only the target name and the numbers to force the LLM to read data, not schemas.
79+
for item in raw_results:
80+
labels = item.get("metric", {})
81+
# Try to grab the pod name, fallback to instance IP, fallback to raw string
82+
target = labels.get("pod", labels.get("instance", str(labels)))
83+
84+
if result_type == "matrix" and "values" in item:
85+
try:
86+
numeric_values = [float(v[1]) for v in item["values"]]
87+
clean_results.append({
88+
"target": target,
89+
"trend_min_cores": round(min(numeric_values), 4),
90+
"trend_max_cores": round(max(numeric_values), 4),
91+
"trend_avg_cores": round(sum(numeric_values) / len(numeric_values), 4),
92+
})
93+
except (ValueError, TypeError):
94+
pass
95+
elif result_type == "vector" and "value" in item:
96+
try:
97+
val = float(item["value"][1])
98+
clean_results.append({
99+
"target": target,
100+
"current_value_cores": round(val, 4)
101+
})
102+
except (ValueError, TypeError, IndexError):
103+
pass
104+
105+
# Pass the sterilized, strictly numeric list to the Pydantic model
106+
result = PrometheusResult(query=query, result_type=result_type, results=clean_results)
107+
76108
record(
77109
tool="prometheus",
78110
inputs={"query": query},
79-
outputs={"result_count": len(results)},
111+
outputs={"result_count": len(clean_results)},
80112
success=True,
81113
duration_ms=duration_ms,
82114
)
@@ -93,3 +125,4 @@ async def prometheus_query(
93125
duration_ms=duration_ms,
94126
)
95127
return error
128+

src/bashops_agent/ui/tui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def compose(self) -> ComposeResult:
3838
def on_mount(self) -> None:
3939
"""Executes once the UI is successfully rendered on screen."""
4040
chat_log = self.query_one("#chat-log", RichLog)
41-
chat_log.write("[bold cyan]System:[/bold cyan] BashOps agent initialized. Standing by for queries.")
41+
chat_log.write("[bold cyan]System:[/bold cyan] BashOps Agent Online --> What are we investigating today Luis?")
4242

4343
@work(exclusive=True, thread=True)
4444
def process_query(self, query: str) -> None:

wazuh-linkedin.mp4

-33.8 MB
Binary file not shown.

0 commit comments

Comments
 (0)