conduct-cli 0.4.31__tar.gz → 0.4.33__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.31
3
+ Version: 0.4.33
4
4
  Summary: CLI for Conduct AI — 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.4.31"
7
+ version = "0.4.33"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -31,11 +31,44 @@ import time
31
31
  import urllib.request
32
32
  from pathlib import Path
33
33
 
34
- GUARD_DIR = Path.home() / ".conductguard"
35
- POLICY_PATH = GUARD_DIR / "policy.json"
36
- CONFIG_PATH = GUARD_DIR / "config.json"
37
- BUDGET_CACHE_PATH = GUARD_DIR / "budget_cache.json"
38
- BUDGET_CACHE_TTL = 300 # 5 minutes
34
+ GUARD_DIR = Path.home() / ".conductguard"
35
+ POLICY_PATH = GUARD_DIR / "policy.json"
36
+ CONFIG_PATH = GUARD_DIR / "config.json"
37
+ BUDGET_CACHE_PATH = GUARD_DIR / "budget_cache.json"
38
+ BUDGET_CACHE_TTL = 300 # 5 minutes
39
+ VERSION_CACHE_PATH = GUARD_DIR / "version_cache.json"
40
+ VERSION_CACHE_TTL = 60 # 1 minute — matches server poll window
41
+
42
+
43
+ def _maybe_sync_policy():
44
+ """Check server policy version once per minute; re-download if stale. Never raises."""
45
+ try:
46
+ cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
47
+ workspace_id = cfg.get("workspace_id")
48
+ api_key = cfg.get("api_key", "")
49
+ api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
50
+ if not workspace_id:
51
+ return
52
+ # Check cache TTL
53
+ if VERSION_CACHE_PATH.exists():
54
+ cache = json.loads(VERSION_CACHE_PATH.read_text())
55
+ if time.time() - cache.get("ts", 0) < VERSION_CACHE_TTL:
56
+ return
57
+ # Fetch current version from server
58
+ url = f"{api_url}/guard/policies/sync?workspace_id={workspace_id}"
59
+ req = urllib.request.Request(url, headers={"Authorization": f"Bearer {api_key}"} if api_key else {})
60
+ with urllib.request.urlopen(req, timeout=2) as resp:
61
+ remote = json.loads(resp.read())
62
+ remote_version = remote.get("version", "")
63
+ # Compare to local
64
+ local_version = ""
65
+ if POLICY_PATH.exists():
66
+ local_version = json.loads(POLICY_PATH.read_text()).get("version", "")
67
+ if remote_version != local_version:
68
+ POLICY_PATH.write_text(json.dumps(remote, indent=2))
69
+ VERSION_CACHE_PATH.write_text(json.dumps({"ts": time.time(), "version": remote_version}))
70
+ except Exception:
71
+ pass # Never block a tool call due to sync failure
39
72
 
40
73
 
41
74
  def _load_budget_cache():
@@ -366,6 +399,9 @@ def main():
366
399
  except Exception:
367
400
  sys.exit(0)
368
401
 
402
+ # Policy version check (cached 60s) — auto-syncs if server version differs
403
+ _maybe_sync_policy()
404
+
369
405
  # Hard budget cap (cached 5 min)
370
406
  hard_blocked, reason = _load_budget_cache()
371
407
  if hard_blocked is None:
@@ -934,15 +970,25 @@ def _report_tools_to_server() -> None:
934
970
  if not email:
935
971
  return
936
972
 
973
+ # Also pull conduct API key for X-Api-Key auth (member_token is not accepted by this endpoint)
974
+ conduct_cfg_path = Path.home() / ".conduct" / "config.json"
975
+ conduct_api_key = ""
976
+ if conduct_cfg_path.exists():
977
+ try:
978
+ conduct_api_key = json.loads(conduct_cfg_path.read_text()).get("api_key", "")
979
+ except Exception:
980
+ pass
981
+
937
982
  payload = json.dumps({"email": email, "tools": tools}).encode()
938
- auth = token or api_key
983
+ headers = {"Content-Type": "application/json"}
984
+ if conduct_api_key and conduct_api_key.startswith("cond_live_"):
985
+ headers["X-Api-Key"] = conduct_api_key
986
+ elif token:
987
+ headers["Authorization"] = f"Bearer {token}"
939
988
  req = urllib.request.Request(
940
989
  f"{base_url}/guard/developer-tools",
941
990
  data=payload,
942
- headers={
943
- "Content-Type": "application/json",
944
- "Authorization": f"Bearer {auth}",
945
- },
991
+ headers=headers,
946
992
  method="POST",
947
993
  )
948
994
  urllib.request.urlopen(req, timeout=8)
@@ -218,7 +218,7 @@ def _write_codex_mcp_config() -> bool:
218
218
  content = config_path.read_text() if config_path.exists() else ""
219
219
  if "conduct-mcp" in content:
220
220
  return True
221
- mcp_block = '\n[[mcp_servers]]\nname = "conduct"\ncommand = "conduct-mcp"\nargs = []\n'
221
+ mcp_block = '\n[mcp_servers.conduct]\ncommand = "conduct-mcp"\nargs = []\n'
222
222
  config_path.write_text(content + mcp_block)
223
223
  return True
224
224
  except Exception:
@@ -400,14 +400,15 @@ def _report_tool_coverage() -> None:
400
400
  return
401
401
 
402
402
  payload = json.dumps({"email": email, "tools": tools}).encode()
403
- auth = token or api_key
403
+ headers = {"Content-Type": "application/json"}
404
+ if api_key and api_key.startswith("cond_live_"):
405
+ headers["X-Api-Key"] = api_key
406
+ elif token:
407
+ headers["Authorization"] = f"Bearer {token}"
404
408
  req = urllib.request.Request(
405
409
  f"{server}/guard/developer-tools",
406
410
  data=payload,
407
- headers={
408
- "Content-Type": "application/json",
409
- "Authorization": f"Bearer {auth}",
410
- },
411
+ headers=headers,
411
412
  method="POST",
412
413
  )
413
414
  urllib.request.urlopen(req, timeout=8)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.31
3
+ Version: 0.4.33
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
File without changes
File without changes
File without changes