From 0d99a39d6f4af26c019b97e5fd19e5ea80ad7344 Mon Sep 17 00:00:00 2001 From: Ahmad Hammad Date: Thu, 25 Jun 2026 22:55:24 +0300 Subject: [PATCH 1/4] feat(inventory): publish introspected tool/permission inventory + honest cloud tiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a trust artifact that exposes exactly what the agent can do, sourced live from the code so it never drifts: - `opendevops_core.agent.inventory.build_inventory()` introspects `ALL_TOOLS` (name, description, type-hinted params), the bash command allowlist, the AWS read-permission matrix, and per-provider capability tiers. - Read-only `GET /api/inventory` endpoint (SPA-safe `/api/` prefix). - Generated `apps/documentation/tool_inventory.md` via `scripts/gen_tool_inventory.py` (never hand-edited). To keep these non-drifting and behavior-preserving: - Promote the kubectl/docker allowlist sets to module constants in `bash_tool.py` (`_KUBECTL_SUBCOMMANDS`, `_DOCKER_SUBCOMMANDS`) — same values, single source. - Refactor `permissions.check_permissions()` to iterate a declarative `PERMISSION_PROBES` table — identical behavior, now introspectable. Honest multi-cloud positioning in README + the doc page: AWS complete (20 structured tools + CLI), Azure CLI + 4 runbook skills (no structured SDK tools, no event-driven/polling loop), GCP not implemented (stub returns no tools). Corrects stale tool counts (actual: 26 total / 20 AWS structured). Tests assert the endpoint reflects `ALL_TOOLS`, the allowlist constants, the permission probes, and the honest provider tiers. Frontend Settings/Trust panel deferred as a follow-up (large file, styling risk); endpoint + doc page cover the artifact. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 1 + README.md | 18 +- apps/backend/scripts/gen_tool_inventory.py | 142 +++++++++ apps/backend/src/api/app.py | 2 + apps/backend/src/api/routers/inventory.py | 19 ++ apps/backend/tests/test_api/test_inventory.py | 110 +++++++ .../tests/test_tools/test_permissions.py | 24 ++ .../src/opendevops_core/agent/inventory.py | 150 +++++++++ .../providers/aws/permissions.py | 30 +- .../src/opendevops_core/tools/bash_tool.py | 9 +- apps/documentation/tool_inventory.md | 293 ++++++++++++++++++ 11 files changed, 784 insertions(+), 14 deletions(-) create mode 100644 apps/backend/scripts/gen_tool_inventory.py create mode 100644 apps/backend/src/api/routers/inventory.py create mode 100644 apps/backend/tests/test_api/test_inventory.py create mode 100644 apps/backend/tests/test_tools/test_permissions.py create mode 100644 apps/core/src/opendevops_core/agent/inventory.py create mode 100644 apps/documentation/tool_inventory.md diff --git a/AGENTS.md b/AGENTS.md index 4a4d94a..1418983 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,7 @@ to core are live without republishing. - **New env var:** if core reads it, add the Pydantic field to `CoreSettings` in `apps/core/src/opendevops_core/config.py`; if it's web/auth-only, add it to `Settings(CoreSettings)` in `apps/backend/src/config/appsettings.py`. Either way mirror it in `.env.example` (with a comment). Never read env vars directly — always go through `settings`. - **New DB column or table:** add a new numbered migration. Core-domain schema (tables core code reads/writes) goes in `apps/core/src/opendevops_core/migrations/` (e.g. `014_name.sql`); OSS-app-only schema goes in `apps/backend/migrations/`. The runner applies core-then-app, tracked in the `schema_migrations(source, version)` ledger. Never add columns inline in Python code. - **New tool:** add it to `ALL_TOOLS` in `apps/core/src/opendevops_core/agent/core.py`. Tool functions must be plain synchronous Python functions — DeepAgents infers the JSON schema from type hints and docstrings. +- **Published tool/permission inventory:** the trust artifact (read-only `GET /api/inventory` + the generated `apps/documentation/tool_inventory.md`) is built **by introspection** in `opendevops_core.agent.inventory.build_inventory` — it never hand-maintains a list. Its sources of truth are `ALL_TOOLS`, the bash allowlist frozensets in `tools/bash_tool.py` (`_AWS_READONLY_VERBS`, `_AZ_READONLY_VERBS`, `_KUBECTL_SUBCOMMANDS`, `_DOCKER_SUBCOMMANDS`, blocked flags), and `providers/aws/permissions.py:PERMISSION_PROBES`. After changing any of those, regenerate the doc: `cd apps/backend && uv run python scripts/gen_tool_inventory.py`. Never edit `tool_inventory.md` by hand. Note: the introspected counts are **26 total tools / 20 structured AWS tools** (CloudWatch 6, CloudTrail 1, ECS 4, Lambda 3, EC2 2, RDS 2, IAM 2) — the "27 / 21" figures elsewhere in this file are stale; trust the inventory. - **New API route that matches a React Router path:** prefix it with `/api/` to avoid the SPA fallback conflict. The `/{full_path:path}` catch-all in `apps/backend/src/api/app.py` intercepts any GET that matches a registered FastAPI route first. - **New skill:** drop a `SKILL.md` file into `apps/core/src/opendevops_core/skills//SKILL.md`. It is picked up automatically at startup (and bundled into the core wheel) — no code changes needed. Use the frontmatter format (`name`, `description`) from the existing `lambda-throttling` skill. - **Docs sync:** if a feature has a corresponding file in `apps/documentation/`, update it when the feature changes. The `apps/documentation/` folder is the public documentation source. diff --git a/README.md b/README.md index 7ef5a5c..99724d5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ On a **reproducible 10-incident suite** (real AWS + Azure resources, scored agai | | **OpenDevOps** | **AWS DevOps Agent / Q Developer** | |---|---|---| | **LLM** | Any (LiteLLM, Claude Code, Ollama) | Bedrock-managed only | -| **Cloud coverage** | AWS + Azure (more coming) | AWS only | +| **Cloud coverage** | AWS (structured tools + CLI) + Azure (CLI + skills) | AWS only | | **Data location** | Your DB / VPC | AWS-managed, not portable | | **Customization** | Open source — modify anything | Closed product | | **Pricing** | LLM at retail (or $0 via Ollama / Claude Code) | Per-investigation + Bedrock markup | @@ -51,11 +51,23 @@ On a **reproducible 10-incident suite** (real AWS + Azure resources, scored agai **When AWS is the better pick:** if you're 100% AWS, never plan to leave, and want zero infrastructure to run, Amazon Q Developer's native Console integration and AWS-only signals (Trusted Advisor, AWS Config, Compute Optimizer) are hard to beat. OpenDevOps is for everyone else. +### Cloud coverage — honest tiers + +Multi-cloud means different things per provider. Here is exactly where each cloud stands today (numbers are introspected from the code and published in the [tool inventory](apps/documentation/tool_inventory.md)): + +| Cloud | Structured SDK tools | CLI investigation (`bash` tool) | Runbook skills | Event-driven + polling | Status | +|---|---|---|---|---|---| +| **AWS** | 20 (CloudWatch, CloudTrail, ECS, Lambda, EC2, RDS, IAM) | `aws` (read-only verbs) | ✅ | ✅ EventBridge → SQS + metric poller | **Complete** | +| **Azure** | 0 | `az` + `kubectl` (AKS) | ✅ 4 (AKS, App Service, Monitor/KQL, VM) | ❌ | **CLI + skills** | +| **GCP** | 0 | — | — | ❌ | **Not implemented** (stub returns no tools) | + +AWS is the complete path (structured tools **and** CLI). Azure is investigated CLI-first through the read-only `az`/`kubectl` allowlist plus runbook skills — there are **no** structured Azure SDK tools and **no** autonomous detection loop. GCP is a stub: the provider loads and returns zero tools. + ## What's inside - **LangChain DeepAgents** as the agent framework — planning, tool orchestration, and session memory out of the box -- **21 read-only AWS tools** across CloudWatch (6), CloudTrail (2), ECS (4), Lambda (4), EC2 (2), RDS (2), IAM (1), plus bash escape hatch, cross-session history analytics, skills, and `submit_investigation` — plain Python functions, schemas inferred automatically -- **Azure support (CLI-first)** — investigates Azure through the read-only `az` CLI + `kubectl` (for AKS) and a set of Azure runbook skills (AKS debugging, App Service errors, Azure Monitor/KQL, VM diagnostics) — no separate SDK tools needed. Read-only; connect via a service principal or `az login` — see [apps/documentation/azure_setup.md](apps/documentation/azure_setup.md) +- **20 read-only AWS tools** across CloudWatch (6), CloudTrail (1), ECS (4), Lambda (3), EC2 (2), RDS (2), IAM (2), plus bash escape hatch, cross-session history analytics, skills, and `submit_investigation` — plain Python functions, schemas inferred automatically. The complete, always-current list (with parameters, the bash allowlist, and the AWS permission matrix) is published as a trust artifact — see [apps/documentation/tool_inventory.md](apps/documentation/tool_inventory.md) or the read-only `GET /api/inventory` endpoint +- **Azure support (CLI-first)** — investigates Azure through the read-only `az` CLI + `kubectl` (for AKS) and a set of Azure runbook skills (AKS debugging, App Service errors, Azure Monitor/KQL, VM diagnostics) — **no structured SDK tools and no event-driven/polling loop** (those are AWS-only). Read-only; connect via a service principal or `az login` — see [apps/documentation/azure_setup.md](apps/documentation/azure_setup.md) - **Sandboxed bash execution tool** — agent can run whitelisted read-only AWS CLI (`aws`), Azure CLI (`az`), kubectl, and docker commands as a last resort when the structured tools fall short; every command validated against an allowlist before execution; never uses `shell=True`; hard 30-second timeout - Includes **CloudWatch Logs Insights** (`query_logs_insights`) — full query language support: `fields`, `filter`, `stats`, `sort`, `limit`; results include scanned MB - **Streaming responses** — FastAPI SSE endpoint streams agent tokens in real time as the LLM reasons; tool calls appear as they complete diff --git a/apps/backend/scripts/gen_tool_inventory.py b/apps/backend/scripts/gen_tool_inventory.py new file mode 100644 index 0000000..31183a2 --- /dev/null +++ b/apps/backend/scripts/gen_tool_inventory.py @@ -0,0 +1,142 @@ +"""Generate apps/documentation/tool_inventory.md from the live introspected inventory. + +This page is generated, never hand-edited — it is sourced from the same +``build_inventory()`` introspection that backs the ``/api/inventory`` endpoint, so the +docs and the runtime can never disagree. Regenerate after changing tools, the bash +allowlist, or the AWS permission probes: + + cd apps/backend && uv run python scripts/gen_tool_inventory.py +""" + +from __future__ import annotations + +from pathlib import Path + +from opendevops_core.agent.inventory import build_inventory + +_OUT = Path(__file__).resolve().parents[2] / "documentation" / "tool_inventory.md" + + +def _param_cell(p: dict) -> str: + req = "required" if p["required"] else f"`{p['default']!r}`" + type_str = p["type"].replace("|", "\\|") # don't break the markdown table + return f"| `{p['name']}` | `{type_str}` | {req} |" + + +def _render(inv: dict) -> str: + out: list[str] = [] + out.append("# Tool & Permission Inventory") + out.append("") + out.append( + "> **Generated file — do not edit by hand.** Produced by " + "`apps/backend/scripts/gen_tool_inventory.py` from the live code " + "(`opendevops_core.agent.inventory.build_inventory`), the same source that backs " + "the read-only `GET /api/inventory` endpoint. Regenerate with " + "`cd apps/backend && uv run python scripts/gen_tool_inventory.py`." + ) + out.append("") + out.append( + "This is the trust artifact: exactly what the agent can inspect — every registered " + "tool and its parameters, the read-only bash command allowlist, the AWS " + "read-permission probe, and the per-cloud capability tiers. Everything is read-only." + ) + out.append("") + + out.append("## Capability by cloud") + out.append("") + out.append( + "| Cloud | Structured SDK tools | CLI access (`bash` tool) | Event-driven + polling |" + ) + out.append("|---|---|---|---|") + for p in inv["providers"]: + active = " (active)" if p["active"] else "" + out.append( + f"| **{p['name'].upper()}**{active} | {p['structured_tools']} | " + f"{'yes' if p['cli_access'] else 'no'} | " + f"{'yes' if p['event_driven_and_polling'] else 'no'} |" + ) + out.append("") + out.append( + f"Active provider: **{inv['active_provider']}** · " + f"total registered tools: **{inv['tool_count']}**." + ) + out.append("") + + out.append("## Registered tools") + out.append("") + # Group by module for readability, preserving registration order within a group. + seen: list[str] = [] + by_mod: dict[str, list[dict]] = {} + for t in inv["tools"]: + mod = t["module"].split(".")[-1] + if mod not in by_mod: + by_mod[mod] = [] + seen.append(mod) + by_mod[mod].append(t) + for mod in seen: + out.append(f"### `{mod}`") + out.append("") + for t in by_mod[mod]: + out.append(f"#### `{t['name']}` → `{t['returns']}`") + out.append("") + if t["description"]: + out.append(t["description"]) + out.append("") + if t["parameters"]: + out.append("| Param | Type | Default |") + out.append("|---|---|---|") + for p in t["parameters"]: + out.append(_param_cell(p)) + else: + out.append("*No parameters.*") + out.append("") + + out.append("## Bash command allowlist") + out.append("") + bash = inv["bash_allowlist"] + out.append( + f"`run_bash_command` runs only read-only commands, validated against this allowlist " + f"before execution. Shell chaining is **{bash['shell_chaining']}**, `shell=True` is " + f"never used, output is capped at {bash['max_output_chars']} chars, and every command " + f"has a hard {bash['timeout_seconds']}s timeout." + ) + out.append("") + + def _verbs(items: list[str]) -> str: + return ", ".join("`" + v + "`" for v in items) or "none" + + aws = bash["aws"] + out.append( + f"- **aws** — {aws['note']}. Read-only verbs: {_verbs(aws['readonly_verbs'])}. " + f"Blocked global flags: {_verbs(aws['blocked_global_flags'])}." + ) + out.append( + f"- **az** — {bash['az']['note']}. Read-only verbs: {_verbs(bash['az']['readonly_verbs'])}." + ) + out.append(f"- **kubectl** — subcommands: {_verbs(bash['kubectl']['subcommands'])}.") + out.append(f"- **docker** — subcommands: {_verbs(bash['docker']['subcommands'])}.") + out.append("") + + out.append("## AWS read-permission matrix") + out.append("") + out.append( + "One lightweight read call per service verifies the agent's credentials " + "(surfaced by the in-app permission checker)." + ) + out.append("") + out.append("| Service | boto3 client | Read operation |") + out.append("|---|---|---|") + for r in inv["aws_permission_matrix"]: + out.append(f"| {r['service']} | `{r['boto3_service']}` | `{r['operation']}` |") + out.append("") + return "\n".join(out) + + +def main() -> None: + inv = build_inventory() + _OUT.write_text(_render(inv), encoding="utf-8") + print(f"Wrote {_OUT} ({inv['tool_count']} tools)") + + +if __name__ == "__main__": + main() diff --git a/apps/backend/src/api/app.py b/apps/backend/src/api/app.py index 7687826..a8e6294 100644 --- a/apps/backend/src/api/app.py +++ b/apps/backend/src/api/app.py @@ -23,6 +23,7 @@ evidence, history, integrations, + inventory, monitoring, sessions, settings, @@ -195,6 +196,7 @@ async def lifespan(_app: FastAPI): app.include_router(integrations.router) app.include_router(init_router.router) app.include_router(monitoring.router) +app.include_router(inventory.router) _DIST = Path(__file__).resolve().parents[3] / "frontend" / "dist" if not _DIST.is_dir(): diff --git a/apps/backend/src/api/routers/inventory.py b/apps/backend/src/api/routers/inventory.py new file mode 100644 index 0000000..131ba81 --- /dev/null +++ b/apps/backend/src/api/routers/inventory.py @@ -0,0 +1,19 @@ +"""Tool / permission inventory endpoint — a read-only trust artifact. + +Exposes exactly what the agent can do (registered tools + their parameters, the bash +command allowlist, the AWS read-permission matrix, and per-provider capability tiers), +introspected live from the code so it never drifts. Read-only; no AWS calls. +""" + +from __future__ import annotations + +from fastapi import APIRouter +from opendevops_core.agent.inventory import build_inventory + +router = APIRouter(prefix="/api/inventory", tags=["inventory"]) + + +@router.get("") +async def get_inventory() -> dict: + """Return the introspected tool/permission inventory.""" + return build_inventory() diff --git a/apps/backend/tests/test_api/test_inventory.py b/apps/backend/tests/test_api/test_inventory.py new file mode 100644 index 0000000..5147a26 --- /dev/null +++ b/apps/backend/tests/test_api/test_inventory.py @@ -0,0 +1,110 @@ +"""Tests for GET /api/inventory — the published tool/permission inventory. + +The inventory is a trust artifact: it must reflect the *live* code (``ALL_TOOLS``, the +bash allowlist, the AWS permission probes) with no hand-maintained duplicate that could +drift. These tests assert exactly that correspondence. +""" + +from __future__ import annotations + +import os + +import pytest + +os.environ.setdefault("CHECKPOINT_BACKEND", "memory") +os.environ.setdefault("LLM_MODEL", "openrouter/anthropic/claude-3.5-sonnet") +os.environ.setdefault("LLM_API_KEY", "test-key") + + +@pytest.mark.asyncio +async def test_inventory_reflects_all_tools(): + """Every registered tool appears in the endpoint, by name, with no extras.""" + from httpx import ASGITransport, AsyncClient + from opendevops_core.agent.core import ALL_TOOLS + + from api.app import app + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + r = await client.get("/api/inventory") + + assert r.status_code == 200 + data = r.json() + + expected = {t.__name__ for t in ALL_TOOLS} + returned = {t["name"] for t in data["tools"]} + assert returned == expected + assert data["tool_count"] == len(ALL_TOOLS) + + +@pytest.mark.asyncio +async def test_inventory_tool_parameters_introspected(): + """Parameters are introspected from signatures, not hand-written.""" + from httpx import ASGITransport, AsyncClient + + from api.app import app + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + data = (await client.get("/api/inventory")).json() + + tools = {t["name"]: t for t in data["tools"]} + # get_alarm_history(alarm_name: str, hours: int = 24) + hist = tools["get_alarm_history"] + params = {p["name"]: p for p in hist["parameters"]} + assert params["alarm_name"]["required"] is True + assert params["hours"]["required"] is False + assert params["hours"]["default"] == 24 + + +@pytest.mark.asyncio +async def test_inventory_bash_allowlist_matches_source(): + """The published allowlist is sourced from the bash-tool constants.""" + from httpx import ASGITransport, AsyncClient + from opendevops_core.tools import bash_tool as bt + + from api.app import app + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + data = (await client.get("/api/inventory")).json() + + allow = data["bash_allowlist"] + assert set(allow["aws"]["readonly_verbs"]) == set(bt._AWS_READONLY_VERBS) + assert set(allow["az"]["readonly_verbs"]) == set(bt._AZ_READONLY_VERBS) + assert set(allow["kubectl"]["subcommands"]) == set(bt._KUBECTL_SUBCOMMANDS) + assert set(allow["docker"]["subcommands"]) == set(bt._DOCKER_SUBCOMMANDS) + + +@pytest.mark.asyncio +async def test_inventory_permission_matrix_matches_probes(): + """The permission matrix is sourced from PERMISSION_PROBES.""" + from httpx import ASGITransport, AsyncClient + from opendevops_core.providers.aws.permissions import PERMISSION_PROBES + + from api.app import app + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + data = (await client.get("/api/inventory")).json() + + matrix = data["aws_permission_matrix"] + returned = {(r["service"], r["boto3_service"], r["operation"]) for r in matrix} + expected = {(label, svc, op) for label, svc, op, _ in PERMISSION_PROBES} + assert returned == expected + + +@pytest.mark.asyncio +async def test_inventory_provider_tiers_honest(): + """Capability tiers match the code: AWS has structured tools, Azure/GCP have none.""" + from httpx import ASGITransport, AsyncClient + + from api.app import app + + async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client: + data = (await client.get("/api/inventory")).json() + + providers = {p["name"]: p for p in data["providers"]} + assert providers["aws"]["structured_tools"] > 0 + assert providers["azure"]["structured_tools"] == 0 + assert providers["gcp"]["structured_tools"] == 0 + # Azure is CLI-first; GCP is a pure stub with no CLI path. + assert providers["azure"]["cli_access"] is True + assert providers["azure"]["event_driven_and_polling"] is False + assert providers["gcp"]["cli_access"] is False diff --git a/apps/backend/tests/test_tools/test_permissions.py b/apps/backend/tests/test_tools/test_permissions.py new file mode 100644 index 0000000..26cef30 --- /dev/null +++ b/apps/backend/tests/test_tools/test_permissions.py @@ -0,0 +1,24 @@ +"""Tests for the AWS permission probe — its declarative table drives both the live +check and the published inventory, so they cannot drift.""" + +from __future__ import annotations + + +def test_check_permissions_runs_every_probe(mocker): + """check_permissions iterates PERMISSION_PROBES: one keyed result per probe, each + invoking its declared boto3 operation.""" + from opendevops_core.providers.aws import permissions + + client = mocker.MagicMock() + session = mocker.MagicMock() + session.client.return_value = client + mocker.patch.object(permissions, "_session", return_value=session) + mocker.patch.object(permissions, "resolve_region", return_value="us-east-1") + + results = permissions.check_permissions() + + assert set(results) == {label for label, _, _, _ in permissions.PERMISSION_PROBES} + assert all(r["passed"] for r in results.values()) + # Each probe's declared operation was actually called on its boto3 client. + called = {c[0] for c in client.method_calls} + assert {op for _, _, op, _ in permissions.PERMISSION_PROBES} <= called diff --git a/apps/core/src/opendevops_core/agent/inventory.py b/apps/core/src/opendevops_core/agent/inventory.py new file mode 100644 index 0000000..c2df4cf --- /dev/null +++ b/apps/core/src/opendevops_core/agent/inventory.py @@ -0,0 +1,150 @@ +"""Tool / permission inventory — a trust artifact, built by introspection. + +Everything here is sourced from the *live* code objects (the registered ``ALL_TOOLS``, +the bash-tool allowlist constants, the AWS permission-probe table, and each provider's +``tools()``) so the published inventory can never drift from what the agent can actually +do. There is no hand-maintained duplicate list — add a tool / verb / probe in its own +module and it shows up here automatically. + +Consumed by the read-only ``/api/inventory`` endpoint and the documentation generator +(``apps/backend/scripts/gen_tool_inventory.py``). +""" + +from __future__ import annotations + +import inspect +import types +from typing import Any + + +def _type_str(annotation: Any) -> str: + """Render a parameter/return annotation as a readable string, tolerating both real + type objects and PEP-563 string annotations (``from __future__ import annotations``).""" + if annotation is inspect.Signature.empty or annotation is inspect.Parameter.empty: + return "any" + if isinstance(annotation, str): + return annotation + # Unions (``str | None``) and generics (``list[str]``) read best via str(). + if isinstance(annotation, types.UnionType) or getattr(annotation, "__args__", None) is not None: + return str(annotation).replace("typing.", "") + name = getattr(annotation, "__name__", None) + if name: + return name + return str(annotation).replace("typing.", "") + + +def _first_paragraph(doc: str | None) -> str: + """First paragraph of a docstring (blank-line delimited), whitespace-collapsed.""" + if not doc: + return "" + para: list[str] = [] + for line in inspect.cleandoc(doc).splitlines(): + if not line.strip(): + break + para.append(line.strip()) + return " ".join(para) + + +def describe_tool(fn: Any) -> dict[str, Any]: + """Introspect a single tool function into {name, module, description, parameters, returns}. + + ``inspect.signature`` follows ``__wrapped__``, so cached tools (``@tool_cached``) are + introspected as their underlying function.""" + sig = inspect.signature(fn) + params: list[dict[str, Any]] = [] + for name, p in sig.parameters.items(): + if name in ("self", "cls"): + continue + required = p.default is inspect.Parameter.empty + params.append( + { + "name": name, + "type": _type_str(p.annotation), + "required": required, + "default": None if required else p.default, + } + ) + return { + "name": getattr(fn, "__name__", repr(fn)), + "module": getattr(fn, "__module__", ""), + "description": _first_paragraph(getattr(fn, "__doc__", None)), + "parameters": params, + "returns": _type_str(sig.return_annotation), + } + + +def _bash_allowlist() -> dict[str, Any]: + """The read-only command allowlist, sourced from the bash-tool constants.""" + from opendevops_core.tools import bash_tool as bt + + return { + "aws": { + "readonly_verbs": sorted(bt._AWS_READONLY_VERBS), + "blocked_global_flags": sorted(bt._AWS_BLOCKED_GLOBAL_FLAGS), + "note": "aws where the operation starts with a read-only verb", + }, + "az": { + "readonly_verbs": sorted(bt._AZ_READONLY_VERBS), + "note": "az where the trailing verb is read-only", + }, + "kubectl": {"subcommands": sorted(bt._KUBECTL_SUBCOMMANDS)}, + "docker": {"subcommands": sorted(bt._DOCKER_SUBCOMMANDS)}, + "timeout_seconds": bt._TIMEOUT, + "max_output_chars": bt._MAX_OUTPUT, + "shell_chaining": "blocked", + } + + +def _permission_matrix() -> list[dict[str, str]]: + """The per-service AWS read-permission probe, sourced from the probe table.""" + from opendevops_core.providers.aws.permissions import PERMISSION_PROBES + + return [ + {"service": label, "boto3_service": svc, "operation": op} + for label, svc, op, _kwargs in PERMISSION_PROBES + ] + + +def _provider_capabilities() -> list[dict[str, Any]]: + """Honest, introspected per-provider capability tiers — the structured-tool count is + read straight from each provider's ``tools()`` (AWS = full set; Azure / GCP = none).""" + from opendevops_core.config import settings + from opendevops_core.providers.aws import AwsProvider + from opendevops_core.providers.azure import AzureProvider + from opendevops_core.providers.gcp import GcpProvider + + active = settings.cloud_provider + rows: list[dict[str, Any]] = [] + for provider, cli, event_driven in ( + (AwsProvider(), True, True), + (AzureProvider(), True, False), + (GcpProvider(), False, False), + ): + rows.append( + { + "name": provider.name, + "active": provider.name == active, + "structured_tools": len(provider.tools()), + "cli_access": cli, + "event_driven_and_polling": event_driven, + } + ) + return rows + + +def build_inventory() -> dict[str, Any]: + """Assemble the full, introspected tool/permission inventory.""" + from opendevops_core.agent.core import ALL_TOOLS + from opendevops_core.providers import get_active_provider + + return { + "active_provider": get_active_provider().name, + "tool_count": len(ALL_TOOLS), + "tools": [describe_tool(t) for t in ALL_TOOLS], + "bash_allowlist": _bash_allowlist(), + "aws_permission_matrix": _permission_matrix(), + "providers": _provider_capabilities(), + } + + +__all__ = ["build_inventory", "describe_tool"] diff --git a/apps/core/src/opendevops_core/providers/aws/permissions.py b/apps/core/src/opendevops_core/providers/aws/permissions.py index 146aa64..3bffe5e 100644 --- a/apps/core/src/opendevops_core/providers/aws/permissions.py +++ b/apps/core/src/opendevops_core/providers/aws/permissions.py @@ -24,21 +24,33 @@ def _c(s, service: str, region: str): return s.client(service, region_name=region) +# Declarative source of truth for the per-service read-permission probe. Each entry is +# (label, boto3 service, read-only operation, kwargs). check_permissions() iterates this, +# and the published tool/permission inventory introspects it — keep them in sync by +# adding probes here only, never by hardcoding calls inline. +PERMISSION_PROBES: tuple[tuple[str, str, str, dict], ...] = ( + ("cloudwatch", "cloudwatch", "describe_alarms", {"MaxRecords": 1}), + ("cloudtrail", "cloudtrail", "lookup_events", {"MaxResults": 1}), + ("ecs", "ecs", "list_clusters", {"maxResults": 1}), + ("lambda", "lambda", "list_functions", {"MaxItems": 1}), + ("ec2", "ec2", "describe_instances", {"MaxResults": 5}), + ("rds", "rds", "describe_db_instances", {}), + ("iam", "sts", "get_caller_identity", {}), + ("sqs", "sqs", "list_queues", {"MaxResults": 1}), + ("events", "events", "list_rules", {"Limit": 1}), +) + + def check_permissions(region: str | None = None) -> dict[str, dict]: """Run one lightweight read call per service. Returns {service: {passed, error}}.""" s = _session() region = region or resolve_region() results: dict[str, dict] = { - "cloudwatch": _check(lambda: _c(s, "cloudwatch", region).describe_alarms(MaxRecords=1)), - "cloudtrail": _check(lambda: _c(s, "cloudtrail", region).lookup_events(MaxResults=1)), - "ecs": _check(lambda: _c(s, "ecs", region).list_clusters(maxResults=1)), - "lambda": _check(lambda: _c(s, "lambda", region).list_functions(MaxItems=1)), - "ec2": _check(lambda: _c(s, "ec2", region).describe_instances(MaxResults=5)), - "rds": _check(lambda: _c(s, "rds", region).describe_db_instances()), - "iam": _check(lambda: _c(s, "sts", region).get_caller_identity()), - "sqs": _check(lambda: _c(s, "sqs", region).list_queues(MaxResults=1)), - "events": _check(lambda: _c(s, "events", region).list_rules(Limit=1)), + label: _check( + lambda svc=svc, op=op, kwargs=kwargs: getattr(_c(s, svc, region), op)(**kwargs) + ) + for label, svc, op, kwargs in PERMISSION_PROBES } for svc, r in results.items(): diff --git a/apps/core/src/opendevops_core/tools/bash_tool.py b/apps/core/src/opendevops_core/tools/bash_tool.py index 6812cd4..8054a99 100644 --- a/apps/core/src/opendevops_core/tools/bash_tool.py +++ b/apps/core/src/opendevops_core/tools/bash_tool.py @@ -82,6 +82,11 @@ _DOCKER_FLAGS_WITH_VALUE: frozenset[str] = frozenset({"-H", "--host", "--context", "--config"}) +# Read-only subcommands allowed per binary (the single source of truth — the published +# tool/permission inventory introspects these, so add verbs here only). +_KUBECTL_SUBCOMMANDS: frozenset[str] = frozenset({"get", "describe", "logs"}) +_DOCKER_SUBCOMMANDS: frozenset[str] = frozenset({"ps", "logs", "inspect"}) + # Azure CLI read-only verbs (the verb is the LAST positional token of the command path, # e.g. "az aks show" → "show", "az aks get-credentials" → "get", "az monitor metrics list" → "list"). _AZ_READONLY_VERBS: frozenset[str] = frozenset( @@ -194,11 +199,11 @@ def _allowed(command: str, tokens: list[str]) -> bool: if binary == "kubectl": subcommand = _find_subcommand(tokens, 1, _KUBECTL_FLAGS_WITH_VALUE) - return subcommand in {"get", "describe", "logs"} + return subcommand in _KUBECTL_SUBCOMMANDS if binary == "docker": subcommand = _find_subcommand(tokens, 1, _DOCKER_FLAGS_WITH_VALUE) - return subcommand in {"ps", "logs", "inspect"} + return subcommand in _DOCKER_SUBCOMMANDS return False diff --git a/apps/documentation/tool_inventory.md b/apps/documentation/tool_inventory.md new file mode 100644 index 0000000..f991f50 --- /dev/null +++ b/apps/documentation/tool_inventory.md @@ -0,0 +1,293 @@ +# Tool & Permission Inventory + +> **Generated file — do not edit by hand.** Produced by `apps/backend/scripts/gen_tool_inventory.py` from the live code (`opendevops_core.agent.inventory.build_inventory`), the same source that backs the read-only `GET /api/inventory` endpoint. Regenerate with `cd apps/backend && uv run python scripts/gen_tool_inventory.py`. + +This is the trust artifact: exactly what the agent can inspect — every registered tool and its parameters, the read-only bash command allowlist, the AWS read-permission probe, and the per-cloud capability tiers. Everything is read-only. + +## Capability by cloud + +| Cloud | Structured SDK tools | CLI access (`bash` tool) | Event-driven + polling | +|---|---|---|---| +| **AWS** (active) | 20 | yes | yes | +| **AZURE** | 0 | yes | no | +| **GCP** | 0 | no | no | + +Active provider: **aws** · total registered tools: **26**. + +## Registered tools + +### `cloudwatch` + +#### `get_alarms` → `dict` + +List CloudWatch alarms, optionally filtered by state (OK, ALARM, INSUFFICIENT_DATA). + +| Param | Type | Default | +|---|---|---| +| `state` | `str \| None` | `None` | + +#### `get_alarm_history` → `dict` + +Fetch state-change history for a specific CloudWatch alarm. + +| Param | Type | Default | +|---|---|---| +| `alarm_name` | `str` | required | +| `hours` | `int` | `24` | + +#### `get_metric_data` → `dict` + +Fetch raw CloudWatch metric data points for a given namespace/metric/dimensions. + +| Param | Type | Default | +|---|---|---| +| `namespace` | `str` | required | +| `metric` | `str` | required | +| `dimensions` | `list[dict[str, str]]` | required | +| `period` | `int` | `300` | +| `hours` | `int` | `3` | +| `stat` | `str` | `'Sum'` | + +#### `get_log_events` → `dict` + +Fetch recent log events from a CloudWatch Logs group, with optional filter pattern. + +| Param | Type | Default | +|---|---|---| +| `log_group` | `str` | required | +| `log_stream` | `str \| None` | `None` | +| `filter_pattern` | `str \| None` | `None` | +| `hours` | `int` | `1` | +| `limit` | `int` | `100` | + +#### `describe_log_groups` → `dict` + +List CloudWatch log groups, optionally filtered by name prefix. + +| Param | Type | Default | +|---|---|---| +| `prefix` | `str \| None` | `None` | + +#### `query_logs_insights` → `dict` + +Run a CloudWatch Logs Insights structured query against a log group. + +| Param | Type | Default | +|---|---|---| +| `log_group` | `str` | required | +| `query` | `str` | required | +| `hours` | `int` | `1` | +| `limit` | `int` | `100` | + +### `cloudtrail` + +#### `lookup_cloudtrail_events` → `dict` + +Look up recent CloudTrail API events. + +| Param | Type | Default | +|---|---|---| +| `hours` | `int` | `2` | +| `resource_name` | `str \| None` | `None` | +| `event_name` | `str \| None` | `None` | +| `limit` | `int` | `50` | + +### `ecs` + +#### `list_ecs_clusters` → `dict` + +List all ECS clusters in the region with their status and active service/task counts. + +*No parameters.* + +#### `list_ecs_services` → `dict` + +List ECS services in a cluster with their desired, running, and pending task counts. + +| Param | Type | Default | +|---|---|---| +| `cluster` | `str` | required | + +#### `describe_ecs_service` → `dict` + +Get detailed info about an ECS service including recent events and deployment status. + +| Param | Type | Default | +|---|---|---| +| `cluster` | `str` | required | +| `service` | `str` | required | + +#### `get_ecs_task_logs` → `dict` + +Fetch stdout/stderr logs for a specific ECS task from CloudWatch Logs. + +| Param | Type | Default | +|---|---|---| +| `cluster` | `str` | required | +| `task_id` | `str` | required | +| `log_group` | `str` | required | +| `limit` | `int` | `100` | + +### `lambda_` + +#### `list_lambda_functions` → `dict` + +List all Lambda functions in the region with their runtime, memory, and timeout. + +*No parameters.* + +#### `get_lambda_function_config` → `dict` + +Get detailed configuration for a Lambda function: memory, timeout, env vars, layers, VPC. + +| Param | Type | Default | +|---|---|---| +| `name` | `str` | required | + +#### `get_lambda_error_rate` → `dict` + +Get Lambda error count and throttle count from CloudWatch for a given time window. + +| Param | Type | Default | +|---|---|---| +| `name` | `str` | required | +| `hours` | `int` | `3` | + +### `ec2` + +#### `describe_ec2_instances` → `dict` + +List EC2 instances with their state, type, and tags. Optionally filter by state or tag. + +| Param | Type | Default | +|---|---|---| +| `filters` | `list[dict[str, Any]] \| None` | `None` | + +#### `get_ec2_system_status` → `dict` + +Get EC2 instance status checks (system reachability and instance reachability). + +| Param | Type | Default | +|---|---|---| +| `instance_id` | `str` | required | + +### `rds` + +#### `describe_rds_instances` → `dict` + +List RDS DB instances with their status, engine, class, and multi-AZ configuration. + +*No parameters.* + +#### `get_rds_events` → `dict` + +Fetch RDS events log for recent database activity, failovers, maintenance, and errors. + +| Param | Type | Default | +|---|---|---| +| `hours` | `int` | `24` | +| `db_identifier` | `str \| None` | `None` | + +### `iam` + +#### `get_caller_identity` → `dict` + +Return the current AWS caller identity: account ID, user/role ARN, and user ID. + +*No parameters.* + +#### `get_iam_role_policies` → `dict` + +List policies attached to an IAM role. + +| Param | Type | Default | +|---|---|---| +| `role_name` | `str` | required | + +### `history` + +#### `get_investigation_history` → `dict` + +Get cross-session investigation analytics: top alarms investigated, top Lambda functions, recurring tool errors, and daily investigation frequency over the last N days. Never loads raw message content — all data is aggregated at the DB level. + +| Param | Type | Default | +|---|---|---| +| `days` | `int` | `30` | + +#### `search_past_investigations` → `dict` + +Search past investigation sessions by keyword in title or message content. Returns session summaries with a short snippet — never full message bodies. + +| Param | Type | Default | +|---|---|---| +| `query` | `str` | required | +| `limit` | `int` | `10` | + +### `bash_tool` + +#### `run_bash_command` → `dict[str, Any]` + +Run a read-only shell command and return structured output. + +| Param | Type | Default | +|---|---|---| +| `command` | `str` | required | + +### `skills` + +#### `list_skills` → `dict` + +List all available investigation skills with their names and descriptions. + +*No parameters.* + +#### `use_skill` → `dict` + +Load the full investigation skill for a named incident type. The skill contains step-by-step investigation guidance, key metrics to check, log patterns to look for, and common root causes with mitigations. + +| Param | Type | Default | +|---|---|---| +| `name` | `str` | required | + +### `final_answer` + +#### `submit_investigation` → `str` + +Submit the final structured investigation result. Call this exactly once when you have gathered sufficient evidence and reached a conclusion. Do not output a JSON block in free text — call this tool instead. + +| Param | Type | Default | +|---|---|---| +| `root_cause_category` | `Literal['SYSTEM_CHANGE', 'INPUT_ANOMALY', 'RESOURCE_LIMIT', 'COMPONENT_FAILURE', 'DEPENDENCY_ISSUE', 'UNKNOWN']` | required | +| `root_cause_summary` | `str` | required | +| `evidence` | `list[str]` | required | +| `mitigation_steps` | `list[str]` | required | +| `validation_steps` | `list[str]` | required | +| `confidence` | `Literal['HIGH', 'MEDIUM', 'LOW']` | required | +| `services_affected` | `list[str]` | required | +| `recommended_follow_up` | `str` | required | +| `follow_up_questions` | `list[str]` | required | + +## Bash command allowlist + +`run_bash_command` runs only read-only commands, validated against this allowlist before execution. Shell chaining is **blocked**, `shell=True` is never used, output is capped at 4000 chars, and every command has a hard 30s timeout. + +- **aws** — aws where the operation starts with a read-only verb. Read-only verbs: `batch-get`, `check`, `describe`, `filter`, `get`, `list`, `lookup`, `query`, `scan`, `search`, `show`, `view`. Blocked global flags: `--endpoint-url`. +- **az** — az where the trailing verb is read-only. Read-only verbs: `check`, `describe`, `get`, `list`, `query`, `show`, `tail`, `version`. +- **kubectl** — subcommands: `describe`, `get`, `logs`. +- **docker** — subcommands: `inspect`, `logs`, `ps`. + +## AWS read-permission matrix + +One lightweight read call per service verifies the agent's credentials (surfaced by the in-app permission checker). + +| Service | boto3 client | Read operation | +|---|---|---| +| cloudwatch | `cloudwatch` | `describe_alarms` | +| cloudtrail | `cloudtrail` | `lookup_events` | +| ecs | `ecs` | `list_clusters` | +| lambda | `lambda` | `list_functions` | +| ec2 | `ec2` | `describe_instances` | +| rds | `rds` | `describe_db_instances` | +| iam | `sts` | `get_caller_identity` | +| sqs | `sqs` | `list_queues` | +| events | `events` | `list_rules` | From 74fbf30c9fde0bca6eeea48888e7c5587f274cd3 Mon Sep 17 00:00:00 2001 From: Ahmad Hammad Date: Thu, 25 Jun 2026 23:01:48 +0300 Subject: [PATCH 2/4] no-mistakes(review): memoize build_inventory and document manual provider flags --- apps/core/src/opendevops_core/agent/inventory.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/core/src/opendevops_core/agent/inventory.py b/apps/core/src/opendevops_core/agent/inventory.py index c2df4cf..932979f 100644 --- a/apps/core/src/opendevops_core/agent/inventory.py +++ b/apps/core/src/opendevops_core/agent/inventory.py @@ -12,6 +12,7 @@ from __future__ import annotations +import functools import inspect import types from typing import Any @@ -115,6 +116,10 @@ def _provider_capabilities() -> list[dict[str, Any]]: active = settings.cloud_provider rows: list[dict[str, Any]] = [] + # structured_tools is introspected from each provider's tools(), but cli_access and + # event_driven_and_polling are manually maintained booleans — those capabilities aren't + # trivially introspectable, so update them by hand if a provider gains CLI access or an + # event/polling loop. for provider, cli, event_driven in ( (AwsProvider(), True, True), (AzureProvider(), True, False), @@ -132,8 +137,14 @@ def _provider_capabilities() -> list[dict[str, Any]]: return rows +@functools.lru_cache(maxsize=1) def build_inventory() -> dict[str, Any]: - """Assemble the full, introspected tool/permission inventory.""" + """Assemble the full, introspected tool/permission inventory. + + Memoized for the process lifetime: every source (``ALL_TOOLS``, the bash allowlist + constants, ``PERMISSION_PROBES``, the active provider, and each provider's ``tools()``) + is fixed at import/config time, so the inventory is immutable per process. Caching also + keeps the Azure/GCP ``tools()`` stub warnings from firing on every ``/api/inventory`` hit.""" from opendevops_core.agent.core import ALL_TOOLS from opendevops_core.providers import get_active_provider From 5c297127f6d12e63049c41696f484134e3030195 Mon Sep 17 00:00:00 2001 From: Ahmad Hammad Date: Thu, 25 Jun 2026 23:09:36 +0300 Subject: [PATCH 3/4] no-mistakes(document): sync stale AGENTS.md tool counts to introspected 26/20 --- AGENTS.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1418983..43f5464 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Overview -OpenDevOps Agent is an open-source AWS incident investigation tool powered by any LLM via LiteLLM. It runs a LangGraph ReAct loop (via DeepAgents) that calls 27 tools — 21 structured boto3 AWS tools plus bash, history analytics, skills, and a structured final-answer tool — then streams results to a React/Vite chat UI over SSE. Auth, multi-user RBAC, event-driven incident detection (EventBridge → SQS), and proactive anomaly polling are all built in and optional. +OpenDevOps Agent is an open-source AWS incident investigation tool powered by any LLM via LiteLLM. It runs a LangGraph ReAct loop (via DeepAgents) that calls 26 tools — 20 structured boto3 AWS tools plus bash, history analytics, skills, and a structured final-answer tool — then streams results to a React/Vite chat UI over SSE. Auth, multi-user RBAC, event-driven incident detection (EventBridge → SQS), and proactive anomaly polling are all built in and optional. --- @@ -48,7 +48,7 @@ to core are live without republishing. - **New env var:** if core reads it, add the Pydantic field to `CoreSettings` in `apps/core/src/opendevops_core/config.py`; if it's web/auth-only, add it to `Settings(CoreSettings)` in `apps/backend/src/config/appsettings.py`. Either way mirror it in `.env.example` (with a comment). Never read env vars directly — always go through `settings`. - **New DB column or table:** add a new numbered migration. Core-domain schema (tables core code reads/writes) goes in `apps/core/src/opendevops_core/migrations/` (e.g. `014_name.sql`); OSS-app-only schema goes in `apps/backend/migrations/`. The runner applies core-then-app, tracked in the `schema_migrations(source, version)` ledger. Never add columns inline in Python code. - **New tool:** add it to `ALL_TOOLS` in `apps/core/src/opendevops_core/agent/core.py`. Tool functions must be plain synchronous Python functions — DeepAgents infers the JSON schema from type hints and docstrings. -- **Published tool/permission inventory:** the trust artifact (read-only `GET /api/inventory` + the generated `apps/documentation/tool_inventory.md`) is built **by introspection** in `opendevops_core.agent.inventory.build_inventory` — it never hand-maintains a list. Its sources of truth are `ALL_TOOLS`, the bash allowlist frozensets in `tools/bash_tool.py` (`_AWS_READONLY_VERBS`, `_AZ_READONLY_VERBS`, `_KUBECTL_SUBCOMMANDS`, `_DOCKER_SUBCOMMANDS`, blocked flags), and `providers/aws/permissions.py:PERMISSION_PROBES`. After changing any of those, regenerate the doc: `cd apps/backend && uv run python scripts/gen_tool_inventory.py`. Never edit `tool_inventory.md` by hand. Note: the introspected counts are **26 total tools / 20 structured AWS tools** (CloudWatch 6, CloudTrail 1, ECS 4, Lambda 3, EC2 2, RDS 2, IAM 2) — the "27 / 21" figures elsewhere in this file are stale; trust the inventory. +- **Published tool/permission inventory:** the trust artifact (read-only `GET /api/inventory` + the generated `apps/documentation/tool_inventory.md`) is built **by introspection** in `opendevops_core.agent.inventory.build_inventory` — it never hand-maintains a list. Its sources of truth are `ALL_TOOLS`, the bash allowlist frozensets in `tools/bash_tool.py` (`_AWS_READONLY_VERBS`, `_AZ_READONLY_VERBS`, `_KUBECTL_SUBCOMMANDS`, `_DOCKER_SUBCOMMANDS`, blocked flags), and `providers/aws/permissions.py:PERMISSION_PROBES`. After changing any of those, regenerate the doc: `cd apps/backend && uv run python scripts/gen_tool_inventory.py`. Never edit `tool_inventory.md` by hand. Note: the introspected counts are **26 total tools / 20 structured AWS tools** (CloudWatch 6, CloudTrail 1, ECS 4, Lambda 3, EC2 2, RDS 2, IAM 2) — trust the inventory as the source of truth. - **New API route that matches a React Router path:** prefix it with `/api/` to avoid the SPA fallback conflict. The `/{full_path:path}` catch-all in `apps/backend/src/api/app.py` intercepts any GET that matches a registered FastAPI route first. - **New skill:** drop a `SKILL.md` file into `apps/core/src/opendevops_core/skills//SKILL.md`. It is picked up automatically at startup (and bundled into the core wheel) — no code changes needed. Use the frontmatter format (`name`, `description`) from the existing `lambda-throttling` skill. - **Docs sync:** if a feature has a corresponding file in `apps/documentation/`, update it when the feature changes. The `apps/documentation/` folder is the public documentation source. @@ -104,14 +104,14 @@ Everything below is built and working in the codebase: ### Agent & Tools - **Framework:** DeepAgents (`create_deep_agent`) wrapping a LangGraph ReAct loop. `ChatLiteLLM` as the model interface — supports OpenRouter, Anthropic, OpenAI, Groq, Ollama, and any OpenAI-compatible endpoint via a single `LLM_MODEL` env var. -- **27 tools total** registered in `ALL_TOOLS` in `apps/core/src/opendevops_core/agent/core.py`: +- **26 tools total** registered in `ALL_TOOLS` in `apps/core/src/opendevops_core/agent/core.py`: - CloudWatch (6): `get_alarms`, `get_alarm_history`, `get_metric_data`, `get_log_events`, `describe_log_groups`, `query_logs_insights` - - CloudTrail (2): trail events + event lookup + - CloudTrail (1): event lookup - ECS (4): clusters, services, service detail, tasks - - Lambda (4): list, config, error rate, concurrent executions + - Lambda (3): list, config, error rate - EC2 (2): list instances, instance details - RDS (2): list DBs, DB details - - IAM (1): describe role + policies + - IAM (2): caller identity + describe role policies - Bash (1): `run_bash_command` — allowlisted read-only `aws`, `kubectl`, `docker` commands; never `shell=True`; 30s hard timeout - History (2): `get_investigation_history`, `search_past_investigations` - Skills (2): `list_skills`, `use_skill` From 054d058e689fd6d941df6cf470b858a10f9b3fe4 Mon Sep 17 00:00:00 2001 From: Ahmad Hammad Date: Thu, 25 Jun 2026 23:46:31 +0300 Subject: [PATCH 4/4] docs(inventory): regenerate tool_inventory after evidence-pack merge The merged evidence pack (PR #67) added a `hypotheses: list[dict]` parameter to submit_investigation. The introspected inventory picks this up automatically; regenerate the generated doc page so it stays in sync with the merged tool surface. Confirms both changesets coexist. Co-Authored-By: Claude Opus 4.8 --- apps/documentation/tool_inventory.md | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/documentation/tool_inventory.md b/apps/documentation/tool_inventory.md index f991f50..0a0bcc6 100644 --- a/apps/documentation/tool_inventory.md +++ b/apps/documentation/tool_inventory.md @@ -259,6 +259,7 @@ Submit the final structured investigation result. Call this exactly once when yo |---|---|---| | `root_cause_category` | `Literal['SYSTEM_CHANGE', 'INPUT_ANOMALY', 'RESOURCE_LIMIT', 'COMPONENT_FAILURE', 'DEPENDENCY_ISSUE', 'UNKNOWN']` | required | | `root_cause_summary` | `str` | required | +| `hypotheses` | `list[dict]` | required | | `evidence` | `list[str]` | required | | `mitigation_steps` | `list[str]` | required | | `validation_steps` | `list[str]` | required |