conduct-cli 0.4.66__tar.gz → 0.4.68__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/PKG-INFO +1 -1
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/pyproject.toml +1 -1
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/guardmcp.py +2 -2
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/hook_precompact_template.py +7 -3
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/main.py +127 -1
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/README.md +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/setup.cfg +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/setup.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.66 → conduct_cli-0.4.68}/tests/test_switch.py +0 -0
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""
|
|
3
3
|
conductguard-mcp — ConductGuard MCP server.
|
|
4
4
|
|
|
5
|
-
Runs as a subprocess started by Claude Code / Cursor / Windsurf via
|
|
6
|
-
mcpServers config written by `conduct guard sync`. Communicates over
|
|
5
|
+
Runs as a subprocess started by Claude Code / Codex / Cursor / Windsurf via
|
|
6
|
+
the mcpServers config written by `conduct guard sync`. Communicates over
|
|
7
7
|
stdin/stdout using JSON-RPC 2.0 (MCP stdio transport).
|
|
8
8
|
|
|
9
9
|
Exposes three tools:
|
|
@@ -35,9 +35,13 @@ def _memory_headline():
|
|
|
35
35
|
try:
|
|
36
36
|
root = Path.cwd()
|
|
37
37
|
mem_key = str(root).replace("/", "-").lstrip("-")
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
candidates = [
|
|
39
|
+
Path.home() / ".claude" / "projects" / mem_key / "memory" / "MEMORY.md",
|
|
40
|
+
Path.home() / ".codex" / "projects" / mem_key / "memory" / "MEMORY.md",
|
|
41
|
+
]
|
|
42
|
+
for mem_path in candidates:
|
|
43
|
+
if mem_path.exists():
|
|
44
|
+
return "\n".join(mem_path.read_text().splitlines()[:10])
|
|
41
45
|
except Exception:
|
|
42
46
|
pass
|
|
43
47
|
return ""
|
|
@@ -2126,6 +2126,111 @@ def classify_finding(text: str) -> "dict | None":
|
|
|
2126
2126
|
return None
|
|
2127
2127
|
|
|
2128
2128
|
|
|
2129
|
+
def cmd_session_report(args):
|
|
2130
|
+
"""
|
|
2131
|
+
Run paxel-local against local Claude Code transcripts and send a
|
|
2132
|
+
developer profile report to the workspace admin via the Conduct API.
|
|
2133
|
+
"""
|
|
2134
|
+
import json as _json
|
|
2135
|
+
import shutil
|
|
2136
|
+
import subprocess
|
|
2137
|
+
import tempfile
|
|
2138
|
+
import urllib.request
|
|
2139
|
+
|
|
2140
|
+
server, workspace_id, api_key, token = _require_auth(args)
|
|
2141
|
+
hdrs = api.headers(workspace_id, token, "application/json", api_key)
|
|
2142
|
+
|
|
2143
|
+
# ── 1. Fetch paxel-local ─────────────────────────────────────────────────
|
|
2144
|
+
PAXEL_URL = "https://raw.githubusercontent.com/Photobombastic/paxel-local/main/paxel.py"
|
|
2145
|
+
tmpdir = tempfile.mkdtemp(prefix="conduct-paxel-")
|
|
2146
|
+
paxel_script = Path(tmpdir) / "paxel.py"
|
|
2147
|
+
|
|
2148
|
+
print("Downloading paxel-local…")
|
|
2149
|
+
try:
|
|
2150
|
+
urllib.request.urlretrieve(PAXEL_URL, paxel_script)
|
|
2151
|
+
except Exception as exc:
|
|
2152
|
+
print(f"ERROR: could not download paxel-local: {exc}")
|
|
2153
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2154
|
+
sys.exit(1)
|
|
2155
|
+
|
|
2156
|
+
# ── 2. Run paxel ─────────────────────────────────────────────────────────
|
|
2157
|
+
print("Analysing sessions…")
|
|
2158
|
+
try:
|
|
2159
|
+
result = subprocess.run(
|
|
2160
|
+
[sys.executable, str(paxel_script), "--no-open"],
|
|
2161
|
+
cwd=tmpdir,
|
|
2162
|
+
capture_output=True,
|
|
2163
|
+
text=True,
|
|
2164
|
+
timeout=120,
|
|
2165
|
+
)
|
|
2166
|
+
except subprocess.TimeoutExpired:
|
|
2167
|
+
print("ERROR: paxel analysis timed out after 120 s.")
|
|
2168
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2169
|
+
sys.exit(1)
|
|
2170
|
+
|
|
2171
|
+
stats_path = Path(tmpdir) / "stats.json"
|
|
2172
|
+
report_path = Path(tmpdir) / "report.md"
|
|
2173
|
+
|
|
2174
|
+
if not stats_path.exists():
|
|
2175
|
+
print("ERROR: paxel did not produce stats.json — no transcripts found?")
|
|
2176
|
+
if result.stderr:
|
|
2177
|
+
print(result.stderr[:500])
|
|
2178
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2179
|
+
sys.exit(1)
|
|
2180
|
+
|
|
2181
|
+
with open(stats_path) as f:
|
|
2182
|
+
stats = _json.load(f)
|
|
2183
|
+
|
|
2184
|
+
report_md = report_path.read_text() if report_path.exists() else ""
|
|
2185
|
+
|
|
2186
|
+
# ── 3. Build summary payload ─────────────────────────────────────────────
|
|
2187
|
+
import getpass, socket
|
|
2188
|
+
developer = getattr(args, "developer", None) or getpass.getuser()
|
|
2189
|
+
hostname = socket.gethostname()
|
|
2190
|
+
|
|
2191
|
+
archetype = stats.get("archetype", {})
|
|
2192
|
+
competency = stats.get("competency_scores", stats.get("competencies", {}))
|
|
2193
|
+
sessions = stats.get("total_sessions", stats.get("session_count", "?"))
|
|
2194
|
+
tools_used = stats.get("tools_detected", stats.get("tools", []))
|
|
2195
|
+
|
|
2196
|
+
payload = {
|
|
2197
|
+
"developer": developer,
|
|
2198
|
+
"hostname": hostname,
|
|
2199
|
+
"archetype": archetype.get("name", archetype) if isinstance(archetype, dict) else str(archetype),
|
|
2200
|
+
"archetype_description": archetype.get("description", "") if isinstance(archetype, dict) else "",
|
|
2201
|
+
"competency_scores": competency,
|
|
2202
|
+
"total_sessions": sessions,
|
|
2203
|
+
"tools_detected": tools_used,
|
|
2204
|
+
"report_md": report_md[:4000], # cap to avoid huge payloads
|
|
2205
|
+
"raw_stats": stats,
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
# ── 4. Send to Conduct API ────────────────────────────────────────────────
|
|
2209
|
+
print("Sending report to admin…")
|
|
2210
|
+
resp = api.post(
|
|
2211
|
+
server,
|
|
2212
|
+
"/session-reports",
|
|
2213
|
+
payload,
|
|
2214
|
+
hdrs,
|
|
2215
|
+
)
|
|
2216
|
+
|
|
2217
|
+
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
2218
|
+
|
|
2219
|
+
if resp and resp.get("id"):
|
|
2220
|
+
arch = payload["archetype"]
|
|
2221
|
+
exec_score = competency.get("execution", competency.get("Execution", "?"))
|
|
2222
|
+
plan_score = competency.get("planning", competency.get("Planning", "?"))
|
|
2223
|
+
eng_score = competency.get("engineering", competency.get("Engineering", "?"))
|
|
2224
|
+
print(f"\nReport sent.")
|
|
2225
|
+
print(f" Archetype : {arch}")
|
|
2226
|
+
print(f" Execution : {exec_score} Planning: {plan_score} Engineering: {eng_score}")
|
|
2227
|
+
print(f" Sessions : {sessions}")
|
|
2228
|
+
if tools_used:
|
|
2229
|
+
print(f" Tools : {', '.join(tools_used) if isinstance(tools_used, list) else tools_used}")
|
|
2230
|
+
else:
|
|
2231
|
+
print(f"WARNING: server response unexpected: {resp}")
|
|
2232
|
+
|
|
2233
|
+
|
|
2129
2234
|
def cmd_emit_finding(args):
|
|
2130
2235
|
"""POST a security finding to /security-findings."""
|
|
2131
2236
|
server, workspace_id, api_key, token = _require_auth(args)
|
|
@@ -2344,8 +2449,23 @@ def cmd_test_security_verify(args):
|
|
|
2344
2449
|
TIMEOUT = 300 # 15 runs × ~28s / 4 workers ≈ 105s; 300s gives headroom for queue variance
|
|
2345
2450
|
POLL_SECS = 5
|
|
2346
2451
|
|
|
2347
|
-
# ── Step
|
|
2452
|
+
# ── Step 0: fresh Security Automation project ─────────────────────────
|
|
2348
2453
|
print(f"\n{BOLD}▶ conduct test-security-verify{RESET}")
|
|
2454
|
+
print(f" {GRAY}Step 0/3 — refreshing Security Automation project…{RESET}")
|
|
2455
|
+
try:
|
|
2456
|
+
req = urllib.request.Request(
|
|
2457
|
+
f"{api_url}/secure/refresh-automation?workspace_id={workspace_id}",
|
|
2458
|
+
data=b"{}",
|
|
2459
|
+
headers={"Content-Type": "application/json", "X-Api-Key": api_key},
|
|
2460
|
+
method="POST",
|
|
2461
|
+
)
|
|
2462
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
2463
|
+
_json.loads(resp.read())
|
|
2464
|
+
print(f" {GREEN}✓{RESET} Fresh project created with latest YAML\n")
|
|
2465
|
+
except Exception as e:
|
|
2466
|
+
print(f" {YELLOW}⚠ refresh failed ({e}) — using existing project{RESET}\n")
|
|
2467
|
+
|
|
2468
|
+
# ── Step 1: post test findings ────────────────────────────────────────
|
|
2349
2469
|
print(f" {GRAY}Step 1/3 — posting {len(_SECURITY_TEST_CASES)} test findings…{RESET}\n")
|
|
2350
2470
|
|
|
2351
2471
|
# Clean previous run
|
|
@@ -2400,6 +2520,7 @@ def cmd_test_security_verify(args):
|
|
|
2400
2520
|
|
|
2401
2521
|
# ── Step 2: poll until all findings move off "open" ───────────────────
|
|
2402
2522
|
print(f"\n {GRAY}Step 2/3 — waiting for triage pipeline (timeout {TIMEOUT}s)…{RESET}\n")
|
|
2523
|
+
|
|
2403
2524
|
deadline = _time.time() + TIMEOUT
|
|
2404
2525
|
final_statuses: dict[str, str] = {}
|
|
2405
2526
|
|
|
@@ -2435,6 +2556,7 @@ def cmd_test_security_verify(args):
|
|
|
2435
2556
|
# ── Step 3: report per-finding results ────────────────────────────────
|
|
2436
2557
|
print(f"\n {GRAY}Step 3/3 — results{RESET}\n")
|
|
2437
2558
|
|
|
2559
|
+
|
|
2438
2560
|
all_pass = True
|
|
2439
2561
|
name_by_id = {}
|
|
2440
2562
|
for i, (name, *_) in enumerate(_SECURITY_TEST_CASES):
|
|
@@ -2713,6 +2835,8 @@ def main():
|
|
|
2713
2835
|
sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
|
|
2714
2836
|
sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
|
|
2715
2837
|
sub.add_parser("test-security-verify", help="Post test findings and verify full triage pipeline end-to-end")
|
|
2838
|
+
sr_p = sub.add_parser("session-report", help="Analyse local AI coding sessions with paxel and send report to admin")
|
|
2839
|
+
sr_p.add_argument("--developer", default=None, help="Developer name (defaults to OS username)")
|
|
2716
2840
|
|
|
2717
2841
|
args = parser.parse_args()
|
|
2718
2842
|
|
|
@@ -2789,6 +2913,8 @@ def main():
|
|
|
2789
2913
|
cmd_test_security(args)
|
|
2790
2914
|
elif args.command == "test-security-verify":
|
|
2791
2915
|
cmd_test_security_verify(args)
|
|
2916
|
+
elif args.command == "session-report":
|
|
2917
|
+
cmd_session_report(args)
|
|
2792
2918
|
else:
|
|
2793
2919
|
parser.print_help()
|
|
2794
2920
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|