conduct-cli 0.5.2__tar.gz → 0.5.4__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.2 → conduct_cli-0.5.4}/PKG-INFO +1 -1
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/pyproject.toml +1 -1
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/guard.py +112 -9
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/main.py +20 -9
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/README.md +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/setup.cfg +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/setup.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_stop_template.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.5.2 → conduct_cli-0.5.4}/tests/test_switch.py +0 -0
|
@@ -159,6 +159,61 @@ def _install_session_hooks() -> None:
|
|
|
159
159
|
|
|
160
160
|
# ── Guard config helpers ──────────────────────────────────────────────────────
|
|
161
161
|
|
|
162
|
+
_PERSONA_LABELS = {
|
|
163
|
+
"conservative": "Conservative — production-safe, default deny",
|
|
164
|
+
"standard": "Standard — engineering teams, balanced",
|
|
165
|
+
"developer": "Developer — local dev, audit-first",
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
|
|
170
|
+
"""Prompt for persona if none is set yet. Saves choice to guard config and API.
|
|
171
|
+
|
|
172
|
+
Returns the active persona name. Skips prompt silently if already set.
|
|
173
|
+
"""
|
|
174
|
+
cfg = _load_guard_config()
|
|
175
|
+
if cfg.get("persona"):
|
|
176
|
+
return cfg["persona"]
|
|
177
|
+
|
|
178
|
+
print(f"\n{BOLD}Choose a policy persona for your agents:{RESET}")
|
|
179
|
+
choices = list(_PERSONA_LABELS.keys())
|
|
180
|
+
for i, key in enumerate(choices, 1):
|
|
181
|
+
print(f" {i}. {_PERSONA_LABELS[key]}")
|
|
182
|
+
|
|
183
|
+
while True:
|
|
184
|
+
try:
|
|
185
|
+
raw = input(f"\nEnter 1-{len(choices)} [default: 2 — Standard]: ").strip()
|
|
186
|
+
if raw == "":
|
|
187
|
+
raw = "2"
|
|
188
|
+
idx = int(raw) - 1
|
|
189
|
+
if 0 <= idx < len(choices):
|
|
190
|
+
chosen = choices[idx]
|
|
191
|
+
break
|
|
192
|
+
print(f" Enter a number between 1 and {len(choices)}")
|
|
193
|
+
except (ValueError, EOFError):
|
|
194
|
+
chosen = "standard"
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
# Push to API
|
|
198
|
+
try:
|
|
199
|
+
_req(
|
|
200
|
+
"PATCH",
|
|
201
|
+
f"{base_url}/guard/config/persona",
|
|
202
|
+
body={"persona": chosen},
|
|
203
|
+
api_key=api_key,
|
|
204
|
+
)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass # non-fatal — local config still records the choice
|
|
207
|
+
|
|
208
|
+
# Persist locally so we skip the prompt on subsequent syncs
|
|
209
|
+
cfg = _load_guard_config()
|
|
210
|
+
cfg["persona"] = chosen
|
|
211
|
+
_save_guard_config(cfg)
|
|
212
|
+
|
|
213
|
+
print(f" {GREEN}Persona set:{RESET} {chosen.capitalize()}")
|
|
214
|
+
return chosen
|
|
215
|
+
|
|
216
|
+
|
|
162
217
|
def _load_guard_config() -> dict:
|
|
163
218
|
if CONFIG_PATH.exists():
|
|
164
219
|
return json.loads(CONFIG_PATH.read_text())
|
|
@@ -484,6 +539,9 @@ def cmd_guard_install(args):
|
|
|
484
539
|
except Exception:
|
|
485
540
|
pass
|
|
486
541
|
|
|
542
|
+
# Persona selection — prompt once, skip if already chosen
|
|
543
|
+
_ensure_persona(workspace_id, api_key, server)
|
|
544
|
+
|
|
487
545
|
# Persist guard config — include api_key so CLI commands can authenticate
|
|
488
546
|
import time as _time
|
|
489
547
|
_save_guard_config({
|
|
@@ -655,15 +713,19 @@ def _report_tools_to_server() -> None:
|
|
|
655
713
|
"hook_registered": False,
|
|
656
714
|
})
|
|
657
715
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
716
|
+
vscode_ext_dir = home / ".vscode" / "extensions"
|
|
717
|
+
copilot_installed = vscode_ext_dir.exists() and any(
|
|
718
|
+
p.name.startswith("github.copilot") for p in vscode_ext_dir.iterdir() if p.is_dir()
|
|
719
|
+
)
|
|
720
|
+
if copilot_installed:
|
|
721
|
+
vscode_candidates = [
|
|
722
|
+
home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
|
|
723
|
+
home / ".config" / "Code" / "User" / "settings.json",
|
|
724
|
+
home / ".vscode" / "settings.json",
|
|
725
|
+
]
|
|
726
|
+
vscode_settings = next((p for p in vscode_candidates if p.exists()), None)
|
|
665
727
|
try:
|
|
666
|
-
d = json.loads(vscode_settings.read_text())
|
|
728
|
+
d = json.loads(vscode_settings.read_text()) if vscode_settings else {}
|
|
667
729
|
mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
|
|
668
730
|
except Exception:
|
|
669
731
|
mcp_reg = False
|
|
@@ -718,6 +780,9 @@ def cmd_guard_sync(args):
|
|
|
718
780
|
api_key = cfg.get("api_key", "")
|
|
719
781
|
base_url = _api_url(cfg)
|
|
720
782
|
|
|
783
|
+
# Persona selection — prompt once, skip if already chosen
|
|
784
|
+
_ensure_persona(workspace_id, api_key, base_url)
|
|
785
|
+
|
|
721
786
|
print(f"Syncing policy…")
|
|
722
787
|
|
|
723
788
|
try:
|
|
@@ -733,6 +798,9 @@ def cmd_guard_sync(args):
|
|
|
733
798
|
rule_count = len(policy.get("rules", []))
|
|
734
799
|
print(f" {GREEN}Policy refreshed:{RESET} {rule_count} rule(s)")
|
|
735
800
|
|
|
801
|
+
if getattr(args, "cursor", False):
|
|
802
|
+
_write_cursorrules(policy)
|
|
803
|
+
|
|
736
804
|
# Re-check Security Loop install status
|
|
737
805
|
try:
|
|
738
806
|
sec = _req("GET", f"{base_url}/secure/installed?workspace_id={workspace_id}", api_key=api_key)
|
|
@@ -785,6 +853,40 @@ def cmd_guard_sync(args):
|
|
|
785
853
|
print(f"\n {CYAN}{mcp_url}{RESET}\n")
|
|
786
854
|
|
|
787
855
|
|
|
856
|
+
def _write_cursorrules(policy: dict) -> None:
|
|
857
|
+
"""Write active Guard policies into .cursorrules in the current directory."""
|
|
858
|
+
rules = policy.get("rules", [])
|
|
859
|
+
enabled = [r for r in rules if r.get("enabled", True)]
|
|
860
|
+
lines = [
|
|
861
|
+
"# .cursorrules — generated by Conduct AI Guard",
|
|
862
|
+
"# Run `conduct guard sync --cursor` to refresh.",
|
|
863
|
+
"# Do not edit manually — changes will be overwritten.",
|
|
864
|
+
"",
|
|
865
|
+
"## Conduct Guard Policies",
|
|
866
|
+
f"# {len(enabled)} active rule(s) enforced by ConductGuard.",
|
|
867
|
+
"",
|
|
868
|
+
]
|
|
869
|
+
for r in enabled:
|
|
870
|
+
action = r.get("action", "warn").upper()
|
|
871
|
+
rule_id = r.get("rule_id", "")
|
|
872
|
+
desc = r.get("description") or r.get("message") or ""
|
|
873
|
+
lines.append(f"# [{action}] {rule_id}" + (f" — {desc}" if desc else ""))
|
|
874
|
+
pattern = r.get("pattern")
|
|
875
|
+
if pattern:
|
|
876
|
+
lines.append(f"# pattern: {pattern}")
|
|
877
|
+
lines += [
|
|
878
|
+
"",
|
|
879
|
+
"## General",
|
|
880
|
+
"# Never include secrets, API keys, or credentials in prompts.",
|
|
881
|
+
"# PII (emails, SSNs, phone numbers) is redacted by Conduct before reaching any model.",
|
|
882
|
+
"# Conduct AI governance is active — all tool calls are audited.",
|
|
883
|
+
"# Independent of Cursor's ownership — policies enforced by your team, not the IDE vendor.",
|
|
884
|
+
]
|
|
885
|
+
out = Path(".cursorrules")
|
|
886
|
+
out.write_text("\n".join(lines) + "\n")
|
|
887
|
+
print(f" {GREEN}.cursorrules written:{RESET} {len(enabled)} rule(s) → {out.resolve()}")
|
|
888
|
+
|
|
889
|
+
|
|
788
890
|
def _ensure_booster(root: Path) -> None:
|
|
789
891
|
"""Auto-init and background-index booster if installed but not yet set up."""
|
|
790
892
|
import shutil
|
|
@@ -1159,7 +1261,8 @@ def register_guard_parser(sub):
|
|
|
1159
1261
|
guard_sub = guard_p.add_subparsers(dest="guard_command")
|
|
1160
1262
|
|
|
1161
1263
|
# conduct guard sync
|
|
1162
|
-
guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
|
|
1264
|
+
sync_p = guard_sub.add_parser("sync", help="Refresh policy and re-scan for AI tools")
|
|
1265
|
+
sync_p.add_argument("--cursor", action="store_true", help="Write active Guard policies to .cursorrules")
|
|
1163
1266
|
|
|
1164
1267
|
# conduct guard status
|
|
1165
1268
|
guard_sub.add_parser("status", help="Show today's spend and violations")
|
|
@@ -302,16 +302,21 @@ def _detect_ai_tools() -> list:
|
|
|
302
302
|
"hook_registered": False, # Windsurf uses MCP only
|
|
303
303
|
})
|
|
304
304
|
|
|
305
|
-
# VS Code (Copilot)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
305
|
+
# VS Code (Copilot) — only report if Copilot extension is actually installed
|
|
306
|
+
vscode_ext_dir = home / ".vscode" / "extensions"
|
|
307
|
+
copilot_installed = vscode_ext_dir.exists() and any(
|
|
308
|
+
p.name.startswith("github.copilot") for p in vscode_ext_dir.iterdir()
|
|
309
|
+
if p.is_dir()
|
|
310
|
+
)
|
|
311
|
+
if copilot_installed:
|
|
312
|
+
vscode_settings_candidates = [
|
|
313
|
+
home / "Library" / "Application Support" / "Code" / "User" / "settings.json",
|
|
314
|
+
home / ".config" / "Code" / "User" / "settings.json",
|
|
315
|
+
home / ".vscode" / "settings.json",
|
|
316
|
+
]
|
|
317
|
+
vscode_settings = next((p for p in vscode_settings_candidates if p.exists()), None)
|
|
313
318
|
try:
|
|
314
|
-
d = json.loads(vscode_settings.read_text())
|
|
319
|
+
d = json.loads(vscode_settings.read_text()) if vscode_settings else {}
|
|
315
320
|
mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
|
|
316
321
|
except Exception:
|
|
317
322
|
mcp_reg = False
|
|
@@ -409,6 +414,12 @@ def cmd_mcp_install(args):
|
|
|
409
414
|
if uncovered:
|
|
410
415
|
print(f"{YELLOW} Not covered: {', '.join(uncovered)} — run: conduct mcp install{RESET}")
|
|
411
416
|
|
|
417
|
+
# Push updated coverage to Guard so the dashboard reflects the new state immediately
|
|
418
|
+
try:
|
|
419
|
+
_report_tool_coverage()
|
|
420
|
+
except Exception:
|
|
421
|
+
pass
|
|
422
|
+
|
|
412
423
|
|
|
413
424
|
def cmd_login(args):
|
|
414
425
|
server = args.server
|
|
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
|
|
File without changes
|
|
File without changes
|