diagram-to-iac 0.6.0__py3-none-any.whl → 0.8.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.
- diagram_to_iac/__init__.py +10 -0
- diagram_to_iac/actions/__init__.py +7 -0
- diagram_to_iac/actions/git_entry.py +174 -0
- diagram_to_iac/actions/supervisor_entry.py +116 -0
- diagram_to_iac/actions/terraform_agent_entry.py +207 -0
- diagram_to_iac/agents/__init__.py +26 -0
- diagram_to_iac/agents/demonstrator_langgraph/__init__.py +10 -0
- diagram_to_iac/agents/demonstrator_langgraph/agent.py +826 -0
- diagram_to_iac/agents/git_langgraph/__init__.py +10 -0
- diagram_to_iac/agents/git_langgraph/agent.py +1018 -0
- diagram_to_iac/agents/git_langgraph/pr.py +146 -0
- diagram_to_iac/agents/hello_langgraph/__init__.py +9 -0
- diagram_to_iac/agents/hello_langgraph/agent.py +621 -0
- diagram_to_iac/agents/policy_agent/__init__.py +15 -0
- diagram_to_iac/agents/policy_agent/agent.py +507 -0
- diagram_to_iac/agents/policy_agent/integration_example.py +191 -0
- diagram_to_iac/agents/policy_agent/tools/__init__.py +14 -0
- diagram_to_iac/agents/policy_agent/tools/tfsec_tool.py +259 -0
- diagram_to_iac/agents/shell_langgraph/__init__.py +21 -0
- diagram_to_iac/agents/shell_langgraph/agent.py +122 -0
- diagram_to_iac/agents/shell_langgraph/detector.py +50 -0
- diagram_to_iac/agents/supervisor_langgraph/__init__.py +17 -0
- diagram_to_iac/agents/supervisor_langgraph/agent.py +1947 -0
- diagram_to_iac/agents/supervisor_langgraph/demonstrator.py +22 -0
- diagram_to_iac/agents/supervisor_langgraph/guards.py +23 -0
- diagram_to_iac/agents/supervisor_langgraph/pat_loop.py +49 -0
- diagram_to_iac/agents/supervisor_langgraph/router.py +9 -0
- diagram_to_iac/agents/terraform_langgraph/__init__.py +15 -0
- diagram_to_iac/agents/terraform_langgraph/agent.py +1216 -0
- diagram_to_iac/agents/terraform_langgraph/parser.py +76 -0
- diagram_to_iac/core/__init__.py +7 -0
- diagram_to_iac/core/agent_base.py +19 -0
- diagram_to_iac/core/enhanced_memory.py +302 -0
- diagram_to_iac/core/errors.py +4 -0
- diagram_to_iac/core/issue_tracker.py +49 -0
- diagram_to_iac/core/memory.py +132 -0
- diagram_to_iac/services/__init__.py +10 -0
- diagram_to_iac/services/observability.py +59 -0
- diagram_to_iac/services/step_summary.py +77 -0
- diagram_to_iac/tools/__init__.py +11 -0
- diagram_to_iac/tools/api_utils.py +108 -26
- diagram_to_iac/tools/git/__init__.py +45 -0
- diagram_to_iac/tools/git/git.py +956 -0
- diagram_to_iac/tools/hello/__init__.py +30 -0
- diagram_to_iac/tools/hello/cal_utils.py +31 -0
- diagram_to_iac/tools/hello/text_utils.py +97 -0
- diagram_to_iac/tools/llm_utils/__init__.py +20 -0
- diagram_to_iac/tools/llm_utils/anthropic_driver.py +87 -0
- diagram_to_iac/tools/llm_utils/base_driver.py +90 -0
- diagram_to_iac/tools/llm_utils/gemini_driver.py +89 -0
- diagram_to_iac/tools/llm_utils/openai_driver.py +93 -0
- diagram_to_iac/tools/llm_utils/router.py +303 -0
- diagram_to_iac/tools/sec_utils.py +4 -2
- diagram_to_iac/tools/shell/__init__.py +17 -0
- diagram_to_iac/tools/shell/shell.py +415 -0
- diagram_to_iac/tools/text_utils.py +277 -0
- diagram_to_iac/tools/tf/terraform.py +851 -0
- diagram_to_iac-0.8.0.dist-info/METADATA +99 -0
- diagram_to_iac-0.8.0.dist-info/RECORD +64 -0
- {diagram_to_iac-0.6.0.dist-info → diagram_to_iac-0.8.0.dist-info}/WHEEL +1 -1
- diagram_to_iac-0.8.0.dist-info/entry_points.txt +4 -0
- diagram_to_iac/agents/codegen_agent.py +0 -0
- diagram_to_iac/agents/consensus_agent.py +0 -0
- diagram_to_iac/agents/deployment_agent.py +0 -0
- diagram_to_iac/agents/github_agent.py +0 -0
- diagram_to_iac/agents/interpretation_agent.py +0 -0
- diagram_to_iac/agents/question_agent.py +0 -0
- diagram_to_iac/agents/supervisor.py +0 -0
- diagram_to_iac/agents/vision_agent.py +0 -0
- diagram_to_iac/core/config.py +0 -0
- diagram_to_iac/tools/cv_utils.py +0 -0
- diagram_to_iac/tools/gh_utils.py +0 -0
- diagram_to_iac/tools/tf_utils.py +0 -0
- diagram_to_iac-0.6.0.dist-info/METADATA +0 -16
- diagram_to_iac-0.6.0.dist-info/RECORD +0 -32
- diagram_to_iac-0.6.0.dist-info/entry_points.txt +0 -2
- {diagram_to_iac-0.6.0.dist-info → diagram_to_iac-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
Policy Agent Tools
|
3
|
+
|
4
|
+
This module provides security policy enforcement tools for the PolicyAgent.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .tfsec_tool import TfSecTool, TfSecScanInput, TfSecScanOutput, TfSecFinding
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"TfSecTool",
|
11
|
+
"TfSecScanInput",
|
12
|
+
"TfSecScanOutput",
|
13
|
+
"TfSecFinding"
|
14
|
+
]
|
@@ -0,0 +1,259 @@
|
|
1
|
+
"""
|
2
|
+
TfSec Tool - Policy scanning tool for Terraform configurations
|
3
|
+
|
4
|
+
This tool provides secure execution of tfsec policy scanning following
|
5
|
+
the established security patterns in the diagram-to-iac system.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import os
|
10
|
+
import time
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Dict, List, Any, Optional
|
13
|
+
from pydantic import BaseModel, Field
|
14
|
+
|
15
|
+
from diagram_to_iac.tools.shell.shell import ShellExecutor, ShellExecInput
|
16
|
+
|
17
|
+
|
18
|
+
class TfSecScanInput(BaseModel):
|
19
|
+
"""Input for tfsec security scan."""
|
20
|
+
repo_path: str = Field(..., description="Path to Terraform repository to scan")
|
21
|
+
output_format: str = Field(default="json", description="Output format (json, text)")
|
22
|
+
severity_filter: Optional[List[str]] = Field(default=None, description="Filter by severity levels")
|
23
|
+
timeout: int = Field(default=120, description="Timeout in seconds for scan")
|
24
|
+
|
25
|
+
|
26
|
+
class TfSecFinding(BaseModel):
|
27
|
+
"""Represents a single tfsec finding."""
|
28
|
+
rule_id: str = Field(..., description="tfsec rule ID")
|
29
|
+
rule_description: str = Field(..., description="Description of the rule")
|
30
|
+
severity: str = Field(..., description="Severity level (CRITICAL, HIGH, MEDIUM, LOW)")
|
31
|
+
resource: str = Field(..., description="Terraform resource affected")
|
32
|
+
location: Dict[str, Any] = Field(..., description="Location information")
|
33
|
+
description: str = Field(..., description="Finding description")
|
34
|
+
|
35
|
+
|
36
|
+
class TfSecScanOutput(BaseModel):
|
37
|
+
"""Output from tfsec security scan."""
|
38
|
+
scan_successful: bool = Field(..., description="Whether scan completed successfully")
|
39
|
+
findings: List[TfSecFinding] = Field(default_factory=list, description="Security findings")
|
40
|
+
total_findings: int = Field(default=0, description="Total number of findings")
|
41
|
+
critical_count: int = Field(default=0, description="Number of critical findings")
|
42
|
+
high_count: int = Field(default=0, description="Number of high severity findings")
|
43
|
+
medium_count: int = Field(default=0, description="Number of medium severity findings")
|
44
|
+
low_count: int = Field(default=0, description="Number of low severity findings")
|
45
|
+
scan_duration: float = Field(default=0.0, description="Scan duration in seconds")
|
46
|
+
raw_output: str = Field(default="", description="Raw tfsec output")
|
47
|
+
error_message: Optional[str] = Field(default=None, description="Error message if scan failed")
|
48
|
+
|
49
|
+
|
50
|
+
class TfSecTool:
|
51
|
+
"""
|
52
|
+
TfSec tool for Terraform security scanning.
|
53
|
+
|
54
|
+
This tool provides secure execution of tfsec following the established
|
55
|
+
security patterns in the diagram-to-iac system.
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(self, config_path: str = None):
|
59
|
+
"""Initialize TfSec tool with security executor."""
|
60
|
+
self.shell_executor = ShellExecutor(config_path=config_path)
|
61
|
+
|
62
|
+
def scan(self, scan_input: TfSecScanInput) -> TfSecScanOutput:
|
63
|
+
"""
|
64
|
+
Execute tfsec security scan on Terraform repository.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
scan_input: TfSec scan configuration
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
TfSecScanOutput: Scan results and findings
|
71
|
+
"""
|
72
|
+
start_time = time.time()
|
73
|
+
|
74
|
+
try:
|
75
|
+
# Validate repository path exists
|
76
|
+
repo_path = Path(scan_input.repo_path)
|
77
|
+
if not repo_path.exists():
|
78
|
+
return TfSecScanOutput(
|
79
|
+
scan_successful=False,
|
80
|
+
error_message=f"Repository path does not exist: {scan_input.repo_path}"
|
81
|
+
)
|
82
|
+
|
83
|
+
# Build tfsec command
|
84
|
+
command = self._build_tfsec_command(scan_input)
|
85
|
+
|
86
|
+
# Execute scan using secure shell executor
|
87
|
+
shell_input = ShellExecInput(
|
88
|
+
command=command,
|
89
|
+
cwd=scan_input.repo_path,
|
90
|
+
timeout=scan_input.timeout
|
91
|
+
)
|
92
|
+
|
93
|
+
shell_result = self.shell_executor.shell_exec(shell_input)
|
94
|
+
scan_duration = time.time() - start_time
|
95
|
+
|
96
|
+
# Parse tfsec output
|
97
|
+
if shell_result.exit_code == 0 or shell_result.exit_code == 1:
|
98
|
+
# Exit code 1 is normal for tfsec when findings are present
|
99
|
+
return self._parse_tfsec_output(
|
100
|
+
shell_result.output,
|
101
|
+
scan_duration,
|
102
|
+
True
|
103
|
+
)
|
104
|
+
else:
|
105
|
+
return TfSecScanOutput(
|
106
|
+
scan_successful=False,
|
107
|
+
scan_duration=scan_duration,
|
108
|
+
raw_output=shell_result.output,
|
109
|
+
error_message=f"tfsec scan failed with exit code {shell_result.exit_code}"
|
110
|
+
)
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
scan_duration = time.time() - start_time
|
114
|
+
return TfSecScanOutput(
|
115
|
+
scan_successful=False,
|
116
|
+
scan_duration=scan_duration,
|
117
|
+
error_message=f"tfsec scan error: {str(e)}"
|
118
|
+
)
|
119
|
+
|
120
|
+
def _build_tfsec_command(self, scan_input: TfSecScanInput) -> str:
|
121
|
+
"""Build tfsec command with appropriate options."""
|
122
|
+
command_parts = ["tfsec"]
|
123
|
+
|
124
|
+
# Add output format
|
125
|
+
command_parts.extend(["--format", scan_input.output_format])
|
126
|
+
|
127
|
+
# Add severity filter if specified
|
128
|
+
if scan_input.severity_filter:
|
129
|
+
for severity in scan_input.severity_filter:
|
130
|
+
command_parts.extend(["--minimum-severity", severity.upper()])
|
131
|
+
|
132
|
+
# Add current directory (will be set by shell executor)
|
133
|
+
command_parts.append(".")
|
134
|
+
|
135
|
+
return " ".join(command_parts)
|
136
|
+
|
137
|
+
def _parse_tfsec_output(
|
138
|
+
self,
|
139
|
+
raw_output: str,
|
140
|
+
scan_duration: float,
|
141
|
+
scan_successful: bool
|
142
|
+
) -> TfSecScanOutput:
|
143
|
+
"""Parse tfsec JSON output into structured format."""
|
144
|
+
findings = []
|
145
|
+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
146
|
+
|
147
|
+
try:
|
148
|
+
if raw_output.strip():
|
149
|
+
# Parse JSON output from tfsec
|
150
|
+
tfsec_data = json.loads(raw_output)
|
151
|
+
|
152
|
+
# Handle both single results and arrays
|
153
|
+
results = tfsec_data.get("results", [])
|
154
|
+
if not isinstance(results, list):
|
155
|
+
results = [results] if results else []
|
156
|
+
|
157
|
+
for result in results:
|
158
|
+
severity = result.get("severity", "UNKNOWN").upper()
|
159
|
+
|
160
|
+
finding = TfSecFinding(
|
161
|
+
rule_id=result.get("rule_id", ""),
|
162
|
+
rule_description=result.get("rule_description", ""),
|
163
|
+
severity=severity,
|
164
|
+
resource=result.get("resource", ""),
|
165
|
+
location=result.get("location", {}),
|
166
|
+
description=result.get("description", "")
|
167
|
+
)
|
168
|
+
findings.append(finding)
|
169
|
+
|
170
|
+
# Count by severity
|
171
|
+
severity_key = severity.lower()
|
172
|
+
if severity_key in counts:
|
173
|
+
counts[severity_key] += 1
|
174
|
+
|
175
|
+
except json.JSONDecodeError:
|
176
|
+
# If JSON parsing fails, treat as text output with no findings
|
177
|
+
pass
|
178
|
+
except Exception as e:
|
179
|
+
# Log parsing error but continue
|
180
|
+
pass
|
181
|
+
|
182
|
+
return TfSecScanOutput(
|
183
|
+
scan_successful=scan_successful,
|
184
|
+
findings=findings,
|
185
|
+
total_findings=len(findings),
|
186
|
+
critical_count=counts["critical"],
|
187
|
+
high_count=counts["high"],
|
188
|
+
medium_count=counts["medium"],
|
189
|
+
low_count=counts["low"],
|
190
|
+
scan_duration=scan_duration,
|
191
|
+
raw_output=raw_output
|
192
|
+
)
|
193
|
+
|
194
|
+
def should_block_apply(self, scan_output: TfSecScanOutput, block_on_severity: List[str]) -> bool:
|
195
|
+
"""
|
196
|
+
Determine if terraform apply should be blocked based on findings.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
scan_output: Results from tfsec scan
|
200
|
+
block_on_severity: List of severity levels that should block apply
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
bool: True if apply should be blocked
|
204
|
+
"""
|
205
|
+
if not scan_output.scan_successful:
|
206
|
+
# Block on scan failures
|
207
|
+
return True
|
208
|
+
|
209
|
+
block_levels = [level.upper() for level in block_on_severity]
|
210
|
+
|
211
|
+
for finding in scan_output.findings:
|
212
|
+
if finding.severity.upper() in block_levels:
|
213
|
+
return True
|
214
|
+
|
215
|
+
return False
|
216
|
+
|
217
|
+
def create_findings_artifact(
|
218
|
+
self,
|
219
|
+
scan_output: TfSecScanOutput,
|
220
|
+
artifact_path: str
|
221
|
+
) -> bool:
|
222
|
+
"""
|
223
|
+
Create JSON artifact file with scan findings.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
scan_output: Results from tfsec scan
|
227
|
+
artifact_path: Path where to save the artifact
|
228
|
+
|
229
|
+
Returns:
|
230
|
+
bool: True if artifact was created successfully
|
231
|
+
"""
|
232
|
+
try:
|
233
|
+
# Ensure directory exists
|
234
|
+
os.makedirs(os.path.dirname(artifact_path), exist_ok=True)
|
235
|
+
|
236
|
+
# Create artifact data
|
237
|
+
artifact_data = {
|
238
|
+
"scan_timestamp": time.time(),
|
239
|
+
"scan_successful": scan_output.scan_successful,
|
240
|
+
"total_findings": scan_output.total_findings,
|
241
|
+
"severity_counts": {
|
242
|
+
"critical": scan_output.critical_count,
|
243
|
+
"high": scan_output.high_count,
|
244
|
+
"medium": scan_output.medium_count,
|
245
|
+
"low": scan_output.low_count
|
246
|
+
},
|
247
|
+
"scan_duration": scan_output.scan_duration,
|
248
|
+
"findings": [finding.model_dump() for finding in scan_output.findings],
|
249
|
+
"raw_output": scan_output.raw_output
|
250
|
+
}
|
251
|
+
|
252
|
+
# Write artifact file
|
253
|
+
with open(artifact_path, 'w') as f:
|
254
|
+
json.dump(artifact_data, f, indent=2)
|
255
|
+
|
256
|
+
return True
|
257
|
+
|
258
|
+
except Exception as e:
|
259
|
+
return False
|
@@ -0,0 +1,21 @@
|
|
1
|
+
"""
|
2
|
+
Shell Agent - Multi-Agent Architecture
|
3
|
+
|
4
|
+
This agent specializes in secure shell command execution and serves as a shared service
|
5
|
+
for other agents (Git, Terraform, etc.) that need to execute shell commands.
|
6
|
+
|
7
|
+
Exports shell tools for now. Agent implementation coming later.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from diagram_to_iac.tools.shell import shell_exec, ShellExecutor
|
11
|
+
from .agent import ShellAgent, ShellAgentInput, ShellAgentOutput
|
12
|
+
from .detector import build_stack_histogram
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"shell_exec",
|
16
|
+
"ShellExecutor",
|
17
|
+
"ShellAgent",
|
18
|
+
"ShellAgentInput",
|
19
|
+
"ShellAgentOutput",
|
20
|
+
"build_stack_histogram",
|
21
|
+
]
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import uuid
|
2
|
+
import logging
|
3
|
+
from typing import TypedDict, Optional
|
4
|
+
|
5
|
+
from langgraph.graph import StateGraph, END
|
6
|
+
from langgraph.checkpoint.memory import MemorySaver
|
7
|
+
from langchain_core.messages import HumanMessage
|
8
|
+
from pydantic import BaseModel, Field
|
9
|
+
|
10
|
+
from diagram_to_iac.tools.shell import ShellExecInput, ShellExecutor
|
11
|
+
from diagram_to_iac.core.agent_base import AgentBase
|
12
|
+
from diagram_to_iac.core.memory import create_memory
|
13
|
+
from diagram_to_iac.services.observability import log_event
|
14
|
+
|
15
|
+
|
16
|
+
class ShellAgentInput(BaseModel):
|
17
|
+
command: str = Field(..., description="Shell command to execute")
|
18
|
+
cwd: Optional[str] = Field(None, description="Working directory")
|
19
|
+
timeout: Optional[int] = Field(None, description="Execution timeout")
|
20
|
+
thread_id: Optional[str] = Field(None, description="Optional thread id")
|
21
|
+
|
22
|
+
|
23
|
+
class ShellAgentOutput(BaseModel):
|
24
|
+
output: str = Field(..., description="Command output")
|
25
|
+
exit_code: int = Field(..., description="Exit code")
|
26
|
+
thread_id: str = Field(..., description="Thread id used")
|
27
|
+
error_message: Optional[str] = Field(None, description="Error message if failed")
|
28
|
+
|
29
|
+
|
30
|
+
class ShellAgentState(TypedDict):
|
31
|
+
input_message: HumanMessage
|
32
|
+
cwd: Optional[str]
|
33
|
+
timeout: Optional[int]
|
34
|
+
result: str
|
35
|
+
exit_code: int
|
36
|
+
error_message: Optional[str]
|
37
|
+
|
38
|
+
|
39
|
+
class ShellAgent(AgentBase):
|
40
|
+
"""Minimal agent that executes a single shell command using ShellExecutor."""
|
41
|
+
|
42
|
+
def __init__(self, memory_type: str = "persistent"):
|
43
|
+
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
44
|
+
if not logging.getLogger().hasHandlers():
|
45
|
+
logging.basicConfig(
|
46
|
+
level=logging.INFO,
|
47
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(threadName)s - %(message)s",
|
48
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
49
|
+
)
|
50
|
+
|
51
|
+
self.memory_type = memory_type
|
52
|
+
self.memory = create_memory(memory_type)
|
53
|
+
self.checkpointer = MemorySaver()
|
54
|
+
self.shell_executor = ShellExecutor(memory_type=memory_type)
|
55
|
+
self.runnable = self._build_graph()
|
56
|
+
|
57
|
+
def _shell_node(self, state: ShellAgentState) -> ShellAgentState:
|
58
|
+
shell_input = ShellExecInput(
|
59
|
+
command=state["input_message"].content,
|
60
|
+
cwd=state.get("cwd"),
|
61
|
+
timeout=state.get("timeout"),
|
62
|
+
)
|
63
|
+
try:
|
64
|
+
result = self.shell_executor.shell_exec(shell_input)
|
65
|
+
state["result"] = result.output
|
66
|
+
state["exit_code"] = result.exit_code
|
67
|
+
state["error_message"] = None
|
68
|
+
except Exception as e:
|
69
|
+
state["result"] = ""
|
70
|
+
state["exit_code"] = -1
|
71
|
+
state["error_message"] = str(e)
|
72
|
+
return state
|
73
|
+
|
74
|
+
def _build_graph(self):
|
75
|
+
graph_builder = StateGraph(ShellAgentState)
|
76
|
+
graph_builder.add_node("shell_node", self._shell_node)
|
77
|
+
graph_builder.set_entry_point("shell_node")
|
78
|
+
graph_builder.add_edge("shell_node", END)
|
79
|
+
return graph_builder.compile(checkpointer=self.checkpointer)
|
80
|
+
|
81
|
+
def run(self, agent_input: ShellAgentInput) -> ShellAgentOutput:
|
82
|
+
thread_id = agent_input.thread_id or str(uuid.uuid4())
|
83
|
+
log_event(
|
84
|
+
"shell_agent_run_start",
|
85
|
+
command=agent_input.command,
|
86
|
+
thread_id=thread_id,
|
87
|
+
)
|
88
|
+
initial_state: ShellAgentState = {
|
89
|
+
"input_message": HumanMessage(content=agent_input.command),
|
90
|
+
"cwd": agent_input.cwd,
|
91
|
+
"timeout": agent_input.timeout,
|
92
|
+
"result": "",
|
93
|
+
"exit_code": 0,
|
94
|
+
"error_message": None,
|
95
|
+
}
|
96
|
+
|
97
|
+
result_state = self.runnable.invoke(
|
98
|
+
initial_state, config={"configurable": {"thread_id": thread_id}}
|
99
|
+
)
|
100
|
+
|
101
|
+
output = ShellAgentOutput(
|
102
|
+
output=result_state.get("result", ""),
|
103
|
+
exit_code=result_state.get("exit_code", -1),
|
104
|
+
thread_id=thread_id,
|
105
|
+
error_message=result_state.get("error_message"),
|
106
|
+
)
|
107
|
+
|
108
|
+
log_event(
|
109
|
+
"shell_agent_run_end",
|
110
|
+
thread_id=thread_id,
|
111
|
+
exit_code=output.exit_code,
|
112
|
+
error=output.error_message,
|
113
|
+
)
|
114
|
+
|
115
|
+
return output
|
116
|
+
|
117
|
+
# AgentBase requirements
|
118
|
+
def plan(self, *args, **kwargs):
|
119
|
+
return {"description": "Execute shell command"}
|
120
|
+
|
121
|
+
def report(self, *args, **kwargs):
|
122
|
+
return {}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
import fnmatch
|
5
|
+
from typing import Dict
|
6
|
+
|
7
|
+
from .agent import ShellAgent, ShellAgentInput
|
8
|
+
|
9
|
+
|
10
|
+
def _count_files(pattern: str, repo_path: str, shell: ShellAgent) -> int:
|
11
|
+
"""Count files matching pattern using shell find with Python fallback."""
|
12
|
+
try:
|
13
|
+
result = shell.run(
|
14
|
+
ShellAgentInput(
|
15
|
+
command=f"bash -c \"find . -name '{pattern}' -type f | wc -l\"",
|
16
|
+
cwd=repo_path,
|
17
|
+
)
|
18
|
+
)
|
19
|
+
if result.exit_code == 0:
|
20
|
+
return int(result.output.strip())
|
21
|
+
raise RuntimeError(result.error_message or "find failed")
|
22
|
+
except Exception:
|
23
|
+
count = 0
|
24
|
+
for root, _dirs, files in os.walk(repo_path):
|
25
|
+
count += len(fnmatch.filter(files, pattern))
|
26
|
+
return count
|
27
|
+
|
28
|
+
|
29
|
+
def build_stack_histogram(repo_path: str, shell: ShellAgent) -> Dict[str, float]:
|
30
|
+
"""Build a normalized stack histogram for Terraform and shell files."""
|
31
|
+
tf_count = _count_files("*.tf", repo_path, shell)
|
32
|
+
sh_count = _count_files("*.sh", repo_path, shell)
|
33
|
+
|
34
|
+
# Apply weighting
|
35
|
+
tf_weight = tf_count * 2
|
36
|
+
if os.path.exists(os.path.join(repo_path, "main.tf")):
|
37
|
+
tf_weight += 3
|
38
|
+
sh_weight = sh_count
|
39
|
+
|
40
|
+
weights = {}
|
41
|
+
if tf_weight:
|
42
|
+
weights["terraform"] = tf_weight
|
43
|
+
if sh_weight:
|
44
|
+
weights["shell"] = sh_weight
|
45
|
+
|
46
|
+
total = sum(weights.values())
|
47
|
+
if total == 0:
|
48
|
+
return {}
|
49
|
+
|
50
|
+
return {k: v / total for k, v in weights.items()}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""Supervisor LangGraph Agent Package."""
|
2
|
+
|
3
|
+
from .agent import (
|
4
|
+
SupervisorAgent,
|
5
|
+
SupervisorAgentInput,
|
6
|
+
SupervisorAgentOutput,
|
7
|
+
)
|
8
|
+
from .demonstrator import DryRunDemonstrator
|
9
|
+
from .pat_loop import request_and_wait_for_pat
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"SupervisorAgent",
|
13
|
+
"SupervisorAgentInput",
|
14
|
+
"SupervisorAgentOutput",
|
15
|
+
"DryRunDemonstrator",
|
16
|
+
"request_and_wait_for_pat",
|
17
|
+
]
|