@oswaldzsh/devhive 0.1.0
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/README.md +91 -0
- package/__init__.py +0 -0
- package/agents/__init__.py +0 -0
- package/agents/base.py +118 -0
- package/agents/execute.py +150 -0
- package/agents/verifier_dynamic.py +164 -0
- package/agents/verifier_semantic.py +84 -0
- package/agents/verifier_static.py +153 -0
- package/bin/dh +77 -0
- package/config.yaml +71 -0
- package/control_plane/__init__.py +0 -0
- package/control_plane/cli.py +596 -0
- package/control_plane/dashboard.py +57 -0
- package/control_plane/notifications.py +54 -0
- package/control_plane/tui.py +352 -0
- package/install.sh +67 -0
- package/orchestrator/__init__.py +0 -0
- package/orchestrator/agent_pool.py +107 -0
- package/orchestrator/convergence_gate.py +133 -0
- package/orchestrator/engine.py +353 -0
- package/orchestrator/event_bus.py +58 -0
- package/orchestrator/task_queue.py +59 -0
- package/package.json +50 -0
- package/protocol/__init__.py +0 -0
- package/protocol/schemas.py +222 -0
- package/setup.py +44 -0
- package/signature/__init__.py +0 -0
- package/signature/engine.py +211 -0
- package/signature/extractor.py +156 -0
- package/signature/learner.py +75 -0
- package/signature/src/matcher.c +263 -0
- package/signature/src/matcher.h +135 -0
- package/signatures/seed_signatures.json +174 -0
- package/storage/__init__.py +0 -0
- package/storage/checkpoint.py +153 -0
- package/storage/signature_db.py +62 -0
- package/tools/__init__.py +0 -0
- package/tools/api_client.py +101 -0
- package/tools/git.py +75 -0
- package/tools/sandbox.py +79 -0
- package/verification/__init__.py +0 -0
- package/verification/diagnostic.py +124 -0
- package/verification/patterns/api_breaking.yaml +25 -0
- package/verification/patterns/code_quality.yaml +41 -0
- package/verification/patterns/security.yaml +41 -0
- package/verification/pipeline.py +61 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Static Verifier — analyzes code diff for suspicious patterns."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import yaml
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from agents.base import AgentProcess
|
|
9
|
+
from protocol.schemas import (
|
|
10
|
+
Task, ExecutionHandoff, Verdict, Finding, FindingEvidence,
|
|
11
|
+
Severity, SuggestedAction, VerdictOverall, VerifierType,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
STATIC_SYSTEM_PROMPT = """You are a Static Verifier Agent. Your job is to:
|
|
16
|
+
|
|
17
|
+
1. Read the code diff and Execution Handoff
|
|
18
|
+
2. Identify suspicious patterns in the code itself (not test results)
|
|
19
|
+
3. Output a structured Verdict in JSON format
|
|
20
|
+
|
|
21
|
+
You focus on CODE PATTERNS, not runtime behavior:
|
|
22
|
+
- Public API breaks (changed signatures, unchanged callers)
|
|
23
|
+
- Unsafe operations (no timeout on network calls, missing error handling)
|
|
24
|
+
- Dependency changes (new packages, version bumps)
|
|
25
|
+
- Security issues (injection risks, exposed secrets, missing auth checks)
|
|
26
|
+
- Code quality regressions (swallowed exceptions, overly complex logic)
|
|
27
|
+
|
|
28
|
+
OUTPUT FORMAT:
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"overall": "PASS|WARN|FAIL",
|
|
32
|
+
"findings": [
|
|
33
|
+
{
|
|
34
|
+
"severity": "HIGH|MEDIUM|LOW",
|
|
35
|
+
"category": "api_break|security|dependency|code_quality|...",
|
|
36
|
+
"title": "short description",
|
|
37
|
+
"detail": "detailed explanation",
|
|
38
|
+
"suggested_action": "ROLLBACK|FIX|ESCALATE|IGNORE"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StaticVerifier(AgentProcess):
|
|
47
|
+
"""Checks code structure for suspicious patterns."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, agent_id: str, task_queue, result_queue, config: dict = None):
|
|
50
|
+
super().__init__(agent_id, "static_verifier", task_queue, result_queue, config)
|
|
51
|
+
self.rules: list[dict] = []
|
|
52
|
+
self.rules_dir = ""
|
|
53
|
+
|
|
54
|
+
def _setup(self):
|
|
55
|
+
self.rules_dir = self.config.get("rules_dir", "verification/patterns/")
|
|
56
|
+
self._load_rules()
|
|
57
|
+
|
|
58
|
+
def _load_rules(self):
|
|
59
|
+
"""Load YAML rule definitions."""
|
|
60
|
+
if not os.path.isdir(self.rules_dir):
|
|
61
|
+
return
|
|
62
|
+
for fname in sorted(os.listdir(self.rules_dir)):
|
|
63
|
+
if fname.endswith((".yaml", ".yml")):
|
|
64
|
+
with open(os.path.join(self.rules_dir, fname)) as f:
|
|
65
|
+
data = yaml.safe_load(f)
|
|
66
|
+
if data and "patterns" in data:
|
|
67
|
+
self.rules.extend(data["patterns"])
|
|
68
|
+
|
|
69
|
+
def _execute(self, task: Task) -> Verdict:
|
|
70
|
+
handoff = task # In practice the task payload contains the Handoff
|
|
71
|
+
|
|
72
|
+
# 1. Run deterministic rule checks
|
|
73
|
+
rule_findings = self._check_rules(task)
|
|
74
|
+
|
|
75
|
+
# 2. Ask LLM for pattern-based analysis
|
|
76
|
+
llm_findings = self._llm_analyze(task)
|
|
77
|
+
|
|
78
|
+
all_findings = rule_findings + llm_findings
|
|
79
|
+
|
|
80
|
+
# Determine overall verdict
|
|
81
|
+
has_high = any(f.severity == Severity.HIGH for f in all_findings)
|
|
82
|
+
has_medium = any(f.severity == Severity.MEDIUM for f in all_findings)
|
|
83
|
+
if has_high:
|
|
84
|
+
overall = VerdictOverall.FAIL
|
|
85
|
+
elif has_medium:
|
|
86
|
+
overall = VerdictOverall.WARN
|
|
87
|
+
else:
|
|
88
|
+
overall = VerdictOverall.PASS
|
|
89
|
+
|
|
90
|
+
return Verdict(
|
|
91
|
+
verifier_type=VerifierType.STATIC,
|
|
92
|
+
task_id=task.id,
|
|
93
|
+
overall=overall,
|
|
94
|
+
findings=all_findings,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _check_rules(self, task: Task) -> list[Finding]:
|
|
98
|
+
"""Run deterministic YAML rules against the diff."""
|
|
99
|
+
findings = []
|
|
100
|
+
for rule in self.rules:
|
|
101
|
+
# Apply each rule's detector logic
|
|
102
|
+
detector = rule.get("detector", {})
|
|
103
|
+
if detector.get("type") == "file_diff":
|
|
104
|
+
findings.append(Finding(
|
|
105
|
+
severity=Severity(rule.get("severity", "MEDIUM")),
|
|
106
|
+
category=rule.get("name", "rule_match"),
|
|
107
|
+
title=rule.get("desc", ""),
|
|
108
|
+
detail=f"Rule {rule['id']} matched: {rule.get('desc', '')}",
|
|
109
|
+
evidence=FindingEvidence(type="rule", data=json.dumps(rule)),
|
|
110
|
+
suggested_action=SuggestedAction.ESCALATE,
|
|
111
|
+
))
|
|
112
|
+
return findings
|
|
113
|
+
|
|
114
|
+
def _llm_analyze(self, task: Task) -> list[Finding]:
|
|
115
|
+
"""Use LLM for pattern-based code analysis."""
|
|
116
|
+
import asyncio
|
|
117
|
+
|
|
118
|
+
task_json = json.dumps(task.model_dump(), indent=2, default=str)
|
|
119
|
+
|
|
120
|
+
loop = asyncio.new_event_loop()
|
|
121
|
+
try:
|
|
122
|
+
response = loop.run_until_complete(
|
|
123
|
+
self._call_model(STATIC_SYSTEM_PROMPT, task_json, max_tokens=4096)
|
|
124
|
+
)
|
|
125
|
+
finally:
|
|
126
|
+
loop.close()
|
|
127
|
+
|
|
128
|
+
return self._parse_findings(response)
|
|
129
|
+
|
|
130
|
+
def _parse_findings(self, response: dict) -> list[Finding]:
|
|
131
|
+
"""Extract findings from LLM response."""
|
|
132
|
+
import re
|
|
133
|
+
text = ""
|
|
134
|
+
for block in response.get("content", []):
|
|
135
|
+
if block.get("type") == "text":
|
|
136
|
+
text += block["text"]
|
|
137
|
+
|
|
138
|
+
match = re.search(r'```json\s*\n(.*?)\n```', text, re.DOTALL)
|
|
139
|
+
if not match:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
data = json.loads(match.group(1))
|
|
143
|
+
return [
|
|
144
|
+
Finding(
|
|
145
|
+
severity=Severity(f.get("severity", "MEDIUM")),
|
|
146
|
+
category=f.get("category", "unknown"),
|
|
147
|
+
title=f.get("title", ""),
|
|
148
|
+
detail=f.get("detail", ""),
|
|
149
|
+
evidence=FindingEvidence(type="llm_analysis", data=json.dumps(f)),
|
|
150
|
+
suggested_action=SuggestedAction(f.get("suggested_action", "ESCALATE")),
|
|
151
|
+
)
|
|
152
|
+
for f in data.get("findings", [])
|
|
153
|
+
]
|
package/bin/dh
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# dh — DevHive CLI entry point
|
|
3
|
+
# Resolves the Python module path and executes it.
|
|
4
|
+
#
|
|
5
|
+
# Resolution order:
|
|
6
|
+
# 1. $DEVHIVE_HOME (explicit override)
|
|
7
|
+
# 2. NPM global install: $(npm root -g)/devhive
|
|
8
|
+
# 3. ~/.devhive (postinstall copy)
|
|
9
|
+
# 4. ../ relative to this script (dev mode)
|
|
10
|
+
# 5. /usr/local/lib/devhive
|
|
11
|
+
|
|
12
|
+
set -e
|
|
13
|
+
|
|
14
|
+
# ── Resolve module directory ────────────────────────────────
|
|
15
|
+
|
|
16
|
+
find_module_dir() {
|
|
17
|
+
# 1. Explicit override
|
|
18
|
+
if [ -n "$DEVHIVE_HOME" ] && [ -d "$DEVHIVE_HOME/control_plane" ]; then
|
|
19
|
+
echo "$DEVHIVE_HOME"
|
|
20
|
+
return
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# 2. NPM global install (same dir as this script is in node_modules/devhive/bin)
|
|
24
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
25
|
+
NPM_MODULE="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)"
|
|
26
|
+
if [ -d "$NPM_MODULE/control_plane" ]; then
|
|
27
|
+
echo "$NPM_MODULE"
|
|
28
|
+
return
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# 3. Home dir copy
|
|
32
|
+
if [ -d "$HOME/.devhive/control_plane" ]; then
|
|
33
|
+
echo "$HOME/.devhive"
|
|
34
|
+
return
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 4. System-wide
|
|
38
|
+
if [ -d "/usr/local/lib/devhive/control_plane" ]; then
|
|
39
|
+
echo "/usr/local/lib/devhive"
|
|
40
|
+
return
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo ""
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
MODULE_DIR=$(find_module_dir)
|
|
47
|
+
|
|
48
|
+
if [ -z "$MODULE_DIR" ]; then
|
|
49
|
+
echo "DevHive: module not found." >&2
|
|
50
|
+
echo " Set DEVHIVE_HOME to the module path, or reinstall:" >&2
|
|
51
|
+
echo " npm install -g devhive" >&2
|
|
52
|
+
echo " pip3 install devhive" >&2
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ── Resolve Python ──────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
PYTHON="${DEVHIVE_PYTHON:-}"
|
|
59
|
+
if [ -z "$PYTHON" ]; then
|
|
60
|
+
for cmd in python3 python; do
|
|
61
|
+
if command -v "$cmd" &>/dev/null; then
|
|
62
|
+
PYTHON="$cmd"
|
|
63
|
+
break
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
if [ -z "$PYTHON" ]; then
|
|
69
|
+
echo "DevHive: Python 3.12+ required but not found." >&2
|
|
70
|
+
exit 1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
# ── Run ─────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export PYTHONPATH="$MODULE_DIR:$PYTHONPATH"
|
|
76
|
+
cd "$MODULE_DIR"
|
|
77
|
+
exec "$PYTHON" -m control_plane.cli "$@"
|
package/config.yaml
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# DevHive Configuration
|
|
2
|
+
api:
|
|
3
|
+
base_url: "https://aiapi.lejurobot.com"
|
|
4
|
+
auth_token: "${LEJU_TOKEN}"
|
|
5
|
+
default_model: "deepseek/deepseek-v4-pro"
|
|
6
|
+
|
|
7
|
+
agents:
|
|
8
|
+
execute:
|
|
9
|
+
count: 2
|
|
10
|
+
max_context: 180000
|
|
11
|
+
timeout_seconds: 600
|
|
12
|
+
tools:
|
|
13
|
+
- read_file
|
|
14
|
+
- write_file
|
|
15
|
+
- edit_file
|
|
16
|
+
- bash
|
|
17
|
+
- git
|
|
18
|
+
sandbox:
|
|
19
|
+
file_whitelist: [".py", ".ts", ".js", ".rs", ".go", ".yaml", ".json", ".md", ".toml", ".cfg"]
|
|
20
|
+
forbidden_commands: ["rm -rf", "git push --force", "sudo", "chmod 777"]
|
|
21
|
+
|
|
22
|
+
static_verifier:
|
|
23
|
+
count: 1
|
|
24
|
+
max_context: 80000
|
|
25
|
+
timeout_seconds: 120
|
|
26
|
+
rules_dir: "verification/patterns/"
|
|
27
|
+
|
|
28
|
+
dynamic_verifier:
|
|
29
|
+
count: 1
|
|
30
|
+
max_context: 80000
|
|
31
|
+
timeout_seconds: 300
|
|
32
|
+
call_graph_db: "storage/call_graph.sqlite"
|
|
33
|
+
drift_thresholds:
|
|
34
|
+
duration_change_pct: 20
|
|
35
|
+
memory_change_pct: 50
|
|
36
|
+
log_volume_change_multiplier: 3.0
|
|
37
|
+
|
|
38
|
+
semantic_verifier:
|
|
39
|
+
count: 1
|
|
40
|
+
max_context: 120000
|
|
41
|
+
timeout_seconds: 180
|
|
42
|
+
trigger: "on_conflict_or_l2"
|
|
43
|
+
|
|
44
|
+
convergence:
|
|
45
|
+
l1_max_retries: 3
|
|
46
|
+
l2_max_retries: 2
|
|
47
|
+
loop_detection_window: 5
|
|
48
|
+
loop_similarity_threshold: 0.8
|
|
49
|
+
|
|
50
|
+
signature_db:
|
|
51
|
+
path: "signatures/signatures.db"
|
|
52
|
+
seed_file: "signatures/seed_signatures.json"
|
|
53
|
+
min_confidence: 0.65
|
|
54
|
+
top_k: 3
|
|
55
|
+
auto_learn: true
|
|
56
|
+
|
|
57
|
+
escalation:
|
|
58
|
+
notify:
|
|
59
|
+
- type: "desktop_notification"
|
|
60
|
+
- type: "terminal_bell"
|
|
61
|
+
require_approval_for:
|
|
62
|
+
- auth
|
|
63
|
+
- payment
|
|
64
|
+
- data_deletion
|
|
65
|
+
- permission_change
|
|
66
|
+
|
|
67
|
+
sandbox:
|
|
68
|
+
docker_image: "devhive-sandbox:latest"
|
|
69
|
+
memory_limit: "2g"
|
|
70
|
+
cpu_limit: 2
|
|
71
|
+
timeout_default: 600
|
|
File without changes
|