conduct-cli 0.7.46__tar.gz → 0.8.0__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.
Files changed (48) hide show
  1. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/PKG-INFO +1 -1
  2. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/pyproject.toml +1 -1
  3. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/api.py +2 -4
  4. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/guard.py +113 -96
  5. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/base.py +3 -3
  6. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/posttooluse.py +2 -2
  7. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/pretooluse.py +2 -2
  8. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/session_report_push.py +3 -3
  9. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/session_start.py +2 -2
  10. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/stop.py +2 -2
  11. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/main.py +70 -77
  12. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/mcp_server.py +1 -1
  13. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/memory.py +4 -4
  14. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  15. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/README.md +0 -0
  16. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/setup.cfg +0 -0
  17. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/setup.py +0 -0
  18. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/__init__.py +0 -0
  19. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/guardmcp.py +0 -0
  20. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/__init__.py +0 -0
  21. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/precompact.py +0 -0
  22. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/hooks/session_parser.py +0 -0
  23. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/log_util.py +0 -0
  24. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/paxel.py +0 -0
  25. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli/tool_groups.py +0 -0
  26. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  27. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  28. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  29. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/requires.txt +0 -0
  30. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/src/conduct_cli.egg-info/top_level.txt +0 -0
  31. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_advisory_and_hook.py +0 -0
  32. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_api_client.py +0 -0
  33. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_bash_operator_signature.py +0 -0
  34. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_blast_radius.py +0 -0
  35. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_cmd_run.py +0 -0
  36. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_command_word_matcher.py +0 -0
  37. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_drain_daemon_health.py +0 -0
  38. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_guard_policy.py +0 -0
  39. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_guard_savings.py +0 -0
  40. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_hook_dispatch.py +0 -0
  41. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_instruction_files.py +0 -0
  42. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_journal_drain.py +0 -0
  43. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_log_util.py +0 -0
  44. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_proxy_coverage.py +0 -0
  45. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_proxy_env.py +0 -0
  46. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_signed_policy.py +0 -0
  47. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_switch.py +0 -0
  48. {conduct_cli-0.7.46 → conduct_cli-0.8.0}/tests/test_token_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.7.46
3
+ Version: 0.8.0
4
4
  Summary: CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.7.46"
7
+ version = "0.8.0"
8
8
  description = "CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -22,11 +22,9 @@ def _friendly(code: int, detail: str) -> str:
22
22
  return detail
23
23
 
24
24
 
25
- def headers(workspace_id: str, token=None, content_type="application/json", api_key=None) -> dict:
25
+ def headers(workspace_id: str, token=None, content_type="application/json") -> dict:
26
26
  h = {"Content-Type": content_type, "X-Workspace-Id": workspace_id}
27
- if api_key:
28
- h["X-Api-Key"] = api_key
29
- elif token:
27
+ if token:
30
28
  h["Authorization"] = f"Bearer {token}"
31
29
  return h
32
30
 
@@ -276,11 +276,12 @@ _PERSONA_LABELS = {
276
276
  }
277
277
 
278
278
 
279
- def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
279
+ def _ensure_persona(workspace_id: str, base_url: str) -> str:
280
280
  """Prompt for persona if none is set yet. Saves choice to guard config and API.
281
281
 
282
282
  Returns the active persona name. Skips prompt silently if already set.
283
283
  """
284
+ agent_token = _default_agent_token() or ""
284
285
  cfg = _load_guard_config()
285
286
  if cfg.get("persona"):
286
287
  return cfg["persona"]
@@ -310,7 +311,7 @@ def _ensure_persona(workspace_id: str, api_key: str, base_url: str) -> str:
310
311
  "PATCH",
311
312
  f"{base_url}/guard/config/persona",
312
313
  body={"persona": chosen},
313
- api_key=api_key,
314
+ token=agent_token or None,
314
315
  )
315
316
  except Exception:
316
317
  pass # non-fatal — local config still records the choice
@@ -352,7 +353,7 @@ def _require_guard_config() -> dict:
352
353
  if not cfg or not ws:
353
354
  print(f"{RED}Guard not connected. Run: conduct login{RESET}", file=sys.stderr)
354
355
  sys.exit(0)
355
- if not cfg.get("agent_token") and not cfg.get("api_key"):
356
+ if not cfg.get("agent_token"):
356
357
  print(f"{RED}Guard config is missing credentials. Run: conduct login{RESET}", file=sys.stderr)
357
358
  sys.exit(0)
358
359
  return cfg
@@ -474,40 +475,67 @@ def _patch_claude_desktop_proxy(api_url: str, agent_token: str) -> None:
474
475
 
475
476
 
476
477
  def _patch_copilot_mcp(agent_token: str, api_url: str) -> None:
477
- """Keep ~/.copilot/mcp-config.json in sync with current agent token.
478
-
479
- Copilot's mcp-config.json uses SSE + Bearer — it can't run a local stdio binary
480
- the way Claude Code / Cursor can, so we write the SSE entry with a fresh token.
481
- """
482
- cfg_path = Path.home() / ".copilot" / "mcp-config.json"
483
- if not cfg_path.exists():
484
- return
485
- try:
486
- cfg = json.loads(cfg_path.read_text())
487
- except (json.JSONDecodeError, OSError):
488
- cfg = {}
489
- mcp = cfg.setdefault("mcpServers", {})
490
- entry = {
478
+ """Keep ~/.copilot/mcp-config.json and any .mcp.json in cwd in sync with current agent token."""
479
+ import shutil
480
+ sse_entry = {
491
481
  "type": "sse",
492
482
  "url": f"{api_url}/guard/mcp",
493
483
  "headers": {"Authorization": f"Bearer {agent_token}"},
494
484
  "tools": ["**"],
495
485
  }
496
- if mcp.get("conduct-guard") == entry:
497
- print(f" {GRAY}conduct-guard MCP already registered in GitHub Copilot{RESET}")
498
- return
499
- mcp["conduct-guard"] = entry
500
- cfg_path.write_text(json.dumps(cfg, indent=2))
501
- print(f" {GREEN}conduct-guard MCP registered in GitHub Copilot (SSE){RESET}")
486
+ booster_entry = {"command": "booster", "args": ["serve"]} if shutil.which("booster") else None
487
+
488
+ # ~/.copilot/mcp-config.json (global Copilot config)
489
+ global_path = Path.home() / ".copilot" / "mcp-config.json"
490
+ if global_path.exists():
491
+ _write_mcp_file(global_path, "conduct-guard", sse_entry, booster_entry, "GitHub Copilot (global)")
492
+
493
+ # .mcp.json in cwd (project-level, picked up by VS Code Copilot)
494
+ local_path = Path.cwd() / ".mcp.json"
495
+ if local_path.exists():
496
+ _write_mcp_file(local_path, "conduct-guard", sse_entry, booster_entry, "GitHub Copilot (.mcp.json)")
497
+
498
+
499
+ def _write_mcp_file(
500
+ cfg_path: Path,
501
+ guard_key: str,
502
+ sse_entry: dict,
503
+ booster_entry: dict | None,
504
+ label: str,
505
+ ) -> None:
506
+ try:
507
+ cfg = json.loads(cfg_path.read_text())
508
+ except (json.JSONDecodeError, OSError):
509
+ cfg = {}
510
+ mcp = cfg.setdefault("mcpServers", {})
511
+ changed = False
512
+ if mcp.get(guard_key) != sse_entry:
513
+ mcp[guard_key] = sse_entry
514
+ changed = True
515
+ print(f" {GREEN}conduct-guard MCP registered in {label}{RESET}")
516
+ else:
517
+ print(f" {GRAY}conduct-guard MCP already registered in {label}{RESET}")
518
+ if booster_entry and mcp.get("agent-booster") != booster_entry:
519
+ mcp["agent-booster"] = booster_entry
520
+ changed = True
521
+ if changed:
522
+ cfg_path.write_text(json.dumps(cfg, indent=2))
502
523
 
503
524
 
504
525
  _GUARD_RULES_TEXT = (
526
+ "## ConductGuard — AI Governance Policy\n\n"
527
+ "A ConductGuard MCP server is connected to this session (server name: `conduct-guard`).\n"
528
+ "All Guard policies are enforced through it. You MUST follow these rules:\n\n"
505
529
  "Before executing any shell command, file write, network request, API call, or code change:\n"
506
- "- Call guard_check (conductguard MCP tool) with the action and parameters.\n"
507
- "- If BLOCKED: stop immediately and explain the policy rule to the user.\n"
508
- "- If WARNING: proceed but surface the warning. If ALLOWED: proceed normally.\n"
509
- "- Never write credentials, API keys, or secrets to files or output.\n"
510
- "- Never send PII (emails, names, payment data) to external endpoints."
530
+ "1. Call the `guard_check` tool from the `conduct-guard` MCP server with the action and parameters.\n"
531
+ "2. If the response is BLOCKED: stop immediately and explain the policy rule to the user. Do not proceed.\n"
532
+ "3. If the response is WARNING: proceed but surface the warning message to the user.\n"
533
+ "4. If the response is ALLOWED: proceed normally.\n\n"
534
+ "Always-on rules (no guard_check needed, enforce unconditionally):\n"
535
+ "- Never write credentials, API keys, tokens, or secrets to any file or output.\n"
536
+ "- Never send PII (emails, names, payment data) to external endpoints.\n\n"
537
+ "Use `guard_status` (conduct-guard MCP) to show the user their current policy and team info.\n"
538
+ "Use `guard_activity` at the start of every session with a one-line summary of what you are doing."
511
539
  )
512
540
 
513
541
 
@@ -562,22 +590,10 @@ def _patch_tool_instruction_files(agent_token: str, api_url: str, dry_run: bool
562
590
  MARKER_END = "<!-- /ConductGuard -->"
563
591
  GUARD_RULES = _GUARD_RULES_TEXT
564
592
 
565
- # Pull team instructions from API (non-fatal)
566
- # Only write if admin has explicitly published (version != "v0")
567
- team_content = ""
568
- try:
569
- resp = _req("GET", f"{api_url}/team-os/instructions", token=agent_token)
570
- if (resp.get("version") or "v0") != "v0":
571
- team_content = (resp.get("content") or "").strip()
572
- except Exception:
573
- pass
574
-
575
- parts = [MARKER_START, "## ConductGuard Policy"]
576
- if team_content:
577
- parts.append(team_content)
578
- parts.append(GUARD_RULES)
579
- parts.append(MARKER_END)
580
- guard_block = "\n".join(parts)
593
+ # Guard-only for now — team-os instructions are hidden pending redesign.
594
+ # This block just ensures every AI tool knows to invoke Guard via hooks
595
+ # or the conduct-guard MCP before taking action.
596
+ guard_block = "\n".join([MARKER_START, "## ConductGuard Policy", GUARD_RULES, MARKER_END])
581
597
 
582
598
  repo_root: Path | None = None
583
599
  for parent in [Path.cwd(), *Path.cwd().parents]:
@@ -731,12 +747,26 @@ def _install_codex_hook(hook_path: Path) -> None:
731
747
 
732
748
  # ── HTTP helpers (no third-party deps — mirrors api.py style) ─────────────────
733
749
 
734
- def _req(method: str, url: str, body=None, token: str = None, api_key: str = None, timeout: int = 20) -> dict:
750
+ def _default_agent_token() -> str | None:
751
+ """Read the agent_token minted by `conduct login` from ~/.conduct/config.json.
752
+ This is the CLI's single canonical credential — every call to the central
753
+ API should carry it via Authorization: Bearer."""
754
+ try:
755
+ cfg_path = Path.home() / ".conduct" / "config.json"
756
+ if cfg_path.exists():
757
+ return json.loads(cfg_path.read_text()).get("agent_token") or None
758
+ except Exception:
759
+ pass
760
+ return None
761
+
762
+
763
+ def _req(method: str, url: str, body=None, token: str = None, timeout: int = 20) -> dict:
764
+ # If no explicit token passed, fall back to the login-minted agent_token.
765
+ if not token:
766
+ token = _default_agent_token()
735
767
  headers = {"Content-Type": "application/json"}
736
768
  if token:
737
769
  headers["Authorization"] = f"Bearer {token}"
738
- if api_key:
739
- headers["X-Api-Key"] = api_key
740
770
  data = json.dumps(body).encode() if body is not None else None
741
771
  r = urllib.request.Request(url, data=data, headers=headers, method=method)
742
772
  try:
@@ -872,10 +902,10 @@ def _install_claude_hook(hook_path: Path) -> None:
872
902
 
873
903
  def cmd_guard_install(args):
874
904
  """Called automatically from `conduct login`. Sets up Guard: downloads policies, installs hook + MCP."""
875
- api_key = getattr(args, "api_key", None)
876
- server = (getattr(args, "server", None) or "https://api.conductai.ai").rstrip("/")
905
+ agent_token = getattr(args, "agent_token", None)
906
+ server = (getattr(args, "server", None) or "https://api.conductai.ai").rstrip("/")
877
907
 
878
- # Load workspace_id from ~/.conduct/config.json
908
+ # Load workspace_id (and fallback agent_token) from ~/.conduct/config.json
879
909
  conduct_cfg_path = Path.home() / ".conduct" / "config.json"
880
910
  conduct_cfg: dict = {}
881
911
  if conduct_cfg_path.exists():
@@ -885,7 +915,9 @@ def cmd_guard_install(args):
885
915
  pass
886
916
 
887
917
  workspace_id = conduct_cfg.get("workspace")
888
- if not workspace_id or not api_key:
918
+ if not agent_token:
919
+ agent_token = conduct_cfg.get("agent_token")
920
+ if not workspace_id or not agent_token:
889
921
  return # nothing to do
890
922
 
891
923
  print(f" Setting up Guard…")
@@ -893,16 +925,16 @@ def cmd_guard_install(args):
893
925
  result = _req(
894
926
  "GET",
895
927
  f"{server}/guard/config/installed?workspace_id={workspace_id}",
896
- api_key=api_key,
928
+ token=agent_token or None,
897
929
  )
898
930
 
899
931
  if not result.get("installed"):
900
932
  print(f" {GRAY}Guard not installed for this workspace — skipping{RESET}")
901
933
  return
902
934
 
903
- agent_token = result.get("agent_token") or ""
904
- user_email = result.get("user_email") or ""
905
- clerk_user_id = result.get("clerk_user_id") or ""
935
+ resp_agent_token = result.get("agent_token") or agent_token
936
+ user_email = result.get("user_email") or ""
937
+ clerk_user_id = result.get("clerk_user_id") or ""
906
938
 
907
939
  # Persona selection — prompt once, skip if already chosen
908
940
 
@@ -910,10 +942,9 @@ def cmd_guard_install(args):
910
942
  import time as _time
911
943
  _save_guard_config({
912
944
  "workspace_id": workspace_id,
913
- "agent_token": agent_token,
945
+ "agent_token": resp_agent_token,
914
946
  "user_email": user_email,
915
947
  "clerk_user_id": clerk_user_id,
916
- "api_key": api_key,
917
948
  "api_url": server,
918
949
  "last_synced_at": _time.time(),
919
950
  })
@@ -1084,7 +1115,7 @@ def _detect_ai_tools() -> list[dict]:
1084
1115
  def _check_json_mcp(path: Path) -> bool:
1085
1116
  try:
1086
1117
  d = json.loads(path.read_text()) if path.exists() else {}
1087
- return "conduct" in d.get("mcpServers", {})
1118
+ return any("conduct" in k for k in d.get("mcpServers", {}))
1088
1119
  except Exception:
1089
1120
  return False
1090
1121
 
@@ -1191,7 +1222,7 @@ def _detect_ai_tools() -> list[dict]:
1191
1222
  vscode_settings = next((p for p in vscode_candidates if p.exists()), None)
1192
1223
  try:
1193
1224
  d = json.loads(vscode_settings.read_text()) if vscode_settings else {}
1194
- mcp_reg = "conduct" in d.get("mcp", {}).get("servers", {})
1225
+ mcp_reg = any("conduct" in k for k in d.get("mcp", {}).get("servers", {}))
1195
1226
  except Exception:
1196
1227
  mcp_reg = False
1197
1228
  # Also check ~/.copilot/mcp-config.json (SSE entry written by guard sync)
@@ -1223,24 +1254,20 @@ def _report_tools_to_server() -> None:
1223
1254
  base_url = _api_url(cfg)
1224
1255
  email = cfg.get("user_email", "")
1225
1256
  token = cfg.get("agent_token", "")
1226
- api_key = cfg.get("api_key", "")
1227
-
1228
1257
  if not email:
1229
1258
  return
1230
-
1231
- # Also pull conduct API key for X-Api-Key auth (member_token is not accepted by this endpoint)
1232
1259
  conduct_cfg_path = Path.home() / ".conduct" / "config.json"
1233
- conduct_api_key = ""
1260
+ conduct_agent_token = ""
1234
1261
  if conduct_cfg_path.exists():
1235
1262
  try:
1236
- conduct_api_key = json.loads(conduct_cfg_path.read_text()).get("api_key", "")
1263
+ conduct_agent_token = json.loads(conduct_cfg_path.read_text()).get("agent_token", "")
1237
1264
  except Exception:
1238
1265
  pass
1239
1266
 
1240
1267
  payload = json.dumps({"email": email, "tools": tools}).encode()
1241
1268
  headers = {"Content-Type": "application/json"}
1242
- if conduct_api_key and conduct_api_key.startswith("cond_live_"):
1243
- headers["X-Api-Key"] = conduct_api_key
1269
+ if conduct_agent_token and conduct_agent_token.startswith("cond_live_"):
1270
+ headers["Authorization"] = f"Bearer {conduct_agent_token}"
1244
1271
  elif token:
1245
1272
  headers["Authorization"] = f"Bearer {token}"
1246
1273
  req = urllib.request.Request(
@@ -1445,14 +1472,6 @@ def cmd_guard_sync(args):
1445
1472
  except Exception as e:
1446
1473
  print(f" {YELLOW}Warning: could not refresh token ({e}) — using cached value{RESET}")
1447
1474
 
1448
- # Fetch team instructions version so session-start hook can detect staleness
1449
- try:
1450
- ver_resp = _req("GET", f"{base_url}/team-os/instructions/version", token=_token)
1451
- if ver_resp.get("version"):
1452
- _save_guard_config({"instructions_version": ver_resp["version"]})
1453
- except Exception:
1454
- pass # non-fatal
1455
-
1456
1475
  # Write LLM proxy env vars so any AI tool (Claude Code, Cursor, Codex, …)
1457
1476
  # routes through Conduct Guard. Customer-overridable via --proxy-url or
1458
1477
  # CONDUCT_PROXY_URL env var; otherwise fetched from server (workspace_config).
@@ -1848,7 +1867,7 @@ def _ensure_booster(root: Path) -> None:
1848
1867
  pass
1849
1868
 
1850
1869
 
1851
- def _report_savings(cfg: dict, base_url: str, api_key: str = "") -> None:
1870
+ def _report_savings(cfg: dict, base_url: str, agent_token: str = "") -> None:
1852
1871
  import subprocess
1853
1872
 
1854
1873
  rtk_data = {}
@@ -1909,12 +1928,10 @@ def _report_savings(cfg: dict, base_url: str, api_key: str = "") -> None:
1909
1928
  }
1910
1929
 
1911
1930
  try:
1912
- agent_token_evt = cfg.get("agent_token", "")
1931
+ agent_token_evt = cfg.get("agent_token", "") or agent_token
1913
1932
  headers = {"Content-Type": "application/json"}
1914
1933
  if agent_token_evt:
1915
1934
  headers["Authorization"] = f"Bearer {agent_token_evt}"
1916
- elif api_key:
1917
- headers["X-Api-Key"] = api_key
1918
1935
  data = json.dumps(payload).encode()
1919
1936
  req = urllib.request.Request(
1920
1937
  f"{base_url}/guard/savings",
@@ -1943,13 +1960,13 @@ def cmd_guard_status(args):
1943
1960
  cfg = _require_guard_config()
1944
1961
  workspace_id = cfg.get("workspace_id")
1945
1962
  user_email = cfg.get("user_email", "")
1946
- api_key = cfg.get("api_key", "")
1963
+ agent_token = cfg.get("agent_token", "")
1947
1964
  base_url = _api_url(cfg)
1948
1965
 
1949
1966
  # Auto-refresh user_email + clerk_user_id into config if missing
1950
- if (not user_email or not cfg.get("clerk_user_id")) and api_key:
1967
+ if (not user_email or not cfg.get("clerk_user_id")) and agent_token:
1951
1968
  try:
1952
- installed = _req("GET", f"{base_url}/guard/config/installed", api_key=api_key)
1969
+ installed = _req("GET", f"{base_url}/guard/config/installed", token=agent_token or None)
1953
1970
  fetched_email = installed.get("user_email") or ""
1954
1971
  fetched_clerk = installed.get("clerk_user_id") or ""
1955
1972
  if fetched_email:
@@ -1973,7 +1990,7 @@ def cmd_guard_status(args):
1973
1990
  spend = _req(
1974
1991
  "GET",
1975
1992
  f"{base_url}/guard/spend?workspace_id={workspace_id}",
1976
- api_key=api_key,
1993
+ token=agent_token or None,
1977
1994
  )
1978
1995
  except SystemExit:
1979
1996
  pass
@@ -1993,7 +2010,7 @@ def cmd_guard_status(args):
1993
2010
  f"&since={today_iso}"
1994
2011
  f"&limit=20"
1995
2012
  ),
1996
- api_key=api_key,
2013
+ token=agent_token or None,
1997
2014
  )
1998
2015
  if not isinstance(events, list):
1999
2016
  events = events.get("events", [])
@@ -2074,14 +2091,14 @@ def cmd_guard_status(args):
2074
2091
  def cmd_guard_savings(args):
2075
2092
  cfg = _require_guard_config()
2076
2093
  workspace_id = cfg.get("workspace_id")
2077
- api_key = cfg.get("api_key", "")
2094
+ agent_token = cfg.get("agent_token", "")
2078
2095
  base_url = _api_url(cfg)
2079
2096
 
2080
2097
  try:
2081
2098
  data = _req(
2082
2099
  "GET",
2083
2100
  f"{base_url}/guard/savings/team-summary?workspace_id={workspace_id}",
2084
- api_key=api_key,
2101
+ token=agent_token or None,
2085
2102
  )
2086
2103
  except Exception:
2087
2104
  print(f"{RED}Failed to fetch team savings.{RESET}")
@@ -2141,7 +2158,7 @@ def cmd_guard_audit(args):
2141
2158
  cfg = _require_guard_config()
2142
2159
  workspace_id = cfg.get("workspace_id")
2143
2160
  user_email = cfg.get("user_email", "")
2144
- api_key = cfg.get("api_key", "")
2161
+ agent_token = cfg.get("agent_token", "")
2145
2162
  base_url = _api_url(cfg)
2146
2163
 
2147
2164
  since_str = getattr(args, "since", None) or "24h"
@@ -2156,7 +2173,7 @@ def cmd_guard_audit(args):
2156
2173
  f"&since={since_iso}"
2157
2174
  f"&limit=50"
2158
2175
  ),
2159
- api_key=api_key,
2176
+ token=agent_token or None,
2160
2177
  )
2161
2178
  events = events_resp if isinstance(events_resp, list) else events_resp.get("events", [])
2162
2179
 
@@ -2321,7 +2338,7 @@ def _discover_config_agents() -> list[tuple]:
2321
2338
  return "conduct" in text and "mcp_servers" in text
2322
2339
  import json as _j
2323
2340
  d = _j.loads(text)
2324
- return "conduct" in d.get("mcpServers", {})
2341
+ return any("conduct" in k for k in d.get("mcpServers", {}))
2325
2342
  except Exception:
2326
2343
  return False
2327
2344
 
@@ -2347,6 +2364,7 @@ def _discover_config_agents() -> list[tuple]:
2347
2364
  ("codex", home / ".codex", home / ".codex" / "config.toml", "mcp"),
2348
2365
  ("cursor", home / ".cursor", home / ".cursor" / "mcp.json", "mcp"),
2349
2366
  ("windsurf", home / ".codeium" / "windsurf", home / ".codeium" / "windsurf" / "mcp_config.json", "mcp"),
2367
+ ("copilot", home / ".copilot", home / ".copilot" / "mcp-config.json", "mcp"),
2350
2368
  ]
2351
2369
  for name, check_dir, config_path, _ in TOOLS:
2352
2370
  if check_dir.exists():
@@ -2465,7 +2483,7 @@ def _watch_loop():
2465
2483
  c = _cfg()
2466
2484
  api_url = _api_url(c)
2467
2485
  token = c.get("agent_token", "")
2468
- api_key = c.get("api_key", "")
2486
+ agent_token = c.get("agent_token", "")
2469
2487
  agents = []
2470
2488
  for name, config_path, mcp_key in _discover_config_agents():
2471
2489
  agents.append({
@@ -2485,7 +2503,7 @@ def _watch_loop():
2485
2503
  pass
2486
2504
  _req("POST", f"{api_url}/guard/discover/scan",
2487
2505
  body={"triggered_by": "watch", "agents": agents},
2488
- token=token, api_key=api_key)
2506
+ token=token or agent_token)
2489
2507
  except Exception:
2490
2508
  pass
2491
2509
  time.sleep(_WATCH_INTERVAL)
@@ -2641,7 +2659,6 @@ def cmd_guard_discover(args):
2641
2659
  cfg = _load_guard_config()
2642
2660
  api_url = _api_url(cfg)
2643
2661
  token = cfg.get("agent_token", "")
2644
- api_key = cfg.get("api_key", "")
2645
2662
  hdrs = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
2646
2663
 
2647
2664
  # Config file scan — detect AI tools from known config dirs
@@ -2701,7 +2718,7 @@ def cmd_guard_discover(args):
2701
2718
  "proxy_routed": t.get("proxy_routed", False),
2702
2719
  })
2703
2720
  payload = {"triggered_by": "cli", "agents": agents_payload}
2704
- _req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token, api_key=api_key)
2721
+ _req("POST", f"{api_url}/guard/discover/scan", body=payload, token=token)
2705
2722
  except SystemExit:
2706
2723
  pass # 401/network errors — local output still useful
2707
2724
 
@@ -2915,14 +2932,14 @@ def cmd_verify(args) -> None:
2915
2932
 
2916
2933
  cfg = _require_guard_config()
2917
2934
  workspace_id = cfg.get("workspace_id")
2918
- api_key = cfg.get("api_key", "")
2935
+ agent_token = cfg.get("agent_token", "")
2919
2936
  base_url = _api_url(cfg)
2920
2937
 
2921
2938
  # ── fetch evidence from API ───────────────────────────────────────────────
2922
2939
  ev = _req(
2923
2940
  "GET",
2924
2941
  f"{base_url}/guard/verify/evidence?workspace_id={workspace_id}",
2925
- api_key=api_key,
2942
+ token=agent_token or None,
2926
2943
  )
2927
2944
 
2928
2945
  grade = ev.get("grade", "F")
@@ -2939,7 +2956,7 @@ def cmd_verify(args) -> None:
2939
2956
  run_result = _req(
2940
2957
  "POST",
2941
2958
  f"{base_url}/guard/verify/run?workspace_id={workspace_id}",
2942
- api_key=api_key,
2959
+ token=agent_token or None,
2943
2960
  )
2944
2961
  if fmt == "json":
2945
2962
  print(_json.dumps(run_result, indent=2))
@@ -148,14 +148,14 @@ def _refresh_policy_from_api() -> None:
148
148
  try:
149
149
  cfg = load_config()
150
150
  workspace_id = cfg.get("workspace_id")
151
- api_key = cfg.get("api_key", "")
151
+ agent_token = cfg.get("agent_token", "")
152
152
  api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
153
- if not api_key:
153
+ if not agent_token:
154
154
  return
155
155
  url = f"{api_url}/guard/policies/sync"
156
156
  if workspace_id:
157
157
  url += f"?workspace_id={workspace_id}"
158
- req = urllib.request.Request(url, headers={"X-Api-Key": api_key})
158
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {agent_token}"})
159
159
  with urllib.request.urlopen(req, timeout=8) as resp:
160
160
  data = resp.read()
161
161
  POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -92,7 +92,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_inpu
92
92
  if not cfg.get("security_emit_enabled", False):
93
93
  return
94
94
  workspace_id = cfg.get("workspace_id")
95
- api_key = cfg.get("api_key", "")
95
+ agent_token = cfg.get("agent_token", "")
96
96
  api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
97
97
  if not workspace_id:
98
98
  return
@@ -138,7 +138,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_inpu
138
138
  "try:\n"
139
139
  f" req = urllib.request.Request(\"{api_url}/security-findings?workspace_id={workspace_id}\","
140
140
  f" data={repr(payload.encode())}, headers={{\"Content-Type\": \"application/json\","
141
- f" \"X-Api-Key\": \"{api_key}\"}}, method=\"POST\")\n"
141
+ f" \"Authorization\": \"Bearer {agent_token}\"}}, method=\"POST\")\n"
142
142
  " urllib.request.urlopen(req, timeout=5)\n"
143
143
  "except: pass\n"
144
144
  )
@@ -156,7 +156,7 @@ def _maybe_sync_policy() -> None:
156
156
  try:
157
157
  cfg = load_config()
158
158
  workspace_id = cfg.get("workspace_id") or cfg.get("workspace")
159
- api_key = cfg.get("agent_token") or cfg.get("api_key", "")
159
+ agent_token = cfg.get("agent_token", "")
160
160
  api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
161
161
  if not workspace_id:
162
162
  return
@@ -175,7 +175,7 @@ def _maybe_sync_policy() -> None:
175
175
  if time.time() - cache.get("ts", 0) < VERSION_CACHE_TTL:
176
176
  return
177
177
  url = f"{api_url}/guard/policies/sync?workspace_id={workspace_id}"
178
- req = urllib.request.Request(url, headers={"Authorization": f"Bearer {api_key}"} if api_key else {})
178
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {agent_token}"} if agent_token else {})
179
179
  with urllib.request.urlopen(req, timeout=2) as resp:
180
180
  remote = json.loads(resp.read())
181
181
 
@@ -33,7 +33,7 @@ def _load_config() -> dict:
33
33
  def run(session_id: str = "") -> None:
34
34
  cfg = _load_config()
35
35
  server = (cfg.get("api_url") or cfg.get("server") or "https://api.conductai.ai").rstrip("/")
36
- api_key = cfg.get("api_key", "")
36
+ agent_token = cfg.get("agent_token", "")
37
37
  member_token = cfg.get("member_token", "")
38
38
  workspace_id = cfg.get("workspace_id", "")
39
39
 
@@ -115,8 +115,8 @@ def run(session_id: str = "") -> None:
115
115
  }
116
116
  if member_token:
117
117
  headers["Authorization"] = f"Bearer {member_token}"
118
- elif api_key:
119
- headers["X-API-Key"] = api_key
118
+ elif agent_token:
119
+ headers["Authorization"] = f"Bearer {agent_token}"
120
120
 
121
121
  req = urllib.request.Request(
122
122
  f"{server}/guard/session-reports",
@@ -124,7 +124,7 @@ def _backfill_prev_session(current_session_id: str, cwd: str | None) -> None:
124
124
  return
125
125
  cfg = json.loads(cfg_path.read_text())
126
126
  server = cfg.get("server", "").rstrip("/")
127
- api_key = cfg.get("api_key", "")
127
+ agent_token = cfg.get("agent_token", "")
128
128
  workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
129
129
  if not server:
130
130
  return
@@ -145,7 +145,7 @@ def _backfill_prev_session(current_session_id: str, cwd: str | None) -> None:
145
145
  data=payload,
146
146
  headers={
147
147
  "Content-Type": "application/json",
148
- "X-API-Key": api_key,
148
+ "Authorization": f"Bearer {agent_token}",
149
149
  "X-Workspace-ID": workspace_id,
150
150
  },
151
151
  method="PATCH",
@@ -26,7 +26,7 @@ def _post_intent(session_id: str, ai_tool: str, cwd: str | None) -> None:
26
26
  return
27
27
  cfg = _json.loads(cfg_path.read_text())
28
28
  server = cfg.get("server", "").rstrip("/")
29
- api_key = cfg.get("api_key", "")
29
+ agent_token = cfg.get("agent_token", "")
30
30
  workspace_id = cfg.get("workspace_id") or cfg.get("workspace", "")
31
31
  if not server or not session_id:
32
32
  return
@@ -43,7 +43,7 @@ def _post_intent(session_id: str, ai_tool: str, cwd: str | None) -> None:
43
43
  data=payload,
44
44
  headers={
45
45
  "Content-Type": "application/json",
46
- "X-API-Key": api_key,
46
+ "Authorization": f"Bearer {agent_token}",
47
47
  "X-Workspace-ID": workspace_id,
48
48
  },
49
49
  method="PATCH",