oneport-debug-core 0.1.0__tar.gz
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.
- oneport_debug_core-0.1.0/PKG-INFO +42 -0
- oneport_debug_core-0.1.0/README.md +1 -0
- oneport_debug_core-0.1.0/pyproject.toml +65 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/__init__.py +18 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/cli/__init__.py +5 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/cli/output.py +104 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/config/__init__.py +5 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/config/settings.py +138 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/engine/__init__.py +6 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/engine/context_builder.py +85 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/engine/orchestrator.py +176 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/__init__.py +6 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/diff_utils.py +73 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/multi_repo_client.py +121 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/symbol_indexers/__init__.py +15 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/symbol_indexers/base_indexer.py +32 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/symbol_indexers/go_indexer.py +143 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/symbol_indexers/java_indexer.py +261 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/git/symbol_indexers/typescript_indexer.py +181 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/__init__.py +6 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/base.py +22 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/circuit_breaker.py +162 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/providers/__init__.py +7 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/providers/anthropic.py +39 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/providers/local_inference.py +73 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/providers/openai.py +42 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/llm/router.py +131 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/models/__init__.py +5 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/models/rca.py +109 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/security/__init__.py +6 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/security/audit_logger.py +158 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/security/auth_cli.py +114 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/security/cli_auth.py +192 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/security/secret_scanner.py +146 -0
- oneport_debug_core-0.1.0/src/oneport_debug_core/security/secrets_manager.py +86 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oneport-debug-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared core for oneport-debug tools: LLM routing, orchestration, audit logging, multi-repo git client
|
|
5
|
+
Project-URL: Homepage, https://github.com/oneport/debug-tools
|
|
6
|
+
Project-URL: Documentation, https://docs.oneport.dev/debug
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/oneport/debug-tools/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/oneport/debug-tools/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Source Code, https://github.com/oneport/debug-tools
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
Keywords: debugging,devtools,enterprise,llm,observability
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: anthropic>=0.34.0
|
|
21
|
+
Requires-Dist: click>=8.1.7
|
|
22
|
+
Requires-Dist: cryptography>=43.0.0
|
|
23
|
+
Requires-Dist: gitpython>=3.1.43
|
|
24
|
+
Requires-Dist: httpx>=0.27.0
|
|
25
|
+
Requires-Dist: openai>=1.45.0
|
|
26
|
+
Requires-Dist: pydantic-settings>=2.5.0
|
|
27
|
+
Requires-Dist: pydantic>=2.9.0
|
|
28
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
29
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
30
|
+
Requires-Dist: rich>=13.8.0
|
|
31
|
+
Requires-Dist: structlog>=24.4.0
|
|
32
|
+
Requires-Dist: tenacity>=9.0.0
|
|
33
|
+
Provides-Extra: multilang
|
|
34
|
+
Requires-Dist: tree-sitter-go>=0.23.0; extra == 'multilang'
|
|
35
|
+
Requires-Dist: tree-sitter-java>=0.23.0; extra == 'multilang'
|
|
36
|
+
Requires-Dist: tree-sitter-kotlin>=0.3.0; extra == 'multilang'
|
|
37
|
+
Requires-Dist: tree-sitter-python>=0.23.0; extra == 'multilang'
|
|
38
|
+
Requires-Dist: tree-sitter-typescript>=0.23.0; extra == 'multilang'
|
|
39
|
+
Requires-Dist: tree-sitter>=0.23.0; extra == 'multilang'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# oneport-debug-core
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# oneport-debug-core
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oneport-debug-core"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared core for oneport-debug tools: LLM routing, orchestration, audit logging, multi-repo git client"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["debugging", "enterprise", "llm", "devtools", "observability"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Topic :: Software Development :: Debuggers",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"License :: OSI Approved :: Apache Software License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"anthropic>=0.34.0",
|
|
24
|
+
"openai>=1.45.0",
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"pydantic>=2.9.0",
|
|
27
|
+
"pydantic-settings>=2.5.0",
|
|
28
|
+
"structlog>=24.4.0",
|
|
29
|
+
"rich>=13.8.0",
|
|
30
|
+
"tenacity>=9.0.0",
|
|
31
|
+
"gitpython>=3.1.43",
|
|
32
|
+
"python-dotenv>=1.0.0",
|
|
33
|
+
"PyYAML>=6.0.2",
|
|
34
|
+
"click>=8.1.7",
|
|
35
|
+
"cryptography>=43.0.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
# Precise multi-language symbol indexing (Java/Kotlin/Go/TS). The indexers fall
|
|
40
|
+
# back to regex when these aren't installed, so they're optional — install for
|
|
41
|
+
# higher-accuracy matching on large Java/Spring/Go codebases.
|
|
42
|
+
multilang = [
|
|
43
|
+
"tree-sitter>=0.23.0",
|
|
44
|
+
"tree-sitter-python>=0.23.0",
|
|
45
|
+
"tree-sitter-java>=0.23.0",
|
|
46
|
+
"tree-sitter-go>=0.23.0",
|
|
47
|
+
"tree-sitter-typescript>=0.23.0",
|
|
48
|
+
"tree-sitter-kotlin>=0.3.0",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.urls]
|
|
52
|
+
Homepage = "https://github.com/oneport/debug-tools"
|
|
53
|
+
Documentation = "https://docs.oneport.dev/debug"
|
|
54
|
+
"Bug Tracker" = "https://github.com/oneport/debug-tools/issues"
|
|
55
|
+
Changelog = "https://github.com/oneport/debug-tools/blob/main/CHANGELOG.md"
|
|
56
|
+
"Source Code" = "https://github.com/oneport/debug-tools"
|
|
57
|
+
|
|
58
|
+
[project.scripts]
|
|
59
|
+
oneport-auth = "oneport_debug_core.security.auth_cli:main"
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.wheel]
|
|
62
|
+
packages = ["src/oneport_debug_core"]
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.sdist]
|
|
65
|
+
include = ["src/", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 OnePort Debug Contributors
|
|
3
|
+
"""oneport-debug-core: Shared foundation for all oneport-debug enterprise tools."""
|
|
4
|
+
|
|
5
|
+
from oneport_debug_core.models.rca import RCAResult, CodeLocation, Evidence
|
|
6
|
+
from oneport_debug_core.engine.orchestrator import Orchestrator
|
|
7
|
+
from oneport_debug_core.config.settings import AppConfig, load_config
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"RCAResult",
|
|
11
|
+
"CodeLocation",
|
|
12
|
+
"Evidence",
|
|
13
|
+
"Orchestrator",
|
|
14
|
+
"AppConfig",
|
|
15
|
+
"load_config",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 OnePort Debug Contributors
|
|
3
|
+
"""
|
|
4
|
+
Shared Rich-based terminal output utilities.
|
|
5
|
+
All 5 CLI tools use these to produce consistent, readable output.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.syntax import Syntax
|
|
17
|
+
from rich import box
|
|
18
|
+
|
|
19
|
+
from oneport_debug_core.models.rca import RCAResult, Severity
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ensure_utf8_console() -> None:
|
|
25
|
+
"""
|
|
26
|
+
Force stdout/stderr to UTF-8 so Unicode glyphs (✔ ✖ → ■ █) used in CLI
|
|
27
|
+
output don't raise UnicodeEncodeError on Windows consoles that default to
|
|
28
|
+
cp1252. Idempotent and safe to call from every CLI entry point; a no-op
|
|
29
|
+
where the stream can't be reconfigured (e.g. under pytest capture).
|
|
30
|
+
"""
|
|
31
|
+
for stream in (sys.stdout, sys.stderr):
|
|
32
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
33
|
+
if reconfigure is None:
|
|
34
|
+
continue
|
|
35
|
+
try:
|
|
36
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
37
|
+
except (ValueError, OSError):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
_SEVERITY_COLORS = {
|
|
41
|
+
Severity.CRITICAL: "bold red",
|
|
42
|
+
Severity.HIGH: "red",
|
|
43
|
+
Severity.MEDIUM: "yellow",
|
|
44
|
+
Severity.LOW: "green",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_rca(result: RCAResult, output_format: str = "rich") -> None:
|
|
49
|
+
if output_format == "json":
|
|
50
|
+
console.print_json(result.model_dump_json(indent=2))
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
color = _SEVERITY_COLORS.get(result.severity, "white")
|
|
54
|
+
confidence_bar = "█" * int(result.confidence * 10) + "░" * (10 - int(result.confidence * 10))
|
|
55
|
+
|
|
56
|
+
console.print()
|
|
57
|
+
console.print(Panel(
|
|
58
|
+
f"[bold]{result.summary}[/bold]",
|
|
59
|
+
title=f"[{color}]■ {result.severity.value.upper()}[/{color}] oneport-debug/{result.module}",
|
|
60
|
+
border_style=color,
|
|
61
|
+
))
|
|
62
|
+
|
|
63
|
+
console.print(f"\n[bold cyan]Root Cause[/bold cyan]\n{result.root_cause}\n")
|
|
64
|
+
console.print(f"[dim]Confidence:[/dim] [{color}]{confidence_bar}[/{color}] {result.confidence:.0%} | Model: {result.model_used} | {result.duration_ms}ms\n")
|
|
65
|
+
|
|
66
|
+
if result.locations:
|
|
67
|
+
table = Table(title="Affected Code Locations", box=box.SIMPLE_HEAVY, show_lines=False)
|
|
68
|
+
table.add_column("Repo", style="cyan", no_wrap=True)
|
|
69
|
+
table.add_column("File", style="white")
|
|
70
|
+
table.add_column("Line", style="yellow", justify="right")
|
|
71
|
+
table.add_column("Function", style="magenta")
|
|
72
|
+
table.add_column("Blame", style="dim")
|
|
73
|
+
for loc in result.locations:
|
|
74
|
+
table.add_row(
|
|
75
|
+
loc.repo.split("/")[-1],
|
|
76
|
+
loc.file_path,
|
|
77
|
+
str(loc.line_number or "—"),
|
|
78
|
+
loc.function_name or "—",
|
|
79
|
+
loc.blame_author or "—",
|
|
80
|
+
)
|
|
81
|
+
console.print(table)
|
|
82
|
+
|
|
83
|
+
if result.recommended_fix:
|
|
84
|
+
console.print(Panel(
|
|
85
|
+
result.recommended_fix,
|
|
86
|
+
title="[bold green]Recommended Fix[/bold green]",
|
|
87
|
+
border_style="green",
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
console.print(f"[dim]Generated: {result.generated_at.isoformat()} | Trace: {result.trace_id or 'N/A'}[/dim]\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def print_error(message: str, hint: str | None = None) -> None:
|
|
94
|
+
console.print(f"[bold red]✖ Error:[/bold red] {message}")
|
|
95
|
+
if hint:
|
|
96
|
+
console.print(f"[dim] Hint: {hint}[/dim]")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_success(message: str) -> None:
|
|
100
|
+
console.print(f"[bold green]✔[/bold green] {message}")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def print_step(message: str) -> None:
|
|
104
|
+
console.print(f"[cyan]→[/cyan] {message}")
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 OnePort Debug Contributors
|
|
3
|
+
"""
|
|
4
|
+
AppConfig — single source of truth for every oneport-debug tool.
|
|
5
|
+
Loaded from env vars, .env file, or YAML (enterprise config management compatible).
|
|
6
|
+
All secrets are never logged (SecretStr).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
import yaml
|
|
16
|
+
from pydantic import AliasChoices, Field, SecretStr, field_validator
|
|
17
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunMode(str, Enum):
|
|
21
|
+
CLOUD = "cloud" # Anthropic API (default)
|
|
22
|
+
LOCAL = "local" # Air-gapped on-prem inference
|
|
23
|
+
HYBRID = "hybrid" # Local primary, cloud fallback (with explicit opt-in)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AnthropicSettings(BaseSettings):
|
|
27
|
+
model_config = SettingsConfigDict(env_prefix="ANTHROPIC_")
|
|
28
|
+
api_key: SecretStr = Field(default=SecretStr(""), description="ANTHROPIC_API_KEY")
|
|
29
|
+
model: str = Field("claude-sonnet-4-6", description="Model ID")
|
|
30
|
+
max_tokens: int = Field(4096, ge=256, le=8192)
|
|
31
|
+
timeout_s: int = Field(60, ge=10)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OpenAISettings(BaseSettings):
|
|
35
|
+
model_config = SettingsConfigDict(env_prefix="OPENAI_")
|
|
36
|
+
api_key: SecretStr = Field(default=SecretStr(""), description="OPENAI_API_KEY")
|
|
37
|
+
model: str = Field("gpt-4o", description="Fallback model ID")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LocalInferenceSettings(BaseSettings):
|
|
41
|
+
model_config = SettingsConfigDict(env_prefix="LOCAL_INFERENCE_")
|
|
42
|
+
url: str = Field("http://localhost:11434", description="Ollama / vLLM / llama.cpp base URL")
|
|
43
|
+
model: str = Field("deepseek-coder:6.7b", description="Model name on local server")
|
|
44
|
+
api_key: SecretStr = Field(default=SecretStr(""), description="Optional bearer token")
|
|
45
|
+
allowed_hosts: list[str] = Field(
|
|
46
|
+
default_factory=lambda: ["localhost", "127.0.0.1"],
|
|
47
|
+
description="Allowlist — enforced by network_enforcer in air-gap mode",
|
|
48
|
+
)
|
|
49
|
+
timeout_s: int = Field(120, ge=10)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AuditSettings(BaseSettings):
|
|
53
|
+
model_config = SettingsConfigDict(env_prefix="AUDIT_")
|
|
54
|
+
log_path: Path = Field(
|
|
55
|
+
default=Path("/var/log/oneport-debug/audit.jsonl"),
|
|
56
|
+
description="AUDIT_LOG_PATH — must be write-accessible; use /dev/stdout in containers",
|
|
57
|
+
)
|
|
58
|
+
enabled: bool = Field(True, description="Disable only in unit tests")
|
|
59
|
+
max_file_mb: int = Field(100, ge=1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AppConfig(BaseSettings):
|
|
63
|
+
model_config = SettingsConfigDict(
|
|
64
|
+
env_file=".env",
|
|
65
|
+
env_file_encoding="utf-8",
|
|
66
|
+
extra="ignore",
|
|
67
|
+
# Fields below use validation_alias for their documented env vars; without
|
|
68
|
+
# this, the field NAME (e.g. AppConfig(mode=...)) would be silently ignored.
|
|
69
|
+
populate_by_name=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Each top-level field is read from its documented env var via validation_alias.
|
|
73
|
+
# The uppercased field name is kept as an alias so load_config()'s YAML overlay
|
|
74
|
+
# (which sets os.environ[KEY.upper()]) keeps working.
|
|
75
|
+
mode: RunMode = Field(
|
|
76
|
+
RunMode.CLOUD,
|
|
77
|
+
validation_alias=AliasChoices("ONEPORT_MODE", "MODE"),
|
|
78
|
+
description="ONEPORT_MODE=cloud|local|hybrid",
|
|
79
|
+
)
|
|
80
|
+
anthropic: AnthropicSettings = Field(default_factory=AnthropicSettings)
|
|
81
|
+
openai: OpenAISettings = Field(default_factory=OpenAISettings)
|
|
82
|
+
local_inference: LocalInferenceSettings = Field(default_factory=LocalInferenceSettings)
|
|
83
|
+
audit: AuditSettings = Field(default_factory=AuditSettings)
|
|
84
|
+
|
|
85
|
+
# Enterprise proxy / certificate settings
|
|
86
|
+
http_proxy: str | None = Field(
|
|
87
|
+
None,
|
|
88
|
+
validation_alias=AliasChoices("HTTPS_PROXY", "HTTP_PROXY"),
|
|
89
|
+
description="HTTPS_PROXY — for enterprise network egress",
|
|
90
|
+
)
|
|
91
|
+
ca_bundle: Path | None = Field(
|
|
92
|
+
None,
|
|
93
|
+
validation_alias=AliasChoices("SSL_CERT_FILE", "CA_BUNDLE"),
|
|
94
|
+
description="SSL_CERT_FILE — corporate CA bundle path",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Telemetry — used by the tool itself (not the code being debugged)
|
|
98
|
+
otel_endpoint: str | None = Field(
|
|
99
|
+
None,
|
|
100
|
+
validation_alias=AliasChoices("OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_ENDPOINT"),
|
|
101
|
+
description="OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
102
|
+
)
|
|
103
|
+
service_name: str = Field(
|
|
104
|
+
"oneport-debug",
|
|
105
|
+
validation_alias=AliasChoices("OTEL_SERVICE_NAME", "SERVICE_NAME"),
|
|
106
|
+
description="OTEL_SERVICE_NAME",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@field_validator("mode", mode="before")
|
|
110
|
+
@classmethod
|
|
111
|
+
def normalise_mode(cls, v: object) -> str:
|
|
112
|
+
# The default is a RunMode enum; str(RunMode.CLOUD) is 'RunMode.CLOUD',
|
|
113
|
+
# which would fail enum validation. Pass enums through by value and only
|
|
114
|
+
# normalise raw strings coming from env/YAML (e.g. "CLOUD" -> "cloud").
|
|
115
|
+
if isinstance(v, RunMode):
|
|
116
|
+
return v.value
|
|
117
|
+
return str(v).strip().lower()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_config(yaml_path: Path | None = None) -> AppConfig:
|
|
121
|
+
"""
|
|
122
|
+
Load AppConfig with optional YAML overlay.
|
|
123
|
+
YAML keys map 1-to-1 to env var names (snake_case), allowing enterprise
|
|
124
|
+
config management tools (Ansible, Helm values) to supply configuration
|
|
125
|
+
without polluting the environment.
|
|
126
|
+
"""
|
|
127
|
+
overrides: dict = {}
|
|
128
|
+
if yaml_path and yaml_path.exists():
|
|
129
|
+
with yaml_path.open() as fh:
|
|
130
|
+
overrides = yaml.safe_load(fh) or {}
|
|
131
|
+
|
|
132
|
+
# Overlay YAML values as env vars so pydantic-settings picks them up
|
|
133
|
+
for key, value in overrides.items():
|
|
134
|
+
env_key = key.upper()
|
|
135
|
+
if env_key not in os.environ:
|
|
136
|
+
os.environ[env_key] = str(value)
|
|
137
|
+
|
|
138
|
+
return AppConfig()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 OnePort Debug Contributors
|
|
3
|
+
"""
|
|
4
|
+
ContextBuilder — assembles structured LLM prompts from evidence payloads.
|
|
5
|
+
|
|
6
|
+
The prompt schema is intentionally strict:
|
|
7
|
+
- JSON-only output is requested (no prose)
|
|
8
|
+
- Required fields are listed so the model never omits them
|
|
9
|
+
- Token budget is controlled (evidence is truncated, not silently dropped)
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from oneport_debug_core.models.rca import Evidence
|
|
17
|
+
|
|
18
|
+
# Maximum characters of evidence raw data to include per source
|
|
19
|
+
_MAX_EVIDENCE_CHARS = 6000
|
|
20
|
+
_MAX_TOTAL_EVIDENCE_CHARS = 20_000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ContextBuilder:
|
|
24
|
+
def build_rca_prompt(
|
|
25
|
+
self,
|
|
26
|
+
module: str,
|
|
27
|
+
evidence: list[Evidence],
|
|
28
|
+
extra: dict[str, Any] | None = None,
|
|
29
|
+
) -> str:
|
|
30
|
+
evidence_block = self._format_evidence(evidence)
|
|
31
|
+
extra_block = json.dumps(extra or {}, indent=2, default=str)
|
|
32
|
+
|
|
33
|
+
return f"""You are an expert enterprise software debugger analyzing a production incident.
|
|
34
|
+
Module: {module}
|
|
35
|
+
|
|
36
|
+
## Evidence
|
|
37
|
+
{evidence_block}
|
|
38
|
+
|
|
39
|
+
## Additional Context
|
|
40
|
+
{extra_block}
|
|
41
|
+
|
|
42
|
+
## Task
|
|
43
|
+
Analyze the evidence and return a JSON object with EXACTLY these fields:
|
|
44
|
+
{{
|
|
45
|
+
"severity": "critical|high|medium|low",
|
|
46
|
+
"summary": "One paragraph plain-English summary of what happened",
|
|
47
|
+
"root_cause": "Precise technical root cause — service, component, and mechanism",
|
|
48
|
+
"confidence": 0.0–1.0,
|
|
49
|
+
"locations": [
|
|
50
|
+
{{
|
|
51
|
+
"repo": "git remote URL or alias",
|
|
52
|
+
"file_path": "repo-relative path",
|
|
53
|
+
"line_number": integer or null,
|
|
54
|
+
"function_name": "string or null",
|
|
55
|
+
"commit_sha": "string or null",
|
|
56
|
+
"blame_author": "string or null"
|
|
57
|
+
}}
|
|
58
|
+
],
|
|
59
|
+
"recommended_fix": "Step-by-step fix instructions with exact code changes",
|
|
60
|
+
"affected_services": ["service-a", "service-b"],
|
|
61
|
+
"tags": {{"env": "prod", "team": "payments"}}
|
|
62
|
+
}}
|
|
63
|
+
|
|
64
|
+
Rules:
|
|
65
|
+
- Output ONLY the JSON object. No markdown, no explanation, no preamble.
|
|
66
|
+
- If you cannot determine a field, use null — never omit it.
|
|
67
|
+
- locations must reference actual symbols from the evidence; do not hallucinate file paths.
|
|
68
|
+
- confidence = 1.0 only if you have direct log/heap/policy evidence pointing to the exact line.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def _format_evidence(evidence: list[Evidence]) -> str:
|
|
73
|
+
parts: list[str] = []
|
|
74
|
+
total = 0
|
|
75
|
+
for ev in evidence:
|
|
76
|
+
raw_str = json.dumps(ev.raw, indent=2, default=str)
|
|
77
|
+
if len(raw_str) > _MAX_EVIDENCE_CHARS:
|
|
78
|
+
raw_str = raw_str[:_MAX_EVIDENCE_CHARS] + "\n... [truncated for token budget]"
|
|
79
|
+
chunk = f"### Source: {ev.source} (collected {ev.collected_at.isoformat()})\n{raw_str}"
|
|
80
|
+
if total + len(chunk) > _MAX_TOTAL_EVIDENCE_CHARS:
|
|
81
|
+
parts.append("... [remaining evidence truncated — token budget exceeded]")
|
|
82
|
+
break
|
|
83
|
+
parts.append(chunk)
|
|
84
|
+
total += len(chunk)
|
|
85
|
+
return "\n\n".join(parts) if parts else "(no evidence provided)"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 OnePort Debug Contributors
|
|
3
|
+
"""
|
|
4
|
+
Orchestrator — the shared analysis engine used by all 5 oneport-debug tools.
|
|
5
|
+
|
|
6
|
+
Every public method:
|
|
7
|
+
1. Builds a structured prompt from evidence
|
|
8
|
+
2. Calls the LLMRouter (cloud or local depending on config)
|
|
9
|
+
3. Parses the response into a typed RCAResult
|
|
10
|
+
4. Writes a tamper-evident audit log entry (required for SOX/HIPAA/PCI-DSS)
|
|
11
|
+
5. Returns the RCAResult to the caller
|
|
12
|
+
|
|
13
|
+
The orchestrator never crashes on a bad LLM response — it degrades gracefully
|
|
14
|
+
to a low-confidence RCAResult so the CI pipeline / Slack alert still fires.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
|
|
25
|
+
import structlog
|
|
26
|
+
|
|
27
|
+
from oneport_debug_core.config.settings import AppConfig
|
|
28
|
+
from oneport_debug_core.engine.context_builder import ContextBuilder
|
|
29
|
+
from oneport_debug_core.llm.router import LLMRouter
|
|
30
|
+
from oneport_debug_core.llm.base import LLMCompletionOptions
|
|
31
|
+
from oneport_debug_core.models.rca import RCAResult, CodeLocation, Evidence, Severity
|
|
32
|
+
from oneport_debug_core.security.audit_logger import AuditLogger
|
|
33
|
+
from oneport_debug_core.security.secret_scanner import SecretScanner
|
|
34
|
+
|
|
35
|
+
log = structlog.get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Orchestrator:
|
|
39
|
+
def __init__(self, config: AppConfig) -> None:
|
|
40
|
+
self._config = config
|
|
41
|
+
self.llm = LLMRouter(config)
|
|
42
|
+
self._audit = AuditLogger(config.audit)
|
|
43
|
+
self._ctx = ContextBuilder()
|
|
44
|
+
self._scanner = SecretScanner()
|
|
45
|
+
|
|
46
|
+
async def analyze(
|
|
47
|
+
self,
|
|
48
|
+
module: str,
|
|
49
|
+
evidence: list[Evidence],
|
|
50
|
+
extra_context: dict[str, Any] | None = None,
|
|
51
|
+
trace_id: str | None = None,
|
|
52
|
+
) -> RCAResult:
|
|
53
|
+
"""
|
|
54
|
+
Core entry point. Called by every tool's CLI command.
|
|
55
|
+
`module` identifies which tool is calling (tracer, apm, iam, cicd, local).
|
|
56
|
+
"""
|
|
57
|
+
t0 = time.monotonic()
|
|
58
|
+
|
|
59
|
+
prompt = self._ctx.build_rca_prompt(
|
|
60
|
+
module=module,
|
|
61
|
+
evidence=evidence,
|
|
62
|
+
extra=extra_context or {},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# Redact secrets before the prompt leaves the machine
|
|
67
|
+
scan_result = self._scanner.scan(prompt)
|
|
68
|
+
if scan_result.findings:
|
|
69
|
+
log.warning(
|
|
70
|
+
"orchestrator.secrets_redacted",
|
|
71
|
+
module=module,
|
|
72
|
+
findings=[f["type"] for f in scan_result.findings],
|
|
73
|
+
)
|
|
74
|
+
safe_prompt = scan_result.redacted_content
|
|
75
|
+
|
|
76
|
+
raw = await self.llm.complete(safe_prompt, LLMCompletionOptions(temperature=0.05, max_tokens=4000))
|
|
77
|
+
result = self._parse_rca(raw, module, evidence, trace_id)
|
|
78
|
+
except Exception as err:
|
|
79
|
+
log.error("orchestrator.llm_failed", module=module, error=str(err))
|
|
80
|
+
result = self._degraded_rca(module, evidence, trace_id, reason=str(err))
|
|
81
|
+
|
|
82
|
+
result.duration_ms = int((time.monotonic() - t0) * 1000)
|
|
83
|
+
result.model_used = self.llm.active_provider
|
|
84
|
+
|
|
85
|
+
await self._audit.record(
|
|
86
|
+
action="rca_generated",
|
|
87
|
+
module=module,
|
|
88
|
+
provider=self.llm.active_provider,
|
|
89
|
+
mode=str(self._config.mode.value),
|
|
90
|
+
duration_ms=result.duration_ms,
|
|
91
|
+
confidence=result.confidence,
|
|
92
|
+
trace_id=trace_id,
|
|
93
|
+
secrets_redacted=len(scan_result.findings) if "scan_result" in dir() else 0,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------------ #
|
|
99
|
+
# Private helpers #
|
|
100
|
+
# ------------------------------------------------------------------ #
|
|
101
|
+
|
|
102
|
+
def _parse_rca(
|
|
103
|
+
self,
|
|
104
|
+
raw: str,
|
|
105
|
+
module: str,
|
|
106
|
+
evidence: list[Evidence],
|
|
107
|
+
trace_id: str | None,
|
|
108
|
+
) -> RCAResult:
|
|
109
|
+
json_str = self._extract_json(raw)
|
|
110
|
+
try:
|
|
111
|
+
obj = json.loads(json_str)
|
|
112
|
+
except json.JSONDecodeError:
|
|
113
|
+
log.warning("orchestrator.json_parse_failed", raw_preview=raw[:300])
|
|
114
|
+
return self._degraded_rca(module, evidence, trace_id, reason="Model returned non-JSON")
|
|
115
|
+
|
|
116
|
+
locations = [
|
|
117
|
+
CodeLocation(
|
|
118
|
+
repo=str(loc.get("repo", "unknown")),
|
|
119
|
+
file_path=str(loc.get("file_path", "unknown")),
|
|
120
|
+
line_number=loc.get("line_number"),
|
|
121
|
+
function_name=loc.get("function_name"),
|
|
122
|
+
commit_sha=loc.get("commit_sha"),
|
|
123
|
+
blame_author=loc.get("blame_author"),
|
|
124
|
+
)
|
|
125
|
+
for loc in (obj.get("locations") or [])
|
|
126
|
+
if isinstance(loc, dict)
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
return RCAResult(
|
|
130
|
+
module=module,
|
|
131
|
+
trace_id=trace_id,
|
|
132
|
+
severity=self._safe_severity(obj.get("severity", "medium")),
|
|
133
|
+
summary=str(obj.get("summary", "No summary returned.")),
|
|
134
|
+
root_cause=str(obj.get("root_cause", "Unable to determine root cause.")),
|
|
135
|
+
confidence=obj.get("confidence", 0.3),
|
|
136
|
+
locations=locations,
|
|
137
|
+
recommended_fix=obj.get("recommended_fix"),
|
|
138
|
+
evidence=evidence,
|
|
139
|
+
affected_services=obj.get("affected_services", []),
|
|
140
|
+
tags=obj.get("tags", {}),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _degraded_rca(
|
|
145
|
+
module: str,
|
|
146
|
+
evidence: list[Evidence],
|
|
147
|
+
trace_id: str | None,
|
|
148
|
+
reason: str,
|
|
149
|
+
) -> RCAResult:
|
|
150
|
+
return RCAResult(
|
|
151
|
+
module=module,
|
|
152
|
+
trace_id=trace_id,
|
|
153
|
+
severity=Severity.LOW,
|
|
154
|
+
summary=f"Analysis degraded: {reason}",
|
|
155
|
+
root_cause=reason,
|
|
156
|
+
confidence=0.05,
|
|
157
|
+
evidence=evidence,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _extract_json(raw: str) -> str:
|
|
162
|
+
"""Strip markdown fences and extract the outermost JSON object."""
|
|
163
|
+
fence = re.search(r"```(?:json)?\s*([\s\S]*?)```", raw, re.IGNORECASE)
|
|
164
|
+
if fence:
|
|
165
|
+
return fence.group(1).strip()
|
|
166
|
+
first, last = raw.find("{"), raw.rfind("}")
|
|
167
|
+
if first != -1 and last > first:
|
|
168
|
+
return raw[first : last + 1]
|
|
169
|
+
return raw
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _safe_severity(v: Any) -> Severity:
|
|
173
|
+
try:
|
|
174
|
+
return Severity(str(v).lower())
|
|
175
|
+
except ValueError:
|
|
176
|
+
return Severity.MEDIUM
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# Copyright 2024 OnePort Debug Contributors
|
|
3
|
+
from oneport_debug_core.git.multi_repo_client import MultiRepoClient
|
|
4
|
+
from oneport_debug_core.git.diff_utils import parse_unified_diff, added_lines, line_to_function
|
|
5
|
+
|
|
6
|
+
__all__ = ["MultiRepoClient", "parse_unified_diff", "added_lines", "line_to_function"]
|