devopsiq 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent/__init__.py +5 -0
- agent/agent.py +232 -0
- agent/investigation.py +339 -0
- agent/prompts.py +125 -0
- agent/store.py +147 -0
- devopsiq-0.1.0.dist-info/METADATA +662 -0
- devopsiq-0.1.0.dist-info/RECORD +30 -0
- devopsiq-0.1.0.dist-info/WHEEL +5 -0
- devopsiq-0.1.0.dist-info/entry_points.txt +2 -0
- devopsiq-0.1.0.dist-info/licenses/LICENSE +21 -0
- devopsiq-0.1.0.dist-info/top_level.txt +3 -0
- main.py +310 -0
- tools/__init__.py +6 -0
- tools/ansible.py +110 -0
- tools/argocd.py +91 -0
- tools/base.py +112 -0
- tools/cloud.py +101 -0
- tools/docker.py +280 -0
- tools/git_ci.py +257 -0
- tools/helm.py +168 -0
- tools/investigation.py +357 -0
- tools/istio.py +43 -0
- tools/kubernetes.py +464 -0
- tools/monitoring.py +162 -0
- tools/newrelic.py +167 -0
- tools/preflight.py +59 -0
- tools/registry.py +49 -0
- tools/system.py +162 -0
- tools/terraform.py +90 -0
- tools/trivy.py +83 -0
tools/git_ci.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Read-only git / CI tools (Phases 5 and 8).
|
|
2
|
+
|
|
3
|
+
Seven tools for code-and-pipeline problems: repository status (git status),
|
|
4
|
+
recent history (git log), working-tree diff summary (git diff), open pull
|
|
5
|
+
requests and (Phase 8) GitHub Actions runs, one run's jobs, and workflow
|
|
6
|
+
listing — all via the GitHub CLI (gh). Operating on the CURRENT WORKING
|
|
7
|
+
DIRECTORY the agent was launched from (like terraform).
|
|
8
|
+
|
|
9
|
+
Safety model:
|
|
10
|
+
- Only `git status --short --branch`, `git log --oneline -n`, `git diff
|
|
11
|
+
--stat HEAD`, `gh pr list`, `gh run list`, `gh run view` and `gh workflow
|
|
12
|
+
list` exist. No git reset, checkout/switch, commit, push, pull, clean, or
|
|
13
|
+
stash; no gh pr create/merge/close, no gh workflow run, no run cancel/
|
|
14
|
+
rerun — nothing that changes state on GitHub.
|
|
15
|
+
- No free-form args: neither git nor gh ever receives user-supplied text
|
|
16
|
+
beyond bounded integers, so flag/dispatch injection is impossible.
|
|
17
|
+
- git and gh must be installed; gh additionally needs authentication and to
|
|
18
|
+
be inside a GitHub repository. Missing pieces surface as the exact CLI
|
|
19
|
+
error — never invented.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from tools.base import Tool, ToolError, read_command_output
|
|
23
|
+
from tools.registry import register
|
|
24
|
+
|
|
25
|
+
_TIMEOUT_S = 10
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _checked_count(args: dict, key: str, default: int, maximum: int, label: str) -> int:
|
|
29
|
+
try:
|
|
30
|
+
count = int(args.get(key, default))
|
|
31
|
+
except (TypeError, ValueError):
|
|
32
|
+
raise ToolError(f"{label} must be an integer between 1 and {maximum}")
|
|
33
|
+
if not 1 <= count <= maximum:
|
|
34
|
+
raise ToolError(f"{label} must be between 1 and {maximum}")
|
|
35
|
+
return count
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _git_repo_status(args: dict) -> str:
|
|
39
|
+
return read_command_output(("git", "status", "--short", "--branch"), timeout=_TIMEOUT_S)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _git_log(args: dict) -> str:
|
|
43
|
+
count = _checked_count(args, "count", 20, 100, "count")
|
|
44
|
+
return read_command_output(
|
|
45
|
+
("git", "log", "--oneline", "-n", str(count)), timeout=_TIMEOUT_S
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _git_diff(args: dict) -> str:
|
|
50
|
+
return read_command_output(("git", "diff", "--stat", "HEAD"), timeout=_TIMEOUT_S)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _gh_prs(args: dict) -> str:
|
|
54
|
+
count = _checked_count(args, "limit", 10, 50, "limit")
|
|
55
|
+
return read_command_output(
|
|
56
|
+
(
|
|
57
|
+
"gh", "pr", "list", "--limit", str(count),
|
|
58
|
+
"--json", "number,title,state,headRefName,isDraft,updatedAt",
|
|
59
|
+
),
|
|
60
|
+
timeout=_TIMEOUT_S,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
GIT_REPO_STATUS = Tool(
|
|
65
|
+
name="git_repo_status",
|
|
66
|
+
description=(
|
|
67
|
+
"Repository status of the current working directory (git status "
|
|
68
|
+
"--short --branch): current branch, ahead/behind, and modified/"
|
|
69
|
+
"staged/untracked files. Use for 'have local changes been applied', "
|
|
70
|
+
"unclean trees before a deploy. Read-only."
|
|
71
|
+
),
|
|
72
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
73
|
+
executor=_git_repo_status,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
GIT_LOG = Tool(
|
|
77
|
+
name="git_log",
|
|
78
|
+
description=(
|
|
79
|
+
"Recent commit history of the current working directory "
|
|
80
|
+
"(git log --oneline -n): short hash + subject per commit. Use to see "
|
|
81
|
+
"what changed recently or whether a fix was committed. Read-only."
|
|
82
|
+
),
|
|
83
|
+
parameters={
|
|
84
|
+
"type": "object",
|
|
85
|
+
"properties": {
|
|
86
|
+
"count": {
|
|
87
|
+
"type": "integer",
|
|
88
|
+
"minimum": 1,
|
|
89
|
+
"maximum": 100,
|
|
90
|
+
"default": 20,
|
|
91
|
+
"description": "How many commits to show (1–100).",
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"additionalProperties": False,
|
|
95
|
+
},
|
|
96
|
+
executor=_git_log,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
GIT_DIFF = Tool(
|
|
100
|
+
name="git_diff",
|
|
101
|
+
description=(
|
|
102
|
+
"Summary of working-tree changes vs HEAD in the current working "
|
|
103
|
+
"directory (git diff --stat HEAD): files changed plus insertions/"
|
|
104
|
+
"deletions. Use to judge the size/impact of uncommitted changes. "
|
|
105
|
+
"Read-only."
|
|
106
|
+
),
|
|
107
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
108
|
+
executor=_git_diff,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
GH_PRS = Tool(
|
|
112
|
+
name="gh_prs",
|
|
113
|
+
description=(
|
|
114
|
+
"List open pull requests of the current GitHub repository as JSON "
|
|
115
|
+
"(gh pr list): number, title, state, head branch, draft flag, updated "
|
|
116
|
+
"time. Use for 'what is waiting to merge', CI/CD pull-request state. "
|
|
117
|
+
"Requires the gh CLI installed, authenticated, and a repository with "
|
|
118
|
+
"a GitHub remote. Read-only."
|
|
119
|
+
),
|
|
120
|
+
parameters={
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"limit": {
|
|
124
|
+
"type": "integer",
|
|
125
|
+
"minimum": 1,
|
|
126
|
+
"maximum": 50,
|
|
127
|
+
"default": 10,
|
|
128
|
+
"description": "How many PRs to list (1–50).",
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"additionalProperties": False,
|
|
132
|
+
},
|
|
133
|
+
executor=_gh_prs,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# --- Phase 8: GitHub Actions (via gh) — run listing, one run's detail,
|
|
137
|
+
# workflow listing. Ids are digits-only validated, which blocks flag
|
|
138
|
+
# injection; `gh workflow run` and every mutating verb stay absent. ----------
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _checked_run_id(args: dict) -> str:
|
|
142
|
+
run_id = args.get("run_id")
|
|
143
|
+
if (
|
|
144
|
+
not isinstance(run_id, str)
|
|
145
|
+
or not run_id.isdigit()
|
|
146
|
+
or not 1 <= len(run_id) <= 20
|
|
147
|
+
):
|
|
148
|
+
raise ToolError("run_id must be the numeric GitHub Actions run id")
|
|
149
|
+
return run_id
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _gh_runs(args: dict) -> str:
|
|
153
|
+
limit = _checked_count(args, "limit", 10, 50, "limit")
|
|
154
|
+
return read_command_output(
|
|
155
|
+
(
|
|
156
|
+
"gh", "run", "list", "--limit", str(limit),
|
|
157
|
+
"--json", "databaseId,displayTitle,status,conclusion,"
|
|
158
|
+
"workflowName,createdAt,event,headBranch",
|
|
159
|
+
),
|
|
160
|
+
timeout=_TIMEOUT_S,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _gh_run_view(args: dict) -> str:
|
|
165
|
+
run_id = _checked_run_id(args)
|
|
166
|
+
return read_command_output(
|
|
167
|
+
("gh", "run", "view", run_id, "--json", "status,conclusion,jobs"),
|
|
168
|
+
timeout=_TIMEOUT_S,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _gh_workflows(args: dict) -> str:
|
|
173
|
+
limit = _checked_count(args, "limit", 20, 100, "limit")
|
|
174
|
+
return read_command_output(
|
|
175
|
+
("gh", "workflow", "list", "--limit", str(limit),
|
|
176
|
+
"--json", "id,name,state"),
|
|
177
|
+
timeout=_TIMEOUT_S,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
GH_RUNS = Tool(
|
|
182
|
+
name="gh_runs",
|
|
183
|
+
description=(
|
|
184
|
+
"List recent GitHub Actions workflow runs as JSON (gh run list): run "
|
|
185
|
+
"id, title, workflow name, status, conclusion, branch, event, created "
|
|
186
|
+
"time. Use for 'which deploy failed', the CI record behind a broken "
|
|
187
|
+
"release. Requires gh installed, authenticated, in a GitHub repo. "
|
|
188
|
+
"Read-only."
|
|
189
|
+
),
|
|
190
|
+
parameters={
|
|
191
|
+
"type": "object",
|
|
192
|
+
"properties": {
|
|
193
|
+
"limit": {
|
|
194
|
+
"type": "integer",
|
|
195
|
+
"minimum": 1,
|
|
196
|
+
"maximum": 50,
|
|
197
|
+
"default": 10,
|
|
198
|
+
"description": "How many runs to list (1–50).",
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"additionalProperties": False,
|
|
202
|
+
},
|
|
203
|
+
executor=_gh_runs,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
GH_RUN_VIEW = Tool(
|
|
207
|
+
name="gh_run_view",
|
|
208
|
+
description=(
|
|
209
|
+
"One GitHub Actions run's detail as JSON (gh run view): overall "
|
|
210
|
+
"status/conclusion plus per-job state (each job's name, status, "
|
|
211
|
+
"conclusion, steps). Use with gh_runs to find which job/step failed. "
|
|
212
|
+
"Read-only."
|
|
213
|
+
),
|
|
214
|
+
parameters={
|
|
215
|
+
"type": "object",
|
|
216
|
+
"properties": {
|
|
217
|
+
"run_id": {
|
|
218
|
+
"type": "string",
|
|
219
|
+
"description": "Numeric Actions run id (from gh_runs).",
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
"required": ["run_id"],
|
|
223
|
+
"additionalProperties": False,
|
|
224
|
+
},
|
|
225
|
+
executor=_gh_run_view,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
GH_WORKFLOWS = Tool(
|
|
229
|
+
name="gh_workflows",
|
|
230
|
+
description=(
|
|
231
|
+
"List the GitHub Actions workflows of the current repository as JSON "
|
|
232
|
+
"(gh workflow list): id, name, state (active/disabled). Use to check "
|
|
233
|
+
"which pipelines exist and whether one is disabled. Read-only."
|
|
234
|
+
),
|
|
235
|
+
parameters={
|
|
236
|
+
"type": "object",
|
|
237
|
+
"properties": {
|
|
238
|
+
"limit": {
|
|
239
|
+
"type": "integer",
|
|
240
|
+
"minimum": 1,
|
|
241
|
+
"maximum": 100,
|
|
242
|
+
"default": 20,
|
|
243
|
+
"description": "How many workflows to list (1–100).",
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
"additionalProperties": False,
|
|
247
|
+
},
|
|
248
|
+
executor=_gh_workflows,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
register(GIT_REPO_STATUS)
|
|
252
|
+
register(GIT_LOG)
|
|
253
|
+
register(GIT_DIFF)
|
|
254
|
+
register(GH_PRS)
|
|
255
|
+
register(GH_RUNS)
|
|
256
|
+
register(GH_RUN_VIEW)
|
|
257
|
+
register(GH_WORKFLOWS)
|
tools/helm.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Read-only Helm release tools (Phase 9).
|
|
2
|
+
|
|
3
|
+
Three tools over the helm CLI: list releases (helm list), one release's
|
|
4
|
+
status (helm status), and its revision history (helm history). All are
|
|
5
|
+
reads of release metadata Helm keeps in the cluster — nothing here installs,
|
|
6
|
+
upgrades, rolls back or uninstalls anything.
|
|
7
|
+
|
|
8
|
+
Safety model (same shape as tools/kubernetes.py):
|
|
9
|
+
- Only fixed argv templates with the list/status/history verbs exist. There
|
|
10
|
+
is no install, upgrade, rollback, uninstall, delete or template path.
|
|
11
|
+
- Release and namespace names are validated against Kubernetes DNS naming
|
|
12
|
+
rules before touching helm — blocks flag injection and garbage input.
|
|
13
|
+
- `--max` on history is a bounded integer (1–50).
|
|
14
|
+
- helm must be installed and pointed at a cluster (its own kubeconfig
|
|
15
|
+
handling); missing/unreachable surfaces as the exact CLI error.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from tools.base import Tool, ToolError, read_command_output
|
|
21
|
+
from tools.registry import register
|
|
22
|
+
|
|
23
|
+
# Helm release names follow Kubernetes naming: lowercase DNS-style.
|
|
24
|
+
_NAME_RE = re.compile(r"[a-z0-9](?:[-a-z0-9.]{0,251}[a-z0-9])?")
|
|
25
|
+
_TIMEOUT_S = 15
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _check_name(value, kind: str) -> str:
|
|
29
|
+
if (
|
|
30
|
+
not isinstance(value, str)
|
|
31
|
+
or len(value) > 253
|
|
32
|
+
or not _NAME_RE.fullmatch(value)
|
|
33
|
+
):
|
|
34
|
+
raise ToolError(
|
|
35
|
+
f"invalid Helm {kind} name {value!r}: expected lowercase "
|
|
36
|
+
"letters, digits, '-' or '.', max 253 characters"
|
|
37
|
+
)
|
|
38
|
+
return value
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _namespace(args: dict) -> str:
|
|
42
|
+
return _check_name(args.get("namespace") or "default", "namespace")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _checked_max(args: dict) -> int:
|
|
46
|
+
try:
|
|
47
|
+
maximum = int(args.get("max", 10))
|
|
48
|
+
except (TypeError, ValueError):
|
|
49
|
+
raise ToolError("max must be an integer between 1 and 50")
|
|
50
|
+
if not 1 <= maximum <= 50:
|
|
51
|
+
raise ToolError("max must be between 1 and 50")
|
|
52
|
+
return maximum
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _helm_list(args: dict) -> str:
|
|
56
|
+
if args.get("all_namespaces"):
|
|
57
|
+
argv = ("helm", "list", "--all-namespaces")
|
|
58
|
+
else:
|
|
59
|
+
argv = ("helm", "list", "-n", _namespace(args))
|
|
60
|
+
return read_command_output(argv, timeout=_TIMEOUT_S)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _helm_status(args: dict) -> str:
|
|
64
|
+
release = _check_name(args.get("release"), "release")
|
|
65
|
+
namespace = _namespace(args)
|
|
66
|
+
return read_command_output(
|
|
67
|
+
("helm", "status", release, "-n", namespace),
|
|
68
|
+
timeout=_TIMEOUT_S,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _helm_history(args: dict) -> str:
|
|
73
|
+
release = _check_name(args.get("release"), "release")
|
|
74
|
+
namespace = _namespace(args)
|
|
75
|
+
return read_command_output(
|
|
76
|
+
("helm", "history", release, "-n", namespace,
|
|
77
|
+
"--max", str(_checked_max(args))),
|
|
78
|
+
timeout=_TIMEOUT_S,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
HELM_LIST = Tool(
|
|
83
|
+
name="helm_list",
|
|
84
|
+
description=(
|
|
85
|
+
"List Helm releases (helm list): name, namespace, revision, "
|
|
86
|
+
"updated time, status (deployed/failed/pending-upgrade), chart "
|
|
87
|
+
"version, app version. Use for 'which releases exist and are they "
|
|
88
|
+
"healthy', and to find the release behind a broken workload. "
|
|
89
|
+
"Requires helm + a configured cluster. Read-only."
|
|
90
|
+
),
|
|
91
|
+
parameters={
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"namespace": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Namespace to list (default: \"default\").",
|
|
97
|
+
},
|
|
98
|
+
"all_namespaces": {
|
|
99
|
+
"type": "boolean",
|
|
100
|
+
"description": "List across all namespaces instead of one.",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
"additionalProperties": False,
|
|
104
|
+
},
|
|
105
|
+
executor=_helm_list,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
HELM_STATUS = Tool(
|
|
109
|
+
name="helm_status",
|
|
110
|
+
description=(
|
|
111
|
+
"One Helm release's status (helm status): revision, state, chart, "
|
|
112
|
+
"and the release's last-deployed notes. Use to see why a release is "
|
|
113
|
+
"failed or pending and which revision is live. Read-only."
|
|
114
|
+
),
|
|
115
|
+
parameters={
|
|
116
|
+
"type": "object",
|
|
117
|
+
"properties": {
|
|
118
|
+
"release": {
|
|
119
|
+
"type": "string",
|
|
120
|
+
"description": "Release name.",
|
|
121
|
+
},
|
|
122
|
+
"namespace": {
|
|
123
|
+
"type": "string",
|
|
124
|
+
"description": "Namespace of the release (default: \"default\").",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
"required": ["release"],
|
|
128
|
+
"additionalProperties": False,
|
|
129
|
+
},
|
|
130
|
+
executor=_helm_status,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
HELM_HISTORY = Tool(
|
|
134
|
+
name="helm_history",
|
|
135
|
+
description=(
|
|
136
|
+
"A Helm release's revision history (helm history --max): each "
|
|
137
|
+
"revision with its status, chart version, and updated time. Use to "
|
|
138
|
+
"correlate an incident start with an upgrade, or to find the last "
|
|
139
|
+
"good revision. Read-only."
|
|
140
|
+
),
|
|
141
|
+
parameters={
|
|
142
|
+
"type": "object",
|
|
143
|
+
"properties": {
|
|
144
|
+
"release": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"description": "Release name.",
|
|
147
|
+
},
|
|
148
|
+
"namespace": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "Namespace of the release (default: \"default\").",
|
|
151
|
+
},
|
|
152
|
+
"max": {
|
|
153
|
+
"type": "integer",
|
|
154
|
+
"minimum": 1,
|
|
155
|
+
"maximum": 50,
|
|
156
|
+
"default": 10,
|
|
157
|
+
"description": "How many revisions to show (1–50).",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
"required": ["release"],
|
|
161
|
+
"additionalProperties": False,
|
|
162
|
+
},
|
|
163
|
+
executor=_helm_history,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
register(HELM_LIST)
|
|
167
|
+
register(HELM_STATUS)
|
|
168
|
+
register(HELM_HISTORY)
|