conduct-cli 0.5.0__tar.gz → 0.5.3__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.3}/PKG-INFO +1 -1
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/pyproject.toml +1 -1
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/guard.py +78 -12
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/guardmcp.py +35 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_template.py +6 -2
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/main.py +32 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/README.md +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/setup.cfg +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/setup.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.5.0 → conduct_cli-0.5.3}/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
|
|
@@ -718,6 +733,9 @@ def cmd_guard_sync(args):
|
|
|
718
733
|
rule_count = len(policy.get("rules", []))
|
|
719
734
|
print(f" {GREEN}Policy refreshed:{RESET} {rule_count} rule(s)")
|
|
720
735
|
|
|
736
|
+
if getattr(args, "cursor", False):
|
|
737
|
+
_write_cursorrules(policy)
|
|
738
|
+
|
|
721
739
|
# Re-check Security Loop install status
|
|
722
740
|
try:
|
|
723
741
|
sec = _req("GET", f"{base_url}/secure/installed?workspace_id={workspace_id}", api_key=api_key)
|
|
@@ -770,6 +788,40 @@ def cmd_guard_sync(args):
|
|
|
770
788
|
print(f"\n {CYAN}{mcp_url}{RESET}\n")
|
|
771
789
|
|
|
772
790
|
|
|
791
|
+
def _write_cursorrules(policy: dict) -> None:
|
|
792
|
+
"""Write active Guard policies into .cursorrules in the current directory."""
|
|
793
|
+
rules = policy.get("rules", [])
|
|
794
|
+
enabled = [r for r in rules if r.get("enabled", True)]
|
|
795
|
+
lines = [
|
|
796
|
+
"# .cursorrules — generated by Conduct AI Guard",
|
|
797
|
+
"# Run `conduct guard sync --cursor` to refresh.",
|
|
798
|
+
"# Do not edit manually — changes will be overwritten.",
|
|
799
|
+
"",
|
|
800
|
+
"## Conduct Guard Policies",
|
|
801
|
+
f"# {len(enabled)} active rule(s) enforced by ConductGuard.",
|
|
802
|
+
"",
|
|
803
|
+
]
|
|
804
|
+
for r in enabled:
|
|
805
|
+
action = r.get("action", "warn").upper()
|
|
806
|
+
rule_id = r.get("rule_id", "")
|
|
807
|
+
desc = r.get("description") or r.get("message") or ""
|
|
808
|
+
lines.append(f"# [{action}] {rule_id}" + (f" — {desc}" if desc else ""))
|
|
809
|
+
pattern = r.get("pattern")
|
|
810
|
+
if pattern:
|
|
811
|
+
lines.append(f"# pattern: {pattern}")
|
|
812
|
+
lines += [
|
|
813
|
+
"",
|
|
814
|
+
"## General",
|
|
815
|
+
"# Never include secrets, API keys, or credentials in prompts.",
|
|
816
|
+
"# PII (emails, SSNs, phone numbers) is redacted by Conduct before reaching any model.",
|
|
817
|
+
"# Conduct AI governance is active — all tool calls are audited.",
|
|
818
|
+
"# Independent of Cursor's ownership — policies enforced by your team, not the IDE vendor.",
|
|
819
|
+
]
|
|
820
|
+
out = Path(".cursorrules")
|
|
821
|
+
out.write_text("\n".join(lines) + "\n")
|
|
822
|
+
print(f" {GREEN}.cursorrules written:{RESET} {len(enabled)} rule(s) → {out.resolve()}")
|
|
823
|
+
|
|
824
|
+
|
|
773
825
|
def _ensure_booster(root: Path) -> None:
|
|
774
826
|
"""Auto-init and background-index booster if installed but not yet set up."""
|
|
775
827
|
import shutil
|
|
@@ -878,6 +930,8 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
|
|
|
878
930
|
"saved_tokens": raw.get("saved_tokens", 0),
|
|
879
931
|
"savings_pct": raw.get("savings_pct", 0.0),
|
|
880
932
|
"total_reads": raw.get("total_reads", 0),
|
|
933
|
+
"crusher": raw.get("crusher", {}),
|
|
934
|
+
"cache_align": raw.get("cache_align", {}),
|
|
881
935
|
}
|
|
882
936
|
except Exception:
|
|
883
937
|
pass
|
|
@@ -929,6 +983,14 @@ def _report_savings(cfg: dict, base_url: str, api_key: str) -> None:
|
|
|
929
983
|
except Exception:
|
|
930
984
|
pass # Never fail sync because savings POST failed
|
|
931
985
|
|
|
986
|
+
# Push booster symbol index to team workspace (best-effort)
|
|
987
|
+
try:
|
|
988
|
+
r = subprocess.run(["booster", "index-push"], capture_output=True, text=True, timeout=30)
|
|
989
|
+
if r.returncode == 0 and r.stdout.strip():
|
|
990
|
+
print(f" {GREEN}Booster index:{RESET} {r.stdout.strip()}")
|
|
991
|
+
except Exception:
|
|
992
|
+
pass
|
|
993
|
+
|
|
932
994
|
|
|
933
995
|
def cmd_guard_status(args):
|
|
934
996
|
cfg = _require_guard_config()
|
|
@@ -1134,7 +1196,8 @@ def register_guard_parser(sub):
|
|
|
1134
1196
|
guard_sub = guard_p.add_subparsers(dest="guard_command")
|
|
1135
1197
|
|
|
1136
1198
|
# conduct guard sync
|
|
1137
|
-
guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
|
|
1199
|
+
sync_p = guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
|
|
1200
|
+
sync_p.add_argument("--cursor", action="store_true", help="Write active Guard policies to .cursorrules")
|
|
1138
1201
|
|
|
1139
1202
|
# conduct guard status
|
|
1140
1203
|
guard_sub.add_parser("status", help="Show today's spend and violations")
|
|
@@ -1154,6 +1217,9 @@ def register_guard_parser(sub):
|
|
|
1154
1217
|
# conduct guard booster-status
|
|
1155
1218
|
guard_sub.add_parser("booster-status", help="Verify Agent Booster intercept is active for this project")
|
|
1156
1219
|
|
|
1220
|
+
# conduct guard skip-setup
|
|
1221
|
+
guard_sub.add_parser("skip-setup", help="Suppress the Guard setup reminder (does not disable Guard)")
|
|
1222
|
+
|
|
1157
1223
|
return guard_p, guard_sub
|
|
1158
1224
|
|
|
1159
1225
|
|
|
@@ -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
|