delimit-cli 3.14.28 → 3.14.29
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.
- package/gateway/ai/backends/deploy_bridge.py +56 -2
- package/gateway/ai/backends/gateway_core.py +212 -1
- package/gateway/ai/backends/generate_bridge.py +84 -13
- package/gateway/ai/backends/governance_bridge.py +63 -16
- package/gateway/ai/backends/memory_bridge.py +77 -76
- package/gateway/ai/backends/ops_bridge.py +76 -6
- package/gateway/ai/backends/os_bridge.py +23 -3
- package/gateway/ai/backends/repo_bridge.py +156 -17
- package/gateway/ai/backends/tools_design.py +116 -9
- package/gateway/ai/backends/tools_infra.py +200 -72
- package/gateway/ai/backends/tools_real.py +8 -0
- package/gateway/ai/backends/ui_bridge.py +115 -5
- package/gateway/ai/backends/vault_bridge.py +69 -114
- package/gateway/ai/content_engine.py +1276 -0
- package/gateway/ai/context_fs.py +193 -0
- package/gateway/ai/daemon.py +500 -0
- package/gateway/ai/data_plane.py +291 -0
- package/gateway/ai/deliberation.py +1033 -6
- package/gateway/ai/events.py +39 -0
- package/gateway/ai/founding_users.py +162 -0
- package/gateway/ai/governance.py +698 -4
- package/gateway/ai/inbox_daemon.py +78 -17
- package/gateway/ai/integrations/__init__.py +1 -0
- package/gateway/ai/integrations/opensage_wrapper.py +288 -0
- package/gateway/ai/key_resolver.py +95 -0
- package/gateway/ai/ledger_manager.py +289 -1
- package/gateway/ai/license.py +62 -4
- package/gateway/ai/license_core.py +208 -7
- package/gateway/ai/local_server.py +215 -0
- package/gateway/ai/loop_engine.py +408 -0
- package/gateway/ai/mcp_bridge.py +178 -0
- package/gateway/ai/release_sync.py +2 -2
- package/gateway/ai/screen_record.py +374 -0
- package/gateway/ai/secrets_broker.py +235 -0
- package/gateway/ai/social.py +189 -27
- package/gateway/ai/social_target.py +1635 -0
- package/gateway/ai/supabase_sync.py +190 -0
- package/gateway/ai/tracing.py +195 -0
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/diff_engine_v2.py +272 -78
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/gateway/core/policy_engine.py +4 -0
- package/package.json +1 -1
|
@@ -1,94 +1,95 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
Memory bridge — file-based semantic memory store.
|
|
3
|
+
Stores memories as JSON files in ~/.delimit/memory/.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import os
|
|
7
|
-
import sys
|
|
8
6
|
import json
|
|
9
|
-
import
|
|
7
|
+
import hashlib
|
|
10
8
|
import logging
|
|
11
9
|
from pathlib import Path
|
|
12
|
-
from
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
13
12
|
|
|
14
13
|
logger = logging.getLogger("delimit.ai.memory_bridge")
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
_server = None
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _run_async(coro):
|
|
22
|
-
"""Run an async coroutine from sync code, handling nested event loops."""
|
|
23
|
-
try:
|
|
24
|
-
loop = asyncio.get_running_loop()
|
|
25
|
-
except RuntimeError:
|
|
26
|
-
loop = None
|
|
27
|
-
|
|
28
|
-
if loop and loop.is_running():
|
|
29
|
-
# We're inside an async context (e.g., FastMCP) — use a new thread
|
|
30
|
-
import concurrent.futures
|
|
31
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
32
|
-
return pool.submit(asyncio.run, coro).result(timeout=30)
|
|
33
|
-
else:
|
|
34
|
-
return asyncio.run(coro)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _get_server():
|
|
38
|
-
global _server
|
|
39
|
-
if _server is not None:
|
|
40
|
-
return _server
|
|
41
|
-
pkg_path = str(MEM_PACKAGE / "delimit_memory")
|
|
42
|
-
if pkg_path not in sys.path:
|
|
43
|
-
sys.path.insert(0, pkg_path)
|
|
44
|
-
if str(MEM_PACKAGE) not in sys.path:
|
|
45
|
-
sys.path.insert(0, str(MEM_PACKAGE))
|
|
46
|
-
try:
|
|
47
|
-
from delimit_memory.server import DelimitMemoryServer
|
|
48
|
-
_server = DelimitMemoryServer()
|
|
49
|
-
_run_async(_server._initialize_engine())
|
|
50
|
-
return _server
|
|
51
|
-
except Exception as e:
|
|
52
|
-
logger.warning(f"Failed to init memory server: {e}")
|
|
53
|
-
return None
|
|
15
|
+
MEMORY_DIR = Path.home() / ".delimit" / "memory"
|
|
54
16
|
|
|
55
17
|
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
srv = _get_server()
|
|
59
|
-
if srv is None:
|
|
60
|
-
return {"error": "Memory server unavailable", "results": []}
|
|
61
|
-
try:
|
|
62
|
-
result = _run_async(srv._handle_search({"query": query, "limit": limit}))
|
|
63
|
-
return json.loads(result) if isinstance(result, str) else result
|
|
64
|
-
except Exception as e:
|
|
65
|
-
return {"error": f"Memory search failed: {e}", "results": []}
|
|
18
|
+
def _ensure_dir():
|
|
19
|
+
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
|
|
66
20
|
|
|
67
21
|
|
|
68
22
|
def store(content: str, tags: Optional[list] = None, context: Optional[str] = None) -> Dict[str, Any]:
|
|
69
23
|
"""Store a memory entry."""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
24
|
+
_ensure_dir()
|
|
25
|
+
|
|
26
|
+
# Generate ID from content hash
|
|
27
|
+
mem_id = "mem-" + hashlib.sha256(content[:100].encode()).hexdigest()[:12]
|
|
28
|
+
ts = datetime.now(timezone.utc).isoformat()
|
|
29
|
+
|
|
30
|
+
entry = {
|
|
31
|
+
"id": mem_id,
|
|
32
|
+
"content": content,
|
|
33
|
+
"tags": tags or [],
|
|
34
|
+
"context": context or "",
|
|
35
|
+
"created_at": ts,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
path = MEMORY_DIR / f"{mem_id}.json"
|
|
39
|
+
path.write_text(json.dumps(entry, indent=2))
|
|
40
|
+
|
|
41
|
+
return {"stored": mem_id, "path": str(path), "created_at": ts}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def search(query: str, limit: int = 10) -> Dict[str, Any]:
|
|
45
|
+
"""Search memories by keyword matching."""
|
|
46
|
+
_ensure_dir()
|
|
47
|
+
query_lower = query.lower()
|
|
48
|
+
results = []
|
|
49
|
+
|
|
50
|
+
for f in sorted(MEMORY_DIR.glob("*.json"), reverse=True):
|
|
51
|
+
try:
|
|
52
|
+
entry = json.loads(f.read_text())
|
|
53
|
+
content = entry.get("content", "").lower()
|
|
54
|
+
tags = " ".join(entry.get("tags", [])).lower()
|
|
55
|
+
context = entry.get("context", "").lower()
|
|
56
|
+
|
|
57
|
+
# Simple keyword matching
|
|
58
|
+
if query_lower in content or query_lower in tags or query_lower in context:
|
|
59
|
+
results.append({
|
|
60
|
+
"id": entry.get("id", f.stem),
|
|
61
|
+
"content": entry.get("content", "")[:500],
|
|
62
|
+
"tags": entry.get("tags", []),
|
|
63
|
+
"created_at": entry.get("created_at", ""),
|
|
64
|
+
"relevance": content.count(query_lower),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if len(results) >= limit:
|
|
68
|
+
break
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
results.sort(key=lambda r: r.get("relevance", 0), reverse=True)
|
|
73
|
+
return {"query": query, "results": results, "count": len(results)}
|
|
83
74
|
|
|
84
75
|
|
|
85
76
|
def get_recent(limit: int = 5) -> Dict[str, Any]:
|
|
86
|
-
"""Get recent
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
77
|
+
"""Get recent memory entries."""
|
|
78
|
+
_ensure_dir()
|
|
79
|
+
entries = []
|
|
80
|
+
|
|
81
|
+
for f in sorted(MEMORY_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
|
|
82
|
+
if len(entries) >= limit:
|
|
83
|
+
break
|
|
84
|
+
try:
|
|
85
|
+
entry = json.loads(f.read_text())
|
|
86
|
+
entries.append({
|
|
87
|
+
"id": entry.get("id", f.stem),
|
|
88
|
+
"content": entry.get("content", "")[:500],
|
|
89
|
+
"tags": entry.get("tags", []),
|
|
90
|
+
"created_at": entry.get("created_at", ""),
|
|
91
|
+
})
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
return {"results": entries, "count": len(entries)}
|
|
@@ -48,33 +48,72 @@ def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str)
|
|
|
48
48
|
# ─── ReleasePilot (Governance Primitive) ────────────────────────────────
|
|
49
49
|
|
|
50
50
|
def release_plan(environment: str, version: str, repository: str, services: Optional[List[str]] = None) -> Dict[str, Any]:
|
|
51
|
+
"""Generate a release plan for the given environment and version."""
|
|
51
52
|
return _call("releasepilot", "create_releasepilot_server", "_tool_plan",
|
|
52
53
|
{"environment": environment, "version": version, "repository": repository, "services": services or []}, "release.plan")
|
|
53
54
|
|
|
54
55
|
|
|
55
56
|
def release_validate(environment: str, version: str) -> Dict[str, Any]:
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
"""Validate release readiness by checking git tags, CHANGELOG, and package.json."""
|
|
58
|
+
import subprocess
|
|
59
|
+
checks = []
|
|
60
|
+
try:
|
|
61
|
+
tags = subprocess.run(["git", "tag", "-l"], capture_output=True, text=True, timeout=10)
|
|
62
|
+
tag_list = tags.stdout.strip().splitlines() if tags.returncode == 0 else []
|
|
63
|
+
has_tag = any(version in t for t in tag_list)
|
|
64
|
+
checks.append({"check": "git_tag", "passed": has_tag, "detail": f"Tag for {version} {'found' if has_tag else 'not found'}"})
|
|
65
|
+
except Exception:
|
|
66
|
+
checks.append({"check": "git_tag", "passed": False, "detail": "git not available"})
|
|
67
|
+
cl = Path("CHANGELOG.md")
|
|
68
|
+
checks.append({"check": "changelog", "passed": cl.exists(), "detail": str(cl) if cl.exists() else "CHANGELOG.md not found"})
|
|
69
|
+
pkg = Path("package.json")
|
|
70
|
+
if pkg.exists():
|
|
71
|
+
try:
|
|
72
|
+
data = json.loads(pkg.read_text())
|
|
73
|
+
pkg_ver = data.get("version", "")
|
|
74
|
+
match = pkg_ver == version.lstrip("v")
|
|
75
|
+
checks.append({"check": "package_version", "passed": match, "detail": f"package.json version={pkg_ver}"})
|
|
76
|
+
except Exception:
|
|
77
|
+
checks.append({"check": "package_version", "passed": False, "detail": "Failed to read package.json"})
|
|
78
|
+
passed = all(c["passed"] for c in checks)
|
|
79
|
+
return {"tool": "release.validate", "status": "pass" if passed else "fail", "environment": environment, "version": version, "checks": checks}
|
|
58
80
|
|
|
59
81
|
|
|
60
82
|
def release_status(environment: str) -> Dict[str, Any]:
|
|
83
|
+
"""Get current release status for the environment."""
|
|
61
84
|
return _call("releasepilot", "create_releasepilot_server", "_tool_status",
|
|
62
85
|
{"environment": environment}, "release.status")
|
|
63
86
|
|
|
64
87
|
|
|
65
88
|
def release_rollback(environment: str, version: str, to_version: str) -> Dict[str, Any]:
|
|
89
|
+
"""Roll back to a previous version in the specified environment."""
|
|
66
90
|
return _call("releasepilot", "create_releasepilot_server", "_tool_rollback",
|
|
67
91
|
{"environment": environment, "version": version, "to_version": to_version}, "release.rollback")
|
|
68
92
|
|
|
69
93
|
|
|
70
94
|
def release_history(environment: str, limit: int = 10) -> Dict[str, Any]:
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
"""Show recent release history from git log."""
|
|
96
|
+
import subprocess
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["git", "log", "--oneline", "--decorate", f"-{limit}"],
|
|
100
|
+
capture_output=True, text=True, timeout=10,
|
|
101
|
+
)
|
|
102
|
+
if result.returncode != 0:
|
|
103
|
+
return {"tool": "release.history", "status": "error", "error": result.stderr.strip()}
|
|
104
|
+
commits = result.stdout.strip().splitlines()
|
|
105
|
+
tags = subprocess.run(["git", "tag", "-l", "--sort=-creatordate"], capture_output=True, text=True, timeout=10)
|
|
106
|
+
tag_list = tags.stdout.strip().splitlines()[:limit] if tags.returncode == 0 else []
|
|
107
|
+
return {"tool": "release.history", "status": "ok", "environment": environment,
|
|
108
|
+
"recent_commits": commits, "tags": tag_list, "total_commits": len(commits)}
|
|
109
|
+
except Exception as e:
|
|
110
|
+
return {"tool": "release.history", "status": "error", "error": str(e)}
|
|
73
111
|
|
|
74
112
|
|
|
75
113
|
# ─── CostGuard (Governance Primitive) ──────────────────────────────────
|
|
76
114
|
|
|
77
115
|
def cost_analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
116
|
+
"""Analyze cloud infrastructure costs for the target path."""
|
|
78
117
|
result = _call("costguard", "create_costguard_server", "_tool_analyze",
|
|
79
118
|
{"target": target, **(options or {})}, "cost.analyze")
|
|
80
119
|
# Guard against hardcoded fake AWS cost data from stub implementation
|
|
@@ -85,11 +124,13 @@ def cost_analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str,
|
|
|
85
124
|
|
|
86
125
|
|
|
87
126
|
def cost_optimize(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
127
|
+
"""Identify cost optimization opportunities for the target infrastructure."""
|
|
88
128
|
return _call("costguard", "create_costguard_server", "_tool_optimize",
|
|
89
129
|
{"target": target, **(options or {})}, "cost.optimize")
|
|
90
130
|
|
|
91
131
|
|
|
92
132
|
def cost_alert(action: str = "list", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
133
|
+
"""Manage cost alerts (list, create, delete)."""
|
|
93
134
|
return _call("costguard", "create_costguard_server", "_tool_alerts",
|
|
94
135
|
{"action": action, **(options or {})}, "cost.alert")
|
|
95
136
|
|
|
@@ -97,6 +138,7 @@ def cost_alert(action: str = "list", options: Optional[Dict] = None) -> Dict[str
|
|
|
97
138
|
# ─── DataSteward (Governance Primitive) ────────────────────────────────
|
|
98
139
|
|
|
99
140
|
def data_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
141
|
+
"""Run data integrity and validation checks on the target database."""
|
|
100
142
|
result = _call("datasteward", "create_datasteward_server", "_tool_integrity_check",
|
|
101
143
|
{"database_url": target, **(options or {})}, "data.validate")
|
|
102
144
|
# Guard against stub that returns "passed" with 0 tables checked
|
|
@@ -107,11 +149,13 @@ def data_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str
|
|
|
107
149
|
|
|
108
150
|
|
|
109
151
|
def data_migrate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
152
|
+
"""Check for pending database migrations and reporting status."""
|
|
110
153
|
return _call("datasteward", "create_datasteward_server", "_tool_migration_status",
|
|
111
154
|
{"database_url": target, **(options or {})}, "data.migrate")
|
|
112
155
|
|
|
113
156
|
|
|
114
157
|
def data_backup(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
158
|
+
"""Create a backup plan or execute backup for the target database."""
|
|
115
159
|
return _call("datasteward", "create_datasteward_server", "_tool_backup_plan",
|
|
116
160
|
{"database_url": target, **(options or {})}, "data.backup")
|
|
117
161
|
|
|
@@ -119,20 +163,46 @@ def data_backup(target: str = ".", options: Optional[Dict] = None) -> Dict[str,
|
|
|
119
163
|
# ─── ObservabilityOps (Internal OS) ────────────────────────────────────
|
|
120
164
|
|
|
121
165
|
def obs_metrics(query: str, time_range: str = "1h", source: Optional[str] = None) -> Dict[str, Any]:
|
|
166
|
+
"""Query live system metrics (CPU, memory, disk, network)."""
|
|
122
167
|
return _call("observabilityops", "create_observabilityops_server", "_tool_metrics",
|
|
123
168
|
{"query": query, "time_range": time_range, "source": source}, "obs.metrics")
|
|
124
169
|
|
|
125
170
|
|
|
126
171
|
def obs_logs(query: str, time_range: str = "1h", source: Optional[str] = None) -> Dict[str, Any]:
|
|
172
|
+
"""Search and retrieve system or application logs."""
|
|
127
173
|
return _call("observabilityops", "create_observabilityops_server", "_tool_logs",
|
|
128
174
|
{"query": query, "time_range": time_range, "source": source}, "obs.logs")
|
|
129
175
|
|
|
130
176
|
|
|
131
177
|
def obs_alerts(action: str, alert_rule: Optional[Dict] = None, rule_id: Optional[str] = None) -> Dict[str, Any]:
|
|
132
|
-
|
|
133
|
-
|
|
178
|
+
"""File-based alert management using ~/.delimit/alerts/."""
|
|
179
|
+
alerts_dir = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "alerts"
|
|
180
|
+
alerts_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
if action == "list":
|
|
182
|
+
alerts = []
|
|
183
|
+
for fp in sorted(alerts_dir.glob("*.json")):
|
|
184
|
+
try:
|
|
185
|
+
alerts.append(json.loads(fp.read_text()))
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
return {"tool": "obs.alerts", "status": "ok", "action": "list", "alerts": alerts, "total": len(alerts)}
|
|
189
|
+
elif action == "create" and alert_rule:
|
|
190
|
+
import time as _time
|
|
191
|
+
aid = f"alert-{int(_time.time())}"
|
|
192
|
+
alert_rule["id"] = aid
|
|
193
|
+
alert_rule["created_at"] = _time.time()
|
|
194
|
+
(alerts_dir / f"{aid}.json").write_text(json.dumps(alert_rule, indent=2))
|
|
195
|
+
return {"tool": "obs.alerts", "status": "created", "alert": alert_rule}
|
|
196
|
+
elif action == "delete" and rule_id:
|
|
197
|
+
fp = alerts_dir / f"{rule_id}.json"
|
|
198
|
+
if fp.exists():
|
|
199
|
+
fp.unlink()
|
|
200
|
+
return {"tool": "obs.alerts", "status": "deleted", "rule_id": rule_id}
|
|
201
|
+
return {"tool": "obs.alerts", "status": "not_found", "rule_id": rule_id}
|
|
202
|
+
return {"tool": "obs.alerts", "status": "unknown_action", "action": action}
|
|
134
203
|
|
|
135
204
|
|
|
136
205
|
def obs_status() -> Dict[str, Any]:
|
|
206
|
+
"""Get overall system health and observability status."""
|
|
137
207
|
return _call("observabilityops", "create_observabilityops_server", "_tool_status",
|
|
138
208
|
{}, "obs.status")
|
|
@@ -22,14 +22,30 @@ _NOT_INIT_MSG = (
|
|
|
22
22
|
"or run the delimit_init tool with your project path."
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
+
_DEPENDENCY_MSG = "delimit-os backend is not installed or not available in this environment."
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_initialized(path: str = ".") -> bool:
|
|
29
|
+
"""A project is initialized if .delimit/policies.yml exists."""
|
|
30
|
+
return (Path(path).resolve() / ".delimit" / "policies.yml").is_file()
|
|
31
|
+
|
|
25
32
|
|
|
26
33
|
def _ensure_os_path():
|
|
27
34
|
if str(OS_PACKAGE) not in sys.path:
|
|
28
35
|
sys.path.insert(0, str(OS_PACKAGE))
|
|
29
36
|
|
|
30
37
|
|
|
38
|
+
def _backend_unavailable(path: Optional[str] = None) -> Dict[str, Any]:
|
|
39
|
+
"""Return a truthful error for missing OS backend support."""
|
|
40
|
+
if path and not _is_initialized(path):
|
|
41
|
+
return {"error": _NOT_INIT_MSG, "fallback": True}
|
|
42
|
+
return {"error": _DEPENDENCY_MSG, "fallback": True}
|
|
43
|
+
|
|
44
|
+
|
|
31
45
|
def create_plan(operation: str, target: str, parameters: Optional[Dict] = None, require_approval: bool = True) -> Dict[str, Any]:
|
|
32
46
|
"""Create an execution plan via delimit-os."""
|
|
47
|
+
if not OS_PACKAGE.exists():
|
|
48
|
+
return _backend_unavailable(target)
|
|
33
49
|
_ensure_os_path()
|
|
34
50
|
try:
|
|
35
51
|
from server import PLANS
|
|
@@ -54,11 +70,13 @@ def create_plan(operation: str, target: str, parameters: Optional[Dict] = None,
|
|
|
54
70
|
PLANS[plan_id] = plan
|
|
55
71
|
return plan
|
|
56
72
|
except ImportError:
|
|
57
|
-
return
|
|
73
|
+
return _backend_unavailable(target)
|
|
58
74
|
|
|
59
75
|
|
|
60
76
|
def get_status() -> Dict[str, Any]:
|
|
61
77
|
"""Get current OS status."""
|
|
78
|
+
if not OS_PACKAGE.exists():
|
|
79
|
+
return {"status": "unavailable", "error": _DEPENDENCY_MSG}
|
|
62
80
|
_ensure_os_path()
|
|
63
81
|
try:
|
|
64
82
|
from server import PLANS, TASKS, TOKENS
|
|
@@ -69,11 +87,13 @@ def get_status() -> Dict[str, Any]:
|
|
|
69
87
|
"tokens": len(TOKENS),
|
|
70
88
|
}
|
|
71
89
|
except ImportError:
|
|
72
|
-
return {"status": "unavailable", "error":
|
|
90
|
+
return {"status": "unavailable", "error": _DEPENDENCY_MSG}
|
|
73
91
|
|
|
74
92
|
|
|
75
93
|
def check_gates(plan_id: str) -> Dict[str, Any]:
|
|
76
94
|
"""Check governance gates for a plan."""
|
|
95
|
+
if not OS_PACKAGE.exists():
|
|
96
|
+
return {"error": _DEPENDENCY_MSG}
|
|
77
97
|
_ensure_os_path()
|
|
78
98
|
try:
|
|
79
99
|
from server import PLANS
|
|
@@ -86,4 +106,4 @@ def check_gates(plan_id: str) -> Dict[str, Any]:
|
|
|
86
106
|
"status": plan.get("status"),
|
|
87
107
|
}
|
|
88
108
|
except ImportError:
|
|
89
|
-
return {"error":
|
|
109
|
+
return {"error": _DEPENDENCY_MSG}
|
|
@@ -49,42 +49,143 @@ def _call(pkg: str, factory_name: str, method: str, args: Dict, tool_label: str)
|
|
|
49
49
|
# ─── RepoDoctor ────────────────────────────────────────────────────────
|
|
50
50
|
|
|
51
51
|
def diagnose(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
"""Check for common repository issues."""
|
|
53
|
+
import subprocess
|
|
54
|
+
root = Path(target).resolve()
|
|
55
|
+
issues = []
|
|
56
|
+
if not (root / ".gitignore").exists():
|
|
57
|
+
issues.append({"severity": "warning", "issue": "No .gitignore file found"})
|
|
58
|
+
if not any((root / d).exists() for d in ["tests", "test", "__tests__", "spec"]):
|
|
59
|
+
issues.append({"severity": "warning", "issue": "No test directory found"})
|
|
60
|
+
if not any((root / f).exists() for f in [".github/workflows", ".gitlab-ci.yml", "Jenkinsfile", ".circleci"]):
|
|
61
|
+
issues.append({"severity": "info", "issue": "No CI configuration detected"})
|
|
62
|
+
# Check for large files
|
|
63
|
+
try:
|
|
64
|
+
result = subprocess.run(["git", "-C", str(root), "ls-files"], capture_output=True, text=True, timeout=10)
|
|
65
|
+
if result.returncode == 0:
|
|
66
|
+
for f in result.stdout.strip().splitlines():
|
|
67
|
+
fp = root / f
|
|
68
|
+
if fp.exists() and fp.stat().st_size > 5_000_000:
|
|
69
|
+
issues.append({"severity": "warning", "issue": f"Large file ({fp.stat().st_size // 1_000_000}MB): {f}"})
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
status = "healthy" if not issues else ("warning" if all(i["severity"] != "error" for i in issues) else "unhealthy")
|
|
73
|
+
return {"tool": "repo.diagnose", "status": status, "target": str(root), "issues": issues, "total_issues": len(issues)}
|
|
54
74
|
|
|
55
75
|
|
|
56
76
|
def analyze(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
"""Analyze repository structure: file counts by type, configs, health."""
|
|
78
|
+
root = Path(target).resolve()
|
|
79
|
+
skip = {"node_modules", "dist", ".next", ".git", "__pycache__", "build", ".cache", "venv", ".venv"}
|
|
80
|
+
ext_counts: Dict[str, int] = {}
|
|
81
|
+
total = 0
|
|
82
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
83
|
+
dirnames[:] = [d for d in dirnames if d not in skip]
|
|
84
|
+
for f in filenames:
|
|
85
|
+
ext = Path(f).suffix or "(no ext)"
|
|
86
|
+
ext_counts[ext] = ext_counts.get(ext, 0) + 1
|
|
87
|
+
total += 1
|
|
88
|
+
top = sorted(ext_counts.items(), key=lambda x: -x[1])[:15]
|
|
89
|
+
configs = {c: (root / c).exists() for c in [
|
|
90
|
+
".gitignore", "package.json", "pyproject.toml", "Makefile", "Dockerfile",
|
|
91
|
+
"tsconfig.json", ".eslintrc.json", "jest.config.js", "pytest.ini", "setup.py",
|
|
92
|
+
]}
|
|
93
|
+
has_tests = any((root / d).exists() for d in ["tests", "test", "__tests__", "spec"])
|
|
94
|
+
has_ci = any((root / f).exists() for f in [".github/workflows", ".gitlab-ci.yml", "Jenkinsfile"])
|
|
95
|
+
return {"tool": "repo.analyze", "status": "ok", "target": str(root), "total_files": total,
|
|
96
|
+
"file_types": dict(top), "configs_found": {k: v for k, v in configs.items() if v},
|
|
97
|
+
"has_tests": has_tests, "has_ci": has_ci}
|
|
59
98
|
|
|
60
99
|
|
|
61
100
|
# ─── ConfigSentry ───────────────────────────────────────────────────────
|
|
62
101
|
|
|
63
102
|
def config_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
"""Validate JSON/YAML config files parse correctly."""
|
|
104
|
+
root = Path(target).resolve()
|
|
105
|
+
results = []
|
|
106
|
+
for ext, loader in [(".json", "json"), (".yaml", "yaml"), (".yml", "yaml")]:
|
|
107
|
+
for fp in root.glob(f"*{ext}"):
|
|
108
|
+
if fp.name.startswith(".") and ext == ".json" and "lock" in fp.name:
|
|
109
|
+
continue
|
|
110
|
+
try:
|
|
111
|
+
text = fp.read_text()
|
|
112
|
+
if loader == "json":
|
|
113
|
+
json.loads(text)
|
|
114
|
+
else:
|
|
115
|
+
try:
|
|
116
|
+
import yaml as _yaml
|
|
117
|
+
_yaml.safe_load(text)
|
|
118
|
+
except ImportError:
|
|
119
|
+
pass # skip YAML validation if pyyaml not installed
|
|
120
|
+
results.append({"file": fp.name, "valid": True})
|
|
121
|
+
except Exception as e:
|
|
122
|
+
results.append({"file": fp.name, "valid": False, "error": str(e)[:200]})
|
|
123
|
+
valid = sum(1 for r in results if r["valid"])
|
|
124
|
+
invalid = sum(1 for r in results if not r["valid"])
|
|
125
|
+
return {"tool": "config.validate", "status": "ok" if invalid == 0 else "issues_found",
|
|
126
|
+
"target": str(root), "files_checked": len(results), "valid": valid, "invalid": invalid, "details": results}
|
|
66
127
|
|
|
67
128
|
|
|
68
129
|
def config_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
69
|
-
|
|
70
|
-
|
|
130
|
+
"""Audit config files for security issues and staleness."""
|
|
131
|
+
root = Path(target).resolve()
|
|
132
|
+
findings = []
|
|
133
|
+
# Check for secrets in config files
|
|
134
|
+
secret_patterns = ["password", "secret", "api_key", "apikey", "token", "private_key"]
|
|
135
|
+
for ext in [".json", ".yaml", ".yml", ".env", ".toml", ".ini", ".cfg"]:
|
|
136
|
+
for fp in root.glob(f"*{ext}"):
|
|
137
|
+
try:
|
|
138
|
+
text = fp.read_text().lower()
|
|
139
|
+
for pat in secret_patterns:
|
|
140
|
+
if pat in text and fp.name != ".gitignore":
|
|
141
|
+
findings.append({"file": fp.name, "severity": "warning",
|
|
142
|
+
"issue": f"Possible secret pattern '{pat}' found"})
|
|
143
|
+
break
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
# Check .env not in .gitignore
|
|
147
|
+
gitignore = root / ".gitignore"
|
|
148
|
+
if (root / ".env").exists() and gitignore.exists():
|
|
149
|
+
gi_text = gitignore.read_text()
|
|
150
|
+
if ".env" not in gi_text:
|
|
151
|
+
findings.append({"file": ".env", "severity": "error", "issue": ".env exists but not in .gitignore"})
|
|
152
|
+
elif (root / ".env").exists() and not gitignore.exists():
|
|
153
|
+
findings.append({"file": ".env", "severity": "error", "issue": ".env exists with no .gitignore"})
|
|
154
|
+
return {"tool": "config.audit", "status": "ok" if not findings else "issues_found",
|
|
155
|
+
"target": str(root), "findings": findings, "total_findings": len(findings)}
|
|
71
156
|
|
|
72
157
|
|
|
73
158
|
# ─── EvidencePack ───────────────────────────────────────────────────────
|
|
74
159
|
|
|
75
160
|
def evidence_collect(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
|
|
161
|
+
"""Collect project evidence: git log, test files, configs, governance data."""
|
|
162
|
+
import subprocess, time as _time
|
|
163
|
+
root = Path(target).resolve()
|
|
164
|
+
evidence: Dict[str, Any] = {"collected_at": _time.time(), "target": str(root)}
|
|
165
|
+
# Git log
|
|
166
|
+
try:
|
|
167
|
+
r = subprocess.run(["git", "-C", str(root), "log", "--oneline", "-10"], capture_output=True, text=True, timeout=10)
|
|
168
|
+
evidence["git_log"] = r.stdout.strip().splitlines() if r.returncode == 0 else []
|
|
169
|
+
except Exception:
|
|
170
|
+
evidence["git_log"] = []
|
|
171
|
+
# Test files
|
|
172
|
+
test_dirs = [d for d in ["tests", "test", "__tests__", "spec"] if (root / d).exists()]
|
|
173
|
+
evidence["test_directories"] = test_dirs
|
|
174
|
+
# Configs
|
|
175
|
+
evidence["configs"] = [f.name for f in root.iterdir() if f.is_file() and (f.suffix in [".json", ".yaml", ".yml", ".toml"] or f.name.startswith("."))]
|
|
176
|
+
# Save bundle
|
|
177
|
+
ev_dir = Path(os.environ.get("DELIMIT_HOME", str(Path.home() / ".delimit"))) / "evidence"
|
|
178
|
+
ev_dir.mkdir(parents=True, exist_ok=True)
|
|
179
|
+
bundle_id = f"ev-{int(_time.time())}"
|
|
180
|
+
bundle_path = ev_dir / f"{bundle_id}.json"
|
|
181
|
+
evidence["bundle_id"] = bundle_id
|
|
182
|
+
bundle_path.write_text(json.dumps(evidence, indent=2))
|
|
183
|
+
return {"tool": "evidence.collect", "status": "ok", "bundle_id": bundle_id,
|
|
184
|
+
"bundle_path": str(bundle_path), "summary": {k: len(v) if isinstance(v, list) else v for k, v in evidence.items()}}
|
|
85
185
|
|
|
86
186
|
|
|
87
187
|
def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str] = None, options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
188
|
+
"""Verify the integrity and authenticity of a collected evidence bundle."""
|
|
88
189
|
args = {**(options or {})}
|
|
89
190
|
if bundle_id:
|
|
90
191
|
args["bundle_id"] = bundle_id
|
|
@@ -92,6 +193,16 @@ def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str]
|
|
|
92
193
|
args["bundle_path"] = bundle_path
|
|
93
194
|
if not bundle_id and not bundle_path:
|
|
94
195
|
return {"tool": "evidence.verify", "status": "no_input", "message": "Provide bundle_id or bundle_path to verify"}
|
|
196
|
+
try:
|
|
197
|
+
importlib.import_module("evidencepack.server")
|
|
198
|
+
except (ImportError, ModuleNotFoundError):
|
|
199
|
+
return {
|
|
200
|
+
"tool": "evidence.verify",
|
|
201
|
+
"status": "not_available",
|
|
202
|
+
"error": "evidencepack backend is not installed or not available in this environment.",
|
|
203
|
+
"hint": "Install evidencepack to enable evidence bundle verification.",
|
|
204
|
+
**args,
|
|
205
|
+
}
|
|
95
206
|
return _call("evidencepack", "create_evidencepack_server", "_tool_validate",
|
|
96
207
|
args, "evidence.verify")
|
|
97
208
|
|
|
@@ -101,7 +212,29 @@ def evidence_verify(bundle_id: Optional[str] = None, bundle_path: Optional[str]
|
|
|
101
212
|
_INTERNAL_TOKEN = os.environ.get("DELIMIT_INTERNAL_BRIDGE_TOKEN", "delimit-internal-bridge")
|
|
102
213
|
|
|
103
214
|
|
|
215
|
+
def _fallback_security_result(target: str, tool_label: str) -> Dict[str, Any]:
|
|
216
|
+
"""Run the built-in local security audit when securitygate is unavailable."""
|
|
217
|
+
from .tools_infra import security_audit as local_security_audit
|
|
218
|
+
|
|
219
|
+
result = local_security_audit(target=target)
|
|
220
|
+
result["tool"] = tool_label
|
|
221
|
+
result.setdefault("status", "ok" if "error" not in result else "error")
|
|
222
|
+
result["fallback"] = True
|
|
223
|
+
result["hint"] = (
|
|
224
|
+
"Running basic security audit (free fallback). "
|
|
225
|
+
"For enhanced scanning with CVE detection, install securitygate: "
|
|
226
|
+
"pip install securitygate"
|
|
227
|
+
)
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
|
|
104
231
|
def security_scan(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
232
|
+
"""Perform an enhanced security scan for CVEs and vulnerabilities."""
|
|
233
|
+
try:
|
|
234
|
+
importlib.import_module("securitygate.server")
|
|
235
|
+
except (ImportError, ModuleNotFoundError):
|
|
236
|
+
logger.warning("securitygate module not found, falling back to local security audit")
|
|
237
|
+
return _fallback_security_result(target=target, tool_label="security.scan")
|
|
105
238
|
result = _call("securitygate", "create_securitygate_server", "_tool_scan",
|
|
106
239
|
{"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.scan")
|
|
107
240
|
# Guard against fabricated/hardcoded CVE data from stub implementations
|
|
@@ -113,5 +246,11 @@ def security_scan(target: str = ".", options: Optional[Dict] = None) -> Dict[str
|
|
|
113
246
|
|
|
114
247
|
|
|
115
248
|
def security_audit(target: str = ".", options: Optional[Dict] = None) -> Dict[str, Any]:
|
|
249
|
+
"""Audit source code for dangerous patterns and hardcoded secrets."""
|
|
250
|
+
try:
|
|
251
|
+
importlib.import_module("securitygate.server")
|
|
252
|
+
except (ImportError, ModuleNotFoundError):
|
|
253
|
+
logger.warning("securitygate module not found, using built-in security audit")
|
|
254
|
+
return _fallback_security_result(target=target, tool_label="security.audit")
|
|
116
255
|
return _call("securitygate", "create_securitygate_server", "_tool_audit",
|
|
117
256
|
{"target": target, "authorization_token": _INTERNAL_TOKEN, **(options or {})}, "security.audit")
|