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/base.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Tool contract for the DevOps investigation agent.
|
|
2
|
+
|
|
3
|
+
All tools are READ-ONLY by construction: an executor inspects the system and
|
|
4
|
+
returns text evidence; nothing in this module can mutate state, and tools
|
|
5
|
+
never accept arbitrary commands (they select from hard-coded, allowlisted
|
|
6
|
+
invocations).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Callable, Sequence
|
|
15
|
+
|
|
16
|
+
# Ceiling for how much of a tool result is fed back to the model in one turn.
|
|
17
|
+
MAX_TOOL_OUTPUT_CHARS = 8_000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ToolError(Exception):
|
|
21
|
+
"""Raised by an executor when a tool could not produce its result."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def truncate_output(text: str, limit: int = MAX_TOOL_OUTPUT_CHARS) -> str:
|
|
25
|
+
"""Cut very long output so the TOTAL returned length stays <= limit.
|
|
26
|
+
|
|
27
|
+
Head and tail of the original are kept; a short digest line connects
|
|
28
|
+
them. 64 characters are reserved for that line, which is enough for any
|
|
29
|
+
realistic input size.
|
|
30
|
+
"""
|
|
31
|
+
if len(text) <= limit:
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
body = max(2, limit - 64) # budget left for actual content
|
|
35
|
+
head_len = body // 2
|
|
36
|
+
tail_len = body - head_len
|
|
37
|
+
|
|
38
|
+
omitted = len(text) - body
|
|
39
|
+
marker = f"\n… [{omitted} characters omitted] …\n"
|
|
40
|
+
if len(marker) > limit - body:
|
|
41
|
+
marker = "\n… (output truncated) …\n"
|
|
42
|
+
|
|
43
|
+
return text[:head_len] + marker + text[-tail_len:]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_command_output(argv: Sequence[str], timeout: int = 10) -> str:
|
|
47
|
+
"""Run an allowlisted read-only command and return its output as evidence.
|
|
48
|
+
|
|
49
|
+
Raises ToolError (so the caller never feeds unvalidated input to a
|
|
50
|
+
shell) when the binary is missing, the command times out, or it exits
|
|
51
|
+
non-zero — in the failure cases, the captured output is embedded in the
|
|
52
|
+
error so the model sees real diagnostics rather than a generic message.
|
|
53
|
+
|
|
54
|
+
Successful output is prefixed with the literal command line (so the
|
|
55
|
+
model can tell it is real captured output, not a canned string) and
|
|
56
|
+
truncated to MAX_TOOL_OUTPUT_CHARS.
|
|
57
|
+
"""
|
|
58
|
+
binary = argv[0]
|
|
59
|
+
if shutil.which(binary) is None:
|
|
60
|
+
raise ToolError(
|
|
61
|
+
f"required executable {binary!r} is not installed on this host"
|
|
62
|
+
)
|
|
63
|
+
try:
|
|
64
|
+
proc = subprocess.run(
|
|
65
|
+
argv,
|
|
66
|
+
capture_output=True,
|
|
67
|
+
text=True,
|
|
68
|
+
timeout=timeout,
|
|
69
|
+
check=False,
|
|
70
|
+
)
|
|
71
|
+
except subprocess.TimeoutExpired as exc:
|
|
72
|
+
raise ToolError(f"command {binary} timed out after {timeout}s") from exc
|
|
73
|
+
|
|
74
|
+
output = ((proc.stdout or "") + (proc.stderr or "")).strip()
|
|
75
|
+
if proc.returncode != 0:
|
|
76
|
+
raise ToolError(
|
|
77
|
+
f"command {binary} exited with code {proc.returncode}:\n"
|
|
78
|
+
f"{output or '(no output)'}"
|
|
79
|
+
)
|
|
80
|
+
if not output:
|
|
81
|
+
output = f"(command {' '.join(argv)} returned no output, exit code 0)"
|
|
82
|
+
return truncate_output(f"$ {' '.join(argv)}\n{output}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class Tool:
|
|
87
|
+
"""A read-only capability the model can call while investigating.
|
|
88
|
+
|
|
89
|
+
Attributes:
|
|
90
|
+
name: stable identifier the model uses to request this tool.
|
|
91
|
+
description: when/how to use it; sent to the model alongside the schema.
|
|
92
|
+
parameters: JSON Schema describing the arguments the tool accepts.
|
|
93
|
+
executor: local callable(args: dict) -> str. It receives the *parsed*
|
|
94
|
+
JSON arguments and returns the text fed back to the model as
|
|
95
|
+
evidence. Must be read-only and must never fabricate output.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
name: str
|
|
99
|
+
description: str
|
|
100
|
+
parameters: dict
|
|
101
|
+
executor: Callable[[dict], str]
|
|
102
|
+
|
|
103
|
+
def schema(self) -> dict:
|
|
104
|
+
"""Render this tool in the OpenAI-compatible `tools` request format."""
|
|
105
|
+
return {
|
|
106
|
+
"type": "function",
|
|
107
|
+
"function": {
|
|
108
|
+
"name": self.name,
|
|
109
|
+
"description": self.description,
|
|
110
|
+
"parameters": self.parameters,
|
|
111
|
+
},
|
|
112
|
+
}
|
tools/cloud.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Read-only cloud-account tools (Phase 8).
|
|
2
|
+
|
|
3
|
+
Identity and listing probes for the three major cloud CLIs: who am I / which
|
|
4
|
+
account is active (aws sts get-caller-identity, gcloud config list, az account
|
|
5
|
+
show) and a first-level resource listing (az group list). These answer "which
|
|
6
|
+
cloud account am I even looking at" — usually the first question when an
|
|
7
|
+
incident report and the infrastructure disagree.
|
|
8
|
+
|
|
9
|
+
Safety model:
|
|
10
|
+
- Only fixed argv templates with read-only verbs exist. There is no create,
|
|
11
|
+
delete, update, start, stop, tag, or IAM-mutation path, and no
|
|
12
|
+
user-supplied argument anywhere: neither AWS, gcloud nor az ever receives
|
|
13
|
+
model-chosen text, so flag/dispatch injection is impossible.
|
|
14
|
+
- `aws sts get-caller-identity` needs no region; listing tools are kept to
|
|
15
|
+
account/subscription level on purpose — region-scoped resource sweeps
|
|
16
|
+
(ec2 describe-*, compute instances list ...) can be added later behind the
|
|
17
|
+
same template pattern.
|
|
18
|
+
- The CLIs must be installed and configured (aws credentials, gcloud/az
|
|
19
|
+
logins). Missing pieces surface as the exact CLI error — never invented.
|
|
20
|
+
- Cloud CLIs are slow on a cold start (helper processes, token refresh), so
|
|
21
|
+
the subprocess timeout is 30s rather than the usual 10s.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from tools.base import Tool, read_command_output
|
|
25
|
+
from tools.registry import register
|
|
26
|
+
|
|
27
|
+
_TIMEOUT_S = 30
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _aws_identity(args: dict) -> str:
|
|
31
|
+
return read_command_output(
|
|
32
|
+
("aws", "sts", "get-caller-identity", "--output", "json"),
|
|
33
|
+
timeout=_TIMEOUT_S,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _gcloud_identity(args: dict) -> str:
|
|
38
|
+
return read_command_output(
|
|
39
|
+
("gcloud", "config", "list", "--format=json"),
|
|
40
|
+
timeout=_TIMEOUT_S,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _az_account(args: dict) -> str:
|
|
45
|
+
return read_command_output(("az", "account", "show"), timeout=_TIMEOUT_S)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _az_groups(args: dict) -> str:
|
|
49
|
+
return read_command_output(("az", "group", "list"), timeout=_TIMEOUT_S)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
AWS_IDENTITY = Tool(
|
|
53
|
+
name="aws_identity",
|
|
54
|
+
description=(
|
|
55
|
+
"AWS caller identity as JSON (aws sts get-caller-identity): account "
|
|
56
|
+
"id, user/role ARN. Use to answer 'which AWS account and principal "
|
|
57
|
+
"are the configured credentials for'. Requires aws CLI with working "
|
|
58
|
+
"credentials. Read-only."
|
|
59
|
+
),
|
|
60
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
61
|
+
executor=_aws_identity,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
GCLOUD_IDENTITY = Tool(
|
|
65
|
+
name="gcloud_identity",
|
|
66
|
+
description=(
|
|
67
|
+
"Active gcloud configuration as JSON (gcloud config list): account, "
|
|
68
|
+
"project, region, zone. Use to answer 'which GCP project/region is "
|
|
69
|
+
"this host pointed at'. Requires the gcloud CLI. Read-only."
|
|
70
|
+
),
|
|
71
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
72
|
+
executor=_gcloud_identity,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
AZ_ACCOUNT = Tool(
|
|
76
|
+
name="az_account",
|
|
77
|
+
description=(
|
|
78
|
+
"The active Azure subscription as JSON (az account show): id, name, "
|
|
79
|
+
"state, tenant. Use to answer 'which Azure subscription am I looking "
|
|
80
|
+
"at'. Requires the az CLI and a logged-in account. Read-only."
|
|
81
|
+
),
|
|
82
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
83
|
+
executor=_az_account,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
AZ_GROUPS = Tool(
|
|
87
|
+
name="az_groups",
|
|
88
|
+
description=(
|
|
89
|
+
"List Azure resource groups in the active subscription as JSON (az "
|
|
90
|
+
"group list): name, location, tags. Use to see the top-level layout "
|
|
91
|
+
"of an Azure subscription during an investigation. Requires az CLI. "
|
|
92
|
+
"Read-only."
|
|
93
|
+
),
|
|
94
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
95
|
+
executor=_az_groups,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
register(AWS_IDENTITY)
|
|
99
|
+
register(GCLOUD_IDENTITY)
|
|
100
|
+
register(AZ_ACCOUNT)
|
|
101
|
+
register(AZ_GROUPS)
|
tools/docker.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Read-only Docker tools (Phases 5, 8, 9).
|
|
2
|
+
|
|
3
|
+
Ten tools for container problems on the host: list containers (docker ps),
|
|
4
|
+
inspect one (JSON), tail logs, live resource stats (docker stats --no-stream),
|
|
5
|
+
the local image list, networks, volumes, disk usage (Phase 8), and (Phase 9)
|
|
6
|
+
Docker Compose project listing + a project's services. Same contract/safety
|
|
7
|
+
model as the other modules.
|
|
8
|
+
|
|
9
|
+
Safety model:
|
|
10
|
+
- Only fixed argv templates exist with the read-only docker verbs ps, inspect,
|
|
11
|
+
logs, stats, images, network ls, volume ls, system df, compose ls, compose
|
|
12
|
+
ps. There is no run, start, stop, restart, rm, rmi, pull, push, exec, build,
|
|
13
|
+
create, prune or compose-up/down path.
|
|
14
|
+
- `docker stats` is hard-coded with `--no-stream`: without it the command
|
|
15
|
+
follows forever and would hang a turn.
|
|
16
|
+
- Container/image names are validated (letters, digits, '.', '-', '_', no
|
|
17
|
+
leading dash) — blocks flag injection and path traversal.
|
|
18
|
+
- Docker must be installed and the daemon running; if not, the exact error
|
|
19
|
+
is returned — nothing is invented. A dangling or dead daemon surfaces as
|
|
20
|
+
a real ToolError, which the model must report honestly.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
|
|
25
|
+
from tools.base import Tool, ToolError, read_command_output
|
|
26
|
+
from tools.registry import register
|
|
27
|
+
|
|
28
|
+
# Docker names: letters, digits, '.', '-', '_'; must start/end alphanumeric.
|
|
29
|
+
_NAME_RE = re.compile(r"[a-zA-Z0-9][a-zA-Z0-9._\-]{0,127}")
|
|
30
|
+
_TIMEOUT_S = 10
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_name(value, kind: str) -> str:
|
|
34
|
+
if (
|
|
35
|
+
not isinstance(value, str)
|
|
36
|
+
or len(value) > 128
|
|
37
|
+
or not _NAME_RE.fullmatch(value)
|
|
38
|
+
):
|
|
39
|
+
raise ToolError(
|
|
40
|
+
f"invalid Docker {kind} name {value!r}: expected letters, digits, "
|
|
41
|
+
"'.', '-' or '_', max 128 chars"
|
|
42
|
+
)
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _checked_lines(args: dict) -> int:
|
|
47
|
+
try:
|
|
48
|
+
lines = int(args.get("lines", 100))
|
|
49
|
+
except (TypeError, ValueError):
|
|
50
|
+
raise ToolError("lines must be an integer between 1 and 500")
|
|
51
|
+
if not 1 <= lines <= 500:
|
|
52
|
+
raise ToolError("lines must be between 1 and 500")
|
|
53
|
+
return lines
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _docker_ps(args: dict) -> str:
|
|
57
|
+
return read_command_output(("docker", "ps", "-a"), timeout=_TIMEOUT_S)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _docker_inspect(args: dict) -> str:
|
|
61
|
+
name = _check_name(args.get("name"), "container or image")
|
|
62
|
+
return read_command_output(("docker", "inspect", name), timeout=_TIMEOUT_S)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _docker_logs(args: dict) -> str:
|
|
66
|
+
name = _check_name(args.get("container"), "container")
|
|
67
|
+
return read_command_output(
|
|
68
|
+
("docker", "logs", "--tail", str(_checked_lines(args)), name),
|
|
69
|
+
timeout=_TIMEOUT_S,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _docker_stats(args: dict) -> str:
|
|
74
|
+
return read_command_output(("docker", "stats", "--no-stream"), timeout=_TIMEOUT_S)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _docker_images(args: dict) -> str:
|
|
78
|
+
return read_command_output(("docker", "images"), timeout=_TIMEOUT_S)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DOCKER_PS = Tool(
|
|
82
|
+
name="docker_ps",
|
|
83
|
+
description=(
|
|
84
|
+
"List all containers (running and stopped) with status: container id, "
|
|
85
|
+
"image, command, status (Up/X minutes, Exited (code) ...), names. The "
|
|
86
|
+
"first tool for 'is my container running / why did it stop'. Read-only."
|
|
87
|
+
),
|
|
88
|
+
parameters={
|
|
89
|
+
"type": "object",
|
|
90
|
+
"properties": {},
|
|
91
|
+
"additionalProperties": False,
|
|
92
|
+
},
|
|
93
|
+
executor=_docker_ps,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
DOCKER_INSPECT = Tool(
|
|
97
|
+
name="docker_inspect",
|
|
98
|
+
description=(
|
|
99
|
+
"Inspect one container or image by name as JSON — state, exit code, "
|
|
100
|
+
"restart count, mounts, env, image id. Use with docker_ps to see the "
|
|
101
|
+
"details behind a container's state. Read-only."
|
|
102
|
+
),
|
|
103
|
+
parameters={
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"name": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "Container or image name/id to inspect.",
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"required": ["name"],
|
|
112
|
+
"additionalProperties": False,
|
|
113
|
+
},
|
|
114
|
+
executor=_docker_inspect,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
DOCKER_LOGS = Tool(
|
|
118
|
+
name="docker_logs",
|
|
119
|
+
description=(
|
|
120
|
+
"Fetch the tail of a container's stdout/stderr logs as plain text. Use "
|
|
121
|
+
"with docker_ps/docker_inspect to see the error that made a container "
|
|
122
|
+
"exit or crash-loop. Read-only."
|
|
123
|
+
),
|
|
124
|
+
parameters={
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"container": {"type": "string", "description": "Container name/id."},
|
|
128
|
+
"lines": {
|
|
129
|
+
"type": "integer",
|
|
130
|
+
"minimum": 1,
|
|
131
|
+
"maximum": 500,
|
|
132
|
+
"default": 100,
|
|
133
|
+
"description": "How many lines to tail (1–500).",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
"required": ["container"],
|
|
137
|
+
"additionalProperties": False,
|
|
138
|
+
},
|
|
139
|
+
executor=_docker_logs,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
DOCKER_STATS = Tool(
|
|
143
|
+
name="docker_stats",
|
|
144
|
+
description=(
|
|
145
|
+
"One snapshot of live resource usage for running containers: CPU %, "
|
|
146
|
+
"memory used/limit, net/block I/O. Use for 'which container burns CPU "
|
|
147
|
+
"or RAM'. Read-only; never blocks (always --no-stream)."
|
|
148
|
+
),
|
|
149
|
+
parameters={
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {},
|
|
152
|
+
"additionalProperties": False,
|
|
153
|
+
},
|
|
154
|
+
executor=_docker_stats,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
DOCKER_IMAGES = Tool(
|
|
158
|
+
name="docker_images",
|
|
159
|
+
description=(
|
|
160
|
+
"List local container images: repository, tag, image id, size. Use to "
|
|
161
|
+
"check whether an image/tag exists locally or is dangling. Read-only."
|
|
162
|
+
),
|
|
163
|
+
parameters={
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {},
|
|
166
|
+
"additionalProperties": False,
|
|
167
|
+
},
|
|
168
|
+
executor=_docker_images,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# --- Phase 8: Docker depth — networks, volumes, disk usage -------------------
|
|
172
|
+
|
|
173
|
+
def _docker_networks(args: dict) -> str:
|
|
174
|
+
return read_command_output(("docker", "network", "ls"), timeout=_TIMEOUT_S)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _docker_volumes(args: dict) -> str:
|
|
178
|
+
return read_command_output(("docker", "volume", "ls"), timeout=_TIMEOUT_S)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _docker_disk_usage(args: dict) -> str:
|
|
182
|
+
return read_command_output(("docker", "system", "df"), timeout=_TIMEOUT_S)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
DOCKER_NETWORKS = Tool(
|
|
186
|
+
name="docker_networks",
|
|
187
|
+
description=(
|
|
188
|
+
"List Docker networks on the host (docker network ls): name, driver, "
|
|
189
|
+
"scope. Use for 'which network does my container use', connectivity "
|
|
190
|
+
"and bridge/overlay questions. Read-only."
|
|
191
|
+
),
|
|
192
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
193
|
+
executor=_docker_networks,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
DOCKER_VOLUMES = Tool(
|
|
197
|
+
name="docker_volumes",
|
|
198
|
+
description=(
|
|
199
|
+
"List Docker volumes on the host (docker volume ls): volume names and "
|
|
200
|
+
"drivers. Use for 'does the volume my data lives in exist', dangling "
|
|
201
|
+
"volume checks. Read-only."
|
|
202
|
+
),
|
|
203
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
204
|
+
executor=_docker_volumes,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
DOCKER_DISK_USAGE = Tool(
|
|
208
|
+
name="docker_disk_usage",
|
|
209
|
+
description=(
|
|
210
|
+
"Docker disk usage on the host (docker system df): how much space "
|
|
211
|
+
"images, containers, volumes and the build cache consume, with "
|
|
212
|
+
"reclaimable amounts. Use for 'is the disk full because of Docker'. "
|
|
213
|
+
"Read-only."
|
|
214
|
+
),
|
|
215
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
216
|
+
executor=_docker_disk_usage,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# --- Phase 9: Docker Compose — compose project listing and one project's
|
|
220
|
+
# services. Same contract: fixed argv, validated project name, ps/ls only. ---
|
|
221
|
+
|
|
222
|
+
def _docker_compose_ls(args: dict) -> str:
|
|
223
|
+
return read_command_output(("docker", "compose", "ls"), timeout=_TIMEOUT_S)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _docker_compose_ps(args: dict) -> str:
|
|
227
|
+
project = args.get("project")
|
|
228
|
+
if project is not None:
|
|
229
|
+
_check_name(project, "compose project")
|
|
230
|
+
argv = ("docker", "compose", "-p", project, "ps", "-a")
|
|
231
|
+
else:
|
|
232
|
+
# No -p: reads the compose project of the current working directory.
|
|
233
|
+
argv = ("docker", "compose", "ps", "-a")
|
|
234
|
+
return read_command_output(argv, timeout=_TIMEOUT_S)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
DOCKER_COMPOSE_LS = Tool(
|
|
238
|
+
name="docker_compose_ls",
|
|
239
|
+
description=(
|
|
240
|
+
"List running Docker Compose projects on the host (docker compose "
|
|
241
|
+
"ls): project name, status, config files. Use to see which compose "
|
|
242
|
+
"stacks exist before looking at one's services. Read-only."
|
|
243
|
+
),
|
|
244
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
245
|
+
executor=_docker_compose_ls,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
DOCKER_COMPOSE_PS = Tool(
|
|
249
|
+
name="docker_compose_ps",
|
|
250
|
+
description=(
|
|
251
|
+
"List the containers of a Docker Compose project (docker compose "
|
|
252
|
+
"ps -a): service, state, ports — running AND stopped. Use with "
|
|
253
|
+
"docker_compose_ls to see what a stack is made of, or which service "
|
|
254
|
+
"inside a project exited. Without 'project', uses the compose file "
|
|
255
|
+
"in the current working directory. Read-only."
|
|
256
|
+
),
|
|
257
|
+
parameters={
|
|
258
|
+
"type": "object",
|
|
259
|
+
"properties": {
|
|
260
|
+
"project": {
|
|
261
|
+
"type": "string",
|
|
262
|
+
"description": "Optional compose project name (from "
|
|
263
|
+
"docker_compose_ls).",
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
"additionalProperties": False,
|
|
267
|
+
},
|
|
268
|
+
executor=_docker_compose_ps,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
register(DOCKER_PS)
|
|
272
|
+
register(DOCKER_INSPECT)
|
|
273
|
+
register(DOCKER_LOGS)
|
|
274
|
+
register(DOCKER_STATS)
|
|
275
|
+
register(DOCKER_IMAGES)
|
|
276
|
+
register(DOCKER_NETWORKS)
|
|
277
|
+
register(DOCKER_VOLUMES)
|
|
278
|
+
register(DOCKER_DISK_USAGE)
|
|
279
|
+
register(DOCKER_COMPOSE_LS)
|
|
280
|
+
register(DOCKER_COMPOSE_PS)
|