Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
python-version: ["3.10", "3.12"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Install uv
uses: astral-sh/setup-uv@v4
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
Expand Down
18 changes: 12 additions & 6 deletions agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,19 @@ def extract_service_name(input_str: str) -> str:
my-service -> my-service
"""
import re
from urllib.parse import urlparse

# Handle GitHub URLs
if "github.com" in input_str:
# Extract repo name from URL
match = re.search(r"github\.com/[^/]+/([^/]+?)(?:\.git)?(?:/.*)?$", input_str)
if match:
return match.group(1)
# Handle GitHub URLs - use proper URL parsing to prevent substring attacks
# e.g., reject "https://github.qkg1.top.attacker.com/..." or "https://attacker.com?q=github.com"
try:
parsed = urlparse(input_str)
if parsed.scheme in ("http", "https") and parsed.netloc == "github.com":
# Extract repo name from URL path
match = re.search(r"^/[^/]+/([^/]+?)(?:\.git)?(?:/.*)?$", parsed.path)
if match:
return match.group(1)
except Exception:
pass

# Handle file paths
if "/" in input_str:
Expand Down
39 changes: 11 additions & 28 deletions agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,19 @@ def load_manifest(artifacts_dir: Path) -> dict | None:


def load_components(artifacts_dir: Path) -> list[dict]:
"""Load components from service_discovery in an existing artifacts directory.
"""Load components from service_discovery in an existing artifacts directory."""
combined = artifacts_dir / "service_discovery" / "components.json"
if not combined.exists():
return []

Merges both 'libraries' and 'applications' arrays from components.json,
falling back to separate libraries.json / applications.json files.
"""
components = []
with open(combined) as f:
data = json.load(f)

# Try components.json first (combined format)
combined = artifacts_dir / "service_discovery" / "components.json"
if combined.exists():
with open(combined) as f:
data = json.load(f)
components.extend(data.get("libraries", []))
components.extend(data.get("applications", []))
return components

# Fall back to separate files
for filename in ("libraries.json", "applications.json"):
path = artifacts_dir / "service_discovery" / filename
if path.exists():
with open(path) as f:
data = json.load(f)
# Handle both array and object-with-key formats
if isinstance(data, list):
components.extend(data)
elif isinstance(data, dict):
for key in ("libraries", "applications"):
components.extend(data.get(key, []))

return components
if isinstance(data, dict):
return data.get("components", [])
if isinstance(data, list):
return data
return []


def git_diff_files(repo_path: Path, last_sha: str, head_sha: str) -> list[str]:
Expand Down
28 changes: 8 additions & 20 deletions agent/discovery/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def discover_components(
except Exception as e:
logger.warning(
"Failed to parse %s with %s plugin: %s",
manifest_path, plugin.name, e,
manifest_path,
plugin.name,
e,
)
continue

Expand Down Expand Up @@ -105,7 +107,8 @@ def _resolve_internal_deps(components: List[Component]) -> None:
else:
logger.debug(
"Could not resolve internal dep '%s' for %s",
dep, comp.name,
dep,
comp.name,
)
comp.internal_dependencies = resolved

Expand All @@ -125,15 +128,11 @@ def _detect_repo_shape(components: List[Component]) -> str:


def _write_output(components: List[Component], output_dir: Path) -> None:
"""Write components.json in the standard format."""
"""Write components.json as a flat list of all components."""
output_dir.mkdir(parents=True, exist_ok=True)

libraries = [c for c in components if c.kind == ComponentKind.LIBRARY]
executables = [c for c in components if c.kind != ComponentKind.LIBRARY]

output = {
"libraries": [c.to_dict() for c in libraries],
"applications": [c.to_dict() for c in executables],
"components": [c.to_dict() for c in components],
"metadata": {
"total_components": len(components),
"by_kind": {
Expand All @@ -148,18 +147,7 @@ def _write_output(components: List[Component], output_dir: Path) -> None:
},
}

# Write combined components.json
with open(output_dir / "components.json", "w") as f:
json.dump(output, f, indent=2)

# Also write separate files for backward compat
with open(output_dir / "libraries.json", "w") as f:
json.dump({"libraries": [c.to_dict() for c in libraries]}, f, indent=2)

with open(output_dir / "applications.json", "w") as f:
json.dump({"applications": [c.to_dict() for c in executables]}, f, indent=2)

logger.info(
"Wrote %d components to %s (%d libraries, %d executables)",
len(components), output_dir, len(libraries), len(executables),
)
logger.info("Wrote %d components to %s", len(components), output_dir)
7 changes: 3 additions & 4 deletions agent/discovery/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,18 @@ def validate_graph(
"""
errors: List[str] = []
name_set = {c.name for c in components}
library_names = {c.name for c in components if c.is_library}

# Every library should be in the depth order
# Every component should appear in exactly one depth level
all_ordered = set()
for level in depth_order:
for name in level:
if name in all_ordered:
errors.append(f"Component '{name}' appears in multiple depth levels")
all_ordered.add(name)

missing = library_names - all_ordered
missing = name_set - all_ordered
if missing:
errors.append(f"Libraries missing from depth order: {missing}")
errors.append(f"Components missing from depth order: {missing}")

# Validate topological property: for each component at depth N,
# all its dependencies should be at depth < N
Expand Down
10 changes: 2 additions & 8 deletions agent/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,22 @@
Component,
ComponentKind,
LanguageType,
KnowledgeBasis,
Application,
Library,
ExternalDependency,
CodeCitation,
component_from_dict,
)
from .dependency_graph import DependencyGraph, ApplicationEdge
from .dependency_graph import DependencyGraph, ComponentEdge
from .manifest import ArtifactManifest, ArtifactFile, MANIFEST_SCHEMA_VERSION

__all__ = [
"Component",
"ComponentKind",
"LanguageType",
"KnowledgeBasis",
"Application",
"Library",
"ExternalDependency",
"CodeCitation",
"component_from_dict",
"DependencyGraph",
"ApplicationEdge",
"ComponentEdge",
"ArtifactManifest",
"ArtifactFile",
"MANIFEST_SCHEMA_VERSION",
Expand Down
Loading
Loading