conduct-cli 0.5.0__tar.gz → 0.5.2__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.5.0 → conduct_cli-0.5.2}/PKG-INFO +1 -1
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/pyproject.toml +1 -1
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/guard.py +39 -11
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/guardmcp.py +35 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_template.py +6 -2
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/main.py +32 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/README.md +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/setup.cfg +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/setup.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.2}/tests/test_switch.py +0 -0
|
@@ -199,12 +199,19 @@ _MCP_TARGETS = [
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
|
|
202
|
-
"""Write conductguard MCP
|
|
202
|
+
"""Write conductguard + agent-booster MCP entries into every AI tool config found.
|
|
203
203
|
|
|
204
204
|
Credentials are NOT stored in the MCP config — the server reads them from
|
|
205
205
|
~/.conductguard/config.json at startup, which is written by guard sync.
|
|
206
206
|
"""
|
|
207
|
-
|
|
207
|
+
import shutil
|
|
208
|
+
servers: dict[str, dict] = {
|
|
209
|
+
"conductguard": {"command": "conductguard-mcp"},
|
|
210
|
+
}
|
|
211
|
+
# Register agent-booster only if the binary is available
|
|
212
|
+
if shutil.which("booster"):
|
|
213
|
+
servers["agent-booster"] = {"command": "booster", "args": ["serve"]}
|
|
214
|
+
|
|
208
215
|
found_any = False
|
|
209
216
|
for cfg_path, label in _MCP_TARGETS:
|
|
210
217
|
if not cfg_path.exists():
|
|
@@ -215,12 +222,16 @@ def _register_mcp(workspace_id: str, member_token: str, api_url: str) -> None:
|
|
|
215
222
|
except (json.JSONDecodeError, OSError):
|
|
216
223
|
existing = {}
|
|
217
224
|
mcp = existing.setdefault("mcpServers", {})
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
225
|
+
changed = False
|
|
226
|
+
for name, entry in servers.items():
|
|
227
|
+
if mcp.get(name) == entry:
|
|
228
|
+
print(f" {GRAY}{name} MCP already registered in {label}{RESET}")
|
|
229
|
+
else:
|
|
230
|
+
mcp[name] = entry
|
|
231
|
+
changed = True
|
|
232
|
+
print(f" {GREEN}{name} MCP registered in {label}{RESET}")
|
|
233
|
+
if changed:
|
|
234
|
+
cfg_path.write_text(json.dumps(existing, indent=2))
|
|
224
235
|
if not found_any:
|
|
225
236
|
print(f" {GRAY}No AI tool configs found for MCP registration{RESET}")
|
|
226
237
|
|
|
@@ -474,6 +485,7 @@ def cmd_guard_install(args):
|
|
|
474
485
|
pass
|
|
475
486
|
|
|
476
487
|
# Persist guard config — include api_key so CLI commands can authenticate
|
|
488
|
+
import time as _time
|
|
477
489
|
_save_guard_config({
|
|
478
490
|
"workspace_id": workspace_id,
|
|
479
491
|
"member_token": member_token,
|
|
@@ -482,6 +494,7 @@ def cmd_guard_install(args):
|
|
|
482
494
|
"api_key": api_key,
|
|
483
495
|
"api_url": server,
|
|
484
496
|
"security_emit_enabled": security_emit,
|
|
497
|
+
"last_synced_at": _time.time(),
|
|
485
498
|
})
|
|
486
499
|
if security_emit:
|
|
487
500
|
print(f" {GREEN}Security Loop:{RESET} installed — classifier active")
|
|
@@ -545,10 +558,12 @@ def cmd_guard_join(args):
|
|
|
545
558
|
print(f" {GREEN}Policy downloaded:{RESET} {rule_count} rule(s)")
|
|
546
559
|
|
|
547
560
|
# Persist guard config
|
|
561
|
+
import time as _time
|
|
548
562
|
cfg = {
|
|
549
|
-
"workspace_id":
|
|
550
|
-
"user_email":
|
|
551
|
-
"api_url":
|
|
563
|
+
"workspace_id": workspace_id,
|
|
564
|
+
"user_email": email,
|
|
565
|
+
"api_url": base_url,
|
|
566
|
+
"last_synced_at": _time.time(),
|
|
552
567
|
}
|
|
553
568
|
if member_token:
|
|
554
569
|
cfg["member_token"] = member_token
|
|
@@ -878,6 +893,8 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
|
|
|
878
893
|
"saved_tokens": raw.get("saved_tokens", 0),
|
|
879
894
|
"savings_pct": raw.get("savings_pct", 0.0),
|
|
880
895
|
"total_reads": raw.get("total_reads", 0),
|
|
896
|
+
"crusher": raw.get("crusher", {}),
|
|
897
|
+
"cache_align": raw.get("cache_align", {}),
|
|
881
898
|
}
|
|
882
899
|
except Exception:
|
|
883
900
|
pass
|
|
@@ -929,6 +946,14 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
|
|
|
929
946
|
except Exception:
|
|
930
947
|
pass # Never fail sync because savings POST failed
|
|
931
948
|
|
|
949
|
+
# Push booster symbol index to team workspace (best-effort)
|
|
950
|
+
try:
|
|
951
|
+
r = subprocess.run(["booster", "index-push"], capture_output=True, text=True, timeout=30)
|
|
952
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
953
|
+
print(f" {GREEN}Booster index:{RESET} {r.stdout.strip()}")
|
|
954
|
+
except Exception:
|
|
955
|
+
pass
|
|
956
|
+
|
|
932
957
|
|
|
933
958
|
def cmd_guard_status(args):
|
|
934
959
|
cfg = _require_guard_config()
|
|
@@ -1154,6 +1179,9 @@ def register_guard_parser(sub):
|
|
|
1154
1179
|
# conduct guard booster-status
|
|
1155
1180
|
guard_sub.add_parser("booster-status", help="Verify Agent Booster intercept is active for this project")
|
|
1156
1181
|
|
|
1182
|
+
# conduct guard skip-setup
|
|
1183
|
+
guard_sub.add_parser("skip-setup", help="Suppress the Guard setup reminder (does not disable Guard)")
|
|
1184
|
+
|
|
1157
1185
|
return guard_p, guard_sub
|
|
1158
1186
|
|
|
1159
1187
|
|
|
@@ -15,7 +15,10 @@ from __future__ import annotations
|
|
|
15
15
|
import argparse
|
|
16
16
|
import json
|
|
17
17
|
import re
|
|
18
|
+
import subprocess
|
|
18
19
|
import sys
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
19
22
|
import uuid
|
|
20
23
|
import urllib.request
|
|
21
24
|
import urllib.error
|
|
@@ -136,6 +139,32 @@ def _load_config() -> dict:
|
|
|
136
139
|
return {}
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
_sync_lock = threading.Lock()
|
|
143
|
+
_SYNC_INTERVAL = 300 # 5 minutes
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _maybe_sync() -> None:
|
|
147
|
+
"""If config is stale (>5 min since last sync), run `conduct guard sync` in background."""
|
|
148
|
+
cfg = _load_config()
|
|
149
|
+
last_sync = cfg.get("last_synced_at", 0)
|
|
150
|
+
if time.time() - last_sync < _SYNC_INTERVAL:
|
|
151
|
+
return
|
|
152
|
+
if not _sync_lock.acquire(blocking=False):
|
|
153
|
+
return # another thread is already syncing
|
|
154
|
+
def _run():
|
|
155
|
+
try:
|
|
156
|
+
subprocess.run(
|
|
157
|
+
["conduct", "guard", "sync"],
|
|
158
|
+
timeout=15,
|
|
159
|
+
capture_output=True,
|
|
160
|
+
)
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
finally:
|
|
164
|
+
_sync_lock.release()
|
|
165
|
+
threading.Thread(target=_run, daemon=True, name="guard-auto-sync").start()
|
|
166
|
+
|
|
167
|
+
|
|
139
168
|
def _detect_surface(client_info: dict) -> str:
|
|
140
169
|
"""Map MCP clientInfo.name → ai_tool label sent to Guard API."""
|
|
141
170
|
name = (client_info.get("name") or "").lower()
|
|
@@ -293,6 +322,12 @@ def _handle_guard_sync(workspace_id: str, token: str) -> str:
|
|
|
293
322
|
|
|
294
323
|
|
|
295
324
|
def _dispatch_tool(name: str, arguments: dict, workspace_id: str, token: str, ai_tool: str) -> str:
|
|
325
|
+
# Always re-read config so workspace_id/token stay fresh between syncs
|
|
326
|
+
_maybe_sync()
|
|
327
|
+
cfg = _load_config()
|
|
328
|
+
workspace_id = cfg.get("workspace_id") or workspace_id
|
|
329
|
+
token = cfg.get("member_token") or token
|
|
330
|
+
|
|
296
331
|
if name == "guard_status":
|
|
297
332
|
return _handle_guard_status(workspace_id)
|
|
298
333
|
if name == "guard_check":
|
|
@@ -219,6 +219,8 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, ses
|
|
|
219
219
|
if not workspace_id:
|
|
220
220
|
return
|
|
221
221
|
|
|
222
|
+
import platform as _platform
|
|
223
|
+
_os = f"{_platform.system()} {_platform.release()} {_platform.machine()}".strip()
|
|
222
224
|
payload = json.dumps({
|
|
223
225
|
"workspace_id": workspace_id,
|
|
224
226
|
"clerk_user_id": cfg.get("user_email"),
|
|
@@ -228,8 +230,10 @@ def _post_event(tool_name, tool_input, decision, rule_id=None, message=None, ses
|
|
|
228
230
|
"input_summary": json.dumps(tool_input)[:200],
|
|
229
231
|
"decision": decision,
|
|
230
232
|
"rule_id": rule_id,
|
|
231
|
-
"rule_message":
|
|
232
|
-
"hook_session_id":
|
|
233
|
+
"rule_message": message,
|
|
234
|
+
"hook_session_id": session_id,
|
|
235
|
+
"os_info": _os,
|
|
236
|
+
"hostname": _platform.node(),
|
|
233
237
|
})
|
|
234
238
|
api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
|
|
235
239
|
script = (
|
|
@@ -2823,6 +2823,30 @@ def cmd_memory(args):
|
|
|
2823
2823
|
|
|
2824
2824
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
2825
2825
|
|
|
2826
|
+
_GUARD_CONFIG = Path.home() / ".conductguard" / "config.json"
|
|
2827
|
+
_GUARD_SKIP = Path.home() / ".conductguard" / ".setup_skip"
|
|
2828
|
+
|
|
2829
|
+
GREEN = "\033[32m"
|
|
2830
|
+
YELLOW = "\033[33m"
|
|
2831
|
+
BOLD = "\033[1m"
|
|
2832
|
+
RESET = "\033[0m"
|
|
2833
|
+
|
|
2834
|
+
|
|
2835
|
+
def _check_guard_setup(command: str) -> None:
|
|
2836
|
+
"""On first run after install, prompt user to run conduct guard sync."""
|
|
2837
|
+
# Skip if: already set up, user said skip, or running guard sync/login itself
|
|
2838
|
+
if command in ("guard", "login", "whoami", "version"):
|
|
2839
|
+
return
|
|
2840
|
+
if _GUARD_CONFIG.exists() or _GUARD_SKIP.exists():
|
|
2841
|
+
return
|
|
2842
|
+
print(
|
|
2843
|
+
f"\n{YELLOW}{BOLD}⚡ Conduct Guard is not set up on this machine.{RESET}\n"
|
|
2844
|
+
f" Run {BOLD}conduct guard sync{RESET} to register policy hooks and MCP servers.\n"
|
|
2845
|
+
f" (This takes ~5 seconds and only needs to happen once per machine.)\n"
|
|
2846
|
+
f" To skip this reminder: {BOLD}conduct guard skip-setup{RESET}\n"
|
|
2847
|
+
)
|
|
2848
|
+
|
|
2849
|
+
|
|
2826
2850
|
def main():
|
|
2827
2851
|
_auto_update()
|
|
2828
2852
|
|
|
@@ -2983,6 +3007,14 @@ def main():
|
|
|
2983
3007
|
|
|
2984
3008
|
args = parser.parse_args()
|
|
2985
3009
|
|
|
3010
|
+
_check_guard_setup(args.command or "")
|
|
3011
|
+
|
|
3012
|
+
if args.command == "guard" and getattr(args, "guard_command", None) == "skip-setup":
|
|
3013
|
+
_GUARD_SKIP.parent.mkdir(parents=True, exist_ok=True)
|
|
3014
|
+
_GUARD_SKIP.touch()
|
|
3015
|
+
print(f"{GREEN}✓ Setup reminder suppressed.{RESET} Run `conduct guard sync` anytime to enable Guard.")
|
|
3016
|
+
return
|
|
3017
|
+
|
|
2986
3018
|
if args.command == "login":
|
|
2987
3019
|
cmd_login(args)
|
|
2988
3020
|
elif args.command == "agents":
|
|
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
|
|
File without changes
|
|
File without changes
|