gdmcode 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.
- gdmcode-0.1.0.dist-info/METADATA +240 -0
- gdmcode-0.1.0.dist-info/RECORD +131 -0
- gdmcode-0.1.0.dist-info/WHEEL +4 -0
- gdmcode-0.1.0.dist-info/entry_points.txt +2 -0
- src/__init__.py +1 -0
- src/_internal/__init__.py +0 -0
- src/_internal/constants.py +244 -0
- src/_internal/domain_skills.py +339 -0
- src/agent/__init__.py +0 -0
- src/agent/commit_classifier.py +91 -0
- src/agent/context_budget.py +391 -0
- src/agent/daemon.py +681 -0
- src/agent/dag_validator.py +153 -0
- src/agent/debug_loop.py +473 -0
- src/agent/impact_analyzer.py +149 -0
- src/agent/impact_graph.py +117 -0
- src/agent/loop.py +1410 -0
- src/agent/orchestrator.py +141 -0
- src/agent/regression_guard.py +251 -0
- src/agent/review_gate.py +648 -0
- src/agent/risk_scorer.py +169 -0
- src/agent/self_healing.py +145 -0
- src/agent/smart_test_selector.py +89 -0
- src/agent/system_prompt.py +226 -0
- src/agent/task_tracker.py +320 -0
- src/agent/test_validator.py +210 -0
- src/agent/tool_orchestrator.py +402 -0
- src/agent/transcript.py +230 -0
- src/agent/verification_loop.py +133 -0
- src/agent/work_director.py +136 -0
- src/agent/worktree_manager.py +53 -0
- src/artifacts/__init__.py +16 -0
- src/artifacts/artifact_store.py +456 -0
- src/artifacts/verification_graph.py +75 -0
- src/auth.py +411 -0
- src/cli.py +1290 -0
- src/commands.py +1398 -0
- src/config.py +762 -0
- src/cost_tracker.py +348 -0
- src/db/__init__.py +4 -0
- src/db/migrations.py +337 -0
- src/enterprise/__init__.py +3 -0
- src/enterprise/audit_log.py +182 -0
- src/enterprise/identity.py +90 -0
- src/enterprise/rbac.py +100 -0
- src/enterprise/team_config.py +125 -0
- src/enterprise/usage_analytics.py +261 -0
- src/exceptions.py +207 -0
- src/git_workflow.py +651 -0
- src/integrations/__init__.py +6 -0
- src/integrations/github_actions.py +106 -0
- src/integrations/mcp_server.py +333 -0
- src/integrations/sentry_integration.py +100 -0
- src/integrations/sentry_server.py +82 -0
- src/integrations/webhook_security.py +19 -0
- src/main.py +27 -0
- src/memory/__init__.py +0 -0
- src/memory/code_index.py +376 -0
- src/memory/compressor.py +378 -0
- src/memory/context_memory.py +135 -0
- src/memory/continuous_memory.py +234 -0
- src/memory/conventions.py +495 -0
- src/memory/db.py +1119 -0
- src/memory/document_index.py +205 -0
- src/memory/file_cache.py +128 -0
- src/memory/project_scanner.py +178 -0
- src/memory/session_store.py +201 -0
- src/models/__init__.py +0 -0
- src/models/client.py +715 -0
- src/models/definitions.py +459 -0
- src/models/router.py +418 -0
- src/models/schemas.py +389 -0
- src/permissions.py +294 -0
- src/remote/__init__.py +5 -0
- src/remote/command_filter.py +33 -0
- src/remote/models.py +31 -0
- src/remote/permission_handler.py +79 -0
- src/remote/phone_ui.py +48 -0
- src/remote/protocol.py +59 -0
- src/remote/qr.py +65 -0
- src/remote/server.py +586 -0
- src/remote/token_manager.py +61 -0
- src/remote/tunnel.py +212 -0
- src/repl.py +475 -0
- src/runtime/__init__.py +1 -0
- src/runtime/branch_farm.py +372 -0
- src/runtime/replay.py +351 -0
- src/sandbox/__init__.py +2 -0
- src/sandbox/hermetic.py +214 -0
- src/sandbox/policy.py +44 -0
- src/sdk/__init__.py +3 -0
- src/sdk/plugin_base.py +39 -0
- src/sdk/plugin_host.py +100 -0
- src/sdk/plugin_loader.py +101 -0
- src/security.py +409 -0
- src/server/__init__.py +7 -0
- src/server/bridge.py +427 -0
- src/server/bridge_cli.py +103 -0
- src/server/bridge_client.py +170 -0
- src/server/protocol_version.py +103 -0
- src/session/__init__.py +10 -0
- src/session/event_fanout.py +46 -0
- src/session/input_broker.py +38 -0
- src/session/permission_bridge.py +100 -0
- src/tools/__init__.py +160 -0
- src/tools/_atomic.py +72 -0
- src/tools/agent_tools.py +423 -0
- src/tools/ask_user_tool.py +83 -0
- src/tools/bash_tool.py +384 -0
- src/tools/browser_tool.py +352 -0
- src/tools/browser_tools.py +179 -0
- src/tools/dep_tools.py +210 -0
- src/tools/document_reader.py +167 -0
- src/tools/document_tool.py +240 -0
- src/tools/document_writer.py +171 -0
- src/tools/impact_tools.py +240 -0
- src/tools/playwright_tool.py +172 -0
- src/tools/quality_tools.py +366 -0
- src/tools/read_tools.py +318 -0
- src/tools/result_cache.py +157 -0
- src/tools/search_tools.py +310 -0
- src/tools/shell_tools.py +311 -0
- src/tools/write_tools.py +337 -0
- src/voice/__init__.py +25 -0
- src/voice/audio_capture.py +92 -0
- src/voice/audio_playback.py +68 -0
- src/voice/errors.py +14 -0
- src/voice/models.py +35 -0
- src/voice/providers.py +143 -0
- src/voice/vad.py +55 -0
- src/voice/voice_loop.py +156 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Usage analytics and cost attribution for enterprise deployments.
|
|
3
|
+
Tracks token usage, tool calls, session costs, and team-level rollups.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import csv
|
|
8
|
+
import sqlite3
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
DB_PATH = Path.home() / ".config" / "gdm" / "analytics.db"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class UsageEvent:
|
|
20
|
+
session_id: str
|
|
21
|
+
actor_id: str
|
|
22
|
+
model: str
|
|
23
|
+
prompt_tokens: int
|
|
24
|
+
completion_tokens: int
|
|
25
|
+
tool_calls: int
|
|
26
|
+
task_id: Optional[str] = None
|
|
27
|
+
team: Optional[str] = None
|
|
28
|
+
timestamp: str = field(
|
|
29
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def total_tokens(self) -> int:
|
|
34
|
+
return self.prompt_tokens + self.completion_tokens
|
|
35
|
+
|
|
36
|
+
def estimated_cost_usd(
|
|
37
|
+
self,
|
|
38
|
+
price_per_1k_input: float = 0.003,
|
|
39
|
+
price_per_1k_output: float = 0.015,
|
|
40
|
+
) -> float:
|
|
41
|
+
return (
|
|
42
|
+
self.prompt_tokens / 1000 * price_per_1k_input
|
|
43
|
+
+ self.completion_tokens / 1000 * price_per_1k_output
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class SessionSummary:
|
|
49
|
+
session_id: str
|
|
50
|
+
actor_id: str
|
|
51
|
+
total_prompt_tokens: int
|
|
52
|
+
total_completion_tokens: int
|
|
53
|
+
total_tool_calls: int
|
|
54
|
+
total_cost_usd: float
|
|
55
|
+
model: str
|
|
56
|
+
started_at: str
|
|
57
|
+
last_event_at: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class TeamRollup:
|
|
62
|
+
team: str
|
|
63
|
+
period: str # "daily" | "weekly" | "monthly"
|
|
64
|
+
total_cost_usd: float
|
|
65
|
+
total_tokens: int
|
|
66
|
+
total_sessions: int
|
|
67
|
+
top_users: list[dict] # [{actor_id, cost_usd, sessions}]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class UsageAnalytics:
|
|
71
|
+
def __init__(self, db_path: Path = None):
|
|
72
|
+
self._path = db_path or DB_PATH
|
|
73
|
+
self._lock = threading.Lock()
|
|
74
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
75
|
+
self._init_db()
|
|
76
|
+
|
|
77
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
78
|
+
if self._conn is None:
|
|
79
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
self._conn = sqlite3.connect(str(self._path), check_same_thread=False)
|
|
81
|
+
self._conn.row_factory = sqlite3.Row
|
|
82
|
+
return self._conn
|
|
83
|
+
|
|
84
|
+
def _init_db(self) -> None:
|
|
85
|
+
with self._lock:
|
|
86
|
+
conn = self._get_conn()
|
|
87
|
+
conn.executescript("""
|
|
88
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
89
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
90
|
+
session_id TEXT NOT NULL,
|
|
91
|
+
actor_id TEXT NOT NULL,
|
|
92
|
+
model TEXT NOT NULL,
|
|
93
|
+
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
94
|
+
completion_tokens INTEGER NOT NULL DEFAULT 0,
|
|
95
|
+
tool_calls INTEGER NOT NULL DEFAULT 0,
|
|
96
|
+
task_id TEXT,
|
|
97
|
+
team TEXT,
|
|
98
|
+
timestamp TEXT NOT NULL,
|
|
99
|
+
cost_usd REAL NOT NULL DEFAULT 0.0
|
|
100
|
+
);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_usage_session ON usage_events(session_id);
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_usage_actor ON usage_events(actor_id);
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_usage_team ON usage_events(team);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage_events(timestamp);
|
|
105
|
+
CREATE TABLE IF NOT EXISTS cost_budgets (
|
|
106
|
+
team TEXT PRIMARY KEY,
|
|
107
|
+
monthly_limit_usd REAL NOT NULL,
|
|
108
|
+
alert_threshold REAL NOT NULL DEFAULT 0.8
|
|
109
|
+
);
|
|
110
|
+
""")
|
|
111
|
+
conn.commit()
|
|
112
|
+
|
|
113
|
+
def record(self, event: "UsageEvent") -> None:
|
|
114
|
+
cost = event.estimated_cost_usd()
|
|
115
|
+
with self._lock:
|
|
116
|
+
self._get_conn().execute(
|
|
117
|
+
"""INSERT INTO usage_events
|
|
118
|
+
(session_id, actor_id, model, prompt_tokens, completion_tokens,
|
|
119
|
+
tool_calls, task_id, team, timestamp, cost_usd)
|
|
120
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)""",
|
|
121
|
+
(
|
|
122
|
+
event.session_id, event.actor_id, event.model,
|
|
123
|
+
event.prompt_tokens, event.completion_tokens, event.tool_calls,
|
|
124
|
+
event.task_id, event.team, event.timestamp, cost,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
self._get_conn().commit()
|
|
128
|
+
|
|
129
|
+
def get_session_summary(self, session_id: str) -> "Optional[SessionSummary]":
|
|
130
|
+
with self._lock:
|
|
131
|
+
row = self._get_conn().execute(
|
|
132
|
+
"""SELECT session_id, actor_id, model,
|
|
133
|
+
SUM(prompt_tokens) as total_prompt,
|
|
134
|
+
SUM(completion_tokens) as total_completion,
|
|
135
|
+
SUM(tool_calls) as total_tools,
|
|
136
|
+
SUM(cost_usd) as total_cost,
|
|
137
|
+
MIN(timestamp) as started_at,
|
|
138
|
+
MAX(timestamp) as last_event_at
|
|
139
|
+
FROM usage_events WHERE session_id=? GROUP BY session_id""",
|
|
140
|
+
(session_id,),
|
|
141
|
+
).fetchone()
|
|
142
|
+
if row is None:
|
|
143
|
+
return None
|
|
144
|
+
return SessionSummary(
|
|
145
|
+
session_id=row["session_id"],
|
|
146
|
+
actor_id=row["actor_id"],
|
|
147
|
+
total_prompt_tokens=row["total_prompt"],
|
|
148
|
+
total_completion_tokens=row["total_completion"],
|
|
149
|
+
total_tool_calls=row["total_tools"],
|
|
150
|
+
total_cost_usd=row["total_cost"],
|
|
151
|
+
model=row["model"],
|
|
152
|
+
started_at=row["started_at"],
|
|
153
|
+
last_event_at=row["last_event_at"],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def get_team_rollup(self, team: str, period: str = "monthly") -> "TeamRollup":
|
|
157
|
+
since = _period_start(period)
|
|
158
|
+
with self._lock:
|
|
159
|
+
conn = self._get_conn()
|
|
160
|
+
agg = conn.execute(
|
|
161
|
+
"""SELECT SUM(cost_usd) as total_cost,
|
|
162
|
+
SUM(prompt_tokens + completion_tokens) as total_tokens,
|
|
163
|
+
COUNT(DISTINCT session_id) as total_sessions
|
|
164
|
+
FROM usage_events WHERE team=? AND timestamp>=?""",
|
|
165
|
+
(team, since),
|
|
166
|
+
).fetchone()
|
|
167
|
+
top = conn.execute(
|
|
168
|
+
"""SELECT actor_id, SUM(cost_usd) as cost_usd,
|
|
169
|
+
COUNT(DISTINCT session_id) as sessions
|
|
170
|
+
FROM usage_events WHERE team=? AND timestamp>=?
|
|
171
|
+
GROUP BY actor_id ORDER BY cost_usd DESC LIMIT 10""",
|
|
172
|
+
(team, since),
|
|
173
|
+
).fetchall()
|
|
174
|
+
return TeamRollup(
|
|
175
|
+
team=team,
|
|
176
|
+
period=period,
|
|
177
|
+
total_cost_usd=agg["total_cost"] or 0.0,
|
|
178
|
+
total_tokens=agg["total_tokens"] or 0,
|
|
179
|
+
total_sessions=agg["total_sessions"] or 0,
|
|
180
|
+
top_users=[
|
|
181
|
+
{"actor_id": r["actor_id"], "cost_usd": r["cost_usd"], "sessions": r["sessions"]}
|
|
182
|
+
for r in top
|
|
183
|
+
],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def check_budget(self, team: str) -> dict:
|
|
187
|
+
"""Returns {over_budget, alert, spent_usd, limit_usd}."""
|
|
188
|
+
since = _period_start("monthly")
|
|
189
|
+
with self._lock:
|
|
190
|
+
budget_row = self._get_conn().execute(
|
|
191
|
+
"SELECT monthly_limit_usd, alert_threshold FROM cost_budgets WHERE team=?",
|
|
192
|
+
(team,),
|
|
193
|
+
).fetchone()
|
|
194
|
+
if budget_row is None:
|
|
195
|
+
return {"over_budget": False, "alert": False, "spent_usd": 0.0, "limit_usd": None}
|
|
196
|
+
spent = (
|
|
197
|
+
self._get_conn().execute(
|
|
198
|
+
"SELECT SUM(cost_usd) as total FROM usage_events WHERE team=? AND timestamp>=?",
|
|
199
|
+
(team, since),
|
|
200
|
+
).fetchone()["total"]
|
|
201
|
+
or 0.0
|
|
202
|
+
)
|
|
203
|
+
limit = budget_row["monthly_limit_usd"]
|
|
204
|
+
threshold = budget_row["alert_threshold"]
|
|
205
|
+
return {
|
|
206
|
+
"over_budget": spent >= limit,
|
|
207
|
+
"alert": spent >= limit * threshold,
|
|
208
|
+
"spent_usd": spent,
|
|
209
|
+
"limit_usd": limit,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
def set_budget(
|
|
213
|
+
self, team: str, monthly_limit_usd: float, alert_threshold: float = 0.8
|
|
214
|
+
) -> None:
|
|
215
|
+
with self._lock:
|
|
216
|
+
self._get_conn().execute(
|
|
217
|
+
"""INSERT OR REPLACE INTO cost_budgets (team, monthly_limit_usd, alert_threshold)
|
|
218
|
+
VALUES (?,?,?)""",
|
|
219
|
+
(team, monthly_limit_usd, alert_threshold),
|
|
220
|
+
)
|
|
221
|
+
self._get_conn().commit()
|
|
222
|
+
|
|
223
|
+
def export_csv(
|
|
224
|
+
self, output_path: "Path", team: str = None, since: str = None
|
|
225
|
+
) -> int:
|
|
226
|
+
clauses, params = [], []
|
|
227
|
+
if team:
|
|
228
|
+
clauses.append("team=?")
|
|
229
|
+
params.append(team)
|
|
230
|
+
if since:
|
|
231
|
+
clauses.append("timestamp>=?")
|
|
232
|
+
params.append(since)
|
|
233
|
+
where = ("WHERE " + " AND ".join(clauses)) if clauses else ""
|
|
234
|
+
with self._lock:
|
|
235
|
+
rows = self._get_conn().execute(
|
|
236
|
+
f"SELECT * FROM usage_events {where} ORDER BY timestamp", params
|
|
237
|
+
).fetchall()
|
|
238
|
+
if not rows:
|
|
239
|
+
return 0
|
|
240
|
+
with open(output_path, "w", newline="") as f:
|
|
241
|
+
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
|
|
242
|
+
writer.writeheader()
|
|
243
|
+
writer.writerows([dict(r) for r in rows])
|
|
244
|
+
return len(rows)
|
|
245
|
+
|
|
246
|
+
def close(self) -> None:
|
|
247
|
+
if self._conn:
|
|
248
|
+
self._conn.close()
|
|
249
|
+
self._conn = None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _period_start(period: str) -> str:
|
|
253
|
+
import datetime as _dt
|
|
254
|
+
now = datetime.now(timezone.utc)
|
|
255
|
+
if period == "daily":
|
|
256
|
+
return now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
257
|
+
if period == "weekly":
|
|
258
|
+
monday = now - _dt.timedelta(days=now.weekday())
|
|
259
|
+
return monday.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
|
|
260
|
+
# monthly
|
|
261
|
+
return now.replace(day=1, hour=0, minute=0, second=0, microsecond=0).isoformat()
|
src/exceptions.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Custom exception hierarchy for gdm code.
|
|
2
|
+
|
|
3
|
+
All gdm exceptions inherit from GdmError so callers can catch the entire
|
|
4
|
+
family with a single `except GdmError` when needed.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"GdmError",
|
|
10
|
+
"ConfigError",
|
|
11
|
+
"ApiError",
|
|
12
|
+
"ApiRateLimitError",
|
|
13
|
+
"ApiBudgetError",
|
|
14
|
+
"FatalApiError",
|
|
15
|
+
"ToolError",
|
|
16
|
+
"ToolPermissionError",
|
|
17
|
+
"SecurityError",
|
|
18
|
+
"InjectionDetectedError",
|
|
19
|
+
"GitError",
|
|
20
|
+
"BudgetError",
|
|
21
|
+
"DatabaseError",
|
|
22
|
+
"SchemaError",
|
|
23
|
+
"IndexError",
|
|
24
|
+
"AgentLoopError",
|
|
25
|
+
"PermissionDeniedError",
|
|
26
|
+
"BudgetExceededError",
|
|
27
|
+
"DirtyWorkingTreeError",
|
|
28
|
+
"MergeConflictError",
|
|
29
|
+
"DaemonUnavailableError",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GdmError(Exception):
|
|
34
|
+
"""Base class for all gdm errors."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Configuration
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
class ConfigError(GdmError):
|
|
42
|
+
"""Invalid or missing configuration (e.g. XAI_API_KEY not set)."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# API / model
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
class ApiError(GdmError):
|
|
50
|
+
"""xAI API call failed."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.status_code = status_code
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FatalApiError(ApiError):
|
|
58
|
+
"""Both primary and fallback providers failed — no recovery possible."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ApiRateLimitError(ApiError):
|
|
62
|
+
"""429 rate limit hit — caller should back off and retry."""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ApiBudgetError(ApiError):
|
|
66
|
+
"""Session cost cap reached — stop calling the API."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Tools
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
class ToolError(GdmError):
|
|
74
|
+
"""A tool execution failed."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, tool_name: str, message: str) -> None:
|
|
77
|
+
super().__init__(f"[{tool_name}] {message}")
|
|
78
|
+
self.tool_name = tool_name
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ToolPermissionError(ToolError):
|
|
82
|
+
"""Tool was denied by ToolPermissionContext."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, tool: str, reason: str) -> None:
|
|
85
|
+
super().__init__(tool_name=tool, message=reason)
|
|
86
|
+
self.tool = tool
|
|
87
|
+
self.reason = reason
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Security
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
class SecurityError(GdmError):
|
|
95
|
+
"""A security invariant was violated."""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class InjectionDetectedError(SecurityError):
|
|
99
|
+
"""Prompt injection pattern found in file content."""
|
|
100
|
+
|
|
101
|
+
def __init__(self, filename: str, pattern: str) -> None:
|
|
102
|
+
super().__init__(
|
|
103
|
+
f"Injection pattern '{pattern}' detected in '{filename}'. "
|
|
104
|
+
"Content blocked from prompt injection."
|
|
105
|
+
)
|
|
106
|
+
self.filename = filename
|
|
107
|
+
self.pattern = pattern
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
# Git
|
|
112
|
+
# ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
class GitError(GdmError):
|
|
115
|
+
"""Git operation failed."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, command: str, message: str) -> None:
|
|
118
|
+
super().__init__(f"git {command}: {message}")
|
|
119
|
+
self.command = command
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
# Cost / budget
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
class BudgetError(GdmError):
|
|
127
|
+
"""Session cost limit exceeded."""
|
|
128
|
+
|
|
129
|
+
def __init__(self, limit_usd: float, current_usd: float) -> None:
|
|
130
|
+
super().__init__(
|
|
131
|
+
f"Cost limit ${limit_usd:.2f} exceeded (current: ${current_usd:.2f})"
|
|
132
|
+
)
|
|
133
|
+
self.limit_usd = limit_usd
|
|
134
|
+
self.current_usd = current_usd
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Storage
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
class DatabaseError(GdmError):
|
|
142
|
+
"""SQLite operation failed."""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class SchemaError(GdmError):
|
|
146
|
+
"""Pydantic model parse/validation failed."""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class IndexError(GdmError):
|
|
150
|
+
"""Code index (Tree-sitter/FAISS) operation failed."""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# Agent loop
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
class AgentLoopError(GdmError):
|
|
158
|
+
"""Agent loop hit an unrecoverable state (max turns, bad API response, etc.)."""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Aliases (backwards-compatible names)
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
class PermissionDeniedError(ToolPermissionError):
|
|
166
|
+
"""Alias for ToolPermissionError — backwards-compatible name."""
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class BudgetExceededError(ApiBudgetError):
|
|
170
|
+
"""Alias for ApiBudgetError — raised by cost_tracker when limit exceeded."""
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Branch Farm
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
class DirtyWorkingTreeError(GitError):
|
|
178
|
+
"""Branch farm requires a clean working tree but uncommitted changes exist."""
|
|
179
|
+
|
|
180
|
+
def __init__(self) -> None:
|
|
181
|
+
super().__init__(
|
|
182
|
+
command="status --porcelain",
|
|
183
|
+
message=(
|
|
184
|
+
"Branch farm requires a clean working tree. "
|
|
185
|
+
"Commit or stash your changes before running a branch farm."
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class MergeConflictError(GitError):
|
|
191
|
+
"""Dry-run merge detected conflicts; cannot auto-merge the farm branch."""
|
|
192
|
+
|
|
193
|
+
def __init__(self, branch: str, details: str = "") -> None:
|
|
194
|
+
super().__init__(
|
|
195
|
+
command=f"merge-tree HEAD {branch}",
|
|
196
|
+
message=f"Conflicts detected when merging {branch!r}.{' ' + details if details else ''}",
|
|
197
|
+
)
|
|
198
|
+
self.branch = branch
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Daemon
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
class DaemonUnavailableError(GdmError):
|
|
206
|
+
"""Daemon crashed too frequently or is otherwise unavailable."""
|
|
207
|
+
|