droidguard 2.0.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.
- droidguard/__init__.py +17 -0
- droidguard/__main__.py +7 -0
- droidguard/_compat.py +176 -0
- droidguard/_errors.py +63 -0
- droidguard/_logging.py +161 -0
- droidguard/adapters/__init__.py +24 -0
- droidguard/adapters/antigravity.py +54 -0
- droidguard/adapters/base.py +28 -0
- droidguard/adapters/claude.py +60 -0
- droidguard/adapters/cline.py +38 -0
- droidguard/adapters/codex.py +90 -0
- droidguard/adapters/copilot.py +50 -0
- droidguard/adapters/cursor.py +59 -0
- droidguard/adapters/qwen.py +38 -0
- droidguard/adapters/windsurf.py +42 -0
- droidguard/android/__init__.py +19 -0
- droidguard/android/gradle.py +129 -0
- droidguard/android/manifest.py +107 -0
- droidguard/android/modules.py +118 -0
- droidguard/android/project.py +62 -0
- droidguard/android/variants.py +51 -0
- droidguard/checks/__init__.py +34 -0
- droidguard/checks/compose.py +122 -0
- droidguard/checks/dagp.py +42 -0
- droidguard/checks/detekt.py +49 -0
- droidguard/checks/gradle_doctor.py +39 -0
- droidguard/checks/konsist.py +88 -0
- droidguard/checks/lint.py +86 -0
- droidguard/checks/models.py +47 -0
- droidguard/checks/perf.py +93 -0
- droidguard/checks/room.py +174 -0
- droidguard/checks/runner.py +164 -0
- droidguard/checks/screenshot.py +60 -0
- droidguard/checks/strings.py +153 -0
- droidguard/cli/__init__.py +5 -0
- droidguard/cli/cmd_deliver.py +152 -0
- droidguard/cli/cmd_doctor.py +88 -0
- droidguard/cli/cmd_explain.py +58 -0
- droidguard/cli/cmd_init.py +118 -0
- droidguard/cli/cmd_preflight.py +88 -0
- droidguard/cli/cmd_rollback.py +92 -0
- droidguard/cli/cmd_selftest.py +105 -0
- droidguard/cli/cmd_status.py +57 -0
- droidguard/cli/cmd_sync.py +81 -0
- droidguard/cli/cmd_verify.py +69 -0
- droidguard/cli/main.py +118 -0
- droidguard/config/__init__.py +35 -0
- droidguard/config/discovery.py +104 -0
- droidguard/config/loader.py +239 -0
- droidguard/config/schema.py +222 -0
- droidguard/device/__init__.py +14 -0
- droidguard/device/logcat.py +151 -0
- droidguard/device/runner.py +117 -0
- droidguard/device/screenshot.py +42 -0
- droidguard/doctor/__init__.py +17 -0
- droidguard/doctor/engine.py +302 -0
- droidguard/doctor/models.py +59 -0
- droidguard/gradle_runner/__init__.py +13 -0
- droidguard/gradle_runner/error_parser.py +105 -0
- droidguard/gradle_runner/executor.py +105 -0
- droidguard/gradle_runner/process.py +97 -0
- droidguard/pm/__init__.py +24 -0
- droidguard/pm/gateway.py +86 -0
- droidguard/pm/github/client.py +110 -0
- droidguard/pm/jira/client.py +133 -0
- droidguard/pm/linear/client.py +131 -0
- droidguard/pm/policy.py +43 -0
- droidguard/pm/zoho/client.py +187 -0
- droidguard/pm/zoho/formatter.py +78 -0
- droidguard/pm/zoho/server.py +159 -0
- droidguard/py.typed +1 -0
- droidguard/review/__init__.py +24 -0
- droidguard/review/adjudicator.py +97 -0
- droidguard/review/consensus.py +118 -0
- droidguard/review/fingerprint.py +117 -0
- droidguard/review/subagents.py +111 -0
- droidguard/safety/__init__.py +20 -0
- droidguard/safety/adb_guard.py +58 -0
- droidguard/safety/git_guard.py +115 -0
- droidguard/safety/hook_bridge.py +120 -0
- droidguard/safety/policy.py +134 -0
- droidguard/safety/shell_guard.py +120 -0
- droidguard/state/__init__.py +18 -0
- droidguard/state/db.py +257 -0
- droidguard/state/ledger.py +126 -0
- droidguard/state/models.py +133 -0
- droidguard/state/trajectory.py +103 -0
- droidguard-2.0.0.dist-info/METADATA +280 -0
- droidguard-2.0.0.dist-info/RECORD +93 -0
- droidguard-2.0.0.dist-info/WHEEL +5 -0
- droidguard-2.0.0.dist-info/entry_points.txt +3 -0
- droidguard-2.0.0.dist-info/licenses/LICENSE +21 -0
- droidguard-2.0.0.dist-info/top_level.txt +1 -0
droidguard/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""DroidGuard — Deterministic Android Verification & AI Agent Governance Engine.
|
|
2
|
+
|
|
3
|
+
Provides fail-closed tool inspection, multi-agent review consensus, supervised
|
|
4
|
+
Gradle execution, multi-locale parity validation, Room schema diff checks,
|
|
5
|
+
Jetpack Compose stability analysis, and unified PM gateways.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "2.0.0"
|
|
9
|
+
__author__ = "DroidGuard Contributors"
|
|
10
|
+
__license__ = "Apache-2.0"
|
|
11
|
+
|
|
12
|
+
from droidguard._errors import DroidGuardError
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"DroidGuardError",
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
droidguard/__main__.py
ADDED
droidguard/_compat.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Cross-platform compatibility layer for DroidGuard.
|
|
2
|
+
|
|
3
|
+
Handles operating system differences for process trees (Win32 Job Objects on Windows,
|
|
4
|
+
process groups on Unix), atomic file writes, filesystem permissions, and terminal coloring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import signal
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_windows() -> bool:
|
|
19
|
+
"""Return True if running on Windows."""
|
|
20
|
+
return sys.platform == "win32" or sys.platform == "cygwin" or os.name == "nt"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_macos() -> bool:
|
|
24
|
+
"""Return True if running on macOS (Darwin)."""
|
|
25
|
+
return sys.platform == "darwin"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_linux() -> bool:
|
|
29
|
+
"""Return True if running on Linux."""
|
|
30
|
+
return sys.platform.startswith("linux")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def make_executable(path: Path | str) -> None:
|
|
34
|
+
"""Set the executable permission bit (0o755) on POSIX systems safely.
|
|
35
|
+
|
|
36
|
+
On Windows, this is a safe no-op.
|
|
37
|
+
"""
|
|
38
|
+
target = Path(path)
|
|
39
|
+
if not is_windows() and target.exists():
|
|
40
|
+
try:
|
|
41
|
+
current_mode = target.stat().st_mode
|
|
42
|
+
target.chmod(current_mode | 0o111 | 0o700)
|
|
43
|
+
except OSError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def atomic_write_text(path: Path | str, content: str, encoding: str = "utf-8") -> None:
|
|
48
|
+
"""Write text atomically using a temporary file and os.replace."""
|
|
49
|
+
target = Path(path).resolve()
|
|
50
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
temp_dir = target.parent
|
|
53
|
+
fd, tmp_path = tempfile.mkstemp(prefix=f".{target.name}.tmp_", dir=temp_dir, text=True)
|
|
54
|
+
try:
|
|
55
|
+
with os.fdopen(fd, "w", encoding=encoding, newline="") as f:
|
|
56
|
+
f.write(content)
|
|
57
|
+
os.replace(tmp_path, target)
|
|
58
|
+
except Exception:
|
|
59
|
+
if os.path.exists(tmp_path):
|
|
60
|
+
try:
|
|
61
|
+
os.remove(tmp_path)
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def atomic_write_bytes(path: Path | str, content: bytes) -> None:
|
|
68
|
+
"""Write binary data atomically using a temporary file and os.replace."""
|
|
69
|
+
target = Path(path).resolve()
|
|
70
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
temp_dir = target.parent
|
|
73
|
+
fd, tmp_path = tempfile.mkstemp(prefix=f".{target.name}.tmp_", dir=temp_dir, text=False)
|
|
74
|
+
try:
|
|
75
|
+
with os.fdopen(fd, "wb") as f:
|
|
76
|
+
f.write(content)
|
|
77
|
+
os.replace(tmp_path, target)
|
|
78
|
+
except Exception:
|
|
79
|
+
if os.path.exists(tmp_path):
|
|
80
|
+
try:
|
|
81
|
+
os.remove(tmp_path)
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def safe_rmtree(path: Path | str) -> None:
|
|
88
|
+
"""Safely remove a directory tree, ignoring missing errors."""
|
|
89
|
+
target = Path(path)
|
|
90
|
+
if target.exists():
|
|
91
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Win32JobObject:
|
|
95
|
+
"""Context manager / helper for Windows Win32 Job Objects."""
|
|
96
|
+
|
|
97
|
+
def __init__(self) -> None:
|
|
98
|
+
self._job_handle: Any = None
|
|
99
|
+
if is_windows():
|
|
100
|
+
try:
|
|
101
|
+
import ctypes
|
|
102
|
+
import ctypes.wintypes
|
|
103
|
+
|
|
104
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
105
|
+
self._create_job = kernel32.CreateJobObjectW
|
|
106
|
+
self._create_job.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p]
|
|
107
|
+
self._create_job.restype = ctypes.wintypes.HANDLE
|
|
108
|
+
|
|
109
|
+
self._assign_job = kernel32.AssignProcessToJobObject
|
|
110
|
+
self._assign_job.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.HANDLE]
|
|
111
|
+
self._assign_job.restype = ctypes.wintypes.BOOL
|
|
112
|
+
|
|
113
|
+
self._terminate_job = kernel32.TerminateJobObject
|
|
114
|
+
self._terminate_job.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.UINT]
|
|
115
|
+
self._terminate_job.restype = ctypes.wintypes.BOOL
|
|
116
|
+
|
|
117
|
+
self._close_handle = kernel32.CloseHandle
|
|
118
|
+
self._close_handle.argtypes = [ctypes.wintypes.HANDLE]
|
|
119
|
+
self._close_handle.restype = ctypes.wintypes.BOOL
|
|
120
|
+
|
|
121
|
+
self._job_handle = self._create_job(None, None)
|
|
122
|
+
except Exception:
|
|
123
|
+
self._job_handle = None
|
|
124
|
+
|
|
125
|
+
def assign_process(self, process_handle: int) -> bool:
|
|
126
|
+
"""Assign a Windows process handle to this job object."""
|
|
127
|
+
if is_windows() and self._job_handle and self._assign_job:
|
|
128
|
+
try:
|
|
129
|
+
return bool(self._assign_job(self._job_handle, process_handle))
|
|
130
|
+
except Exception:
|
|
131
|
+
return False
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def terminate(self, exit_code: int = 1) -> bool:
|
|
135
|
+
"""Terminate all processes assigned to this job object."""
|
|
136
|
+
if is_windows() and self._job_handle and self._terminate_job:
|
|
137
|
+
try:
|
|
138
|
+
return bool(self._terminate_job(self._job_handle, exit_code))
|
|
139
|
+
except Exception:
|
|
140
|
+
return False
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
def close(self) -> None:
|
|
144
|
+
"""Close the job object handle."""
|
|
145
|
+
if is_windows() and self._job_handle and self._close_handle:
|
|
146
|
+
try:
|
|
147
|
+
self._close_handle(self._job_handle)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
finally:
|
|
151
|
+
self._job_handle = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def configure_utf8_streams() -> None:
|
|
155
|
+
"""Configure sys.stdout and sys.stderr to use UTF-8 with fallback replacement to prevent Windows CP1252 UnicodeEncodeErrors."""
|
|
156
|
+
for stream in (sys.stdout, sys.stderr):
|
|
157
|
+
if hasattr(stream, "reconfigure"):
|
|
158
|
+
try:
|
|
159
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def kill_posix_process_group(pid: int) -> None:
|
|
165
|
+
"""Safely terminate a POSIX process group without crashing on non-POSIX platforms."""
|
|
166
|
+
if not is_windows():
|
|
167
|
+
try:
|
|
168
|
+
getpgid = getattr(os, "getpgid", None)
|
|
169
|
+
killpg = getattr(os, "killpg", None)
|
|
170
|
+
if getpgid and killpg:
|
|
171
|
+
pgid = getpgid(pid)
|
|
172
|
+
killpg(pgid, signal.SIGTERM)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
|
droidguard/_errors.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Structured Exception Hierarchy for DroidGuard.
|
|
2
|
+
|
|
3
|
+
Every exception raised by DroidGuard derives from DroidGuardError.
|
|
4
|
+
All error messages must be actionable, clear, and descriptive.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DroidGuardError(Exception):
|
|
11
|
+
"""Base exception for all DroidGuard errors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, message: str, hint: str | None = None) -> None:
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
self.message = message
|
|
16
|
+
self.hint = hint
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
if self.hint:
|
|
20
|
+
return f"{self.message}\n Hint: {self.hint}"
|
|
21
|
+
return self.message
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigError(DroidGuardError):
|
|
25
|
+
"""Raised when droidguard.toml is invalid, missing required keys, or corrupted."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SafetyViolation(DroidGuardError):
|
|
29
|
+
"""Raised when an AI tool call attempts an unsafe or prohibited command.
|
|
30
|
+
|
|
31
|
+
Fails closed (deny-by-default) to protect git history, hardware, and developer data.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CheckError(DroidGuardError):
|
|
36
|
+
"""Raised when a static or architectural check fails."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CheckSkipped(DroidGuardError):
|
|
40
|
+
"""Raised/used when an optional check is skipped due to missing project components.
|
|
41
|
+
|
|
42
|
+
Not a failure; used for graceful degradation.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BuildError(DroidGuardError):
|
|
47
|
+
"""Raised when a Gradle build or compilation task fails."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PMError(DroidGuardError):
|
|
51
|
+
"""Raised when a Project Management integration (Zoho, Jira, Linear, GitHub) fails."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DeviceError(DroidGuardError):
|
|
55
|
+
"""Raised when an ADB or device interaction fails."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StateError(DroidGuardError):
|
|
59
|
+
"""Raised when an internal state, SQLite, or lock operation fails."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NonAndroidProjectError(DroidGuardError):
|
|
63
|
+
"""Raised when a command is executed in a directory that is not an Android project."""
|
droidguard/_logging.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Structured Logging and Terminal Output for DroidGuard.
|
|
2
|
+
|
|
3
|
+
Follows Rule 17 (Unicode & UTF-8 Safety) and Rule 18 (Unified Logging).
|
|
4
|
+
Handles legacy Windows console code pages (cp1252, cp437) gracefully with ASCII fallbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from typing import TextIO
|
|
13
|
+
|
|
14
|
+
from droidguard._compat import is_windows
|
|
15
|
+
|
|
16
|
+
_LOG_FORMAT = "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s"
|
|
17
|
+
_DATE_FORMAT = "%H:%M:%S"
|
|
18
|
+
|
|
19
|
+
# ANSI Colors
|
|
20
|
+
_COLOR_RESET = "\033[0m"
|
|
21
|
+
_COLOR_BOLD = "\033[1m"
|
|
22
|
+
_COLOR_RED = "\033[31m"
|
|
23
|
+
_COLOR_GREEN = "\033[32m"
|
|
24
|
+
_COLOR_YELLOW = "\033[33m"
|
|
25
|
+
_COLOR_BLUE = "\033[34m"
|
|
26
|
+
_COLOR_CYAN = "\033[36m"
|
|
27
|
+
_COLOR_GRAY = "\033[90m"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _can_encode_symbol(symbol: str, stream: TextIO) -> bool:
|
|
31
|
+
"""Return True if the stream can encode the given symbol."""
|
|
32
|
+
encoding = getattr(stream, "encoding", None) or "utf-8"
|
|
33
|
+
try:
|
|
34
|
+
symbol.encode(encoding)
|
|
35
|
+
return True
|
|
36
|
+
except (UnicodeEncodeError, LookupError):
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_print(text: str, stream: TextIO | None = None) -> None:
|
|
41
|
+
"""Safely print text to stream with automatic fallback if encoding fails."""
|
|
42
|
+
target_stream = stream or sys.stdout
|
|
43
|
+
encoding = getattr(target_stream, "encoding", None) or "utf-8"
|
|
44
|
+
try:
|
|
45
|
+
target_stream.write(text + "\n")
|
|
46
|
+
target_stream.flush()
|
|
47
|
+
except UnicodeEncodeError:
|
|
48
|
+
safe_bytes = (text + "\n").encode(encoding, errors="replace")
|
|
49
|
+
target_stream.buffer.write(safe_bytes)
|
|
50
|
+
target_stream.buffer.flush()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _supports_color(stream: TextIO) -> bool:
|
|
54
|
+
"""Return True if the stream supports ANSI escape codes."""
|
|
55
|
+
if os.environ.get("NO_COLOR") or os.environ.get("DROIDGUARD_NO_COLOR"):
|
|
56
|
+
return False
|
|
57
|
+
if not hasattr(stream, "isatty") or not stream.isatty():
|
|
58
|
+
return False
|
|
59
|
+
if is_windows():
|
|
60
|
+
return "WT_SESSION" in os.environ or "ANSICON" in os.environ or "TERM" in os.environ
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DroidGuardFormatter(logging.Formatter):
|
|
65
|
+
"""Custom logging formatter with optional terminal colors."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, use_color: bool = True) -> None:
|
|
68
|
+
super().__init__(fmt=_LOG_FORMAT, datefmt=_DATE_FORMAT)
|
|
69
|
+
self.use_color = use_color
|
|
70
|
+
|
|
71
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
72
|
+
formatted = super().format(record)
|
|
73
|
+
if not self.use_color:
|
|
74
|
+
return formatted
|
|
75
|
+
|
|
76
|
+
if record.levelno >= logging.ERROR:
|
|
77
|
+
return f"{_COLOR_RED}{formatted}{_COLOR_RESET}"
|
|
78
|
+
if record.levelno >= logging.WARNING:
|
|
79
|
+
return f"{_COLOR_YELLOW}{formatted}{_COLOR_RESET}"
|
|
80
|
+
if record.levelno >= logging.INFO:
|
|
81
|
+
return f"{_COLOR_CYAN}{formatted}{_COLOR_RESET}"
|
|
82
|
+
return f"{_COLOR_GRAY}{formatted}{_COLOR_RESET}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def setup_logging(verbose: bool = False, log_file: str | None = None) -> None:
|
|
86
|
+
"""Configure root logger for DroidGuard."""
|
|
87
|
+
# Ensure UTF-8 stream reconfiguration if supported
|
|
88
|
+
for stream_name in ("stdout", "stderr"):
|
|
89
|
+
stream = getattr(sys, stream_name, None)
|
|
90
|
+
if stream and hasattr(stream, "reconfigure"):
|
|
91
|
+
try:
|
|
92
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
level = logging.DEBUG if verbose else logging.INFO
|
|
97
|
+
root = logging.getLogger("droidguard")
|
|
98
|
+
root.setLevel(level)
|
|
99
|
+
root.handlers.clear()
|
|
100
|
+
|
|
101
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
102
|
+
console_handler.setLevel(level)
|
|
103
|
+
console_handler.setFormatter(DroidGuardFormatter(use_color=_supports_color(sys.stderr)))
|
|
104
|
+
root.addHandler(console_handler)
|
|
105
|
+
|
|
106
|
+
if log_file:
|
|
107
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
108
|
+
file_handler.setLevel(logging.DEBUG)
|
|
109
|
+
file_handler.setFormatter(DroidGuardFormatter(use_color=False))
|
|
110
|
+
root.addHandler(file_handler)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_logger(name: str) -> logging.Logger:
|
|
114
|
+
"""Return a scoped logger under droidguard.* namespace."""
|
|
115
|
+
if not name.startswith("droidguard"):
|
|
116
|
+
name = f"droidguard.{name}"
|
|
117
|
+
return logging.getLogger(name)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def print_success(message: str) -> None:
|
|
121
|
+
"""Print user-facing green success message."""
|
|
122
|
+
color = _COLOR_GREEN if _supports_color(sys.stdout) else ""
|
|
123
|
+
reset = _COLOR_RESET if _supports_color(sys.stdout) else ""
|
|
124
|
+
sym = "✓" if _can_encode_symbol("✓", sys.stdout) else "[+]"
|
|
125
|
+
_safe_print(f"{color}{sym} {message}{reset}", sys.stdout)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def print_error(message: str) -> None:
|
|
129
|
+
"""Print user-facing red error message."""
|
|
130
|
+
color = _COLOR_RED if _supports_color(sys.stderr) else ""
|
|
131
|
+
reset = _COLOR_RESET if _supports_color(sys.stderr) else ""
|
|
132
|
+
sym = "✗" if _can_encode_symbol("✗", sys.stderr) else "[-]"
|
|
133
|
+
_safe_print(f"{color}{sym} {message}{reset}", sys.stderr)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def print_warning(message: str) -> None:
|
|
137
|
+
"""Print user-facing yellow warning message."""
|
|
138
|
+
color = _COLOR_YELLOW if _supports_color(sys.stdout) else ""
|
|
139
|
+
reset = _COLOR_RESET if _supports_color(sys.stdout) else ""
|
|
140
|
+
sym = "!" if _can_encode_symbol("!", sys.stdout) else "[!]"
|
|
141
|
+
_safe_print(f"{color}{sym} {message}{reset}", sys.stdout)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def print_info(message: str) -> None:
|
|
145
|
+
"""Print user-facing blue info message."""
|
|
146
|
+
color = _COLOR_BLUE if _supports_color(sys.stdout) else ""
|
|
147
|
+
reset = _COLOR_RESET if _supports_color(sys.stdout) else ""
|
|
148
|
+
sym = "ℹ" if _can_encode_symbol("ℹ", sys.stdout) else "[*]"
|
|
149
|
+
_safe_print(f"{color}{sym} {message}{reset}", sys.stdout)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def print_banner() -> None:
|
|
153
|
+
"""Print the DroidGuard banner to stdout."""
|
|
154
|
+
bold = _COLOR_BOLD if _supports_color(sys.stdout) else ""
|
|
155
|
+
green = _COLOR_GREEN if _supports_color(sys.stdout) else ""
|
|
156
|
+
reset = _COLOR_RESET if _supports_color(sys.stdout) else ""
|
|
157
|
+
shield = "🛡️" if _can_encode_symbol("🛡️", sys.stdout) else "[DG]"
|
|
158
|
+
_safe_print(
|
|
159
|
+
f"{bold}{green}{shield} DroidGuard 2.0{reset} — Universal Android Engineering Governance & Verification",
|
|
160
|
+
sys.stdout,
|
|
161
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""DroidGuard IDE and AI Assistant Adapters Package."""
|
|
2
|
+
|
|
3
|
+
from droidguard.adapters.antigravity import AntigravityAdapter
|
|
4
|
+
from droidguard.adapters.base import AdapterSyncResult, BaseAdapter
|
|
5
|
+
from droidguard.adapters.claude import ClaudeAdapter
|
|
6
|
+
from droidguard.adapters.cline import ClineAdapter
|
|
7
|
+
from droidguard.adapters.codex import CodexAdapter
|
|
8
|
+
from droidguard.adapters.copilot import CopilotAdapter
|
|
9
|
+
from droidguard.adapters.cursor import CursorAdapter
|
|
10
|
+
from droidguard.adapters.qwen import QwenAdapter
|
|
11
|
+
from droidguard.adapters.windsurf import WindsurfAdapter
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AdapterSyncResult",
|
|
15
|
+
"AntigravityAdapter",
|
|
16
|
+
"BaseAdapter",
|
|
17
|
+
"ClaudeAdapter",
|
|
18
|
+
"ClineAdapter",
|
|
19
|
+
"CodexAdapter",
|
|
20
|
+
"CopilotAdapter",
|
|
21
|
+
"CursorAdapter",
|
|
22
|
+
"QwenAdapter",
|
|
23
|
+
"WindsurfAdapter",
|
|
24
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Antigravity Sidecar and Agent Environment Adapter for DroidGuard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from droidguard._compat import atomic_write_text
|
|
8
|
+
from droidguard.adapters.base import AdapterSyncResult, BaseAdapter
|
|
9
|
+
from droidguard.config.schema import DroidGuardConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AntigravityAdapter(BaseAdapter):
|
|
13
|
+
"""Adapter for Google Antigravity AI agent platform."""
|
|
14
|
+
|
|
15
|
+
name: str = "antigravity"
|
|
16
|
+
|
|
17
|
+
def sync(self, project_root: Path, config: DroidGuardConfig) -> AdapterSyncResult:
|
|
18
|
+
created: list[Path] = []
|
|
19
|
+
updated: list[Path] = []
|
|
20
|
+
|
|
21
|
+
agents_dir = project_root / ".agents"
|
|
22
|
+
rules_dir = agents_dir / "rules"
|
|
23
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
rules_file = rules_dir / "droidguard-rules.md"
|
|
26
|
+
content = f"""# DroidGuard Agent Governance Rules for {config.project.name}
|
|
27
|
+
|
|
28
|
+
## 1. Non-Negotiable Governance & Security
|
|
29
|
+
- **Git Authority**: AI agents are strictly forbidden from executing mutating Git commands (`commit`, `push`, `reset`, `rebase`, `checkout -b`). Only the human developer holds Git mutation authority.
|
|
30
|
+
- **Fail-Closed Verification**: Before declaring tasks ready for review, execute `dg preflight`.
|
|
31
|
+
- **Review Quorum**: All builds and release gates require {config.review.leaf_count}-leaf subagent consensus (`dg verify`).
|
|
32
|
+
- **PM Authority**: AI agents may only advance tasks to 'Ready for QA'. Marking tasks as 'Done' or 'Solved' is prohibited.
|
|
33
|
+
|
|
34
|
+
## 2. Android Architecture & Quality Standards
|
|
35
|
+
- **UI Toolkit**: {config.project.ui_toolkit.upper()} (Enforce @Immutable on Compose state models).
|
|
36
|
+
- **DI Framework**: {config.project.di_framework.capitalize()}.
|
|
37
|
+
- **Multi-Locale Parity**: All string resources must be added to `{config.checks.strings.base_locale}/strings.xml` and translated in all target locales.
|
|
38
|
+
- **Room Database**: Any schema change in `schemas/` must have a corresponding `Migration` class.
|
|
39
|
+
- **Performance & ANR**: Never use `runBlocking` or blocking `Thread.sleep()` in production code.
|
|
40
|
+
"""
|
|
41
|
+
is_update = rules_file.exists()
|
|
42
|
+
atomic_write_text(rules_file, content)
|
|
43
|
+
|
|
44
|
+
if is_update:
|
|
45
|
+
updated.append(rules_file)
|
|
46
|
+
else:
|
|
47
|
+
created.append(rules_file)
|
|
48
|
+
|
|
49
|
+
return AdapterSyncResult(
|
|
50
|
+
adapter_name=self.name,
|
|
51
|
+
files_created=created,
|
|
52
|
+
files_updated=updated,
|
|
53
|
+
message=f"Synchronized Antigravity rules at {rules_file.name}.",
|
|
54
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Base Adapter Protocol and Model for AI IDE and Assistant Integrations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from droidguard.config.schema import DroidGuardConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AdapterSyncResult:
|
|
14
|
+
adapter_name: str
|
|
15
|
+
files_created: list[Path]
|
|
16
|
+
files_updated: list[Path]
|
|
17
|
+
is_success: bool = True
|
|
18
|
+
message: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseAdapter(Protocol):
|
|
22
|
+
"""Protocol for all IDE / Agent environment adapters."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
|
|
26
|
+
def sync(self, project_root: Path, config: DroidGuardConfig) -> AdapterSyncResult:
|
|
27
|
+
"""Generate and synchronize rule and config files for this environment."""
|
|
28
|
+
...
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Claude Code CLI Adapter for DroidGuard.
|
|
2
|
+
|
|
3
|
+
Generates `CLAUDE.md` with deterministic build, preflight, and safety instructions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from droidguard._compat import atomic_write_text
|
|
11
|
+
from droidguard.adapters.base import AdapterSyncResult, BaseAdapter
|
|
12
|
+
from droidguard.config.schema import DroidGuardConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ClaudeAdapter(BaseAdapter):
|
|
16
|
+
"""Adapter for Anthropic Claude Code CLI."""
|
|
17
|
+
|
|
18
|
+
name: str = "claude"
|
|
19
|
+
|
|
20
|
+
def sync(self, project_root: Path, config: DroidGuardConfig) -> AdapterSyncResult:
|
|
21
|
+
created: list[Path] = []
|
|
22
|
+
updated: list[Path] = []
|
|
23
|
+
|
|
24
|
+
claude_file = project_root / "CLAUDE.md"
|
|
25
|
+
content = f"""# CLAUDE.md — Android Engineering Guidelines for {config.project.name}
|
|
26
|
+
|
|
27
|
+
## 🛡️ DroidGuard Governance & Safety Policy
|
|
28
|
+
- **Git Mutations Blocked**: Do NOT execute `git commit`, `git push`, `git checkout -b`, or `git reset`. The developer commits code.
|
|
29
|
+
- **Preflight Verification**: Always run `dg preflight` before finishing tasks.
|
|
30
|
+
- **Verification Consensus**: Run `dg verify` to inspect the package hash and consensus quorum.
|
|
31
|
+
- **Delivery Pipeline**: Use `dg deliver` to execute supervised build and post QA reports.
|
|
32
|
+
|
|
33
|
+
## 🛠️ Build & Verification Commands
|
|
34
|
+
- Run Preflight Checks: `dg preflight`
|
|
35
|
+
- Verify Quorum: `dg verify`
|
|
36
|
+
- Supervised Gradle Build: `./gradlew assembleDebug`
|
|
37
|
+
- Supervised Unit Tests: `./gradlew testDebugUnitTest`
|
|
38
|
+
- Android Lint: `./gradlew lintDebug`
|
|
39
|
+
|
|
40
|
+
## 📐 Architecture & Quality Rules
|
|
41
|
+
- **UI Toolkit**: {config.project.ui_toolkit.upper()} (Annotate MVI state data classes with `@Immutable`).
|
|
42
|
+
- **Dependency Injection**: {config.project.di_framework.capitalize()}.
|
|
43
|
+
- **String Parity**: Add string keys to base `values/strings.xml` and target `values-*/strings.xml`.
|
|
44
|
+
- **Room Migrations**: Keep schema versions contiguous (1.json -> 2.json) and define Migration objects.
|
|
45
|
+
- **Performance**: Prohibit `runBlocking`, blocking I/O on Main thread, and holding Context in ViewModels.
|
|
46
|
+
"""
|
|
47
|
+
is_update = claude_file.exists()
|
|
48
|
+
atomic_write_text(claude_file, content)
|
|
49
|
+
|
|
50
|
+
if is_update:
|
|
51
|
+
updated.append(claude_file)
|
|
52
|
+
else:
|
|
53
|
+
created.append(claude_file)
|
|
54
|
+
|
|
55
|
+
return AdapterSyncResult(
|
|
56
|
+
adapter_name=self.name,
|
|
57
|
+
files_created=created,
|
|
58
|
+
files_updated=updated,
|
|
59
|
+
message=f"Synchronized Claude Code guidelines at {claude_file.name}.",
|
|
60
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Cline and Roo Code AI Adapter for DroidGuard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from droidguard._compat import atomic_write_text
|
|
8
|
+
from droidguard.adapters.base import AdapterSyncResult, BaseAdapter
|
|
9
|
+
from droidguard.config.schema import DroidGuardConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClineAdapter(BaseAdapter):
|
|
13
|
+
"""Adapter for Cline and Roo Code AI assistants."""
|
|
14
|
+
|
|
15
|
+
name: str = "cline"
|
|
16
|
+
|
|
17
|
+
def sync(self, project_root: Path, config: DroidGuardConfig) -> AdapterSyncResult:
|
|
18
|
+
created: list[Path] = []
|
|
19
|
+
updated: list[Path] = []
|
|
20
|
+
|
|
21
|
+
rules_file = project_root / ".clinerules"
|
|
22
|
+
content = f"""# Cline & Roo Rules for {config.project.name}
|
|
23
|
+
|
|
24
|
+
- Human developer holds exclusive Git commit authority (no git commits from AI).
|
|
25
|
+
- Run `dg preflight` to verify changes before completing tasks.
|
|
26
|
+
- Architecture: UI={config.project.ui_toolkit.upper()}, DI={config.project.di_framework.capitalize()}.
|
|
27
|
+
- No runBlocking or Thread.sleep in production code.
|
|
28
|
+
"""
|
|
29
|
+
is_update = rules_file.exists()
|
|
30
|
+
atomic_write_text(rules_file, content)
|
|
31
|
+
(updated if is_update else created).append(rules_file)
|
|
32
|
+
|
|
33
|
+
return AdapterSyncResult(
|
|
34
|
+
adapter_name=self.name,
|
|
35
|
+
files_created=created,
|
|
36
|
+
files_updated=updated,
|
|
37
|
+
message=f"Synchronized Cline rules at {rules_file.name}.",
|
|
38
|
+
)
|